openvisengine/src/components/ava/help-panel.tsx
2026-10-29 07:19:50 +00:00

176 lines
7.8 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'
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'],
['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)'],
]
const POINT_VARS: [string, string][] = [
['i', '0..1 point index'],
['v', 'waveform sample −1..1'],
['sv', 'spectrum magnitude 0..1'],
['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'],
]
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="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 # additive | alpha
persist: 0.85 # trail persistence 0..0.98
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>
</HelpSection>
</div>
)
}