openvisengine/src/components/ava/help-panel.tsx

225 lines
12 KiB
TypeScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// AVA — script editor help panel: variables, functions, operators, examples
// and the .ava file format. Collapsible, opened from the editor toolbar.
'use client'
import type { ReactNode } from 'react'
import { BLEND_SPECS, BLEND_GROUP_LABELS, type BlendGroup } from '@/lib/ava/blend'
const FRAME_VARS: [string, string][] = [
['n', 'point count (4–4096, default 400 -- set in [init] or [frame])'],
['t', 'your own counter (do with it whatever you want)'],
['time', 'seconds since the script started'],
['dt', 'frame delta in seconds'],
['frame', 'frame number'],
['bass / mid / treb', 'instant band levels, 0..1 and above'],
['bass_att / mid_att / treb_att', 'smoothed bands (fast attack, slow release) -- use these for beat detection logic'],
['vol / vol_att', 'overall level, instant + smoothed'],
['beat', '1 on the frame a beat fired, else 0'],
['bpm', 'detected tempo'],
['beatphase', '0..1 position within the beat period'],
['beatconf', '0..1 confidence of the tempo lock'],
['beats', 'total beat count since load'],
['beatage', 'seconds since the last beat (0 on the beat frame) -- exp(-beatage*4) is a kick envelope'],
['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 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'],
['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][] = [
['i', '0..1 point index'],
['v', 'waveform sample −1..1'],
['sv', 'spectrum magnitude 0..1 (≈12 Hz to 16 kHz, log-ish -- the range music actually occupies)'],
['x / y', 'position −1..1 (x = sin(a)*rad / aspect keeps circles round)'],
['red / green / blue', 'point color 0..1'],
['skip', '> 0.5 hides the point / breaks the line'],
['size', 'dot radius in pixels (dots mode)'],
['alpha', 'opacity 0..1'],
]
const FUNCS: [string, string][] = [
['sin cos tan asin acos atan', 'trigonometry (radians)'],
['atan2(x, y)', 'arctangent of y/x keeping quadrant'],
['abs sqrt sqr pow(x, y) exp log log10', 'powers, roots & logs'],
['floor ceil round int frac sign', 'rounding & sign'],
['min max', 'pairwise extrema'],
['rand(seed)', 'deterministic per-seed random 0..1 -- rand(i) is stable per point'],
['hsvr(h, s?, v?) hsvg hsvb', 'HSV→RGB channels (hue wraps 0..1; s, v default 1) -- red = hsvr(hue) + green = hsvg(hue) + blue = hsvb(hue) paints the rainbow'],
['smoothstep(e0, e1, x)', 'hermite-smooth 0..1 step between e0 and e1'],
['lerp(a, b, t) clamp(x, lo, hi)', 'interpolate & constrain'],
['inrange(x, lo, hi)', '1 when lo ≤ x ≤ hi'],
['above(a, b) below(a, b) equal(a, b)', 'comparisons returning 1/0'],
['band(a, b) bor(a, b) bnot(a)', 'logical and / or / not (also && || !)'],
['sigmoid(x, y)', '1 / (1 + exp(-x·y))'],
['if(cond, a, b)', 'lazy conditional -- only the taken branch runs, so if(beat, flash=1, 0) works'],
]
const EXAMPLES: [string, string][] = [
['hue = frac(hue + beat * 0.125)', 'rotate the color wheel 1/8 turn on every beat'],
['red = hsvr(hue); green = hsvg(hue); blue = hsvb(hue)', 'full rainbow from one hue variable'],
['flash = exp(-beatage * 5)', 'kick envelope -- 1 on the beat, fades fast'],
['if(equal(beats % 4, 0), hue = frac(hue + 0.25), 0)', 'shift color once per 4-beat bar'],
['rad = 0.2 + bass * 0.6', 'bass pulse'],
['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 }) {
return (
<table className="w-full border-collapse text-left">
<tbody>
{rows.map(([name, desc]) => (
<tr key={name} className="border-b border-zinc-800/60 last:border-0 align-top">
<td
className={`py-1 pr-3 whitespace-nowrap text-emerald-400/90 ${mono ? 'font-mono text-[11px]' : 'text-xs'}`}
>
{name}
</td>
<td className="py-1 text-[11px] leading-relaxed text-zinc-400">{desc}</td>
</tr>
))}
</tbody>
</table>
)
}
function HelpSection({ title, children }: { title: string; children: ReactNode }) {
return (
<section>
<h4 className="mb-2 text-[11px] font-semibold uppercase tracking-[0.15em] text-zinc-400">{title}</h4>
{children}
</section>
)
}
export function HelpPanel() {
return (
<div className="space-y-5 rounded-lg border border-zinc-800/80 bg-zinc-950/60 p-4">
<HelpSection title="Quick start">
<ol className="list-inside list-decimal space-y-1 text-[11px] leading-relaxed text-zinc-400">
<li>Write header keys (name, mode, blend, persist, linewidth) at the top.</li>
<li>
Put setup code in <span className="font-mono text-emerald-400/90">[init]</span>, per-frame code in{' '}
<span className="font-mono text-emerald-400/90">[frame]</span>, reactions in{' '}
<span className="font-mono text-emerald-400/90">[beat]</span>, and the geometry in{' '}
<span className="font-mono text-emerald-400/90">[point]</span>.
</li>
<li>
Set <span className="font-mono text-emerald-400/90">n</span> (point count) in [init], drive it with{' '}
<span className="font-mono text-emerald-400/90">bass</span>,{' '}
<span className="font-mono text-emerald-400/90">beat</span> and <span className="font-mono text-emerald-400/90">v</span>.
</li>
<li>Hit Apply ▶ -- errors are flagged next to the exact line.</li>
</ol>
</HelpSection>
<HelpSection title="Variables -- everywhere">
<HelpTable rows={FRAME_VARS} />
</HelpSection>
<HelpSection title="Variables -- [point] only">
<HelpTable rows={POINT_VARS} />
</HelpSection>
<HelpSection title="Functions">
<HelpTable rows={FUNCS} />
<p className="mt-2 text-[11px] text-zinc-500">
Constants: <span className="font-mono text-emerald-400/90">pi tau e</span>
</p>
</HelpSection>
<HelpSection title="Operators">
<p className="font-mono text-[11px] leading-relaxed text-zinc-400">
+ - * / % ^ &nbsp;(power) &nbsp;·&nbsp; == != &lt; &lt;= &gt; &gt;= &nbsp;·&nbsp; &amp;&amp; || ! &nbsp;·&nbsp;
ternary <span className="text-emerald-400/90">c ? a : b</span> &nbsp;·&nbsp; assignment{' '}
<span className="text-emerald-400/90">=</span> (works inside if() and ternary branches)
</p>
<p className="mt-1.5 text-[11px] leading-relaxed text-zinc-500">
Statements are separated by newlines or semicolons. Comments start with <span className="font-mono">{'#'}</span> or{' '}
<span className="font-mono">{'//'}</span>.
</p>
</HelpSection>
<HelpSection title="Example formulas">
<div className="space-y-1.5">
{EXAMPLES.map(([code, note]) => (
<div key={code} className="flex flex-col gap-0.5 rounded border border-zinc-800/70 bg-zinc-800/40 px-2.5 py-1.5">
<code className="font-mono text-[11px] text-zinc-200">{code}</code>
<span className="text-[10px] text-zinc-500">{note}</span>
</div>
))}
</div>
</HelpSection>
<HelpSection title="Blend modes (blend:)">
<p className="mb-2 text-[11px] leading-relaxed text-zinc-400">
The <span className="font-mono text-emerald-400/90">blend:</span> header picks how fresh strokes mix with what is
already on screen (base = the picture, blend = the stroke). Names are case- and separator-insensitive —{' '}
<span className="font-mono">color burn</span> = <span className="font-mono">colorburn</span>. The{' '}
<span className="font-mono text-emerald-400/90">blend</span> variable can switch the mode per frame by its number.
</p>
<div className="space-y-3">
{(
['core', 'darken', 'lighten', 'contrast', 'invert', 'component', 'mask'] as BlendGroup[]
).map((g) => (
<div key={g}>
<p className="mb-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-zinc-500">
{BLEND_GROUP_LABELS[g]}
</p>
<HelpTable
rows={BLEND_SPECS.map((s, i) => [s, i] as const)
.filter(([s]) => s.group === g)
.map(([s, i]) => [
`${i + 1} · ${s.key}`,
s.aliases?.length ? `${s.note} [also: ${s.aliases.join(', ')}]` : s.note,
])}
/>
</div>
))}
</div>
</HelpSection>
<HelpSection title="File format (.ava)">
<p className="mb-2 text-[11px] leading-relaxed text-zinc-400">
A plain-text file: optional header keys before the first section, then any of the four sections in any order.
</p>
<pre className="overflow-x-auto rounded border border-zinc-800/70 bg-zinc-800/40 p-2.5 font-mono text-[11px] leading-relaxed text-zinc-300">
{`# header keys
name: My Visualization
author: you
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: 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
[frame] # runs every frame
[beat] # runs on beat frames (after [frame])
[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: 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>
)
}