Built-in Relations

PinePaper includes several pre-defined relation types for common behaviors.

orbits

Circular motion around a target.

app.addRelation(moonId, earthId, 'orbits', {
  radius: 100,
  speed: 0.5,
  direction: 'counterclockwise',
  phase: 0
});
Parameter Type Default Description
radius number 100 Orbit radius
speed number 1 Rotation speed
direction string 'counterclockwise' 'clockwise' or 'counterclockwise'
phase number 0 Starting angle offset

follows

Move toward target with smoothing.

app.addRelation(labelId, playerId, 'follows', {
  offset: [0, -50],
  smoothing: 0.1,
  delay: 0
});
Parameter Type Default Description
offset array [0, 0] Position offset from target
smoothing number 0.1 Movement smoothing (0-1)
delay number 0 Follow delay in seconds

attached_to

Move with target (parent-child relationship).

app.addRelation(hatId, characterId, 'attached_to', {
  offset: [0, -30],
  inherit_rotation: true
});
Parameter Type Default Description
offset array [0, 0] Fixed offset from target
inherit_rotation boolean false Also rotate with target

maintains_distance

Stay at fixed distance from target.

app.addRelation(satelliteId, stationId, 'maintains_distance', {
  distance: 200,
  strength: 0.8
});
Parameter Type Default Description
distance number 100 Target distance
strength number 1 How strongly to maintain distance

points_at

Rotate to face target.

app.addRelation(arrowId, targetId, 'points_at', {
  offset_angle: -90,
  smoothing: 0.2
});
Parameter Type Default Description
offset_angle number 0 Angle offset in degrees
smoothing number 0 Rotation smoothing

mirrors

Mirror position across axis.

app.addRelation(reflectionId, originalId, 'mirrors', {
  axis: 'vertical',
  center: [400, 300]
});
Parameter Type Default Description
axis string 'vertical' 'vertical', 'horizontal', or 'both'
center array canvas center Mirror center point

parallax

Move relative to target by depth factor.

app.addRelation(backgroundId, cameraId, 'parallax', {
  depth: 0.5,
  origin: [400, 300]
});
Parameter Type Default Description
depth number 0.5 Depth factor (0-1)
origin array [0, 0] Parallax origin point

bounds_to

Stay within target’s bounds.

app.addRelation(playerId, arenaId, 'bounds_to', {
  padding: 20,
  bounce: true
});
Parameter Type Default Description
padding number 0 Inner padding
bounce boolean false Bounce off edges

Manim-Inspired Animation Relations

These relations are inspired by the Manim Community animation library, providing similar animation capabilities in PinePaper’s declarative relation system.

grows_from

Item scales from zero to full size, emanating from a point. Similar to Manim’s GrowFromPoint and GrowFromCenter.

app.addRelation(itemId, null, 'grows_from', {
  origin: 'center',
  duration: 0.5,
  delay: 0,
  easing: 'easeOut'
});
Parameter Type Default Description
origin string 'center' Origin point: 'center', 'top', 'bottom', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight'
duration number 0.5 Growth duration in seconds
delay number 0 Delay before growth starts
easing string 'easeOut' Easing function

staggered_with

Staggered animation timing for groups. Similar to Manim’s LaggedStart.

// Apply to each item in a group with increasing index
app.addRelation(item1Id, leaderId, 'staggered_with', { index: 0, stagger: 0.1, effect: 'popIn' });
app.addRelation(item2Id, leaderId, 'staggered_with', { index: 1, stagger: 0.1, effect: 'popIn' });
app.addRelation(item3Id, leaderId, 'staggered_with', { index: 2, stagger: 0.1, effect: 'popIn' });
Parameter Type Default Description
index number 0 Position in stagger sequence (0-based)
stagger number 0.1 Delay between each item in seconds
effect string 'fadeIn' Effect type: 'fadeIn', 'fadeOut', 'growIn', 'slideIn', 'popIn'

indicates

Temporary highlight effect with scale and color pulse. Similar to Manim’s Indicate.

app.addRelation(itemId, null, 'indicates', {
  scale: 1.2,
  color: '#fbbf24',
  duration: 0.5,
  repeat: 2
});
Parameter Type Default Description
scale number 1.2 Maximum scale during indication
color string '#fbbf24' Highlight color (null to skip color)
duration number 0.5 Duration of one indication cycle
delay number 0 Delay before indication starts
repeat number 1 Number of times to repeat

circumscribes

Draw a temporary shape around the target item. Similar to Manim’s Circumscribe.

