@typogram/paper.js

Paper.js, rendered as SVG

Stock Paper.js redraws the whole scene into a canvas on every frame, so panning a document with tens of thousands of items costs a full scene traversal per frame. This fork adds a second renderer that mirrors the scene graph into a native SVG DOM tree and keeps one node per item alive. Moving the camera then costs a single transform attribute — no matter how many items there are.

npm i github:Typogram/paper.js#dist-only Source on GitHub →

Upstream Paper.js 0.12.18 plus the renderer. Same API, same items, same hit-testing, same events — a drop-in replacement for the paper package, and it installs under that name, so nothing that imports paper has to change.

See it

The same scene and the same auto-pan through both renderers, one at a time so each is measured on its own frames. Drag to pan, scroll to zoom, or switch Drag to Select and click an item — hit-testing and selection handles work the same either way.

10,000
Drag
SvgView svg
drag to pan · scroll to zoom
Per frame
Hit test Switch Drag to Select, then click an item.
FPS
Frame
In update()
Build
Viol. 0
Frame violations >50ms between frames — the threshold Chrome's own "'requestAnimationFrame' handler took Nms" warning uses. Only one renderer runs at a time; the log keeps earlier runs, each entry named by the renderer that produced it.
None yet — try raising the item count, or dragging the scene around fast.

Use it

Nothing to rewrite

import paper from 'paper';

paper.setup(canvas);
new paper.Path.Circle({
  center: [80, 50],
  radius: 35,
  fillColor: 'red'
});

Items, styles, tools, events, hitTest(), import and export are all upstream Paper.js. The renderer is the only thing that changed.

Opting out

// per scope, before setup()
paper.settings.renderer = 'canvas';

// or per view
<canvas data-paper-renderer="canvas">

SVG is the default here. A view that needs the canvas back — to read its pixels, say — opts out on its own, and everything else keeps rendering as SVG.

How it works

The scene becomes DOM once

One node per item, kept alive and updated in place. The view learns what changed through Paper.js's own change tracking, so an update touches the items that actually changed rather than walking the scene.

Panning is one attribute

The view matrix lives on the root <g>. A pan or zoom writes transform="matrix(…)" and nothing else — no item is visited, no path rebuilt. The browser keeps the rasterized geometry and skips what is off screen.

What it costs instead

Setup time and memory: 100,000 live nodes is a real burden, and changing every item in one frame is more expensive here than a redraw. It trades per-frame work for per-change work, which is the trade an editor wants.

Know this before you switch

  • The element is swapped. Handed a <canvas>, the view replaces it with an <svg> and puts the original back on remove(). Code holding its own reference to that canvas — a framework ref, listeners bound to it, or canvas.toDataURL() for a thumbnail — should render an <svg> element itself, or opt that view out. To read pixels, go through Item#rasterize(), which makes its own canvas either way.
  • Five blend modes render as normal. add maps to CSS plus-lighter and the other 15 map to the CSS mode of the same name, but subtract, average, pin-light and negation have no CSS equivalent — Paper.js emulates those in JavaScript for the canvas renderer, which a DOM tree cannot do.
  • Text measurement still uses a canvas. SVG cannot measure text without laying it out, so an offscreen 1×1 context is kept for PointText bounds. It is the only canvas left in the rendering path.
@typogram/paper.js · MIT Frame times on this page are measured between animation frames, so the browser's own layout and paint are counted too, not just Paper.js. 60fps is the display's ceiling, not the renderer's.