Scene Graph
The scene graph is a tree of Nodes. Container adds children; Group, Layer, and Stage are the concrete containers, with Stage as the root and engine entry point.
abstract class Node — base of every scene-graph element. Owns the local transform, visual state, hierarchy linkage, and the lazy, version-counted world-matrix cache. It knows nothing about rendering (there is no draw()).
NodeConfig
Section titled “NodeConfig”Optional constructor props (transform + visual + identity).
interface NodeConfig { x?: number; y?: number scaleX?: number; scaleY?: number rotation?: number skewX?: number; skewY?: number offsetX?: number; offsetY?: number opacity?: number visible?: boolean listening?: boolean id?: string name?: string}Properties
Section titled “Properties”Each is a real typed getter/setter (not a stringly-typed attrs bag); a transform setter only invalidates when the value actually changes.
// transform (get/set, all numbers)x y scaleX scaleY rotation skewX skewY offsetX offsetY// visualopacity: numbervisible: booleanlistening: boolean // reserved for the event system; does not affect rendering// identity / hierarchyparent // managed by the container — attach via add(), detach via remove()id: stringname: stringtype: string // read-only discriminantMethods
Section titled “Methods”position(x: number, y: number) // set x and y togethermove(dx: number, dy: number) // translate by a deltalocalMatrix(): Matrix // cached local transform (Matrix.compose)worldMatrix(): Matrix // lazy, version-counted world transformgetLocalBounds(): Bounds // abstract — bounds in local spacegetWorldBounds(): Bounds // bounds in world spacemarkDirty() // flag a visual/transform change; walks to the rootremove() // detach from parentdestroy() // tear downtoObject() // serialize to a plain objectEvents
Section titled “Events”on(type, handler, options?) // register a listener; options: { capture }once(type, handler, options?) // one-shot listeneroff(type, handler?) // remove a listener (or all for the type)hasListeners(type): boolean // any listeners for the type/phase?const group = new Group({ x: 100 })const child = new TestRect({ x: 10 })group.add(child)child.worldMatrix().applyToPoint({ x: 0, y: 0 }) // { x: 110, y: 0 }group.x = 200child.worldMatrix().applyToPoint({ x: 0, y: 0 }) // { x: 210, y: 0 } (recomputed lazily)The world matrix is cached against a version counter, so changing one node never eagerly walks its subtree — a descendant recomputes only when its own transform changes or an ancestor’s world matrix actually moves. See Matrix and Bounds.
Container
Section titled “Container”abstract class Container extends Node — branch node holding children, z-order, traversal, and a derived bounds union. Concrete containers: Group, Layer, Stage.
Children
Section titled “Children”children // readonly view — mutate via the methods, not the arraychildCount: numberadd(...nodes) // attach; re-parents and throws on cyclesremoveChild(node)removeChildren()getChildIndex(node): numberZ-order
Section titled “Z-order”Child array order is paint order (earlier = drawn first = visually behind).
moveToTop(node)moveToBottom(node)moveUp(node) // swap with the next neighbormoveDown(node) // swap with the previous neighborTraversal
Section titled “Traversal”All depth-first, in child order.
find(predicate) // first matching descendanttraverse(visitor) // visit each descendantgetDescendants() // flat DFS listisAncestorOf(node): booleangetLocalBounds(): Bounds // overrides Node — union of child boundsconst g = new Group()g.add(a, b, c)g.moveToTop(a) // a now paints last (front)g.traverse((n) => console.log(n.type))g.getLocalBounds() // tight box around a, b, cclass Group extends Container — type = 'Group'. The everyday grouping primitive: transform it and its children follow. Adds nothing beyond Container except a concrete type.
new Group(config?: NodeConfig)const g = new Group({ x: 50, rotation: 15 })g.add(rect, label) // both move/rotate with the groupUse a Group for logical grouping anywhere in the tree; reach for a Layer only for top-level render partitions.
class Layer extends Container — type = 'Layer'. A top-level partition directly under the Stage; the Stage’s direct children must be Layers. Create via stage.createLayer().
new Layer(config?: NodeConfig)const layer = stage.createLayer()layer.add(rect, group)Today every layer renders to the stage’s single canvas in order; the per-layer offscreen canvas is a later, opt-in caching optimization. Use layers sparingly — they are partitions, not the grouping mechanism.
class Stage extends Container — root of the scene graph and engine entry point. Owns the renderer (defaults to the Canvas2D renderer), the frame scheduler, the camera, and the viewport. Turns “a property changed” into “one frame rendered.”
StageOptions
Section titled “StageOptions”interface StageOptions { container: HTMLElement width?: number height?: number pixelRatio?: number background?: string renderer?: Renderer camera?: Camera hitTester?: HitTester}Overlay
Section titled “Overlay”interface Overlay { drawOps(): DrawOp[] // a screen-space overlay drawn after the scene}Getters
Section titled “Getters”get width(): numberget height(): numberget pixelRatio(): numberget canvas(): HTMLCanvasElement | undefined // reflects the renderer's canvas (may be undefined when injected)get camera(): CameraCoordinates
Section titled “Coordinates”Delegate to the camera.
screenToWorld(point): Vec2worldToScreen(point): Vec2Hit testing
Section titled “Hit testing”Zoom-aware, options-driven. See hit testing.
hitTest(worldPoint, options?): HitResult | nullgetIntersection(worldPoint, options?): Node | null // convenience — just the nodeTree & layers
Section titled “Tree & layers”add(...layers) // Layer-only; throws TypeError on a non-Layer childcreateLayer(config?): Layer // make and attach a layer in one callViewport & rendering
Section titled “Viewport & rendering”setSize(w: number, h: number)setPixelRatio(dpr: number)requestRender() // coalesced (async) — the normal pathrender() // synchronous; for explicit needs and testsOverlays & teardown
Section titled “Overlays & teardown”addOverlay(overlay: Overlay)removeOverlay(overlay: Overlay)destroy() // cancel scheduler → remove children → destroy rendererimport { Stage } from '@veyrajs/core'
const stage = new Stage({ container: el, width: 800, height: 480, background: '#0b1220' })const layer = stage.createLayer()layer.add(myShape) // schedules a coalesced render automatically// ...stage.destroy()You don’t normally call render() — mutating properties schedules a coalesced frame. The camera is applied at render time as screen = view · world, so world coordinates stay camera-independent.