Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 8 additions & 61 deletions static/highway.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
_PAUSED_FRAME_INTERVAL_MS,
_SHIMMER_LUT_SIZE,
} from './js/highway-constants.js';
import {
bnvNormalizedPoints,
chordHarmonyLabels,
project,
roundRect,
teachingDegreeLabel,
teachingFingerLabel,
} from './js/highway-geometry.js';

function createHighway() {
// R3c: per-instance mutable state in one object, so extracted renderer/ws
Expand Down Expand Up @@ -362,19 +370,6 @@
return _toHex(c.r + (255 - c.r) * t, c.g + (255 - c.g) * t, c.b + (255 - c.b) * t);
}

// ── Projection ───────────────────────────────────────────────────────
function project(tOffset) {
if (tOffset > VISIBLE_SECONDS || tOffset < -0.05) return null;
if (tOffset < 0) return { y: 0.82 + Math.abs(tOffset) * 0.3, scale: 1.0 };

const z = tOffset * (Z_MAX / VISIBLE_SECONDS);
const denom = z + Z_CAM;
if (denom < 0.01) return null;
const scale = Z_CAM / denom;
const y = 0.82 + (0.08 - 0.82) * (1.0 - scale);
return { y, scale };
}

// ── Anchor / Fret mapping ────────────────────────────────────────────
// Zoom approach: fret 0 at the left edge, fret N at the right (entire canvas mirrored when lefty).
// The "zoom level" determines how many frets are visible.
Expand Down Expand Up @@ -437,50 +432,16 @@
/** Map a bend curve [{t, v}] (§6.2.1) to [{x, v}] with x normalized to
* 0..1 across the curve's time span (0 when the span is degenerate).
* Pure — drives the 2D bend-shape glyph. */
function bnvNormalizedPoints(bnv, sus) {
if (!Array.isArray(bnv) || bnv.length === 0) return [];
// Map each point's time over the NOTE's span [0, sus] so it sits at its
// real fraction of the note (a bend that completes before the note ends
// draws short of the glyph's right edge). Fall back to the curve's own
// t-range only when the note has no usable sustain.
if (Number.isFinite(sus) && sus > 0) {
return bnv.map(p => ({ x: Math.min(Math.max(p.t / sus, 0), 1), v: p.v }));
}
const t0 = bnv[0].t;
const span = bnv[bnv.length - 1].t - t0;
return bnv.map(p => ({ x: span > 0 ? (p.t - t0) / span : 0, v: p.v }));
}

/** Teaching mark (§6.2.2): fret-hand-finger label for a note's `fg`.
* '' when unset/out of range; 0 → 'T' (thumb), 1..4 → '1'..'4'. Pure. */
function teachingFingerLabel(fg) {
if (!Number.isInteger(fg) || fg < 0 || fg > 4) return '';
return fg === 0 ? 'T' : String(fg);
}

/** Teaching mark (§6.2.2): scale-degree label for a note's `sd` (chromatic
* 0..11 above the active key tonic). '' when unset/out of range. Pure. */
function teachingDegreeLabel(sd) {
if (!Number.isInteger(sd) || sd < 0 || sd > 11) return '';
return String(sd);
}

/** Harmony annotations (§6.3.1 / §6.6): display labels for a chord's
* harmonic function (the instance `fn.rn` Roman numeral) and its template
* `voicing`, `caged` shape, and `guideTones`. Returns '' for each when
* absent or malformed; `caged`/`guideTones` come back pre-formatted
* ("CAGED: E" / "gt 4,10"). Pure; node-tested and shared by both highways.
* Display/teaching only — MUST NEVER feed a grader (honesty rule). */
function chordHarmonyLabels(fn, voicing, caged, guideTones) {
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
const cg = (typeof caged === 'string' && /^[CAGED]$/.test(caged.trim()))
? 'CAGED: ' + caged.trim() : '';
const gt = Array.isArray(guideTones)
? guideTones.filter(n => Number.isInteger(n) && n >= 0 && n <= 11) : [];
return { rn, voicing: vc, caged: cg, guideTones: gt.length ? 'gt ' + gt.join(',') : '' };
}

