Skip to content

Architecture Overview

Oroya Animate follows a decoupled architecture where the scene representation is completely independent of the rendering technology.


Core Principle

β€œDefine once, render anywhere.” The scene graph is the single source of truth. Renderers are translators.

graph TD
    subgraph "Input Layer"
        UC["User Code"]
        GLTF["glTF / GLB Files"]
        JSON["Serialized JSON"]
    end

    subgraph "@joroya/core β€” Engine-agnostic core"
        SG["Scene Graph"]
        N["Node"]
        T["Transform"]
        G["Geometry"]
        M["Material"]
        C["Camera"]
        SER["Serializer"]
        MATH["Math (Matrix4)"]
    end

    subgraph "Output Layer"
        R3["@joroya/renderer-three"]
        RS["@joroya/renderer-svg"]
        RC["@joroya/renderer-canvas2d"]
        R_FUTURE["Future: WebGPU..."]
    end

    subgraph "Result"
        WEBGL["WebGL Canvas (pixels)"]
        SVG["SVG String (vectors)"]
        CANVAS["Canvas2D Canvas (pixels)"]
    end

    UC -->|"builds"| SG
    GLTF -->|"@joroya/loader-gltf"| SG
    JSON -->|"deserialize()"| SG
    SG -->|"serialize()"| JSON

    SG --> N
    N --> T
    N --> G
    N --> M
    N --> C
    SG --> MATH

    SG -->|"mount + render"| R3
    SG -->|"renderToSVG()"| RS
    SG -->|"renderToCanvas()"| RC
    SG -.->|"future"| R_FUTURE

    R3 --> WEBGL
    RS --> SVG
    RC --> CANVAS

Architecture Layers

LayerPackageResponsibilityDependencies
Core@joroya/coreScene graph, components, transforms, serialization, mathuuid (only dependency)
3D Renderer@joroya/renderer-threeTranslation to Three.js WebGL@joroya/core, three
SVG Renderer@joroya/renderer-svgPure SVG generation@joroya/core
Canvas2D Renderer@joroya/renderer-canvas2dBrowser-native Canvas2D drawing@joroya/core
glTF Loader@joroya/loader-gltf3D model importing@joroya/core, three
Physics@joroya/physicscannon-es rigid bodies, joints, sensors, raycasts, vehicles@joroya/core, cannon-es
Tooling@joroya/inspector, @joroya/input, @joroya/assetsDebug overlay, input mapping, asset cache@joroya/core
Frameworks@joroya/react, @joroya/vueExperimental UI bindings@joroya/core, @joroya/renderer-three, framework peer

Dependency Graph

graph BT
    CORE["@joroya/core"]
    R3["@joroya/renderer-three"]
    RS["@joroya/renderer-svg"]
    RC["@joroya/renderer-canvas2d"]
    LG["@joroya/loader-gltf"]
    PHYS["@joroya/physics"]
    TOOLS["@joroya/inspector/input/assets"]
    FW["@joroya/react/vue"]
    THREE["three (npm)"]
    CANNON["cannon-es (npm)"]
    UUID["uuid (npm)"]
    DEMO["apps/demo-react"]

    CORE -->|"depends on"| UUID
    R3 -->|"depends on"| CORE
    R3 -->|"depends on"| THREE
    RS -->|"depends on"| CORE
    RC -->|"depends on"| CORE
    LG -->|"depends on"| CORE
    LG -->|"depends on"| THREE
    PHYS -->|"depends on"| CORE
    PHYS -->|"depends on"| CANNON
    TOOLS -->|"depends on"| CORE
    FW -->|"depends on"| CORE
    FW -->|"depends on"| R3
    DEMO -->|"depends on"| CORE
    DEMO -->|"depends on"| R3

Key rule: Dependency arrows are unidirectional and always point towards @joroya/core. The core never imports from renderers or loaders.


The β€œCompiler” Pattern

Renderers work like compilers: they translate an intermediate representation (the scene graph) into a specific output format.

flowchart LR
    IR["Scene Graph\n(Intermediate representation)"] -->|"ThreeRenderer"| OUT1["THREE.Scene\nTHREE.Mesh\nTHREE.Camera"]
    IR -->|"renderToSVG()"| OUT2["<svg>\n  <path/>\n</svg>"]
    IR -->|"renderToCanvas()"| OUT3["CanvasRenderingContext2D"]
    IR -.->|"Future: WebGPU"| OUT4["GPUBuffer\nGPURenderPipeline"]
ConceptClassic CompilerOroya Animate
Source code.c fileUser Code (TypeScript)
Intermediate representationAST / IRScene Graph
Backendx86, ARM, WASMThree.js, SVG, WebGPU
OutputMachine codePixels, vectors

This pattern enables:

  • Adding backends without modifying the core.
  • Testing without a renderer β€” logic lives in the scene graph.
  • Server-side rendering β€” the SVG renderer works in Node.js without DOM.

Rendering Lifecycle

