Skip to content

Serialization

Oroya Animate includes a JSON serialization system that allows saving and loading scene definitions. It is the foundation for visual editors, collaboration, and scene versioning.


Table of contents


Serialize a scene

The serialize function converts the entire scene graph to a formatted JSON string:

import { Scene, Node, createBox, Material, serialize } from '@joroya/core';

const scene = new Scene();

const cube = new Node('hero-cube');
cube.addComponent(createBox(2, 2, 2));
cube.addComponent(new Material({ color: { r: 1, g: 0.5, b: 0 } }));
cube.transform.position = { x: 3, y: 0, z: -1 };
scene.add(cube);

const json = serialize(scene);
console.log(json);

Serialization flow

flowchart LR
    S["Scene"] -->|"serialize()"| SR["serializeNode(root)"]
    SR -->|"recursive"| SN["For each node:\nid, name, components, children"]
    SN -->|"adapter"| SC["Declarative component data"]
    SC --> JSON["JSON.stringify()\nwith indent 2"]
    JSON --> STR["string"]

Deserialize a scene

The deserialize function reconstructs a functional Scene from a JSON string:

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

const restoredScene = deserialize(json);

const found = restoredScene.findNodeByName('hero-cube');
console.log(found?.transform.position); // { x: 3, y: 0, z: -1 }

The restored scene is fully functional and can be mounted on any renderer:

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

const renderer = new ThreeRenderer({ canvas, width, height });
renderer.mount(restoredScene);
renderer.render();

Deserialization flow

flowchart LR
    STR["JSON string"] -->|"JSON.parse()"| OBJ["SerializableScene"]
    OBJ -->|"deserializeNode()"| RN["Rebuild nodes\nrecursively"]
    RN -->|"switch(type)"| COMP["Recreate components:\nTransform, Geometry, Material,\nCamera, Animation"]
    COMP --> SCENE["new Scene()"]
    RN -->|"children"| SCENE

JSON format

General structure

graph TD
    SS["SerializableScene"] -->|"root"| SN["SerializableNode"]
    SN -->|"id"| ID["string (UUID)"]
    SN -->|"name"| NAME["string"]
    SN -->|"components"| COMPS["SerializableComponent[]"]
    SN -->|"children"| CHILDREN["SerializableNode[]"]
    CHILDREN -->|"recursive"| SN
    COMPS --> SC["{ type: ComponentType, ...data }"]

Complete output example

{
  "root": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "root",
    "components": [
      {
        "type": "Transform",
        "position": { "x": 0, "y": 0, "z": 0 },
        "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
        "scale": { "x": 1, "y": 1, "z": 1 },
        "localMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1],
        "worldMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1],
        "isDirty": true
      }
    ],
    "children": [
      {
        "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
        "name": "hero-cube",
        "components": [
          {
            "type": "Transform",
            "position": { "x": 3, "y": 0, "z": -1 },
            "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
            "scale": { "x": 1, "y": 1, "z": 1 },
            "localMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 3,0,-1,1],
            "worldMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 3,0,-1,1],
            "isDirty": false
          },
          {
            "type": "Geometry",
            "definition": {
              "type": "Box",
              "width": 2,
              "height": 2,
              "depth": 2
            }
          },
          {
            "type": "Material",
            "definition": {
              "color": { "r": 1, "g": 0.5, "b": 0 }
            }
          }
        ],
        "children": []
      }
    ]
  }
}

Internal interfaces

InterfaceFieldsDescription
SerializableSceneroot: SerializableNodeTop-level container
SerializableNodeid, name, cssClass?, cssId?, components[], children[]Flat representation of a node
SerializableComponenttype: ComponentType, + ...dataEach component with its type and data

Supported components

serialize() uses typed internal adapters to save declarative definitions, TRS values and SVG animations. It excludes runtime state, callbacks, node references and backend instances, including references nested in event data. Bound Animator components can be saved safely.

