62fe0e7e-3025-47cc-bab0-d72875f1698e

This commit is contained in:
Z User 2026-11-04 23:55:48 +00:00
parent 401963b9f3
commit 1ea335a2fe
12 changed files with 1565 additions and 12 deletions

Binary file not shown.

View file

@ -23,6 +23,9 @@ const FRAME_VARS: [string, string][] = [
['w / h', 'canvas size in pixels'],
['aspect', 'canvas width / height -- divide x by it for perfect circles'],
['persist', 'motion-trail persistence 0..0.98 (writable per frame)'],
['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'],
['bgzoom', 'background scale per frame -- below 1 draw-in (shrinks toward the center), above 1 blow-out (expands); 1 = off'],
]
const POINT_VARS: [string, string][] = [
@ -62,6 +65,9 @@ const EXAMPLES: [string, string][] = [
['rot = rot + dt * 1.2 + beat * 0.4', 'beat kick'],
['ang = rand(i) * 6.28318', 'stable per-point random'],
['x = sin(i * 6.28318 + t) * rad / aspect', 'circle'],
['bgrot = dir * (0.004 + exp(-beatage * 3) * 0.02)', 'the background spins to the music -- every beat accelerates it, then it relaxes (slow persist blends the trails)'],
['if(above(bass, bass_att * 1.25), dir = -dir, 0)', 'a hard hit flips the background spin direction -- put this in [beat]'],
['bgzoom = 1 - 0.001', 'draw-in -- the picture slowly spirals toward the center'],
]
function HelpTable({ rows, mono = true }: { rows: [string, string][]; mono?: boolean }) {

View file

@ -7,16 +7,18 @@
// for originals that rely on effects the script engine cannot run.
//
// The "Zai-Magic" folder at the END of EMBEDDED_LIBRARY is the exception to
// "do not edit": ten presets HAND-AUTHORED for this engine (v7.00) to show
// off beat-synced color — they use hsvr/hsvg/hsvb, beatage, megabuf particle
// physics, getspec() and loop(). If the converter ever regenerates this
// file, keep that section verbatim.
// "do not edit": eleven presets HAND-AUTHORED for this engine (v7.00 + the
// v7.12 osc-rotations addition) to show off beat-synced color and the v7.12
// background transform -- they use hsvr/hsvg/hsvb, beatage, megabuf particle
// physics, getspec(), loop() and the bg* framebuffer controls. If the
// converter ever regenerates this file, keep that section verbatim.
//
// EMBEDDED_SEED_VERSION: bump whenever this library is regenerated — the
// GET /api/presets route re-seeds the root/embedded subtree when it changes
// (v7.10: em dashes in descriptions/script comments replaced with '--').
// (v7.10: em dashes in descriptions/script comments replaced with '--';
// v7.12: eleventh Zai-Magic preset "osc-rotations" added).
export const EMBEDDED_SEED_VERSION = '7.10'
export const EMBEDDED_SEED_VERSION = '7.12'
export interface EmbeddedPreset {
folder: string
@ -842,11 +844,13 @@ export const EMBEDDED_LIBRARY: EmbeddedPreset[] = [
},
// ==========================================================================
// Zai-Magic — ten presets hand-authored for this engine (v7.00).
// Every one is built around BEAT-SYNCED COLOR: the color wheel turns on
// the kick, palettes swap per beat or per 4-beat bar, and kick envelopes
// (beatage) drive brightness. Techniques span all three render modes,
// megabuf particle physics, direct getspec() calls and loop() setup.
// Zai-Magic — eleven presets hand-authored for this engine (v7.00; the
// eleventh, "osc-rotations", shipped with v7.12). Every one is built around
// BEAT-SYNCED COLOR: the color wheel turns on the kick, palettes swap per
// beat or per 4-beat bar, and kick envelopes (beatage) drive brightness;
// osc-rotations adds the v7.12 background transform (bgrot/bgzoom) to the
// toolkit. Techniques span all three render modes, megabuf particle
// physics, direct getspec() calls and loop() setup.
// KEEP VERBATIM when regenerating this file with the converter.
// ==========================================================================
{
@ -1314,6 +1318,46 @@ red = hsvr(hue, sat, lum + flash * lit * 0.55)
green = hsvg(hue, sat, lum + flash * lit * 0.55)
blue = hsvb(hue, sat, lum + flash * lit * 0.55)
size = 2.4 + lit * 1.7 + treb * 1.2
`,
},
{
folder: "root/embedded/Zai-Magic",
name: "osc-rotations",
author: "Zai",
description:
"A stationary centered oscilloscope over a background that rotates to the music -- every beat temporarily accelerates the spin, a hard hit flips it between clockwise and counter-clockwise, and the fade to black stays slow so the colors blend. The left and right points of the line color-shift together while the center points hold their own hue. Hand-authored for OVE (v7.12 background transform + mirror-symmetric hsv color).",
content: `name: "osc-rotations"
author: "Zai"
mode: lines
blend: additive
persist: 0.965
linewidth: 2.2
[init]
n = 512
hue = 0.58
dir = 1
[frame]
punch = exp(-beatage * 3.2)
bgrot = dir * (0.0032 + punch * (0.014 + vol_att * 0.012))
bgzoom = 1 - 0.0009 - punch * 0.0011
hue = frac(hue + dt * 0.012 + beat * 0.02)
[beat]
if(above(bass, bass_att * 1.25), dir = -dir, 0)
[point]
x = i * 2 - 1
y = v * (0.3 + vol_att * 0.8)
d = abs(i - 0.5) * 2
h = hue + d * 0.34
lum = 0.5 + 0.38 * vol_att + punch * 0.22
red = hsvr(h, 0.95, lum)
green = hsvg(h, 0.95, lum)
blue = hsvb(h, 0.95, lum)
size = 2 + punch * 1.6
alpha = 0.92
`,
},
]

View file

@ -16,6 +16,15 @@ import { DEFAULT_BAND_CONFIG, DEFAULT_BEAT_CONFIG } from './types'
const MAX_POINTS = 8192
const POINT_STRIDE = 8 // x, y, red, green, blue, skip, size, alpha
/** Read a script bg* control value. Non-finite (NaN / Infinity from a bad
* script computation) and missing values fall back to the neutral value --
* a NaN must never reach a canvas transform (it poisons the matrix and every
* later draw becomes a silent no-op: a black screen with no error). */
function bgNum(v: number | undefined, neutral: number): number {
const x = typeof v === 'number' ? v : neutral
return Number.isFinite(x) ? x : neutral
}
export interface EngineStats {
fps: number
levels: BandLevels
@ -54,6 +63,12 @@ export class AvaEngine {
/** ctx.filter support (older Safari lacks it → plain crossfade fallback). */
private canFilter = false
/** v7.12 background-feedback buffer. Same pixel size as the live canvas;
* recreated automatically on resize. Null until a script first asks for a
* background transform (zero cost for every preset that doesn't). */
private bgBuffer: HTMLCanvasElement | null = null
private bgBufCtx: CanvasRenderingContext2D | null = null
constructor() {
// AVS-eel compatibility hooks — the expression engine reads audio/time
// through these (getspec/getosc/gettime). Headless tests can override.
@ -141,6 +156,60 @@ export class AvaEngine {
return this.script?.meta.name ?? '--'
}
/**
* v7.12 — background feedback transform (the AVS movement family).
*
* The whole framebuffer is moved BEFORE this frame's points are drawn:
* 1. snapshot the live canvas into the offscreen buffer;
* 2. wipe the canvas black;
* 3. draw the snapshot back through the frame's transform.
*
* bgrot radians per frame — + rotates the picture CLOCKWISE on screen
* (canvas y is down, so ctx.rotate(+θ) spins cw); - = ccw.
* bgx/bgy drift per frame in the -1..1 screen convention (y is UP,
* matching script x/y) — slide up, down, left, right.
* bgzoom per-frame scale — <1 draw-in (shrinks toward center),
* >1 blow-out (expands outward); 1 = neutral.
*
* Uncovered regions (rotated corners, slid-off edges) read black, and the
* persistence fade (persist header / env.persist) blends the trails —
* which is exactly how the classic AVS plugins made "speed accelerates to
* the beat, a hard hit flips the spin, fade to black stays slow" work:
* the rate is just a script variable driven by beatage envelopes, and a
* [beat] section flips the sign on a hard hit (bass vs bass_att).
*/
private applyBackground(w: number, h: number, rot: number, tx: number, ty: number, zoom: number): void {
const canvas = this.canvas
const ctx = this.ctx
if (!canvas || !ctx) return
if (!this.bgBuffer || this.bgBuffer.width !== w || this.bgBuffer.height !== h) {
const buf = document.createElement('canvas')
buf.width = w
buf.height = h
this.bgBuffer = buf
this.bgBufCtx = buf.getContext('2d')
}
const bctx = this.bgBufCtx
if (!bctx || !this.bgBuffer) return
// 1. snapshot the live canvas ('copy' replaces the buffer wholesale)
bctx.globalCompositeOperation = 'copy'
bctx.drawImage(canvas, 0, 0)
// 2. wipe — the transformed snapshot must REPLACE the old content, not
// double-expose over it
ctx.globalCompositeOperation = 'source-over'
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, w, h)
// 3. draw the snapshot back through this frame's transform. Screen y is
// down; script y is up — bgy > 0 moves the picture UP.
ctx.save()
ctx.imageSmoothingEnabled = true
ctx.translate(w / 2 + tx * (w / 2), h / 2 - ty * (h / 2))
if (rot !== 0) ctx.rotate(rot)
if (zoom !== 1) ctx.scale(zoom, zoom)
ctx.drawImage(this.bgBuffer, -w / 2, -h / 2)
ctx.restore()
}
private resize(): void {
if (!this.canvas) return
const parent = this.canvas.parentElement
@ -227,6 +296,20 @@ export class AvaEngine {
} catch (err) {
this.scriptError = `runtime error: ${String(err)}`
}
// ---- background feedback transform (v7.12) --------------------------
// AVS-style whole-framebuffer movement, read AFTER [frame]/[beat] ran
// so this frame's values apply this frame. Set from [init] (persists
// forever), [frame] or [beat] (every frame / on the kick). Values live
// in env like any variable: a script that sets them only on beats gets
// on-beat jumps. Neutral readings skip the pass entirely.
const bgRot = Math.max(-0.6, Math.min(0.6, bgNum(env.bgrot, 0)))
const bgX = Math.max(-0.25, Math.min(0.25, bgNum(env.bgx, 0)))
const bgY = Math.max(-0.25, Math.min(0.25, bgNum(env.bgy, 0)))
const bgZoom = Math.max(0.5, Math.min(2, bgNum(env.bgzoom, 1)))
if (bgRot !== 0 || bgX !== 0 || bgY !== 0 || bgZoom !== 1) {
this.applyBackground(w, h, bgRot, bgX, bgY, bgZoom)
}
}
// ---- point evaluation ---------------------------------------------------

View file

@ -39,6 +39,20 @@ export interface ReleaseNote {
}
export const RELEASE_NOTES: ReleaseNote[] = [
{
version: '7.12',
name: 'Background movement (AVS-style) + osc-rotations',
changes: [
{
kind: 'added',
text: 'Scripts can move the whole picture now: bgrot spins the background clockwise or counter-clockwise, bgx / bgy slide it up, down, left or right, and bgzoom draws it in or blows it out -- the movement family the classic Winamp AVS plugins were built on. The speed can ride the music: accelerate on every beat, flip the spin direction on a hard hit, and let a slow fade to black blend the colors. All four variables are documented in the script editor\'s help panel.',
},
{
kind: 'added',
text: 'I added an eleventh Zai-Magic preset: "osc-rotations" -- a stationary centered oscilloscope whose left and right points color-shift together while the center points hold their own hue, over a background that rotates clockwise and counter-clockwise to the music and temporarily accelerates with the beat. Hard hits flip the spin; the fade stays slow so the colors blend.',
},
],
},
{
version: '7.11',
name: 'Wake lock, a respectful Demo, one-click save',

View file

@ -12,6 +12,40 @@
// warns in the console when the two drift apart.
//
// History:
// 7.12 — background movement + osc-rotations. Scripts can now move the
// whole framebuffer every frame, the way the classic Winamp AVS
// plugins did: bgrot (radians/frame, + clockwise on screen),
// bgx / bgy (per-frame drift in the -1..1 screen convention, y
// up), bgzoom (< 1 draw-in, > 1 blow-out) -- read after [frame]/
// [beat] so this frame's values apply this frame. The engine
// snapshots the canvas into an offscreen buffer, wipes, and
// draws the snapshot back through the transform, so the
// persistence fade blends the moving trails: "speed accelerates
// to the beat, a hard hit flips the spin, fade to black stays
// slow" is pure script (a beatage envelope drives the rate; a
// [beat] section flips a dir variable on hard hits via bass vs
// bass_att). Neutral readings (0/0/0/1) skip the pass entirely
// (zero cost for every preset that doesn't move); non-finite
// values fall back to neutral (a NaN in a canvas transform
// matrix would black the screen with no error); per-frame
// clamps: rot ±0.6 rad, drift ±0.25, zoom 0.5..2. New 11th
// Zai-Magic preset "osc-rotations" (user-requested): a
// stationary centered oscilloscope (time-domain v) whose color
// is mirror-symmetric -- points equidistant left/right of
// center shift together, the center points hold their own hue
// -- over a background rotating cw/ccw to the music that
// temporarily accelerates on the beat; hard hits
// (bass > 1.25 × bass_att) flip the spin; a whisper of draw-in
// (bgzoom ~0.999) spirals the trails inward; persist 0.965
// keeps the fade slow so colors blend. Help panel: bg* variable
// rows + three new example formulas. EMBEDDED_SEED_VERSION
// 7.10 -> 7.12 (library re-seed adds the preset; the share
// codec's dictionary stays frozen per its compatibility
// contract -- bg* identifiers ride the pair packing like any
// custom variable). Headless validation of the preset's
// compiled programs: quiet 0.004 rad/frame, hard beat 0.026
// (6.5×) + direction flip, soft beat no flip, mirrored points
// share hue / center distinct
// 7.11 — wake lock, a respectful Demo button, one-click save. While any
// audio source is live the site holds a Screen Wake Lock so phone
// and tablet screens stay on (released the moment the engine goes
@ -340,4 +374,4 @@
export const APP_NAME = 'Open Visualization Engine'
export const APP_SHORT = 'OVE'
export const APP_VERSION = '7.11'
export const APP_VERSION = '7.12'

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because it is too large Load diff

View file

@ -2240,3 +2240,55 @@ VERIFICATION (three independent measurements):
Stage Summary:
- v7.10.1 shipped: the frozen right edge of every spectrum visual is unfrozen. The bar field now spans ~12 Hz to 16 kHz (the range music actually occupies) instead of stretching to Nyquist, so the right bars land on the 10-16 kHz air band and dance with the hats/cymbals. The user's own Spectrum Bars script (whose 0.78+0.45*f treble tilt now has something real to boost) verified live: rightmost bars swing 4..76%.
---
Task ID: 38
Agent: main (Z.ai Code)
Task: v7.11 (user request, three parts). RESTORED RETROACTIVELY — the entry was lost in a session cutoff (same as Task 35 once was); scope reconstructed from the version.ts 7.11 history and the tool-results/v711-* evidence screenshots (v711-01 through v711-22, all PASS). (1) Mobile wake lock: while any audio source is live the site holds a Screen Wake Lock so phone/tablet screens stay on; released the moment the engine goes idle so normal inactivity dimming returns. (2) A respectful Demo Track button: no longer force-engages the Sequence checkbox when the visitor has loaded a preset (tree double-click / Load / copied preset) or brought their own script material into the editor — Demo just plays the track; the Zai-Magic showcase only starts for a visit that never picked or typed anything. (3) Script tab one-click Save: a valid script shows the Save button with no save target needed; auto-names from the script's name header (a hash import of "Spectrum Bars:[▼…]" saves as "Spectrum Bars"), "Empty Name" when nameless, " (1)"/" (2)"… suffixes on conflicts — and when the name had to be invented, a "Give This Preset a Name" dialog opens to rename the just-saved preset in one step (saved FIRST, renamed second, so nothing is ever lost).
Work Log (reconstructed):
- src/lib/ava/wake-lock.ts (NEW): Screen Wake Lock sentinel manager — acquire on source-live, release on idle, visibilitychange re-acquire (browsers silently drop the sentinel on hide), silent degradation where unsupported.
- audio-source.ts / control-panel / page.tsx: per-visit manualChoice flag + boot-script snapshot decide whether the Demo showcase may engage; ensureDefaultPreset gained the same guard (mic/file/link never clobber a custom draft either). Demo teardown paths (ended/stop/switch) all respect it.
- script-tab.tsx: Save button visible on any valid script with no target; auto-naming ladder (script name header -> hash import label -> "Empty Name" -> conflict suffixes); "Give This Preset a Name" dialog after any invented save. Hash⇢Script imports clear the stale save target and pass the decoded label through as the fallback name.
- version.ts 7.11 + history; release-notes.ts 7.11 entry.
E2E (evidence: tool-results/v711-01..22, ALL PASS):
- Fresh boot (no selection, no editor material): Demo engages the sequence showcase as before — v711-19/20.
- Preset loaded first (double-click, v711-03; embedded, v711-04): Demo plays the track, Sequence stays UNCHECKED (v711-06), the running script is untouched — a custom script can be debugged against the demo track without fear of replacement (v711-05 custom material case).
- Hash⇢Script -> Save: lands under "user created" as "Spectrum Bars" from the share-string label (v711-09/10/11); nameless script saves as "Empty Name" then the rename dialog appears (v711-12/15); duplicate conflicts get " (1)"/" (2)" suffixes (v711-16/17) and the rename updates the saved preset (v711-13).
- Default boot script + reload states (v711-18); mobile 375 px (v711-21) and desktop (v711-22) clean; wake lock verified by code path (headless browsers do not expose navigator.wakeLock — degrade-silent branch exercised).
Stage Summary:
- v7.11 shipped: the screen stays on while music plays, the Demo button stopped hijacking the preset selection, and one click saves any valid script with a smart name. This restored entry closes the gap in the build diary.
---
Task ID: 39
Agent: main (Z.ai Code)
Task: v7.12 (user request): add the classic-AVS background-movement family to the scripting — the background shifts up/down/left/right, spins clockwise/counter-clockwise, draws in and blows out, with the speed accelerating to the beat and hard hits flipping the spin direction while the fade to black stays slow (so colors blend) — then ship a permanent Zai-Magic preset "osc-rotations": a stationary centered oscilloscope whose left/right points color-shift together and whose center points hold their own hue, over a background rotating cw/ccw to the music with temporary beat acceleration.
Work Log:
- src/lib/ava/engine.ts — NEW background feedback transform (the AVS movement family):
* Script controls, read from env AFTER [frame]/[beat] run so this frame's values apply this frame: bgrot (radians/frame, + = CLOCKWISE on screen — canvas y is down so ctx.rotate(+) spins cw), bgx/bgy (per-frame drift, the -1..1 screen convention, y is UP matching script x/y), bgzoom (per-frame scale: <1 draw-in, >1 blow-out, 1 = neutral). Values persist in env like any variable — set from [init] (persists forever), [frame] (every frame) or [beat] (on-beat jumps); neutral readings skip the pass entirely (zero cost for every preset that doesn't move).
* applyBackground(): snapshot the live canvas into an offscreen buffer (globalCompositeOperation 'copy'), wipe the canvas black (the transformed snapshot must REPLACE the old content, not double-expose), then draw the snapshot back through the frame's transform (translate w/2+bgx*w/2, h/2-bgy*h/2; rotate; scale). Uncovered regions (rotated corners, slid-off edges) read black and the persistence fade blends the trails — exactly how the classic AVS plugins made "speed accelerates to the beat, a hard hit flips the spin, fade stays slow" work: the rate is just a script variable driven by a beatage envelope, and a [beat] section flips a dir variable on hard hits (bass vs bass_att).
* Guards: bgNum() falls back to neutral for missing/NaN values (a NaN in a canvas transform matrix would black the screen with no error — every later draw becomes a silent no-op); per-frame clamps rot ±0.6 rad, drift ±0.25, zoom 0.5..2. Buffer recreated automatically on resize; null until first use (zero cost otherwise). Gated on this.script so a stale env after clearScript can never spin a blank canvas.
- src/lib/ava/embedded-presets.ts — eleventh Zai-Magic preset "osc-rotations" (user-named, lowercase so it sorts last in the tree): persist 0.965 (slow fade — colors blend), [frame] drives bgrot = dir * (0.0032 + punch * (0.014 + vol_att * 0.012)) with punch = exp(-beatage*3.2) (temporary beat acceleration that relaxes with the kick ring), bgzoom = 1 - 0.0009 - punch*0.0011 (a whisper of draw-in so trails spiral inward instead of ringing), hue drifts dt*0.012 + beat*0.02; [beat] flips dir on hard hits only (bass > 1.25 * bass_att — soft beats leave the spin alone); [point] draws the stationary centered scope x = i*2-1, y = v*(0.3+vol_att*0.8) with mirror-symmetric color h = hue + |i-0.5|*2 * 0.34 (points equidistant left/right of center share one hue, the center holds its own). EMBEDDED_SEED_VERSION 7.10 -> 7.12 (route re-seeds the subtree; fingerprint self-heal would have caught it too). "ten presets" comments updated to eleven (both the file-top and the Zai-Magic section header).
- src/components/ava/help-panel.tsx — bgrot / bgx/bgy / bgzoom rows in the variables table + three example formulas (beat-accelerated spin, hard-hit direction flip in [beat], draw-in).
- src/lib/version.ts 7.12 + history; src/lib/release-notes.ts 7.12 entry (2x added, first-person voice). Share codec deliberately untouched: its dictionary is a frozen compatibility contract (adding entries needs a new scheme marker) — bg* identifiers ride the pair packing like any custom variable.
HEADLESS VALIDATION (/home/z/tmp-validate/bg-transform/validate-osc.ts, ALL PASS):
- osc-rotations compiles; all 101 embedded presets still compile (regression guard).
- Compiled-program behavior: quiet frame bgrot 0.00397 rad/frame cw + bgzoom 0.99906; hard beat bgrot 0.0256 (6.5x) AND dir flips to -1; soft beat no flip; point pass x spans ±1 with |y| symmetric, mirrored positions share hue exactly (0.7906 both) while center is distinct (0.6206).
BROWSER E2E (agent-browser + pixel measurement + VLM, ALL PASS):
- Fresh boot: title "OVE -- Open Visualization Engine (v7.12)"; tree shows "Zai-Magic -- 11"; /api/presets re-seeded on the version gate and serves osc-rotations (106 presets total; sorts last in Zai-Magic as designed).
- Preset live on the demo collection (manual load first, then Demo Track — v7.11's respectful-Demo still holds: Random/Sequence stayed UNCHECKED with a preset loaded): footer "SCRIPT: osc-rotations · Demo Track 1/2 · FPS 59-60 · PTS 512 · BPM 132.3/138.8 locked". Mid-band (scope zone) mean 133/255; outer zones lit only by rotation-carried trails.
- bgrot isolation (line mathematically confined to the middle 35% band, so outer-zone content can ONLY be rotation-carried): outer zones hold a steady mean ~14/255 vs EXACTLY 0.00 in the no-bg control — rotation puts content where the line cannot reach.
- bgx sign: +0.012 drifts the bright cloud right (mean x 0.518); -0.012 mirrors it exactly (0.480) — the sign convention verified by symmetry.
- bgy sign: +0.02 pulls the bright cloud UP (mean y-from-top 0.368 vs 0.5 neutral).
- bgzoom: at 1.05/frame the outer zones went from dead 0.00 to ~38/255 (blow-out pushes the confined line's trails outward); at 1.003/frame with persist 0.92 trails die before traveling far (consistent with the fade math — equilibrium analysis, not a defect).
- What's New dialog: 7.12 "Background movement (AVS-style) + osc-rotations" first with CURRENT badge, drift guard silent. Help panel: bg* rows + examples all present.
- VLM on the live screenshot: horizontal oscillating waveform line at vertical center; large soft colorful trail masses around it; hue variation green at the outer left/right edges, yellow mid, red/pink at the center — the mirror-symmetric color design visibly realized; no defects; "60 FPS . 512 PTS" overlay normal.
- Console clean (0 errors/0 warnings); dev.log clean (59 successful requests, no compile errors). Footer: sticks to the viewport bottom on short pages (1280x800 Beat tab: docH 800, footer bottom 800) and pushes to the document end on long pages (375 px Presets tab: docH 1469, footer at doc end, no overlap); 375 px scrollWidth 375 = clientWidth (no horizontal overflow). Test-browser localStorage cleared afterwards.
Stage Summary:
- v7.12 shipped: scripts can move the whole picture — bgrot spins, bgx/bgy slide, bgzoom draws in / blows out — with the speed riding the music exactly the way the classic AVS plugins did it (beatage envelopes drive the rate, [beat] flips the spin on hard hits, persist keeps the fade slow so colors blend). The eleventh Zai-Magic preset "osc-rotations" demonstrates the full recipe: a stationary centered oscilloscope with mirror-symmetric color (left/right shift together, center holds its own hue) over a beat-accelerated, hard-hit-flipping rotating background.