Skip to content

Getting Started with Oroya Animate

Welcome! This guide will help you get your first 3D scene up and running using Oroya Animate, a renderer-agnostic scene graph engine for the web.

Oroya Animate separates your scene definition from the rendering backend. You describe your world once using @joroya/core and then choose how to render it: WebGL via Three.js, SVG for vector output, or Canvas2D for lightweight 2D drawing.


Installation

Install the packages you need from npm using your preferred package manager:

# npm
npm install @joroya/core @joroya/renderer-three three

# yarn
yarn add @joroya/core @joroya/renderer-three three

# pnpm
pnpm add @joroya/core @joroya/renderer-three three

Available packages (v1.0.0)

The library ships as 11 small, focused packages. Install only what you need.

Core engine

PackageDescription
@joroya/coreScene graph, nodes, components, animation, math, plugins, serialization
@joroya/renderer-threeWebGL backend that translates Oroya scenes into Three.js objects (PBR, shadows, post-FX, audio, skinned meshes)
@joroya/renderer-svgSVG renderer for 2D vector graphics
@joroya/renderer-canvas2dLightweight Canvas2D renderer
@joroya/loader-gltfglTF / GLB model loader with skinned-mesh support
@joroya/physicscannon-es-backed physics: rigid bodies, joints, sensors, raycast, vehicles

Developer ecosystem

PackageDescription
@joroya/inspectorVanilla-DOM debug overlay: hierarchy, transform inspector, FPS / frame-time metrics
@joroya/inputKeyboard / mouse / gamepad layer with declarative action mapping
@joroya/assetsAsset cache with deduplication, ref-counting, progress events

Framework wrappers (@public from 1.2)

PackageDescription
@joroya/reactReact bindings: <OroyaCanvas>, useFrame, useScene, JSX primitives
@joroya/vueVue 3 composables: useOroyaCanvas, useFrame, useNode

Peer dependencies: @joroya/renderer-three and @joroya/loader-gltf need three (npm install three). @joroya/physics needs cannon-es (auto-installed). @joroya/react needs react, react-dom, and the Three renderer stack. @joroya/vue needs vue and the Three renderer stack.

Stability: untagged exports default to @public. Public APIs deprecated in major N remain available through N+1 and may be removed in N+2. See API Stability.


The HTML below uses a bundler to load TypeScript and package imports. For a new project:

pnpm create vite oroya-demo --template vanilla-ts
cd oroya-demo
pnpm install
pnpm add @joroya/core @joroya/renderer-three three
pnpm dev

Put the scene code in src/main.ts and use /src/main.ts as the script URL in index.html.

Quick Start - Rotating Cube

This minimal example creates a scene with a camera and a red cube, then renders it with Three.js.

1. HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Oroya Animate - Quick Start</title>
  <style>
    body { margin: 0; overflow: hidden; }
    canvas { display: block; }
  </style>
</head>
<body>
  <canvas id="oroya-canvas"></canvas>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>

2. Scene Setup (main.ts)

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

const canvas = document.querySelector<HTMLCanvasElement>('#oroya-canvas');
if (!canvas) throw new Error('Canvas element not found');

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

const ambient = new Node('ambient');
ambient.addComponent(new Light({ type: LightType.Ambient, intensity: 0.6 }));
scene.add(ambient);

const sun = new Node('sun');
sun.addComponent(new Light({ type: LightType.Directional, intensity: 1.5 }));
sun.transform.position = { x: 3, y: 5, z: 4 };
scene.add(sun);

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

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

let previousTime: number | undefined;
let elapsed = 0;
let frameId = 0;
function frame(time: number) {
  const dt = previousTime === undefined ? 0 : (time - previousTime) / 1000;
  previousTime = time;
  elapsed += dt;
  cube.transform.rotation = setFromAxisAngle({ x: 0, y: 1, z: 0 }, elapsed * 0.5);
  cube.transform.updateLocalMatrix();
  renderer.render(dt);
  frameId = requestAnimationFrame(frame);
}
frameId = requestAnimationFrame(frame);

function stop() {
  cancelAnimationFrame(frameId);
  renderer.dispose();
}
window.addEventListener('pagehide', stop, { once: true });

dt is in seconds. render(dt) calls scene.update(dt) itself; do not call it twice in the same loop. Rotations are quaternions: use setFromAxisAngle() with a unit axis. Explicit lights are required for MeshStandardMaterial. Stop the animation loop before disposing the renderer. setSize() changes the render target size; it does not update the camera projection.

That’s it - you should see a red cube rotating on screen.


Key Concepts

Scene

The Scene is the top-level container. It holds a root Node and provides utility methods to add/remove nodes, traverse the graph, and update world matrices.

const scene = new Scene();
scene.add(someNode);                    // add to root
scene.add(childNode, parentNode);       // add under a specific parent
scene.remove(someNode);                 // remove from its parent
scene.findNodeByName('my-cube');        // search by name
scene.findNodeById(id);                 // search by unique ID
scene.traverse((node) => { /* ... */ });

Nodes

Nodes are the building blocks of the scene graph. They form a parent-child hierarchy where children inherit the world transform of their parents.

