Paper.js API Reference

This page documents Paper.js APIs available in PinePaper for building MCP tools and advanced integrations. Paper.js is installed globally, so all classes are available on the paper namespace or directly (e.g., Point, Path, Color).

When to Use Paper.js vs PinePaper API

Use Case Recommended API
Create standard shapes (text, circle, star, etc.) app.create()
Create custom paths with points app.create('path', { segments: [...] })
Complex path operations (boolean, offsets) Paper.js directly
Fine-grained path manipulation Paper.js directly
Advanced effects and compound paths Paper.js directly
After Paper.js creation, register for selection app.itemRegistry.register()

Core Classes

Point

Represents a 2D point or vector.

// Creation
const p1 = new Point(100, 200);
const p2 = new Point({ x: 100, y: 200 });
const p3 = new Point([100, 200]);

// Operations
const sum = p1.add(p2);              // Add points
const diff = p1.subtract(p2);        // Subtract points
const scaled = p1.multiply(2);       // Scale
const divided = p1.divide(2);        // Divide
const normalized = p1.normalize();   // Unit vector
const negated = p1.negate();         // Negate

// Properties
const length = p1.length;            // Distance from origin
const angle = p1.angle;              // Angle in degrees
const quadrant = p1.quadrant;        // 1-4

// Measurements
const distance = p1.getDistance(p2);
const angleTo = p1.getAngle(p2);     // Angle between points

// Utilities
const rotated = p1.rotate(45);       // Rotate around origin
const rotatedAround = p1.rotate(45, center);  // Rotate around point
const rounded = p1.round();          // Round coordinates
const isClose = p1.isClose(p2, 5);   // Within tolerance

Size

Represents width and height dimensions.

// Creation
const size = new Size(200, 150);
const size2 = new Size({ width: 200, height: 150 });

// Operations
const scaled = size.multiply(2);
const added = size.add(new Size(50, 50));

// Properties
size.width;   // 200
size.height;  // 150

Rectangle

Represents a rectangular area.

// Creation methods
const rect1 = new Rectangle(0, 0, 200, 150);  // x, y, width, height
const rect2 = new Rectangle(point, size);
const rect3 = new Rectangle(topLeft, bottomRight);
const rect4 = new Rectangle({ point: [0, 0], size: [200, 150] });

// Properties
rect.x, rect.y;                    // Position
rect.width, rect.height;           // Dimensions
rect.point;                        // Top-left as Point
rect.size;                         // Size object
rect.center;                       // Center point

// Corner points
rect.topLeft, rect.topRight;
rect.bottomLeft, rect.bottomRight;
rect.leftCenter, rect.rightCenter;
rect.topCenter, rect.bottomCenter;

// Bounds
rect.left, rect.right;             // x bounds
rect.top, rect.bottom;             // y bounds

// Methods
rect.contains(point);              // Point inside?
rect.contains(otherRect);          // Rect inside?
rect.intersects(otherRect);        // Overlaps?
rect.intersect(otherRect);         // Get intersection
rect.unite(otherRect);             // Get union
rect.expand(10);                   // Expand by amount
rect.scale(2);                     // Scale from center

Color

Represents colors with multiple format support.

// Creation
const c1 = new Color('#3b82f6');           // Hex
const c2 = new Color(0.2, 0.5, 1);         // RGB (0-1)
const c3 = new Color(0.2, 0.5, 1, 0.8);    // RGBA
const c4 = new Color('rgb(59, 130, 246)'); // CSS rgb
const c5 = new Color('red');               // Named colors

// HSL/HSB
const hsl = new Color({ hue: 220, saturation: 0.9, lightness: 0.6 });
const hsb = new Color({ hue: 220, saturation: 0.9, brightness: 0.6 });

// Components (all 0-1 range)
color.red, color.green, color.blue;
color.hue;          // 0-360
color.saturation;   // 0-1
color.brightness;   // 0-1
color.lightness;    // 0-1
color.alpha;        // 0-1

// Operations
const lighter = color.add(0.1);       // Lighten
const darker = color.subtract(0.1);   // Darken
const blended = color1.blend(color2, 0.5);  // Mix colors

// Convert
color.toCSS();      // 'rgb(59, 130, 246)'
color.toCSS(true);  // 'rgba(59, 130, 246, 1)'

Path Classes

Path

The fundamental drawing primitive.

// Create empty path
const path = new Path();

// Add segments
path.add(new Point(100, 100));
path.add([150, 50]);            // Array shorthand
path.add({ x: 200, y: 100 });   // Object shorthand

