Files
furumi_tui/src/visualizations/scope_spectrum.rhai
T

275 lines
8.4 KiB
Plaintext

// name: Scope spectrum
// protocol: furumi-visualizer-v2
// bundle-version: 5
//
// This bundled script is both a preset and an API example. It demonstrates
// every drawing command currently understood by Furumi's visualizer runtime:
// clear, cell, hline, vline, rect, trace and text.
//
// Script entry point:
// fn render(input) -> Array<Map>
//
// Important input fields:
// width, height terminal size in cells
// time, position, progress animation time and playback progress
// volume, paused player state
// energy, bass, mid, treble smoothed audio bands in the 0.0..1.0 range
// beat transient pulse estimate in the 0.0..1.0 range
// samples recent mono waveform samples in the -1.0..1.0 range
// show_clock, clock user setting and formatted clock text
// track_title, track_artist metadata for optional labels
//
// Useful numeric helpers available to scripts:
// to_int, to_float Rhai conversions
// sin, cos, tan, sqrt, abs, pow
// Scripts cannot import modules; keep every visualization self-contained.
//
// Return value: an array of command maps. Colors are 0xRRGGBB integers.
// #{ op: "clear", bg: 0x000000 }
// #{ op: "cell", x: 10, y: 4, ch: "*", fg: 0x33ffee, bg: 0x000000 }
// #{ op: "hline", x: 0, y: 4, w: 20, ch: "-", fg: 0x33ffee, bg: 0x000000 }
// #{ op: "vline", x: 10, y: 0, h: 12, ch: "|", fg: 0x33ffee, bg: 0x000000 }
// #{ op: "rect", x: 10, y: 6, w: 2, h: 4, ch: "#", fg: 0x33ffee, bg: 0x000000 }
// #{ op: "trace", x: 0, ys: [4, 5, 3], ch: "*", line_ch: "|", fg: 0xffffff, line_fg: 0x558888, bg: 0x000000 }
// #{ op: "text", x: 2, y: 1, text: "hello", fg: 0xffffff, bg: 0x000000 }
//
// Performance tip: prefer hline/vline/rect/trace for large shapes and reserve
// cell for isolated details. Crossing the Rhai -> Rust boundary once per cell
// is much slower than returning one bulk command.
fn clamp(value, low, high) {
if value < low {
low
} else if value > high {
high
} else {
value
}
}
fn min(left, right) {
if left < right { left } else { right }
}
fn rgb(r, g, b) {
let rr = to_int(clamp(r, 0, 255));
let gg = to_int(clamp(g, 0, 255));
let bb = to_int(clamp(b, 0, 255));
rr * 65536 + gg * 256 + bb
}
fn lerp(left, right, mix) {
left * (1.0 - mix) + right * mix
}
fn sample_at(samples, nx) {
let len = samples.len();
if len <= 0 {
0.0
} else {
let pos = clamp(nx, 0.0, 1.0) * (len - 1);
let left = to_int(pos);
let right = if left + 1 < len { left + 1 } else { left };
let mix = pos - to_float(left);
lerp(samples[left], samples[right], mix)
}
}
fn cell(x, y, ch, fg, bg) {
#{ op: "cell", x: x, y: y, ch: ch, fg: fg, bg: bg }
}
fn hline(x, y, w, ch, fg, bg) {
#{ op: "hline", x: x, y: y, w: w, ch: ch, fg: fg, bg: bg }
}
fn vline(x, y, h, ch, fg, bg) {
#{ op: "vline", x: x, y: y, h: h, ch: ch, fg: fg, bg: bg }
}
fn rect(x, y, w, h, ch, fg, bg) {
#{ op: "rect", x: x, y: y, w: w, h: h, ch: ch, fg: fg, bg: bg }
}
fn trace(x, ys, ch, line_ch, fg, line_fg, bg) {
#{ op: "trace", x: x, ys: ys, ch: ch, line_ch: line_ch, fg: fg, line_fg: line_fg, bg: bg }
}
fn text(x, y, value, fg, bg) {
#{ op: "text", x: x, y: y, text: value, fg: fg, bg: bg }
}
fn draw_grid(width, height, progress, energy, bg) {
let cmds = [];
let grid = rgb(22, 48 + energy * 30, 54 + energy * 35);
let center = height / 2;
let x = 0;
while x < width {
cmds.push(vline(x, 0, height, ".", grid, bg));
x += 12;
}
let y = 0;
while y < height {
cmds.push(hline(0, y, width, ".", grid, bg));
y += 4;
}
cmds.push(hline(0, center, width, "-", grid, bg));
let cross_x = 0;
while cross_x < width {
let cross_y = 0;
while cross_y < height {
cmds.push(cell(cross_x, cross_y, "+", grid, bg));
cross_y += 4;
}
cmds.push(cell(cross_x, center, "+", grid, bg));
cross_x += 12;
}
let scan_x = to_int(progress * to_float(width - 1));
let scan = rgb(25, 120 + energy * 80, 130 + energy * 80);
cmds.push(vline(scan_x, 0, height, "|", scan, bg));
cmds
}
fn draw_scope(input, scope_height, bg) {
let cmds = [];
let width = input.width;
if width <= 0 || scope_height <= 0 {
return cmds;
}
let center = to_float(scope_height - 1) / 2.0;
let half = center;
let fg = rgb(55 + input.beat * 80, 230, 210 + input.treble * 45);
let bridge = rgb(20, 120 + input.energy * 70, 115 + input.energy * 60);
let amplitude = 0.74 + input.energy * 0.55 + input.beat * 0.10;
let ys = [];
let x = 0;
while x < width {
let nx = if width > 1 { to_float(x) / to_float(width - 1) } else { 0.0 };
let sample = sample_at(input.samples, nx) * amplitude;
let y = to_int(clamp(center - sample * half, 0.0, to_float(scope_height - 1)));
ys.push(y);
x += 1;
}
cmds.push(trace(0, ys, "*", "|", fg, bridge, bg));
cmds
}
fn spectrum_value(freq, input) {
let band = if freq < 0.28 {
input.bass
} else if freq < 0.68 {
input.mid
} else {
input.treble
};
let wobble = abs(sin(input.time * (1.7 + freq * 5.0) + input.seed * 17.0 + freq * 31.0));
let harmonic = abs(cos(input.time * 0.63 - freq * 23.0));
clamp(0.06 + band * (0.30 + wobble * 0.48 + harmonic * 0.18) + input.beat * (1.0 - freq) * 0.35, 0.0, 1.0)
}
fn draw_bars(input, top, height, bg) {
let cmds = [];
if height <= 0 {
return cmds;
}
let width = input.width;
let bar_width = if width >= 80 { 2 } else { 1 };
let bars = width / bar_width;
let bar = 0;
while bar < bars {
let freq = to_float(bar) / to_float(bars);
let value = spectrum_value(freq, input);
let bar_height = to_int(clamp(value * to_float(height), 1.0, to_float(height)));
let fg = rgb(60 + (1.0 - freq) * 80, 160 + value * 95, 210 - freq * 110);
let x = bar * bar_width;
let y = top + height - bar_height;
let w = if bar_width > 1 && x + 1 < width { 2 } else { 1 };
if bar_height > 0 {
cmds.push(rect(x, y, w, bar_height, "#", fg, bg));
}
bar += 1;
}
cmds
}
fn append_all(cmds, more) {
for cmd in more {
cmds.push(cmd);
}
cmds
}
fn track_label(input) {
if input.track_artist != "" && input.track_title != "" {
input.track_artist + " - " + input.track_title
} else if input.track_title != "" {
input.track_title
} else {
input.track_artist
}
}
fn draw_track_badge(input, bg) {
let cmds = [];
let label = track_label(input);
if label == "" || input.width <= 8 || input.height <= 4 {
return cmds;
}
let max_text = input.width - 6;
let value = if label.len() > max_text {
label.sub_string(0, max_text)
} else {
label
};
let badge_width = min(value.len() + 4, input.width);
let x = (input.width - badge_width) / 2;
let y = input.height - 1;
let badge_bg = rgb(0, 30 + input.energy * 36, 38 + input.beat * 42);
let badge_fg = rgb(190 + input.treble * 45, 255, 235);
cmds.push(rect(x, y, badge_width, 1, " ", badge_fg, badge_bg));
cmds.push(text(x + 2, y, value, badge_fg, badge_bg));
cmds
}
fn render(input) {
let cmds = [];
let bg = rgb(0, 0, 0);
cmds.push(#{ op: "clear", bg: bg });
if input.width <= 0 || input.height <= 2 {
return cmds;
}
let content_height = input.height - 2;
let bars_height = if content_height > 12 { 8 } else { content_height / 3 };
let separator = if bars_height > 0 { 1 } else { 0 };
let scope_height = content_height - bars_height - separator;
cmds = append_all(cmds, draw_grid(input.width, scope_height, input.progress, input.energy, bg));
cmds = append_all(cmds, draw_scope(input, scope_height, bg));
if bars_height > 0 {
let sep_y = scope_height;
let sep = rgb(20, 68, 74);
cmds.push(hline(0, sep_y, input.width, "-", sep, bg));
cmds = append_all(cmds, draw_bars(input, sep_y + separator, bars_height, bg));
}
if input.show_clock {
let clock_bg = rgb(0, 26, 30);
let clock_fg = rgb(180, 255, 245);
cmds.push(text(1, 0, " " + input.clock + " ", clock_fg, clock_bg));
}
cmds = append_all(cmds, draw_track_badge(input, bg));
cmds
}