const parent = new Node('solar-system');
const earth  = new Node('earth');
const moon   = new Node('moon');

parent.add(earth);
earth.add(moon);   // moon inherits earth's transform

scene.add(parent);

Components

Components attach data or behavior to nodes. Every node gets a Transform component by default.

ComponentDescription
TransformPosition (Vec3), rotation (Quat), and scale (Vec3). Call updateLocalMatrix() after mutating values.
GeometryDefines the shape of a node. Created via factory functions.
MaterialDefines appearance - color, opacity, fill/stroke (for SVG).
CameraDefines a viewpoint - Perspective and Orthographic cameras.
LightAmbient, directional, point, and spot lights for Three.js scenes.
AnimatorRuntime clip playback, cross-fades, easing, and keyframe events.
RigidBody / ColliderPhysics data consumed by @joroya/physics.
// Adding components
node.addComponent(createBox(2, 2, 2));
node.addComponent(new Material({ color: { r: 0, g: 1, b: 0 }, opacity: 0.8 }));

// Querying components
node.hasComponent(ComponentType.Geometry); // true
const geo = node.getComponent<Geometry>(ComponentType.Geometry);

Geometry Primitives

Oroya provides factory functions for creating geometry components:

Box

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

const box = createBox(2, 1, 3); // width, height, depth
node.addComponent(box);

Sphere

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

const sphere = createSphere(0.5, 32, 32); // radius, widthSegments, heightSegments
node.addComponent(sphere);

Path2D (for SVG rendering)

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

const triangle = createPath2D([
  { command: 'M', args: [50, 0] },
  { command: 'L', args: [100, 100] },
  { command: 'L', args: [0, 100] },
  { command: 'Z', args: [] },
]);
node.addComponent(triangle);

Other built-in geometry helpers include createCylinder, createPlane, createCone, createText, createTorus, and createCircle. Buffer and CSG definitions use the Geometry component types directly.


Materials

The Material component controls the visual appearance of a node. Its properties adapt to the chosen renderer.

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

// For 3D (Three.js renderer)
node.addComponent(new Material({
  color: { r: 0.2, g: 0.5, b: 1.0 },
  opacity: 0.9,
}));

// For 2D (SVG renderer)
node.addComponent(new Material({
  fill: { r: 1, g: 0.8, b: 0 },
  stroke: { r: 0, g: 0, b: 0 },
  strokeWidth: 2,
}));

Colors use the ColorRGB format where each channel ranges from 0 to 1.


Camera

A camera defines the viewpoint from which the scene is rendered. Attach it to a node and position it using the node’s transform.

import { Camera, CameraType } from '@joroya/core';

const cameraNode = new Node('camera');
cameraNode.addComponent(new Camera({
  type: CameraType.Perspective,
  fov: 60,
  aspect: 16 / 9,
  near: 0.1,
  far: 500,
}));

// Position and orientation
cameraNode.transform.position = { x: 0, y: 3, z: 10 };
cameraNode.transform.updateLocalMatrix();

scene.add(cameraNode);

The ThreeRenderer uses the first camera it finds in the scene. If none is present, it creates a default perspective camera at z = 5.


Renderers

Three.js (WebGL)

Use @joroya/renderer-three for interactive, high-performance 3D scenes.

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

const renderer = new ThreeRenderer({
  canvas: document.getElementById('canvas') as HTMLCanvasElement,
  width: 800,
  height: 600,
  dpr: window.devicePixelRatio, // optional - defaults to devicePixelRatio
});

renderer.mount(scene);

function loop() {
  renderer.render();
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

// Clean up when done
renderer.dispose();

Add Light components to the scene for lit Three.js materials. If no camera exists, ThreeRenderer creates a default perspective camera at z = 5.

SVG

Use @joroya/renderer-svg for scalable 2D vector output - ideal for documentation, icons, or static illustrations.

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

const svgString = renderToSVG(scene, {
  width: 200,
  height: 200,
  viewBox: '0 0 200 200', // optional
});

document.getElementById('svg-container')!.innerHTML = svgString;

SVG supports Path2D, Box, Sphere, Circle, Plane, Cylinder, Cone, Torus, and Text geometries, with 3D-only dimensions flattened where appropriate.

Canvas2D

Use @joroya/renderer-canvas2d for lightweight 2D canvas output:

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 },
});

Scene Hierarchy Example

Transforms propagate through the tree. A child’s world matrix is the product of its parent’s world matrix and its own local matrix.

const solarSystem = new Node('solar-system');

// Sun at the origin
const sun = new Node('sun');
sun.addComponent(createSphere(2, 32, 32));
sun.addComponent(new Material({ color: { r: 1, g: 0.9, b: 0.2 } }));
solarSystem.add(sun);

// Earth orbits the sun
const earth = new Node('earth');
earth.addComponent(createSphere(0.5, 32, 32));
earth.addComponent(new Material({ color: { r: 0.2, g: 0.5, b: 1.0 } }));
earth.transform.position.x = 5;
solarSystem.add(earth);