// Insert at index
path.insert(1, new Point(125, 75));

// Remove segments
path.removeSegment(1);
path.removeSegments(1, 3);      // Remove range

// Close path
path.closed = true;

// Path operations
path.smooth();                  // Smooth all segments
path.smooth({ type: 'catmull-rom' });
path.simplify(2.5);            // Reduce points (tolerance)
path.flatten(4);               // Convert curves to lines
path.reverse();                // Reverse direction

// Get points along path
const point = path.getPointAt(50);      // Point at offset
const tangent = path.getTangentAt(50);  // Tangent vector
const normal = path.getNormalAt(50);    // Normal vector
const curvature = path.getCurvatureAt(50);

// Path properties
path.length;                   // Total length
path.area;                     // Enclosed area (closed paths)
path.clockwise;                // Winding direction
path.segments;                 // Array of Segment objects
path.curves;                   // Array of Curve objects
path.firstSegment;
path.lastSegment;

// Bounds
path.bounds;                   // Bounding rectangle
path.strokeBounds;             // Including stroke
path.handleBounds;             // Including handles

// Hit testing
const hit = path.hitTest(point);
const contains = path.contains(point);  // Point inside?
const intersections = path.getIntersections(otherPath);

Path Factory Methods

Create common shapes with a single call:

// Circle
const circle = new Path.Circle({
  center: [200, 200],
  radius: 50,
  fillColor: '#3b82f6'
});

// Rectangle
const rect = new Path.Rectangle({
  point: [100, 100],
  size: [200, 150],
  fillColor: '#22c55e'
});

// Rounded rectangle
const rounded = new Path.Rectangle({
  point: [100, 100],
  size: [200, 100],
  radius: 20,            // Corner radius
  fillColor: '#f59e0b'
});

// Ellipse
const ellipse = new Path.Ellipse({
  point: [100, 100],
  size: [200, 100],
  fillColor: '#8b5cf6'
});

// Line
const line = new Path.Line({
  from: [100, 100],
  to: [300, 200],
  strokeColor: '#ef4444',
  strokeWidth: 2
});

// Arc (three-point)
const arc = new Path.Arc({
  from: [100, 200],
  through: [200, 100],
  to: [300, 200],
  strokeColor: '#06b6d4'
});

// Regular polygon
const hexagon = new Path.RegularPolygon({
  center: [200, 200],
  sides: 6,
  radius: 50,
  fillColor: '#a855f7'
});

// Star
const star = new Path.Star({
  center: [200, 200],
  points: 5,
  radius1: 50,          // Outer radius
  radius2: 25,          // Inner radius
  fillColor: '#fbbf24'
});

CompoundPath

Multiple paths as a single item (for holes, complex shapes):

// Create compound path with hole
const outer = new Path.Circle({
  center: [200, 200],
  radius: 100
});

const inner = new Path.Circle({
  center: [200, 200],
  radius: 40
});

const donut = new CompoundPath({
  children: [outer, inner],
  fillColor: '#3b82f6',
  fillRule: 'evenodd'    // Creates hole
});

// Boolean operations create compound paths
const result = path1.subtract(path2);  // Returns CompoundPath

Boolean Operations

Combine paths using set operations:

// Union (combine shapes)
const union = path1.unite(path2);

// Intersection (overlapping area)
const intersection = path1.intersect(path2);

// Subtraction (cut out)
const subtracted = path1.subtract(path2);

// Exclusion (XOR - non-overlapping areas)
const excluded = path1.exclude(path2);

// Divide (split at intersections)
const divided = path1.divide(path2);

// Note: Original paths are not modified
// Results are new Path or CompoundPath objects

Styling Properties

Apply to any path or shape:

// Fill
item.fillColor = '#3b82f6';
item.fillColor = new Color(0.2, 0.5, 1, 0.8);  // With alpha

// Stroke
item.strokeColor = '#1e40af';
item.strokeWidth = 3;
item.strokeCap = 'round';     // 'round', 'square', 'butt'
item.strokeJoin = 'round';    // 'round', 'miter', 'bevel'
item.miterLimit = 10;         // For miter joins
item.dashArray = [10, 5];     // Dash pattern
item.dashOffset = 0;          // Dash phase

// Opacity & Blend
item.opacity = 0.8;
item.blendMode = 'multiply';  // CSS blend modes

