Tutorial
This tutorial walks you through Ripl step-by-step, starting from the most basic use-case and progressively adding complexity. By the end, you'll understand how to draw elements, organize them into groups and scenes, and add interactivity and animation.
The Basics
Render a Basic Element
The most basic usage of Ripl involves 3 steps:
- Create a context — the rendering target (Canvas by default)
- Create an element — specify the element's properties
- Render — draw the element to the context
import {
createCircle,
createContext,
} from '@ripl/web';
const context = createContext('.mount-element');
const circle = createCircle({
fill: '#3a86ff',
cx: context.width / 2,
cy: context.height / 2,
radius: Math.min(context.width, context.height) / 3,
});
circle.render(context);TIP
Ripl renders to a Canvas context by default. To render to SVG instead, import createContext from @ripl/svg instead of @ripl/core. Try toggling between Canvas and SVG using the buttons above the demo!
Change Element Properties
An element can be modified at any point by changing its properties and re-rendering. Use the slider below to change the circle's radius in real time.
// Change any property directly
circle.radius = 80;
circle.fill = '#ff006e';
// Then re-render
context.clear();
circle.render(context);Creating Structure
Using Groups
Groups let you organize elements into a hierarchy — just like the DOM. A group can hold any number of child elements and even other groups. Properties set on a group are inherited by its children, so you can set a shared fill once on the group instead of on every element.
import {
createCircle,
createContext,
createGroup,
createRect,
} from '@ripl/web';
const context = createContext('.mount-element');
const circle = createCircle({
cx: context.width / 3,
cy: context.height / 2,
radius: Math.min(context.width, context.height) / 5,
});
const rect = createRect({
x: context.width / 2,
y: context.height / 3,
width: context.width / 4,
height: context.height / 3,
});
// Both children inherit fill from the group
const group = createGroup({
fill: '#3a86ff',
children: [circle, rect],
});
group.render(context);Querying Elements
Groups and scenes support DOM-like querying methods — getElementById, getElementsByType, query, and queryAll. This makes it easy to find elements deep in the tree without keeping references to every element.
import {
createCircle,
createContext,
createGroup,
createRect,
} from '@ripl/web';
const context = createContext('.mount-element');
const group = createGroup({
fill: '#3a86ff',
children: [
createCircle({ id: 'c1',
class: 'shape',
cx: 80,
cy: 100,
radius: 40 }),
createCircle({ id: 'c2',
class: 'shape',
cx: 200,
cy: 100,
radius: 30 }),
createRect({ id: 'r1',
class: 'shape',
x: 260,
y: 70,
width: 80,
height: 60 }),
],
});
group.render(context);
// Find by ID
group.getElementById('c1');
// Find by type
group.getElementsByType('circle');
// CSS-like selectors
group.query('#c1');
group.queryAll('.shape');Interaction
Ripl's context automatically handles pointer event delegation via hit testing. You can listen for events like mouseenter, mouseleave, click, dragstart, drag, and dragend directly on any element — no scene or renderer required.
import {
createCircle,
createContext,
} from '@ripl/web';
const context = createContext('.mount-element');
const circle = createCircle({
fill: '#3a86ff',
cx: 200,
cy: 150,
radius: 60,
});
function render() {
context.batch(() => {
circle.render(context);
});
}
render();
circle.on('mouseenter', () => {
circle.fill = '#ff006e';
render();
});
circle.on('mouseleave', () => {
circle.fill = '#3a86ff';
render();
});
circle.on('click', () => {
circle.radius = circle.radius === 60 ? 90 : 60;
render();
});TIP
Hover over and click the circle to see the events in action.
TIP
After changing element properties, re-render to see the updates. The batch() call clears the surface and tells the context which elements are on screen for hit testing.
Animation
Ripl provides a standalone transition() function for animating values over time. Combined with an element's interpolate() method, you can smoothly animate element properties without a renderer.
The transition() function calls your callback on each animation frame with an eased time value (0–1). The element's interpolate() method creates an interpolator that maps that time value to intermediate property states.
import {
createCircle,
createContext,
easeOutCubic,
transition,
} from '@ripl/web';
const context = createContext('.mount-element');
const circle = createCircle({
fill: '#3a86ff',
cx: 200,
cy: 150,
radius: 60,
});
function render() {
context.batch(() => {
circle.render(context);
});
}
render();
const interpolator = circle.interpolate({
radius: 100,
fill: '#ff006e',
});
await transition(time => {
interpolator(time);
render();
}, {
duration: 1000,
ease: easeOutCubic,
});NOTE
The transition() function returns a Transition (which extends Task) — it is both awaitable and cancellable via transition.abort().
The standalone transition() gives you full control, but for most use-cases a Scene and Renderer provide a more convenient approach.
Using Scenes
A Scene is a special group that binds to a context. It manages the full rendering lifecycle — clearing the context, rendering all children in z-index order, and automatically re-rendering when the context resizes.
import {
createCircle,
createRect,
createScene,
} from '@ripl/web';
const scene = createScene('.mount-element', {
children: [
createCircle({
fill: '#3a86ff',
cx: 200,
cy: 150,
radius: 60,
}),
createRect({
fill: '#ff006e',
x: 280,
y: 100,
width: 120,
height: 100,
}),
],
});
scene.render();TIP
When you use a Scene, you don't need to call context.batch() yourself — the scene handles clearing and render markers automatically.
Using a Renderer
A Renderer provides an automatic render loop powered by requestAnimationFrame. It continuously re-renders the scene each frame, and its transition() method handles interpolation and re-rendering for you — no manual scene.render() calls needed.
import {
createCircle,
createRenderer,
createScene,
easeOutCubic,
} from '@ripl/web';
const scene = createScene('.mount-element', {
children: [
createCircle({
id: 'my-circle',
fill: '#3a86ff',
cx: 200,
cy: 150,
radius: 60,
}),
],
});
const renderer = createRenderer(scene);
const circle = scene.query('#my-circle');
await renderer.transition(circle, {
duration: 1000,
ease: easeOutCubic,
state: {
radius: 100,
fill: '#ff006e',
},
});Chaining Transitions
Because renderer.transition() returns a promise, you can chain animations sequentially. You can also animate multiple elements at once and use different easing functions.
async function animate() {
await renderer.transition(circle, {
duration: 800,
ease: easeOutCubic,
state: {
radius: 100,
fill: '#ff006e',
},
});
await renderer.transition(circle, {
duration: 800,
ease: easeInOutQuad,
state: {
radius: 60,
fill: '#3a86ff',
},
});
}Next Steps
Now that you understand the basics, explore the rest of the documentation:
- Essentials — Deep dive into each core concept
- Elements — Reference for all built-in elements
- Advanced — Events, animations, gradients, and custom elements