// Moon orbits the earth
const moon = new Node('moon');
moon.addComponent(createSphere(0.15, 16, 16));
moon.addComponent(new Material({ color: { r: 0.8, g: 0.8, b: 0.8 } }));
moon.transform.position.x = 1;
earth.add(moon);

scene.add(solarSystem);

// In the animation loop, rotate the system
function animate(time: number) {
  time *= 0.001;
  requestAnimationFrame(animate);

  solarSystem.transform.rotation = setFromAxisAngle({ x: 0, y: 1, z: 0 }, time * 0.1);
  solarSystem.transform.updateLocalMatrix();

  earth.transform.rotation = setFromAxisAngle({ x: 0, y: 1, z: 0 }, time * 0.5);
  earth.transform.updateLocalMatrix();

  renderer.render();
}
requestAnimationFrame(animate);

Loading glTF Models

The @joroya/loader-gltf package lets you load standard glTF/GLB files and convert them into Oroya scene nodes.

npm install @joroya/loader-gltf three
import { Animator, Light, LightType, Node } from '@joroya/core';
import { loadGLTF } from '@joroya/loader-gltf';
import { ThreeRenderer } from '@joroya/renderer-three';

const result = await loadGLTF('/models/robot.glb');
const animationNode = new Node('model-animation');
const animator = new Animator();
for (const clip of result.animations) animator.addClip(clip);
animationNode.addComponent(animator);
result.scene.add(animationNode);
animator.bindToScene(result.scene);

const ambient = new Node('ambient');
ambient.addComponent(new Light({ type: LightType.Ambient, intensity: 1 }));
result.scene.add(ambient);

const canvas = document.querySelector<HTMLCanvasElement>('#oroya-canvas');
if (!canvas) throw new Error('Canvas element not found');
const renderer = new ThreeRenderer({ canvas, width: 800, height: 600 });
renderer.mount(result.scene);
if (result.animations[0]) animator.play(result.animations[0].name);

let previousTime: number | undefined;
function loop(time: number) {
  const dt = previousTime === undefined ? 0 : (time - previousTime) / 1000;
  previousTime = time;
  renderer.render(dt);
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

loadGLTF() returns { scene, animations }. Imported scenes may need a camera and lights. For clip playback, attach an Animator to a child node, bind it to the scene, register the clips, and call play(). When merging scenes, iterate over [...result.scene.root.children]; reparenting modifies the original child list.


Serialization

Oroya scenes can be serialized to JSON and deserialized back, making it easy to save/load scenes or transmit them over the network.

import { serialize, deserialize } from '@joroya/core';

// Save scene to JSON
const json = serialize(scene);
localStorage.setItem('my-scene', json);

// Load scene from JSON
const restored = deserialize(localStorage.getItem('my-scene')!);
renderer.mount(restored);

React Integration

For React apps, install the stable @joroya/react package:

import { OroyaCanvas, AmbientLight, DirectionalLight, Box } from '@joroya/react';

function App() {
  return (
    <OroyaCanvas width="100vw" height="100vh">
      <AmbientLight intensity={0.5} />
      <DirectionalLight position={{ x: 3, y: 5, z: 4 }} intensity={1.2} />
      <Box color={{ r: 0.1, g: 0.6, b: 0.9 }} />
    </OroyaCanvas>
  );
}

You can still mount ThreeRenderer manually if you need full control over the render loop or canvas lifecycle.


Transform in Depth

Every Node has a Transform component with three properties:

PropertyTypeDefaultDescription
positionVec3 { x, y, z }{ 0, 0, 0 }Translation in 3D space
rotationQuat { x, y, z, w }{ 0, 0, 0, 1 }Rotation as a quaternion
scaleVec3 { x, y, z }{ 1, 1, 1 }Scale factors

Important: After modifying any transform property, call updateLocalMatrix() to recompute the local matrix. The renderer calls scene.updateWorldMatrices() each frame to propagate transforms down the hierarchy.

const node = new Node('box');
node.transform.position = { x: 2, y: 0, z: -3 };
node.transform.scale = { x: 2, y: 2, z: 2 };
node.transform.updateLocalMatrix();

Direct writes to position, rotation, or scale do not set isDirty. Call updateLocalMatrix() afterwards, or set isDirty = true before the next world-matrix update. rotateOnAxis() and lookAt() mark the transform dirty themselves.


Oroya provides multiple ways to navigate the scene graph:

// Traverse all nodes depth-first
scene.traverse((node) => {
  console.log(node.name, node.id);
});

// Find a specific node
const cam = scene.findNodeByName('main-camera');
const node = scene.findNodeById('some-uuid');

// Access the hierarchy
const parent = node.parent;           // Node | null
const children = node.children;       // Node[]

Next Steps

Explicit transform updates

Use setPosition(value: Vec3), setRotation(value: Quat) and setScale(value: Vec3). Each copies its input, returns void and marks isDirty. Direct property writes retain v1 behavior: call updateLocalMatrix() or set isDirty = true.

node.transform.setPosition({ x: 2, y: 0, z: -3 });
node.transform.setScale({ x: 2, y: 2, z: 2 });
node.transform.setRotation({ x: 0, y: 0, z: 0, w: 1 });