// Shadow
item.shadowColor = new Color(0, 0, 0, 0.3);
item.shadowBlur = 10;
item.shadowOffset = new Point(5, 5);

// Selection (for editing)
item.selected = true;
item.fullySelected = true;    // Show all handles

Transformations

Modify position, rotation, and scale:

// Position
item.position = new Point(400, 300);
item.position.x += 50;
item.position.y -= 20;

// Bounds-based positioning
item.bounds.center = new Point(400, 300);
item.bounds.topLeft = new Point(0, 0);

// Rotation (degrees)
item.rotation = 45;           // Set absolute rotation
item.rotate(30);              // Add rotation
item.rotate(30, pivot);       // Rotate around point

// Scale
item.scale(2);                // Uniform scale
item.scale(2, 0.5);           // Non-uniform (x, y)
item.scale(2, pivot);         // Scale around point

// Skew/Shear
item.shear(0.5, 0);           // Horizontal shear
item.shear(0, 0.5);           // Vertical shear

// Matrix transform
item.transform(matrix);
item.matrix;                  // Get transformation matrix
item.globalMatrix;            // Including parent transforms

// Reset
item.rotation = 0;
item.scaling = new Point(1, 1);

Text

Create and style text:

const text = new PointText({
  point: [400, 300],
  content: 'Hello World',
  fontSize: 48,
  fontFamily: 'Arial, sans-serif',
  fontWeight: 'bold',
  fillColor: '#1f2937',
  justification: 'center'     // 'left', 'center', 'right'
});

// Modify text
text.content = 'Updated Text';
text.fontSize = 64;

// Text bounds
text.bounds;                  // Bounding rectangle

// Character positioning (read-only)
text.characterStyle;          // Default style for new chars

Rasters (Images)

Load and manipulate images:

// Create raster from URL (basic Paper.js way)
const raster = new paper.Raster('https://example.com/image.png');
raster.position = view.center;
raster.onLoad = () => {
  console.log('Image loaded:', raster.size);
  raster.scale(0.5);
};

// Create raster from data URL
const dataURL = 'data:image/png;base64,...';
const raster2 = new paper.Raster(dataURL);

// Raster properties
raster.size;                    // Size object
raster.width, raster.height;    // Dimensions
raster.resolution;              // DPI

// Raster operations
raster.scale(0.5);              // Scale
raster.rotate(45);              // Rotate
raster.smoothing = true;        // Anti-aliasing (default: true)

// Get pixel data
const color = raster.getPixel(x, y);  // Color at point
raster.setPixel(x, y, color);         // Set pixel color

// Get average color
const avgColor = raster.getAverageColor();
const regionColor = raster.getAverageColor(path);  // Within path

For MCP tools, use PinePaper’s ImageTools for better integration:

// Upload image to library (stores for reuse)
const entry = await app.imageTools.uploadFromURL('https://example.com/logo.png');

// Place image on canvas (auto-registers with ItemRegistry)
const raster = await app.imageTools.placeImage(entry.id, {
  position: [400, 300],
  maxWidth: 200
});

// Now the raster is fully integrated:
// - Selectable with click
// - Has registry ID: raster.data.registryId
// - Included in undo/redo
// - Exports with scene

// Apply shape mask
app.imageTools.applyMask(raster, 'circle');

// Apply effects
app.applyEffect(raster, 'sparkle', { color: '#fbbf24' });

// Interactive cropping
app.imageTools.startCrop(raster, { aspectRatio: '1:1' });
app.imageTools.confirmCrop();

Manual Raster Registration

If creating rasters directly with Paper.js, use app.registerItem():

// Create raster
const raster = new paper.Raster('https://example.com/photo.jpg');

await new Promise(resolve => {
  raster.onLoad = resolve;
});

// Register with PinePaper (handles layer, data, registry automatically)
const itemId = app.registerItem(raster, 'image', { source: 'external' });

// Now it's interactive
app.historyManager.saveState();

SVG Import

Import and work with SVG graphics:

// Import SVG string (basic Paper.js way)
const svg = paper.project.importSVG(`
  <svg viewBox="0 0 100 100">
    <circle cx="50" cy="50" r="40" fill="#3b82f6"/>
  </svg>
`, { expandShapes: true });

svg.position = view.center;
svg.scale(2);

// SVG import options
const svg2 = paper.project.importSVG(svgString, {
  expandShapes: true,    // Convert shapes to paths
  insert: false,         // Don't auto-add to project
  applyMatrix: true      // Apply transforms to paths
});
// Import with automatic registration
const svg = app.importSVG(`
  <svg viewBox="0 0 100 100">
    <rect x="10" y="10" width="80" height="80" fill="#22c55e"/>
    <circle cx="50" cy="50" r="30" fill="#ffffff"/>
  </svg>
`);