ComponentDeserialization
TransformRestores position, quaternion and scale; cached matrices are ignored
Geometry, Material, Camera, Interactive, Light, EnvironmentReconstructs from definition
RigidBody, Collider, PostProcessing, ParticleSystemReconstructs definitions; backend runtime is recreated separately
Animator, AudioListener, AudioSource, SkinReconstructs definitions; playback and loaded resources are not a saved session
AnimationReconstructs from animations
InstancedMesh, Script, unknown component typesSkipped; restore through application adapters

Typed arrays

Float32Array, Uint8Array, Uint16Array, and Uint32Array are encoded as { __typedArray, base64 } and restored to their original array type. This supports animation tracks and buffer geometry. It does not make an InstancedMesh component restorable. Other typed-array classes are not supported by this encoding.

import { typedArrayReplacer, typedArrayReviver } from '@joroya/core';

const encoded = JSON.stringify({ values: new Float32Array([0, 1, 2]) }, typedArrayReplacer);
const decoded = JSON.parse(encoded, typedArrayReviver);
console.log(decoded.values instanceof Float32Array); // true

What is preserved

Node IDs (including the root), names, parent-child relationships, transforms, cssClass, cssId, and supported component definitions are preserved. component.node is reattached by addComponent(). World matrices are recomputed on rendering.

The saved root is reconstructed with its ID, name, CSS metadata, transform and supported components. Scene(root?: Node) accepts a parentless root; the no-argument constructor remains supported. Legacy JSON without a Transform receives the default Transform.


Use cases

localStorage persistence

function saveScene(scene: Scene): void {
  const json = serialize(scene);
  localStorage.setItem('oroya-scene', json);
}

function loadScene(): Scene | null {
  const json = localStorage.getItem('oroya-scene');
  if (!json) return null;
  return deserialize(json);
}

Export as downloadable file

function downloadScene(scene: Scene, filename: string): void {
  const json = serialize(scene);
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();

  URL.revokeObjectURL(url);
}

downloadScene(scene, 'my-scene.json');

Import from file

async function importScene(file: File): Promise<Scene> {
  const text = await file.text();
  return deserialize(text);
}

// With input[type=file]
input.addEventListener('change', async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (file) {
    const scene = await importScene(file);
    renderer.mount(scene);
  }
});

Server-side rendering

// Receive serialized scene, render to SVG on server
import { deserialize } from '@joroya/core';
import { renderToSVG } from '@joroya/renderer-svg';

function handleRequest(jsonBody: string): string {
  const scene = deserialize(jsonBody);
  return renderToSVG(scene, { width: 800, height: 600, dt: 0 });
}

Git versioning

Saving scenes as .json allows:

scenes/
β”œβ”€β”€ level-01.json    β†’ Versioned with git
β”œβ”€β”€ level-02.json
└── hub-world.json

Git diffs show exactly what changed:

 "name": "hero-cube",
 "components": [
   {
     "type": "Transform",
-    "position": { "x": 3, "y": 0, "z": -1 },
+    "position": { "x": 5, "y": 2, "z": -1 },

Limitations

LimitationImpact and workaround
Runtime callbacksonUpdate and event handlers are not persisted; register them again after loading
Instancing and custom componentsInstancedMesh, Script, and unknown types are skipped by the reviver; restore with an adapter
IDs are preserved on all nodesImporting the same scene twice can create duplicate IDs; create new nodes for independent copies (id is readonly)
Schema validationvalidateSceneJSON() checks structure, IDs, numeric definitions and supported buffers before construction; errors throw SceneValidationError
External resourcesJSON stores resource URIs and definitions, not texture/audio/model files or live physics state

Internal adapters save transforms, component definitions and SVG animations. Bound, playing and paused Animator components are safe to serialize; mixers, callbacks, node references and backend resources are excluded. Clip definitions survive loading, but the exact playback instant is not restored.

1.2

Capabilities, diagnostics and acceptance describe the current rendering and persistence contracts.