// SCAN SERMON — the pure-document effect: NO engine geometry at all, the // whole frame is the fragment function in this file. // // This is the pattern to copy when an author wants a look no engine has: // pick `engine: "screen"`, declare a `shader: draw.DrawVjFxScreen` block, // and write `fx_color(uv, content, cmix)`. The engine contributes nothing // but the quad and the clock — every pixel below is doc-authored. The // stage chain still runs on top, so bloom/feedback/warps compose with it. // // The look: a broadcast scan head sweeping down the frame, tearing the // video into bands that lag behind it, with the beat driving the tear and // a phosphor rgb-split on the leading edge. { name: "Scan Sermon" engine: "screen" seed: 5 input0: "test" speed: 1.0 beat_pulse: 0.6 beat_rate: 1.0 bar_beats: 4 glow: 1.0 // PERFORMANCE DIALS — each default reproduces the stock look exactly // (mid-knob multipliers are 1.0 at 0.5). p0: 0.5 p1: 0.5 p2: 0.5 dials: [ {name: "TEAR", bind: "p0", default: 0.5}, {name: "BANDS", bind: "p1", default: 0.5}, {name: "SPLIT", bind: "p2", default: 0.5} ] color_bg: #x03040a color_a: #x30ffd0 color_b: #xff3a86 color_c: #xfff0d0 stages: [ {kind: "bloom", threshold: 0.55, strength: 1.15, levels: 3} ] // THE LOOK LIVES HERE. There is no engine shading under this at all: // whatever this function returns IS the frame. // Inputs: uv = screen uv (0,0 top-left), content = input0 at uv, // cmix = the pre-gated content strength (0 without real channel video, // so a look can tell the fallback pattern from a live clip). // 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.DrawVjFxScreen { fx_color: fn(uv: vec2, content: vec4, cmix: float) -> vec4 { // The scan head: one sweep per bar, wrapping. let head = fract(self.sig.x) // Horizontal bands; more of them as BANDS opens. let bands = 6.0 + 26.0 * clamp(self.user.y, 0.0, 1.0) let band = floor(uv.y * bands) let band01 = fract(uv.y * bands) // Distance BELOW the head, wrapped: a band the head just // crossed is hot and displaced, and it settles as it ages. let age = fract(uv.y - head + 1.0) let hot = pow(1.0 - age, 9.0) // Tear: each band slides by its own hash, hardest right after // the head passes and on the beat pulse. let hash = fract(sin(band * 12.9898 + 3.0) * 43758.5453) let drive = (0.30 + 1.7 * clamp(self.user.x, 0.0, 1.0)) * (0.25 + self.time_beat.w) let slide = (hash - 0.5) * hot * drive * 0.35 let suv = clamp(vec2(fract(uv.x + slide), uv.y), vec2(0.0, 0.0), vec2(1.0, 1.0)) // Phosphor split: the channels separate along the tear. let sp = (0.002 + 0.03 * clamp(self.user.z, 0.0, 1.0)) * (0.2 + hot) let r = self.tex0.sample_as_bgra(clamp(suv + vec2(sp, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0))) let g = self.tex0.sample_as_bgra(suv) let b = self.tex0.sample_as_bgra(clamp(suv - vec2(sp, 0.0), vec2(0.0, 0.0), vec2(1.0, 1.0))) let mut rgb = vec3(r.x, g.y, b.z) // Scanline gate inside each band + a dark seam between bands. let line = 0.86 + 0.14 * sin(uv.y * 900.0) let seam = smoothstep(0.0, 0.06, band01) * smoothstep(0.0, 0.06, 1.0 - band01) rgb = rgb * (line * (0.35 + 0.65 * seam)) // The head itself: a bar of col_a with a col_c core. let edge = pow(1.0 - age, 60.0) rgb = rgb + self.col_a.xyz * (hot * 0.35) + self.col_c.xyz * (edge * 0.9) // A band that has not been scanned yet sits in col_b's dark. rgb = rgb.mix(self.col_bg.xyz + self.col_b.xyz * 0.06, (1.0 - hot) * 0.22) // Without real content (cmix 0) the fallback pattern is all // there is, so lift it a little to keep the frame alive. let lift = 1.0 + (1.0 - clamp(cmix, 0.0, 1.0)) * 0.25 return vec4(rgb * (self.fog.y * lift), 1.0) } } }