651 lines
46 KiB
TypeScript
Executable file
651 lines
46 KiB
TypeScript
Executable file
// OVE — app identity + version stamp.
|
||
//
|
||
// Single source of truth for the product name and version shown in the UI
|
||
// (header, footer, document title). Bump APP_VERSION whenever we ship a
|
||
// large change so the running build can always be identified at a glance —
|
||
// useful both for the z.ai Version History and for naming local backups
|
||
// (v6.0 = rename to Open Visualization Engine + version indicator).
|
||
//
|
||
// STANDING RULE (since v7.6): every APP_VERSION bump MUST come with a
|
||
// matching entry at the top of RELEASE_NOTES in src/lib/release-notes.ts —
|
||
// that list powers the header's "What's New!" dialog. A dev-mode guard
|
||
// 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
|
||
// every frame (the time axis) while persist 1 (v7.14.1's
|
||
// zero-fade framebuffer) keeps the history locked, so the print
|
||
// only ever leaves by scrolling off the left edge. The write
|
||
// head is a 256-point vertical line at the right edge: sv (the
|
||
// per-point spectrum) drives a megabuf attack/slow-decay level
|
||
// per frequency bin, which indexes a six-anchor palette table
|
||
// (also megabuf) -- deep dark blue background, then pink -> red
|
||
// -> orange -> yellow -> white as bins get loud. Two tricks make
|
||
// it read clean: (1) the stroke is exactly four scroll-steps
|
||
// wide with its per-frame alpha pre-divided by the accumulation
|
||
// factor (core 0.9 + halo 0.22*3.2 passes, times four overlapping
|
||
// steps ~= 1.6*4), so overlapping columns sum to exactly full
|
||
// palette strength and the quiet field renders as SOLID navy
|
||
// rather than banded; (2) a half-second erase phase (per-frame
|
||
// persist override to 0.45) drains the previous preset's
|
||
// framebuffer before the zero-fade lock engages, and a 15x
|
||
// three-second fill sweep (plus a replay on stage resize) paints
|
||
// the navy field across the whole screen instead of making the
|
||
// visitor wait 25 s for the crawl to cover it.
|
||
// 7.17 — True-black trail fade (user request, "Orange Burst"): the
|
||
// persistence fade multiplies the framebuffer by persist in
|
||
// 8-bit steps, so any persist >= 0.5 leaves fixed points
|
||
// (round(v*p) == v -- at 0.88 every value 1..4 is stuck) and long
|
||
// trails settle into a gray wash that a bg* blow-out then smears
|
||
// into every corner; scripts were forced below 0.5, where the
|
||
// trail dies in ~5 frames and the blow-out echo becomes
|
||
// unreadable. New opt-in header trueblack: on adds a subtractive
|
||
// tail right after the alpha erase: one fullscreen 'difference'
|
||
// fill of rgb(1,1,1) = an exact -1/255 per pixel per frame, which
|
||
// sweeps the stuck quantum values to a true 0 while bright
|
||
// pixels never notice (a 0.93 multiply halves them every ~9
|
||
// frames anyway). Costs one extra fullscreen fill, only when the
|
||
// flag is on; runs inside the persist < 1 branch so persist 1
|
||
// keeps its zero-fade (permanent) semantics. The tail forces
|
||
// pixels opaque (alpha composites to 1 under 'difference'), so it
|
||
// must never be combined with 'behind'/'clear' art -- documented
|
||
// in the Help panel's file-format section, off by default. Also:
|
||
// the script parser's unknown-header error now lists all nine
|
||
// valid keys (ysign had gone missing from the list).
|
||
// 7.16 — Presets tab rearranged (user request): "instead of having the
|
||
// two sections side by side ... the 'Load Preset' button section
|
||
// is on top with full width use of the preset section, the height
|
||
// set to where there is no empty space being wasted (even when a
|
||
// preset is selected). The preset tree should be placed below
|
||
// that with its width expanded to use the whole width of the
|
||
// Presets section ... so the preset names won't be clipped due to
|
||
// a restriction of horizontal space." The old 58/42 side-by-side
|
||
// split is gone: the management panel (selection header, action
|
||
// buttons, club-mode randomizer) sits ON TOP at the section's
|
||
// full width and its natural height -- never stretched, so the
|
||
// dead space that used to sit under the buttons when a preset was
|
||
// selected is gone. Its buttons pack into one compact grid row on
|
||
// desktop (Load Preset | Rename | Delete for an own preset with
|
||
// the primary Load cell wider; Load | Copy to my presets for an
|
||
// embedded one; a 2x3 grid -- Create | Subfolder | Rename over
|
||
// Delete | Export | Import -- for a user folder), all cells one
|
||
// uniform h-9; phones keep the previous stacking (primary action
|
||
// full-width, pairs below). The tree follows BELOW at full width
|
||
// and fills every bit of height the tab has left (the tab body's
|
||
// h-full flex chain; capped at 420 px only below lg, where the
|
||
// page itself scrolls and the cap keeps nested scrolling sane) --
|
||
// full-width rows mean preset names never clip against the old
|
||
// 58% column's horizontal squeeze. The club-mode scope line and
|
||
// the active RANDOM/SEQUENCE pill now share one row (truncate +
|
||
// shrink-0) instead of stacking.
|
||
// 7.15.2 — Facebook page + theme switch relocation (user request): the
|
||
// project now has its own Facebook page, and the footer's
|
||
// right-hand group carries the link (facebook.com/openvisengine)
|
||
// beside the version stamp -- an icon-only button (lucide
|
||
// Facebook glyph) that opens in a new tab, same hover/focus
|
||
// treatment as the portfolio link. The light/dark tick switch
|
||
// (v6.3) moved out of the header into the Settings tab's
|
||
// Appearance section, as a "Light / dark mode" row ABOVE the
|
||
// base-theme cards -- the header is one control lighter and all
|
||
// appearance controls now live in one place. Mechanics are
|
||
// identical: checking = the white theme, unchecking returns to
|
||
// the visitor's stored dark base (kept fresh by the base cards),
|
||
// both choices persist per visitor, and the mount guard keeps
|
||
// the SSR render mismatch-free. The row states the current side
|
||
// ("Light theme" / "Dark theme") and explains the return-to-base
|
||
// behavior in its caption.
|
||
// 7.15.1 — the silence hold (user request): "if silence is detected,
|
||
// it needs to place an immediate override and hold on the beat
|
||
// detection / prediction ... if silence is no longer detected,
|
||
// release the hold ... (the waveforms effects should still come
|
||
// through but if there is no beat, the visual should stop acting
|
||
// like there is a beat)." Sustained near-digital quiet on the
|
||
// OVERALL level (< 0.006 for 300 ms — the 3-band mean the engine
|
||
// already computes; digital silence reads exactly 0, mastered
|
||
// music stays above ~0.05 even between hits) now places a HOLD:
|
||
// nothing fires — real or predicted — the tempo estimate and
|
||
// confidence freeze exactly where they are (the old path let the
|
||
// predictor fake beats for up to ~2 s into a silence while its
|
||
// confidence quietly eroded), and beatphase freezes for scripts
|
||
// instead of saturating at "beat imminent". The hold is STICKY
|
||
// (only a decisive level above 0.015 releases — noise-floor
|
||
// wobble never does) and the release is immediate, re-anchoring
|
||
// the grid from the phase captured the frame the audio went QUIET
|
||
// (before the debounce window, so a pause/resume keeps perfect
|
||
// time — the music continues from the exact position it stopped
|
||
// at); after a hard cut the comb + first real beats re-align
|
||
// within a couple of periods. The 300 ms debounce only has to
|
||
// beat the ~800 ms the old flux-domain path needed before it
|
||
// started faking beats
|
||
// (bassGone = 1.6 periods at 120 BPM), so inter-attack dips in
|
||
// sparse masters never trip it. "Set Beat" taps release the hold
|
||
// outright — the manual override. Band/waveform data is NOT
|
||
// gated: it still flows (and is simply ~0, because the audio is
|
||
// gone). Scripts get a silent variable (1 while held) and the
|
||
// beat indicator shows an amber HOLD badge while it is active.
|
||
// BeatFrame.silent carries the state; rust-agent/src/beat.rs
|
||
// ports the same gate (its bass level doubles as the silence
|
||
// signal) with the same thresholds.
|
||
// 7.15 — the complete blend mode collection (user request): the blend
|
||
// header grew from six keywords to a 41-mode registry
|
||
// (src/lib/ava/blend.ts) covering every compositing family in the
|
||
// request list -- the full Photoshop set, the GIMP grain pair,
|
||
// the After Effects stencil/silhouette masks, Apple/CSS plus-
|
||
// lighter/plus-darker and the Pegtop exotica (negation, phoenix,
|
||
// reflect, glow). Native keywords map straight to Canvas2D
|
||
// operators; the 21 modes Canvas2D lacks run through a new WebGL
|
||
// compositor (blend-gpu.ts) with per-pixel formulas from the W3C
|
||
// compositing spec, the glsl-blend collection and GIMP (phoenix =
|
||
// min - max + 1, verified against four independent sources; hard
|
||
// mix = vivid light thresholded at 0.5). 'subtract' is now a TRUE
|
||
// clamped subtract, replacing 7.14.1's difference approximation.
|
||
// The framebuffer went alpha-capable so behind and clear work
|
||
// honestly: the persistence fade is an alpha ERASE
|
||
// (destination-out) and the bg* wipe a clearRect -- over the
|
||
// black stage both read pixel-identically to the old opaque
|
||
// fills, so every existing preset renders as before. The blend
|
||
// variable can switch the mode per frame (same contract as
|
||
// persist; it carries the mode's 1-based registry number because
|
||
// the expression language is numeric-only -- the Help table lists
|
||
// every number). Names are case/separator-insensitive
|
||
// with aliases (color burn = colorburn = ColorBurn; plus-lighter
|
||
// = additive; plus-darker = linear burn). Help panel gained a
|
||
// grouped 41-row Blend modes table; parser errors point at it.
|
||
// WebGL-less browsers fall back to the nearest native operator
|
||
// per mode.
|
||
// 7.14.1 — Script engine: blend modes + zero-fade persistence (user
|
||
// request, Brainstorm preset session). The renderer's content
|
||
// pass could only composite additively ('lighter') or plain
|
||
// alpha; the persistence header was hard-capped at 0.98, so a
|
||
// feedback blow-out ALWAYS faded -- the user wanted colors that
|
||
// ride outward forever at full brightness, replaced at the center
|
||
// by new color. Blend header extended to six values: multiply,
|
||
// screen, subtract (canvas 'difference' |dst-src| -- Canvas2D has
|
||
// no clamped true-subtract op; difference is the classic
|
||
// approximation and can never climb to white) and replace
|
||
// (source-over with the CORE pass at FULL alpha -- fresh strokes
|
||
// overwrite the pixels under them instead of stacking, so a
|
||
// figure redrawn over its own accumulating trails keeps its color
|
||
// instead of saturating to white; halo pass 0.25, dots core
|
||
// alpha 1.0, solid fill alpha 1.0). persist header + env override
|
||
// widened 0..0.98 -> 0..1; at exactly 1 the fade fill is SKIPPED
|
||
// entirely (engine.ts). The pairing this was asked for: blend
|
||
// replace + persist 1 + the v7.12 bgzoom/bgrot feedback = a
|
||
// static-speed blow-out that never fades, the fresh drawing
|
||
// replacing the old color at the center -- the blow-out itself is
|
||
// the cleanup. Help panel + parser error strings updated; no
|
||
// library changes (existing presets keep additive/alpha and their
|
||
// behavior byte-for-byte).
|
||
// 7.14 — Arctic Rainbow re-imagined (user request): the old stand-in was
|
||
// plain white columns of rain dots sweeping upward. Rebuilt as a
|
||
// music-driven water spray, stateless projectile physics in the
|
||
// [point] program: each of up to 120 droplets is a deterministic
|
||
// parabola (own apex height 1.26..1.88, own flight time 1.9..3.4 s,
|
||
// phase-staggered clock; every splash cycle re-rolls its origin,
|
||
// drift and arch) -- launch from below the bottom edge, slow rise,
|
||
// hang, arch left or right, fall back off-screen. Drop count
|
||
// follows the music (vol_att/bass_att envelope, fast attack 6/s,
|
||
// slow release 1.2/s: silence = 0 drops, loud = all 120). Each
|
||
// drop owns a hue that shifts by its own per-drop amount on every
|
||
// detected beat, plus a subtle gradient along its tail. The tail
|
||
// is 12 explicit trail samples (0.014..0.44 s back along the arc)
|
||
// drawn as a tapered polyline in lines mode, gated by smoothstep
|
||
// on the fall speed: nothing while rising (crisp dot), fading in
|
||
// as the arch turns downward -- no persistence smear (persist
|
||
// 0.12). Slot map through cell centers (the v7.13.1 lesson), 14
|
||
// slots per drop (separator + head + 12 trail), n = 1680.
|
||
// EMBEDDED_SEED_VERSION 7.13.1 -> 7.14 re-seeds the library.
|
||
// 7.13.1 — Beat Matrix endpoint fix (user report): the rightmost column
|
||
// of dots sat one row short -- its bottom dot missing -- while a
|
||
// phantom dot spilled one column past the wall's right edge at
|
||
// the top. Root cause: the engine's point position i spans 0..1
|
||
// INCLUSIVE (classic AVS Superscope convention, engine.ts: i =
|
||
// k/(n-1)), but Beat Matrix's grid math assumed exclusive i = k/n:
|
||
// floor(i*cols) turned the last point into col 16 (one past the
|
||
// 16-column wall) at row 0, and column 15 was left with nine
|
||
// dots (rows 0-8, no bottom). Fix: the script now maps through
|
||
// cell centers -- cell = i*(cols*rows-1)+0.5, col =
|
||
// floor(cell/rows), row = floor(cell-col*rows) -- which puts
|
||
// every floor() half a cell away from an integer boundary, so
|
||
// the endpoint can never spill and float dust can never shuffle
|
||
// a dot. Full 16x10 bijection verified headlessly (all 160
|
||
// points in 160 distinct cells, col<=15, row<=9, bottom-right
|
||
// cell present). Sweep of the other 100 presets: every other
|
||
// floor(i*M) use is benign -- angular presets close their loop
|
||
// (bar=bars lands exactly on bar 0's angle), Aurora Veil's
|
||
// phantom lands off-screen (x=1.19), the megabuf particle
|
||
// presets draw their phantom slot at alpha 0, and the converted
|
||
// Winamp classics rendered identically under original AVS
|
||
// (which also uses inclusive i). EMBEDDED_SEED_VERSION 7.12 ->
|
||
// 7.13.1 re-seeds the library so every visitor gets the fixed
|
||
// Beat Matrix.
|
||
// 7.13 — one build, every feature: the parallel development lines are
|
||
// merged. This tree now carries BOTH the community OVE-Directory
|
||
// (v7.11 here) AND the parallel line's additions — the spectrum
|
||
// 16 kHz cap (its 7.10.1), the screen wake lock + respectful
|
||
// Demo button + one-click Save (its 7.11, folded into this
|
||
// history as 7.10.2 to keep one clean timeline), and the
|
||
// AVS-style background movement family (bgrot / bgx / bgy /
|
||
// bgzoom) with the eleventh Zai-Magic preset "osc-rotations"
|
||
// (its 7.12). Git three-way merge against the shared v7.10
|
||
// ancestor (identical tree hashes verified on both sides); the
|
||
// only hand-merged surface was the Script tab toolbar, which
|
||
// now carries the one-click Save and "Upload to OVE-Directory"
|
||
// side by side, and the two changelogs (here + release-notes).
|
||
// 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 — the community OVE-Directory. A sixth tab (Directory) lists every
|
||
// script published to the community directory — a separate VPS at
|
||
// s3374dir.openvisengine.com, reached ONLY through this server's
|
||
// backend proxy (/api/ove-directory/*), which holds the API key in
|
||
// env and re-validates every upload (the share string must decode
|
||
// AND compile). Publishing: the Script tab gains "Upload to
|
||
// OVE-Directory" (enabled only while the editor shows ✔ Compiled OK
|
||
// with no runtime error); the script travels as its Script⇢Hash
|
||
// string, so the directory stores a fraction of the source. The
|
||
// listing carries each entry's name, type and date, a 1–5 star
|
||
// widget (the voter id is an HttpOnly cookie minted by the proxy;
|
||
// one vote per script per visitor, enforced upstream), the live
|
||
// "Average: #.#" to the right of the stars, and sorting by Name /
|
||
// Type / Date / Rating (done upstream, both directions). "Get"
|
||
// fetches the full record, decodes the hash back into the exact
|
||
// script, files it in this browser's preset tree under
|
||
// root/user created/OVE-Directory (content-deduped, names numbered
|
||
// "(1) (2) (3)…" on collision) and offers "Open in editor".
|
||
// Duplicate uploads resolve as success (409 DUPLICATE_HASH carries
|
||
// the existing row); duplicate votes show the stored aggregates.
|
||
// 7.10.2 — 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
|
||
// idle so the normal inactivity dimming returns; browsers silently
|
||
// drop the sentinel on hide, so a visibilitychange watcher
|
||
// re-acquires it on return while music is still going;
|
||
// unsupported browsers degrade silently). The Demo Track button no
|
||
// longer force-engages the Sequence checkbox: a visitor who loaded
|
||
// a preset (tree double-click / Load / Copy to my presets / created
|
||
// one), brought their own script material into the editor (typed,
|
||
// pasted, Hash⇢Script, file), saved their own default startup
|
||
// script, or runs their own Random/Sequence mix keeps their
|
||
// selection -- Demo just plays the track (the showcase's tree
|
||
// re-scope, 20 s interval and retire-on-end all skip too; a new
|
||
// per-visit manualChoice flag + a boot-script snapshot decide, and
|
||
// ensureDefaultPreset gained the same guard so mic/file/link never
|
||
// clobber a custom draft either; the showcase now only starts for
|
||
// a visit that never picked or typed anything). Script tab: a valid
|
||
// script shows the Save button with NO save target needed -- one
|
||
// click creates a preset under "user created", auto-named 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 whenever the name had to be
|
||
// invented a "Give This Preset a Name" dialog opens to rename the
|
||
// just-saved preset in one step (the preset is always saved FIRST,
|
||
// renamed second, so nothing is ever lost to a naming problem).
|
||
// Hash⇢Script imports clear the stale save target (the editor now
|
||
// holds foreign content) and pass the decoded label through as the
|
||
// save-name fallback when the script has no name: header
|
||
// 7.10.1 — the frozen right edge of spectrum visuals. getspec()/sv mapped
|
||
// the bar field across the whole FFT (bin 2 ... Nyquist, ~12 Hz to
|
||
// 24 kHz), but music carries almost nothing above ~16 kHz and every
|
||
// lossy source low-passes even lower (192 kbps MP3 at ~18.6 kHz,
|
||
// YouTube AAC at ~15.5 kHz; measured across the demo collection:
|
||
// 0.000% of total magnitude above 18.6 kHz), so the rightmost ~20%
|
||
// of every spectrum visual read a permanent zero -- bars that never
|
||
// moved. getSpectrum() now tops the field out at 16 kHz
|
||
// (SPEC_TOP_HZ, matching the spectral-flux cap in getBands()), so
|
||
// the right edge lands on the 12-16 kHz "air" band where cymbals,
|
||
// hats and synth sparkle live. rust-agent's spectrum_at() mirrors
|
||
// the change, and the help panel's sv entry now states the span.
|
||
// 7.10 — ten demo tracks + first-person changelog + declutter. The demo
|
||
// source is now a COLLECTION: ten mastered tracks in
|
||
// public/demo-tracks/ (demo-01.mp3 … demo-10.mp3, filename order
|
||
// 1→10, replacing the single Neural Mecha Pulse file). Each Demo
|
||
// click plays the visitor's PLACE in the collection once through;
|
||
// the place advances when a pass truly ends — natural finish or
|
||
// the Stop button ('ended'/'stop' teardown reasons, a new
|
||
// onDemoEnded signature) — and wraps 10→1; switching to another
|
||
// source keeps the place. The place persists per visitor in
|
||
// localStorage (ove.demoTrack.next), so returning picks up where
|
||
// they left off. The engine caches one decoded buffer at a time,
|
||
// keyed by track index (~50–60 MB PCM each — all ten would be half
|
||
// a gigabyte). The header chip + footer SRC label show the current
|
||
// track's name (Demo Track N). RELEASE_NOTES rewritten in the
|
||
// operator's first-person voice (no more third-person marketing
|
||
// copy or addressing the visitor), and every em dash in the
|
||
// interface replaced with a plain double minus ( -- ) — including
|
||
// the embedded preset library's descriptions and script comments
|
||
// (EMBEDDED_SEED_VERSION 7.01 → 7.10 re-seed). Declutter: the
|
||
// landing menu's source list line, the presets panel's two
|
||
// read-only paragraphs, the Random/Sequence hint line, the empty
|
||
// user-folder note reworded, the Settings theme/highlight/Native
|
||
// paragraphs, the link dialog's privacy footnote and the footer's
|
||
// engine-ok dot are all gone — compile errors still toast, so the
|
||
// always-green badge was noise
|
||
// 7.9 — footer credit + What's New polish. The footer's blank middle now
|
||
// carries the maker's signature: "© {year} · Site Created &
|
||
// Deployed by Sum'ting White" (year generated from the clock,
|
||
// hydration-guarded) with a "View my portfolio at zcreations.dev"
|
||
// link that opens zcreations.dev in a new tab — centered in the
|
||
// blank area from xl (mx-auto between the stats and the version
|
||
// stamp), dropping to its own centered row on narrower screens so
|
||
// nothing ever crowds. The What's New dialog got a typographic
|
||
// tidy-up: a hairline divider above every release box
|
||
// (border-zinc-800 — a ramp step, so it matches every base theme
|
||
// through the globals.css remaps), and each change row became an
|
||
// invisible two-column grid — the kind tag top-right justified
|
||
// against the column boundary, the description top-left with word
|
||
// wrap — so all descriptions start on the same pixel column
|
||
// 7.8 — share strings 2.0: ~72% shorter than the script (was ~49%). The
|
||
// base-62 idea, cranked to 11,172… 32,164: after the symbol
|
||
// dictionary (which grew ~170 data-mined entries — every function
|
||
// WITH its open paren, every engine variable, the AVS idioms
|
||
// "hue = hue + dt * " and "t = t + dt * ", " * 0.", ") * ",
|
||
// blank-line+section skeletons, "linewidth: " / "blend: additive"
|
||
// headers, ~70 number literals, and the library's common
|
||
// variable names incl. chroma) the whole body is read as ONE BIG
|
||
// NUMBER and rewritten in base 32,164 — one digit per character
|
||
// from the CJK + Hangul blocks, ~15 bits a character vs base-62's
|
||
// 5.95 (so "1400" costs ~0.85 of a character). The decoder gets
|
||
// the body's alphabet back from a ~32-character bitmap header
|
||
// instead of ~80 literal symbols. Short scripts where the header
|
||
// wouldn't pay keep the v7.7 symbol form (encoder picks whichever
|
||
// is shorter); ▲ strings from v7.7 still decode; still lossless
|
||
// for ANY text, still copy-safe, still self-checking. Corpus
|
||
// measure: median share string is 27% of the script's length
|
||
// (best 22%), e.g. Bass Bloom 1341 → 326 characters
|
||
// 7.7 — script sharing + settings declutter. Script⇢Hash / Hash⇢Script:
|
||
// two buttons in the Script tab turn the editor's script into ONE
|
||
// short shareable Unicode string ("Bass Bloom:[▲…①①①①①①①①]") and
|
||
// paste one back — the script returns exactly as written (lossless
|
||
// for ANY text, compiling NOT required, so WIP scripts can be
|
||
// shared for debugging). The string is built from built-in symbol
|
||
// references (♤ = line return, ♡ = beats, ♠ = beatage, ♦ = space,
|
||
// ♩♪♫♬ = the four sections, one symbol per function/variable, plus
|
||
// a pair-packing layer that folds leftover identifier/number text
|
||
// two chars per CJK code point) — ~50% shorter than the script on
|
||
// real presets. Copy-safe by construction (no raw whitespace or
|
||
// control chars; platform-injected variation selectors / zero-
|
||
// widths stripped on paste) and self-checking: 8 check digits (an
|
||
// FNV-1a digest rendered as ①–⑯) reject strings that were cut off
|
||
// by a character limit or altered in transit, instead of decoding
|
||
// into garbage. Also: the Music Services log in / "log out of all"
|
||
// section in Settings is parked (MUSIC_SERVICES_UI flag) — the
|
||
// built-in house account already powers Stream-a-Link for
|
||
// everyone; the section returns reworked in a later update
|
||
// 7.6.1 — header declutter: the Set Beat button is gone from the header
|
||
// (tap-tempo lives in the Beat tab, and the Space shortcut works
|
||
// anywhere — both unchanged). The beat indicator dot stays.
|
||
// 7.6 — "What's New!" dialog: a Sparkles button in the header (left of
|
||
// the theme switch) opens the visitor-facing changelog — every
|
||
// release from v4.0 on, newest first, each with one-line Added /
|
||
// Improved / Fixed / Removed notes (src/lib/release-notes.ts,
|
||
// color-coded tags, the running version badged "current",
|
||
// scrollable with the thin scrollbar). A small emerald dot on the
|
||
// button marks updates the visitor hasn't seen yet — per visitor
|
||
// in localStorage, cleared on open (mount-guarded, no hydration
|
||
// mismatch). Standing rule added: version bumps ship with their
|
||
// release note; a dev-mode guard warns when they drift
|
||
// 7.5.1 — Link dialog width fix: text no longer escapes the dialog box.
|
||
// Root cause: Radix dialogs lay their children in an implicit CSS
|
||
// grid whose single column sizes to the widest child's min-content;
|
||
// the RECENT LINKS rows carry truncate spans (white-space: nowrap),
|
||
// and Chromium measures a nowrap flex item's full text width for
|
||
// that measurement — so a long saved title (e.g. "Bobby Foxx — This
|
||
// Is Why We Can't Have Nice Things (Official Music Video)") blew
|
||
// the column ~40 px past the 448 px dialog, and EVERY child (title,
|
||
// description, input + Load, rows, footer) laid out beyond the
|
||
// right border. Fix: the shared Dialog/AlertDialog content now pins
|
||
// the column to minmax(0,1fr) — capped at the available width, so
|
||
// no nowrap content can ever stretch a dialog again (covers every
|
||
// dialog app-wide) — the Link dialog widened to max-w-lg for
|
||
// comfier wrapping, and min-w-0 + break-words added on the
|
||
// recents/playlist containers and error text as belt-and-braces
|
||
// 7.5 — yt-dlp compatibility: single tracks resolve by DOWNLOAD + CONVERT
|
||
// instead of proxying the extractor's stream URL. The old stream
|
||
// URLs were IP-locked (googlevideo bakes in the resolver's IP),
|
||
// expired within hours, and arrived as webm/opus — a container
|
||
// Safari & co. refuse ("The media resource … was not suitable").
|
||
// The server now runs the operator's pipeline per track:
|
||
// yt-dlp -f bestaudio/best -x --audio-format mp3
|
||
// --audio-quality 0 -o "<dir>/%(title)s.%(ext)s" URL
|
||
// — audio-only download, universal MP3 at VBR-0 (~245 kbps), the
|
||
// video's real title preserved for the Playing: label. The
|
||
// finished file streams same-origin with full Range support from
|
||
// a per-track workspace dir (30-min sliding TTL + orphan sweeper);
|
||
// /api/audio/stream gained a file-serving mode. Hardening around
|
||
// it: shared failure classifier for both yt-dlp passes (metadata +
|
||
// download), 180 s download budget, 300 MB cap, bot-wall/stale-
|
||
// extractor answers unchanged, ffmpeg-missing degrades to the old
|
||
// proxy path (logged once), queue look-ahead lead 15 s → 60 s so
|
||
// playlist transitions don't stall behind conversions
|
||
// 7.4 — Sequence mode: a second mix checkbox next to Random. Ticked, it
|
||
// cycles the scope folder's presets IN ORDER (the tree's exact
|
||
// top-to-bottom order) instead of shuffling — and when it moves
|
||
// past the last preset it wraps back to the first, so the folder
|
||
// loops forever. Random and Sequence are mutually exclusive
|
||
// (ticking either one unticks the other; both share the same
|
||
// delay and folder scope), Random now never loads the same preset
|
||
// twice in a row, and Sequence holds its breath through silence
|
||
// exactly like Random does. The Demo showcase now runs SEQUENCE —
|
||
// the one play-through walks every Zai-Magic preset in order
|
||
// (20 s each) rather than repeating random picks, and when the
|
||
// demo stops, whichever mode it checked is unchecked again
|
||
// 7.3 — demo-flow refinements: the Demo Track is a single pass — it plays
|
||
// "Neural Mecha Pulse" once through and stops (a demo, not a radio
|
||
// station). However it ends (natural finish, Stop, or switching to
|
||
// another source) the showcase unwinds: the randomizer the Demo
|
||
// flow force-checked is unchecked again, and the visitor lands
|
||
// back on the "Select an audio source" overlay. Preset-change
|
||
// popups are gone ("Preset loaded" / "Random preset" / "Default
|
||
// preset running") — the footer's SCRIPT: label already echoes
|
||
// every change, so the toasts were pure intrusion; only compile
|
||
// errors still toast. Club-mode randomizer now respects silence:
|
||
// when no music is playing it never re-shuffles the selected
|
||
// preset, even with the checkbox on — it waits for the next track
|
||
// 7.2.1 — Supernova (Zai-Magic) preset fix: the particle kill condition
|
||
// (|px|+|py| > 2.4) was physically unreachable — drag caps a
|
||
// particle's total travel at vel/1.6 ≈ 1.0, so nothing ever
|
||
// crossed 1.4, nothing ever died, and since respawning requires a
|
||
// dead particle, the per-beat "budget = n" detonation could never
|
||
// fire after the initial one — the preset exploded once on load,
|
||
// then faded to permanent black. Now every beat force-clears the
|
||
// whole 620-particle field in [beat] (guaranteed full re-detonation
|
||
// on the very same frame), fully-cooled particles die by fade so
|
||
// long beat gaps leave a gentle ember drizzle instead of black,
|
||
// and each blast re-randomizes its geometry (no two explosions
|
||
// alike). Embedded library re-seeded (seed marker 7.01)
|
||
// 7.2 — new-visitor showcase flow. Defaults for first-time visitors:
|
||
// the Black + Purple Outline theme with the Blue highlight
|
||
// (previously royal purple + yellow), and Zai-Magic "Bass Bloom"
|
||
// as the startup preset (previously the first Synth-C classic).
|
||
// The idle overlay's Demo Track button now carries an animated
|
||
// reflective shimmer (skewed light band sweeping left→right every
|
||
// 3.2 s, reduced-motion safe) to draw the click — and that one
|
||
// click starts the full showcase: the demo track plays while the
|
||
// preset tree selects + expands Zai-Magic, randomize switches on
|
||
// scoped to that folder with a 20-second change interval. Music
|
||
// started with NO preset active (own draft running / selection
|
||
// cleared) auto-loads Bass Bloom. Preset changes — manual load,
|
||
// the random mix, the Demo flow — no longer hard-cut: the engine
|
||
// snapshots the outgoing frame and blur-dissolves it into the new
|
||
// preset over 700 ms (progressive blur + easeOutCubic fade + a
|
||
// slight scale-up so the softening edges never gap; plain
|
||
// crossfade where ctx.filter is unsupported)
|
||
// 7.1 — real demo track + a beat detector that survives real music.
|
||
// The Demo button now plays "Neural Mecha Pulse" (the operator's
|
||
// mastered industrial-EDM banger, public/demo-track.mp3, loops
|
||
// gaplessly) instead of the synthetic techno pattern — a real
|
||
// recording is the honest test: deep sub for the bass bands,
|
||
// driving mids, crisp highs, and a kick buried in sustained
|
||
// sub-bass. That last part exposed the 6.x detector as fundamentally
|
||
// broken on mastered music: a LEVEL threshold can never see the
|
||
// kick through the sub (peak-to-mean caps at ~1.2× → sporadic
|
||
// fires, phantom 120-BPM locks from the minInterval floor).
|
||
// v7.1 rewrites detection the way real beat trackers work
|
||
// (aubio/librosa-style): a full-band spectral-flux ONSET envelope
|
||
// → autocorrelation over 300–1000 ms lags with a log-BPM prior →
|
||
// comb-filter phase with grid hysteresis → beats fired on an
|
||
// absolute grid kept glued by a PLL pull. Headlessly validated
|
||
// against the actual track PCM (AnalyserNode chain replicated
|
||
// frame-for-frame): locks the true ~133 BPM (the song drifts
|
||
// 130.7→133.1 — the grid TRACKS it) in 1.9 s, 92% beat-gap
|
||
// regularity, confidence 0.99 through the body; techno/hip-hop
|
||
// patterns lock exactly; silence yields zero beats. Known limit:
|
||
// sparse kick-vs-snare backbeats (dnb) may lock half-tempo —
|
||
// stable, and two taps of Set Beat correct it. Visitor-stored
|
||
// beat configs migrate (floorLevel changed units, level → flux).
|
||
// The demo BPM slider is gone (a real track's tempo is detected,
|
||
// not dialed in) — replaced by a track-name chip in the header
|
||
// 7.00 — Zai-Magic: ten hand-authored presets in root/embedded/Zai-Magic
|
||
// built around BEAT-SYNCED COLOR — the color wheel turns on the
|
||
// kick, palettes swap per beat / per 4-beat bar, megabuf particles
|
||
// remember the hue of the beat that spawned them + new script
|
||
// tools: hsvr/hsvg/hsvb (HSV→RGB channels), smoothstep and the
|
||
// beatage kick-envelope variable — all documented in the help
|
||
// panel. (Numbered 7.00 rather than 6.10 — a new preset family
|
||
// plus new script-language features is a major release; 6.9.3 was
|
||
// the last patch of the 6.x line.) Beat engine fixes underneath so
|
||
// the Demo track actually
|
||
// locks tempo (was sporadic): demo synth remixed for detection
|
||
// (hot kick owning the 20–100 Hz band, off-beat bassline voiced
|
||
// C3+ with a sidechain pump — chronological envelope events!),
|
||
// and the detector's default threshold 1.4 → 1.2 (the 170 ms FFT
|
||
// window caps a clean kick's peak-to-mean at ~1.5×, so 1.4 fired
|
||
// only sporadically and the BPM never locked)
|
||
// 6.9.3 — canonical domain fix: metadataBase/og:url/og:image pointed at
|
||
// openvisualengine.com (a DEAD domain baked in by 6.9.1) while the
|
||
// real site is openvisengine.com — Facebook's "corrupted image"
|
||
// error was its crawler fetching the image from a host that serves
|
||
// nothing; all share URLs now use the correct short domain
|
||
// 6.9.2 — share-image fix: og:image re-encoded as BASELINE JPEG (the
|
||
// 6.9.1 progressive encoding rendered fine in every browser but
|
||
// Facebook's Sharing Debugger rejected it as "corrupted"), swapped
|
||
// in the title-card artwork (gradient wordmark over three
|
||
// visualizations) and added a ?v=2 cache-bust so crawlers fetch it
|
||
// 6.9.1 — social share preview: Open Graph + Twitter Card metadata with
|
||
// a 1200×630 screenshot of the app (public/og-image.jpg) — sharing
|
||
// openvisualengine.com on Facebook/X/WhatsApp/Discord now shows the
|
||
// big image card instead of a bare link
|
||
// 6.9 — house account: anonymous visitors play YouTube links through
|
||
// the operator's shared disposable-account cookie (server-side
|
||
// fallback under secrets/, never exposed) — YouTube bot walls stop
|
||
// applying to guests; visitor's own login always wins. Engine fix
|
||
// underneath: Deno runtime shipped in bin/ (YouTube requires a JS
|
||
// runtime for signature/n-challenge solving on authenticated
|
||
// sessions — without it, logged-in extractions return no formats)
|
||
// 6.8.2 — login friction cut to ~4 clicks: the exported cookies.txt /
|
||
// Cookie-Editor JSON file can be dragged straight onto the
|
||
// login box (or picked via “Import the exported file…”) — no
|
||
// opening the file, no copy/paste; “Log out” / sign-out
|
||
// revocation note added (the official Google OAuth flow can't
|
||
// power the site: Google's APIs never hand out stream URLs and
|
||
// restricted-scope verification would reject an audio player)
|
||
// 6.8.1 — cookie login made foolproof: the login dialog recognizes raw
|
||
// Cookie headers, cookies.txt exports AND Cookie-Editor JSON,
|
||
// validates the paste LIVE through one shared parser (a single
|
||
// cookie's value — like a bare LOGIN_INFO string — gets an honest
|
||
// "this will not log you in" warning), and the how-to steps lead
|
||
// with the one-click extension export for Chrome AND Firefox
|
||
// 6.8 — self-healing audio engine: when YouTube changes their mechanics
|
||
// the server detects the stale extractor, updates yt-dlp in the
|
||
// background (verified GitHub release download, atomic swap) and the
|
||
// player retries seamlessly behind an "Updating in Background"
|
||
// notice; bot walls answer "Google login required…" with an Open
|
||
// Settings action, and unfixable failures get the honest
|
||
// "A back-end problem has occurred. Please try again later."
|
||
// 6.7 — lazy playlist queue: a playlist link becomes a queue of URLs that
|
||
// streams ONE song at a time (each track is resolved by yt-dlp only
|
||
// right before it plays — next-track look-ahead at ≤15 s to go),
|
||
// queue bar with skip/jump/replay + server-side caps (max 2 parallel
|
||
// extractions); music-service framework: Spotify + Apple Music are
|
||
// registered providers whose playback ships in a later update
|
||
// (logins already save, links answer with an honest notice)
|
||
// 6.6 — music service logins (Settings tab): YouTube / YouTube Music,
|
||
// SoundCloud, Spotify, Apple Music cookie login, encrypted on the
|
||
// visitor's computer; Link button gains playlist picking + uses the
|
||
// login to bypass bot walls
|
||
// 6.5 — Settings tab (replaces the parked Native tab): theme picker with
|
||
// base chrome (Royal Purple / Matte Black / Black + Purple Outline /
|
||
// Light) × selectable highlight color (Purple, Yellow, Blue, Green,
|
||
// Red, Orange), all per-visitor; simple OVE logo as the site favicon
|
||
// 6.4 — beat detection suspends when no music is playing/loaded (no ghost
|
||
// beats after Stop, BPM resets) + dark theme restyle: royal purple
|
||
// chrome with light-yellow highlights
|
||
// 6.3 — light/dark theme tick switch in the header (white theme via .light
|
||
// palette remap; per-visitor, persisted by next-themes)
|
||
// 6.2 — security hardening: read-only embedded library (server refuses all
|
||
// writes) + per-visitor "user created" presets stored in the browser
|
||
// 6.1 — preset library quality pass: every embedded stand-in is now a UNIQUE
|
||
// generated visual (16 families × seeded parameters), approx badge
|
||
// 6.0 — renamed to "Open Visualization Engine" + footer version stamp
|
||
// 5.0 — folder-tree Presets page (Task 9–11 state, restored in Task 12)
|
||
// 4.0 — link audio source (yt-dlp resolve + stream proxy)
|
||
|
||
export const APP_NAME = 'Open Visualization Engine'
|
||
export const APP_SHORT = 'OVE'
|
||
export const APP_VERSION = '7.19.0'
|