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.
Same operation chain on every surface:
search → doc → author .sk → validate → rendersearch → doc → author .sk → validate → create_sceneSearch topics, fetch scoped documents, author <sk-scene> markup, validate, then render. Never guess an element or attribute — doc it first.
| You are building | Reach for | Example |
|---|---|---|
| Shapes with colors/materials | primitives + scene-root materials bound via paint children | primitives-materials |
| Property motion over time | <sk-animation> + <sk-keyframe> inside the owning element | keyframes-easing |
| Frame/pose the view | scene-root <sk-camera-orbit-control fit-targets="auto"> + a camera | camera-fit |
| Motion derived from another node | constraints (orbit, target-at, follow, align-to-spline) | constraints |
| Many copies of a shape | <sk-cloner> + a points-source child | cloners |
| Spatially varying effects/masking | by-id field resources referenced from constraints/tonemaps | fields |
| Labels, callouts, HUD | annotations (live/overlay/rasterized) + spline connectors | annotations |
| Interaction / custom logic | lowercase on* handler attributes; <script> with standard DOM | scripting |
| 2D-only UI (spinner, progress) | do NOT use Skenra — CSS/SVG/Canvas2D are the right tools | — |
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:
| Scenario | Status | Write |
|---|---|---|
| Multi-touch gestures (pinch, swipe, rotate) | intentionally imperative | compose from pointer handlers (onpointerdown/onpointermove) |
| Multi-branch conditional state logic on events | intentionally imperative | branch in the handler body (onclick="…" is ordinary JS) |
| Runtime restructuring (reparent, transient groups, add/remove nodes) | intentionally imperative | standard DOM (createElement/append); attachChild() to reparent preserving world transform |
| Bespoke per-frame procedural logic | intentionally imperative | scene.onFrame (scene-time driven) — never a bare rAF loop |
Custom responses to dragging beyond the draggable attribute | intentionally imperative | the draggable attribute + drag event handlers |
| Wrong by instinct | What actually happens | Write instead |
|---|---|---|
Put <sk-animation> next to the thing that moves | keyframes for a property the host does not own do NOTHING | put 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 it | per-keyframe easing governs the segment STARTING at that keyframe | set 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 state | a persistent forwards fill pins the property against all later writes | for 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 loop | a standalone rAF loop fights the engine and breaks deterministic capture | declarative <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 inert | declare 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 black | reserve tonemaps for genuinely different shadow/light hues; rely on the built-in darken factor otherwise |
offset in milliseconds | offset is the normalized 0–1 fraction; offset="500" is invalid | offset="0.5" for halfway |
| a keyframe anywhere under the node | an <sk-keyframe> outside a direct <sk-animation> parent never runs | keyframes 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 scenes | give the scene an id and scope by it: sk-scene#my-id { --name: value } |
scene.add(mesh) and an injected scene variable | neither exists — the DOM IS the scene graph and scripts run as ordinary inline scripts | document.createElement('sk-…') + append; select with document.querySelector |
Fetch an example document before authoring an unfamiliar pattern; every example is a complete scene verified by the test suite.
guide:for-agents-example-primitives-materials — primitives, by-id materials, surface/edge paints, ground, lightguide:for-agents-example-keyframes-easing — multi-keyframe animation, per-keyframe easing, infinite iterationsguide:for-agents-example-camera-fit — auto-fit orbit control, azimuth/elevation pose, focal lengthguide:for-agents-example-constraints — orbit constraint, animated constraint property, target-atguide:for-agents-example-cloners — cloner, grid points-source, clone paints, whole-cloner animationguide:for-agents-example-fields — by-id field resource, field-weighted point constraint, field animationguide:for-agents-example-annotations — live annotation, world-space anchor, spline callout connectorguide:for-agents-example-scripting — dynamic node creation, IDL properties vs attributes, event handler