/** Teaching mark (§6.2.2): bucket drawn notes by their strum-group key `ch`.
* Returns the groups (in first-seen order) for each ch value >= 0 that has
* at least two members — a lone note is not a strum gesture. Pure; drives
Expand Down Expand Up @@ -1537,7 +1498,7 @@
// Glow
for (let i = 1; i < 5; i++) {
const a = Math.max(0, 70 - i * 15);
hwState.ctx.strokeStyle = `rgba(${a},${a},${a+8},1)`;

Check warning on line 1501 in static/highway.js

View workflow job for this annotation

GitHub Actions / ci / lint

File has too many lines (4104). Maximum allowed is 1500
hwState.ctx.lineWidth = 1;
hwState.ctx.beginPath();
hwState.ctx.moveTo(W/2 - hw, y - i);
Expand Down Expand Up @@ -2586,20 +2547,6 @@
}
}

function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}

function bsearch(arr, time) {
let lo = 0, hi = arr.length;
while (lo < hi) {
Expand Down
79 changes: 79 additions & 0 deletions static/js/highway-geometry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// highway.js's PURE geometry + label primitives.
//
// Every function here is a pure function of its arguments. None of them touches hwState, and
// none closes over the canvas context — roundRect() already took `ctx` explicitly, and the
// rest need nothing but numbers. project() reads only the module-level constants from
// ./highway-constants.js.
//
// THAT PURITY IS WHY THIS SLICE IS SAFE, and why it is the one to do first. createHighway() is
// a FACTORY — a plugin can build a second highway for its own panel — so anything holding
// per-instance state (hwState) must be passed it as an argument rather than importing it, or
// two panels silently share one clock and palette. These six hold no state at all, so they
// move VERBATIM: not one call site changes.
//
// The primitives that DO need hwState (fretX, fillTextReadable, _noteState, _paintGemGlow)
// are deliberately left behind. They need an explicit hwState parameter threaded through 53
// call sites, which is a real change and belongs in its own commit, not smuggled in beside a
// provably-identical move.
import { VISIBLE_SECONDS, Z_CAM, Z_MAX } from './highway-constants.js';

// ── Projection ───────────────────────────────────────────────────────
export function project(tOffset) {
if (tOffset > VISIBLE_SECONDS || tOffset < -0.05) return null;
if (tOffset < 0) return { y: 0.82 + Math.abs(tOffset) * 0.3, scale: 1.0 };

const z = tOffset * (Z_MAX / VISIBLE_SECONDS);
const denom = z + Z_CAM;
if (denom < 0.01) return null;
const scale = Z_CAM / denom;
const y = 0.82 + (0.08 - 0.82) * (1.0 - scale);
return { y, scale };
}

export function bnvNormalizedPoints(bnv, sus) {
if (!Array.isArray(bnv) || bnv.length === 0) return [];
// Map each point's time over the NOTE's span [0, sus] so it sits at its
// real fraction of the note (a bend that completes before the note ends
// draws short of the glyph's right edge). Fall back to the curve's own
// t-range only when the note has no usable sustain.
if (Number.isFinite(sus) && sus > 0) {
return bnv.map(p => ({ x: Math.min(Math.max(p.t / sus, 0), 1), v: p.v }));
}
const t0 = bnv[0].t;
const span = bnv[bnv.length - 1].t - t0;
return bnv.map(p => ({ x: span > 0 ? (p.t - t0) / span : 0, v: p.v }));
}

export function teachingFingerLabel(fg) {
if (!Number.isInteger(fg) || fg < 0 || fg > 4) return '';
return fg === 0 ? 'T' : String(fg);
}

export function teachingDegreeLabel(sd) {
if (!Number.isInteger(sd) || sd < 0 || sd > 11) return '';
return String(sd);
}

export function chordHarmonyLabels(fn, voicing, caged, guideTones) {
const rn = (fn && typeof fn.rn === 'string') ? fn.rn.trim() : '';
const vc = (typeof voicing === 'string') ? voicing.trim() : '';
const cg = (typeof caged === 'string' && /^[CAGED]$/.test(caged.trim()))
? 'CAGED: ' + caged.trim() : '';
const gt = Array.isArray(guideTones)
? guideTones.filter(n => Number.isInteger(n) && n >= 0 && n <= 11) : [];
return { rn, voicing: vc, caged: cg, guideTones: gt.length ? 'gt ' + gt.join(',') : '' };
}

export function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
4 changes: 3 additions & 1 deletion tests/js/highway_bend_curve.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ function loadFn(file, name) {
return new Function('"use strict";' + extractFn(src, name) + `\nreturn ${name};`)();
}

const bnvNormalizedPoints = loadFn('static/highway.js', 'bnvNormalizedPoints');
// R3c: the PURE geometry/label primitives were carved out of highway.js into
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
const bnvNormalizedPoints = loadFn('static/js/highway-geometry.js', 'bnvNormalizedPoints');
const bnvSampleAt = loadFn('plugins/highway_3d/screen.js', 'bnvSampleAt');

// ── bnvNormalizedPoints (2D) ─────────────────────────────────────────────────
Expand Down
4 changes: 3 additions & 1 deletion tests/js/highway_chord_harmony.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ function loadFn(file, name) {
return new Function('"use strict";' + extractFn(src, name) + `\nreturn ${name};`)();
}

const labels2D = loadFn('static/highway.js', 'chordHarmonyLabels');
// R3c: the PURE geometry/label primitives were carved out of highway.js into
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
const labels2D = loadFn('static/js/highway-geometry.js', 'chordHarmonyLabels');
const labels3D = loadFn('plugins/highway_3d/screen.js', 'chordHarmonyLabels');

for (const [name, fn] of [['2D', labels2D], ['3D', labels3D]]) {
Expand Down
6 changes: 4 additions & 2 deletions tests/js/highway_teaching_marks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ function loadFn(file, name) {
return new Function('"use strict";' + extractFn(src, name) + `\nreturn ${name};`)();
}

const fingerLabel2D = loadFn('static/highway.js', 'teachingFingerLabel');
const degreeLabel2D = loadFn('static/highway.js', 'teachingDegreeLabel');
// R3c: the PURE geometry/label primitives were carved out of highway.js into
// static/js/highway-geometry.js. Same bodies, byte-for-byte — only the file moved.
const fingerLabel2D = loadFn('static/js/highway-geometry.js', 'teachingFingerLabel');
const degreeLabel2D = loadFn('static/js/highway-geometry.js', 'teachingDegreeLabel');
const fingerLabel3D = loadFn('plugins/highway_3d/screen.js', 'teachingFingerLabel');
const degreeLabel3D = loadFn('plugins/highway_3d/screen.js', 'teachingDegreeLabel');
const strumGroupBuckets = loadFn('static/highway.js', 'strumGroupBuckets');
Expand Down
Loading