import { describe, expect, it } from 'vitest'; import { createWaveModel } from '@/journey/nav-wave'; const run = (model: ReturnType, seconds: number, speed: number, hue = -1): void => { for (let t = 0; t < seconds; t += 1 / 60) model.step(1 / 60, speed, hue); }; const line = (model: ReturnType, h: number, width = 1440, step = 3): number[] => { const ys: number[] = []; for (let x = 0; x <= width; x += step) ys.push(model.lineY(x, h)); return ys; }; describe('nav bar wave', () => { it('never leaves its band, at any speed or band height', () => { // One expectation per band rather than per sample: half a million // expect() calls ran past the timeout on a loaded machine. for (const h of [18, 14]) { const model = createWaveModel(); model.resize(1440); let low = Infinity; let high = -Infinity; for (const speed of [0, 0.37, 0.7, 1, 0.4, 1]) { for (let k = 0; k < 90; k += 1) { model.step(1 / 60, speed, -1); for (const y of line(model, h, 1440, 6)) { low = Math.min(low, y); high = Math.max(high, y); } } } expect(low).toBeGreaterThanOrEqual(1.6); expect(high).toBeLessThanOrEqual(h - 1.6); } }); it('is a smooth ribbon at cruise that drifts rather than standing still', () => { const model = createWaveModel(); model.resize(1440); run(model, 2, 0.37); expect(model.frame.spectrum).toBe(0); const before = line(model, 18); // Smooth: samples 3 px apart move by well under a pixel. for (let i = 1; i < before.length; i += 1) expect(Math.abs(before[i]! - before[i - 1]!)).toBeLessThan(0.6); // It travels: the highest crest moves along the bar within a second, // gently (no more than a third of the bar). const crest = (ys: number[]): number => ys.indexOf(Math.min(...ys)) * 3; const a = crest(before); run(model, 1, 0.37); const moved = Math.abs(crest(line(model, 18)) - a); expect(moved).toBeGreaterThan(6); expect(moved).toBeLessThan(480); }); it('is a spectrum flat out: peaks rising from the floor into the band', () => { const model = createWaveModel(); model.resize(1440); run(model, 2, 1); expect(model.frame.spectrum).toBeGreaterThan(0.99); const h = 18; expect(model.axisY(h)).toBeGreaterThan(h - 2.5); const ys = line(model, h); const tall = ys.filter((y) => y < h / 2).length; expect(tall).toBeGreaterThan(ys.length / 5); // Well-defined peaks: many local maxima, not one blob. let peaks = 0; for (let i = 1; i < ys.length - 1; i += 1) if (ys[i]! < ys[i - 1]! && ys[i]! <= ys[i + 1]! && ys[i]! < h - 6) peaks += 1; expect(peaks).toBeGreaterThan(20); // Smooth, not blocky: no vertical cliffs between samples 3 px apart. for (let i = 1; i < ys.length; i += 1) expect(Math.abs(ys[i]! - ys[i - 1]!)).toBeLessThan(6); }); it('follows a change of speed within a fraction of a second', () => { const model = createWaveModel(); model.resize(800); run(model, 1, 0.37); run(model, 0.2, 1); expect(model.frame.speed).toBeGreaterThan(0.9); run(model, 0.2, 0.37); expect(model.frame.speed).toBeLessThan(0.45); }); it('brings the hue in over 1.2 s and lets it go over 4 s', () => { const model = createWaveModel(); model.resize(800); run(model, 1.25, 1, 3); expect(model.frame.hueMix).toBe(1); run(model, 2, 0.4, 0); expect(model.frame.hueMix).toBeGreaterThan(0.1); expect(model.frame.hueMix).toBeLessThan(0.9); run(model, 2.1, 0.4, -1); expect(model.frame.hueMix).toBe(0); }); it('has a still frame for reduced motion', () => { const model = createWaveModel(); model.resize(800); run(model, 1, 1, 3); const frame = model.still(1); expect(frame.hueMix).toBe(0); expect(frame.spectrum).toBe(0); expect(model.level(10)).toBe(0); }); });