Add in.dt, slider warnings in --check, and two agent-written effects
Spec fixes come from a test where pi + deepseek-v4.1-flash wrote effects from EFFECTS.md alone: glitch-blocks and ghost-stamps both compiled on the first try. The agent's feedback led to in.dt (frame time), documented limits of in.prev as memory, and a --check warning for ignored slider lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a20d3310db
commit
169c5f9b63
6 changed files with 111 additions and 9 deletions
34
effects/ghost-stamps.metal
Normal file
34
effects/ghost-stamps.metal
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Every `stamp` seconds the moving parts of the picture are frozen in place as a translucent,
|
||||
// tinted ghost; each ghost fades over `life` seconds, so moving things leave a row of copies.
|
||||
// order 70
|
||||
// slider stamp 0.2 3.0 0.5
|
||||
// slider life 0.2 6.0 2.0
|
||||
// slider hue 0.0 1.0 0.55
|
||||
// hue 0..1 -> a fully saturated colour (0 red, 0.08 orange, 0.33 green, 0.66 blue)
|
||||
static float3 hueColor(float h) {
|
||||
return clamp(abs(fract(h + float3(0.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0) - 1.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 effect(float2 uv, Inputs in) {
|
||||
// The frozen ghosts are one number per pixel (a tinted brightness), so they live in the
|
||||
// alpha channel of this effect's own output. The colour is only added when displaying, so
|
||||
// `in.prev.rgb` never feeds the live picture back into itself.
|
||||
float ghosts = in.prev.sample(in.smp, uv).a;
|
||||
|
||||
float frame = in.dt;
|
||||
ghosts *= exp(-frame / max(in.p.life, 0.05));
|
||||
|
||||
float3 live = in.image.sample(in.smp, uv).rgb;
|
||||
float diff = length(in.camera.sample(in.smp, uv).rgb - in.last.sample(in.smp, uv).rgb);
|
||||
float moving = smoothstep(0.03, 0.30, diff);
|
||||
float fresh = dot(live, float3(0.299, 0.587, 0.114)) * moving;
|
||||
|
||||
// A stamp is taken just after each `stamp`-second slot of in.time begins. The window is two
|
||||
// frames wide, so a stamp is not missed when the frame rate falls below the nominal one.
|
||||
float slot = max(in.p.stamp, 0.05);
|
||||
float begins = 1.0 - step(2.0 * frame / slot, fract(in.time / slot));
|
||||
ghosts = max(ghosts, fresh * begins);
|
||||
|
||||
float3 tint = hueColor(in.p.hue);
|
||||
return float4(live + tint * ghosts * 0.85, ghosts);
|
||||
}
|
||||
57
effects/glitch-blocks.metal
Normal file
57
effects/glitch-blocks.metal
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Movement breaks the picture into horizontal bands that jump sideways and flash
|
||||
// with shifted colours. Still frames pass through unchanged.
|
||||
// order 70
|
||||
// slider band 2.0 200.0 48.0
|
||||
// slider intensity 0.0 2.0 1.0
|
||||
// slider sensitivity 1.0 60.0 24.0
|
||||
// slider shift 0.0 0.3 0.08
|
||||
// slider split 0.0 0.06 0.012
|
||||
// slider flash 0.0 1.0 0.7
|
||||
// slider rate 1.0 30.0 12.0
|
||||
|
||||
// A repeatable pseudo random number in 0..1 from a 2D seed.
|
||||
static float hash21(float2 p) {
|
||||
p = fract(p * float2(123.34, 456.21));
|
||||
p += dot(p, p + 45.32);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
// How much the picture moved near this spot, in 0..1.
|
||||
static float motionAt(float2 uv, Inputs in) {
|
||||
float2 flow = in.flow.sample(in.smp, uv).xy;
|
||||
float3 now = in.camera.sample(in.smp, uv).rgb;
|
||||
float3 before = in.last.sample(in.smp, uv).rgb;
|
||||
float moved = length(now - before);
|
||||
return clamp(length(flow) * in.p.sensitivity + moved * in.p.sensitivity * 0.05, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 effect(float2 uv, Inputs in) {
|
||||
float bandPixels = max(in.p.band, 1.0);
|
||||
float bandHeight = bandPixels / in.resolution.y;
|
||||
float band = floor(uv.y / bandHeight);
|
||||
float centre = (band + 0.5) * bandHeight;
|
||||
|
||||
// The whole band reacts to the motion measured at its middle.
|
||||
float strength = clamp(motionAt(float2(uv.x, centre), in) * in.p.intensity, 0.0, 1.0);
|
||||
|
||||
// Each band gets fresh random values a few times per second.
|
||||
float tick = floor(in.time * in.p.rate);
|
||||
float r1 = hash21(float2(band, tick));
|
||||
float r2 = hash21(float2(band + 71.3, tick));
|
||||
float r3 = hash21(float2(band + 13.7, tick + 5.0));
|
||||
|
||||
float slide = (r1 * 2.0 - 1.0) * in.p.shift * strength;
|
||||
float split = (r2 * 2.0 - 1.0) * in.p.split * strength;
|
||||
float2 base = float2(uv.x + slide, uv.y);
|
||||
|
||||
float3 col = in.image.sample(in.smp, base).rgb;
|
||||
col.r = in.image.sample(in.smp, float2(base.x + split, base.y)).r;
|
||||
col.b = in.image.sample(in.smp, float2(base.x - split, base.y)).b;
|
||||
|
||||
// Flash with a shifted copy of the channels.
|
||||
float3 other = mix(col.gbr, col.brg, step(0.5, r3));
|
||||
col = mix(col, other, in.p.flash * strength);
|
||||
col *= 1.0 + (r1 - 0.5) * in.p.flash * strength;
|
||||
|
||||
return float4(col, 1.0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue