Skip to content

FAQ ​

Common questions and clarifications about Ripl's concepts and usage.

What's the difference between Element and Shape? ​

Element is the base class for all drawable objects. It provides style properties, event handling, interpolation, and rendering infrastructure.

Shape extends Element and adds path-based rendering. Shapes automatically fill and stroke based on your style properties, and provide pixel-accurate hit testing via the path geometry.

ElementShape
ExamplesTextCircle, Rect, Arc, Line, Polygon, Polyline, Ellipse
RenderingManual: you control what gets drawnPath-based: define geometry, auto fill/stroke
Hit testingBounding boxPixel-accurate path geometry
When to useNon-path elements (text, images, composites)Geometric shapes

Rule of thumb: If your element draws a geometric path, extend Shape. If it draws something else (text, images, custom composites), extend Element.

When should I use a Group? ​

Use a Group when you want to:

  • Share styles: set fill once on the group instead of on every child
  • Organize elements: create a logical hierarchy (like a DOM tree)
  • Query elements: use getElementById, getElementsByType, query, etc.
  • Batch operations: render, add, or remove multiple elements at once

Groups are lightweight and don't draw anything themselves; they just manage their children.

When should I use a Scene vs a Group? ​

GroupScene
RenderingMust pass a context to render(context)Binds to a context, call render() with no args
EventsNo DOM event delegationFull DOM event delegation (click, hover, etc.)
ResizeNo automatic resize handlingAuto re-renders on resize
BufferNo render bufferFlat render buffer for O(n) performance

Use a Scene when:

  • You need pointer events (click, hover) on elements
  • You want automatic resize handling
  • You're rendering many elements and want the performance of a flat render buffer

Use a Group when:

  • You just need to organize elements into a hierarchy
  • You're rendering a small number of elements manually
  • You don't need pointer events

Do I need a Renderer? ​

Not always. A Renderer is needed when you want:

  • Animations: smooth transitions between property values
  • Continuous rendering: a requestAnimationFrame loop
  • Auto start/stop: automatic render loop management

If you're just drawing static elements, you can render them directly without a Renderer:

ts
// No renderer needed for static content
circle.render(context);

// Renderer needed for animations
const renderer = createRenderer(scene);
await renderer.transition(circle, {
    duration: 1000,
    state: { radius: 100 },
});

Canvas or SVG: which should I use? ​

CanvasSVG
PerformanceBetter for many elements (1000+)Better for fewer elements (< 500)
QualityBitmap, so it can pixelate on zoomVector, crisp at any zoom level
DOM accessNo DOM elements for rendered contentEach element is a DOM node
DevToolsCan't inspect individual shapesCan inspect/style individual elements
Pixel opsSupports getImageData/putImageDataNo pixel-level access

Default recommendation: Start with Canvas (the default). Switch to SVG if you need DOM accessibility, CSS styling of individual elements, or vector-quality output.

The beauty of Ripl is that switching is a one-line change:

ts
// Canvas (default)
import {
    createContext,
} from '@ripl/web';

// SVG: just change the import
import {
    createContext,
} from '@ripl/svg';

Why aren't my pointer events working? ​

Pointer events (click, mousedown, mouseup, mouseenter, mouseleave, mousemove, dragstart, drag, dragend) only work once the element has been rendered to a context. The Context — not the Scene — owns DOM event listening and hit testing; a Scene is the usual way to get elements rendered, but it is not required.

ts
// ❌ Won't work: nothing was rendered, so the context tracks no elements
const circle = createCircle({ /* ... */ });
circle.on('click', () => {}); // Never fires

// ✅ Works: rendered straight to the context
context.batch(() => circle.render(context));
circle.on('click', () => {}); // Fires correctly

// ✅ Works: the scene renders it for you
const scene = createScene(context, { children: [circle] });
scene.render();
circle.on('click', () => {}); // Fires correctly

For interactivity that isn't tied to an element — a marquee selection, a background click — subscribe on the context itself: context.on('mousedown' | 'mouseup' | 'click' | 'mousemove', …) fires whether or not an element was hit.

Why is my element invisible? ​

Common causes:

  1. No fill or stroke: elements need fill and/or stroke to be visible
  2. Zero dimensions: check that radius, width, height, etc. are non-zero
  3. Off-screen: check that coordinates are within the context bounds
  4. Wrong context: make sure you're rendering to the right context
  5. Not rendered: make sure you called .render(context) or scene.render()
  6. Covered by another element: check zIndex or rendering order

How do I render to a specific size? ​

The context automatically fills its parent container. To control the size, set the container's dimensions:

html
<div class="my-container" style="width: 600px; height: 400px;"></div>
ts
const context = createContext('.my-container');
// context.width === 600, context.height === 400

Can I use Ripl with React/Vue/Angular? ​

Yes. Ripl renders to a DOM element, so it works with any framework. Create the context in a lifecycle hook (e.g., onMounted in Vue, useEffect in React) and clean up with context.destroy() on unmount.

ts
// Vue 3 example
onMounted(() => {
    const context = createContext(containerRef.value);
    const scene = createScene(context, { children: [/* ... */] });
    scene.render();

    onUnmounted(() => scene.destroy());
});