v7.19.0 — trueblack auto: trails reach pure black at any persist without opting in; exact alpha-preserving -1/255 GPU sweep (mode 22) runs by default in the sticky zone (persist >= 0.5), Canvas2D difference tail demoted to a gated no-WebGL fallback (alphaArt latch), trueblack header tri-state (auto/on/off) — fixes the Texe-rings stuck-quantum smear
This commit is contained in:
parent
2f0ed4f657
commit
60a7d40568
7 changed files with 229 additions and 32 deletions
|
|
@ -24,7 +24,7 @@ const FRAME_VARS: [string, string][] = [
|
||||||
['silent', '1 while silence holds the beat grid (v7.15.1) -- no beats fire and beatphase freezes; releases the moment real audio returns. Waveform / band data still flows, it is just near zero'],
|
['silent', '1 while silence holds the beat grid (v7.15.1) -- no beats fire and beatphase freezes; releases the moment real audio returns. Waveform / band data still flows, it is just near zero'],
|
||||||
['w / h', 'canvas size in pixels'],
|
['w / h', 'canvas size in pixels'],
|
||||||
['aspect', 'canvas width / height -- divide x by it for perfect circles'],
|
['aspect', 'canvas width / height -- divide x by it for perfect circles'],
|
||||||
['persist', 'motion-trail persistence 0..1, writable per frame -- 1 = the framebuffer never fades (old content then leaves only through a bg* transform or a blend: replace repaint); at 0.5 and above the 8-bit fade can leave a faint gray wash -- the trueblack: header fixes that'],
|
['persist', 'motion-trail persistence 0..1, writable per frame -- 1 = the framebuffer never fades (old content then leaves only through a bg* transform or a blend: replace repaint); at 0.5 and above the 8-bit fade used to leave a faint gray wash -- the engine now sweeps that to pure black automatically (see trueblack)'],
|
||||||
['blend', 'per-frame override of the blend: header -- set it to a mode\'s number from the Blend modes table below (1 = normal, 2 = additive, 3 = replace, 6 = subtract, ...); read after [frame]/[beat], before drawing'],
|
['blend', 'per-frame override of the blend: header -- set it to a mode\'s number from the Blend modes table below (1 = normal, 2 = additive, 3 = replace, 6 = subtract, ...); read after [frame]/[beat], before drawing'],
|
||||||
['bgrot', 'background spin in radians per frame -- + clockwise, - counter-clockwise; 0 = off (set in [init] / [frame] / [beat]; persists like any variable)'],
|
['bgrot', 'background spin in radians per frame -- + clockwise, - counter-clockwise; 0 = off (set in [init] / [frame] / [beat]; persists like any variable)'],
|
||||||
['bgx / bgy', 'background drift per frame, the -1..1 screen convention (y is up) -- slide the whole picture up, down, left or right'],
|
['bgx / bgy', 'background drift per frame, the -1..1 screen convention (y is up) -- slide the whole picture up, down, left or right'],
|
||||||
|
|
@ -200,9 +200,9 @@ description: shown in the preset list
|
||||||
mode: lines # lines | dots | solid
|
mode: lines # lines | dots | solid
|
||||||
blend: additive # any of the 41 keywords -- see the Blend modes table
|
blend: additive # any of the 41 keywords -- see the Blend modes table
|
||||||
persist: 0.85 # trail persistence 0..1 (1 = never fades)
|
persist: 0.85 # trail persistence 0..1 (1 = never fades)
|
||||||
trueblack: off # subtractive fade tail: trails reach PURE black even at
|
trueblack: auto # on | off | auto (default auto): subtractive fade tail
|
||||||
# high persist (no gray wash) -- but it turns transparent
|
# so trails reach PURE black even at high persist --
|
||||||
# pixels opaque, so leave it off for behind/clear art
|
# auto runs it exactly in the sticky zone (persist >= 0.5)
|
||||||
linewidth: 2
|
linewidth: 2
|
||||||
|
|
||||||
[init] # runs once on load
|
[init] # runs once on load
|
||||||
|
|
@ -211,11 +211,13 @@ linewidth: 2
|
||||||
[point] # runs n times per frame`}
|
[point] # runs n times per frame`}
|
||||||
</pre>
|
</pre>
|
||||||
<p className="mt-2 text-[11px] leading-relaxed text-zinc-500">
|
<p className="mt-2 text-[11px] leading-relaxed text-zinc-500">
|
||||||
<span className="font-mono text-emerald-400/90">trueblack: on</span> pairs with a high persist: the trail fade multiplies the
|
<span className="font-mono text-emerald-400/90">trueblack: auto</span> keeps every trail honest: the fade multiplies the framebuffer in 8-bit
|
||||||
framebuffer in 8-bit steps, so values like 0.9 leave a stuck gray residue (round(v·p) = v for the smallest
|
steps, so a persist of 0.5 or more leaves a stuck residue (round(v·p) = v for the smallest values) -- a faint
|
||||||
values); the tail subtracts 1/255 per frame so everything reaches true black -- long echo trails stay clean
|
colored wash that never fully fades. auto (the default) subtracts 1/255 per frame exactly in that sticky zone,
|
||||||
over a pure background. Because it forces pixels opaque, never use it together with{' '}
|
so everything ends in true black while cheap low-persist scripts pay nothing. The sweep runs on the GPU and
|
||||||
<span className="font-mono">behind</span>/<span className="font-mono">clear</span> blending.
|
preserves transparency, so <span className="font-mono">behind</span>/<span className="font-mono">clear</span>{' '}
|
||||||
|
art keeps its holes (the no-WebGL fallback stands down for it instead). <span className="font-mono">on</span>{' '}
|
||||||
|
forces the sweep at any persist below 1, <span className="font-mono">off</span> keeps the wash for looks.
|
||||||
</p>
|
</p>
|
||||||
</HelpSection>
|
</HelpSection>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@
|
||||||
// negation, phoenix, grain extract/merge, reflect, glow, and the four
|
// negation, phoenix, grain extract/merge, reflect, glow, and the four
|
||||||
// After Effects masks (stencil / silhouette, luma + alpha).
|
// After Effects masks (stencil / silhouette, luma + alpha).
|
||||||
//
|
//
|
||||||
|
// v7.19 adds mode 22, the trueblack decay sweep (sweep()): every pixel's
|
||||||
|
// premultiplied rgb AND alpha lose exactly 1/255, clamped at 0 -- the
|
||||||
|
// engine's persistence-fade tail. Unlike the Canvas2D 'difference'
|
||||||
|
// fallback it is exact (black stays a true 0, no round-trip oscillation)
|
||||||
|
// and alpha-preserving (behind/clear holes keep healing), which is what
|
||||||
|
// lets the engine run the sweep by default for every sticky persist.
|
||||||
|
//
|
||||||
// Per frame the engine draws the stroke passes onto a scratch canvas
|
// Per frame the engine draws the stroke passes onto a scratch canvas
|
||||||
// (source-over, so the two-pass glow keeps its usual tuning), then:
|
// (source-over, so the two-pass glow keeps its usual tuning), then:
|
||||||
// 1. uploads the live canvas (backdrop) and the scratch (stroke layer)
|
// 1. uploads the live canvas (backdrop) and the scratch (stroke layer)
|
||||||
|
|
@ -67,6 +74,13 @@ void main() {
|
||||||
vec3 b = c > 0.0 ? S.rgb / max(c, 1e-4) : vec3(0.0); // straight stroke
|
vec3 b = c > 0.0 ? S.rgb / max(c, 1e-4) : vec3(0.0); // straight stroke
|
||||||
|
|
||||||
// -- special-tail modes ------------------------------------------------
|
// -- special-tail modes ------------------------------------------------
|
||||||
|
if (uMode == 22) { // trueblack decay sweep (engine.ts fade tail)
|
||||||
|
// premultiplied rgb AND alpha lose exactly 1/255, clamped at 0; the
|
||||||
|
// stroke layer is not read. 1/255 = 0.00392156863...
|
||||||
|
gl_FragColor = vec4(max(D.rgb - vec3(0.00392156863), vec3(0.0)),
|
||||||
|
max(D.a - 0.00392156863, 0.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (uMode == 1) { // dissolve: stochastic full-intensity speckle
|
if (uMode == 1) { // dissolve: stochastic full-intensity speckle
|
||||||
// the +uFrame offset re-rolls the grain every frame (animated speckle)
|
// the +uFrame offset re-rolls the grain every frame (animated speckle)
|
||||||
gl_FragColor = hash(vUV * 997.0 + vec2(uFrame * 0.618, uFrame * 0.317)) < c ? vec4(b, 1.0) : D;
|
gl_FragColor = hash(vUV * 997.0 + vec2(uFrame * 0.618, uFrame * 0.317)) < c ? vec4(b, 1.0) : D;
|
||||||
|
|
@ -221,6 +235,28 @@ export class BlendCompositor {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** v7.19 — trueblack decay sweep: render one pass of mode 22 into
|
||||||
|
* this.canvas. The engine then copies it back onto the live canvas
|
||||||
|
* with composite 'copy'. Only the backdrop is uploaded (the stroke
|
||||||
|
* sampler is pointed at the same texture — mode 22 never reads it). */
|
||||||
|
sweep(dst: HTMLCanvasElement): void {
|
||||||
|
const gl = this.gl
|
||||||
|
const prog = this.prog
|
||||||
|
if (!gl || !prog) return
|
||||||
|
gl.viewport(0, 0, this.canvas.width, this.canvas.height)
|
||||||
|
gl.useProgram(prog)
|
||||||
|
gl.activeTexture(gl.TEXTURE0)
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.texDst)
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, dst)
|
||||||
|
gl.uniform1i(this.uDst, 0)
|
||||||
|
gl.activeTexture(gl.TEXTURE1)
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.texDst) // uSrc unread by mode 22
|
||||||
|
gl.uniform1i(this.uSrc, 1)
|
||||||
|
gl.uniform1i(this.uMode, 22)
|
||||||
|
gl.uniform1f(this.uFrame, 0)
|
||||||
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||||
|
}
|
||||||
|
|
||||||
/** Render one blended frame into this.canvas. The engine then copies it
|
/** Render one blended frame into this.canvas. The engine then copies it
|
||||||
* back onto the live canvas with composite 'copy'. */
|
* back onto the live canvas with composite 'copy'. */
|
||||||
blend(dst: HTMLCanvasElement, src: HTMLCanvasElement, mode: number, frame: number): void {
|
blend(dst: HTMLCanvasElement, src: HTMLCanvasElement, mode: number, frame: number): void {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,26 @@
|
||||||
// every pixel opaque (alpha composites to 1 under 'difference'), so it
|
// every pixel opaque (alpha composites to 1 under 'difference'), so it
|
||||||
// must not be combined with 'behind'/'clear' art -- opt-in, off by
|
// must not be combined with 'behind'/'clear' art -- opt-in, off by
|
||||||
// default, and the Help panel documents the caveat.
|
// default, and the Help panel documents the caveat.
|
||||||
|
//
|
||||||
|
// v7.19: trueblack is AUTOMATIC. The subtractive tail now runs by default
|
||||||
|
// in exactly the regime where the multiply sticks (persist >= 0.5), so
|
||||||
|
// every trail -- not just scripts that know the header -- ends in pure
|
||||||
|
// black. The default path is the WebGL compositor's mode-22 sweep
|
||||||
|
// (blend-gpu.ts): premultiplied rgb AND alpha each lose exactly 1/255,
|
||||||
|
// clamped at 0. That is exact in 8-bit (black stays a true 0 -- no
|
||||||
|
// round-trip oscillation like the 'difference' fill's 0<->1 flicker) and
|
||||||
|
// alpha-PRESERVING, so 'behind'/'clear' art keeps its transparent holes
|
||||||
|
// (which heal to truly empty, too -- stuck alpha quanta are swept like
|
||||||
|
// everything else). Because the GPU sweep is safe for alpha art, it runs
|
||||||
|
// unconditionally. The v7.17 Canvas2D 'difference' fill survives only as
|
||||||
|
// the no-WebGL fallback, and there it stands down once the script has
|
||||||
|
// been seen drawing with 'behind'/'clear' (the alphaArt latch; the header
|
||||||
|
// blend pre-latches it, a per-frame env.blend switch latches at draw
|
||||||
|
// time). The header itself is now a tri-state: auto (default -- sweep in
|
||||||
|
// the sticky zone only), on (always, the v7.17 behavior) and off (never,
|
||||||
|
// for authors who want the wash). Scripts at persist < 0.5 pay nothing:
|
||||||
|
// their multiply already decays every quantum value, so the sweep is
|
||||||
|
// skipped entirely.
|
||||||
|
|
||||||
import { blendByIndex, blendIndex, findBlend } from './blend'
|
import { blendByIndex, blendIndex, findBlend } from './blend'
|
||||||
import { BlendCompositor } from './blend-gpu'
|
import { BlendCompositor } from './blend-gpu'
|
||||||
|
|
@ -122,6 +142,13 @@ export class AvaEngine {
|
||||||
* nearest native operator (spec.fallbackOp). */
|
* nearest native operator (spec.fallbackOp). */
|
||||||
private gpuBroken = false
|
private gpuBroken = false
|
||||||
|
|
||||||
|
/** v7.19 — latched while the current script draws alpha-dependent art
|
||||||
|
* ('behind' / 'clear', from the header blend or a per-frame env.blend
|
||||||
|
* switch). The GPU trueblack sweep preserves alpha and always runs; the
|
||||||
|
* Canvas2D 'difference' fallback forces pixels opaque, so it stands
|
||||||
|
* down for these scripts instead of punching their holes shut. */
|
||||||
|
private alphaArt = false
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// AVS-eel compatibility hooks — the expression engine reads audio/time
|
// AVS-eel compatibility hooks — the expression engine reads audio/time
|
||||||
// through these (getspec/getosc/gettime). Headless tests can override.
|
// through these (getspec/getosc/gettime). Headless tests can override.
|
||||||
|
|
@ -191,6 +218,11 @@ export class AvaEngine {
|
||||||
this.script = script
|
this.script = script
|
||||||
this.scriptError = null
|
this.scriptError = null
|
||||||
this.ysign = script.meta.ysign === -1 ? -1 : 1
|
this.ysign = script.meta.ysign === -1 ? -1 : 1
|
||||||
|
// v7.19: pre-latch the alpha-art guard when the header blend itself is
|
||||||
|
// behind/clear (a per-frame env.blend switch latches at draw time) --
|
||||||
|
// the Canvas2D fallback sweep must not punch those holes shut.
|
||||||
|
const headerSpec = findBlend(script.meta.blend)
|
||||||
|
this.alphaArt = headerSpec?.key === 'behind' || headerSpec?.key === 'clear'
|
||||||
// beatage starts "long ago" so exp(-beatage*k) envelopes read as 0 until
|
// beatage starts "long ago" so exp(-beatage*k) envelopes read as 0 until
|
||||||
// the first real beat arrives.
|
// the first real beat arrives.
|
||||||
this.env = { n: 400, t: 0, time: 0, frame: 0, beats: 0, beatage: 99 }
|
this.env = { n: 400, t: 0, time: 0, frame: 0, beats: 0, beatage: 99 }
|
||||||
|
|
@ -205,6 +237,7 @@ export class AvaEngine {
|
||||||
clearScript(): void {
|
clearScript(): void {
|
||||||
this.script = null
|
this.script = null
|
||||||
this.scriptError = null
|
this.scriptError = null
|
||||||
|
this.alphaArt = false
|
||||||
}
|
}
|
||||||
|
|
||||||
getScriptName(): string {
|
getScriptName(): string {
|
||||||
|
|
@ -290,6 +323,31 @@ export class AvaEngine {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** v7.19 — run the trueblack decay sweep through the WebGL compositor
|
||||||
|
* (mode 22: premultiplied rgb and alpha each lose exactly 1/255,
|
||||||
|
* clamped at 0 -- see the file header). Exact in 8-bit and
|
||||||
|
* alpha-preserving, which is why the automatic sweep prefers it over
|
||||||
|
* the Canvas2D 'difference' tail. False when WebGL is unavailable or
|
||||||
|
* the pass throws (the caller then falls back, or stands down for
|
||||||
|
* alpha-dependent art). */
|
||||||
|
private sweepViaGpu(w: number, h: number): boolean {
|
||||||
|
if (this.gpuBroken || !this.gpuEnsure(w, h)) return false
|
||||||
|
const compositor = this.compositor
|
||||||
|
const canvas = this.canvas
|
||||||
|
const ctx = this.ctx
|
||||||
|
if (!compositor || !canvas || !ctx) return false
|
||||||
|
try {
|
||||||
|
compositor.sweep(canvas)
|
||||||
|
ctx.globalCompositeOperation = 'copy'
|
||||||
|
ctx.drawImage(compositor.canvas, 0, 0)
|
||||||
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
this.gpuBroken = true // one bad frame never repeats; fallback next frame
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** v7.15 — one place for the stroke/dot/polygon passes, target-agnostic:
|
/** v7.15 — one place for the stroke/dot/polygon passes, target-agnostic:
|
||||||
* the native path draws onto the live canvas with the mode's composite
|
* the native path draws onto the live canvas with the mode's composite
|
||||||
* op; the gpu path draws onto the scratch with source-over (the shader
|
* op; the gpu path draws onto the scratch with source-over (the shader
|
||||||
|
|
@ -435,9 +493,16 @@ export class AvaEngine {
|
||||||
// persist thins trails exactly like the old rgb scaling did over the
|
// persist thins trails exactly like the old rgb scaling did over the
|
||||||
// black stage, while pixels that were painted behind / cleared keep
|
// black stage, while pixels that were painted behind / cleared keep
|
||||||
// fading honestly.
|
// fading honestly.
|
||||||
// v7.17: trueblack: on adds the subtractive tail (see the file header)
|
// v7.19: trueblack is AUTOMATIC (see the file header). The subtractive
|
||||||
// -- the 'difference' fill runs INSIDE this branch so persist 1 keeps
|
// tail runs right after the erase whenever the multiply can stick
|
||||||
// its true zero-fade (permanent) semantics.
|
// (persist >= 0.5; below that every quantum value already decays), so
|
||||||
|
// trails always end in pure black -- Texe-rings at 0.78, blow-outs at
|
||||||
|
// 0.93, anything. Preferred path: the GPU sweep (exact, alpha-safe);
|
||||||
|
// the 'difference' fill is only the no-WebGL fallback and it stands
|
||||||
|
// down for behind/clear art (alphaArt). trueblack: on forces the tail
|
||||||
|
// at any persist < 1 (v7.17 behavior), off disables it entirely.
|
||||||
|
// persist 1 keeps its true zero-fade (permanent) semantics -- neither
|
||||||
|
// the erase nor the tail runs there.
|
||||||
const meta = this.script?.meta
|
const meta = this.script?.meta
|
||||||
const persistBase = meta ? meta.persist : 0.85
|
const persistBase = meta ? meta.persist : 0.85
|
||||||
const persist = Math.min(Math.max(this.env.persist ?? persistBase, 0), 1)
|
const persist = Math.min(Math.max(this.env.persist ?? persistBase, 0), 1)
|
||||||
|
|
@ -445,10 +510,14 @@ export class AvaEngine {
|
||||||
ctx.globalCompositeOperation = 'destination-out'
|
ctx.globalCompositeOperation = 'destination-out'
|
||||||
ctx.fillStyle = `rgba(0,0,0,${(1 - persist).toFixed(4)})`
|
ctx.fillStyle = `rgba(0,0,0,${(1 - persist).toFixed(4)})`
|
||||||
ctx.fillRect(0, 0, w, h)
|
ctx.fillRect(0, 0, w, h)
|
||||||
if (meta?.trueblack) {
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
const tb = meta?.trueblack ?? 'auto'
|
||||||
|
const wantSweep = tb === 'on' ? true : tb === 'off' ? false : persist >= 0.5
|
||||||
|
if (wantSweep && !this.sweepViaGpu(w, h) && !this.alphaArt) {
|
||||||
ctx.globalCompositeOperation = 'difference'
|
ctx.globalCompositeOperation = 'difference'
|
||||||
ctx.fillStyle = '#010101'
|
ctx.fillStyle = '#010101'
|
||||||
ctx.fillRect(0, 0, w, h)
|
ctx.fillRect(0, 0, w, h)
|
||||||
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.globalCompositeOperation = 'source-over'
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
|
@ -583,6 +652,9 @@ export class AvaEngine {
|
||||||
// so the stroke owns its pixels outright.
|
// so the stroke owns its pixels outright.
|
||||||
const headerSpec = findBlend(meta.blend)
|
const headerSpec = findBlend(meta.blend)
|
||||||
const spec = blendByIndex(env.blend ?? 0) ?? headerSpec ?? undefined
|
const spec = blendByIndex(env.blend ?? 0) ?? headerSpec ?? undefined
|
||||||
|
// v7.19: remember alpha-dependent art for the Canvas2D fallback
|
||||||
|
// sweep's stand-down (the GPU sweep is alpha-safe and keeps running).
|
||||||
|
if (spec && (spec.key === 'behind' || spec.key === 'clear')) this.alphaArt = true
|
||||||
if (spec) {
|
if (spec) {
|
||||||
const viaGpu =
|
const viaGpu =
|
||||||
spec.impl === 'gpu' &&
|
spec.impl === 'gpu' &&
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,13 @@
|
||||||
// per frame through the blend variable
|
// per frame through the blend variable
|
||||||
// (by its number in the Help table)
|
// (by its number in the Help table)
|
||||||
// persist: 0.9 <- motion trail persistence 0..1 (1 = never fades)
|
// persist: 0.9 <- motion trail persistence 0..1 (1 = never fades)
|
||||||
// trueblack: on <- v7.17: adds a -1/255/frame subtractive tail to
|
// trueblack: auto <- v7.19: tri-state subtractive tail on the
|
||||||
// the trail fade so it reaches PURE black at any
|
// trail fade. auto (default): the engine
|
||||||
// persist (long trails normally settle into a
|
// sweeps stuck 8-bit quanta to PURE black
|
||||||
// faint gray wash from 8-bit rounding)
|
// exactly when the fade can stick
|
||||||
|
// (persist >= 0.5); on: sweep at any
|
||||||
|
// persist < 1; off: never (the old faint
|
||||||
|
// wash look)
|
||||||
// linewidth: 2
|
// linewidth: 2
|
||||||
//
|
//
|
||||||
// [init] runs once when the script loads
|
// [init] runs once when the script loads
|
||||||
|
|
@ -33,9 +36,12 @@ export interface ScriptMeta {
|
||||||
mode: RenderMode
|
mode: RenderMode
|
||||||
blend: BlendMode
|
blend: BlendMode
|
||||||
persist: number
|
persist: number
|
||||||
/** v7.17 — subtractive tail on the trail fade (see engine.ts). Off by default:
|
/** v7.19 — subtractive tail on the trail fade (see engine.ts), now a
|
||||||
* it forces every pixel opaque, which breaks 'behind'/'clear' transparency. */
|
* tri-state. 'auto' (default): the tail runs exactly in the sticky
|
||||||
trueblack: boolean
|
* zone (persist >= 0.5) through the alpha-safe GPU sweep, so every
|
||||||
|
* trail ends in pure black without the script opting in. 'on': forced
|
||||||
|
* at any persist < 1 (the v7.17 flag). 'off': never. */
|
||||||
|
trueblack: 'on' | 'off' | 'auto'
|
||||||
linewidth: number
|
linewidth: number
|
||||||
/** +1 (default, y is up) or -1 (AVS convention, y is down — used by auto-ported presets). */
|
/** +1 (default, y is up) or -1 (AVS convention, y is down — used by auto-ported presets). */
|
||||||
ysign: number
|
ysign: number
|
||||||
|
|
@ -77,7 +83,7 @@ const DEFAULT_META: ScriptMeta = {
|
||||||
mode: 'lines',
|
mode: 'lines',
|
||||||
blend: 'additive',
|
blend: 'additive',
|
||||||
persist: 0.85,
|
persist: 0.85,
|
||||||
trueblack: false,
|
trueblack: 'auto',
|
||||||
linewidth: 2,
|
linewidth: 2,
|
||||||
ysign: 1,
|
ysign: 1,
|
||||||
}
|
}
|
||||||
|
|
@ -191,18 +197,19 @@ function tryParse(text: string): { script: AvaScript | null; meta?: ScriptMeta;
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'trueblack': {
|
case 'trueblack': {
|
||||||
// v7.17: on/off flag (also 1/0, true/false, yes/no). The engine's
|
// v7.19: tri-state (on/off/auto, also 1/0, true/false, yes/no).
|
||||||
// fade multiplies the framebuffer by persist in 8-bit steps, so any
|
// The engine's fade multiplies the framebuffer by persist in 8-bit
|
||||||
// persist >= 0.5 has fixed points (round(v*p) == v) -- a gray wash
|
// steps, so any persist >= 0.5 has fixed points (round(v*p) == v)
|
||||||
// that never fully fades. The subtractive tail this flag enables
|
// -- a faint colored wash that never fully fades. auto (the new
|
||||||
// sweeps those quantum values to a true 0 every frame. Costs one
|
// default) sweeps those quantum values to a true 0 exactly in that
|
||||||
// fullscreen 'difference' fill; forces pixels opaque (never combine
|
// zone, through the alpha-preserving GPU sweep (the Canvas2D
|
||||||
// with 'behind'/'clear' art -- the Help panel says so too).
|
// 'difference' fallback stands down for behind/clear art); on
|
||||||
|
// forces the sweep at any persist < 1; off keeps the wash.
|
||||||
const t = value.toLowerCase()
|
const t = value.toLowerCase()
|
||||||
if (!['on', 'off', '1', '0', 'true', 'false', 'yes', 'no'].includes(t)) {
|
if (!['on', 'off', 'auto', '1', '0', 'true', 'false', 'yes', 'no'].includes(t)) {
|
||||||
return { script: null, meta: { ...meta }, error: { message: `trueblack must be on/off (got '${value}')`, section: 'header', line: lineNo } }
|
return { script: null, meta: { ...meta }, error: { message: `trueblack must be on/off/auto (got '${value}')`, section: 'header', line: lineNo } }
|
||||||
}
|
}
|
||||||
meta.trueblack = ['on', '1', 'true', 'yes'].includes(t)
|
meta.trueblack = t === 'auto' ? 'auto' : ['on', '1', 'true', 'yes'].includes(t) ? 'on' : 'off'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'linewidth': {
|
case 'linewidth': {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,24 @@ export interface ReleaseNote {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RELEASE_NOTES: ReleaseNote[] = [
|
export const RELEASE_NOTES: ReleaseNote[] = [
|
||||||
|
{
|
||||||
|
version: '7.19.0',
|
||||||
|
name: 'Automatic true-black',
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
kind: 'fixed',
|
||||||
|
text: 'Trails now fade all the way to pure black on their own. The fade works in 8-bit steps, so a persist of 0.5 or more used to leave a faint stuck residue behind everything that moved -- a colored smear where rings or blooms had passed. The engine now sweeps that residue away every frame, in every script, with no header needed.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'improved',
|
||||||
|
text: 'The sweep runs on the GPU as an exact -1/255 step that also preserves transparency, so behind/clear art keeps its holes (they heal to truly empty now too), and black stays a true black with no flicker. Scripts with a low persist pay nothing -- the sweep only switches on in the range where the 8-bit math could stick.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'improved',
|
||||||
|
text: 'The trueblack: header grows an auto mode (the new default) next to on and off, and the Help panel documents all three.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '7.18.0',
|
version: '7.18.0',
|
||||||
name: 'Beautiful Spectrogram',
|
name: 'Beautiful Spectrogram',
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,38 @@
|
||||||
// warns in the console when the two drift apart.
|
// warns in the console when the two drift apart.
|
||||||
//
|
//
|
||||||
// History:
|
// History:
|
||||||
|
// 7.19 — Automatic true-black (user request, "Texe-rings"): the v7.17
|
||||||
|
// trueblack tail is no longer opt-in. Root cause of the smear
|
||||||
|
// the user saw: the persistence fade multiplies the framebuffer
|
||||||
|
// by persist in 8-bit steps, and round-to-nearest leaves fixed
|
||||||
|
// points -- at persist 0.78 every premultiplied value 1..2
|
||||||
|
// satisfies round(v*0.78) == v, so ring trails settle into a
|
||||||
|
// faint permanently-stuck wash (colored, not gray, because the
|
||||||
|
// script's channels divide by different constants and each
|
||||||
|
// channel sticks at its own quantum). No multiply-only
|
||||||
|
// compositing can fix that (proven: unsticking v=1 needs a
|
||||||
|
// multiplier < 0.5, which halves the trail every frame), so the
|
||||||
|
// fix is a subtractive -1/255/frame tail -- and the reason it
|
||||||
|
// could become automatic is a NEW implementation: shader mode 22
|
||||||
|
// in the WebGL compositor (blend-gpu.ts) clamps
|
||||||
|
// max(premult-1/255, 0) on rgb AND alpha in one pass. That is
|
||||||
|
// exact in 8-bit (black stays a true 0, unlike the Canvas2D
|
||||||
|
// 'difference' fill's 0<->1 flicker) and alpha-PRESERVING, so
|
||||||
|
// behind/clear art keeps its holes (which now heal to truly
|
||||||
|
// empty too -- stuck alpha quanta sweep like everything else).
|
||||||
|
// Engine policy: the sweep runs by default exactly in the
|
||||||
|
// sticky zone (persist >= 0.5), so low-persist scripts pay
|
||||||
|
// nothing and nothing visual changes where the multiply already
|
||||||
|
// decayed fully. The old 'difference' fill survives only as the
|
||||||
|
// no-WebGL fallback, and it stands down once a script has drawn
|
||||||
|
// with behind/clear (the alphaArt latch -- pre-latched from the
|
||||||
|
// header blend, latched at draw time on a per-frame env.blend
|
||||||
|
// switch). The trueblack: header becomes a tri-state: auto
|
||||||
|
// (default, the new behavior), on (forced at any persist < 1,
|
||||||
|
// the v7.17 flag), off (never -- the wash look, for authors who
|
||||||
|
// want it). Texe-rings itself needs no edit: at 0.78 the auto
|
||||||
|
// sweep clears each channel's quanta in 2-3 frames after the
|
||||||
|
// multiply stalls, and the background returns to pure black.
|
||||||
// 7.18 — "Beautiful Spectrogram" (user request, twelfth Zai-Magic
|
// 7.18 — "Beautiful Spectrogram" (user request, twelfth Zai-Magic
|
||||||
// preset): a true scrolling spectrogram as a pure script -- the
|
// preset): a true scrolling spectrogram as a pure script -- the
|
||||||
// v7.12 bgx feedback slide moves the whole framebuffer left
|
// v7.12 bgx feedback slide moves the whole framebuffer left
|
||||||
|
|
@ -616,4 +648,4 @@
|
||||||
|
|
||||||
export const APP_NAME = 'Open Visualization Engine'
|
export const APP_NAME = 'Open Visualization Engine'
|
||||||
export const APP_SHORT = 'OVE'
|
export const APP_SHORT = 'OVE'
|
||||||
export const APP_VERSION = '7.18.0'
|
export const APP_VERSION = '7.19.0'
|
||||||
|
|
|
||||||
30
worklog.md
30
worklog.md
|
|
@ -2684,3 +2684,33 @@ BROWSER E2E (agent-browser, ALL PASS):
|
||||||
|
|
||||||
Stage Summary:
|
Stage Summary:
|
||||||
- v7.18.0 shipped: "Beautiful Spectrogram", the twelfth hand-authored Zai-Magic preset -- a true scrolling spectrogram as a pure script (bgx slide + persist 1 zero-fade + megabuf bin memory + palette table), with the per-frame ink computed from live stroke geometry so the accumulation lands on the palette at any fps/aspect/brush width. Browser-verified end to end: navy field, pink-to-white print, leftward crawl at design rate, no channel contamination, clean console.
|
- v7.18.0 shipped: "Beautiful Spectrogram", the twelfth hand-authored Zai-Magic preset -- a true scrolling spectrogram as a pure script (bgx slide + persist 1 zero-fade + megabuf bin memory + palette table), with the per-frame ink computed from live stroke geometry so the accumulation lands on the palette at any fps/aspect/brush width. Browser-verified end to end: navy field, pink-to-white print, leftward crawl at design rate, no channel contamination, clean console.
|
||||||
|
|
||||||
|
---
|
||||||
|
Task ID: 52
|
||||||
|
Agent: main (Z.ai Code)
|
||||||
|
Task: v7.19.0 (user request, "Texe-rings" script): diagnose why the background where the two rings move over leaves a slight off color instead of returning to pure black -- fix it wherever the fault lies (script, engine, blend, or persist/fade).
|
||||||
|
|
||||||
|
Diagnosis (engine, not the script):
|
||||||
|
- The persistence fade multiplies the framebuffer by persist via destination-out, quantized to 8 bits with round-to-nearest. For any persist >= 0.5 there are fixed points: round(v*p) == v. At the script's persist 0.78 every premultiplied value 1..2 is stuck; ring trails therefore settle into a permanent faint wash. Colored, not gray, because the script divides its channels by different constants (red /7, green /12, blue /20) so each channel freezes at its own quantum.
|
||||||
|
- Proof that no multiply-only compositing can fix it: unsticking v=1 requires a multiplier < 0.5, which halves the trail every frame. Subtraction (or float precision) is required -- exactly why v7.17's opt-in trueblack: on existed. The script simply did not opt in, and the v7.17 tail could not be made default because the Canvas2D 'difference' fill forces every pixel opaque (breaks behind/clear art) and leaves untouched black oscillating 0<->1.
|
||||||
|
|
||||||
|
Fix (default-on, exact, alpha-safe):
|
||||||
|
- blend-gpu.ts: new shader mode 22 + BlendCompositor.sweep() -- one GPU pass computing max(premult-1/255, 0) on rgb AND alpha. Exact in 8-bit (black stays a true 0) and alpha-preserving, which is what makes automatic default-on safe: behind/clear holes keep healing to truly empty too.
|
||||||
|
- engine.ts: after the destination-out erase, the sweep runs automatically exactly in the sticky zone (persist >= 0.5; below it every quantum already decays, so low-persist scripts pay nothing). GPU path preferred; the 'difference' fill survives only as the no-WebGL fallback and stands down once the script draws behind/clear (new alphaArt latch -- pre-latched from the header blend, latched at draw time on a per-frame env.blend switch; the GPU sweep never stands down).
|
||||||
|
- script.ts: trueblack header becomes a tri-state -- auto (default: sweep only in the sticky zone), on (forced at any persist < 1, the v7.17 flag), off (never; the wash look). persist 1 keeps its zero-fade semantics.
|
||||||
|
- Docs: help-panel persist row + file-format block + explainer rewritten for auto; version.ts 7.19.0 + history entry; release-notes 7.19.0 "Automatic true-black" (fixed + 2x improved).
|
||||||
|
|
||||||
|
Work Log:
|
||||||
|
- src/lib/ava/blend-gpu.ts: mode 22 shader branch + sweep() (single dst texture upload; uSrc pointed at the same texture, unread).
|
||||||
|
- src/lib/ava/engine.ts: file-header v7.19 section; alphaArt field + pre-latch in setScript + reset in clearScript + draw-time latch; sweepViaGpu() helper (copy-back like the GPU blend path, gpuBroken latching on throw); fade section rewritten (tri-state gate, GPU-first, gated fallback).
|
||||||
|
- src/lib/ava/script.ts: ScriptMeta.trueblack: 'on' | 'off' | 'auto'; DEFAULT_META 'auto'; parser accepts auto alongside on/off/1/0/true/false/yes/no; header docs updated.
|
||||||
|
- src/components/ava/help-panel.tsx, src/lib/version.ts, src/lib/release-notes.ts: docs + version bump per the standing rule.
|
||||||
|
|
||||||
|
BROWSER E2E (agent-browser, ALL PASS):
|
||||||
|
- Bug reproduced first: time-driven sweep probe (no audio needed), persist 0.9, trueblack off -- old-trail strip frozen at premultiplied 4 (max 4, avg 4, 100% nonzero) across two samples 3 s / ~140 frames apart. Non-decaying fixed point, exactly the reported smear.
|
||||||
|
- Fix verified: same probe with trueblack auto -- old-trail strip max 0, avg 0, 0% nonzero, alpha healed to 0 (GPU sweep path; WebGL confirmed available). Active drawing unharmed: the bar itself renders 255 with a ~21% fresh wake.
|
||||||
|
- Texe-rings verbatim (persist 0.78, no script edits): compiles (dots, n=100), runs against the Demo track (BPM 116.9); center disc census 81% exact-zero pure black, stuck band only 2-3% and moving between samples (the live decay wake behind the rings, not frozen residue), outer region pure black, 24 fps headless with audio decode.
|
||||||
|
- App: title v7.19.0; What's New leads with 7.19.0 "Automatic true-black" (CURRENT); console clean after reload (one stale version-drift warning from a mid-edit Fast Refresh cycle, gone on reload); mobile 420x800 footer flush at content end, zero gap; bun run lint clean; dev.log 200s only.
|
||||||
|
|
||||||
|
Stage Summary:
|
||||||
|
- v7.19.0 shipped: the trueblack tail is automatic. Every trail now ends in pure black at any persist without opting in, via an exact -1/255 GPU sweep that preserves alpha (behind/clear safe); the Canvas2D difference tail is demoted to a gated no-WebGL fallback; the header is a tri-state (auto/on/off). Texe-rings works unmodified -- the smear is gone and the background returns to pure black behind the rings.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue