autumn-plugin-three
This plugin adds Three.js 3D scenes to Autumn apps. It works with Maud and htmx. It does not use npm, a bundler, or inline script. You write a scene in Rust or as HTML attributes. Scenes work in htmx partials.
- The crate contains Three.js 0.185.1 (MIT license). A
sha384hash locks each vendored file. - The crate needs
autumn-web0.9 and Rust 1.88 or later.
Quickstart
Add the plugin:
use ThreePlugin;
app
.plugin
.run
.await;
Put the tags in the layout <head>. Put them before your own module
scripts:
use ;
html!
Write a scene:
use ;
html!
Model viewer
use ;
fit(size) scales the model so that its largest side is size. It also
puts the model center at the model position. play_all() or
play("Walk") plays animation clips in a loop.
htmx
The runtime scans the page after each htmx swap. When htmx or a script removes a scene element, the runtime disposes the scene. This releases the GPU memory and the WebGL context.
async
Next shape
When a swap changes the declarations inside a scene element (for example,
hx-target is the scene), the runtime builds the scene again.
Your own JavaScript
Listen for three:ready. The event detail is the scene handle. Add the
listener in a module or defer script, so that it runs before
DOMContentLoaded. A script that runs later can read element.autumnThree.
document.;
Import Three.js only from the plugin URL. Then your code and the plugin use one Three.js instance:
import * as THREE from "/static/_plugins/three/three.module.min.js";
| Handle field | Meaning |
|---|---|
THREE |
The Three.js module. |
scene, camera, renderer |
The scene objects. |
root |
The group that holds meshes and models. The turntable turns it. |
controls |
OrbitControls, or null. |
models, mixers |
Model groups and animation mixers. |
render() |
Renders one frame now. |
requestRender() |
Renders one frame on the next animation frame. |
update() |
Checks for motion again. Call it after you add a mixer. |
looping |
true while the animation loop runs. |
Attribute reference
The builder writes these attributes. You can also write them by hand.
Angles use degrees. Speeds use degrees per second. Colors use #rrggbb or
#rgb.
If a value is not valid, the runtime uses the default. The runtime clamps numbers to their range. If a mesh kind, light kind, or model URL is not valid, the runtime ignores the object and logs a warning.
Scene (data-three="scene")
| Attribute | Values | Default |
|---|---|---|
data-three-aspect |
16/9, 4/3, 1/1, 21/9, 3/2, 3/4, or w/h (clamped to 0.1–10) |
16/9 |
data-three-background |
color | transparent |
data-three-camera |
camera position x,y,z |
0,1,4 |
data-three-target |
look-at point x,y,z |
0,0,0 |
data-three-fov |
degrees, 1–179 |
50 |
data-three-controls |
none, orbit, orbit-no-zoom |
none |
data-three-environment |
room |
none |
data-three-turntable |
degrees per second around Y | 0 |
data-three-reduced |
animate: keep motion for reduced-motion users |
— |
Put fallback content in a child <div data-three-fallback>. The browser
shows it when JavaScript or WebGL is not available, and when a model does
not load.
Mesh (data-three-mesh)
| Attribute | Values | Default |
|---|---|---|
data-three-mesh |
box, sphere, plane, torus, torus-knot, cylinder, cone, capsule, icosahedron, dodecahedron, octahedron, tetrahedron, ring |
— |
data-three-args |
sizes, comma-separated (see below) | per kind |
data-three-material |
standard, physical, basic, lambert, phong, normal |
standard |
data-three-color |
color | #ffffff |
data-three-emissive |
color | #000000 |
data-three-metalness |
0–1 |
0 |
data-three-roughness |
0–1 |
0.5 |
data-three-opacity |
0–1 |
1 |
data-three-wireframe |
true |
false |
Sizes: box w,h,d (1,1,1) · sphere r (0.5) · plane w,h (1,1) ·
torus r,tube (0.5,0.2) · torus-knot r,tube (0.5,0.15) · cylinder
top,bottom,h (0.5,0.5,1) · cone r,h (0.5,1) · capsule r,length
(0.3,0.6) · polyhedra r (0.5) · ring inner,outer (0.25,0.5).
Model (data-three-model)
| Attribute | Values | Default |
|---|---|---|
data-three-model |
.glb or .gltf URL (http(s) or relative) |
— |
data-three-fit |
largest side after scaling | no fit |
data-three-clip |
* (all clips) or a clip name |
none |
Mesh and model transform
| Attribute | Values | Default |
|---|---|---|
data-three-position |
x,y,z |
0,0,0 |
data-three-rotation |
degrees x,y,z |
0,0,0 |
data-three-scale |
x,y,z or one number |
1,1,1 |
data-three-spin |
degrees per second x,y,z |
0,0,0 |
Light (data-three-light)
| Attribute | Values | Default |
|---|---|---|
data-three-light |
ambient, directional, point, spot, hemisphere |
— |
data-three-color |
color (the sky color for hemisphere) |
#ffffff |
data-three-ground |
color, hemisphere only |
#444444 |
data-three-intensity |
0 or more |
per kind |
data-three-position |
x,y,z. For hemisphere, the sky direction. |
per kind |
If a scene has no light and no environment, the runtime adds a hemisphere light and a directional light.
Events and states
| Name | Meaning |
|---|---|
three:ready |
The scene is ready. detail is the handle. The event bubbles. |
three:error |
WebGL, the context, or a model failed. detail is { error, src }. The event bubbles. |
data-three-state |
loading, ready, error, or disposed. The runtime sets it. |
stateDiagram-v2
[*] --> loading: scan (load, htmx swap, DOM insert)
loading --> ready: built
loading --> error: no WebGL, or a model failed
ready --> error: WebGL context lost
ready --> disposed: element removed
ready --> loading: declarations changed
error --> loading: element inserted again, or declarations changed
disposed --> loading: element inserted again
A scene in the error state stays in that state. Other swaps do not
build it again.
Behavior
- Reduced motion. When the user prefers reduced motion, spin,
turntable, and clips stop. Orbit controls continue to work. To keep
motion in a scene, use
animate_reduced_motion(). - Performance. The animation loop runs only while the scene is visible and has motion. A static scene renders only when it changes. The runtime limits the pixel ratio to 2.
- Accessibility.
label("…")setsrole="img"andaria-label. The canvas hasaria-hidden="true".
Security and CSP
- The plugin works with the default Autumn CSP and in nonce mode
(
[security.headers.csp_nonce] enabled = true). It uses no inline script, no inline style, no import map, and noeval. - Images inside a GLB work with the default CSP. Safari 16 and older, and
Firefox 97 and older, load these images from
blob:URLs. For textured models in these browsers, addblob:toimg-src. - Do not let user content keep
data-three-*attributes. A sanitizer that keepsdata-*attributes lets user markup start scenes and load model URLs. - SRI covers
init.jsand the core modules (three_script()preloads them). The addons load from the same origin without SRI. See ADR 0001.
htmx adds an inline <style> for its indicators. Nonce mode blocks this
style. To stop the style, add this tag to the layout:
meta name="htmx-config" content=r#"{"includeIndicatorStyles":false}"#;
How it works
assets/holds the vendored files and the plugin files.THREE_ASSETSserves them under/static/_plugins/three/. The browser keeps hashed URLs in its cache. It checks plain URLs again at each page load.- Three.js is a set of ES modules. Module imports use plain URLs, so the page, the addons, and your code use one Three.js instance. See ADR 0001.
- The addons import the bare specifier
three.scripts/vendor.shmakes the imports relative. A test reverses the rewrites and checks the upstream hash. See ADR 0002. parse.jsreads the attributes.init.jsbuilds and disposes the scenes. See ADR 0003.
Demo
# Open http://127.0.0.1:3000
Tests
&&
The E2E tests read real canvas pixels. They also run in CSP nonce mode.
They measure strict line coverage of init.js (minimum 95 %).
Limits
- The plugin uses WebGL only. It does not support the WebGPU renderer.
- The declarative layer has no shadows, post-processing, or physics. Use
three:readyfor custom code. - Each scene has its own WebGL context. Browsers keep about 16 contexts. When the browser drops a context, the scene shows its fallback.
- The plugin loads only glTF 2.0 and GLB files. It does not decode Draco, KTX2, or Meshopt compression.
- External model and image URLs need a CSP that allows them.
- The runtime does not watch attribute changes on a live scene. Change the declarations, or swap the scene.
- To upgrade Three.js, release a new plugin version. Use
scripts/vendor.sh.
License
Apache-2.0 for the plugin. Three.js is MIT: see assets/THREE-LICENSE.