Squashed from work; the fine-grained history is under tag archive/work-2026-08-26: - vjfx: 104 new presets — the transition lane fills out and the screen family goes wide - vjfx: three lanes land — engine hooks + hold stage, the videomesh engine, and the audio picture - vj: the thumbnail pipeline becomes one honest machine, and effects go livecodable - vj: thumbnails become mp4 — hardware-coded sheets at measured-4K cells, and the bake stops racing the GPU - store: the ceremony dies — batch publish, one transaction, and the engine stops re-reading its own log - store: the ceremony dies — batch publish, one transaction, and the engine stops re-reading its own log - vj: the console grows real transports, and the deck stops lying about reverse - vj: reverse earns a memory, and the effects stop aging - fab: a 3D creation shell and the viewer built on it
153 lines
5.7 KiB
Text
153 lines
5.7 KiB
Text
// FIREWORKS SHOW — the programmable-emitters engine. The `frame:` function
|
|
// below runs EVERY FRAME (bounded, like a game tick): it launches climbing
|
|
// shells on the beat, pops them into burst emitters at fuse time, and keeps
|
|
// a sparkle field simmering. It only ever touches EMITTERS — every visible
|
|
// spark is stateless GPU work derived from the emitter's parameters.
|
|
//
|
|
// Emitter spawn keys: kind (burst/jet/sparkle/ring), pos, vel, life, speed,
|
|
// size, gravity, stagger, fraction, spread, color, color2, seed, slot
|
|
// (re-spawning a slot MOVES that emitter — persistent, script-animated).
|
|
{
|
|
name: "Fireworks Show"
|
|
engine: "emitters"
|
|
particles: 1024
|
|
size: 0.14
|
|
|
|
// PERFORMANCE DIALS — the tick reads the dial params as fx.p0..fx.p3;
|
|
// mid-knob reproduces the stock show exactly.
|
|
p0: 0.5 p1: 0.5 p2: 0.5
|
|
dials: [
|
|
{name: "BURST", bind: "p0", default: 0.5},
|
|
{name: "TRAIL", bind: "p1", default: 0.5},
|
|
{name: "GLOW", bind: "p2", default: 0.5}
|
|
]
|
|
|
|
color_bg: #x030309
|
|
cam_dist: 15.0
|
|
cam_height: 4.0
|
|
cam_orbit: 0.03
|
|
|
|
stages: [
|
|
{kind: "bloom", threshold: 0.3, strength: "1.7 * (0.25 + 1.5*p2)", levels: 3},
|
|
{kind: "feedback", amount: "min(0.5 + (p1-0.5)*0.8, 0.97)", zoom: 1.0, rotate: 0.0, dim: 0.88}
|
|
]
|
|
|
|
frame: fn(fx) {
|
|
let out = []
|
|
// BEAT EDGE, robustly: a bare `phase < dt*2` is a single-frame
|
|
// window a fixed-step clock can straddle forever (the thumbnail
|
|
// bake's 30fps at 120bpm steps phase by exactly dt*2). Floor the
|
|
// window wider than one step and key each spawn to the BEAT
|
|
// NUMBER via `slot`, so an extra frame in the window replaces the
|
|
// spawn instead of doubling it.
|
|
let edge = fx.phase < max(fx.dt * 2.0, 0.075)
|
|
let n = floor(fx.beat)
|
|
// One shell per beat, launched RIGHT after the beat lands.
|
|
if edge {
|
|
let lane = modf(n, 5.0) - 2.0
|
|
out.push({
|
|
kind: "jet"
|
|
slot: 90 + modf(n, 2.0)
|
|
pos: vec3(lane * 4.0, -8.0, 0.0)
|
|
vel: vec3(0.0 - lane * 0.4, 11.0, 0.0)
|
|
life: 1.1
|
|
speed: 1.2
|
|
size: 0.10
|
|
fraction: 0.12
|
|
color: #xffe0b0
|
|
color2: #xff8030
|
|
seed: n
|
|
})
|
|
}
|
|
// The pop: a burst where the shell arrived, one beat later.
|
|
if edge {
|
|
let lane2 = modf(n - 1.0, 5.0) - 2.0
|
|
let hue = modf(n, 3.0)
|
|
let col = #x66aaff
|
|
let col2 = #x2244aa
|
|
if hue < 0.5 {
|
|
col = #xffd24a
|
|
col2 = #xaa6620
|
|
} else if hue < 1.5 {
|
|
col = #xff5588
|
|
col2 = #x992244
|
|
}
|
|
out.push({
|
|
kind: "burst"
|
|
slot: 80 + modf(n, 6.0)
|
|
pos: vec3(lane2 * 4.0 - lane2 * 0.4, 3.0, 0.0)
|
|
vel: vec3(0.0, 0.6, 0.0)
|
|
life: 2.6
|
|
// BURST dial: the pop's reach and spark size ride fx.p0.
|
|
speed: 5.5 * (0.5 + fx.p0)
|
|
size: 0.16 * (0.4 + 1.2 * fx.p0)
|
|
gravity: 1.0
|
|
stagger: 0.05
|
|
color: col
|
|
color2: col2
|
|
seed: n * 1.7
|
|
})
|
|
// A crackle ring under the big break, every other beat.
|
|
if modf(n, 2.0) < 0.5 {
|
|
out.push({
|
|
kind: "ring"
|
|
slot: 70 + modf(n * 0.5, 2.0)
|
|
pos: vec3(lane2 * 4.0, 3.0, 0.0)
|
|
life: 1.2
|
|
speed: 4.0
|
|
size: 0.09
|
|
fraction: 0.4
|
|
color: #xffffff
|
|
color2: #x8899ff
|
|
seed: n * 3.1
|
|
})
|
|
}
|
|
}
|
|
// A slow sparkle field that simmers between the big hits.
|
|
if fx.emitters < 3 {
|
|
out.push({
|
|
kind: "sparkle"
|
|
pos: vec3(0.0, 5.0, -3.0)
|
|
life: 6.0
|
|
spread: 9.0
|
|
size: 0.05
|
|
fraction: 0.5
|
|
color: #xaaccff
|
|
color2: #x334488
|
|
seed: fx.time
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// THE LOOK LIVES HERE. This block is not a reference to the
|
|
// engine's shader — it IS the pixel math this effect runs.
|
|
// Rewrite it and the effect looks different; everything else
|
|
// (geometry, motion, the content coupling, the beat) keeps
|
|
// working, because the block SUBCLASSES the engine shader.
|
|
// Inputs: t = life 0..1, attr = (id, seconds alive, ignition rnd, spread rnd).
|
|
// Signals: self.time_beat (time, beat, phase, pulse),
|
|
// self.sig (bar, bpm, energy, dt), self.user (p0..p3),
|
|
// self.col_a/b/c/bg, self.fog (density, glow, content, -).
|
|
shader: draw.DrawVjFxEmitter {
|
|
fx_color: fn(t: float, attr: vec4, content: vec4, cmix: float) -> vec4 {
|
|
let heat = clamp(1.0 - attr.y * 3.0, 0.0, 1.0)
|
|
let tint0 = self.e_col_a.mix(self.e_col_b, t)
|
|
let pal = tint0.xyz.mix(
|
|
content.xyz * (1.05 + 0.5 * self.time_beat.w),
|
|
clamp(cmix * 1.2, 0.0, 1.0)
|
|
)
|
|
let rgb = mix(pal, vec3(1.0, 0.98, 0.92), heat * heat)
|
|
let fade = (1.0 - t) * (1.0 - t)
|
|
return vec4(rgb, fade)
|
|
}
|
|
|
|
fx_sprite: fn(uv: vec2, tint: vec4) -> vec4 {
|
|
let d = length(uv - vec2(0.5, 0.5)) * 2.0
|
|
let core = 1.0 - smoothstep(0.0, 0.4, d)
|
|
let halo = 1.0 - smoothstep(0.1, 0.9, d)
|
|
let a = clamp(core + halo * 0.5, 0.0, 1.0) * tint.w
|
|
return vec4(tint.x * a, tint.y * a, tint.z * a, 0.0)
|
|
}
|
|
}
|
|
}
|