// SVG is now:
// - Positioned at center
// - Registered: svg.data.registryId
// - Selected and ready to edit
// - Interactive (hover, drag, etc.)

// Apply animations to imported SVG
app.animate(svg, { animationType: 'pulse', animationSpeed: 0.5 });

// Access SVG children
svg.children.forEach(child => {
  console.log(child.className, child.fillColor);
});

Groups

Organize items hierarchically:

// Create group
const group = new Group();

// Add children
group.addChild(circle);
group.addChildren([rect, text]);

// Create with children
const group2 = new Group([circle, rect, text]);

// Access children
group.children;               // Array of children
group.firstChild;
group.lastChild;
group.children[0];

// Transform group (affects all children)
group.position = new Point(400, 300);
group.rotate(45);
group.scale(0.5);

// Clipping (mask children to shape)
const clipGroup = new Group({
  children: [maskPath, content],
  clipped: true               // First child becomes mask
});

Layers

Manage drawing order and organization:

// Get layers
const activeLayer = paper.project.activeLayer;
const layers = paper.project.layers;

// Create new layer
const newLayer = new Layer({ name: 'effects' });

// Layer operations
layer.activate();             // Make active for new items
layer.bringToFront();
layer.sendToBack();
layer.insertAbove(otherLayer);
layer.insertBelow(otherLayer);

// Layer visibility
layer.visible = false;
layer.opacity = 0.5;

// PinePaper layers (already created)
// app.backgroundLayer - Background patterns
// app.textLayer - Interactive items
// app.toolLayer - UI overlays

View

Access canvas viewport properties:

const view = paper.view;

// Dimensions
view.size;                    // Size object
view.size.width;
view.size.height;
view.bounds;                  // View rectangle
view.center;                  // Center point

// Zoom and pan
view.zoom = 1.5;              // Zoom level
view.center = new Point(500, 300);  // Pan to center

// Convert coordinates
view.viewToProject(viewPoint);   // Screen to canvas
view.projectToView(canvasPoint); // Canvas to screen

// Force redraw
view.draw();
view.update();

// Animation (use app.addOnFrameCallback for managed callbacks)
view.onFrame = (event) => {
  // event.delta - time since last frame (seconds)
  // event.time - total elapsed time
  // event.count - frame count
};

Project

Access the Paper.js project via app.project or paper.project:

// PinePaper exposes the project directly
const project = app.project;

// Or access via paper namespace
const project = paper.project;

// All items
project.activeLayer;
project.layers;

// Selection
project.selectedItems;        // Currently selected items
project.deselectAll();

// Hit testing
const result = project.hitTest(point, {
  fill: true,
  stroke: true,
  segments: true,
  tolerance: 5
});

if (result) {
  result.item;                // Hit item
  result.type;                // 'fill', 'stroke', 'segment', etc.
  result.point;               // Exact hit point
  result.segment;             // If segment hit
}

// Export/Import
const json = project.exportJSON();
const svg = project.exportSVG({ asString: true });
project.importJSON(json);
project.importSVG(svgString);

// Clear
project.clear();
project.activeLayer.removeChildren();

Registering Paper.js Items with PinePaper

After creating items with Paper.js directly, always register them for full PinePaper integration. This enables relations, selection, export, and history.

// Create complex path with Paper.js
const complexShape = new Path();
complexShape.add([100, 100]);
complexShape.arcTo([200, 50], [300, 100]);
complexShape.lineTo([300, 200]);
complexShape.closePath();
complexShape.fillColor = '#3b82f6';

// Register with PinePaper using registerItem()
// This handles: parent layer, item data, cursor, registry
const itemId = app.registerItem(complexShape, 'path', {
  customProperty: 'value'
});

// Now the item:
// - Can be selected with click
// - Appears in item registry queries
// - Is included in undo/redo
// - Exports with the scene
// - Can use RELATIONS for animation (see below)

// Save state for undo
app.historyManager.saveState();

registerItem(item, type, properties) - Convenience method that:

  • Adds item to the correct layer
  • Initializes item data and cursor behavior
  • Registers with ItemRegistry
  • Returns the registry ID

Animating Paper.js Items with Relations

