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 animateparams(object): Animation settings
Animation Types:
pulse- Scale up/down rhythmicallyrotate- Continuous rotationbounce- Vertical bouncing motionfade- Opacity cyclingwobble- Side-to-side wobblingslide- Horizontal slidingtypewriter- 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 speedeaseIn- Slow starteaseOut- Slow endeaseInOut- Slow start and endbounce- Bounce effectelastic- 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();