Animation

PinePaper supports both simple loop animations and complex keyframe-based animations.

Simple Animations

animate(item, params)

Apply a predefined animation to an item.

Parameters:

  • item: Item to animate
  • params (object): Animation settings

Animation Types:

  • pulse - Scale up/down rhythmically
  • rotate - Continuous rotation
  • bounce - Vertical bouncing motion
  • fade - Opacity cycling
  • wobble - Side-to-side wobbling
  • slide - Horizontal sliding
  • typewriter - Character-by-character reveal (text only)
// Pulsing animation
app.animate(circle, {
  animationType: 'pulse',
  animationSpeed: 1.0  // Speed multiplier
});

// Rotating animation
app.animate(star, {
  animationType: 'rotate',
  animationSpeed: 0.5
});

// Bouncing text
app.animate(text, {
  animationType: 'bounce',
  animationSpeed: 1.5
});

// Typewriter effect
app.animate(text, {
  animationType: 'typewriter',
  animationSpeed: 10  // Characters per second
});

stopAnimation(item)

Stop and remove animation from an item.

app.stopAnimation(circle);

Keyframe Animations

For complex, multi-property animations over time.

Adding Keyframes

app.modify(sun, {
  animationType: 'keyframe',
  keyframes: [
    {
      time: 0,
      properties: { position: [100, 400], fillColor: '#fbbf24' },
      easing: 'linear'
    },
    {
      time: 2,
      properties: { position: [400, 100], fillColor: '#f97316' },
      easing: 'easeInOut'
    },
    {
      time: 4,
      properties: { position: [700, 400], fillColor: '#ef4444' },
      easing: 'easeOut'
    }
  ]
});

Keyframe Properties

Property Description
position Array [x, y] or separate x, y
scale Uniform scale, or scaleX/scaleY
rotation Rotation in degrees
opacity Transparency (0-1)
fillColor Fill color (hex, rgb, named)
strokeColor Stroke color
fontSize Text size

Easing Functions

  • linear - Constant speed
  • easeIn - Slow start
  • easeOut - Slow end
  • easeInOut - Slow start and end
  • bounce - Bounce effect
  • elastic - Elastic overshoot

Playback Control

// Start playback
app.playKeyframeTimeline(duration, loop);

// Pause without resetting time
app.pauseKeyframeTimeline();

// Resume from paused position
app.resumeKeyframeTimeline();

// Stop playback (resets to beginning)
app.stopKeyframeTimeline();

// Scrub to specific time
app.setPlaybackTime(2.5);  // Jump to 2.5 seconds

// Check playback state
if (app.isPlayingKeyframes) {
  console.log('Playing at:', app.playbackTime);
}

Example: Day/Night Cycle

// Create sky
const sky = app.create('rectangle', {
  x: 400, y: 300, width: 800, height: 600,
  color: '#1e3a5f'
});

// Create sun
const sun = app.create('circle', {
  x: 100, y: 500, radius: 60,
  color: '#fbbf24'
});

// Animate sky color
app.modify(sky, {
  animationType: 'keyframe',
  keyframes: [
    { time: 0, properties: { fillColor: '#1e3a5f' }, easing: 'easeInOut' },
    { time: 2, properties: { fillColor: '#fb923c' }, easing: 'easeInOut' },
    { time: 4, properties: { fillColor: '#38bdf8' }, easing: 'easeInOut' },
    { time: 6, properties: { fillColor: '#f97316' }, easing: 'easeInOut' },
    { time: 8, properties: { fillColor: '#1e3a5f' }, easing: 'easeInOut' }
  ]
});

// Animate sun position
app.modify(sun, {
  animationType: 'keyframe',
  keyframes: [
    { time: 0, properties: { position: [100, 600], opacity: 0 } },
    { time: 2, properties: { position: [200, 100], opacity: 1 } },
    { time: 4, properties: { position: [400, 50], opacity: 1 } },
    { time: 6, properties: { position: [600, 100], opacity: 1 } },
    { time: 8, properties: { position: [700, 600], opacity: 0 } }
  ]
});

// Play looping
app.playKeyframeTimeline(8, true);

Animation Presets Library

Import pre-built keyframe sequences from js/templates/_shared/animations.js:

