diff --git a/plugins/highway_3d/plugin.json b/plugins/highway_3d/plugin.json index 7297cdf..b4988f6 100644 --- a/plugins/highway_3d/plugin.json +++ b/plugins/highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "highway_3d", "name": "3D Highway", - "version": "3.28.0", + "version": "3.29.0", "type": "visualization", "bundled": true, "script": "screen.js", diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index d7f7de3..3006698 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -1768,7 +1768,7 @@ return _bgBandsCache; } - const BG_DEFAULTS = { style: 'particles', intensity: 0.5, reactive: true, palette: 'default', bgTheme: 'default', hwTheme: 'default', showFretOnNote: true, fretNumberGhostScope: 'chords', cameraSmoothing: 0.5, zoomSmoothing: 0.5, tiltSmoothing: 0.5, cameraLockLow: false, cameraLockZoom: 0.5, cameraMode: 'lookahead', nutHeadstockVisible: true, tuningLabelsVisible: true, nutColor: '#f5f3f0', headstockColor: '#d4b48a', textSize: 0.5, vibrancy: 0.85, glow: 0.25, customImageDataUrl: '', customImageName: '', customVideoName: '', chordDiagramVisible: true, chordDiagramSize: 0.5, chordDiagramPosition: 'tl', fretColumnMarkerCadence: 1, projectionVisible: true, inlayLabelsVisible: false, sectionLabelsOnHighway: false, sectionHudVisible: false, sectionHudPosition: 'tr', sectionHudSize: 0.5, toneHudVisible: false, toneHudPosition: 'tl', toneHudSize: 0.5, fpsVisible: false, fretDividersVisible: true, slideArrowApproachVisible: true, slideArrowNeckVisible: true, slideArrowChainPreviewVisible: true, hitFx: 0.7, sparks: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: true }; + const BG_DEFAULTS = { style: 'particles', intensity: 0.5, reactive: true, palette: 'default', bgTheme: 'default', hwTheme: 'default', showFretOnNote: true, fretNumberGhostScope: 'chords', cameraSmoothing: 0.5, zoomSmoothing: 0.5, tiltSmoothing: 0.5, cameraLockLow: false, cameraLockZoom: 0.5, cameraMode: 'lookahead', nutHeadstockVisible: true, tuningLabelsVisible: true, nutColor: '#f5f3f0', headstockColor: '#d4b48a', textSize: 0.5, vibrancy: 0.85, glow: 0.25, customImageDataUrl: '', customImageName: '', customVideoName: '', chordDiagramVisible: true, chordDiagramSize: 0.5, chordDiagramPosition: 'tl', fretColumnMarkerCadence: 1, projectionVisible: true, inlayLabelsVisible: false, sectionLabelsOnHighway: false, sectionHudVisible: false, sectionHudPosition: 'tr', sectionHudSize: 0.5, toneHudVisible: false, toneHudPosition: 'tl', toneHudSize: 0.5, fpsVisible: false, fretDividersVisible: true, slideArrowApproachVisible: true, slideArrowNeckVisible: true, slideArrowChainPreviewVisible: true, hitFx: 0.7, sparks: true, strikeLine: true, cinematic: true, verdictMarks: true, timingFx: true, streakFx: true, bloom: true }; // User-selectable, persistable bg styles — must mirror settings.html's // VALID_STYLES. 'venue' is deliberately NOT here: it is an internal effective // style reached only via _venueSceneOverride (the viz-picker Venue flow), so @@ -2115,7 +2115,7 @@ // means (fall back to default rather than silently flipping to // false). Add new boolean keys to BG_DEFAULTS and they pick this // up via the dispatch below. - const _BG_BOOL_KEYS = new Set(['reactive', 'showFretOnNote', 'cameraLockLow', 'inlayLabelsVisible', 'sectionLabelsOnHighway', 'sectionHudVisible', 'nutHeadstockVisible', 'tuningLabelsVisible', 'projectionVisible', 'chordDiagramVisible', 'fpsVisible', 'toneHudVisible', 'fretDividersVisible', 'slideArrowApproachVisible', 'slideArrowNeckVisible', 'slideArrowChainPreviewVisible', 'sparks', 'cinematic', 'verdictMarks', 'timingFx', 'streakFx', 'bloom']); + const _BG_BOOL_KEYS = new Set(['reactive', 'showFretOnNote', 'cameraLockLow', 'inlayLabelsVisible', 'sectionLabelsOnHighway', 'sectionHudVisible', 'nutHeadstockVisible', 'tuningLabelsVisible', 'projectionVisible', 'chordDiagramVisible', 'fpsVisible', 'toneHudVisible', 'fretDividersVisible', 'slideArrowApproachVisible', 'slideArrowNeckVisible', 'slideArrowChainPreviewVisible', 'sparks', 'strikeLine', 'cinematic', 'verdictMarks', 'timingFx', 'streakFx', 'bloom']); function _bgCoerceBool(val, fallback) { if (val === 'true' || val === '1') return true; if (val === 'false' || val === '0') return false; @@ -2261,6 +2261,7 @@ window.h3dBgSetGlow = (v) => _bgWriteGlobal('glow', v); window.h3dBgSetHitFx = (v) => _bgWriteGlobal('hitFx', v); window.h3dBgSetSparks = (v) => _bgWriteGlobal('sparks', !!v); + window.h3dBgSetStrikeLine = (v) => _bgWriteGlobal('strikeLine', !!v); window.h3dBgSetCinematic = (v) => _bgWriteGlobal('cinematic', !!v); window.h3dBgSetVerdictMarks = (v) => _bgWriteGlobal('verdictMarks', !!v); window.h3dBgSetTimingFx = (v) => _bgWriteGlobal('timingFx', !!v); @@ -3561,6 +3562,7 @@ let glowMul = BG_DEFAULTS.glow; let _hitFx = BG_DEFAULTS.hitFx; let _sparks = BG_DEFAULTS.sparks; + let _strikeFx = BG_DEFAULTS.strikeLine; let _cinematic = BG_DEFAULTS.cinematic; let _verdictMarks = BG_DEFAULTS.verdictMarks; let _timingFx = BG_DEFAULTS.timingFx; @@ -6170,6 +6172,13 @@ transparent: true, opacity: 1.0, depthWrite: false, })); mHitBrightArrays = mHitBright.map(m => [m, m, m, m, mEdgeTransparent, mEdgeTransparent]); + // Readability (#2 / charrette): the note gems + their outlines punch THROUGH + // the distance fog so upcoming notes stay legible as they render in at the + // horizon. The board, lane, sustains and background scenery keep their + // atmospheric fog — only the note-defining materials are exempted, so the + // highway still reads as deep while the notes never dissolve into the haze. + [mWhiteOutline, mMissOutline].forEach(m => { if (m) m.fog = false; }); + [mStr, mGlow, mStrHitOutline, mHitBright].forEach(arr => arr && arr.forEach(m => { if (m) m.fog = false; })); // Outline materials render at a lower renderOrder than the body. // The body is rendered on top with opacity:1 on hit/miss, which // fully covers the outline center — only the fringe that extends @@ -7385,6 +7394,7 @@ glowMul = _bgReadSetting(panelKey, 'glow'); _hitFx = _bgReadSetting(panelKey, 'hitFx'); _sparks = _bgReadSetting(panelKey, 'sparks'); + _strikeFx = _bgReadSetting(panelKey, 'strikeLine'); _cinematic = _bgReadSetting(panelKey, 'cinematic'); _verdictMarks = _bgReadSetting(panelKey, 'verdictMarks'); _timingFx = _bgReadSetting(panelKey, 'timingFx'); @@ -7834,7 +7844,7 @@ // Toggle via the 'cinematic' setting so it's directly comparable. function _applyCinematic() { if (!ambLight || !dirLight) return; - ambLight.intensity = _cinematic ? 0.35 : 0.85; + ambLight.intensity = _cinematic ? 0.45 : 0.85; dirLight.intensity = _cinematic ? 1.15 : 0.8; } // #5 early/late: tint the hit feedback by timing — on-time green, early cyan, @@ -7892,7 +7902,12 @@ try { const sz = canvasSize(highwayCanvas) || { w: 1280, h: 720 }; const w = Math.max(2, sz.w | 0), h = Math.max(2, sz.h | 0); - const comp = new EC.EffectComposer(ren); + // Multisampled (WebGL2 MSAA) HalfFloat target so anti-aliasing + // survives the bloom path — EffectComposer's default target has no + // `samples`, which is why bloom-on looked jagged (worst on non-Retina + // DPR1 displays that have no supersampling cushion). + const _bloomRT = new T.WebGLRenderTarget(w, h, { type: T.HalfFloatType, samples: 4 }); + const comp = new EC.EffectComposer(ren, _bloomRT); comp.addPass(new RP.RenderPass(scene, cam)); _bloomPass = new UB.UnrealBloomPass(new T.Vector2(w, h), 0.65, 0.5, 0.82); // strength, radius, threshold (high → only emissive blooms) comp.addPass(_bloomPass); @@ -7949,13 +7964,17 @@ p.position.set(board.center, S_BASE - NH / 2 - 2 * K, -blAhead / 2); fretG.add(p); - // Strike line (#1): a thin glowing bar at the hit line (Z=0). Idle dim; - // flashes green on a hit / red on a miss (driven each frame in the render loop). + // Strike line (#1): a thin glowing "now" bar at the hit line (Z=0). Idle + // faint; flashes green on a HIT only (driven each frame in the render loop). + // Misses are shown at the gem (red wash + ✗), NOT here — a single missed + // string never reds-out the whole bar (charrette panel rec 1a). + // Placed at the vertical CENTRE of the string field (not the bottom edge, + // which read as the fretboard's lower border and fused with open-string gems). { const slGeo = new T.BoxGeometry(bw, NH * 0.42, 0.7 * K); const slMat = new T.MeshBasicMaterial({ color: 0x2a3a52, transparent: true, opacity: 0.0, depthWrite: false, blending: T.AdditiveBlending }); _strikeLine = new T.Mesh(slGeo, slMat); - _strikeLine.position.set(board.center, S_BASE - NH / 2, 0); + _strikeLine.position.set(board.center, (sY(0) + sY(nStr - 1)) / 2, 0); _strikeLine.renderOrder = 6; fretG.add(_strikeLine); } @@ -14494,12 +14513,17 @@ _sparkUpdate(_jdt); _streakHeat += (Math.min(1, _streakHits / 16) - _streakHeat) * 0.08; // #7 ease heat if (_strikeLine) { - const hf = _ndHitFlash * _hitFx, mf = _ndMissFlash * _hitFx; - const lit = Math.max(hf, mf); - const m = _strikeLine.material; - m.opacity = 0.10 + (_streakFx ? 0.12 * _streakHeat : 0) + 0.85 * Math.min(1, lit); - if (lit > 0.002) m.color.setHex(hf >= mf ? 0x22ff88 : 0xff0066); - else m.color.setHex(0x2a3a52); + if (_strikeFx) { + _strikeLine.visible = true; + // Hit-only celebration: faint idle, flashes green on a confirmed + // hit. No red/miss branch — misses live at the gem (charrette 1a). + const hf = _ndHitFlash * _hitFx; + const m = _strikeLine.material; + m.opacity = 0.06 + (_streakFx ? 0.10 * _streakHeat : 0) + 0.85 * Math.min(1, hf); + m.color.setHex(hf > 0.002 ? 0x22ff88 : 0x2a3a52); + } else { + _strikeLine.visible = false; + } _ndHitFlash *= 0.82; _ndMissFlash *= 0.82; } } diff --git a/plugins/highway_3d/settings.html b/plugins/highway_3d/settings.html index d8c1996..21089cb 100644 --- a/plugins/highway_3d/settings.html +++ b/plugins/highway_3d/settings.html @@ -602,6 +602,16 @@ class="accent-accent mt-0.5"> +
+ + +
+