182 lines
8.7 KiB
TypeScript
Executable file
182 lines
8.7 KiB
TypeScript
Executable file
// 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)'],
|
||
['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">
|
||
+ - * / % ^ (power) · == != < <= > >= · && || ! ·
|
||
ternary <span className="text-emerald-400/90">c ? a : b</span> · 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>
|
||
)
|
||
}
|