app.addRelation(sourceId, targetId, 'circumscribes', {
  shape: 'rectangle',
  color: '#fbbf24',
  strokeWidth: 3,
  padding: 10,
  duration: 1.0,
  fadeOut: true
});
Parameter Type Default Description
shape string 'rectangle' Shape type: 'rectangle', 'circle', 'ellipse'
color string '#fbbf24' Stroke color
strokeWidth number 3 Stroke width in pixels
padding number 10 Padding around target item
duration number 1.0 Animation duration
fadeOut boolean true Fade out after drawing

wave_through

Send a wave distortion through the item. Similar to Manim’s ApplyWave.

app.addRelation(itemId, null, 'wave_through', {
  amplitude: 20,
  frequency: 2,
  direction: 'horizontal',
  duration: 1.0
});
Parameter Type Default Description
amplitude number 20 Wave amplitude in pixels
frequency number 2 Number of wave cycles
direction string 'horizontal' Wave direction: 'horizontal' or 'vertical'
duration number 1.0 Wave duration in seconds
delay number 0 Delay before wave starts

camera_follows

View pans to follow a target item. Similar to Manim’s MovingCameraScene.

app.addRelation(null, targetId, 'camera_follows', {
  smoothing: 0.1,
  offset: [0, 0],
  zoom: 1,
  deadzone: 50
});
Parameter Type Default Description
smoothing number 0.1 Follow smoothing (0 = instant, 1 = very slow)
offset array [0, 0] Offset from target center
zoom number 1 Zoom level (1 = normal, >1 = zoom in)
deadzone number 50 Pixels target can move before camera follows
bounds object null Camera bounds: {minX, maxX, minY, maxY}

camera_animates

Keyframe-based camera zoom and pan animation. Uses 'camera' as a virtual item ID.

Each keyframe’s target is given by a polymorphic focus field resolved per-frame against the live scene graph — so the camera chases items as they move, rig, or get nudged. The legacy center: [x, y] field is still honored when focus is absent or unresolvable, for backward compat.

// Cinematic walkthrough — focus on items instead of pixel coordinates.
app.addRelation('camera', null, 'camera_animates', {
  duration: 6,
  loop: true,
  keyframes: [
    { time: 0, zoom: 1,   focus: [400, 300] },                              // literal
    { time: 2, zoom: 2.2, focus: 'hero_shape',            easing: 'easeInOut' }, // item id
    { time: 4, zoom: 2,   focus: { item: 'intro_text', offset: [0, -40] }, easing: 'easeOut' }, // item + offset
    { time: 6, zoom: 1,   focus: [400, 300],              easing: 'easeInOut' }
  ]
});

// Or use the camera helper API
app.camera.zoomIn(2, 0.5);
app.camera.panTo(200, 200, 0.5);
app.camera.moveTo(200, 200, 2, 0.5);
app.camera.reset(0.5);
Parameter Type Default Description
keyframes array [] Array of {time, zoom, focus, pitch, yaw, easing} objects
duration number 2 Total animation duration in seconds
loop boolean false Loop the animation
delay number 0 Delay before animation starts
pitch number 0 3D tilt in degrees (global default; per-keyframe overrides)
yaw number 0 3D rotation in degrees (global default; per-keyframe overrides)
fov number 60 Perspective field of view in degrees

Keyframe Properties:

Property Type Description
time number Time in seconds
zoom number Zoom level (1=normal, 2=2x zoom in, 0.5=zoom out)
focus [x, y] | 'itemId' | { item, offset } What the camera frames this frame. Resolved live — see below.
center [x, y] Legacy alias for focus: [x, y]. Honored when focus is absent.
pitch number 3D tilt in degrees (0 = flat, positive = tilt forward)
yaw number 3D rotation in degrees
easing string Easing function for transition to this keyframe
pathOut [dx, dy] Tangent handle when leaving this keyframe (promotes segment to cubic bezier)
pathIn [dx, dy] Tangent handle when arriving at this keyframe
pathMode 'arc' | 'custom' Shorthand path types (see below)
pivot [x, y] Arc center when pathMode: 'arc'
arcDirection 'ccw' | 'cw' Arc sweep direction (default 'ccw')
path SVG path string Path data when pathMode: 'custom'

Focus resolution:

Shape Resolves to When to use
[x, y] The literal point Free-floating waypoints with no associated item
'itemId' itemRegistry.get(id).item.bounds.center Track a shape as it moves
{ item: 'id', offset: [x, y] } item.bounds.center + offset Frame part of an item (e.g., a head via offset from a body’s center)

