Skip to content

Renderers

Oroya Animate renderers are translators that convert the agnostic scene graph into visual output. The core does not know about renderers β€” each one reads the scene graph and produces its own representation.


Overview

graph TD
    SG["Scene Graph\n(@joroya/core)"]
    R3["ThreeRenderer\n(@joroya/renderer-three)"]
    RS["renderToSVG\n(@joroya/renderer-svg)"]
    RC["renderToCanvas / CanvasRenderer\n(@joroya/renderer-canvas2d)"]
    WEBGL["WebGL Canvas (3D)"]
    SVG["SVG String (2D)"]
    CANVAS["Canvas2D Canvas (2D)"]

    SG -->|"mount + render"| R3
    SG -->|"function call"| RS
    SG -->|"function call / class"| RC
    R3 --> WEBGL
    RS --> SVG
    RC --> CANVAS
AspectThreeRendererrenderToSVG / renderToSVGElementrenderToCanvas / CanvasRenderer
ParadigmStateful instance (class)Pure string function or DOM element helperOne-shot function or persistent class
OutputDraws to a WebGL <canvas>Returns an SVG string or SVGSVGElementDraws to a Canvas2D <canvas>
Requires DOMYes (HTMLCanvasElement)String mode: no; DOM mode: yesYes (HTMLCanvasElement)
3DPerspective/orthographic, lights, shadows2D projection only2D projection only
VectorRasterizedInfinitely scalableRasterized

@joroya/renderer-three β€” Three.js (WebGL)

The main renderer for interactive 3D visualization.

Setup

import { ThreeRenderer } from '@joroya/renderer-three';

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: window.innerWidth,
  height: window.innerHeight,
  dpr: window.devicePixelRatio,  // optional
});

Constructor options

OptionTypeDefaultDescription
canvasHTMLCanvasElement(required)Target canvas element
widthnumber(required)Viewport width
heightnumber(required)Viewport height
dprnumberwindow.devicePixelRatioDevice pixel ratio (HiDPI)

Methods

MethodDescription
mount(scene)Connects a scene, rebuilds the Three.js scene, and detects the active camera
render(dt?)Runs scene.update(dt), syncs transforms, updates renderer-managed systems, and draws a frame
dispose()Releases WebGL resources

Lifecycle

sequenceDiagram
    participant U as User Code
    participant TR as ThreeRenderer
    participant TS as THREE.Scene

    Note over U,TS: Mounting
    U->>TR: mount(scene)
    TR->>TS: clear scene
    TR->>TR: traverse β†’ create Mesh/Group/Camera/Light/etc. per node
    TR->>TR: Set first Camera as activeCamera

    Note over U,TS: Render loop
    loop requestAnimationFrame
        U->>TR: render(dt)
        TR->>TR: scene.update(dt)
        TR->>TR: updateWorldMatrices()
        TR->>TS: sync worldMatrix β†’ Three.js objects
        TR->>TR: webglRenderer.render()
    end

Component translation

Oroya NodeThree.js Object
Node without Geometry or CameraTHREE.Group
Node + supported mesh GeometryTHREE.Mesh with Box/Sphere/Cylinder/Plane/Cone/Torus/Circle/Buffer/CSG geometry
Node + Geometry + SkinTHREE.SkinnedMesh with skeleton binding after mount
Node + InstancedMeshTHREE.InstancedMesh
Node + Geometry(Path2D) / Geometry(Text)Ignored by the Three.js backend
Node + Camera(Perspective)THREE.PerspectiveCamera
Node + Camera(Orthographic)THREE.OrthographicCamera
Node + LightTHREE.Light subclass
Node + ParticleSystemTHREE.Points
Node + AudioListener / AudioSourceTHREE.AudioListener / THREE.PositionalAudio
Material with colorMeshStandardMaterial({ color })
Material with opacity < 1MeshStandardMaterial({ transparent: true })
PBR and texture fieldsMeshStandardMaterial roughness/metalness/emissive/maps
Without MaterialDefault MeshStandardMaterial

Lighting

ThreeRenderer translates explicit Light components from the scene graph. Add ambient, directional, point, or spot lights to control illumination; no implicit default lights are injected.

