mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-25 14:21:21 +00:00
Some checks are pending
ship-ci / ci (push) Waiting to run
* feat(keys_highway_3d): add note-colour palettes and selectable camera angles
Give the 3D keys highway player-facing view options and a tuned default
look, so the piano highway is readable out of the box and customisable
from the settings panel without touching code.
Note colours (settings -> Note colours, `keys3d_bg_palette`):
- Octaves (new default): each octave its own hue climbing the rainbow,
darker sharps, so pitch height reads at a glance on any note range.
- Rainbow: the original per-pitch table (colours unchanged).
- Vivid / Pastel: per-pitch variants.
- Emerald / Ice: single-hue two-tone (uniform naturals, darker sharps).
The pick drives the notes, key glow, lane guides and hit flames, live.
Camera (settings -> Camera angle, `keys3d_bg_camera`):
- Classic (the original low rig) / Elevated / Overhead (new default).
- Height, distance and tilt fine-tune sliders nudge the base vantage the
auto-pan/zoom follow-motion orbits; presets apply live.
The new defaults are opinionated for plug-and-play (octaves palette,
overhead camera, tilt -0.6); anyone who prefers the original look can
pick Rainbow + Classic. Settings changes are re-read on init() so they
apply on return from the settings screen, not only after a relaunch.
Scoring, hit-timing and MIDI handling are untouched -- these are purely
visual. Numeric FX keys clamp to declared ranges (FX_RANGES); the pure
colour/camera helpers are covered by unit tests (node --test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: LegionaryLeader <legionaryleader@gmail.com>
* fix(keys_highway_3d): make Classic camera reproduce the original rig + drop per-frame camera alloc
Review follow-ups on the palettes/camera feature:
- "Classic" preset now reproduces the historical rig exactly. The tuned
plug-and-play downward aim (camTilt -0.6 x CAM_TILT_UNITS = -33) is baked
into CAM_PRESETS.overhead.lookY, and camTilt now defaults to 0 (neutral).
The default overhead look is byte-identical (effective lookY still -33),
but "pick Classic for the original look" is now actually true instead of
leaving a -33 down-tilt applied. settings.html tilt slider defaults to 0.
- _rig() writes into a hoisted reusable object instead of allocating a fresh
{y,z,lookY,lookZ} literal every frame, honoring the module's documented
"no per-frame allocations in draw()" discipline. Callers read it
synchronously and never retain it, so one shared instance is safe.
Tests updated for the neutral camTilt default; adds an invariant test that
the default overhead framing is unchanged and Classic + neutral tilt == the
historical LOOK_Y. Full JS suite green (1069 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: LegionaryLeader <legionaryleader@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
300 lines
17 KiB
HTML
300 lines
17 KiB
HTML
<div role="group" aria-labelledby="keysh3d-heading">
|
||
<h3 id="keysh3d-heading" class="text-sm font-medium text-gray-400 mb-2">3D Keys Highway</h3>
|
||
<p class="text-xs text-gray-500 mb-3">
|
||
Pick a song with keys notation (sloppak-spec §5.3) and choose
|
||
<em>Keys Highway 3D</em> from the viz picker (Auto selects it for
|
||
notation charts). The plugin auto-attaches to your MIDI keyboard;
|
||
device, channel and transpose are configured from the player for
|
||
now — this panel holds the graphics controls.
|
||
</p>
|
||
|
||
<!-- Graphics -->
|
||
<div class="mt-3">
|
||
<h4 class="text-xs font-medium text-gray-300 mb-2">Graphics</h4>
|
||
|
||
<label for="keysh3d-fx-palette" class="text-xs font-medium text-gray-400 mb-1 block">Note colours</label>
|
||
<select id="keysh3d-fx-palette"
|
||
onchange="window.keys3dSetPalette && window.keys3dSetPalette(this.value)"
|
||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-xs text-gray-300 outline-none">
|
||
<option value="octaves" selected>Octaves (colour per octave, darker sharps)</option>
|
||
<option value="emerald">Emerald (green, darker sharps)</option>
|
||
<option value="ice">Ice (blue, darker sharps)</option>
|
||
<option value="classic">Rainbow (per-pitch)</option>
|
||
<option value="vivid">Vivid (per-pitch, punchier)</option>
|
||
<option value="pastel">Pastel (per-pitch, soft)</option>
|
||
</select>
|
||
<p class="text-xs text-gray-500 mt-1 mb-3">
|
||
Choose the colour scheme for the falling notes, key glow, lane
|
||
guides and hit flames. Each option is described in its own label.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-theme" class="text-xs font-medium text-gray-400 mb-1 block">Scene theme</label>
|
||
<select id="keysh3d-fx-theme"
|
||
onchange="window.keys3dSetTheme && window.keys3dSetTheme(this.value)"
|
||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-xs text-gray-300 outline-none">
|
||
<option value="default">Default (original)</option>
|
||
<option value="midnight">Midnight</option>
|
||
<option value="charcoal">Charcoal</option>
|
||
<option value="deeppurple">Deep Purple</option>
|
||
<option value="forest">Forest</option>
|
||
<option value="warmslate">Warm Slate</option>
|
||
<option value="deepfocus">Deep Focus</option>
|
||
<option value="deepsea">Deep Sea</option>
|
||
<option value="cathode">Cathode</option>
|
||
<option value="cathodegreen">Cathode Green</option>
|
||
<option value="hearth">Hearth</option>
|
||
</select>
|
||
<p class="text-xs text-gray-500 mt-1 mb-3">
|
||
Background gradient, floor and lane rails — the same theme names
|
||
as the guitar highway. Note colours come from the
|
||
"Note colours" palette above.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-camera" class="text-xs font-medium text-gray-400 mb-1 block">Camera angle</label>
|
||
<select id="keysh3d-fx-camera"
|
||
onchange="window.keys3dSetCamera && window.keys3dSetCamera(this.value)"
|
||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-xs text-gray-300 outline-none">
|
||
<option value="classic">Classic (low, deep runway)</option>
|
||
<option value="elevated">Elevated (higher, more board)</option>
|
||
<option value="overhead" selected>Overhead (top-down reading view)</option>
|
||
</select>
|
||
<p class="text-xs text-gray-500 mt-1 mb-3">
|
||
Where the camera sits. Classic is the original low rig; Elevated
|
||
lifts it for a fuller view of the keybed; Overhead looks down the
|
||
lanes for a sheet-reading feel. Applies live, keeps the
|
||
auto-pan/zoom that follows your hands.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-camheight" class="text-xs font-medium text-gray-400 mb-1 block">
|
||
Camera height <span id="keysh3d-fx-camheight-val" class="text-gray-500 font-mono">1.00</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-camheight"
|
||
min="0.4" max="2.2" step="0.02" value="1"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('camHeight', this.value); document.getElementById('keysh3d-fx-camheight-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Raise or lower the camera around the angle above (higher = more
|
||
top-down). Fine-tunes the base view; the follow-motion stays.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-camdist" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
|
||
Camera distance <span id="keysh3d-fx-camdist-val" class="text-gray-500 font-mono">1.00</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-camdist"
|
||
min="0.4" max="2.2" step="0.02" value="1"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('camDist', this.value); document.getElementById('keysh3d-fx-camdist-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Pull the camera back or push it in (larger = further away, smaller
|
||
= closer).
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-camtilt" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
|
||
Camera tilt <span id="keysh3d-fx-camtilt-val" class="text-gray-500 font-mono">0.00</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-camtilt"
|
||
min="-1" max="1" step="0.02" value="0"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('camTilt', this.value); document.getElementById('keysh3d-fx-camtilt-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<p class="text-xs text-gray-500 mt-1 mb-3">
|
||
Tilt the view up (+) or down (−) without moving the camera —
|
||
aims higher up the runway or down toward the keys. 0 = neutral.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-cinematic" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer">
|
||
<input type="checkbox" id="keysh3d-fx-cinematic" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('cinematic', this.checked)">
|
||
Cinematic lighting
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Dimmer ambience, stronger key light — deeper shading on the keys
|
||
and gems.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-glow" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
|
||
Glow strength <span id="keysh3d-fx-glow-val" class="text-gray-500 font-mono">0.50</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-glow"
|
||
min="0" max="1" step="0.05" value="0.5"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('glow', this.value); document.getElementById('keysh3d-fx-glow-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
How hard the notes, key glow and sustain consume-flash
|
||
self-illuminate (0.5 = stock). Pairs with bloom.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-bgstyle" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">Background ambience</label>
|
||
<select id="keysh3d-fx-bgstyle"
|
||
onchange="window.keys3dSetBgStyle && window.keys3dSetBgStyle(this.value)"
|
||
class="w-full bg-dark-700 border border-gray-800 rounded-lg px-3 py-2 text-xs text-gray-300 outline-none">
|
||
<option value="off">Off</option>
|
||
<option value="particles" selected>Particles</option>
|
||
<option value="lights">Stage lights</option>
|
||
<option value="geometric">Geometric</option>
|
||
</select>
|
||
<label for="keysh3d-fx-bgintensity" class="text-xs font-medium text-gray-400 mb-1 mt-2 block">
|
||
Ambience intensity <span id="keysh3d-fx-bgintensity-val" class="text-gray-500 font-mono">0.50</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-bgintensity"
|
||
min="0" max="1" step="0.05" value="0.5"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('bgIntensity', this.value); document.getElementById('keysh3d-fx-bgintensity-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<label for="keysh3d-fx-bgreactive" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-2">
|
||
<input type="checkbox" id="keysh3d-fx-bgreactive" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('bgReactive', this.checked)">
|
||
Audio-reactive ambience
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
The backdrop pulses with the mix (stems analyser when a sloppak
|
||
is loaded). Off = it animates on time only.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-scorefx" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
|
||
<input type="checkbox" id="keysh3d-fx-scorefx" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('scoreFx', this.checked)">
|
||
Score effects
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
+1 pops on scored presses, a ring pulse every 10-combo,
|
||
milestone bursts at 25/50/100, and a brief red flicker when a
|
||
streak breaks.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-bloom" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
|
||
<input type="checkbox" id="keysh3d-fx-bloom" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('bloom', this.checked)">
|
||
Glow (bloom)
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Soft light-bleed around the hit line, hit flames and consumed
|
||
sustains. Applies live; turn off to reclaim GPU headroom on
|
||
weak machines.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-sparks" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
|
||
<input type="checkbox" id="keysh3d-fx-sparks" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('sparks', this.checked)">
|
||
Hit sparks
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
A small particle burst on every scored key press, beside the
|
||
pitch-colored flame.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-timing" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
|
||
<input type="checkbox" id="keysh3d-fx-timing" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('timingFx', this.checked)">
|
||
Timing colours
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Tint the sparks by timing — on-time green, early cyan, late
|
||
amber. Off = always green.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-streak" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer mt-3">
|
||
<input type="checkbox" id="keysh3d-fx-streak" checked
|
||
onchange="window.keys3dSetFx && window.keys3dSetFx('streakFx', this.checked)">
|
||
Streak feedback
|
||
</label>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Spark bursts grow with your combo.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-hitfx" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
|
||
Hit feedback intensity <span id="keysh3d-fx-hitfx-val" class="text-gray-500 font-mono">0.70</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-hitfx"
|
||
min="0" max="1" step="0.05" value="0.7"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('hitFx', this.value); document.getElementById('keysh3d-fx-hitfx-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Drives the hit-line brightness kick on scored presses. 0 turns
|
||
it off.
|
||
</p>
|
||
|
||
<label for="keysh3d-fx-vibrancy" class="text-xs font-medium text-gray-400 mb-1 mt-3 block">
|
||
Note vibrancy <span id="keysh3d-fx-vibrancy-val" class="text-gray-500 font-mono">0.85</span>
|
||
</label>
|
||
<input type="range" id="keysh3d-fx-vibrancy"
|
||
min="0" max="1" step="0.05" value="0.85"
|
||
oninput="window.keys3dSetFx && window.keys3dSetFx('vibrancy', this.value); document.getElementById('keysh3d-fx-vibrancy-val').textContent = parseFloat(this.value).toFixed(2)"
|
||
class="w-full">
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
Note-gem solidity and lane-guide strength — lower to see more of
|
||
the keyboard through the notes.
|
||
</p>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
// Hydrate controls from stored config on first paint. Narrow the
|
||
// try/catch to just the localStorage reads (drum_highway_3d
|
||
// settings-hydration convention).
|
||
try {
|
||
// FX toggles (keys3d_bg_* — guitar-parity graphics controls).
|
||
// Only explicit values override; absent/corrupt keys keep the
|
||
// default (ON), matching screen.js readFxSettings.
|
||
const hydrateFxBool = (key, elId) => {
|
||
const raw = localStorage.getItem('keys3d_bg_' + key);
|
||
if (raw === '1' || raw === 'true') document.getElementById(elId).checked = true;
|
||
else if (raw === '0' || raw === 'false') document.getElementById(elId).checked = false;
|
||
};
|
||
hydrateFxBool('bloom', 'keysh3d-fx-bloom');
|
||
hydrateFxBool('sparks', 'keysh3d-fx-sparks');
|
||
hydrateFxBool('timingFx', 'keysh3d-fx-timing');
|
||
hydrateFxBool('streakFx', 'keysh3d-fx-streak');
|
||
hydrateFxBool('cinematic', 'keysh3d-fx-cinematic');
|
||
hydrateFxBool('bgReactive', 'keysh3d-fx-bgreactive');
|
||
hydrateFxBool('scoreFx', 'keysh3d-fx-scorefx');
|
||
const hydrateFxRange = (key, elId, valId) => {
|
||
const n = parseFloat(localStorage.getItem('keys3d_bg_' + key));
|
||
if (!Number.isFinite(n)) return;
|
||
const v = Math.min(1, Math.max(0, n));
|
||
document.getElementById(elId).value = String(v);
|
||
document.getElementById(valId).textContent = v.toFixed(2);
|
||
};
|
||
hydrateFxRange('hitFx', 'keysh3d-fx-hitfx', 'keysh3d-fx-hitfx-val');
|
||
hydrateFxRange('vibrancy', 'keysh3d-fx-vibrancy', 'keysh3d-fx-vibrancy-val');
|
||
hydrateFxRange('glow', 'keysh3d-fx-glow', 'keysh3d-fx-glow-val');
|
||
hydrateFxRange('bgIntensity', 'keysh3d-fx-bgintensity', 'keysh3d-fx-bgintensity-val');
|
||
// Camera fine-tune sliders live outside 0-1 — clamp to the
|
||
// control's own min/max (mirrors screen.js FX_RANGES).
|
||
const hydrateFxRangeIn = (key, elId, valId) => {
|
||
const n = parseFloat(localStorage.getItem('keys3d_bg_' + key));
|
||
if (!Number.isFinite(n)) return;
|
||
const el = document.getElementById(elId);
|
||
const v = Math.min(parseFloat(el.max), Math.max(parseFloat(el.min), n));
|
||
el.value = String(v);
|
||
document.getElementById(valId).textContent = v.toFixed(2);
|
||
};
|
||
hydrateFxRangeIn('camHeight', 'keysh3d-fx-camheight', 'keysh3d-fx-camheight-val');
|
||
hydrateFxRangeIn('camDist', 'keysh3d-fx-camdist', 'keysh3d-fx-camdist-val');
|
||
hydrateFxRangeIn('camTilt', 'keysh3d-fx-camtilt', 'keysh3d-fx-camtilt-val');
|
||
const storedCamera = localStorage.getItem('keys3d_bg_camera');
|
||
const cameraSel = document.getElementById('keysh3d-fx-camera');
|
||
if (storedCamera && Array.from(cameraSel.options).some(o => o.value === storedCamera)) {
|
||
cameraSel.value = storedCamera;
|
||
}
|
||
const storedStyle = localStorage.getItem('keys3d_bg_style');
|
||
const styleSel = document.getElementById('keysh3d-fx-bgstyle');
|
||
if (storedStyle && Array.from(styleSel.options).some(o => o.value === storedStyle)) {
|
||
styleSel.value = storedStyle;
|
||
}
|
||
const storedTheme = localStorage.getItem('keys3d_bg_theme');
|
||
const themeSel = document.getElementById('keysh3d-fx-theme');
|
||
if (storedTheme && Array.from(themeSel.options).some(o => o.value === storedTheme)) {
|
||
themeSel.value = storedTheme;
|
||
}
|
||
const storedPalette = localStorage.getItem('keys3d_bg_palette');
|
||
const paletteSel = document.getElementById('keysh3d-fx-palette');
|
||
if (storedPalette && Array.from(paletteSel.options).some(o => o.value === storedPalette)) {
|
||
paletteSel.value = storedPalette;
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Keys-Hwy3D settings] hydration failed:', e);
|
||
}
|
||
})();
|
||
</script>
|
||
</div>
|