Because items can themselves be bound by other relations (part_of, follows, attached_to), focus lookups transitively inherit those behaviors — rig a character’s head via skeletal animation and a keyframe with focus: 'head' tracks it automatically. The camera becomes a derivation over the scene graph rather than an independent pixel tour.

Path shapes between keyframes:

The spatial path from one keyframe to the next defaults to a straight line. Four escape hatches let you shape the in-between path:

  1. Bezier handlespathOut on the previous keyframe, pathIn on the next keyframe. Both are 2D offsets relative to the resolved focus point. Either handle alone is enough to promote the segment to a cubic bezier.

    keyframes: [
      { time: 0, focus: 'st_idle',      pathOut: [200, -100] }, // leave going right-and-up
      { time: 3, focus: 'st_error',     pathIn:  [-200, 100]  }, // arrive from upper-right
    ]
    
  2. Arc (shorthand)pathMode: 'arc' on the previous keyframe plus a pivot: [x, y] center. arcDirection: 'ccw' (default) takes the counter-clockwise sweep; 'cw' reverses. Compiles to cubic-bezier handles internally via the standard h = (4/3) tan(Δ/4) r formula.

    keyframes: [
      { time: 0, focus: [100, 0],
        pathMode: 'arc', pivot: [0, 0], arcDirection: 'ccw' },
      { time: 2, focus: [0, 100] },   // quarter-circle around the origin
    ]
    
  3. Custom SVG pathpathMode: 'custom' with an SVG-path d string. The camera traces the path at eased progress. Requires Paper.js (i.e. the main-thread renderer); falls back to a straight line in headless environments.

    keyframes: [
      { time: 0, focus: [0, 0],
        pathMode: 'custom', path: 'M 0 0 C 50 -100, 150 -100, 200 0' },
      { time: 2, focus: [200, 0] },
    ]
    
  4. Linear — the default when none of the above are present. Identical to the pre-0.5 behavior; existing templates keep working unchanged.

Endpoints always match the keyframe’s focus position regardless of which path mode you pick — pathOut doesn’t shift where the camera leaves the previous keyframe, only how it curves on the way out.

Camera Helper Methods:

Method Description
app.camera.zoomIn(level, duration) Animate zoom in
app.camera.zoomOut(level, duration) Animate zoom out
app.camera.panTo(x, y, duration) Pan to coordinates
app.camera.panBy(dx, dy, duration) Pan by relative offset
app.camera.panLeft/Right/Up/Down(amount, duration) Directional pan
app.camera.moveTo(x, y, zoom, duration) Combined zoom and pan
app.camera.reset(duration) Reset to default state
app.camera.stop() Stop current animation
app.camera.getState() Get {zoom, center}
app.camera.isAnimating() Check if animating

morphs_to

Shape morphing animation. Similar to Manim’s Transform. Supports cross-type morphing (e.g., Path to PointText) using particle-based denoising effects.

app.addRelation(sourceId, targetId, 'morphs_to', {
  duration: 1.0,
  delay: 0,
  easing: 'easeInOut',
  morphColor: true,
  morphSize: true,
  hideTarget: true,
  removeTargetOnComplete: false
});
Parameter Type Default Description
duration number 1.0 Morph duration in seconds
delay number 0 Delay before morph starts
easing string 'easeInOut' Easing function
morphColor boolean true Also morph colors
morphSize boolean true Also morph size/scale
hideTarget boolean true Hide target item during morph (serves as shape reference)
removeTargetOnComplete boolean false Remove target item when morph completes

Discovery

getAvailableRelations()

Get a list of all registered relation type names.

Returns: Array<string>

const types = app.getAvailableRelations();
// ['orbits', 'follows', 'attached_to', 'maintains_distance', 'points_at',
//  'mirrors', 'parallax', 'bounds_to', 'grows_from', 'staggered_with',
//  'indicates', 'circumscribes', 'wave_through', 'camera_follows',
//  'camera_animates', 'morphs_to', 'driven_by', 'wiggle', 'time_expression',
//  'bone_attached', 'ik_target', 'blend_reacts_to', 'blend_transition']

Morph Strategy by Type:

Source → Target Strategy Description
Path → Path Direct Point-by-point path interpolation
Circle → Circle Direct Radius and position interpolation
Text → Text Direct Font size, position, opacity blend
Path → Text Denoising Particle effect transition
Text → Path Denoising Particle effect transition
Group → Any Denoising Complex shape dissolution

Note: Path segment interpolation works best when source and target have the same number of segments. Cross-type morphs use a particle-based denoising effect for smooth transitions.