Skenra for agents

← prev index next →

Skenra is a WebGPU 3D framework authored as HTML: <sk-scene> and its children ARE the scene graph. Write declarative markup first; script is the escape hatch, not the default. Read the reflex table before writing your first scene — it catches the mistakes agents make by instinct.

Workflow

Same operation chain on every surface:

Search topics, fetch scoped documents, author <sk-scene> markup, validate, then render. Never guess an element or attribute — doc it first.

Declarative-first decision tree

You are buildingReach forExample
Shapes with colors/materialsprimitives + scene-root materials bound via paint childrenprimitives-materials
Property motion over time<sk-animation> + <sk-keyframe> inside the owning elementkeyframes-easing
Frame/pose the viewscene-root <sk-camera-orbit-control fit-targets="auto"> + a cameracamera-fit
Motion derived from another nodeconstraints (orbit, target-at, follow, align-to-spline)constraints
Many copies of a shape<sk-cloner> + a points-source childcloners
Spatially varying effects/maskingby-id field resources referenced from constraints/tonemapsfields
Labels, callouts, HUDannotations (live/overlay/rasterized) + spline connectorsannotations
Interaction / custom logiclowercase on* handler attributes; <script> with standard DOMscripting
2D-only UI (spinner, progress)do NOT use Skenra — CSS/SVG/Canvas2D are the right tools

Script escape hatches

Everything in the tree above is declarative today — including spatial fields (noise, random, voronoi), composite fields, radial-push point constraints, and dragging via the draggable attribute. Script is the right tool ONLY for:

ScenarioStatusWrite
Multi-touch gestures (pinch, swipe, rotate)intentionally imperativecompose from pointer handlers (onpointerdown/onpointermove)
Multi-branch conditional state logic on eventsintentionally imperativebranch in the handler body (onclick="…" is ordinary JS)
Runtime restructuring (reparent, transient groups, add/remove nodes)intentionally imperativestandard DOM (createElement/append); attachChild() to reparent preserving world transform
Bespoke per-frame procedural logicintentionally imperativescene.onFrame (scene-time driven) — never a bare rAF loop
Custom responses to dragging beyond the draggable attributeintentionally imperativethe draggable attribute + drag event handlers

Reflex table

Wrong by instinctWhat actually happensWrite instead
Put <sk-animation> next to the thing that moveskeyframes for a property the host does not own do NOTHINGput the animation inside the element that OWNS the property: constraint props in the constraint, material colors in the material, stroke props in the stroke-paint, transforms on the node
easing on a keyframe eases INTO itper-keyframe easing governs the segment STARTING at that keyframeset easing on the segment's starting keyframe
bare numbers everywhere (h="90", duration="2000")unitless angle/time attributes are invalid and IGNORED (the property keeps its default).sk attributes take CSS units (h="90deg", duration="2s"); IDL properties take canonical numbers (node.h is radians)
fill="forwards" to keep an end statea persistent forwards fill pins the property against all later writesfor a one-shot, commit the end state: onfinish="this.commitStyles()" (lowercase handler form); replay/cancel are separate topics — doc them
drive scene mutation with a requestAnimationFrame loopa standalone rAF loop fights the engine and breaks deterministic capturedeclarative <sk-animation>/constraints first; animate() for imperative WAAPI; scene.onFrame only for custom scene-time per-frame work
inline a material inside the mesh (mesh.material = …)materials inlined into geometry are rejected; a paint without material is marked inertdeclare materials with an id under <sk-scene>; bind from a paint child: <sk-surface-paint material="#id">
add a same-hue dark→light tonemap "for shading"base-color-darken-factor already shades base→dark; stacking a same-hue tonemap double-darkens toward blackreserve tonemaps for genuinely different shadow/light hues; rely on the built-in darken factor otherwise
offset in millisecondsoffset is the normalized 0–1 fraction; offset="500" is invalidoffset="0.5" for halfway
a keyframe anywhere under the nodean <sk-keyframe> outside a direct <sk-animation> parent never runskeyframes are DIRECT children of <sk-animation>
:host or bare sk-scene selectors for scene config:host is ignored; bare sk-scene is document-global and leaks config across scenesgive the scene an id and scope by it: sk-scene#my-id { --name: value }
scene.add(mesh) and an injected scene variableneither exists — the DOM IS the scene graph and scripts run as ordinary inline scriptsdocument.createElement('sk-…') + append; select with document.querySelector

Verified examples

Fetch an example document before authoring an unfamiliar pattern; every example is a complete scene verified by the test suite.