After registering a Paper.js item, use relations for animation (not direct keyframes). Relations:

  • Describe behavior in natural language
  • Generate training data for LLM fine-tuning
  • Compose together for complex animations
// 1. Create and register items
const planet = new Path.Circle({ center: [400, 300], radius: 30, fillColor: '#3b82f6' });
const sun = new Path.Circle({ center: [400, 300], radius: 60, fillColor: '#fbbf24' });

const planetId = app.registerItem(planet, 'planet');
const sunId = app.registerItem(sun, 'sun');

// 2. Use RELATIONS for animation (RECOMMENDED)
app.addRelation(planetId, sunId, 'orbits', { radius: 150, speed: 0.2 });

// 3. For keyframe animations, use the 'animates' relation
app.addRelation(planetId, planetId, 'animates', {
  keyframes: [
    { time: 0, properties: { scale: 1 }, easing: 'easeInOut' },
    { time: 1, properties: { scale: 1.2 }, easing: 'easeInOut' },
    { time: 2, properties: { scale: 1 }, easing: 'easeInOut' }
  ],
  duration: 2,
  loop: true
});

// This is BETTER than direct keyframes because:
// - Training data: "planet animates with keyframes over 2 seconds"
// - Natural language description for LLM training
// - Composable with other relations

Why Relations over Keyframes? Relations generate natural language training data. When you use app.exportRelationTrainingData(), each relation produces instruction/code pairs for fine-tuning. Direct keyframes don’t generate training data.

Complete Example: MCP Tool Pattern

Important: In PinePaper, the API is exposed via window.PinePaper (or window.app). Paper.js classes like Path, Point, Group, PointText are available globally because Paper.js is installed with paper.install(window).

// Example: Create a custom badge shape via MCP
// Access PinePaper instance (both are equivalent):
const app = window.PinePaper;  // or window.app

function createBadge(params) {
  const {
    x = 400,
    y = 300,
    size = 100,
    text = 'BADGE',
    primaryColor = '#3b82f6',
    secondaryColor = '#1e40af'
  } = params;

  // Create badge shape using Paper.js (classes are global)
  const outer = new Path.Circle({
    center: [x, y],
    radius: size
  });

  const inner = new Path.Circle({
    center: [x, y],
    radius: size * 0.85
  });

  // Create ring using boolean subtraction
  const ring = outer.subtract(inner);
  ring.fillColor = secondaryColor;

  // Clean up source shapes (important!)
  outer.remove();
  inner.remove();

  // Create center circle
  const center = new Path.Circle({
    center: [x, y],
    radius: size * 0.8,
    fillColor: primaryColor
  });

  // Create text
  const label = new PointText({
    point: [x, y + size * 0.1],
    content: text,
    fontSize: size * 0.25,
    fontFamily: 'Arial Black, sans-serif',
    fillColor: '#ffffff',
    justification: 'center'
  });

  // Group all parts
  const badge = new Group({
    children: [ring, center, label]
  });

  // Register with PinePaper (handles layer, data, registry)
  const itemId = app.registerItem(badge, 'badge', {
    text: text,
    size: size
  });

  // Save state for undo
  app.historyManager.saveState();

  return { badge, itemId };
}

// Usage in MCP tool
const { badge, itemId } = createBadge({
  x: 500,
  y: 400,
  size: 80,
  text: 'NEW',
  primaryColor: '#ef4444',
  secondaryColor: '#b91c1c'
});

// Add animation using RELATIONS (not direct keyframes)
// This generates training data for LLM fine-tuning
app.addRelation(itemId, itemId, 'animates', {
  keyframes: [
    { time: 0, properties: { scale: 1 }, easing: 'easeInOut' },
    { time: 0.5, properties: { scale: 1.1 }, easing: 'easeInOut' },
    { time: 1, properties: { scale: 1 }, easing: 'easeInOut' }
  ],
  duration: 1,
  loop: true
});

// Or make it orbit another item (if you have another item)
// const targetId = someOtherItem.data.registryId;
// app.addRelation(itemId, targetId, 'orbits', { radius: 100, speed: 0.3 });

Performance Tips

  1. Batch operations - Wrap multiple changes in paper.project.activeLayer.removeChildren() + recreate
  2. Use Groups - Transform groups instead of individual items
  3. Simplify paths - Use path.simplify() for complex paths
  4. Visibility over removal - Use item.visible = false for temporary hiding
  5. Avoid onFrame creation - Create items once, update in onFrame
  6. Use compounds - CompoundPath is faster than many separate paths

See Also