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'],
|
||||
['w / h', 'canvas size in pixels'],
|
||||
['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'],
|
||||
['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'],
|
||||
|
|
@ -200,9 +200,9 @@ description: shown in the preset list
|
|||
mode: lines # lines | dots | solid
|
||||
blend: additive # any of the 41 keywords -- see the Blend modes table
|
||||
persist: 0.85 # trail persistence 0..1 (1 = never fades)
|
||||
trueblack: off # subtractive fade tail: trails reach PURE black even at
|
||||
# high persist (no gray wash) -- but it turns transparent
|
||||
# pixels opaque, so leave it off for behind/clear art
|
||||
trueblack: auto # on | off | auto (default auto): subtractive fade tail
|
||||
# so trails reach PURE black even at high persist --
|
||||
# auto runs it exactly in the sticky zone (persist >= 0.5)
|
||||
linewidth: 2
|
||||
|
||||
[init] # runs once on load
|
||||
|
|
@ -211,11 +211,13 @@ linewidth: 2
|
|||
[point] # runs n times per frame`}
|
||||
</pre>
|
||||
<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
|
||||
framebuffer in 8-bit steps, so values like 0.9 leave a stuck gray residue (round(v·p) = v for the smallest
|
||||
values); the tail subtracts 1/255 per frame so everything reaches true black -- long echo trails stay clean
|
||||
over a pure background. Because it forces pixels opaque, never use it together with{' '}
|
||||
<span className="font-mono">behind</span>/<span className="font-mono">clear</span> blending.
|
||||
<span className="font-mono text-emerald-400/90">trueblack: auto</span> keeps every trail honest: the fade multiplies the framebuffer in 8-bit
|
||||
steps, so a persist of 0.5 or more leaves a stuck residue (round(v·p) = v for the smallest values) -- a faint
|
||||
colored wash that never fully fades. auto (the default) subtracts 1/255 per frame exactly in that sticky zone,
|
||||
so everything ends in true black while cheap low-persist scripts pay nothing. The sweep runs on the GPU and
|
||||
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>
|
||||
</HelpSection>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@
|
|||
// negation, phoenix, grain extract/merge, reflect, glow, and the four
|
||||
// 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
|
||||
// (source-over, so the two-pass glow keeps its usual tuning), then:
|
||||
// 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
|
||||
|
||||
// -- 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
|
||||
// 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;
|
||||
|
|
@ -221,6 +235,28 @@ export class BlendCompositor {
|
|||
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
|
||||
* back onto the live canvas with composite 'copy'. */
|
||||
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
|
||||
// must not be combined with 'behind'/'clear' art -- opt-in, off by
|
||||
// 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 { BlendCompositor } from './blend-gpu'
|
||||
|
|
@ -122,6 +142,13 @@ export class AvaEngine {
|
|||
* nearest native operator (spec.fallbackOp). */
|
||||
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() {
|
||||
// AVS-eel compatibility hooks — the expression engine reads audio/time
|
||||
// through these (getspec/getosc/gettime). Headless tests can override.
|
||||
|
|
@ -191,6 +218,11 @@ export class AvaEngine {
|
|||
this.script = script
|
||||
this.scriptError = null
|
||||
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
|
||||
// the first real beat arrives.
|
||||
this.env = { n: 400, t: 0, time: 0, frame: 0, beats: 0, beatage: 99 }
|
||||
|
|
@ -205,6 +237,7 @@ export class AvaEngine {
|
|||
clearScript(): void {
|
||||
this.script = null
|
||||
this.scriptError = null
|
||||
this.alphaArt = false
|
||||
}
|
||||
|
||||
getScriptName(): string {
|
||||
|
|
@ -290,6 +323,31 @@ export class AvaEngine {
|
|||
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:
|
||||
* 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
|
||||
|
|
@ -435,9 +493,16 @@ export class AvaEngine {
|
|||
// persist thins trails exactly like the old rgb scaling did over the
|
||||
// black stage, while pixels that were painted behind / cleared keep
|
||||
// fading honestly.
|
||||
// v7.17: trueblack: on adds the subtractive tail (see the file header)
|
||||
// -- the 'difference' fill runs INSIDE this branch so persist 1 keeps
|
||||
// its true zero-fade (permanent) semantics.
|
||||
// v7.19: trueblack is AUTOMATIC (see the file header). The subtractive
|
||||
// tail runs right after the erase whenever the multiply can stick
|
||||
// (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 persistBase = meta ? meta.persist : 0.85
|
||||
const persist = Math.min(Math.max(this.env.persist ?? persistBase, 0), 1)
|
||||
|
|
@ -445,10 +510,14 @@ export class AvaEngine {
|
|||
ctx.globalCompositeOperation = 'destination-out'
|
||||
ctx.fillStyle = `rgba(0,0,0,${(1 - persist).toFixed(4)})`
|
||||
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.fillStyle = '#010101'
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
}
|
||||
}
|
||||
ctx.globalCompositeOperation = 'source-over'
|
||||
|
|
@ -583,6 +652,9 @@ export class AvaEngine {
|
|||
// so the stroke owns its pixels outright.
|
||||
const headerSpec = findBlend(meta.blend)
|
||||
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) {
|
||||
const viaGpu =
|
||||
spec.impl === 'gpu' &&
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@
|
|||
// per frame through the blend variable
|
||||
// (by its number in the Help table)
|
||||
// persist: 0.9 <- motion trail persistence 0..1 (1 = never fades)
|
||||
// trueblack: on <- v7.17: adds a -1/255/frame subtractive tail to
|
||||
// the trail fade so it reaches PURE black at any
|
||||
// persist (long trails normally settle into a
|
||||
// faint gray wash from 8-bit rounding)
|
||||
// trueblack: auto <- v7.19: tri-state subtractive tail on the
|
||||
// trail fade. auto (default): the engine
|
||||
// sweeps stuck 8-bit quanta to PURE black
|
||||
// exactly when the fade can stick
|
||||
// (persist >= 0.5); on: sweep at any
|
||||
// persist < 1; off: never (the old faint
|
||||
// wash look)
|
||||
// linewidth: 2
|
||||
//
|
||||
// [init] runs once when the script loads
|
||||
|
|
@ -33,9 +36,12 @@ export interface ScriptMeta {
|
|||
mode: RenderMode
|
||||
blend: BlendMode
|
||||
persist: number
|
||||
/** v7.17 — subtractive tail on the trail fade (see engine.ts). Off by default:
|
||||
* it forces every pixel opaque, which breaks 'behind'/'clear' transparency. */
|
||||
trueblack: boolean
|
||||
/** v7.19 — subtractive tail on the trail fade (see engine.ts), now a
|
||||
* tri-state. 'auto' (default): the tail runs exactly in the sticky
|
||||
* 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
|
||||
/** +1 (default, y is up) or -1 (AVS convention, y is down — used by auto-ported presets). */
|
||||
ysign: number
|
||||
|
|
@ -77,7 +83,7 @@ const DEFAULT_META: ScriptMeta = {
|
|||
mode: 'lines',
|
||||
blend: 'additive',
|
||||
persist: 0.85,
|
||||
trueblack: false,
|
||||
trueblack: 'auto',
|
||||
linewidth: 2,
|
||||
ysign: 1,
|
||||
}
|
||||
|
|
@ -191,18 +197,19 @@ function tryParse(text: string): { script: AvaScript | null; meta?: ScriptMeta;
|
|||
break
|
||||
}
|
||||
case 'trueblack': {
|
||||
// v7.17: on/off flag (also 1/0, true/false, yes/no). The engine's
|
||||
// fade multiplies the framebuffer by persist in 8-bit steps, so any
|
||||
// persist >= 0.5 has fixed points (round(v*p) == v) -- a gray wash
|
||||
// that never fully fades. The subtractive tail this flag enables
|
||||
// sweeps those quantum values to a true 0 every frame. Costs one
|
||||
// fullscreen 'difference' fill; forces pixels opaque (never combine
|
||||
// with 'behind'/'clear' art -- the Help panel says so too).
|
||||
// v7.19: tri-state (on/off/auto, also 1/0, true/false, yes/no).
|
||||
// The engine's fade multiplies the framebuffer by persist in 8-bit
|
||||
// steps, so any persist >= 0.5 has fixed points (round(v*p) == v)
|
||||
// -- a faint colored wash that never fully fades. auto (the new
|
||||
// default) sweeps those quantum values to a true 0 exactly in that
|
||||
// zone, through the alpha-preserving GPU sweep (the Canvas2D
|
||||
// 'difference' fallback stands down for behind/clear art); on
|
||||
// forces the sweep at any persist < 1; off keeps the wash.
|
||||
const t = value.toLowerCase()
|
||||
if (!['on', 'off', '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 } }
|
||||
if (!['on', 'off', 'auto', '1', '0', 'true', 'false', 'yes', 'no'].includes(t)) {
|
||||
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
|
||||
}
|
||||
case 'linewidth': {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,24 @@ export interface 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',
|
||||
name: 'Beautiful Spectrogram',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,38 @@
|
|||
// warns in the console when the two drift apart.
|
||||
//
|
||||
// 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
|
||||
// preset): a true scrolling spectrogram as a pure script -- the
|
||||
// v7.12 bgx feedback slide moves the whole framebuffer left
|
||||
|
|
@ -616,4 +648,4 @@
|
|||
|
||||
export const APP_NAME = 'Open Visualization Engine'
|
||||
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:
|
||||
- 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