sequenceDiagram
    participant U as User Code
    participant S as Scene
    participant N as Node
    participant T as Transform
    participant R as Renderer

    Note over U,R: PHASE 1 β€” Preparation
    U->>S: new Scene()
    U->>N: new Node('box')
    U->>N: addComponent(createBox(...))
    U->>N: addComponent(new Material(...))
    U->>S: scene.add(node)

    Note over U,R: PHASE 2 β€” Mounting
    U->>R: renderer.mount(scene)
    R->>S: scene.traverse(callback)
    R->>R: Create backend objects
    R->>R: Detect active camera

    Note over U,R: PHASE 3 β€” Render loop
    loop requestAnimationFrame
        U->>T: transform.rotation = {...}
        U->>T: transform.updateLocalMatrix()
        U->>R: renderer.render()
        R->>S: scene.updateWorldMatrices()
        S->>N: node.updateWorldMatrix(parentMatrix)
        N->>T: worldMatrix = parent Γ— local
        R->>R: Sync with backend
        R->>R: Draw frame
    end

    Note over U,R: PHASE 4 β€” Cleanup
    U->>R: renderer.dispose()

Phase Details

PhaseActionExecuted by
1. PreparationBuild the scene graph with nodes, components, and parent-child relationshipsUser code
2. Mountingrenderer.mount(scene) β€” traverse the tree and create backend objectsRenderer
3. Render loopMutate transforms β†’ updateLocalMatrix() β†’ renderer.render() β†’ propagate matrices β†’ drawUser code + Renderer
4. Cleanuprenderer.dispose() β€” release GPU/memory resourcesUser code

Simplified Entity-Component System (ECS)

Oroya uses a lightweight ECS where:

ECS TermOroya EquivalentDescription
EntityNodeContainer with ID and hierarchy
ComponentTransform, Geometry, Material, CameraData attached to a node
SystemRenderers, updateWorldMatrices()Logic that processes components
graph LR
    subgraph "Entity (Node)"
        N["Node 'player'"]
    end

    subgraph "Components"
        T["Transform\nposition, rotation, scale"]
        G["Geometry\nBox 1Γ—1Γ—1"]
        M["Material\ncolor: blue"]
    end

    subgraph "Systems"
        S1["updateWorldMatrices()\nPropagates matrices"]
        S2["ThreeRenderer.render()\nDraws with Three.js"]
    end

    N --> T
    N --> G
    N --> M
    T --> S1
    G --> S2
    M --> S2

ECS Rules

  1. One component per type per node β€” You cannot have two Geometry components on the same node.
  2. Transform is automatic β€” All nodes have it from creation.
  3. Components are data β€” They contain no rendering logic.
  4. Renderers are the β€œsystems” β€” They read components and produce visual output.

Extensibility

Adding a new renderer

class MyRenderer {
  private scene: Scene | null = null;

  mount(scene: Scene): void {
    this.scene = scene;
    scene.traverse(node => {
      const geo = node.getComponent<Geometry>(ComponentType.Geometry);
      const mat = node.getComponent<Material>(ComponentType.Material);
      // Create objects for the target engine/framework
    });
  }

  render(): void {
    if (!this.scene) return;
    this.scene.updateWorldMatrices();
    this.scene.traverse(node => {
      // Read node.transform.worldMatrix
      // Sync with engine objects
    });
    // Draw frame
  }

  dispose(): void { /* release resources */ }
}

Adding a new loader

async function loadMyFormat(url: string): Promise<Scene> {
  const data = await fetch(url).then(r => r.json());
  const scene = new Scene();

  for (const obj of data.objects) {
    const node = new Node(obj.name);
    node.addComponent(createBox(obj.width, obj.height, obj.depth));
    node.addComponent(new Material({ color: obj.color }));
    node.transform.position = obj.position;
    scene.add(node);
  }

  return scene;
}

Monorepo Structure

oroya-animate/
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ core/               β†’ Engine-agnostic core (Scene, Node, Components, Math)
β”‚   β”œβ”€β”€ renderer-three/      β†’ Three.js WebGL backend
β”‚   β”œβ”€β”€ renderer-svg/        β†’ Pure SVG backend
β”‚   └── loader-gltf/         β†’ glTF model importer
β”œβ”€β”€ apps/
β”‚   └── demo-react/          β†’ Demo application (Vite + React)
β”œβ”€β”€ docs/                    β†’ Project documentation
└── package.json             β†’ Monorepo root (pnpm workspaces)

Design Decisions

DecisionRejected AlternativeReason
Scene graph as IRDirect API over Three.jsEnables multiple backends and GPU-free testing
Simplified ECS (1 comp/type)Full ECS with SystemsLower complexity for current scope
Quaternions for rotationEuler anglesNo gimbal lock, natural interpolation (SLERP)
Column-major matricesRow-major matricesCompatible with WebGL and Three.js
uuid for node IDsIncremental IDsGlobally unique IDs, required for serialization
tsup as bundlertsc, rollup, esbuildDTS + CJS + ESM in a single tool with minimal config
pnpm workspacesnpm/yarn workspacesFaster, strict deduplication, better for monorepos