Category Presets
Attention Seekers tada, headShake, heartBeat, flash, shakeX, shakeY, jello, shake, wobble, rubberBand, swing
Fading Entrances fadeIn, fadeInUp, fadeInDown, fadeInLeft, fadeInRight
Bouncing Entrances bounceIn, bounceInDown, bounceInUp, bounceInLeft, bounceInRight
Zooming Entrances zoomIn, zoomInDown, zoomInUp, zoomInLeft, zoomInRight
Flipping flipInX, flipInY, flipOutX, flipOutY
Rotating Entrances rotateIn, rotateInDownLeft, rotateInDownRight, rotateInUpLeft, rotateInUpRight
Back Entrances backInDown, backInUp, backInLeft, backInRight
Sliding Entrances slideInDown, slideInUp, slideInLeft, slideInRight
Scale popIn, explosivePop, scalePulse, breathe
Physics gravityDrop, elasticSnap, joyJump
Paths spiralIn, sineFloat, figureEight, circularOrbit, bezierCurve
Specials lightSpeedInRight, lightSpeedInLeft, rollIn, heroReveal, cyberScan, motionBlurSnap
Fading Exits fadeOut, fadeOutDown, fadeOutUp, fadeOutLeft, fadeOutRight
Bouncing Exits bounceOut, bounceOutDown, bounceOutUp, bounceOutLeft, bounceOutRight
Zooming Exits zoomOut, zoomOutDown, zoomOutUp, zoomOutLeft, zoomOutRight
Rotating Exits rotateOut, rotateOutDownLeft, rotateOutDownRight, rotateOutUpLeft, rotateOutUpRight
Back Exits backOutDown, backOutUp, backOutLeft, backOutRight
Sliding Exits slideOutDown, slideOutUp, slideOutRight, slideOutLeft
Other Exits hingeDrop, shrinkAway, lightSpeedOutRight, rollOut
UX Feedback successPops, errorShake, circumscribeClockwise, circumscribeCounterClockwise
Glitch glitchFlicker, staticNoise
import { bounceIn, tada, fadeOutRight } from './js/templates/_shared/animations.js';

// Apply entrance animation
app.modify(item, {
  animationType: 'keyframe',
  keyframes: bounceIn(0)  // starts at time 0
});

// Delayed attention seeker
app.modify(item, {
  animationType: 'keyframe',
  keyframes: tada(1.5)  // starts at 1.5s
});

// Exit with directional fade
app.modify(item, {
  animationType: 'keyframe',
  keyframes: fadeOutRight(3, [item.position.x, item.position.y])
});

Vertex Deformation

Deformation presets manipulate actual path geometry (vertices + bezier handles) per frame — not just transform properties.

applyDeform(item, presetName, params)

// Eye blink effect — fold toward horizontal midline
app.applyDeform(circle, 'fold', {
  frequency: 2,       // cycles per second
  amplitude: 0.8,     // deformation strength (0-1)
  phase: 'blink'      // quick close/open with pause
});

// Jelly wobble
app.applyDeform(shape, 'wobble', {
  frequency: 1, amplitude: 0.5,
  speed: 3, maxDisplacement: 12
});

// Traveling wave
app.applyDeform(path, 'wave', {
  waves: 3, speed: 2, maxDisplacement: 20
});

removeDeform(item)

app.removeDeform(circle);  // restores original geometry

triggerDeform(item, presetName, params)

One-shot deformation that plays once then auto-removes:

app.triggerDeform(item, 'squash', { frequency: 3 });

Deformation Presets:

Preset Effect
fold Compress toward midline (blink). axis: 'horizontal'/'vertical'
squeeze Compress sides, stretch top/bottom
squash Classic squash & stretch (area-preserving)
pinch Pull vertices toward center
bulge Push vertices outward
twist Rotate vertices ∝ distance. turns: 0.5
ripple Sinusoidal radial displacement. waves, maxDisplacement
wave Traveling sine wave. waves, speed, axis
breathe Organic radial expansion/contraction
melt Top droops, bottom spreads
shear Horizontal skew ∝ vertical position
inflate Non-uniform expansion (edges > center)
wobble Per-vertex jelly distortion. speed, maxDisplacement

Phase Drivers:

Phase Behavior
sin Smooth oscillation (default)
blink Quick close/open with long pause
pingpong Triangle wave 0→1→0
once Play once then stop
elastic Bouncy decay
heartbeat Double pulse
stepped Quantized to N levels. steps: 4

Item type behavior:

  • Path/CompoundPath: Direct vertex + handle deformation
  • Group: Recursive, all children deformed using group bounds
  • PointText/Raster: Transform approximation (scaleX/Y, rotation)

OnFrame Callbacks

For custom per-frame animation logic:

// Add custom animation
app.addOnFrameCallback('myAnimation', (event) => {
  // event.delta - Time since last frame
  // event.time - Total elapsed time
  // event.count - Frame count

  myItem.rotate(event.delta * 30);  // 30 degrees per second
}, {
  throttleMs: 16,  // Minimum ms between calls
  priority: 0      // Execution order
});

// Remove callback
app.removeOnFrameCallback('myAnimation');

// Check if callback exists
if (app.hasOnFrameCallback('myAnimation')) {
  console.log('Callback is registered');
}

// Get callback statistics
const stats = app.getCallbackStats();
// { count: 3, max: 100, callbacks: ['myAnimation', 'physics', 'particles'] }

Animated Items Tracking

PinePaper optimizes performance by tracking only animated items:

// Get count of animated items
const count = app.getAnimatedItemCount();

// Rebuild tracking after scene changes
app.rebuildAnimatedItemsSet();