mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(keys_highway_3d): anti-plastic materials, studio env, themes, gradient sky, cinematic + glow (K3) (#711)
- Note gems -> MeshPhysicalMaterial: clearcoat 1.0/0.18, roughness 0.32, envMapIntensity 0.9 — lacquered glass, not plastic (the explicit ask) - _makeStudioEnv (PORTED drum_highway_3d): PMREM studio -> scene.environment; black keys 0.22 roughness / 1.3 envInt (glossy piano black), whites 0.42/0.55 ivory, floor 0.55/0.15/0.4 stage sheen - Vertical-gradient background (light horizon -> theme clear -> dark deck), sRGB-tagged for the composer path - BG_THEMES port (guitar ids/values; keys 'default' = original palette; themes drive gradient/fog/floor/rails, never the pitch-class colors); keys3dSetTheme + keys3d_bg_theme, live _applyTheme - Cinematic lighting toggle (0.55/1.3 on; stock 0.75/1.1 off); Glow slider across NOTE_EMISSIVE_BASE / consume-flash / key approach-glow - Env RT + gradient texture disposed in teardown - Tests: theme table parity + default preservation (28 total) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
95b0786725
commit
80fc371f11
@@ -37,6 +37,12 @@ tunes one of these, mirror the change here (and in `drum_highway_3d`):
|
||||
colors (green/cyan/amber) + the 40%-window classifier
|
||||
- `_ssActive()` — host splitscreen probe (minus the guitar's focus-API
|
||||
checks, which it needs for input routing and we don't)
|
||||
- `BG_THEMES` / `_bgThemeColors()` — the scene theme table (same ids/values
|
||||
as the guitar's, except `default` which is this plugin's original
|
||||
palette); one pick drives background gradient + floor + lane rails
|
||||
- `_makeStudioEnv()` — procedural PMREM studio environment (shared with
|
||||
drum_highway_3d; RoomEnvironment isn't vendored)
|
||||
- `_applyCinematic()` — ambient/key rebalance (values tuned per plugin)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -908,8 +908,52 @@
|
||||
streakFx: true, // consecutive-hit escalation (bigger bursts)
|
||||
hitFx: 0.7, // 0–1 master intensity for the hit-line kick
|
||||
vibrancy: 0.85, // note-gem opacity + lane-guide strength
|
||||
cinematic: true, // rebalanced lighting (dimmer ambient, stronger key)
|
||||
glow: 0.5, // 0–1 emissive multiplier (0.5 = the stock look)
|
||||
};
|
||||
const FX_LS_PREFIX = 'keys3d_bg_';
|
||||
// Theme id lives OUTSIDE FX_DEFAULTS (string, not bool/number) — its own
|
||||
// localStorage key + validation against BG_THEMES.
|
||||
const FX_LS_THEME = 'keys3d_bg_theme';
|
||||
|
||||
// Scene color themes — PORTED FROM highway_3d BG_THEMES (keep the ids,
|
||||
// names and values in sync so a user's look carries across instruments).
|
||||
// One pick drives clear/fog + the highway floor + the lane-edge rails;
|
||||
// pitch-class note/key colors are identity and never themed.
|
||||
// clear/fog — background gradient anchor + distance fog
|
||||
// board — the highway floor plane
|
||||
// laneDim — the thin lane-edge rails (lane itself is pitch-colored)
|
||||
const BG_THEMES = {
|
||||
// 'default' is THIS plugin's original palette (fog 0x1a1a2e, floor
|
||||
// 0x141422, rail 0x2a2a3e) — byte-identical for anyone who never
|
||||
// touches the setting. Every OTHER id matches the guitar's table.
|
||||
default: { clear: 0x1a1a2e, fog: 0x1a1a2e, board: 0x141422, laneDim: 0x2a2a3e },
|
||||
midnight: { clear: 0x0a0e1a, fog: 0x0a0e1a, board: 0x080d1c, lane: 0x244fae, laneDim: 0x122a5e },
|
||||
charcoal: { clear: 0x16181c, fog: 0x16181c, board: 0x141417, lane: 0x525a66, laneDim: 0x282d34 },
|
||||
deeppurple: { clear: 0x140a1e, fog: 0x140a1e, board: 0x0b0610, lane: 0x3a1f6e, laneDim: 0x1f1040 },
|
||||
forest: { clear: 0x0a1614, fog: 0x0a1614, board: 0x06100c, lane: 0x15602a, laneDim: 0x0a3318 },
|
||||
warmslate: { clear: 0x1c130b, fog: 0x1c130b, board: 0x0e0805, lane: 0x5e3a12, laneDim: 0x341f0a },
|
||||
deepfocus: { clear: 0x0c0c0d, fog: 0x0c0c0d, board: 0x060606, lane: 0x2f7fa0, laneDim: 0x163c4e },
|
||||
deepsea: { clear: 0x06222b, fog: 0x06222b, board: 0x03141a, lane: 0x0e5a63, laneDim: 0x063338 },
|
||||
cathode: { clear: 0x140b03, fog: 0x140b03, board: 0x0c0702, lane: 0x6e4a0e, laneDim: 0x3a2806 },
|
||||
cathodegreen: { clear: 0x07301a, fog: 0x07301a, board: 0x031a0c, lane: 0x0e6e2a, laneDim: 0x073a18 },
|
||||
hearth: { clear: 0x280806, fog: 0x280806, board: 0x1a0606, lane: 0x7a2410, laneDim: 0x3f1409 },
|
||||
};
|
||||
function _bgThemeColors(id) { return BG_THEMES[id] || BG_THEMES.default; }
|
||||
function readThemeSetting() {
|
||||
try {
|
||||
const id = localStorage.getItem(FX_LS_THEME);
|
||||
if (id && BG_THEMES[id]) return id;
|
||||
} catch (_) {}
|
||||
return 'default';
|
||||
}
|
||||
window.keys3dSetTheme = function (id) {
|
||||
if (!BG_THEMES[id]) return;
|
||||
try { localStorage.setItem(FX_LS_THEME, id); } catch (_) {}
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('keys3d:settings', { detail: { theme: id } }));
|
||||
} catch (_) { /* dispatch unavailable — persisted value applies next init */ }
|
||||
};
|
||||
|
||||
function readFxSettings() {
|
||||
const fx = Object.assign({}, FX_DEFAULTS);
|
||||
@@ -1155,6 +1199,7 @@
|
||||
// ── Visual FX state (guitar-highway parity) ─────────────────────
|
||||
let fx = readFxSettings(); // live snapshot, mutated by 'keys3d:settings'
|
||||
let _fxHandler = null;
|
||||
let _fxThemeHandler = null;
|
||||
// Host adaptive-quality scale (bundle.renderScale, 0.25–1) —
|
||||
// multiplied into the device pixel ratio like highway_3d does.
|
||||
let _renderScale = 1;
|
||||
@@ -1177,6 +1222,15 @@
|
||||
let _hitGlowKick = 0; // hit-line brightness kick, decays exp(-t*6)
|
||||
const _laneGuideMats = []; // lane guide materials (vibrancy slider)
|
||||
|
||||
// Theme/material handles (built by buildScene/buildKeyboardAndHighway;
|
||||
// _applyTheme / _applyCinematic / the glow slider retune them live).
|
||||
let _theme = readThemeSetting();
|
||||
let ambLight = null, dirLight = null;
|
||||
let _floorMat = null;
|
||||
const _railMats = []; // lane-edge rail materials (theme laneDim)
|
||||
let _envRT = null; // PMREM render target backing scene.environment
|
||||
let _bgTex = null; // vertical-gradient background texture
|
||||
|
||||
// ── MIDI scoring + live feedback state ──────────────────────────
|
||||
let _layoutInfo = null; // {layout, whiteCount} of current chart
|
||||
let _hits = 0, _misses = 0, _streak = 0, _bestStreak = 0;
|
||||
@@ -1274,26 +1328,133 @@
|
||||
return (layoutEntry.slot - (whiteCount - 1) / 2) * WHITE_W;
|
||||
}
|
||||
|
||||
// Emissive multiplier from the Glow slider — 0.5 is neutral (the
|
||||
// stock look), so `base * _glowMul()` leaves defaults byte-identical.
|
||||
function _glowMul() {
|
||||
return Math.min(1, Math.max(0, fx.glow)) * 2;
|
||||
}
|
||||
|
||||
// Cinematic lighting: dimmer ambient + stronger key light. Off = the
|
||||
// plugin's pre-parity values.
|
||||
function _applyCinematic() {
|
||||
if (!ambLight || !dirLight) return;
|
||||
ambLight.intensity = fx.cinematic ? 0.55 : 0.75;
|
||||
dirLight.intensity = fx.cinematic ? 1.3 : 1.1;
|
||||
}
|
||||
|
||||
// Procedural "studio" environment for image-based lighting — the
|
||||
// anti-plastic core: the clearcoat note gems and the piano-black
|
||||
// keys need something to reflect. PORTED FROM drum_highway_3d
|
||||
// _makeStudioEnv — keep in sync. Returns the PMREM RT or null.
|
||||
function _makeStudioEnv(ThreeLib, renderer) {
|
||||
if (!renderer) return null;
|
||||
try {
|
||||
const envScene = new ThreeLib.Scene();
|
||||
const own = [];
|
||||
const shellGeo = new ThreeLib.BoxGeometry(100, 100, 100);
|
||||
const shellMat = new ThreeLib.MeshBasicMaterial({ color: 0x11131c, side: ThreeLib.BackSide });
|
||||
envScene.add(new ThreeLib.Mesh(shellGeo, shellMat));
|
||||
own.push(shellGeo, shellMat);
|
||||
const strip = (w, h, hex, intensity, x, y, z, rx, ry) => {
|
||||
const g = new ThreeLib.PlaneGeometry(w, h);
|
||||
// DoubleSide: orientation-proof — the bake only runs once,
|
||||
// so the extra faces are free insurance.
|
||||
const m = new ThreeLib.MeshBasicMaterial({ color: hex, side: ThreeLib.DoubleSide });
|
||||
m.color.multiplyScalar(intensity);
|
||||
const mesh = new ThreeLib.Mesh(g, m);
|
||||
mesh.position.set(x, y, z);
|
||||
mesh.rotation.set(rx, ry, 0);
|
||||
envScene.add(mesh);
|
||||
own.push(g, m);
|
||||
};
|
||||
// Deliberate delta from drum_highway_3d: the strips run at
|
||||
// a third of the drum scene's intensity — this scene is
|
||||
// dominated by a WHITE keyboard and a sheened floor, and the
|
||||
// drum-strength strips overexpose it into a milky bloom
|
||||
// flood (screenshot-verified). The blacks' glints survive.
|
||||
strip(60, 8, 0xdfe8ff, 6, 0, 45, 0, Math.PI / 2, 0); // overhead key
|
||||
strip(30, 50, 0xffd9a8, 2.5, -48, 5, 0, 0, Math.PI / 2); // warm left wall
|
||||
strip(30, 50, 0x9fd8ff, 2.5, 48, 5, 0, 0, -Math.PI / 2); // cool right wall
|
||||
const pmrem = new ThreeLib.PMREMGenerator(renderer);
|
||||
const rt = pmrem.fromScene(envScene, 0.04);
|
||||
pmrem.dispose();
|
||||
for (const r of own) r.dispose();
|
||||
return rt;
|
||||
} catch (e) {
|
||||
console.warn('[Keys-Hwy3D] studio env failed (continuing without IBL)', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _disposeEnv() {
|
||||
if (_envRT) { try { _envRT.dispose(); } catch (_) {} _envRT = null; }
|
||||
if (_bgTex) { try { _bgTex.dispose(); } catch (_) {} _bgTex = null; }
|
||||
}
|
||||
|
||||
// Vertical-gradient background: lighter above the horizon, the theme
|
||||
// clear at the midline, darker toward the keyboard — depth the flat
|
||||
// clear color never had. sRGB-tagged so the composer's output
|
||||
// transform reads it correctly.
|
||||
function _makeBgTexture(clearHex) {
|
||||
const cnv = document.createElement('canvas');
|
||||
cnv.width = 2; cnv.height = 256;
|
||||
const ctx = cnv.getContext('2d');
|
||||
const c = new T.Color(clearHex);
|
||||
const top = c.clone().lerp(new T.Color(0xffffff), 0.10);
|
||||
const bottom = c.clone().multiplyScalar(0.55);
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, 256);
|
||||
grad.addColorStop(0, '#' + top.getHexString());
|
||||
grad.addColorStop(0.55, '#' + c.getHexString());
|
||||
grad.addColorStop(1, '#' + bottom.getHexString());
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 2, 256);
|
||||
const tex = new T.CanvasTexture(cnv);
|
||||
tex.colorSpace = T.SRGBColorSpace;
|
||||
return tex;
|
||||
}
|
||||
|
||||
// Apply the active theme to the live scene. Pitch-class note/key
|
||||
// colors are identity — themes own the scene, not the notes.
|
||||
function _applyTheme() {
|
||||
const c = _bgThemeColors(_theme);
|
||||
if (scene) {
|
||||
if (_bgTex) { try { _bgTex.dispose(); } catch (_) {} }
|
||||
_bgTex = _makeBgTexture(c.clear);
|
||||
scene.background = _bgTex;
|
||||
if (scene.fog) scene.fog.color.setHex(c.fog);
|
||||
}
|
||||
if (ren) ren.setClearColor(c.clear, 1);
|
||||
if (_floorMat) _floorMat.color.setHex(c.board);
|
||||
for (const m of _railMats) m.color.setHex(c.laneDim != null ? c.laneDim : 0x2a2a3e);
|
||||
}
|
||||
|
||||
function buildScene() {
|
||||
scene = new T.Scene();
|
||||
// Explicit scene.background (not just the renderer clear color):
|
||||
// the bloom EffectComposer's RenderPass + OutputPass handle a
|
||||
// scene background color-space-correctly, while a bare clear
|
||||
// color comes out visibly lightened under the ACES output
|
||||
// transform (washed-out purple instead of the dark navy).
|
||||
// drum_highway_3d / highway_3d set it the same way.
|
||||
scene.background = new T.Color(FOG_COLOR);
|
||||
scene.fog = new T.Fog(FOG_COLOR, FOG_START, FOG_END);
|
||||
// Theme-driven gradient background (an explicit scene.background
|
||||
// is also what keeps the composer's ACES output from washing out
|
||||
// a bare clear color) + matching fog.
|
||||
const themeCols = _bgThemeColors(_theme);
|
||||
_bgTex = _makeBgTexture(themeCols.clear);
|
||||
scene.background = _bgTex;
|
||||
scene.fog = new T.Fog(themeCols.fog, FOG_START, FOG_END);
|
||||
if (ren) ren.setClearColor(themeCols.clear, 1);
|
||||
|
||||
// Studio environment map — image-based lighting for the
|
||||
// clearcoat gems / piano-black keys. Renderer-bound; disposed in
|
||||
// teardown via _disposeEnv.
|
||||
_envRT = _makeStudioEnv(T, ren);
|
||||
if (_envRT) scene.environment = _envRT.texture;
|
||||
|
||||
cam = new T.PerspectiveCamera(CAM_FOV, 1, 0.1, 2000 * K);
|
||||
_camX = 0; _camTargetX = 0; _camZoom = 1; _camTargetZoom = 1;
|
||||
cam.position.set(0, CAM_Y, CAM_Z);
|
||||
cam.lookAt(0, LOOK_Y, LOOK_Z);
|
||||
|
||||
const ambient = new T.AmbientLight(0xffffff, 0.75);
|
||||
const dir = new T.DirectionalLight(0xffffff, 1.1);
|
||||
dir.position.set(60 * K, 200 * K, 80 * K);
|
||||
scene.add(ambient, dir);
|
||||
ambLight = new T.AmbientLight(0xffffff, 0.75);
|
||||
dirLight = new T.DirectionalLight(0xffffff, 1.1);
|
||||
dirLight.position.set(60 * K, 200 * K, 80 * K);
|
||||
scene.add(ambLight, dirLight);
|
||||
_applyCinematic();
|
||||
|
||||
keyboardGroup = new T.Group();
|
||||
notesGroup = new T.Group();
|
||||
@@ -1434,13 +1595,23 @@
|
||||
let mat = _noteMatCache.get(key);
|
||||
if (mat) return mat;
|
||||
const col = noteColor(midi, hand);
|
||||
mat = new T.MeshStandardMaterial({
|
||||
// MeshPhysicalMaterial with a clearcoat: lacquered glass-gem
|
||||
// look — a sharp coat highlight over a colored body, lit by the
|
||||
// studio env map. This is the "not plastic" ask: the old matte
|
||||
// MeshStandard (roughness 0.78, no envMap) had a dead surface.
|
||||
// (~20-30% more fragment cost than MeshStandard on note pixels;
|
||||
// acceptable — notes cover a modest screen fraction and all
|
||||
// share one shader program.)
|
||||
mat = new T.MeshPhysicalMaterial({
|
||||
color: col,
|
||||
vertexColors: true, // multiply colour by the baked gem ramp
|
||||
emissive: col,
|
||||
emissiveIntensity: NOTE_EMISSIVE_BASE,
|
||||
roughness: 0.78, // matte — kills the glossy "plastic" highlight
|
||||
emissiveIntensity: NOTE_EMISSIVE_BASE * _glowMul(),
|
||||
roughness: 0.32,
|
||||
metalness: 0.0,
|
||||
clearcoat: 1.0,
|
||||
clearcoatRoughness: 0.18,
|
||||
envMapIntensity: 0.9,
|
||||
transparent: true,
|
||||
// Vibrancy-driven: 0.72 (airy, keys clearly visible through
|
||||
// the gems) → 0.94 (solid, saturated). The old fixed 0.8
|
||||
@@ -1632,6 +1803,7 @@
|
||||
_clearGroup(keyboardGroup);
|
||||
_hitGlowMats.length = 0;
|
||||
_laneGuideMats.length = 0;
|
||||
_railMats.length = 0;
|
||||
keyMeshes = new Map();
|
||||
_keyAnim.clear();
|
||||
_keyFlash.clear();
|
||||
@@ -1643,9 +1815,17 @@
|
||||
const hitZ = -WHITE_L / 2;
|
||||
|
||||
// Highway floor receding to the vanishing point.
|
||||
_floorMat = new T.MeshStandardMaterial({
|
||||
color: _bgThemeColors(_theme).board,
|
||||
// Stage-floor sheen (was matte 0.9): the env strips give the
|
||||
// deck a soft reflection lane without mirroring the notes.
|
||||
roughness: 0.55,
|
||||
metalness: 0.15,
|
||||
envMapIntensity: 0.25,
|
||||
});
|
||||
const floor = new T.Mesh(
|
||||
new T.PlaneGeometry(floorW, HIGHWAY_LEN),
|
||||
new T.MeshStandardMaterial({ color: 0x141422, roughness: 0.9 }),
|
||||
_floorMat,
|
||||
);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.set(0, -0.5 * K, -HIGHWAY_LEN / 2 + WHITE_L);
|
||||
@@ -1706,8 +1886,10 @@
|
||||
keyboardGroup.add(strip);
|
||||
// Thin brighter rails at the lane edges for crisp separation.
|
||||
const railMat = new T.MeshBasicMaterial({
|
||||
color: 0x2a2a3e, transparent: true, opacity: 0.5, depthWrite: false,
|
||||
color: (() => { const c = _bgThemeColors(_theme); return c.laneDim != null ? c.laneDim : 0x2a2a3e; })(),
|
||||
transparent: true, opacity: 0.5, depthWrite: false,
|
||||
});
|
||||
_railMats.push(railMat);
|
||||
const rail = new T.Mesh(new T.PlaneGeometry(0.6 * K, guideLen), railMat);
|
||||
rail.rotation.x = -Math.PI / 2;
|
||||
rail.position.set(keyX(entry, whiteCount) - WHITE_W / 2, laneY + 0.05 * K, hitZ - guideLen / 2);
|
||||
@@ -1736,7 +1918,11 @@
|
||||
// intensity up by proximity.
|
||||
emissive: noteColor(midi, 'rh'),
|
||||
emissiveIntensity: 0,
|
||||
roughness: 0.55,
|
||||
// Anti-plastic: glossy piano black with visible strip
|
||||
// reflections; whites keep a subtle ivory sheen (they're
|
||||
// already near-white — env light saturates them fast).
|
||||
roughness: black ? 0.22 : 0.42,
|
||||
envMapIntensity: black ? 1.3 : 0.3,
|
||||
});
|
||||
const mesh = new T.Mesh(black ? blackGeo : whiteGeo, material);
|
||||
// Positions place the geometry where the old centred boxes
|
||||
@@ -2055,6 +2241,9 @@
|
||||
}
|
||||
|
||||
function updateScene(now) {
|
||||
// Rest emissive with the Glow slider applied — computed once so
|
||||
// the per-note '!==' guards stay effective at any glow value.
|
||||
const _restEmissive = NOTE_EMISSIVE_BASE * _glowMul();
|
||||
const hitZ = -WHITE_L / 2;
|
||||
|
||||
// Pan the camera x to follow the active hand: a hit-line-weighted
|
||||
@@ -2136,8 +2325,8 @@
|
||||
if (past <= 0) {
|
||||
if (mesh.scale.z !== 1) mesh.scale.set(1, 1, 1);
|
||||
mesh.position.z = z;
|
||||
if (mesh.material.emissiveIntensity !== NOTE_EMISSIVE_BASE) {
|
||||
mesh.material.emissiveIntensity = NOTE_EMISSIVE_BASE;
|
||||
if (mesh.material.emissiveIntensity !== _restEmissive) {
|
||||
mesh.material.emissiveIntensity = _restEmissive;
|
||||
}
|
||||
} else {
|
||||
const remaining = len - past; // = hitZ - backZ, length still up the runway
|
||||
@@ -2147,14 +2336,14 @@
|
||||
// Glow as it's eaten — intensifies the more of the note is consumed.
|
||||
const consumed = Math.min(1, past / len);
|
||||
mesh.material.emissiveIntensity =
|
||||
NOTE_EMISSIVE_BASE + (CONSUME_GLOW - NOTE_EMISSIVE_BASE) * (0.45 + 0.55 * consumed);
|
||||
(NOTE_EMISSIVE_BASE + (CONSUME_GLOW - NOTE_EMISSIVE_BASE) * (0.45 + 0.55 * consumed)) * _glowMul();
|
||||
}
|
||||
}
|
||||
// Apply the approach-glow (a wrong-note red flash owns emissive meanwhile).
|
||||
for (const [midi, km] of keyMeshes) {
|
||||
if (_keyFlash.has(midi)) continue;
|
||||
const g = km.userData.glow || 0;
|
||||
km.material.emissiveIntensity = g * g * KEY_GLOW_STRENGTH; // ease → pops near the hit-line
|
||||
km.material.emissiveIntensity = g * g * KEY_GLOW_STRENGTH * _glowMul(); // ease → pops near the hit-line
|
||||
}
|
||||
for (const entry of markerSprites) {
|
||||
const z = scrollZ(entry.t, now, hitZ, TS);
|
||||
@@ -2457,6 +2646,13 @@
|
||||
window.removeEventListener('keys3d:settings', _fxHandler);
|
||||
_fxHandler = null;
|
||||
}
|
||||
if (_fxThemeHandler) {
|
||||
window.removeEventListener('keys3d:settings', _fxThemeHandler);
|
||||
_fxThemeHandler = null;
|
||||
}
|
||||
_disposeEnv();
|
||||
_railMats.length = 0;
|
||||
ambLight = dirLight = _floorMat = null;
|
||||
const sm = window.slopsmith;
|
||||
if (sm && typeof sm.off === 'function') {
|
||||
if (_songHandler) sm.off('song:loaded', _songHandler);
|
||||
@@ -2507,6 +2703,10 @@
|
||||
if (_isReady) teardown();
|
||||
highwayCanvas = canvas;
|
||||
fx = readFxSettings();
|
||||
// Persisted string settings refresh here too — a theme saved
|
||||
// while no instance was listening must not come up stale on a
|
||||
// later init().
|
||||
_theme = readThemeSetting();
|
||||
loadThree().then(() => {
|
||||
if (!highwayCanvas) return; // destroyed before load resolved
|
||||
try {
|
||||
@@ -2530,7 +2730,23 @@
|
||||
// Vibrancy is baked into built materials — retint
|
||||
// the live scene without a chart rebuild.
|
||||
if ('vibrancy' in d.fx) _applyVibrancy();
|
||||
if ('cinematic' in d.fx) _applyCinematic();
|
||||
if ('glow' in d.fx) {
|
||||
// Material emissive bases are also per-frame
|
||||
// (updateScene) — only the cached/cloned rest
|
||||
// values need a nudge here.
|
||||
const base = NOTE_EMISSIVE_BASE * _glowMul();
|
||||
for (const m of _noteMatCache.values()) m.emissiveIntensity = base;
|
||||
}
|
||||
};
|
||||
_fxThemeHandler = (ev) => {
|
||||
const d = ev && ev.detail;
|
||||
if (d && d.theme && BG_THEMES[d.theme]) {
|
||||
_theme = d.theme;
|
||||
_applyTheme();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keys3d:settings', _fxThemeHandler);
|
||||
window.addEventListener('keys3d:settings', _fxHandler);
|
||||
_injectHud();
|
||||
_isReady = true;
|
||||
@@ -2687,6 +2903,9 @@
|
||||
judgeHit,
|
||||
sweepMissed,
|
||||
readFxSettings,
|
||||
readThemeSetting,
|
||||
_bgThemeColors,
|
||||
BG_THEMES,
|
||||
FX_DEFAULTS,
|
||||
_classifyTiming,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,51 @@
|
||||
<!-- Graphics -->
|
||||
<div class="mt-3">
|
||||
<h4 class="text-xs font-medium text-gray-300 mb-2">Graphics</h4>
|
||||
<label for="keysh3d-fx-bloom" class="flex items-center gap-2 text-xs text-gray-300 cursor-pointer">
|
||||
|
||||
<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. Pitch-class note colours never change.
|
||||
</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-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)
|
||||
@@ -95,6 +139,7 @@
|
||||
hydrateFxBool('sparks', 'keysh3d-fx-sparks');
|
||||
hydrateFxBool('timingFx', 'keysh3d-fx-timing');
|
||||
hydrateFxBool('streakFx', 'keysh3d-fx-streak');
|
||||
hydrateFxBool('cinematic', 'keysh3d-fx-cinematic');
|
||||
const hydrateFxRange = (key, elId, valId) => {
|
||||
const n = parseFloat(localStorage.getItem('keys3d_bg_' + key));
|
||||
if (!Number.isFinite(n)) return;
|
||||
@@ -104,6 +149,12 @@
|
||||
};
|
||||
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');
|
||||
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;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Keys-Hwy3D settings] hydration failed:', e);
|
||||
}
|
||||
|
||||
@@ -109,3 +109,27 @@ test('FX defaults: hit-FX + vibrancy controls ship enabled', () => {
|
||||
assert.equal(FX_DEFAULTS.hitFx, 0.7);
|
||||
assert.equal(FX_DEFAULTS.vibrancy, 0.85);
|
||||
});
|
||||
|
||||
test('themes: table ids match the guitar highway, default is the stock palette', () => {
|
||||
const { BG_THEMES, _bgThemeColors, readThemeSetting } = load().slopsmithViz_keys_highway_3d.__test;
|
||||
assert.deepEqual(Object.keys(BG_THEMES), [
|
||||
'default', 'midnight', 'charcoal', 'deeppurple', 'forest', 'warmslate',
|
||||
'deepfocus', 'deepsea', 'cathode', 'cathodegreen', 'hearth',
|
||||
]);
|
||||
// 'default' preserves THIS plugin's original look.
|
||||
assert.equal(BG_THEMES.default.clear, 0x1a1a2e);
|
||||
assert.equal(BG_THEMES.default.board, 0x141422);
|
||||
assert.equal(BG_THEMES.default.laneDim, 0x2a2a3e);
|
||||
assert.equal(_bgThemeColors('nonsense'), BG_THEMES.default);
|
||||
assert.equal(readThemeSetting(), 'default'); // no localStorage in the vm
|
||||
for (const [id, t] of Object.entries(BG_THEMES)) {
|
||||
assert.equal(t.clear, t.fog, id + ' clear==fog (horizon dissolve)');
|
||||
assert.ok(t.laneDim != null, id + ' rail color');
|
||||
}
|
||||
});
|
||||
|
||||
test('FX defaults: theme-PR controls ship enabled at stock-neutral values', () => {
|
||||
const { FX_DEFAULTS } = load().slopsmithViz_keys_highway_3d.__test;
|
||||
assert.equal(FX_DEFAULTS.cinematic, true);
|
||||
assert.equal(FX_DEFAULTS.glow, 0.5); // 0.5 = 1.0x multiplier (stock)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user