Oroya lightThree.js object
LightType.AmbientTHREE.AmbientLight
LightType.DirectionalTHREE.DirectionalLight with optional shadows and target
LightType.PointTHREE.PointLight with optional shadows
LightType.SpotTHREE.SpotLight with optional shadows, target, angle, and penumbra

Camera resolution

flowchart TD
    START["mount(scene)"] --> TRAVERSE["Traverse scene graph"]
    TRAVERSE --> FOUND{"Camera found?"}
    FOUND -->|"Yes"| USE["Use first as active"]
    FOUND -->|"No"| FALLBACK["Fallback: PerspectiveCamera, FOV 75, z=5"]

Full example

import { Scene, Node, createBox, Material, Camera, CameraType } from '@joroya/core';
import { ThreeRenderer } from '@joroya/renderer-three';

const scene = new Scene();

const cam = new Node('cam');
cam.addComponent(new Camera({
  type: CameraType.Perspective, fov: 75,
  aspect: window.innerWidth / window.innerHeight, near: 0.1, far: 1000,
}));
cam.transform.position.z = 5;
scene.add(cam);

const box = new Node('box');
box.addComponent(createBox(1, 1, 1));
box.addComponent(new Material({ color: { r: 0.2, g: 0.6, b: 1.0 } }));
scene.add(box);

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: window.innerWidth, height: window.innerHeight,
});
renderer.mount(scene);

