efx-motion-canvas is a fork of Motion Canvas, enhanced with a Ratio-Independent Animation System. This allows creating a single animation build that adapts to any aspect ratio at runtime.
Create animations that work across all aspect ratios with a single build:
import { useResponsive, viewLayout } from '@layout'
export default makeScene2D(function* (view) {
const l = viewLayout(view.width(), view.height())
const config = useResponsive({
base: { fontSize: 80 },
portrait: { fontSize: 100 },
'9x16': { fontSize: 100 },
})
view.add(<Txt fontSize={l.sz(config.fontSize)} />)
})16x9- Standard landscape (default)9x16- Portrait mobile / TikTok4x3- Traditional TV1x1- Square / Instagramfullwindow- Full viewport coverage
| Package | Description |
|---|---|
@efxlab/motion-canvas-core |
Core animation engine with threadable generators |
@efxlab/motion-canvas-2d |
2D rendering components (Circle, Rect, Text, etc.) |
@efxlab/motion-canvas-responsive |
NEW Ratio-independent animation system |
@efxlab/motion-canvas-player |
Web component player for embedding animations |
@efxlab/motion-canvas-ui |
UI components for animation playback controls |
@efxlab/motion-canvas-vite-plugin |
Vite plugin for building animations |
The responsive package provides several hooks for ratio-independent animations:
Resolves properties with cascading overrides:
base → ratioClass → specificRatio
const props = useResponsive({
base: {x: 0.1, y: 0.2, size: 100},
portrait: {x: 0, y: 0.15},
'9x16': {size: 120},
});
// In 9x16: { x: 0, y: 0.15, size: 120 }Returns current layout with ratio info:
const layout = useRatio();
// layout.ratio → '9x16'
// layout.isPortrait → true
// layout.sz(64), layout.sx(0.5) → pixel valuesConditional effect for specific ratios:
useRatioEffect('portrait', () => {
view.add(<PortraitOverlay />)
return () => overlay.remove()
})Conditional rendering:
const mobileElement = useRatioElement({
only: ['9x16', 'portrait'],
element: () => <MobileWatermark />
})Animation parameter overrides:
const anim = useResponsiveAnimation({
base: {duration: 0.5, easing: easeInOut},
portrait: {duration: 0.3},
});
yield * element.opacity(1, anim.duration, anim.easing);Position, scale, rotation, visibility, color, path, and filter helpers:
import {
position,
scale,
rotation,
visibility,
color,
path,
filter,
} from '@efxlab/motion-canvas-responsive';// Off-screen (element fully outside view)
offLeft(margin); // x = -viewWidth/2 - elementWidth/2 - margin
offRight(margin); // x = +viewWidth/2 + elementWidth/2 + margin
offTop(margin); // y = -viewHeight/2 - elementHeight/2 - margin
offBottom(margin); // y = +viewHeight/2 + elementHeight/2 + margin
// Edge-aligned (element inside view, touching edge)
fromLeft(margin); // Element's left edge at view's left edge + margin
fromRight(margin); // Element's right edge at view's right edge - margin
fromTop(margin); // Element's top edge at view's top edge + margin
fromBottom(margin); // Element's bottom edge at view's bottom edge - margin
// Usage with responsive config
const pos = position({
base: {x: 100, y: 50},
portrait: {x: 50, y: 100},
});rotate: {
spin(turns); // Full rotations: spin(2) = 720deg
spinCW(turns); // Clockwise only
spinCCW(turns); // Counter-clockwise only
// Pivot offset (rotation center)
pivot: {
center(); // Default: element center
topLeft();
topRight();
bottomLeft();
bottomRight();
custom(x, y); // Offset from center in pixels
}
}
// Usage
element.pivot(rotate.pivot.topLeft());
yield * element.rotation(rotate.spin(1), 1.5);scale: {
from(value); // scale.from(0) = start invisible
to(value); // scale.to(2) = double size
pop(); // Quick bounce: 1 -> 1.1 -> 1
pulse(intensity); // Pulsing scale animation
}
transform: {
scaleX(value); // Horizontal stretch
scaleY(value); // Vertical stretch
skewX(degrees); // Horizontal skew
skeY(degrees); // Vertical skew
}
// Usage
yield * element.scale(scale.from(0).to(1), 1);opacity: {
fadeIn(duration);
fadeOut(duration);
flash(times); // Quick flash effect
blink(interval); // Repeating blink
}
// Usage
yield * element.opacity(opacity.fadeIn(1));color: {
tint(from, to);
highlight(color); // Quick color flash
gradient(colors); // Cycle through colors
}
stroke: {
draw(duration); // Line drawing animation
dash(length, gap);
}path: {
arc(startAngle, endAngle, radius);
bezier(controlPoints);
orbit(centerX, centerY, radius);
wave(amplitude, frequency);
shake(intensity); // Random shake
wobble(intensity); // Organic wobble
}
// Usage
yield * path.orbit(0, 0, 200, 2);filter: {
blur(from, to);
brightness(value);
contrast(value);
saturate(value);
grayscale(value); // 0-1
}const layout = useRatio();
// Element slides in from left, rotates, and scales up
yield *
all(
element.x(offLeft(0), 0),
element.x(fromRight(50), 1.5, easeOutCubic),
element.rotation(rotate.spin(1), 1.5),
element.scale(scale.from(0).to(1), 1),
);
// Rotate around top-left corner
element.pivot(rotate.pivot.topLeft());
yield * element.rotation(90, 0.5);
// Path animation - orbit around center
yield * path.orbit(0, 0, 200, 2);Register custom aspect ratios beyond the built-in defaults:
import {
registerCustomRatios,
useResponsive,
} from '@efxlab/motion-canvas-responsive';
// Register custom ratios
registerCustomRatios({
cinema: {aspect: '21:9'},
story: {aspect: '9:18'},
portrait2: {aspect: '3:4'},
});
// Use custom ratios in your scene
const config = useResponsive({
base: {fontSize: 80},
cinema: {fontSize: 100}, // Custom ratio
story: {fontSize: 90}, // Custom ratio
});| Ratio ID | Aspect | Class |
|---|---|---|
16x9 |
16:9 | landscape |
9x16 |
9:16 | portrait |
4x3 |
4:3 | landscape |
1x1 |
1:1 | square |
fullwindow |
auto | computed |
ar > 2→ultrawidear > 1→landscapear === 1→squarear < 1→portrait
const player = document.querySelector('motion-canvas-player');
// Playback
player.play();
player.pause();
// Ratio control (responsive templates)
player.setRatio('9x16'); // Switch ratio at runtime
player.getRatio(); // Get current ratio
// Variables
player.player.setVariables({titleText: 'New Title'});cd apps/packages/efx-motion-canvas
# Watch mode for specific package
pnpm core:dev
pnpm 2d:dev
pnpm player:dev
pnpm ui:dev# Build specific package
pnpm core:build
pnpm 2d:build
pnpm player:build
# Bundle for distribution
pnpm core:bundle
pnpm 2d:bundle# Run tests for specific package
pnpm core:test
pnpm 2d:test
# Or use pnpm filter
pnpm --filter @efxlab/motion-canvas-core test
pnpm --filter @efxlab/motion-canvas-2d test
pnpm --filter @efxlab/motion-canvas-responsive test
# Run single test file
pnpm --filter @efxlab/motion-canvas-responsive test -- src/hooks/useResponsive.test.tspnpm eslint # Run ESLint
pnpm eslint:fix # Fix ESLint issues
pnpm prettier # Check Prettier
pnpm prettier:fix # Fix with Prettierapps/packages/efx-motion-canvas/
├── packages/
│ ├── core/ # Core animation engine
│ │ └── src/
│ │ ├── app/ # Player application
│ │ ├── scenes/ # Scene management
│ │ └── threading/ # Threadable generators
│ ├── 2d/ # 2D rendering components
│ │ └── src/
│ │ └── components/ # Circle, Rect, Img, etc.
│ ├── responsive/ # NEW Ratio-independent animations
│ │ └── src/
│ │ ├── hooks/ # useRatio, useResponsive, etc.
│ │ ├── helpers/ # position, scale, rotation
│ │ ├── components/ # RatioLayer, ParticleGroup
│ │ └── context/ # ResponsiveContext
│ ├── player/ # Web component player
│ ├── ui/ # UI controls
│ └── vite-plugin/ # Build plugin
pnpm template:devStarts a vite server that watches the core, 2d, ui, and vite-plugin
packages.
pnpm template:build
pnpm player:dev- TypeScript - Type safety throughout
- Vite - Fast builds with hot reload
- Vitest - Unit testing
- ESLint + Prettier - Code quality
This project is based on Motion Canvas.