mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-19 19:31:26 +00:00
refactor(highway): carve the constants into static/js/highway-constants.js (R3c) (#914)
29 constants, 190 lines. highway.js 4,267 -> 4,158. The first real slice, and the one that
every later one imports.
━━━ WHY ONLY THE CONSTANTS MAY LIVE AT MODULE SCOPE ━━━
createHighway() is a FACTORY, not a singleton. The constitution publishes
window.createHighway precisely so a plugin can build a SECOND highway for its own panel, and
highway.js already says so at the top of the closure:
// R3c: per-instance mutable state in one object, so extracted renderer/ws
// modules can close over it as a factory arg without cross-panel sharing.
So hwState — all 79 mutable properties — must NEVER become a module-level singleton: two
highways would silently share it, and one panel would drive the other's clock, scale and
colour tables. Extracted functions will take it as an ARGUMENT.
That is the OPPOSITE of the app.js carve, where a single state container (player-state.js,
library-state.js) was exactly right, because there is exactly one app. Same epic, same
language, opposite answer — because one is a singleton and the other is a factory.
These 29 are pure literals: numbers, strings and colour tables, never reassigned, never
mutated. Sharing them across instances is not merely safe, it is what you want — one copy of
the shimmer LUT bounds and the string palettes rather than one per panel. Anything with a
runtime dependency (document, window, performance, localStorage) stays in the factory;
checked, and none of these has one.
ESLint now knows static/highway.js is a module. It could not have known before this commit:
the flip (#913) changed the SCRIPT TAG, but the file had no import/export yet, so it still
parsed as a script and lint stayed green. The first `import` is what makes the config wrong.
TESTS. Four source-shape harnesses asserted `const _AUTO_SCALE_MIN = …` etc. lived in
highway.js. They now read highway.js AND every static/js/highway-*.js — deliberately, rather
than being re-pinned at whichever file currently holds a constant. Re-pinning just breaks
again on the next carve, and a source-shape assertion that silently stops finding its target
is indistinguishable from one that passes. Bite-tested: renaming two constants away fails
them.
VERIFIED. A/B against origin/main: 15 probes IDENTICAL, zero page errors. AND THE PERF GATE
PASSES AT 1.97ms against its 12ms budget — which is the point of having built it (#910)
first: these constants moved from closure scope to module scope, and V8 does not treat those
identically. It does here. Now I know rather than hope.
node 1045, pytest 2416, ESLint 0, Codex 0.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6963fdf30
commit
8e89b39ad3
@ -55,7 +55,7 @@ module.exports = [
|
||||
// module graph, which is what makes no-cycle meaningful here — a carved
|
||||
// module that imports app.js back would close a cycle and fail this gate.
|
||||
{
|
||||
files: ['**/src/**/*.js', '**/*.mjs', 'static/app.js', 'static/js/**/*.js'],
|
||||
files: ['**/src/**/*.js', '**/*.mjs', 'static/app.js', 'static/js/**/*.js', 'static/highway.js'],
|
||||
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
plugins: { 'import-x': importX },
|
||||
// v4 flat-config resolver (resolver-next + createNodeResolver). Without
|
||||
|
||||
@ -2,6 +2,38 @@
|
||||
* Canvas-based note highway renderer.
|
||||
* Receives note data via WebSocket, renders on requestAnimationFrame.
|
||||
*/
|
||||
import {
|
||||
BG,
|
||||
CHAIN_GAP_THRESHOLD,
|
||||
CHAIN_RENDER_FULL_MAX,
|
||||
CHORD_FRAME_FRETS,
|
||||
DEFAULT_STRING_BRIGHT,
|
||||
DEFAULT_STRING_COLORS,
|
||||
DEFAULT_STRING_DIM,
|
||||
FRETLINE_TARGET_OFFSET,
|
||||
FRETLINE_WINDOW_AFTER,
|
||||
FRETLINE_WINDOW_BEFORE,
|
||||
MAX_RENDERER_DRAW_FAILURES,
|
||||
MUTE_BOX_BAR,
|
||||
MUTE_BOX_STROKE,
|
||||
REPEAT_BOX_BAR,
|
||||
REPEAT_BOX_FILL,
|
||||
VISIBLE_SECONDS,
|
||||
Z_CAM,
|
||||
Z_MAX,
|
||||
_AUTO_ADJUST_COOLDOWN_MS,
|
||||
_AUTO_SCALE_MIN,
|
||||
_AUTO_UPSCALE_COOLDOWN_MS,
|
||||
_CHART_MAX_INTERP_MS,
|
||||
_DOM_VIS_CHECK_FRAMES,
|
||||
_DRAW_BUDGET_HI_MS,
|
||||
_DRAW_BUDGET_LO_MS,
|
||||
_LYRIC_MEASURE_INNER_MAX,
|
||||
_LYRIC_MEASURE_OUTER_MAX,
|
||||
_PAUSED_FRAME_INTERVAL_MS,
|
||||
_SHIMMER_LUT_SIZE,
|
||||
} from './js/highway-constants.js';
|
||||
|
||||
function createHighway() {
|
||||
// R3c: per-instance mutable state in one object, so extracted renderer/ws
|
||||
// modules can close over it as a factory arg without cross-panel sharing.
|
||||
@ -111,11 +143,6 @@ function createHighway() {
|
||||
// re-anchor refines the estimate from the latest segment. Default
|
||||
// to 1 until we have two anchors to compare.
|
||||
hwState._chartObservedRate = 1;
|
||||
// Cap the interpolation so a stalled main thread (long task, GC,
|
||||
// dropped tick) can't make getTime drift far past reality. Also the
|
||||
// threshold for "audio looks paused" — if setTime hasn't advanced t
|
||||
// in this long, treat as paused.
|
||||
const _CHART_MAX_INTERP_MS = 100;
|
||||
// Visibility-aware rAF (feedBack#246): when the canvas is hidden
|
||||
// (display:none on itself or any ancestor — e.g. splitscreen's
|
||||
// workaround), pause renderer.draw and emit highway:visibility on
|
||||
@ -125,39 +152,9 @@ function createHighway() {
|
||||
// where offsetParent === null isn't enough.
|
||||
hwState._visibleOverride = null;
|
||||
hwState._lastVisible = null;
|
||||
// Throttled DOM visibility sampling. Reading canvas.offsetParent
|
||||
// every rAF frame forces a style/layout recalc — profiled at ~0.5 s
|
||||
// main-thread self-time over a 63 s session. The displayed state
|
||||
// changes rarely (navigate / splitscreen panel toggle), so the DOM
|
||||
// is only re-sampled every _DOM_VIS_CHECK_FRAMES frames; the cached
|
||||
// value serves the frames in between (worst-case transition latency
|
||||
// ~10 frames ≈ 166 ms at 60 Hz — fine for a hide/show pause signal).
|
||||
// Set _domVisSampledFrame to NaN to force a fresh sample on the next
|
||||
// check (done on init, canvas replace, resize, and override-clear so
|
||||
// deliberate transitions don't wait out the throttle window).
|
||||
// NOTE those manual resets are LATENCY optimizations, not correctness
|
||||
// requirements: the periodic re-sample runs every _DOM_VIS_CHECK_FRAMES
|
||||
// frames regardless, so a visibility-affecting path that forgets to
|
||||
// reset self-heals within ~10 frames — stale visibility can never be
|
||||
// served indefinitely.
|
||||
const _DOM_VIS_CHECK_FRAMES = 10;
|
||||
hwState._domVisCached = false;
|
||||
hwState._domVisSampledFrame = NaN;
|
||||
hwState.animFrame = null;
|
||||
// Paused-render throttle (feedBack#654). The rAF loop runs
|
||||
// unconditionally and only gates on visibility + ready, never on
|
||||
// playback — so an expensive renderer (3D Highway's Three.js WebGL
|
||||
// scene) does a full render every frame even while paused. That is
|
||||
// pure waste, and the dominant cost on high-refresh / ANGLE setups
|
||||
// (Chromium on Windows paces rAF to the fastest attached monitor,
|
||||
// so the loop can run at 144 Hz even on a 60 Hz panel). While the
|
||||
// audio clock is stalled, cap draws to one per
|
||||
// _PAUSED_FRAME_INTERVAL_MS. Note position is clock-derived
|
||||
// (n.t - currentTime), so this changes smoothness only — never
|
||||
// audio/visual sync. A low non-zero rate (not a hard skip) keeps
|
||||
// resize / seek-scrub / renderer-swap repaints correct without
|
||||
// having to hook each of those paths.
|
||||
const _PAUSED_FRAME_INTERVAL_MS = 100;
|
||||
hwState._lastPausedDrawAt = 0;
|
||||
hwState._connectOpts = {};
|
||||
hwState._resizeContainer = null;
|
||||
@ -273,9 +270,9 @@ function createHighway() {
|
||||
hwState._perfHud = null;
|
||||
hwState._hudOn = false; // cached highwayPerfHud flag (re-read ~2x/sec, not per-frame)
|
||||
hwState._hudFlagAt = 0;
|
||||
const _DRAW_BUDGET_HI_MS = 12; // sustained draw cost above this -> scale down
|
||||
const _DRAW_BUDGET_LO_MS = 7; // sustained draw cost below this -> scale back up
|
||||
const _AUTO_SCALE_MIN = 0.25; // hard floor (lowest the user-configurable floor may be set)
|
||||
// sustained draw cost above this -> scale down
|
||||
// sustained draw cost below this -> scale back up
|
||||
// hard floor (lowest the user-configurable floor may be set)
|
||||
// User-configurable floor for the load-adaptive render scale (#654):
|
||||
// 0.25 = stock (can drop to quarter-res on heavy frames → pixelated),
|
||||
// 1.0 = never auto-downscale below the Quality (renderScale) ceiling — so
|
||||
@ -287,12 +284,6 @@ function createHighway() {
|
||||
const v = parseFloat(localStorage.getItem('highwayMinRenderScale'));
|
||||
return Number.isFinite(v) ? Math.max(_AUTO_SCALE_MIN, Math.min(1, v)) : _AUTO_SCALE_MIN;
|
||||
})();
|
||||
const _AUTO_ADJUST_COOLDOWN_MS = 600;
|
||||
// Upscaling is deliberately LAZY (longer cooldown than the downscale path) so
|
||||
// the resolution doesn't visibly hunt up/down on passages that hover near the
|
||||
// budget — testers saw "quality going up and down" as parts got busier (#618
|
||||
// charrette). Downscale stays prompt to protect the frame rate.
|
||||
const _AUTO_UPSCALE_COOLDOWN_MS = 2500;
|
||||
hwState._inverted = localStorage.getItem('invertHighway') === 'true';
|
||||
hwState._lefty = localStorage.getItem('lefty') === '1';
|
||||
hwState._lastChordOnFretLine = null; // chord object currently shown on fret line
|
||||
@ -311,22 +302,6 @@ function createHighway() {
|
||||
// shimmer). Incremented once per rAF in draw().
|
||||
hwState._frameIdx = 0;
|
||||
|
||||
// 64-entry precomputed jitter LUT replacing Math.random() in the
|
||||
// lit-sustain shimmer hot path (drawSustains). Visually
|
||||
// indistinguishable from per-frame Math.random at rAF cadence,
|
||||
// allocation-free, and removes 4 RNG calls per visible lit sustain
|
||||
// per frame on dense charts. Seeded deterministically (xorshift32)
|
||||
// so the LUT itself is identical across `createHighway()` instances
|
||||
// — shimmer is therefore reload-stable and test-reproducible PER
|
||||
// instance for a given (frameIdx, n.s, n.t) seed. The seed includes
|
||||
// closure-scope `_frameIdx` which is per-instance, so two
|
||||
// splitscreen highways with different rAF cadence will shimmer
|
||||
// differently at any given wall-clock moment; what's stable is the
|
||||
// LUT contents.
|
||||
//
|
||||
// _SHIMMER_LUT_SIZE MUST stay a power of two — `_shimmerNoise`
|
||||
// indexes with `& (_SHIMMER_LUT_SIZE - 1)` for the cheap modulo.
|
||||
const _SHIMMER_LUT_SIZE = 64;
|
||||
const _shimmerLut = new Float32Array(_SHIMMER_LUT_SIZE);
|
||||
for (let i = 0; i < _SHIMMER_LUT_SIZE; i++) {
|
||||
let x = (i + 1) | 0; // +1 dodges the all-zero xorshift trap
|
||||
@ -341,22 +316,6 @@ function createHighway() {
|
||||
return _shimmerLut[(seed >>> 0) & (_SHIMMER_LUT_SIZE - 1)];
|
||||
}
|
||||
|
||||
// Memoize ctx.measureText() for the lyric overlay. Per-syllable
|
||||
// measurement was the dominant cost in dense karaoke charts; text
|
||||
// and fontSize are the only inputs (font face string is constant
|
||||
// `bold ${fontSize}px sans-serif`). Two-level Map (outer: fontSize,
|
||||
// inner: text) so a cache hit avoids the `fontSize + '|' + text`
|
||||
// concat that previously allocated on every lookup.
|
||||
//
|
||||
// Bounded on BOTH levels: window resizes change `fontSize`, so each
|
||||
// resize creates a fresh inner Map; without an outer cap, the cache
|
||||
// would retain every fontSize ever rendered for the page lifetime.
|
||||
// Cap outer at 16 distinct fontSize buckets (more than enough — a
|
||||
// session typically sees one or two), inner at 4096 entries per
|
||||
// bucket. Clear-on-overflow on both — a karaoke cold start re-warms
|
||||
// in one frame.
|
||||
const _LYRIC_MEASURE_OUTER_MAX = 16;
|
||||
const _LYRIC_MEASURE_INNER_MAX = 4096;
|
||||
const _lyricMeasureCache = new Map(); // Map<fontSize, Map<text, width>>
|
||||
function _measureLyricText(c, fontSize, text) {
|
||||
let inner = _lyricMeasureCache.get(fontSize);
|
||||
@ -374,36 +333,6 @@ function createHighway() {
|
||||
return w;
|
||||
}
|
||||
|
||||
// Rendering config
|
||||
const VISIBLE_SECONDS = 3.0;
|
||||
const Z_CAM = 2.2;
|
||||
const Z_MAX = 10.0;
|
||||
const BG = '#080810';
|
||||
|
||||
// String color palettes. Indices 0–5 cover guitar / bass; 6–7
|
||||
// are added for extended-range GP imports (7-string, 8-string).
|
||||
// Lookups still use `|| '#888'` as a safety fallback for any
|
||||
// out-of-range index.
|
||||
//
|
||||
// These are `let`, not `const`: setStringColors() (used by the core
|
||||
// "Highway String Colors" theming UI) overrides per-index entries at
|
||||
// runtime, deriving the dim/bright variants from the chosen base color.
|
||||
// DEFAULT_* keep the originals so a reset restores them byte-for-byte.
|
||||
const DEFAULT_STRING_COLORS = [
|
||||
'#cc0000', '#cca800', '#0066cc',
|
||||
'#cc6600', '#00cc66', '#9900cc',
|
||||
'#cc00aa', '#00cccc', // 7th = magenta, 8th = teal
|
||||
];
|
||||
const DEFAULT_STRING_DIM = [
|
||||
'#520000', '#524200', '#002952',
|
||||
'#522900', '#005229', '#3d0052',
|
||||
'#520042', '#005252',
|
||||
];
|
||||
const DEFAULT_STRING_BRIGHT = [
|
||||
'#ff3c3c', '#ffe040', '#3c9cff',
|
||||
'#ff9c3c', '#3cff9c', '#cc3cff',
|
||||
'#ff3ce0', '#3ce0e0',
|
||||
];
|
||||
hwState.STRING_COLORS = DEFAULT_STRING_COLORS.slice();
|
||||
hwState.STRING_DIM = DEFAULT_STRING_DIM.slice();
|
||||
hwState.STRING_BRIGHT = DEFAULT_STRING_BRIGHT.slice();
|
||||
@ -964,8 +893,6 @@ function createHighway() {
|
||||
// every frame. Reset on every successful draw and whenever a new
|
||||
// renderer is installed.
|
||||
hwState._rendererDrawFailures = 0;
|
||||
const MAX_RENDERER_DRAW_FAILURES = 3;
|
||||
|
||||
// True only while the current renderer has had a successful init
|
||||
// since its last destroy (or was freshly installed but never init'd
|
||||
// because canvas was null). Gates destroy calls so an uninit'd
|
||||
@ -2705,42 +2632,6 @@ function createHighway() {
|
||||
return lo;
|
||||
}
|
||||
|
||||
// ── Chord rendering — chains, frames, fretline preview (feedBack#88) ──
|
||||
//
|
||||
// Charts often repeat the same chord shape several times in a
|
||||
// row (e.g. a G strummed 4 times). We call a contiguous run of same-id
|
||||
// chords with gaps < CHAIN_GAP_THRESHOLD a "chain". Chains drive two
|
||||
// visual choices:
|
||||
// • The first chord in a chain renders in full; subsequent chords in
|
||||
// a chain of CHAIN_RENDER_FULL_MAX or longer render as a "repeat
|
||||
// box" — a translucent boxed frame so the eye can see the rhythm
|
||||
// pattern without re-scanning identical fret numbers.
|
||||
// • Each chord anchors a CHORD_FRAME_FRETS-wide frame; muted and
|
||||
// open-only chords inherit the frame from their predecessor so
|
||||
// they don't snap to fret 0.
|
||||
//
|
||||
// We compute chain stats and frame anchors once per `src` array via
|
||||
// _ensureChordRenderCache (lazy, invalidates when the array reference
|
||||
// changes — which happens on chord ingest, mastery rebuild, or song
|
||||
// reset). The render path is then pure read.
|
||||
const CHAIN_GAP_THRESHOLD = 0.5;
|
||||
const CHAIN_RENDER_FULL_MAX = 4;
|
||||
const CHORD_FRAME_FRETS = 4;
|
||||
|
||||
// Fretline preview: the static fret line at the bottom shows the chord
|
||||
// closest to the strum line (currentTime + FRETLINE_TARGET_OFFSET) within
|
||||
// the [target - FRETLINE_WINDOW_BEFORE, target + FRETLINE_WINDOW_AFTER]
|
||||
// window, as a teaching aid.
|
||||
const FRETLINE_TARGET_OFFSET = -0.25;
|
||||
const FRETLINE_WINDOW_BEFORE = 0.1;
|
||||
const FRETLINE_WINDOW_AFTER = 0.3;
|
||||
|
||||
// Repeat / mute box colors.
|
||||
const REPEAT_BOX_FILL = 'rgba(48, 80, 128, 0.06)';
|
||||
const REPEAT_BOX_BAR = '#50a0dc';
|
||||
const MUTE_BOX_STROKE = '#6060809b';
|
||||
const MUTE_BOX_BAR = '#606080d1';
|
||||
|
||||
// Reset all chord-render-derived state. Called from init() and
|
||||
// reconnect() so per-song state (preview, frame-mismatch warnings,
|
||||
// chain cache) doesn't leak across songs that reuse chord IDs.
|
||||
|
||||
190
static/js/highway-constants.js
Normal file
190
static/js/highway-constants.js
Normal file
@ -0,0 +1,190 @@
|
||||
// highway.js's immutable constants: geometry, colour tables, timing budgets, and the
|
||||
// load-adaptive render-scale thresholds.
|
||||
//
|
||||
// WHY THESE — AND ONLY THESE — MAY LIVE AT MODULE SCOPE
|
||||
//
|
||||
// createHighway() is a FACTORY, not a singleton. The constitution publishes
|
||||
// window.createHighway precisely so a plugin can build a SECOND highway for its own panel,
|
||||
// and highway.js says so at the top of the closure:
|
||||
//
|
||||
// // R3c: per-instance mutable state in one object, so extracted renderer/ws
|
||||
// // modules can close over it as a factory arg without cross-panel sharing.
|
||||
//
|
||||
// So MUTABLE state (hwState) must never become a module-level singleton — two highways would
|
||||
// silently share it. That is the opposite of the app.js carve, where a single state container
|
||||
// was right because there is exactly one app.
|
||||
//
|
||||
// These 29 are pure literals: frozen numbers, strings and colour tables, never reassigned and
|
||||
// never mutated. Sharing them across instances is not just safe, it is what you want — one
|
||||
// copy of the shimmer LUT bounds and the string palettes rather than one per panel.
|
||||
//
|
||||
// Anything with a runtime dependency (document, window, performance, localStorage) stays in
|
||||
// the factory. Checked: none of these has one.
|
||||
|
||||
// Cap the interpolation so a stalled main thread (long task, GC,
|
||||
// dropped tick) can't make getTime drift far past reality. Also the
|
||||
// threshold for "audio looks paused" — if setTime hasn't advanced t
|
||||
// in this long, treat as paused.
|
||||
export const _CHART_MAX_INTERP_MS = 100;
|
||||
|
||||
// Throttled DOM visibility sampling. Reading canvas.offsetParent
|
||||
// every rAF frame forces a style/layout recalc — profiled at ~0.5 s
|
||||
// main-thread self-time over a 63 s session. The displayed state
|
||||
// changes rarely (navigate / splitscreen panel toggle), so the DOM
|
||||
// is only re-sampled every _DOM_VIS_CHECK_FRAMES frames; the cached
|
||||
// value serves the frames in between (worst-case transition latency
|
||||
// ~10 frames ≈ 166 ms at 60 Hz — fine for a hide/show pause signal).
|
||||
// Set _domVisSampledFrame to NaN to force a fresh sample on the next
|
||||
// check (done on init, canvas replace, resize, and override-clear so
|
||||
// deliberate transitions don't wait out the throttle window).
|
||||
// NOTE those manual resets are LATENCY optimizations, not correctness
|
||||
// requirements: the periodic re-sample runs every _DOM_VIS_CHECK_FRAMES
|
||||
// frames regardless, so a visibility-affecting path that forgets to
|
||||
// reset self-heals within ~10 frames — stale visibility can never be
|
||||
// served indefinitely.
|
||||
export const _DOM_VIS_CHECK_FRAMES = 10;
|
||||
|
||||
// Paused-render throttle (feedBack#654). The rAF loop runs
|
||||
// unconditionally and only gates on visibility + ready, never on
|
||||
// playback — so an expensive renderer (3D Highway's Three.js WebGL
|
||||
// scene) does a full render every frame even while paused. That is
|
||||
// pure waste, and the dominant cost on high-refresh / ANGLE setups
|
||||
// (Chromium on Windows paces rAF to the fastest attached monitor,
|
||||
// so the loop can run at 144 Hz even on a 60 Hz panel). While the
|
||||
// audio clock is stalled, cap draws to one per
|
||||
// _PAUSED_FRAME_INTERVAL_MS. Note position is clock-derived
|
||||
// (n.t - currentTime), so this changes smoothness only — never
|
||||
// audio/visual sync. A low non-zero rate (not a hard skip) keeps
|
||||
// resize / seek-scrub / renderer-swap repaints correct without
|
||||
// having to hook each of those paths.
|
||||
export const _PAUSED_FRAME_INTERVAL_MS = 100;
|
||||
|
||||
export const _DRAW_BUDGET_HI_MS = 12;
|
||||
|
||||
export const _DRAW_BUDGET_LO_MS = 7;
|
||||
|
||||
export const _AUTO_SCALE_MIN = 0.25;
|
||||
|
||||
export const _AUTO_ADJUST_COOLDOWN_MS = 600;
|
||||
|
||||
// Upscaling is deliberately LAZY (longer cooldown than the downscale path) so
|
||||
// the resolution doesn't visibly hunt up/down on passages that hover near the
|
||||
// budget — testers saw "quality going up and down" as parts got busier (#618
|
||||
// charrette). Downscale stays prompt to protect the frame rate.
|
||||
export const _AUTO_UPSCALE_COOLDOWN_MS = 2500;
|
||||
|
||||
// 64-entry precomputed jitter LUT replacing Math.random() in the
|
||||
// lit-sustain shimmer hot path (drawSustains). Visually
|
||||
// indistinguishable from per-frame Math.random at rAF cadence,
|
||||
// allocation-free, and removes 4 RNG calls per visible lit sustain
|
||||
// per frame on dense charts. Seeded deterministically (xorshift32)
|
||||
// so the LUT itself is identical across `createHighway()` instances
|
||||
// — shimmer is therefore reload-stable and test-reproducible PER
|
||||
// instance for a given (frameIdx, n.s, n.t) seed. The seed includes
|
||||
// closure-scope `_frameIdx` which is per-instance, so two
|
||||
// splitscreen highways with different rAF cadence will shimmer
|
||||
// differently at any given wall-clock moment; what's stable is the
|
||||
// LUT contents.
|
||||
//
|
||||
// _SHIMMER_LUT_SIZE MUST stay a power of two — `_shimmerNoise`
|
||||
// indexes with `& (_SHIMMER_LUT_SIZE - 1)` for the cheap modulo.
|
||||
export const _SHIMMER_LUT_SIZE = 64;
|
||||
|
||||
// Memoize ctx.measureText() for the lyric overlay. Per-syllable
|
||||
// measurement was the dominant cost in dense karaoke charts; text
|
||||
// and fontSize are the only inputs (font face string is constant
|
||||
// `bold ${fontSize}px sans-serif`). Two-level Map (outer: fontSize,
|
||||
// inner: text) so a cache hit avoids the `fontSize + '|' + text`
|
||||
// concat that previously allocated on every lookup.
|
||||
//
|
||||
// Bounded on BOTH levels: window resizes change `fontSize`, so each
|
||||
// resize creates a fresh inner Map; without an outer cap, the cache
|
||||
// would retain every fontSize ever rendered for the page lifetime.
|
||||
// Cap outer at 16 distinct fontSize buckets (more than enough — a
|
||||
// session typically sees one or two), inner at 4096 entries per
|
||||
// bucket. Clear-on-overflow on both — a karaoke cold start re-warms
|
||||
// in one frame.
|
||||
export const _LYRIC_MEASURE_OUTER_MAX = 16;
|
||||
|
||||
export const _LYRIC_MEASURE_INNER_MAX = 4096;
|
||||
|
||||
// Rendering config
|
||||
export const VISIBLE_SECONDS = 3.0;
|
||||
|
||||
export const Z_CAM = 2.2;
|
||||
|
||||
export const Z_MAX = 10.0;
|
||||
|
||||
export const BG = '#080810';
|
||||
|
||||
// String color palettes. Indices 0–5 cover guitar / bass; 6–7
|
||||
// are added for extended-range GP imports (7-string, 8-string).
|
||||
// Lookups still use `|| '#888'` as a safety fallback for any
|
||||
// out-of-range index.
|
||||
//
|
||||
// These are `let`, not `const`: setStringColors() (used by the core
|
||||
// "Highway String Colors" theming UI) overrides per-index entries at
|
||||
// runtime, deriving the dim/bright variants from the chosen base color.
|
||||
// DEFAULT_* keep the originals so a reset restores them byte-for-byte.
|
||||
export const DEFAULT_STRING_COLORS = [
|
||||
'#cc0000', '#cca800', '#0066cc',
|
||||
'#cc6600', '#00cc66', '#9900cc',
|
||||
'#cc00aa', '#00cccc', // 7th = magenta, 8th = teal
|
||||
];
|
||||
|
||||
export const DEFAULT_STRING_DIM = [
|
||||
'#520000', '#524200', '#002952',
|
||||
'#522900', '#005229', '#3d0052',
|
||||
'#520042', '#005252',
|
||||
];
|
||||
|
||||
export const DEFAULT_STRING_BRIGHT = [
|
||||
'#ff3c3c', '#ffe040', '#3c9cff',
|
||||
'#ff9c3c', '#3cff9c', '#cc3cff',
|
||||
'#ff3ce0', '#3ce0e0',
|
||||
];
|
||||
|
||||
export const MAX_RENDERER_DRAW_FAILURES = 3;
|
||||
|
||||
// ── Chord rendering — chains, frames, fretline preview (feedBack#88) ──
|
||||
//
|
||||
// Charts often repeat the same chord shape several times in a
|
||||
// row (e.g. a G strummed 4 times). We call a contiguous run of same-id
|
||||
// chords with gaps < CHAIN_GAP_THRESHOLD a "chain". Chains drive two
|
||||
// visual choices:
|
||||
// • The first chord in a chain renders in full; subsequent chords in
|
||||
// a chain of CHAIN_RENDER_FULL_MAX or longer render as a "repeat
|
||||
// box" — a translucent boxed frame so the eye can see the rhythm
|
||||
// pattern without re-scanning identical fret numbers.
|
||||
// • Each chord anchors a CHORD_FRAME_FRETS-wide frame; muted and
|
||||
// open-only chords inherit the frame from their predecessor so
|
||||
// they don't snap to fret 0.
|
||||
//
|
||||
// We compute chain stats and frame anchors once per `src` array via
|
||||
// _ensureChordRenderCache (lazy, invalidates when the array reference
|
||||
// changes — which happens on chord ingest, mastery rebuild, or song
|
||||
// reset). The render path is then pure read.
|
||||
export const CHAIN_GAP_THRESHOLD = 0.5;
|
||||
|
||||
export const CHAIN_RENDER_FULL_MAX = 4;
|
||||
|
||||
export const CHORD_FRAME_FRETS = 4;
|
||||
|
||||
// Fretline preview: the static fret line at the bottom shows the chord
|
||||
// closest to the strum line (currentTime + FRETLINE_TARGET_OFFSET) within
|
||||
// the [target - FRETLINE_WINDOW_BEFORE, target + FRETLINE_WINDOW_AFTER]
|
||||
// window, as a teaching aid.
|
||||
export const FRETLINE_TARGET_OFFSET = -0.25;
|
||||
|
||||
export const FRETLINE_WINDOW_BEFORE = 0.1;
|
||||
|
||||
export const FRETLINE_WINDOW_AFTER = 0.3;
|
||||
|
||||
// Repeat / mute box colors.
|
||||
export const REPEAT_BOX_FILL = 'rgba(48, 80, 128, 0.06)';
|
||||
|
||||
export const REPEAT_BOX_BAR = '#50a0dc';
|
||||
|
||||
export const MUTE_BOX_STROKE = '#6060809b';
|
||||
|
||||
export const MUTE_BOX_BAR = '#606080d1';
|
||||
@ -27,16 +27,33 @@ function extractBlock(src, signature) {
|
||||
return src.slice(start, i);
|
||||
}
|
||||
|
||||
|
||||
// R3c: highway.js is being carved into modules, so its source is no longer ONE file. Read the
|
||||
// whole set. Re-pinning these assertions at whichever file currently holds a constant just
|
||||
// means they break again on the next carve — and worse, a source-shape assertion that silently
|
||||
// stops finding its target is indistinguishable from one that passes.
|
||||
function highwaySources() {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const jsDir = path.join(root, 'static', 'js');
|
||||
const parts = [fs.readFileSync(path.join(root, 'static', 'highway.js'), 'utf8')];
|
||||
for (const f of fs.readdirSync(jsDir).sort()) {
|
||||
if (f.startsWith('highway-') && f.endsWith('.js')) {
|
||||
parts.push(fs.readFileSync(path.join(jsDir, f), 'utf8'));
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
test('highway declares adaptive-scale state with a floor', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /hwState\._autoScale\s*=\s*1/, 'missing _autoScale multiplier');
|
||||
assert.match(src, /const\s+_AUTO_SCALE_MIN\s*=\s*0?\.25/, 'missing _AUTO_SCALE_MIN floor (0.25)');
|
||||
assert.match(src, /const\s+_DRAW_BUDGET_HI_MS\s*=\s*\d+/, 'missing high draw budget');
|
||||
assert.match(src, /const\s+_DRAW_BUDGET_LO_MS\s*=\s*\d+/, 'missing low draw budget');
|
||||
assert.match(src, /(?:export\s+)?const\s+_AUTO_SCALE_MIN\s*=\s*0?\.25/, 'missing _AUTO_SCALE_MIN floor (0.25)');
|
||||
assert.match(src, /(?:export\s+)?const\s+_DRAW_BUDGET_HI_MS\s*=\s*\d+/, 'missing high draw budget');
|
||||
assert.match(src, /(?:export\s+)?const\s+_DRAW_BUDGET_LO_MS\s*=\s*\d+/, 'missing low draw budget');
|
||||
});
|
||||
|
||||
test('_effectiveRenderScale clamps user ceiling * auto factor to [MIN, 1]', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const fn = extractBlock(src, 'function _effectiveRenderScale()');
|
||||
// Derives from the (sanitized) user ceiling and auto factor.
|
||||
assert.match(fn, /_renderScale/, 'effective scale must derive from the user _renderScale');
|
||||
@ -47,7 +64,7 @@ test('_effectiveRenderScale clamps user ceiling * auto factor to [MIN, 1]', () =
|
||||
});
|
||||
|
||||
test('min render scale floor is user-configurable + exposed on the api (#654)', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
// Hard floor constant kept; configurable floor read from localStorage.
|
||||
assert.match(src, /hwState\._autoScaleMin\s*=/, 'missing configurable _autoScaleMin');
|
||||
assert.match(src, /localStorage\.getItem\('highwayMinRenderScale'\)/,
|
||||
@ -65,7 +82,7 @@ test('min render scale floor is user-configurable + exposed on the api (#654)',
|
||||
});
|
||||
|
||||
test('_adaptRenderScale uses the draw budget + cooldown and re-applies via resize', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const fn = extractBlock(src, 'function _adaptRenderScale(');
|
||||
assert.match(fn, /_DRAW_BUDGET_HI_MS/, 'must scale down past the high budget');
|
||||
assert.match(fn, /_DRAW_BUDGET_LO_MS/, 'must scale up below the low budget');
|
||||
@ -74,27 +91,27 @@ test('_adaptRenderScale uses the draw budget + cooldown and re-applies via resiz
|
||||
});
|
||||
|
||||
test('draw() only adapts during active playback and feeds the HUD', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const fn = extractBlock(src, 'function draw()');
|
||||
assert.match(fn, /if\s*\(\s*!_paused\s*\)\s*_adaptRenderScale/, 'must skip adaptation while paused');
|
||||
assert.match(fn, /_updatePerfHud\(\)/, 'must update the perf HUD each drawn frame');
|
||||
});
|
||||
|
||||
test('bundle + canvas sizing use the effective scale, not the raw user value', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /renderScale\s*[:=]\s*_effectiveRenderScale\(\)/, 'bundle.renderScale must be the effective scale');
|
||||
assert.match(src, /canvas\.width\s*=\s*Math\.round\(w\s*\*\s*_effectiveRenderScale\(\)\)/, 'canvas backing store must use effective scale');
|
||||
});
|
||||
|
||||
test('api exposes effective scale + perf stats', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /getEffectiveRenderScale\(\)\s*\{\s*return\s+_effectiveRenderScale\(\)/, 'api.getEffectiveRenderScale missing');
|
||||
assert.match(src, /getPerfStats\(\)\s*\{/, 'api.getPerfStats missing');
|
||||
});
|
||||
|
||||
// Robustness fixes from the #655 Copilot review.
|
||||
test('render scale is sanitized on load and effective scale guards non-finite', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /parseFloat\(localStorage\.getItem\('renderScale'\)[\s\S]{0,160}?Number\.isFinite/,
|
||||
'render scale load must validate via Number.isFinite + clamp');
|
||||
const eff = extractBlock(src, 'function _effectiveRenderScale()');
|
||||
@ -102,7 +119,7 @@ test('render scale is sanitized on load and effective scale guards non-finite',
|
||||
});
|
||||
|
||||
test('stop() tears down the perf HUD and resets per-session accumulators', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /stop\(\)\s*\{[\s\S]{0,400}?_perfHud\.remove\(\)/,
|
||||
'stop() must remove the perf HUD so it cannot strand in the DOM');
|
||||
assert.match(src, /stop\(\)\s*\{[\s\S]{0,1200}?_autoScale\s*=\s*1/,
|
||||
@ -112,7 +129,7 @@ test('stop() tears down the perf HUD and resets per-session accumulators', () =>
|
||||
});
|
||||
|
||||
test('perf HUD throttles its localStorage flag read off the hot path', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const fn = extractBlock(src, 'function _updatePerfHud()');
|
||||
assert.match(fn, /_hudFlagAt/, 'HUD must cache the flag and re-read on an interval, not every frame');
|
||||
});
|
||||
|
||||
@ -60,7 +60,7 @@ function buildClockSandbox(perfNowImpl) {
|
||||
performance: { now: perfNowImpl },
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const src = highwaySources();
|
||||
const setTimeBody = extractBlock(src, 'setTime(t) {');
|
||||
const getTimeBody = extractBlock(src, 'getTime() {');
|
||||
// Strip trailing comma if present (object-literal method declarations).
|
||||
@ -72,8 +72,25 @@ function buildClockSandbox(perfNowImpl) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
|
||||
// R3c: highway.js is being carved into modules, so its source is no longer ONE file. Read the
|
||||
// whole set. Re-pinning these assertions at whichever file currently holds a constant just
|
||||
// means they break again on the next carve — and worse, a source-shape assertion that silently
|
||||
// stops finding its target is indistinguishable from one that passes.
|
||||
function highwaySources() {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const jsDir = path.join(root, 'static', 'js');
|
||||
const parts = [fs.readFileSync(path.join(root, 'static', 'highway.js'), 'utf8')];
|
||||
for (const f of fs.readdirSync(jsDir).sort()) {
|
||||
if (f.startsWith('highway-') && f.endsWith('.js')) {
|
||||
parts.push(fs.readFileSync(path.join(jsDir, f), 'utf8'));
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
test('highway declares chart anchor + stall-detect + rate state', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const src = highwaySources();
|
||||
// Both anchor fields use NaN sentinels — _chartAnchorAudioT in
|
||||
// particular MUST start as NaN, not 0, otherwise setTime(0) on the
|
||||
// very first 60 Hz tick fails the `t !== _chartAnchorAudioT` check
|
||||
@ -82,11 +99,11 @@ test('highway declares chart anchor + stall-detect + rate state', () => {
|
||||
assert.match(src, /hwState\._chartAnchorPerfNow\s*=\s*NaN/, 'missing _chartAnchorPerfNow (NaN sentinel)');
|
||||
assert.match(src, /hwState\._chartLastAdvanceAt\s*=\s*0/, 'missing _chartLastAdvanceAt (pause detection)');
|
||||
assert.match(src, /hwState\._chartObservedRate\s*=\s*1/, 'missing _chartObservedRate (playback rate awareness)');
|
||||
assert.match(src, /const\s+_CHART_MAX_INTERP_MS\s*=\s*100/, 'missing _CHART_MAX_INTERP_MS cap');
|
||||
assert.match(src, /(?:export\s+)?const\s+_CHART_MAX_INTERP_MS\s*=\s*100/, 'missing _CHART_MAX_INTERP_MS cap');
|
||||
});
|
||||
|
||||
test('getTime scales interpolation by _chartObservedRate (speed-slider safe)', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const src = highwaySources();
|
||||
const m = src.match(/getTime\(\)\s*\{[\s\S]+?\n\s*\},/);
|
||||
assert.ok(m, 'getTime() body not found');
|
||||
const slice = m[0];
|
||||
@ -98,7 +115,7 @@ test('getTime scales interpolation by _chartObservedRate (speed-slider safe)', (
|
||||
});
|
||||
|
||||
test('setTime re-anchors and updates _chartLastAdvanceAt only when t actually changes', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const src = highwaySources();
|
||||
// Repeated setTime calls with the same value must not refresh the
|
||||
// anchor (else interpolation stutters); they also must not refresh
|
||||
// _chartLastAdvanceAt (else getTime would never detect a stalled
|
||||
@ -115,7 +132,7 @@ test('setTime re-anchors and updates _chartLastAdvanceAt only when t actually ch
|
||||
});
|
||||
|
||||
test('getTime falls back to chartTime when audio has stalled (paused)', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const src = highwaySources();
|
||||
// Find the actual getTime body. Match the whole brace-balanced
|
||||
// method (using a generous greedy slice to ensure we capture both
|
||||
// the stall check and the interpolation expression below it).
|
||||
@ -139,7 +156,7 @@ test('getTime falls back to chartTime when audio has stalled (paused)', () => {
|
||||
});
|
||||
|
||||
test('api.stop() clears the chart anchor state so re-init starts fresh', () => {
|
||||
const src = fs.readFileSync(HIGHWAY_JS, 'utf8');
|
||||
const src = highwaySources();
|
||||
// Use the brace-balanced extractor so the assertions are scoped to
|
||||
// the actual stop() body — a fixed-size slice would falsely match
|
||||
// resets that landed in an adjacent method.
|
||||
|
||||
@ -29,14 +29,31 @@ function extractBlock(src, signature) {
|
||||
return src.slice(start, i);
|
||||
}
|
||||
|
||||
|
||||
// R3c: highway.js is being carved into modules, so its source is no longer ONE file. Read the
|
||||
// whole set. Re-pinning these assertions at whichever file currently holds a constant just
|
||||
// means they break again on the next carve — and worse, a source-shape assertion that silently
|
||||
// stops finding its target is indistinguishable from one that passes.
|
||||
function highwaySources() {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const jsDir = path.join(root, 'static', 'js');
|
||||
const parts = [fs.readFileSync(path.join(root, 'static', 'highway.js'), 'utf8')];
|
||||
for (const f of fs.readdirSync(jsDir).sort()) {
|
||||
if (f.startsWith('highway-') && f.endsWith('.js')) {
|
||||
parts.push(fs.readFileSync(path.join(jsDir, f), 'utf8'));
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
test('highway declares the paused-render throttle state', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
assert.match(src, /const\s+_PAUSED_FRAME_INTERVAL_MS\s*=\s*\d+/, 'missing _PAUSED_FRAME_INTERVAL_MS cap');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /(?:export\s+)?const\s+_PAUSED_FRAME_INTERVAL_MS\s*=\s*\d+/, 'missing _PAUSED_FRAME_INTERVAL_MS cap');
|
||||
assert.match(src, /hwState\._lastPausedDrawAt\s*=\s*0/, 'missing _lastPausedDrawAt accumulator');
|
||||
});
|
||||
|
||||
test('draw() throttles full renders while the audio clock is stalled', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const fn = extractBlock(src, 'function draw()');
|
||||
// Reuse getTime()'s pause signal rather than inventing a parallel one.
|
||||
assert.match(fn, /_chartLastAdvanceAt/, 'throttle must key off _chartLastAdvanceAt (the advance timestamp)');
|
||||
@ -46,7 +63,7 @@ test('draw() throttles full renders while the audio clock is stalled', () => {
|
||||
});
|
||||
|
||||
test('throttle runs after the ready gate, before bundle/draw', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const fn = extractBlock(src, 'function draw()');
|
||||
// Regex landmarks (not exact-string indexOf) so harmless spacing /
|
||||
// semicolon changes don't break the ordering guard — matches the
|
||||
|
||||
@ -37,18 +37,35 @@ function extractBlock(src, signature) {
|
||||
|
||||
// ── 2D highway (static/highway.js) ────────────────────────────────────────
|
||||
|
||||
|
||||
// R3c: highway.js is being carved into modules, so its source is no longer ONE file. Read the
|
||||
// whole set. Re-pinning these assertions at whichever file currently holds a constant just
|
||||
// means they break again on the next carve — and worse, a source-shape assertion that silently
|
||||
// stops finding its target is indistinguishable from one that passes.
|
||||
function highwaySources() {
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const jsDir = path.join(root, 'static', 'js');
|
||||
const parts = [fs.readFileSync(path.join(root, 'static', 'highway.js'), 'utf8')];
|
||||
for (const f of fs.readdirSync(jsDir).sort()) {
|
||||
if (f.startsWith('highway-') && f.endsWith('.js')) {
|
||||
parts.push(fs.readFileSync(path.join(jsDir, f), 'utf8'));
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
test('2D palette arrays are mutable (let) with frozen DEFAULT_* originals', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
assert.match(src, /const\s+DEFAULT_STRING_COLORS\s*=/, 'DEFAULT_STRING_COLORS must exist for reset');
|
||||
assert.match(src, /const\s+DEFAULT_STRING_DIM\s*=/, 'DEFAULT_STRING_DIM must exist for reset');
|
||||
assert.match(src, /const\s+DEFAULT_STRING_BRIGHT\s*=/, 'DEFAULT_STRING_BRIGHT must exist for reset');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /(?:export\s+)?const\s+DEFAULT_STRING_COLORS\s*=/, 'DEFAULT_STRING_COLORS must exist for reset');
|
||||
assert.match(src, /(?:export\s+)?const\s+DEFAULT_STRING_DIM\s*=/, 'DEFAULT_STRING_DIM must exist for reset');
|
||||
assert.match(src, /(?:export\s+)?const\s+DEFAULT_STRING_BRIGHT\s*=/, 'DEFAULT_STRING_BRIGHT must exist for reset');
|
||||
assert.match(src, /hwState\.STRING_COLORS\s*=\s*DEFAULT_STRING_COLORS\.slice\(\)/, 'STRING_COLORS must be a mutable copy of the defaults');
|
||||
assert.match(src, /hwState\.STRING_DIM\s*=\s*DEFAULT_STRING_DIM\.slice\(\)/, 'STRING_DIM must be a mutable copy of the defaults');
|
||||
assert.match(src, /hwState\.STRING_BRIGHT\s*=\s*DEFAULT_STRING_BRIGHT\.slice\(\)/, 'STRING_BRIGHT must be a mutable copy of the defaults');
|
||||
});
|
||||
|
||||
test('2D public API exposes getStringColors / setStringColors', () => {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
assert.match(src, /getStringColors\s*\(\s*\)\s*\{\s*return\s+hwState\.STRING_COLORS\.slice\(\)/, 'getStringColors must return a copy');
|
||||
const fn = extractBlock(src, 'setStringColors(arr)');
|
||||
// Each provided index sets base + derived dim/bright; missing → default.
|
||||
@ -110,7 +127,7 @@ test('app.js color manager name-maps to both highways, with identity no-op + bui
|
||||
// ── Executable: dim/bright derivation math ────────────────────────────────
|
||||
|
||||
function loadColorMath() {
|
||||
const src = fs.readFileSync(highwayJs, 'utf8');
|
||||
const src = highwaySources();
|
||||
const snippet = [
|
||||
extractBlock(src, 'function _clampByte(n)'),
|
||||
extractBlock(src, 'function _parseHex(hex)'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user