animader-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +121 -0
- package/adapters/build-node.mjs +265 -0
- package/adapters/devserver.mjs +481 -0
- package/adapters/devsession.mjs +619 -0
- package/adapters/export-node.mjs +603 -0
- package/adapters/export-run.mjs +126 -0
- package/adapters/generations.mjs +183 -0
- package/adapters/http.mjs +294 -0
- package/adapters/launch.mjs +83 -0
- package/adapters/node-fs.mjs +80 -0
- package/adapters/pngine.mjs +87 -0
- package/adapters/snapshot.mjs +143 -0
- package/adapters/target.mjs +104 -0
- package/adapters/watch.mjs +355 -0
- package/adapters/webaudio.mjs +102 -0
- package/bin/animader.mjs +31 -0
- package/hello/join.js +46 -0
- package/hello/run.js +259 -0
- package/lib/build.mjs +126 -0
- package/lib/buildloop.mjs +254 -0
- package/lib/contract.mjs +371 -0
- package/lib/errors.mjs +51 -0
- package/lib/export.mjs +445 -0
- package/lib/new.mjs +304 -0
- package/lib/pipeline.mjs +247 -0
- package/lib/receipt.mjs +331 -0
- package/lib/rmtree.mjs +49 -0
- package/lib/routes.mjs +170 -0
- package/lib/score.mjs +434 -0
- package/lib/show.mjs +458 -0
- package/lib/soundtrack.mjs +67 -0
- package/lib/vendor.mjs +99 -0
- package/lib/wav.mjs +197 -0
- package/package.json +47 -4
- package/player/boot.mjs +792 -0
- package/player/controller.mjs +366 -0
- package/player/devchannel.mjs +306 -0
- package/player/index.html +35 -0
- package/player/page.mjs +134 -0
- package/player/player.css +393 -0
- package/player/release.mjs +76 -0
- package/src/commands/build.mjs +201 -0
- package/src/commands/dev.mjs +214 -0
- package/src/commands/doctor.mjs +348 -0
- package/src/commands/export.mjs +158 -0
- package/src/commands/new.mjs +164 -0
- package/src/commands/project.mjs +46 -0
- package/src/config.mjs +110 -0
- package/src/router.mjs +149 -0
- package/src/toolchain.mjs +691 -0
- package/starters/circle/README.md +50 -0
- package/starters/circle/animader.json +4 -0
- package/starters/circle/gitignore +9 -0
- package/starters/circle/kernels/circle.wgsl +32 -0
- package/starters/circle/program.sjon +34 -0
- package/starters/circle/timeline.piece.sjon +26 -0
- package/starters/ember/bake/spans.bin +0 -0
- package/starters/ember/kernels/curl.wgsl +10 -0
- package/starters/ember/kernels/ember.wgsl +10 -0
- package/starters/ember/kernels/hash.wgsl +12 -0
- package/starters/ember/kernels/simplex.wgsl +27 -0
- package/starters/ember/program.sjon +328 -0
- package/starters/ember/timeline.piece.sjon +34 -0
- package/toolchain/licenses/animader.txt +121 -0
- package/toolchain/licenses/esbuild.txt +21 -0
- package/toolchain/licenses/pacer.txt +121 -0
- package/toolchain/licenses/pngine.txt +121 -0
- package/toolchain/manifest.json +212 -0
- package/toolchain/prepare.mjs +446 -0
- package/toolchain/uniform-type-name.js +184 -0
- package/README.md +0 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# An Animader project
|
|
2
|
+
|
|
3
|
+
This folder was created by `animader new` with the circle starter: one
|
|
4
|
+
circle kernel, an eight-second path with beat pulses, and a complete
|
|
5
|
+
timeline. Three authored files carry the piece:
|
|
6
|
+
|
|
7
|
+
- `program.sjon` — the GPU program: the buffers, the circle kernel and
|
|
8
|
+
the passes that run each frame.
|
|
9
|
+
- `timeline.piece.sjon` — the musical timeline: tempo, meter, sections
|
|
10
|
+
and the lanes that animate the program's fields.
|
|
11
|
+
- one soundtrack file — `soundtrack.wav`, the labeled test tone (not
|
|
12
|
+
music).
|
|
13
|
+
|
|
14
|
+
## Host duration
|
|
15
|
+
|
|
16
|
+
`animader.json` holds the host settings. `seconds` is the real playback
|
|
17
|
+
length the host uses. The musical lengths inside the timeline (bars,
|
|
18
|
+
beats, tempo) are musical time; the host plays the `seconds` it finds in
|
|
19
|
+
`animader.json`, or an explicit `--seconds` you pass.
|
|
20
|
+
|
|
21
|
+
## The workflow this project is shaped for
|
|
22
|
+
|
|
23
|
+
`animader dev` (play the project while you edit), `animader build`
|
|
24
|
+
(validate and write the generated output) and `animader export --out dist`
|
|
25
|
+
(write a standalone release to a fresh folder) are the commands this
|
|
26
|
+
layout targets. They are provided by the `animader` CLI — run
|
|
27
|
+
`animader --help` to see which commands your version provides.
|
|
28
|
+
|
|
29
|
+
## Editing
|
|
30
|
+
|
|
31
|
+
Edit `program.sjon` or `timeline.piece.sjon` in any editor. The dev
|
|
32
|
+
server keeps running until you stop it: press Ctrl-C in its terminal
|
|
33
|
+
before running other project commands.
|
|
34
|
+
|
|
35
|
+
## Replacing the soundtrack
|
|
36
|
+
|
|
37
|
+
Move your own audio into this folder as `soundtrack.wav`,
|
|
38
|
+
`soundtrack.ogg` or `soundtrack.opus`. Exactly one recognized soundtrack
|
|
39
|
+
may exist here at a time, so move the replaced file out of the project
|
|
40
|
+
yourself — the host never deletes author audio. The generated
|
|
41
|
+
`soundtrack.wav` is a labeled test tone, not music.
|
|
42
|
+
|
|
43
|
+
## Generated output and exports
|
|
44
|
+
|
|
45
|
+
`out/` holds generated build output. `.gitignore` already ignores
|
|
46
|
+
`out/`, the default `dist/` export, its staging directory and its
|
|
47
|
+
detached size report; an export to another name needs matching ignore
|
|
48
|
+
entries of its own (for `--out release`: `release/`, `release.next`,
|
|
49
|
+
`release.size.json`). Exports write to a fresh folder and never
|
|
50
|
+
overwrite an existing one.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Generated build output.
|
|
2
|
+
out/
|
|
3
|
+
|
|
4
|
+
# The default export destination, its staging directory and its detached
|
|
5
|
+
# size report. An export to another name needs its own ignore entries,
|
|
6
|
+
# for example release/, release.next and release.size.json for --out release.
|
|
7
|
+
dist/
|
|
8
|
+
dist.next
|
|
9
|
+
dist.size.json
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
struct Params {
|
|
2
|
+
beat: f32,
|
|
3
|
+
scene: f32,
|
|
4
|
+
x: f32,
|
|
5
|
+
radius: f32,
|
|
6
|
+
}
|
|
7
|
+
struct Inputs {
|
|
8
|
+
time: f32,
|
|
9
|
+
width: f32,
|
|
10
|
+
height: f32,
|
|
11
|
+
aspect: f32,
|
|
12
|
+
}
|
|
13
|
+
@group(0) @binding(0) var<uniform> params: Params;
|
|
14
|
+
@group(0) @binding(1) var<uniform> inputs: Inputs;
|
|
15
|
+
|
|
16
|
+
@vertex
|
|
17
|
+
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
18
|
+
let x = f32(i & 1u) * 4.0 - 1.0;
|
|
19
|
+
let y = f32(i >> 1u) * 4.0 - 1.0;
|
|
20
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@fragment
|
|
24
|
+
fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
25
|
+
let uv = pos.xy / vec2f(inputs.width, inputs.height);
|
|
26
|
+
let p = (uv - vec2f(params.x, 0.5)) * vec2f(inputs.aspect, 1.0);
|
|
27
|
+
let aa = 1.0 / inputs.height;
|
|
28
|
+
let pulse = 1.0 + 0.15 * (1.0 - smoothstep(0.0, 0.30, fract(params.beat)));
|
|
29
|
+
let r = params.radius * pulse;
|
|
30
|
+
let circle = 1.0 - smoothstep(r - aa, r + aa, length(p));
|
|
31
|
+
return vec4f(mix(vec3f(0.96, 0.95, 0.92), vec3f(0.12, 0.12, 0.15), circle), 1.0);
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
; program.sjon — the circle program from TUTORIAL.md step 4: one fullscreen
|
|
2
|
+
; triangle, one fragment-shaded circle, one pass per frame. The defaults
|
|
3
|
+
; feed Params in kernels/circle.wgsl in order: beat, scene, x, radius.
|
|
4
|
+
|
|
5
|
+
(data :name defaults :float32 [0.0 0.0 0.20 0.045])
|
|
6
|
+
(buffer :name params :usage [uniform copy-dst] :data defaults)
|
|
7
|
+
|
|
8
|
+
(buffer :name inputs :size 16 :usage [uniform copy-dst])
|
|
9
|
+
(queue :name writeInputs
|
|
10
|
+
(write-buffer :buffer inputs :offset 0 :data pngine-inputs))
|
|
11
|
+
|
|
12
|
+
(shader-module :name circleCode :file "kernels/circle.wgsl")
|
|
13
|
+
|
|
14
|
+
(render-pipeline :name circlePipe :layout auto
|
|
15
|
+
(vertex :module circleCode :entry vs)
|
|
16
|
+
(fragment :module circleCode :entry fs
|
|
17
|
+
(color-target :format preferred-canvas-format))
|
|
18
|
+
(primitive :topology triangle-list))
|
|
19
|
+
|
|
20
|
+
(bind-group :name circleBinds :layout circlePipe :group 0
|
|
21
|
+
(entry :binding 0 :buffer params)
|
|
22
|
+
(entry :binding 1 :buffer inputs))
|
|
23
|
+
|
|
24
|
+
(render-pass :name picture
|
|
25
|
+
(color-attachment :view context-current-texture
|
|
26
|
+
:clear-value [0.96 0.95 0.92 1.0]
|
|
27
|
+
:load-op clear :store-op store)
|
|
28
|
+
:pipeline circlePipe
|
|
29
|
+
:bind-groups [circleBinds]
|
|
30
|
+
(draw :vertex-count 3))
|
|
31
|
+
|
|
32
|
+
(frame :name main
|
|
33
|
+
:before [writeInputs]
|
|
34
|
+
:perform [picture])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
; timeline.piece.sjon — TUTORIAL.md steps 5 and 6: four bars of 4/4 at
|
|
2
|
+
; 120 qpm (eight seconds), the score's beat handed to the shader, the
|
|
3
|
+
; circle's journey across the canvas and its radius pulse on every beat.
|
|
4
|
+
|
|
5
|
+
(animader :name circle-journey :version 1
|
|
6
|
+
:default-world live :worlds [live])
|
|
7
|
+
|
|
8
|
+
(world :name live
|
|
9
|
+
:time live :params defaults :input live :viewport [1280 720]
|
|
10
|
+
:assets project :state live)
|
|
11
|
+
|
|
12
|
+
(timeline :title "A circle, for now" :length 4bar :end hold)
|
|
13
|
+
(tempo-point :at 0q :qpm 120)
|
|
14
|
+
(meter-point :at 0q :sig [4 4])
|
|
15
|
+
(section :name one :at 0bar :dur 4bar)
|
|
16
|
+
|
|
17
|
+
(lane :name beat-ramp :kind formula :target beat :expr beat)
|
|
18
|
+
|
|
19
|
+
(lane :name journey-x :kind curve :target x
|
|
20
|
+
(keyframe :at 0b :value 0.20 :ease linear)
|
|
21
|
+
(keyframe :at 16b :value 0.80 :ease linear))
|
|
22
|
+
|
|
23
|
+
(lane :name size :kind curve :target radius
|
|
24
|
+
(keyframe :at 0b :value 0.045 :ease linear)
|
|
25
|
+
(keyframe :at 8b :value 0.080 :ease in-out-sine)
|
|
26
|
+
(keyframe :at 16b :value 0.045 :ease in-out-sine))
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The curl of a simplex field: a divergence-free velocity, the thing
|
|
2
|
+
// `evolve`'s `(curl :scale s :amount a)` step samples. Two finite
|
|
3
|
+
// differences of the scalar field, rotated a quarter turn.
|
|
4
|
+
fn curl(p: vec2f, scale: f32, t: f32) -> vec2f {
|
|
5
|
+
let e = 0.01;
|
|
6
|
+
let q = p * scale + vec2f(t * 0.15, -t * 0.11);
|
|
7
|
+
let dx = simplex(q + vec2f(e, 0.0)) - simplex(q - vec2f(e, 0.0));
|
|
8
|
+
let dy = simplex(q + vec2f(0.0, e)) - simplex(q - vec2f(0.0, e));
|
|
9
|
+
return vec2f(dy, -dx) / (2.0 * e);
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The author's own kernel: a colour for a particle from its speed, black
|
|
2
|
+
// body from a deep red through orange to a pale yellow, the way an ember
|
|
3
|
+
// brightens when it moves.
|
|
4
|
+
fn ember(speed: f32) -> vec3f {
|
|
5
|
+
let s = clamp(speed * 6.0, 0.0, 1.0);
|
|
6
|
+
let cold = vec3f(0.55, 0.08, 0.02);
|
|
7
|
+
let warm = vec3f(1.0, 0.45, 0.08);
|
|
8
|
+
let hot = vec3f(1.0, 0.92, 0.6);
|
|
9
|
+
return select(mix(cold, warm, s * 2.0), mix(warm, hot, s * 2.0 - 1.0), s > 0.5);
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// A small integer hash to [0, 1). PCG-style, deterministic on every
|
|
2
|
+
// device, which is what a seed table wants: the same particle draws the
|
|
3
|
+
// same home on every run.
|
|
4
|
+
fn hash_u32(x: u32) -> u32 {
|
|
5
|
+
var h = x * 747796405u + 2891336453u;
|
|
6
|
+
h = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u;
|
|
7
|
+
return (h >> 22u) ^ h;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fn hash(x: u32) -> f32 {
|
|
11
|
+
return f32(hash_u32(x)) / 4294967296.0;
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 2D simplex noise in [-1, 1], Ashima's, the field `haze` ramps and the
|
|
2
|
+
// field `curl` differentiates. `position` in, one value out.
|
|
3
|
+
fn mod289_2(x: vec2f) -> vec2f { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
4
|
+
fn mod289_3(x: vec3f) -> vec3f { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
5
|
+
fn permute3(x: vec3f) -> vec3f { return mod289_3(((x * 34.0) + 1.0) * x); }
|
|
6
|
+
|
|
7
|
+
fn simplex(v: vec2f) -> f32 {
|
|
8
|
+
let C = vec4f(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
|
|
9
|
+
var i = floor(v + dot(v, C.yy));
|
|
10
|
+
let x0 = v - i + dot(i, C.xx);
|
|
11
|
+
var i1 = vec2f(0.0, 1.0);
|
|
12
|
+
if (x0.x > x0.y) { i1 = vec2f(1.0, 0.0); }
|
|
13
|
+
var x12 = x0.xyxy + C.xxzz;
|
|
14
|
+
x12 = vec4f(x12.xy - i1, x12.zw);
|
|
15
|
+
i = mod289_2(i);
|
|
16
|
+
let p = permute3(permute3(i.y + vec3f(0.0, i1.y, 1.0)) + i.x + vec3f(0.0, i1.x, 1.0));
|
|
17
|
+
var m = max(vec3f(0.5) - vec3f(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), vec3f(0.0));
|
|
18
|
+
m = m * m;
|
|
19
|
+
m = m * m;
|
|
20
|
+
let x = 2.0 * fract(p * C.www) - 1.0;
|
|
21
|
+
let h = abs(x) - 0.5;
|
|
22
|
+
let ox = floor(x + 0.5);
|
|
23
|
+
let a0 = x - ox;
|
|
24
|
+
m = m * (1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h));
|
|
25
|
+
let g = vec3f(a0.x * x0.x + h.x * x0.y, a0.yz * x12.xz + h.yz * x12.yw);
|
|
26
|
+
return 130.0 * dot(m, g);
|
|
27
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
; program.sjon — the hello piece, written by hand in pngine's forms. The twin
|
|
2
|
+
; of `piece.sjon`, which says the same thing in animader's vocabulary; this
|
|
3
|
+
; file is what the CLI runs. Every place the writing was tedious is a line in
|
|
4
|
+
; TEDIUM.md, with the verb that would have absorbed it.
|
|
5
|
+
;
|
|
6
|
+
; pngine validate program.sjon
|
|
7
|
+
; pngine render -f -s 1280x720 -t 1.44 -o out.png program.sjon ; one tick, at t
|
|
8
|
+
; pngine inspect program.sjon --deep --phase frame --frames 0,1,10,60 --time-step 0.01 --json
|
|
9
|
+
;
|
|
10
|
+
; The word EMBER is a span table baked by canto (bake/spans.bin). A seed
|
|
11
|
+
; pass draws every particle a home inside the letters; a compute step moves
|
|
12
|
+
; the particles by a curl field, a spring back home and the pointer; a draw
|
|
13
|
+
; paints them as sprites into a texture; a feedback pass keeps their trails;
|
|
14
|
+
; a last pass composes the trails over a haze and tonemaps to the canvas.
|
|
15
|
+
|
|
16
|
+
; density: the one structural number. `(define …)` reaches every numeric slot
|
|
17
|
+
; and never the WGSL, so the shaders read the buffer's own length instead.
|
|
18
|
+
(define :name N :value 24000)
|
|
19
|
+
|
|
20
|
+
; --- kernels, checked inside the modules that assemble them ---
|
|
21
|
+
(wgsl :name hash :file "kernels/hash.wgsl")
|
|
22
|
+
(wgsl :name simplex :file "kernels/simplex.wgsl")
|
|
23
|
+
(wgsl :name curl :file "kernels/curl.wgsl" :imports [simplex])
|
|
24
|
+
(wgsl :name ember :file "kernels/ember.wgsl")
|
|
25
|
+
|
|
26
|
+
; --- the params: PROGRAM defaults in a uniform the world may overwrite ---
|
|
27
|
+
; grain, heat, and two pads. Filled from the data at creation and never
|
|
28
|
+
; written by the frame, so a host write to `params` (the knob, the score)
|
|
29
|
+
; lands and stays. (`:init` takes compute passes only, so a `(queue …)`
|
|
30
|
+
; could not do this once.)
|
|
31
|
+
(data :name paramDefaults :float32 [4.7 0.6 0.0 0.0])
|
|
32
|
+
(buffer :name params :usage [uniform copy-dst] :data paramDefaults)
|
|
33
|
+
|
|
34
|
+
; --- what the world supplies every frame: time and the pointer ---
|
|
35
|
+
; The struct's members are named t, w, h, asp because the sugar passes
|
|
36
|
+
; below build their own `pngine` uniform with time/width/height/aspect, and
|
|
37
|
+
; a name shared by two buffers makes `setUniform` ambiguous (graph_compose).
|
|
38
|
+
; `ptr` and `target` are reserved words in WGSL; naga says so where
|
|
39
|
+
; wgslender lets `ptr` pass.
|
|
40
|
+
(buffer :name inputs :size 16 :usage [uniform copy-dst])
|
|
41
|
+
(queue :name writeInputs (write-buffer :buffer inputs :offset 0 :data pngine-inputs))
|
|
42
|
+
(buffer :name pointerBuf :size 48 :usage [uniform copy-dst])
|
|
43
|
+
(queue :name writePointer (write-buffer :buffer pointerBuf :offset 0 :data pointer-inputs))
|
|
44
|
+
|
|
45
|
+
; --- letters: the span table, bytes verbatim, read by the seed pass ---
|
|
46
|
+
(data :name lettersTable (bytes :file "bake/spans.bin"))
|
|
47
|
+
(buffer :name letters :usage [storage] :data lettersTable)
|
|
48
|
+
|
|
49
|
+
; --- dust: one record per particle, home, position, velocity, seed, pad ---
|
|
50
|
+
(buffer :name dust :size (* N 32) :usage [vertex storage] :pool 2)
|
|
51
|
+
|
|
52
|
+
; seeds: `pixel → element`. Each invocation draws a cell of the word's
|
|
53
|
+
; coverage, area-weighted, and makes it the particle's home. The table's
|
|
54
|
+
; row cumulative is inclusive (zlug SPN1); row 0 is the top of the box.
|
|
55
|
+
(shader-module :name seedCode :imports [hash] :code """
|
|
56
|
+
struct Particle { home : vec2f, pos : vec2f, vel : vec2f, seed : f32, pad : f32 }
|
|
57
|
+
@group(0) @binding(0) var<storage, read> table : array<u32>;
|
|
58
|
+
@group(0) @binding(1) var<storage, read_write> a : array<Particle>;
|
|
59
|
+
@group(0) @binding(2) var<storage, read_write> b : array<Particle>;
|
|
60
|
+
|
|
61
|
+
// The bake's box, [0, 3.248] x [-0.244, 0.928] em, as an aspect. Baked
|
|
62
|
+
// into the WGSL because a `(define …)` cannot reach it; see bake/provenance.txt.
|
|
63
|
+
const BOX_ASPECT : f32 = 2.772;
|
|
64
|
+
|
|
65
|
+
@compute @workgroup_size(64)
|
|
66
|
+
fn main(@builtin(global_invocation_id) id : vec3u) {
|
|
67
|
+
let i = id.x;
|
|
68
|
+
if (i >= arrayLength(&a)) { return; }
|
|
69
|
+
let rows = table[1] & 0xffffu;
|
|
70
|
+
let cols = table[1] >> 16u;
|
|
71
|
+
let count = table[2];
|
|
72
|
+
let total = table[3];
|
|
73
|
+
let goal = min(u32(hash(i) * f32(total)), total - 1u);
|
|
74
|
+
// the first row whose inclusive cumulative passes the target
|
|
75
|
+
var lo = 0u;
|
|
76
|
+
var hi = rows - 1u;
|
|
77
|
+
while (lo < hi) {
|
|
78
|
+
let mid = (lo + hi) / 2u;
|
|
79
|
+
if (table[4u + mid * 2u + 1u] > goal) { hi = mid; } else { lo = mid + 1u; }
|
|
80
|
+
}
|
|
81
|
+
let r = lo;
|
|
82
|
+
let first = table[4u + r * 2u];
|
|
83
|
+
var next = count;
|
|
84
|
+
if (r + 1u < rows) { next = table[4u + (r + 1u) * 2u]; }
|
|
85
|
+
var before = 0u;
|
|
86
|
+
if (r > 0u) { before = table[4u + (r - 1u) * 2u + 1u]; }
|
|
87
|
+
var rem = goal - before;
|
|
88
|
+
var col = 0u;
|
|
89
|
+
for (var s = first; s < next; s = s + 1u) {
|
|
90
|
+
let w = table[4u + rows * 2u + s];
|
|
91
|
+
let x0 = w & 0xffffu;
|
|
92
|
+
let x1 = w >> 16u;
|
|
93
|
+
let width = x1 - x0;
|
|
94
|
+
if (rem < width) { col = x0 + rem; break; }
|
|
95
|
+
rem = rem - width;
|
|
96
|
+
col = x1 - 1u;
|
|
97
|
+
}
|
|
98
|
+
let u = (f32(col) + hash(i * 3u + 1u)) / f32(cols);
|
|
99
|
+
let v = (f32(r) + hash(i * 3u + 2u)) / f32(rows);
|
|
100
|
+
// title space: the word one unit wide, centred, y up
|
|
101
|
+
let home = vec2f((u - 0.5) * 1.0, (0.5 - v) / BOX_ASPECT);
|
|
102
|
+
var p : Particle;
|
|
103
|
+
p.home = home;
|
|
104
|
+
p.pos = home;
|
|
105
|
+
p.vel = vec2f(0.0, 0.0);
|
|
106
|
+
p.seed = hash(i * 3u + 3u);
|
|
107
|
+
p.pad = 0.0;
|
|
108
|
+
a[i] = p;
|
|
109
|
+
b[i] = p;
|
|
110
|
+
}
|
|
111
|
+
""")
|
|
112
|
+
|
|
113
|
+
; dust: `element → element`. The curl step scaled by heat, the spring that
|
|
114
|
+
; reassembles the word, the repel from the pointer, in title space, where
|
|
115
|
+
; `title` is the space that rotates with time and carries the user's nudge.
|
|
116
|
+
(shader-module :name stepCode :imports [curl] :code """
|
|
117
|
+
struct Particle { home : vec2f, pos : vec2f, vel : vec2f, seed : f32, pad : f32 }
|
|
118
|
+
struct Params { grain : f32, heat : f32, pad0 : f32, pad1 : f32 }
|
|
119
|
+
struct Inputs { t : f32, w : f32, h : f32, asp : f32 }
|
|
120
|
+
struct Pointer { x : f32, y : f32, clickX : f32, clickY : f32, dx : f32, dy : f32,
|
|
121
|
+
buttons : f32, pressure : f32, modifiers : f32, scrollX : f32, scrollY : f32, pad : f32 }
|
|
122
|
+
@group(0) @binding(0) var<uniform> params : Params;
|
|
123
|
+
@group(0) @binding(1) var<uniform> inputs : Inputs;
|
|
124
|
+
@group(0) @binding(2) var<uniform> pointer : Pointer;
|
|
125
|
+
@group(0) @binding(3) var<storage, read> src : array<Particle>;
|
|
126
|
+
@group(0) @binding(4) var<storage, read_write> dst : array<Particle>;
|
|
127
|
+
|
|
128
|
+
// (space :name title :transform (trs :rotate (* time .05) :scale 1.1) :user (translate [.12 -.03]))
|
|
129
|
+
fn title(p : vec2f, t : f32) -> vec2f {
|
|
130
|
+
let a = t * 0.05;
|
|
131
|
+
let c = cos(a);
|
|
132
|
+
let s = sin(a);
|
|
133
|
+
let r = vec2f(p.x * c - p.y * s, p.x * s + p.y * c) * 1.1;
|
|
134
|
+
return r + vec2f(0.12, -0.03);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@compute @workgroup_size(64)
|
|
138
|
+
fn main(@builtin(global_invocation_id) id : vec3u) {
|
|
139
|
+
let i = id.x;
|
|
140
|
+
if (i >= arrayLength(&src)) { return; }
|
|
141
|
+
var p = src[i];
|
|
142
|
+
let t = inputs.t;
|
|
143
|
+
// A second is a hundred ticks and this pass runs once per tick, so the
|
|
144
|
+
// step is a hundredth whatever the paint rate (docs/05-model.md, the tick
|
|
145
|
+
// and the paint). One frame of the program is one tick; the player steps
|
|
146
|
+
// the due ticks and paints the last.
|
|
147
|
+
let dt = 0.01;
|
|
148
|
+
let goal = title(p.home, t);
|
|
149
|
+
// (curl :scale 3 :amount heat). The amount is per tick, a hundred a
|
|
150
|
+
// second: the curl's gradient is order one, and the spring below wins
|
|
151
|
+
// at rest, so the word breathes and reassembles rather than dissolves.
|
|
152
|
+
let flow = curl(p.pos + vec2f(p.seed * 0.01, 0.0), 3.0, t) * params.heat * 0.06;
|
|
153
|
+
// the reassembling: a spring to the home the title space carries
|
|
154
|
+
let spring = (goal - p.pos) * 6.0;
|
|
155
|
+
// (repel :from (convert pointer :to title) :radius .18): the pointer arrives in
|
|
156
|
+
// canvas uv, y down; title space is the sprite space below, y up
|
|
157
|
+
var repel = vec2f(0.0, 0.0);
|
|
158
|
+
if (pointer.x > 0.0 || pointer.y > 0.0) {
|
|
159
|
+
let at = vec2f((pointer.x * 2.0 - 1.0) / 1.3, (1.0 - pointer.y * 2.0) / (1.3 * inputs.asp));
|
|
160
|
+
let away = p.pos - at;
|
|
161
|
+
let d = length(away);
|
|
162
|
+
if (d < 0.18) { repel = normalize(away + vec2f(0.0001, 0.0)) * (0.18 - d) * 30.0; }
|
|
163
|
+
}
|
|
164
|
+
p.vel = (p.vel + (flow + spring + repel) * dt) * 0.92;
|
|
165
|
+
p.pos = p.pos + p.vel * dt;
|
|
166
|
+
dst[i] = p;
|
|
167
|
+
}
|
|
168
|
+
""")
|
|
169
|
+
|
|
170
|
+
; sparks: `element → surface`. Every particle as a sprite, its colour from
|
|
171
|
+
; its speed through the author's `ember` kernel, drawn into a texture.
|
|
172
|
+
(data :name quad :float32 [-1.0 -1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0])
|
|
173
|
+
(buffer :name quadVB :usage [vertex] :data quad)
|
|
174
|
+
|
|
175
|
+
(shader-module :name sparkCode :imports [ember] :code """
|
|
176
|
+
struct Inputs { t : f32, w : f32, h : f32, asp : f32 }
|
|
177
|
+
@group(0) @binding(0) var<uniform> inputs : Inputs;
|
|
178
|
+
struct VsOut { @builtin(position) pos : vec4f, @location(0) uv : vec2f, @location(1) color : vec3f }
|
|
179
|
+
|
|
180
|
+
@vertex
|
|
181
|
+
fn vs(@location(0) home : vec2f, @location(1) pos : vec2f, @location(2) vel : vec2f,
|
|
182
|
+
@location(3) corner : vec2f) -> VsOut {
|
|
183
|
+
// (paint … :size .004): title space to clip space, the word 1.3 wide,
|
|
184
|
+
// which keeps it inside the frame with the title's scale and nudge
|
|
185
|
+
let size = 0.004 * 1.3;
|
|
186
|
+
let centre = vec2f(pos.x * 1.3, pos.y * 1.3 * inputs.asp);
|
|
187
|
+
var out : VsOut;
|
|
188
|
+
out.pos = vec4f(centre + corner * vec2f(size, size * inputs.asp), 0.0, 1.0);
|
|
189
|
+
out.uv = corner;
|
|
190
|
+
out.color = ember(length(vel));
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@fragment
|
|
195
|
+
fn fs(in : VsOut) -> @location(0) vec4f {
|
|
196
|
+
// The deposit per tick. The trails keep 0.965 of it a tick, a hundred
|
|
197
|
+
// ticks a second, so a still particle sums to about 28 deposits; this
|
|
198
|
+
// keeps the letters short of white and lets a moving one glow.
|
|
199
|
+
let d = length(in.uv);
|
|
200
|
+
let a = smoothstep(1.0, 0.3, d) * 0.035;
|
|
201
|
+
return vec4f(in.color * a, a);
|
|
202
|
+
}
|
|
203
|
+
""")
|
|
204
|
+
|
|
205
|
+
(texture :name sparksTex :size canvas :format rgba16float
|
|
206
|
+
:usage [render-attachment texture-binding])
|
|
207
|
+
|
|
208
|
+
(render-pipeline :name sparkPipe :layout auto
|
|
209
|
+
(vertex :module sparkCode :entry vs
|
|
210
|
+
(vertex-buffer :array-stride 32 :step-mode instance
|
|
211
|
+
(attribute :shader-location 0 :offset 0 :format float32x2)
|
|
212
|
+
(attribute :shader-location 1 :offset 8 :format float32x2)
|
|
213
|
+
(attribute :shader-location 2 :offset 16 :format float32x2))
|
|
214
|
+
(vertex-buffer :array-stride 8 :step-mode vertex
|
|
215
|
+
(attribute :shader-location 3 :offset 0 :format float32x2)))
|
|
216
|
+
(fragment :module sparkCode :entry fs
|
|
217
|
+
(color-target :format rgba16float
|
|
218
|
+
(blend (color :src-factor one :dst-factor one :operation add)
|
|
219
|
+
(alpha :src-factor one :dst-factor one :operation add))))
|
|
220
|
+
(primitive :topology triangle-list))
|
|
221
|
+
|
|
222
|
+
(bind-group :name sparkBinds :layout sparkPipe :group 0
|
|
223
|
+
(entry :binding 0 :buffer inputs))
|
|
224
|
+
|
|
225
|
+
(render-pass :name sparks
|
|
226
|
+
(color-attachment :view sparksTex :clear-value [0.0 0.0 0.0 0.0] :load-op clear :store-op store)
|
|
227
|
+
:pipeline sparkPipe
|
|
228
|
+
:bind-groups [sparkBinds]
|
|
229
|
+
:vertex-buffers [dust quadVB]
|
|
230
|
+
:vertex-buffers-pool-offsets [1 0]
|
|
231
|
+
(draw :vertex-count 6 :instance-count N))
|
|
232
|
+
|
|
233
|
+
; ground: `pixel → surface`. The haze, a ramp over simplex noise at the
|
|
234
|
+
; grain the params hold, as a hand-written fullscreen pass, because a
|
|
235
|
+
; `(pass …)` cannot bind the params buffer.
|
|
236
|
+
(shader-module :name groundCode :imports [simplex] :code """
|
|
237
|
+
struct Params { grain : f32, heat : f32, pad0 : f32, pad1 : f32 }
|
|
238
|
+
struct Inputs { t : f32, w : f32, h : f32, asp : f32 }
|
|
239
|
+
@group(0) @binding(0) var<uniform> params : Params;
|
|
240
|
+
@group(0) @binding(1) var<uniform> inputs : Inputs;
|
|
241
|
+
|
|
242
|
+
@vertex
|
|
243
|
+
fn vs(@builtin(vertex_index) i : u32) -> @builtin(position) vec4f {
|
|
244
|
+
let x = f32(i32(i & 1u) * 4 - 1);
|
|
245
|
+
let y = f32(i32(i >> 1u) * 4 - 1);
|
|
246
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@fragment
|
|
250
|
+
fn fs(@builtin(position) pos : vec4f) -> @location(0) vec4f {
|
|
251
|
+
let uv = pos.xy / vec2f(inputs.w, inputs.h);
|
|
252
|
+
let p = vec2f(uv.x * inputs.asp, uv.y);
|
|
253
|
+
// (field :name haze :value (ramp (simplex position :scale grain)))
|
|
254
|
+
let n = simplex(p * params.grain + vec2f(inputs.t * 0.02, 0.0)) * 0.5 + 0.5;
|
|
255
|
+
let ramp = mix(vec3f(0.015, 0.008, 0.02), vec3f(0.14, 0.06, 0.05), n * n);
|
|
256
|
+
return vec4f(ramp, 1.0);
|
|
257
|
+
}
|
|
258
|
+
""")
|
|
259
|
+
|
|
260
|
+
(texture :name groundTex :size canvas :format rgba8unorm
|
|
261
|
+
:usage [render-attachment texture-binding])
|
|
262
|
+
|
|
263
|
+
(render-pipeline :name groundPipe :layout auto
|
|
264
|
+
(vertex :module groundCode :entry vs)
|
|
265
|
+
(fragment :module groundCode :entry fs (color-target :format rgba8unorm))
|
|
266
|
+
(primitive :topology triangle-list))
|
|
267
|
+
|
|
268
|
+
(bind-group :name groundBinds :layout groundPipe :group 0
|
|
269
|
+
(entry :binding 0 :buffer params)
|
|
270
|
+
(entry :binding 1 :buffer inputs))
|
|
271
|
+
|
|
272
|
+
(render-pass :name ground
|
|
273
|
+
(color-attachment :view groundTex :clear-value [0.0 0.0 0.0 1.0] :load-op clear :store-op store)
|
|
274
|
+
:pipeline groundPipe
|
|
275
|
+
:bind-groups [groundBinds]
|
|
276
|
+
(draw :vertex-count 3))
|
|
277
|
+
|
|
278
|
+
; --- the compute pipelines and passes ---
|
|
279
|
+
(compute-pipeline :name seedPipe :layout auto (compute :module seedCode :entry main))
|
|
280
|
+
(bind-group :name seedBinds :layout seedPipe :group 0 :pool 2
|
|
281
|
+
(entry :binding 0 :buffer letters)
|
|
282
|
+
(entry :binding 1 :buffer dust :ping-pong 0)
|
|
283
|
+
(entry :binding 2 :buffer dust :ping-pong 1))
|
|
284
|
+
(compute-pass :name seed :pipeline seedPipe :bind-groups [seedBinds]
|
|
285
|
+
(dispatch :workgroups [(ceil (/ N 64))]))
|
|
286
|
+
|
|
287
|
+
(compute-pipeline :name stepPipe :layout auto (compute :module stepCode :entry main))
|
|
288
|
+
(bind-group :name stepBinds :layout stepPipe :group 0 :pool 2
|
|
289
|
+
(entry :binding 0 :buffer params)
|
|
290
|
+
(entry :binding 1 :buffer inputs)
|
|
291
|
+
(entry :binding 2 :buffer pointerBuf)
|
|
292
|
+
(entry :binding 3 :buffer dust :ping-pong 0)
|
|
293
|
+
(entry :binding 4 :buffer dust :ping-pong 1))
|
|
294
|
+
(compute-pass :name step :pipeline stepPipe :bind-groups [stepBinds]
|
|
295
|
+
(dispatch :workgroups [(ceil (/ N 64))]))
|
|
296
|
+
|
|
297
|
+
; trails and final: `surface → memory` and `surface, surface → surface`.
|
|
298
|
+
; The sugar tier reads the two textures by name; `prev_trails` is last
|
|
299
|
+
; frame's output, decayed.
|
|
300
|
+
(pass-graph :name look
|
|
301
|
+
(pass :name trails :feedback true :reads [sparksTex] :format rgba16float :code """
|
|
302
|
+
@fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
303
|
+
// The pass addresses its canvas-sized textures through pngine's own
|
|
304
|
+
// extent; the group's list therefore carries `trails__uq` in front of
|
|
305
|
+
// it, and the frame's solo text names that queue.
|
|
306
|
+
let uv = pos.xy / vec2f(pngine.width, pngine.height);
|
|
307
|
+
let at = vec2i(uv * vec2f(pngine.width, pngine.height));
|
|
308
|
+
// (feedback :name trails :source sparks :decay .965)
|
|
309
|
+
let prev = textureLoad(prev_trails, at, 0).rgb * 0.965;
|
|
310
|
+
let now = textureLoad(sparksTex, at, 0).rgb;
|
|
311
|
+
return vec4f(prev + now, 1.0);
|
|
312
|
+
}
|
|
313
|
+
""")
|
|
314
|
+
(pass :name final :reads [groundTex] :code """
|
|
315
|
+
@fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
316
|
+
let uv = pos.xy / vec2f(pngine.width, pngine.height);
|
|
317
|
+
// (compose :name final :a ground :b trails), then a tonemap to the canvas
|
|
318
|
+
let g = textureSample(groundTex, samp, uv).rgb;
|
|
319
|
+
let t = textureSample(trails, samp, uv).rgb;
|
|
320
|
+
let hdr = g + t;
|
|
321
|
+
return vec4f(hdr / (hdr + vec3f(1.0)), 1.0);
|
|
322
|
+
}
|
|
323
|
+
"""))
|
|
324
|
+
|
|
325
|
+
(frame :name main
|
|
326
|
+
:init [seed]
|
|
327
|
+
:before [writeInputs writePointer]
|
|
328
|
+
:perform [step sparks ground look])
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
; The hello piece's derived spelling, the browser seat: the aggregate
|
|
2
|
+
; carries tempura's own roots inline, so one fetch from the page is the
|
|
3
|
+
; whole document the boundary walks (`PieceHost.score`, the wasm
|
|
4
|
+
; `anm_piece_score`). A wasm boundary has no filesystem, so the split
|
|
5
|
+
; spelling's `:timeline` file cannot reach it; these inline roots can. The
|
|
6
|
+
; page opens this file with `?seat=timeline` and drives the derived score
|
|
7
|
+
; through the same compile, evaluate and route path as its authored score
|
|
8
|
+
; (`player/player.js`, `player/seat.js`). The param's appended control and
|
|
9
|
+
; the `warmth` lane both name registry-0 `heat`, and pacer writes the
|
|
10
|
+
; control after every track, so `heat` is the control's value on every
|
|
11
|
+
; frame, idle at 0.6 and held at the slider's value, while the `churn`
|
|
12
|
+
; lane's own `grain` target moves the picture (the tenth ruling,
|
|
13
|
+
; 2026-10-07). The gate's bytes and numbers live in
|
|
14
|
+
; `npm/animader/test/derived-seat.test.js`; the browser pass replays them
|
|
15
|
+
; through `player/check.mjs` case 14.
|
|
16
|
+
(animader :name ember :version 1
|
|
17
|
+
:default-world live
|
|
18
|
+
:worlds [live])
|
|
19
|
+
|
|
20
|
+
(param :name heat :default 0.6 :range [0 2])
|
|
21
|
+
|
|
22
|
+
(world :name live
|
|
23
|
+
:time live :params defaults :input live :viewport [1280 720]
|
|
24
|
+
:assets project :state live)
|
|
25
|
+
|
|
26
|
+
(timeline :title "ember, the mix" :length 4bar :end hold)
|
|
27
|
+
(tempo-point :at 0q :qpm 120)
|
|
28
|
+
(meter-point :at 0q :sig [4 4])
|
|
29
|
+
(lane :name warmth :kind curve :target heat
|
|
30
|
+
(keyframe :at 0q :value 0.1)
|
|
31
|
+
(keyframe :at 4bar :value 0.9))
|
|
32
|
+
(lane :name churn :kind curve :target grain
|
|
33
|
+
(keyframe :at 0q :value 4.7)
|
|
34
|
+
(keyframe :at 4bar :value 9.4))
|