function loop() {
  box.transform.rotation.y = performance.now() * 0.001;
  box.transform.updateLocalMatrix();
  renderer.render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

@joroya/renderer-svg β€” SVG (2D)

Lightweight renderer that generates SVG markup. Ideal for generative art, vector export, and server-side rendering.

renderToSVG β€” Pure string (server-safe)

Pure, stateless function that returns an SVG string. Works in Node.js without DOM.

import { renderToSVG } from '@joroya/renderer-svg';

const svg: string = renderToSVG(scene, { width: 400, height: 300 });

Options (SvgRenderOptions)

OptionTypeDefaultDescription
widthnumber(required)SVG width
heightnumber(required)SVG height
viewBoxstring"0 0 {width} {height}"Custom viewBox

renderToSVGElement β€” Interactive DOM

Creates a real SVGSVGElement with event delegation. Nodes with the Interactive component receive pointer/click/wheel listeners.

import { renderToSVGElement } from '@joroya/renderer-svg';

const { svg, dispose } = renderToSVGElement(scene, {
  width: 800,
  height: 600,
  container: document.getElementById('app')!,
});

// When no longer needed:
dispose(); // Cleans listeners and removes SVG from DOM

Options (SvgElementRenderOptions)

Extends SvgRenderOptions with:

OptionTypeDescription
containerHTMLElement(optional) Parent element where the SVG is automatically attached

Return

FieldTypeDescription
svgSVGSVGElementThe created SVG element
dispose() => voidCleans event listeners and removes SVG from DOM

Supported interactive events

DOM EventInteractionEventType
clickClick
pointerdownPointerDown
pointerupPointerUp
pointermovePointerMove
pointerenterPointerEnter
pointerleavePointerLeave
wheelWheel

Pipeline

flowchart TD
    START["renderToSVG / renderToSVGElement"] --> TICK["scene.update(dt)"]
    TICK --> UPDATE["scene.updateWorldMatrices()"]
    UPDATE --> WALK["Traverse tree recursively"]
    WALK --> GEO{"Geometry?"}
    GEO -->|"Path2D"| PATH["β†’ path"]
    GEO -->|"Box"| RECT["β†’ rect"]
    GEO -->|"Sphere"| CIRCLE["β†’ circle"]
    GEO -->|"Text"| TEXT["β†’ text"]
    GEO -->|"Circle / Plane / Cylinder / Cone / Torus"| MORE["β†’ flattened SVG primitive/path"]
    GEO -->|"None"| GROUP["Only g if has children"]
    PATH & RECT & CIRCLE & TEXT & MORE --> MAT{"Material?"}
    MAT -->|"fill/stroke"| STYLE["fill + stroke + opacity"]
    MAT -->|"fillGradient"| GRAD["url(#gradient-id) + defs"]
    MAT -->|"filter/clip/mask"| FILT["url(#filter-id) + defs"]
    MAT -->|"None"| NONE["fill='none'"]
    STYLE & GRAD & FILT & NONE --> ANIM{"Animation?"}
    ANIM -->|"Yes"| ANIMC["animate / animateTransform children"]
    ANIM -->|"No"| NOANIM["No animation"]
    ANIMC & NOANIM --> TRANSFORM{"Transform β‰  identity?"}
    TRANSFORM -->|"Yes"| MATRIX["g transform='matrix(a,b,c,d,e,f)'"]
    TRANSFORM -->|"No"| DIRECT["Direct element"]
    MATRIX & DIRECT --> CHILDREN{"Children?"}
    CHILDREN -->|"Yes"| NEST["Nest in g"]
    CHILDREN -->|"No"| LEAF["Leaf node"]

Geometry support

GeometryGenerated SVG Element
Path2D<path d="...">
Box<rect> (width Γ— height, depth ignored)
Sphere<circle> (radius)
Text<text> with font-size, font-family, font-weight, text-anchor, dominant-baseline

Material properties for SVG

FieldTypeSVG EffectIf absent
fillColorRGBfill="rgb(R,G,B)"fill="none"
strokeColorRGBstroke="rgb(R,G,B)"No stroke
strokeWidthnumberstroke-width="N"1
opacitynumberopacity="N"No attribute (opaque)
fillGradientGradientDeffill="url(#id)" + <defs>Uses normal fill
strokeGradientGradientDefstroke="url(#id)" + <defs>Uses normal stroke
filterSvgFilterDeffilter="url(#id)" + <filter> in <defs>No filter
clipPathSvgClipPathDefclip-path="url(#id)" + <clipPath> in <defs>No clipping
maskSvgMaskDefmask="url(#id)" + <mask> in <defs>No mask

Transforms and hierarchy

The SVG renderer applies each node’s localMatrix as a transform="matrix(a,b,c,d,e,f)" attribute and generates <g> elements to represent the parent-child hierarchy of the scene graph.

const parent = new Node('group');
parent.transform.position = { x: 100, y: 50, z: 0 };

const child = new Node('square');
child.addComponent(createBox(30, 30, 0));
child.addComponent(new Material({ fill: { r: 1, g: 0, b: 0 } }));

parent.add(child);
scene.add(parent);

Generates:

<g transform="matrix(1,0,0,1,100,50)">
  <rect x="-15" y="-15" width="30" height="30" fill="rgb(255, 0, 0)" />
</g>

Gradients

const circle = new Node('sun');
circle.addComponent(createSphere(80));
circle.addComponent(new Material({
  fillGradient: {
    type: 'radial',
    cx: 0.5, cy: 0.5, r: 0.5,
    stops: [
      { offset: 0, color: { r: 1, g: 1, b: 0 } },
      { offset: 1, color: { r: 1, g: 0.3, b: 0 }, opacity: 0.8 },
    ],
  },
}));

Gradient types:

TypeDefinitionSVG Element
linearLinearGradientDef (x1, y1, x2, y2)<linearGradient>
radialRadialGradientDef (cx, cy, r, fx, fy)<radialGradient>

Text

const label = new Node('title');
label.addComponent(createText('Oroya Animate', {
  fontSize: 24,
  fontFamily: 'Inter',
  fontWeight: 'bold',
  textAnchor: 'middle',
}));
label.addComponent(new Material({ fill: { r: 0, g: 0, b: 0 } }));
label.transform.position = { x: 200, y: 30, z: 0 };
scene.add(label);

CSS Classes and Semantic IDs

Each node can have a cssClass and/or cssId that are emitted as class and id attributes on the generated SVG elements.

const node = new Node('highlight-box');
node.addComponent(createBox(100, 60, 0));
node.addComponent(new Material({ fill: { r: 1, g: 0.9, b: 0 } }));
node.cssClass = 'highlight animated';
node.cssId = 'main-callout';
scene.add(node);

Generates:

<rect id="main-callout" class="highlight animated" x="-50" y="-30" width="100" height="60" fill="rgb(255, 230, 0)" />

When the node has children or a transform, the attribute is applied to the container <g>:

<g id="main-callout" class="highlight animated" transform="matrix(1,0,0,1,50,25)">
  <rect x="-50" y="-30" width="100" height="60" fill="rgb(255, 230, 0)" />
</g>

Serialization: cssClass and cssId are preserved in serialize() / deserialize().

Orthographic camera and viewBox

If the scene contains a node with OrthographicCameraDef, the SVG renderer automatically calculates the viewBox from the camera frustum. An explicit viewBox in the options takes priority.

const cam = new Node('ortho-cam');
cam.addComponent(new Camera({
  type: CameraType.Orthographic,
  left: -400, right: 400,
  top: -300, bottom: 300,
  near: 0.1, far: 1000,
}));
scene.add(cam);

// viewBox is computed as "-400 -300 800 600"
const svg = renderToSVG(scene, { width: 800, height: 600 });

The camera position is applied as an offset to the viewBox:

cam.transform.position = { x: 50, y: 25, z: 0 };
// viewBox is computed as "-350 -275 800 600"

SVG Filters, Clip Paths and Masks

The renderer supports native SVG filters, clip paths, and masks through fields in MaterialDef.

Blur

const blurred = new Node('soft');
blurred.addComponent(createSphere(40));
blurred.addComponent(new Material({
  fill: { r: 0.5, g: 0.8, b: 1 },
  filter: { effects: [{ type: 'blur', stdDeviation: 3 }] },
}));

Generates:

<defs>
  <filter id="oroya-filter-0">
    <feGaussianBlur stdDeviation="3" />
  </filter>
</defs>
<circle cx="0" cy="0" r="40" fill="rgb(128, 204, 255)" filter="url(#oroya-filter-0)" />

Drop Shadow

new Material({
  fill: { r: 1, g: 0, b: 0 },
  filter: {
    effects: [{
      type: 'dropShadow', dx: 4, dy: 4,
      stdDeviation: 2, floodColor: '#333', floodOpacity: 0.6,
    }],
  },
});

Clip Path

new Material({
  fill: { r: 0, g: 1, b: 0 },
  clipPath: {
    path: [
      { command: 'M', args: [0, 0] },
      { command: 'L', args: [100, 0] },
      { command: 'L', args: [50, 100] },
      { command: 'Z', args: [] },
    ],
  },
});

Mask

new Material({
  fill: { r: 0, g: 0, b: 1 },
  mask: {
    path: [
      { command: 'M', args: [0, 0] },
      { command: 'L', args: [80, 0] },
      { command: 'L', args: [80, 80] },
      { command: 'Z', args: [] },
    ],
    fill: 'white',
    opacity: 0.8,
  },
});

Native SVG Animations

The Animation component allows adding declarative SVG animations (<animate> and <animateTransform>) that run in the browser without JavaScript.

import { Animation } from '@joroya/core';

const circle = new Node('pulse');
circle.addComponent(createSphere(30));
circle.addComponent(new Material({ fill: { r: 1, g: 0, b: 0 } }));
circle.addComponent(new Animation([
  {
    type: 'animate',
    attributeName: 'opacity',
    values: '1;0.3;1',
    dur: '2s',
    repeatCount: 'indefinite',
  },
]));

Generates:

<circle cx="0" cy="0" r="30" fill="rgb(255, 0, 0)">
  <animate attributeName="opacity" values="1;0.3;1" dur="2s" repeatCount="indefinite" />
</circle>

Transform animations:

new Animation([
  {
    type: 'animateTransform',
    transformType: 'rotate',
    from: '0 50 50',
    to: '360 50 50',
    dur: '4s',
    repeatCount: 'indefinite',
  },
]);

Generates:

<animateTransform attributeName="transform" type="rotate"
  from="0 50 50" to="360 50 50" dur="4s" repeatCount="indefinite" />

fill=β€œfreeze” keeps the final value after the animation ends, instead of reverting.

Note: Native animations only apply to the SVG renderer. The Three.js renderer ignores them.

Full Example

const triangle = new Node('triangle');
triangle.addComponent(createPath2D([
  { command: 'M', args: [200, 50] },
  { command: 'L', args: [350, 250] },
  { command: 'L', args: [50, 250] },
  { command: 'Z', args: [] },
]));
triangle.addComponent(new Material({
  fill: { r: 0.2, g: 0.8, b: 0.4 },
  stroke: { r: 0, g: 0, b: 0 },
  strokeWidth: 2,
  opacity: 0.9,
}));
scene.add(triangle);

const svg = renderToSVG(scene, { width: 400, height: 300 });

Use Cases

Use CaseAdvantage
Export to .svgOpen in Figma, Illustrator, Inkscape
Server-side renderingNode.js without DOM
Generative artProcedural patterns as vectors
PrintingLosslessly scalable
SVG InteractivityrenderToSVGElement with event delegation

@joroya/renderer-canvas2d β€” Canvas2D

Canvas2D is the lightweight browser-native 2D backend. It is useful for dense 2D scenes, HUDs, particles, and simple game-like views where vector export is not required.

import { renderToCanvas } from '@joroya/renderer-canvas2d';

renderToCanvas(scene, document.getElementById('canvas') as HTMLCanvasElement, {
  width: 800,
  height: 600,
  backgroundColor: { r: 0.05, g: 0.07, b: 0.09 },
  dt: 1 / 60,
});

For a persistent renderer, use CanvasRenderer:

import { CanvasRenderer } from '@joroya/renderer-canvas2d';

const renderer = new CanvasRenderer();
const canvas = renderer.mount(document.getElementById('app')!, {
  width: 800,
  height: 600,
});

renderer.startLoop(() => {
  renderer.render(scene, { width: 800, height: 600 });
});

Renderer Comparison

Geometry Support

GeometryThree.jsSVGCanvas2D
BoxYes<rect>fillRect / strokeRect
SphereYes<circle>arc
CylinderYesFlattened shapeNo
PlaneYes<rect>No
ConeYesFlattened pathNo
TorusYesRing pathNo
CircleYes<circle>No
Path2DNo<path>Canvas path
TextNo<text>fillText / strokeText
BufferYesNoNo
CSGYesNoNo

Material Support

PropertyThree.jsSVGCanvas2D
colorYesNoNo
opacityYesYesYes
metalness / roughnessYesNoNo
texture mapsYesNoNo
fillNoYesYes
strokeNoYesYes
strokeWidthNoYesYes
fillGradientNoYesYes
strokeGradientNoYesYes
filterNoYesNo
clipPathNoYesNo
maskNoYesNo

Transform Support

FeatureThree.jsSVGCanvas2D
Position (translate)Yesmatrix()ctx.transform()
RotationYesmatrix()ctx.transform()
ScaleYesmatrix()ctx.transform()
HierarchyGroups<g>Recursive canvas state stack

Special Component Support

FeatureThree.jsSVGCanvas2D
Camera (Perspective)YesNoCentered fallback
Camera (Orthographic)YesviewBox2D view transform
LightYesNoNo
PostProcessingYesNoNo
ParticleSystemYesNoNo
AudioListener / AudioSourceYesNoNo
InstancedMeshYesNoNo
InteractiveRaycasterEvent delegationNo
Animation (native SVG)No<animate> / <animateTransform>No
Animator / scene.update(dt)YesYesYes
cssClass / cssIdNoclass / id attributesNo

Creating a Custom Renderer

The contract is simple β€” implement mount, render, and dispose:

import { Scene, ComponentType, Geometry, Material, GeometryPrimitive } from '@joroya/core';

export class Canvas2DRenderer {
  private ctx: CanvasRenderingContext2D;
  private scene: Scene | null = null;

  constructor(canvas: HTMLCanvasElement) {
    this.ctx = canvas.getContext('2d')!;
  }

  mount(scene: Scene): void { this.scene = scene; }

  render(): void {
    if (!this.scene) return;
    this.scene.updateWorldMatrices();
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    this.scene.traverse(node => {
      const geo = node.getComponent<Geometry>(ComponentType.Geometry);
      if (!geo) return;
      const mat = node.getComponent<Material>(ComponentType.Material);
      const wm = node.transform.worldMatrix;

      this.ctx.save();
      this.ctx.translate(wm[12], wm[13]);

      if (geo.definition.type === GeometryPrimitive.Box) {
        const { width, height } = geo.definition;
        if (mat?.definition.color) {
          const c = mat.definition.color;
          this.ctx.fillStyle = `rgb(${c.r*255},${c.g*255},${c.b*255})`;
        }
        this.ctx.fillRect(-width/2, -height/2, width, height);
      }
      this.ctx.restore();
    });
  }

  dispose(): void { this.scene = null; }
}

Checklist

StepDescription
1Create package in packages/renderer-xxx/
2Add @joroya/core as dependency
3Implement mount() β€” traverse tree and create objects
4Implement render() β€” sync transforms and draw
5Implement dispose() β€” release resources
6Document supported geometries and materials