Paper.js — векторная графическая библиотека для работы с HTML5 Canvas, ориентированная на создание интерактивной графики, анимаций и сложных визуальных редакторов. В сочетании с Vue возникает задача корректного разделения реактивной логики интерфейса и императивной модели рендеринга Canvas.
Ключевая особенность: Paper.js управляет своим собственным состоянием сцены, тогда как Vue управляет DOM и реактивными данными. Это требует явного контроля над жизненным циклом и синхронизацией данных.
Минимальная структура компонента Vue для работы с Paper.js:
<template>
<canvas ref="canvas" resize></canvas>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import paper from 'paper'
const canvas = ref(null)
let scope = null
onMounted(() => {
scope = new paper.PaperScope()
scope.setup(canvas.value)
const path = new scope.Path.Circle({
center: [100, 100],
radius: 50,
fillColor: 'red'
})
scope.view.draw()
})
onBeforeUnmount(() => {
scope.remove()
})
</script>
Ключевые моменты:
PaperScope для изоляции контекстаrefremove()Paper.js требует точной инициализации и очистки:
| Хук Vue | Назначение |
|---|---|
onMounted |
Инициализация Paper.js |
onBeforeUnmount |
Удаление scope |
watch |
Реакция на изменения данных |
Пример реакции на изменение состояния:
import { watch } from 'vue'
watch(color, (newColor) => {
if (scope) {
scope.project.activeLayer.children.forEach(item => {
item.fillColor = newColor
})
scope.view.update()
}
})
Paper.js не является реактивной системой. Поэтому:
Антипаттерн:
const circle = reactive(new Path.Circle(...))
Правильный подход:
Разделение ответственности:
| Vue | Paper.js |
|---|---|
| данные | визуализация |
| логика UI | геометрия |
| реактивность | рендер |
Пример:
const shapes = ref([
{ x: 100, y: 100, radius: 50 }
])
Рендер:
function renderShapes() {
scope.project.clear()
shapes.value.forEach(shape => {
new scope.Path.Circle({
center: [shape.x, shape.y],
radius: shape.radius,
fillColor: 'blue'
})
})
scope.view.update()
}
Вынос логики Paper.js в composable повышает переиспользуемость:
import paper from 'paper'
export function usePaper(canvasRef) {
let scope = new paper.PaperScope()
function init() {
scope.setup(canvasRef.value)
}
function clear() {
scope.project.clear()
}
function destroy() {
scope.remove()
}
return { scope, init, clear, destroy }
}
Paper.js имеет собственную систему событий:
const tool = new scope.Tool()
tool.onMouseD own = function(event) {
new scope.Path.Circle({
center: event.point,
radius: 20,
fillColor: 'green'
})
}
Интеграция с Vue:
const lastClick = ref(null)
tool.onMouseD own = function(event) {
lastClick.value = { x: event.point.x, y: event.point.y }
}
Paper.js использует view.onFrame:
scope.view.onFr ame = function(event) {
scope.project.activeLayer.children.forEach(item => {
item.rotate(1)
})
}
Контроль из Vue:
const isAnimating = ref(true)
scope.view.onFr ame = function() {
if (!isAnimating.value) return
scope.project.activeLayer.children.forEach(item => {
item.rotate(1)
})
}
Проблемы:
Решения:
view.update()requestAnimationFrame через Paper.jsconst group = new scope.Group()
shapes.forEach(shape => {
group.addChild(new scope.Path.Circle(...))
})
Layer:const backgroundLayer = new scope.Layer()
const uiLayer = new scope.Layer()
Адаптация к размеру контейнера:
function resizeCanvas() {
const rect = canvas.value.getBoundingClientRect()
scope.view.viewSize = new scope.Size(rect.width, rect.height)
}
window.addEventListener('resize', resizeCanvas)
Компонент может принимать параметры:
<script setup>
const props = defineProps({
color: String,
radius: Number
})
</script>
Применение:
watch(() => props.color, (newColor) => {
circle.fillColor = newColor
scope.view.update()
})
Каждый canvas должен иметь свой PaperScope:
const scope1 = new paper.PaperScope()
const scope2 = new paper.PaperScope()
Нельзя использовать глобальный paper, если несколько
сцен.
При смене страниц важно уничтожать контекст:
onBeforeUnmount(() => {
scope.remove()
})
Иначе возможны утечки памяти и конфликты событий.
Paper.js не работает на сервере, так как требует DOM и Canvas.
Решение:
onMountedif (typeof window !== 'undefined') {
// инициализация
}
1. Повторная инициализация Paper.js
2. Отсутствие очистки
3. Попытка использовать реактивность Vue напрямую
4. Использование глобального scope
const layer1 = new scope.Layer()
const layer2 = new scope.Layer()
layer2.activate()
const cache = new Map()
function getCircle(id) {
if (!cache.has(id)) {
cache.set(id, new scope.Path.Circle(...))
}
return cache.get(id)
}
scope.project.importSVG(svgString, function(item) {
item.scale(2)
})
Оптимальная структура:
Vue:
Paper.js слой:
Связь через:
При росте проекта:
Пример менеджера:
class SceneManager {
constructor(scope) {
this.scope = scope
}
render(data) {
this.scope.project.clear()
// отрисовка
}
}
Полезные инструменты:
console.log(scope.project)
console.log(scope.view.size)
Визуальная проверка:
item.strokeColor = 'red'
Paper.js можно комбинировать с:
Типичные кейсы:
Разделение на подсистемы:
Позволяет избежать хаотичной логики и облегчает поддержку.