Paper.js и Vue

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 для изоляции контекста
  • Canvas получаем через ref
  • Обязательно очищение через remove()

Работа с жизненным циклом Vue

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

Paper.js не является реактивной системой. Поэтому:

  • изменения Vue не обновляют Canvas автоматически
  • требуется вручную синхронизировать состояние

Антипаттерн:

const circle = reactive(new Path.Circle(...))

Правильный подход:

  • хранить только данные в Vue
  • создавать/обновлять объекты Paper.js вручную

Управление состоянием сцены

Разделение ответственности:

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()
}

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

Вынос логики 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)
  })
}

Оптимизация производительности

Проблемы:

  • частые перерисовки
  • большое количество объектов

Решения:

  1. Минимизировать вызовы view.update()
  2. Использовать requestAnimationFrame через Paper.js
  3. Группировать объекты:
const group = new scope.Group()

shapes.forEach(shape => {
  group.addChild(new scope.Path.Circle(...))
})
  1. Использовать Layer:
const backgroundLayer = new scope.Layer()
const uiLayer = new scope.Layer()

Работа с размерами Canvas

Адаптация к размеру контейнера:

function resizeCanvas() {
  const rect = canvas.value.getBoundingClientRect()
  scope.view.viewSize = new scope.Size(rect.width, rect.height)
}

window.addEventListener('resize', resizeCanvas)

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

Компонент может принимать параметры:

<script setup>
const props = defineProps({
  color: String,
  radius: Number
})
</script>

Применение:

watch(() => props.color, (newColor) => {
  circle.fillColor = newColor
  scope.view.update()
})

Работа с несколькими Canvas

Каждый canvas должен иметь свой PaperScope:

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

Нельзя использовать глобальный paper, если несколько сцен.


Интеграция с Vue Router

При смене страниц важно уничтожать контекст:

onBeforeUnmount(() => {
  scope.remove()
})

Иначе возможны утечки памяти и конфликты событий.


SSR и ограничения

Paper.js не работает на сервере, так как требует DOM и Canvas.

Решение:

  • использовать только в onMounted
  • проверка:
if (typeof window !== 'undefined') {
  // инициализация
}

Типичные ошибки

1. Повторная инициализация Paper.js

  • приводит к конфликтам контекста

2. Отсутствие очистки

  • утечки памяти

3. Попытка использовать реактивность Vue напрямую

  • Paper.js не отслеживает изменения

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)
}

Импорт SVG

scope.project.importSVG(svgString, function(item) {
  item.scale(2)
})

Архитектурный подход

Оптимальная структура:

  • Vue:

    • хранит состояние
    • управляет UI
  • Paper.js слой:

    • отвечает за отрисовку
    • не содержит бизнес-логики

Связь через:

  • composables
  • watchers
  • события

Масштабируемость

При росте проекта:

  • выделять отдельные модули рендеринга
  • использовать паттерн “scene manager”
  • избегать логики внутри обработчиков 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 можно комбинировать с:

  • Vuex / Pinia — для хранения состояния
  • D3 — для вычислений
  • Three.js — для гибридных сцен (2D + 3D)

Практическое применение

Типичные кейсы:

  • графические редакторы
  • интерактивные карты
  • визуализация данных
  • генеративная графика
  • образовательные инструменты

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

Разделение на подсистемы:

  • input (события)
  • state (Vue)
  • render (Paper.js)

Позволяет избежать хаотичной логики и облегчает поддержку.