Paper.js и Angular

Связка Paper.js и Angular строится вокруг правильного разделения ответственности: Angular управляет состоянием и жизненным циклом приложения, а Paper.js отвечает за отрисовку и работу с графикой на <canvas>.

Ключевые принципы:

  • Canvas изолируется в отдельном компоненте
  • Paper.js инициализируется после рендера DOM
  • Логика рисования выносится в сервисы или отдельные классы
  • Избегается прямое манипулирование DOM вне Angular lifecycle

Установка и базовая настройка

npm install paper

Подключение в компоненте:

import * as paper from 'paper';

Создание Canvas-компонента

import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import * as paper from 'paper';

@Component({
  selector: 'app-canvas',
  template: `<canvas #canvas width="800" height="600"></canvas>`
})
export class CanvasComponent implements AfterViewInit {

  @ViewChild('canvas', { static: true })
  canvasRef!: ElementRef<HTMLCanvasElement>;

  ngAfterViewInit(): void {
    paper.setup(this.canvasRef.nativeElement);
    this.draw();
  }

  draw(): void {
    const circle = new paper.Path.Circle({
      center: [100, 100],
      radius: 50,
      fillColor: 'red'
    });

    paper.view.draw();
  }
}

Ключевой момент: инициализация происходит в ngAfterViewInit, так как canvas должен существовать в DOM.


Управление жизненным циклом

Paper.js создаёт собственный контекст (paper.project, paper.view), который необходимо очищать при уничтожении компонента:

import { OnDestroy } from '@angular/core';

ngOnDestroy(): void {
  paper.project.clear();
}

При сложных сценах:

paper.project.remove();

Инкапсуляция логики в сервисах

Для масштабируемости логика рисования выносится в сервис:

import { Injectable } from '@angular/core';
import * as paper from 'paper';

@Injectable({ providedIn: 'root' })
export class CanvasService {

  init(canvas: HTMLCanvasElement): void {
    paper.setup(canvas);
  }

  drawCircle(): void {
    new paper.Path.Circle({
      center: paper.view.center,
      radius: 40,
      fillColor: 'blue'
    });
  }

  clear(): void {
    paper.project.clear();
  }
}

Использование в компоненте:

constructor(private canvasService: CanvasService) {}

ngAfterViewInit(): void {
  this.canvasService.init(this.canvasRef.nativeElement);
  this.canvasService.drawCircle();
}

Работа с событиями

Paper.js предоставляет собственную систему событий через Tool:

const tool = new paper.Tool();

tool.onMouseD own = (event: paper.ToolEvent) => {
  new paper.Path.Circle({
    center: event.point,
    radius: 20,
    fillColor: 'green'
  });
};

В Angular важно:

  • не дублировать обработчики
  • уничтожать tool при пересоздании сцены

Связь с Angular State

Paper.js не реактивен, поэтому взаимодействие со state реализуется вручную.

Пример через RxJS:

import { BehaviorSubject } from 'rxjs';

private color$ = new BehaviorSubject<string>('red');

setColor(color: string) {
  this.color$.next(color);
}

Подписка в сервисе:

this.color$.subscribe(color => {
  this.currentColor = color;
});

Использование при рисовании:

new paper.Path.Circle({
  center: event.point,
  radius: 20,
  fillColor: this.currentColor
});

Анимации

Paper.js использует view.onFrame:

paper.view.onFr ame = (event) => {
  circle.position.x += 1;

  if (circle.position.x > paper.view.size.width) {
    circle.position.x = 0;
  }
};

В Angular:

  • анимация не должна вызывать change detection
  • желательно запускать вне Angular зоны
import { NgZone } from '@angular/core';

constructor(private zone: NgZone) {}

this.zone.runOutsideAngular(() => {
  paper.view.onFr ame = () => {
    // анимация
  };
});

Масштабирование и адаптивность

Обработка изменения размера:

window.addEventListener('resize', () => {
  paper.view.viewSize = new paper.Size(
    window.innerWidth,
    window.innerHeight
  );
});

В Angular лучше использовать HostListener:

@HostListener('window:resize')
onResize() {
  paper.view.viewSize = new paper.Size(
    window.innerWidth,
    window.innerHeight
  );
}

Работа с слоями

Paper.js поддерживает слои (Layer):

const backgroundLayer = new paper.Layer();
const drawingLayer = new paper.Layer();

backgroundLayer.activate();

new paper.Path.Rectangle({
  point: [0, 0],
  size: paper.view.size,
  fillColor: 'lightgray'
});

drawingLayer.activate();

Это позволяет:

  • разделять UI и графику
  • управлять видимостью
  • оптимизировать перерисовку

Импорт и экспорт графики

Экспорт в SVG:

const svg = paper.project.exportSVG({ asString: true });

Импорт:

paper.project.importSVG(svgString);

Экспорт в изображение:

const dataURL = this.canvasRef.nativeElement.toDataURL();

Производительность

Основные рекомендации:

  • минимизировать количество объектов Path
  • использовать Group для объединения элементов
  • избегать частых project.clear()
  • отключать автоматическую перерисовку при массовых изменениях:
paper.view.autoUpdate = false;

// изменения

paper.view.update();

Работа с группами и трансформациями

const group = new paper.Group([
  new paper.Path.Circle({ center: [100, 100], radius: 30 }),
  new paper.Path.Rectangle({ point: [80, 80], size: [40, 40] })
]);

group.rotate(45);
group.scale(1.5);
group.position = paper.view.center;

Интеграция с Angular Forms

Связывание параметров:

<input type="range" (input)="setRadius($event)">
setRadius(event: any) {
  this.radius = event.target.value;
  this.updateShape();
}
updateShape() {
  this.shape.remove();

  this.shape = new paper.Path.Circle({
    center: paper.view.center,
    radius: this.radius
  });
}

Организация структуры проекта

Рекомендуемая структура:

/canvas
  canvas.component.ts
  canvas.service.ts
  tools/
    draw.tool.ts
    erase.tool.ts
  models/
    shape.model.ts

Расширение через кастомные инструменты

Создание собственного инструмента:

export class DrawTool {
  private tool: paper.Tool;

  constructor() {
    this.tool = new paper.Tool();

    this.tool.onMouseD rag = (event) => {
      new paper.Path.Circle({
        center: event.point,
        radius: 5,
        fillColor: 'black'
      });
    };
  }

  activate() {
    this.tool.activate();
  }
}

Синхронизация нескольких canvas

При работе с несколькими сценами:

const scope1 = new paper.PaperScope();
const scope2 = new paper.PaperScope();

scope1.setup(canvas1);
scope2.setup(canvas2);

Это позволяет изолировать проекты.


Ошибки и типичные проблемы

Canvas не отображается

  • не вызван paper.setup
  • canvas не в DOM

События не работают

  • Tool не активирован
  • конфликт с Angular событиями

Утечки памяти

  • не очищается project
  • остаются подписки RxJS

Вывод ключевых практик

  • Инициализация строго в lifecycle Angular
  • Логика рисования вне компонента
  • Контроль состояния через RxJS
  • Оптимизация через слои и группы
  • Анимации вне Angular зоны

Такой подход позволяет использовать Paper.js как полноценный графический движок внутри Angular-приложения без потери производительности и управляемости кода.