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
| Package | Description |
|---|---|
@joroya/core | Scene graph, nodes, components, animation, math, plugins, serialization |
@joroya/renderer-three | WebGL backend that translates Oroya scenes into Three.js objects (PBR, shadows, post-FX, audio, skinned meshes) |
@joroya/renderer-svg | SVG renderer for 2D vector graphics |
@joroya/renderer-canvas2d | Lightweight Canvas2D renderer |
@joroya/loader-gltf | glTF / GLB model loader with skinned-mesh support |
@joroya/physics | cannon-es-backed physics: rigid bodies, joints, sensors, raycast, vehicles |
Developer ecosystem
| Package | Description |
|---|---|
@joroya/inspector | Vanilla-DOM debug overlay: hierarchy, transform inspector, FPS / frame-time metrics |
@joroya/input | Keyboard / mouse / gamepad layer with declarative action mapping |
@joroya/assets | Asset cache with deduplication, ref-counting, progress events |
Framework wrappers (@public from 1.2)
| Package | Description |
|---|---|
@joroya/react | React bindings: <OroyaCanvas>, useFrame, useScene, JSX primitives |
@joroya/vue | Vue 3 composables: useOroyaCanvas, useFrame, useNode |
Peer dependencies:
@joroya/renderer-threeand@joroya/loader-gltfneedthree(npm install three).@joroya/physicsneedscannon-es(auto-installed).@joroya/reactneedsreact,react-dom, and the Three renderer stack.@joroya/vueneedsvueand the Three renderer stack.
Stability: untagged exports default to
@public. Public APIs deprecated in majorNremain available throughN+1and may be removed inN+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.
| Component | Description |
|---|---|
| Transform | Position (Vec3), rotation (Quat), and scale (Vec3). Call updateLocalMatrix() after mutating values. |
| Geometry | Defines the shape of a node. Created via factory functions. |
| Material | Defines appearance - color, opacity, fill/stroke (for SVG). |
| Camera | Defines a viewpoint - Perspective and Orthographic cameras. |
| Light | Ambient, directional, point, and spot lights for Three.js scenes. |
| Animator | Runtime clip playback, cross-fades, easing, and keyframe events. |
| RigidBody / Collider | Physics 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:
| Property | Type | Default | Description |
|---|---|---|---|
position | Vec3 { x, y, z } | { 0, 0, 0 } | Translation in 3D space |
rotation | Quat { x, y, z, w } | { 0, 0, 0, 1 } | Rotation as a quaternion |
scale | Vec3 { 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.
Traversal and Search
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
- Architecture - Understand the design philosophy and module boundaries.
- Renderers - Deep-dive into renderer implementations.
- Scene Graph - Learn how the node hierarchy and transforms work.
- Serialization - Full details on the JSON format.
- API Reference - Complete API documentation.
- Contributing - How to contribute to Oroya Animate.
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 });