Связка Paper.js и Angular строится вокруг
правильного разделения ответственности: Angular управляет состоянием и
жизненным циклом приложения, а Paper.js отвечает за отрисовку и работу с
графикой на <canvas>.
Ключевые принципы:
npm install paper
Подключение в компоненте:
import * as paper from 'paper';
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 важно:
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:
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();
Это позволяет:
Экспорт в SVG:
const svg = paper.project.exportSVG({ asString: true });
Импорт:
paper.project.importSVG(svgString);
Экспорт в изображение:
const dataURL = this.canvasRef.nativeElement.toDataURL();
Основные рекомендации:
PathGroup для объединения элементов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;
Связывание параметров:
<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();
}
}
При работе с несколькими сценами:
const scope1 = new paper.PaperScope();
const scope2 = new paper.PaperScope();
scope1.setup(canvas1);
scope2.setup(canvas2);
Это позволяет изолировать проекты.
Canvas не отображается
paper.setupСобытия не работают
Утечки памяти
projectТакой подход позволяет использовать Paper.js как полноценный графический движок внутри Angular-приложения без потери производительности и управляемости кода.