A lightweight 2D graphics library providing canvas and SVG rendering for Apache ECharts
ZRender provides a comprehensive animation framework that supports property interpolation, easing functions, timeline control, and advanced features like path morphing. The animation system is frame-based and optimized for smooth 60fps performance.
The primary interface for controlling animations on elements:
interface Animator {
// Timeline control
when(time: number, props: any): Animator;
during(callback: (percent: number) => void): Animator;
delay(time: number): Animator;
// Lifecycle callbacks
done(callback: () => void): Animator;
aborted(callback: () => void): Animator;
// Playback control
start(easing?: string | Function): Animator;
stop(): void;
pause(): void;
resume(): void;
// Configuration
getLoop(): boolean;
setLoop(loop: boolean): Animator;
}All graphics elements provide animation methods:
interface Element {
animate(props?: string): Animator;
animateStyle(): Animator;
animateShape(): Animator;
stopAnimation(forwardToLast?: boolean): this;
// Animation state queries
isAnimationFinished(): boolean;
getAnimationDelay(): number;
}ZRender includes a comprehensive set of easing functions:
function linear(t: number): number;
function easeInQuad(t: number): number;
function easeOutQuad(t: number): number;
function easeInOutQuad(t: number): number;
function easeInCubic(t: number): number;
function easeOutCubic(t: number): number;
function easeInOutCubic(t: number): number;
function easeInQuart(t: number): number;
function easeOutQuart(t: number): number;
function easeInOutQuart(t: number): number;
function easeInQuint(t: number): number;
function easeOutQuint(t: number): number;
function easeInOutQuint(t: number): number;function easeInSine(t: number): number;
function easeOutSine(t: number): number;
function easeInOutSine(t: number): number;
function easeInExpo(t: number): number;
function easeOutExpo(t: number): number;
function easeInOutExpo(t: number): number;
function easeInCirc(t: number): number;
function easeOutCirc(t: number): number;
function easeInOutCirc(t: number): number;function easeInElastic(t: number): number;
function easeOutElastic(t: number): number;
function easeInOutElastic(t: number): number;
function easeInBack(t: number): number;
function easeOutBack(t: number): number;
function easeInOutBack(t: number): number;
function easeInBounce(t: number): number;
function easeOutBounce(t: number): number;
function easeInOutBounce(t: number): number;const easeIn: typeof easeInQuad;
const easeOut: typeof easeOutQuad;
const easeInOut: typeof easeInOutQuad;
const ease: typeof easeInOut;interface Clip {
life: number; // Duration in milliseconds
delay: number; // Delay before starting
loop: boolean; // Whether to loop
gap: number; // Gap between loops
easing: string | Function; // Easing function
onframe: (percent: number) => void; // Frame callback
ondestroy: () => void; // Cleanup callback
onrestart: () => void; // Restart callback
}interface AnimationOptions {
duration?: number; // Animation duration
easing?: string | Function; // Easing function
delay?: number; // Start delay
loop?: boolean; // Loop animation
gap?: number; // Gap between loops
during?: (percent: number) => void; // Progress callback
done?: () => void; // Completion callback
aborted?: () => void; // Abortion callback
}Advanced morphing capabilities for transforming between different paths:
namespace morph {
function morphPath(from: string, to: string, animationOpts?: AnimationOptions): string;
function combineMorphing(morphList: any[]): any;
function isCombineMorphing(obj: any): boolean;
}import { Circle } from "zrender";
const circle = new Circle({
shape: { cx: 100, cy: 100, r: 30 },
style: { fill: '#74b9ff' },
position: [0, 0]
});
// Animate position
circle.animate('position')
.when(1000, [200, 150]) // Move to (200, 150) over 1 second
.when(2000, [100, 100]) // Return to (100, 100) over next second
.start('easeInOut');
// Animate shape properties
circle.animate('shape')
.when(1500, { r: 60 }) // Grow radius to 60
.start('easeOutElastic');
// Animate style properties
circle.animate('style')
.when(800, { fill: '#e17055', opacity: 0.7 })
.start('easeInOutQuad');
zr.add(circle);import { Rect } from "zrender";
const rect = new Rect({
shape: { x: 50, y: 50, width: 80, height: 60 },
style: { fill: '#00b894' }
});
// Chain multiple animations
rect.animate('position')
.when(1000, [200, 50])
.when(2000, [200, 200])
.when(3000, [50, 200])
.when(4000, [50, 50])
.start('easeInOutCubic');
// Parallel animation on different properties
rect.animate('shape')
.delay(500) // Start after 500ms delay
.when(1000, { width: 120, height: 40 })
.when(2000, { width: 40, height: 120 })
.when(3000, { width: 80, height: 60 })
.start('easeOutBounce');
// Color animation with callbacks
rect.animate('style')
.when(1000, { fill: '#fdcb6e' })
.when(2000, { fill: '#e84393' })
.when(3000, { fill: '#74b9ff' })
.when(4000, { fill: '#00b894' })
.during((percent) => {
// Custom logic during animation
if (percent > 0.5) {
rect.style.shadowBlur = (percent - 0.5) * 20;
}
})
.done(() => {
console.log('Animation completed!');
})
.start();
zr.add(rect);import { Circle } from "zrender";
// Create circles to demonstrate different easing functions
const easingFunctions = [
'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
'easeInCubic', 'easeOutBounce', 'easeOutElastic', 'easeInBack'
];
easingFunctions.forEach((easing, index) => {
const circle = new Circle({
shape: { cx: 50, cy: 50 + index * 60, r: 20 },
style: { fill: '#74b9ff' }
});
// Animate position with different easing
circle.animate('position')
.when(2000, [400, 50 + index * 60])
.setLoop(true) // Loop the animation
.start(easing);
// Add label
const label = new Text({
style: {
text: easing,
fontSize: 12,
fill: '#2d3436'
},
position: [460, 55 + index * 60]
});
zr.add(circle);
zr.add(label);
});import { Star } from "zrender";
const star = new Star({
shape: { cx: 200, cy: 200, n: 5, r0: 30, r: 60 },
style: { fill: '#fdcb6e', stroke: '#e17055', lineWidth: 2 }
});
// Hover animations
star.on('mouseover', () => {
star.stopAnimation(); // Stop any running animations
star.animate('shape')
.when(300, { r: 80, r0: 40 })
.start('easeOutElastic');
star.animate('style')
.when(200, { fill: '#fff5b4', shadowBlur: 15, shadowColor: '#fdcb6e' })
.start();
});
star.on('mouseout', () => {
star.stopAnimation();
star.animate('shape')
.when(300, { r: 60, r0: 30 })
.start('easeOutQuad');
star.animate('style')
.when(200, { fill: '#fdcb6e', shadowBlur: 0 })
.start();
});
// Click animation with rotation
star.on('click', () => {
star.animate('rotation')
.when(1000, Math.PI * 2) // Full rotation
.done(() => {
star.rotation = 0; // Reset rotation
})
.start('easeInOutCubic');
});
zr.add(star);import { Group, Circle, Rect, Text } from "zrender";
// Create a group of elements for coordinated animation
const animationGroup = new Group();
const elements = [];
for (let i = 0; i < 5; i++) {
const circle = new Circle({
shape: { cx: 100 + i * 80, cy: 200, r: 25 },
style: { fill: `hsl(${i * 60}, 70%, 60%)` }
});
elements.push(circle);
animationGroup.add(circle);
}
// Staggered animation sequence
elements.forEach((element, index) => {
const delay = index * 200; // 200ms stagger
// Bounce animation
element.animate('position')
.delay(delay)
.when(600, [0, -100]) // Move up (relative to group)
.when(1200, [0, 0]) // Return to original position
.start('easeOutBounce');
// Scale animation
element.animate('scale')
.delay(delay + 300)
.when(400, [1.5, 1.5])
.when(800, [1, 1])
.start('easeInOutQuad');
});
// Group-level animation
animationGroup.animate('position')
.delay(1500)
.when(1000, [0, 100])
.start('easeInOutCubic');
zr.add(animationGroup);import { Rect } from "zrender";
// Define custom easing function
const customEasing = (t: number): number => {
// Elastic overshoot effect
return t === 0 ? 0 : t === 1 ? 1 :
-Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI);
};
// Apply custom easing
const rect = new Rect({
shape: { x: 50, y: 150, width: 60, height: 40 },
style: { fill: '#e84393' }
});
rect.animate('position')
.when(2000, [400, 150])
.start(customEasing);
// Bezier curve easing (cubic-bezier equivalent)
const bezierEasing = (t: number): number => {
// Equivalent to cubic-bezier(0.25, 0.46, 0.45, 0.94)
const p0 = 0, p1 = 0.25, p2 = 0.45, p3 = 1;
const u = 1 - t;
return 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
};
rect.animate('shape')
.when(2000, { width: 120, height: 80 })
.start(bezierEasing);
zr.add(rect);import { Circle } from "zrender";
const controlledCircle = new Circle({
shape: { cx: 200, cy: 200, r: 40 },
style: { fill: '#74b9ff' }
});
// Create controllable animation
let currentAnimation: Animator;
const startAnimation = () => {
currentAnimation = controlledCircle.animate('position')
.when(3000, [400, 200])
.when(6000, [200, 200])
.setLoop(true)
.start('easeInOutQuad');
};
const pauseAnimation = () => {
if (currentAnimation) {
currentAnimation.pause();
}
};
const resumeAnimation = () => {
if (currentAnimation) {
currentAnimation.resume();
}
};
const stopAnimation = () => {
if (currentAnimation) {
currentAnimation.stop();
}
};
// Control via keyboard events
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 's': startAnimation(); break;
case 'p': pauseAnimation(); break;
case 'r': resumeAnimation(); break;
case 'x': stopAnimation(); break;
}
});
// Click to toggle animation
controlledCircle.on('click', () => {
if (controlledCircle.isAnimationFinished()) {
startAnimation();
} else {
stopAnimation();
}
});
zr.add(controlledCircle);import { Group, Circle } from "zrender";
// Animate many elements efficiently
const createOptimizedAnimation = () => {
const group = new Group();
const elements: Circle[] = [];
// Create many elements
for (let i = 0; i < 100; i++) {
const circle = new Circle({
shape: {
cx: Math.random() * 800,
cy: Math.random() * 600,
r: 5 + Math.random() * 10
},
style: {
fill: `hsl(${Math.random() * 360}, 70%, 60%)`,
opacity: 0.8
}
});
elements.push(circle);
group.add(circle);
}
// Use single group animation instead of individual animations
// This is more performant for coordinated movements
group.animate('rotation')
.when(10000, Math.PI * 2)
.setLoop(true)
.start('linear');
// Stagger individual element animations efficiently
elements.forEach((element, index) => {
if (index % 10 === 0) { // Animate every 10th element
element.animate('scale')
.delay(Math.random() * 2000)
.when(1000, [1.5, 1.5])
.when(2000, [1, 1])
.setLoop(true)
.start('easeInOutSine');
}
});
return group;
};
const optimizedGroup = createOptimizedAnimation();
zr.add(optimizedGroup);Install with Tessl CLI
npx tessl i tessl/npm-zrender