From 60d2223cb225612f6473ac97f867d1679f9ccf16 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 11:06:56 +0000 Subject: [PATCH 1/4] Make grip-sense bar activation read clearly red, not washed-out pink (#74) The grip-sense bar was meant to read white when idle and red when the grip is held, but only the emissive glow changed on activation. Sitting on top of the model's near-white bar material, a saturated grip color (default red) blended out to a faint pink, so the activation was easy to miss. Drive the bar mesh's BASE color from its original (white) toward the grip color while held, lerping back on release. Per-mesh materials are already cloned, and the original color is captured once into material.userData, so this is safe and reversible. A reusable scratch THREE.Color avoids a per-frame allocation. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017NiS2a4jZ877XgftkH1Dd1 --- packages/visualizer/src/controller-overlay.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/visualizer/src/controller-overlay.js b/packages/visualizer/src/controller-overlay.js index 5fd4f29..ab3c644 100644 --- a/packages/visualizer/src/controller-overlay.js +++ b/packages/visualizer/src/controller-overlay.js @@ -104,6 +104,9 @@ export class ControllerOverlay { // Grip highlight color — shares the global highlight color (#45's // overlay:highlightColor / --hl-color). Default matches that picker. this._gripColor = 0xff0000; // follows the highlight default: red + // Reusable scratch color so the per-frame grip-bar tint lerp doesn't + // allocate a THREE.Color every frame. + this._tmpGripColor = new THREE.Color(); // ── Layout editor (#51): click-drag + keyboard-rotate the floatable parts ── this._editMode = false; // editing the float layout right now @@ -1938,6 +1941,19 @@ export class ControllerOverlay { // brightness at max (and stays subtle at the lower default). const barTarget = barOn ? this._gripBrightness * 1.5 : 0; mat.emissiveIntensity = THREE.MathUtils.lerp(mat.emissiveIntensity, barTarget, LERP_SPEED); + // Also drive the bar's BASE color white→grip-color on activation (#74). + // The emissive glow alone sat on top of the model's near-white bar + // material, so a saturated grip color (e.g. red) washed out to a barely + // visible pink and the activation was easy to miss. Tinting the base + // color makes the on/off transition unmistakable. Capture the model's + // original color once so we can lerp back to it on release. + if ('color' in mat) { + if (mat.userData._gripBarBaseColor === undefined) { + mat.userData._gripBarBaseColor = mat.color.getHex(); + } + const targetColor = barOn ? this._gripColor : mat.userData._gripBarBaseColor; + mat.color.lerp(this._tmpGripColor.setHex(targetColor), LERP_SPEED); + } } // (2) always-on-top billboard glow marker (brightness = peak opacity) const marker = this._gripMarkers?.[side]; From e3ed5d6b7b36c2e08e1f1e2f4badba0302607df7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 19:01:19 +0000 Subject: [PATCH 2/4] Grip sense: make bars static + move glow into the handles (PR #76 feedback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the #74 attempt per the reviewer's clarification of the model: the BARS are the cosmetic left/right side strips; the GLOW (the on-top marker inside the handle) is the activation indicator, and Grip Brightness drives the glow. - Stop tinting/flashing the bars on touch. The #74 white→color base-color lerp (plus the brightness-scaled bar emissive) made the bars flash light→dark and rendered differently on web vs the Electron build. The bars are now left fully static, so activation is shown solely by the glow and both targets render identically. - setGripState now only animates the glow marker opacity (brightness = peak). - Move the glow markers farther out into the handles (gripMarkerSideOffset 0.06 → 0.14) so they sit where the hands grip instead of bunched near center. - Clarify settings wording: "Grip Brightness" → "Grip Glow Brightness", and drop the "bars highlight on touch" / "+ bar highlight" copy so Glow vs Bar reflect the actual behavior. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017NiS2a4jZ877XgftkH1Dd1 --- apps/overlay/src/index.html | 6 +- packages/visualizer/src/controller-overlay.js | 55 +++++-------------- .../visualizer/src/controller-profiles.js | 6 +- 3 files changed, 22 insertions(+), 45 deletions(-) diff --git a/apps/overlay/src/index.html b/apps/overlay/src/index.html index 7fa704c..7cf65af 100644 --- a/apps/overlay/src/index.html +++ b/apps/overlay/src/index.html @@ -982,15 +982,15 @@

Window Display

- +
- +
- +
diff --git a/packages/visualizer/src/controller-overlay.js b/packages/visualizer/src/controller-overlay.js index ab3c644..34b62c1 100644 --- a/packages/visualizer/src/controller-overlay.js +++ b/packages/visualizer/src/controller-overlay.js @@ -104,9 +104,6 @@ export class ControllerOverlay { // Grip highlight color — shares the global highlight color (#45's // overlay:highlightColor / --hl-color). Default matches that picker. this._gripColor = 0xff0000; // follows the highlight default: red - // Reusable scratch color so the per-frame grip-bar tint lerp doesn't - // allocate a THREE.Color every frame. - this._tmpGripColor = new THREE.Color(); // ── Layout editor (#51): click-drag + keyboard-rotate the floatable parts ── this._editMode = false; // editing the float layout right now @@ -1914,50 +1911,28 @@ export class ControllerOverlay { } /** - * Highlight the capacitive grip sensors while held (digital on/off). Two - * visuals: (1) emissive glow on the grip meshes — only seen when the back of - * the controller faces the camera; and (2) a billboard glow marker per grip - * rendered ON TOP (depthTest off, at the top of the controller) so grip state - * reads at ANY angle. Toggle both via setGripVisible. Intensity lerps. + * Highlight the capacitive grip sensors while held (digital on/off). The + * activation indicator is a billboard glow marker per grip, rendered ON TOP + * (depthTest off) so grip state reads at ANY angle; its peak opacity is the + * Grip Brightness setting. Gated by setGripVisible. The grip-sense BARS (the + * separate left/right side strips) are purely cosmetic and do NOT change on + * touch — keeping them static avoids the white→color flash and makes web and + * desktop render identically. * @param {{left:boolean, right:boolean}} grips */ setGripState(grips) { const map = PROFILES[this.controllerType]?.gripMeshes; if (!map || !grips) return; for (const side of ['left', 'right']) { - const gripped = !!grips[side]; - // The bars and the on-top glow markers are INDEPENDENT: - // - bars highlight on touch whenever the bars are shown (their own glow), - // - the on-top markers are the separate, toggleable "glow". - const barOn = this._gripBarsVisible && gripped; - const glowOn = this._gripEnabled && gripped; - // (1) bar mesh emissive highlight (on touch, if the bars are visible) - const obj = this.meshes[map[side]]; - const mesh = obj && (obj.isMesh ? obj : obj.children?.find((c) => c.isMesh)); - const mat = mesh?.material; - if (mat && 'emissive' in mat) { - mat.emissive.set(this._gripColor); // set each frame so a color change applies live - // Bar glow scales with the brightness slider too, so it can reach full - // brightness at max (and stays subtle at the lower default). - const barTarget = barOn ? this._gripBrightness * 1.5 : 0; - mat.emissiveIntensity = THREE.MathUtils.lerp(mat.emissiveIntensity, barTarget, LERP_SPEED); - // Also drive the bar's BASE color white→grip-color on activation (#74). - // The emissive glow alone sat on top of the model's near-white bar - // material, so a saturated grip color (e.g. red) washed out to a barely - // visible pink and the activation was easy to miss. Tinting the base - // color makes the on/off transition unmistakable. Capture the model's - // original color once so we can lerp back to it on release. - if ('color' in mat) { - if (mat.userData._gripBarBaseColor === undefined) { - mat.userData._gripBarBaseColor = mat.color.getHex(); - } - const targetColor = barOn ? this._gripColor : mat.userData._gripBarBaseColor; - mat.color.lerp(this._tmpGripColor.setHex(targetColor), LERP_SPEED); - } - } - // (2) always-on-top billboard glow marker (brightness = peak opacity) + // On-top billboard glow marker — the activation cue. Brightness sets its + // peak opacity; lerps in/out. (The bars are left untouched on purpose.) + const glowOn = this._gripEnabled && !!grips[side]; const marker = this._gripMarkers?.[side]; - if (marker) marker.material.opacity = THREE.MathUtils.lerp(marker.material.opacity, glowOn ? this._gripBrightness : 0, LERP_SPEED); + if (marker) { + marker.material.opacity = THREE.MathUtils.lerp( + marker.material.opacity, glowOn ? this._gripBrightness : 0, LERP_SPEED + ); + } } } diff --git a/packages/visualizer/src/controller-profiles.js b/packages/visualizer/src/controller-profiles.js index de7f963..b41fd37 100644 --- a/packages/visualizer/src/controller-profiles.js +++ b/packages/visualizer/src/controller-profiles.js @@ -392,8 +392,10 @@ export const PROFILES = { // Lift the glow up a little into the handle body (fraction toward the top). gripMarkerHeight: 0.1, // Nudge each glow outward into the handle (away from the body center), as a - // fraction of the model width. - gripMarkerSideOffset: 0.06, + // fraction of the model width. Pushed farther out (was 0.06) so the glows + // sit over the controller handles where the hands grip, instead of bunched + // near the center (PR #76 feedback). + gripMarkerSideOffset: 0.14, // Two-tone (ceski/Larf SC2 look, issue #61): light body = shells + grips // only. Everything else is dark accent — the trackpads, the system buttons // (view/menu/steam + the "…" quick-access), and the controls (sticks, face From ae76a8f8997e09e8a04d1b73bddd09a47c64d4a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 19:39:18 +0000 Subject: [PATCH 3/4] Grip sense: stop the bars inheriting the body color; paint them with the grip color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the bar color confusion (spotted in review): the grip-sense bar meshes (left/right_gripsense) were listed in the Steam profile's bodyColorMeshes, so they took the body color and changed with it — washing into the body instead of reading as the grip-sense indicator. - Remove left/right_gripsense from bodyColorMeshes (light body = shells only). - Paint the bars with the grip color via a new _applyGripBarColor(), called on model setup and whenever setGripColor changes — so the bars track the "Grip Sense Color" setting and stay distinct from the body. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_017NiS2a4jZ877XgftkH1Dd1 --- packages/visualizer/src/controller-overlay.js | 21 +++++++++++++++++++ .../visualizer/src/controller-profiles.js | 9 +++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/visualizer/src/controller-overlay.js b/packages/visualizer/src/controller-overlay.js index 34b62c1..72d30d5 100644 --- a/packages/visualizer/src/controller-overlay.js +++ b/packages/visualizer/src/controller-overlay.js @@ -583,6 +583,9 @@ export class ControllerOverlay { this._repositionGripBars(profile); // Honor the current "show grip bars" setting on this freshly-loaded model. this._applyGripBarsVisible(); + // Paint the bars with the grip color (they're not in a theme group, so this + // is the only thing that colors them). + this._applyGripBarColor(); // Diagnostic: per-mapping check at load time so we can tell whether // each gamepad-index → mesh path will animate at runtime. The press @@ -1979,6 +1982,24 @@ export class ControllerOverlay { if (this._gripMarkers) { for (const side of ['left', 'right']) this._gripMarkers[side]?.material.color.setHex(this._gripColor); } + this._applyGripBarColor(); + } + + // Paint the grip-sense BAR meshes with the grip color. They're intentionally + // excluded from the body/accent theme groups (PR #76) — otherwise they'd take + // the body color and vanish into it — so their color is set here instead, and + // re-applied whenever the grip color changes. + _applyGripBarColor() { + const map = PROFILES[this.controllerType]?.gripMeshes; + if (!map) return; + for (const side of ['left', 'right']) { + const obj = this.meshes[map[side]]; + const mesh = obj && (obj.isMesh ? obj : obj.children?.find((c) => c.isMesh)); + if (mesh?.material?.color) { + mesh.material.color.setHex(this._gripColor); + mesh.material.needsUpdate = true; + } + } } // Set up the on-top grip glow markers. Computes each marker's pinned handle diff --git a/packages/visualizer/src/controller-profiles.js b/packages/visualizer/src/controller-profiles.js index b41fd37..9625096 100644 --- a/packages/visualizer/src/controller-profiles.js +++ b/packages/visualizer/src/controller-profiles.js @@ -396,12 +396,15 @@ export const PROFILES = { // sit over the controller handles where the hands grip, instead of bunched // near the center (PR #76 feedback). gripMarkerSideOffset: 0.14, - // Two-tone (ceski/Larf SC2 look, issue #61): light body = shells + grips - // only. Everything else is dark accent — the trackpads, the system buttons + // Two-tone (ceski/Larf SC2 look, issue #61): light body = shells only. + // The grip-sense bars (left/right_gripsense) are deliberately NOT in a + // theme group — they're painted with the grip color so they read as the + // grip-sense indicator instead of disappearing into the body color (PR #76). + // Everything else is dark accent — the trackpads, the system buttons // (view/menu/steam + the "…" quick-access), and the controls (sticks, face // buttons, dpad, shoulders, triggers, paddles). bodyColorMeshes: [ - 'top_shell', 'bottom_shell', 'left_gripsense', 'right_gripsense', + 'top_shell', 'bottom_shell', ], accentColorMeshes: [ 'misc1', 'misc2', 'touchpad', 'touch_point1', 'touch_point2', From a64cf23ec6b9f2ef0977692a8e5159fd1f740460 Mon Sep 17 00:00:00 2001 From: Pete Gordon Date: Sat, 20 Jun 2026 13:39:52 -0400 Subject: [PATCH 4/4] Grip sense: bar reacts to grip, glow rework, distinct definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grip-sense glow (on-top marker) and bars (handle strip meshes) are now two clearly separate, independently-toggled indicators: - Bars react to grip: sit at an inert rest grey and ease to the grip color (with a fixed emissive lift) while that side is gripped — so the bar itself reads activation (#74). Per-side materials are cloned so left/right light independently. Driven by setGripBarsVisible, independent of the glow. - Glow rework: the cylinder reads as a real glow again — a band texture that wraps the full circumference (consistent from any angle) and fills most of the length with soft fades at the ends, using normal blending so it stays visible over the light handles (additive vanished over near-white). The Glow Length slider now scales the lit length as expected. - gripMarkerYaw knob (per profile, degrees) angles each glow's bumper/trigger end toward the controller centerline; set to 18 for the Steam Controller. - Decoupled the bar's lit emissive from the Grip Glow Brightness slider (fixed GRIP_BAR_LIT_EMISSIVE) so brightness is glow-only, matching its label. Refreshed all stale "bars are static/cosmetic" docs + tooltips. Also: fix settings dropdowns rendering light text on a white popup on Windows (color-scheme: dark + explicit option colors). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/overlay/src/index.html | 14 +- packages/visualizer/src/controller-overlay.js | 137 ++++++++++++++---- .../visualizer/src/controller-profiles.js | 9 +- 3 files changed, 125 insertions(+), 35 deletions(-) diff --git a/apps/overlay/src/index.html b/apps/overlay/src/index.html index 78739a9..da5126e 100644 --- a/apps/overlay/src/index.html +++ b/apps/overlay/src/index.html @@ -124,6 +124,16 @@ .setting-row button:hover { background: rgba(255,255,255,0.15); } + /* The select's translucent dark bg + light text looks right on the panel, + but the OPEN option list is drawn by the OS — on Windows it came up + light-mode, leaving unselected options as light text on a white popup + (unreadable). `color-scheme: dark` renders the native popup dark; the + explicit option colors are a solid-background fallback across platforms. */ + .setting-row select { color-scheme: dark; } + .setting-row select option { + background: #26262c; + color: #eee; + } .setting-row input[type="color"] { width: 32px; @@ -1045,7 +1055,7 @@

Window Display

- +
@@ -1053,7 +1063,7 @@

Window Display

- +
diff --git a/packages/visualizer/src/controller-overlay.js b/packages/visualizer/src/controller-overlay.js index f0e2a39..3cad87e 100644 --- a/packages/visualizer/src/controller-overlay.js +++ b/packages/visualizer/src/controller-overlay.js @@ -28,6 +28,14 @@ const PRESS_GLOW = 1.5; // between render frames still shows a noticeably-sized circle, then keeps // expanding to full while held. const TRACKPAD_FILL_MIN = 0.45; +// Grip-sense bars sit at this inert dark grey at rest and brighten to the grip +// color while that side is gripped (issue #74 — activation should read on the +// bar itself, not just the handle glow). +const GRIP_BAR_REST_COLOR = 0x44444c; +// Emissive intensity of a grip-sense BAR while its side is gripped. Fixed on +// purpose — the Grip Glow Brightness slider drives the GLOW only, so the bar and +// the glow stay independently defined. +const GRIP_BAR_LIT_EMISSIVE = 0.6; const FLOAT_ZERO = new THREE.Vector3(); // shared read-only lerp target (parts seated) export class ControllerOverlay { @@ -583,8 +591,9 @@ export class ControllerOverlay { this._repositionGripBars(profile); // Honor the current "show grip bars" setting on this freshly-loaded model. this._applyGripBarsVisible(); - // Paint the bars with the grip color (they're not in a theme group, so this - // is the only thing that colors them). + // Set the bars to their inert rest grey + clone their materials (they're not + // in a theme group, so this is the only thing that colors them); setGripState + // then lights them to the grip color while a side is gripped. this._applyGripBarColor(); // Diagnostic: per-mapping check at load time so we can tell whether @@ -2013,32 +2022,58 @@ export class ControllerOverlay { } /** - * Highlight the capacitive grip sensors while held (digital on/off). The - * activation indicator is a billboard glow marker per grip, rendered ON TOP - * (depthTest off) so grip state reads at ANY angle; its peak opacity is the - * Grip Brightness setting. Gated by setGripVisible. The grip-sense BARS (the - * separate left/right side strips) are purely cosmetic and do NOT change on - * touch — keeping them static avoids the white→color flash and makes web and - * desktop render identically. + * Highlight the capacitive grip sensors while held (digital on/off). Two + * INDEPENDENT indicators, each with its own toggle: + * • GLOW — an on-top billboard/cylinder marker per grip, rendered with + * depthTest off so it reads at ANY angle. Peak opacity = the Grip Glow + * Brightness setting. Gated by setGripVisible (_gripEnabled). + * • BARS — the left/right side strip meshes. They ease from a rest grey to + * the grip color (with a fixed emissive lift) while that side is gripped, + * so the bar itself shows activation (issue #74). Gated by + * setGripBarsVisible (_gripBarsVisible); brightness does NOT affect them. * @param {{left:boolean, right:boolean}} grips */ setGripState(grips) { const map = PROFILES[this.controllerType]?.gripMeshes; if (!map || !grips) return; + const gripCol = this._gripColorC || (this._gripColorC = new THREE.Color()); + gripCol.setHex(this._gripColor); + const restCol = this._gripBarRestC || (this._gripBarRestC = new THREE.Color(GRIP_BAR_REST_COLOR)); + const black = this._gripBlackC || (this._gripBlackC = new THREE.Color(0x000000)); for (const side of ['left', 'right']) { - // On-top billboard glow marker — the activation cue. Brightness sets its - // peak opacity; lerps in/out. (The bars are left untouched on purpose.) - const glowOn = this._gripEnabled && !!grips[side]; + const gripped = !!grips[side]; + // On-top billboard glow marker — the handle activation cue. Brightness + // sets its peak opacity; lerps in/out. Gated by the glow toggle. const marker = this._gripMarkers?.[side]; if (marker) { marker.material.opacity = THREE.MathUtils.lerp( - marker.material.opacity, glowOn ? this._gripBrightness : 0, LERP_SPEED + marker.material.opacity, (this._gripEnabled && gripped) ? this._gripBrightness : 0, LERP_SPEED ); } + // Bar mesh — ease toward the grip color (lit) while that side is gripped, + // back to the inert rest grey when released, so the bar itself reads the + // activation (issue #74). Gated by the bars toggle, independent of glow. + const barActive = this._gripBarsVisible && gripped; + const bar = this._gripBarMesh(map[side]); + if (bar?.material?.color) { + bar.material.color.lerp(barActive ? gripCol : restCol, LERP_SPEED); + if ('emissive' in bar.material) { + bar.material.emissive.lerp(barActive ? gripCol : black, LERP_SPEED); + bar.material.emissiveIntensity = THREE.MathUtils.lerp( + bar.material.emissiveIntensity ?? 0, barActive ? GRIP_BAR_LIT_EMISSIVE : 0, LERP_SPEED + ); + } + } } } - /** Toggle the grip-sense glow (mesh emissive + on-top markers). */ + // The Mesh that carries a grip-bar's material (the GLB node may wrap it). + _gripBarMesh(name) { + const obj = this.meshes[name]; + return obj && (obj.isMesh ? obj : obj.children?.find((c) => c.isMesh)) || null; + } + + /** Toggle the grip-sense GLOW (the on-top marker only — not the bars). */ setGripVisible(enabled) { this._gripEnabled = !!enabled; } @@ -2075,29 +2110,38 @@ export class ControllerOverlay { this._gripBrightness = Math.max(0, Math.min(1, value)); } - /** Grip highlight color (markers + mesh glow); CSS hex e.g. '#3388ff'. */ + /** Grip highlight color (markers + the bar's lit state); CSS hex e.g. '#3388ff'. */ setGripColor(hexColor) { this._gripColor = new THREE.Color(hexColor).getHex(); if (this._gripMarkers) { for (const side of ['left', 'right']) this._gripMarkers[side]?.material.color.setHex(this._gripColor); } - this._applyGripBarColor(); + // The bars' rest grey is independent of the grip color; setGripState picks + // up the new _gripColor for the lit state on the next report. } - // Paint the grip-sense BAR meshes with the grip color. They're intentionally - // excluded from the body/accent theme groups (PR #76) — otherwise they'd take - // the body color and vanish into it — so their color is set here instead, and - // re-applied whenever the grip color changes. + // Reset the grip-sense BAR meshes to their inert rest appearance (grey, no + // emissive); setGripState lights them to the grip color while gripped. The + // bars are excluded from the body/accent theme groups (PR #76) — otherwise + // they'd take the body color and vanish into it — so they're driven here. + // Each bar's material is cloned once so left/right light up independently + // (GLB models often share one material across both meshes). _applyGripBarColor() { const map = PROFILES[this.controllerType]?.gripMeshes; if (!map) return; for (const side of ['left', 'right']) { - const obj = this.meshes[map[side]]; - const mesh = obj && (obj.isMesh ? obj : obj.children?.find((c) => c.isMesh)); - if (mesh?.material?.color) { - mesh.material.color.setHex(this._gripColor); - mesh.material.needsUpdate = true; + const mesh = this._gripBarMesh(map[side]); + if (!mesh?.material?.color) continue; + if (!mesh.material._gripCloned) { + mesh.material = mesh.material.clone(); + mesh.material._gripCloned = true; + } + mesh.material.color.setHex(GRIP_BAR_REST_COLOR); + if ('emissive' in mesh.material) { + mesh.material.emissive.setHex(0x000000); + mesh.material.emissiveIntensity = 0; } + mesh.material.needsUpdate = true; } } @@ -2177,6 +2221,27 @@ export class ControllerOverlay { const w = this._gripGlowWidth; const len = this._gripGlowLength; const isCircle = w === 1 && len === 1; + // Yaw (about the vertical Y axis) that swings each glow's front (+Z, the + // bumper/trigger end) toward the controller's centerline — see it in the Top + // view. Per-profile knob in degrees; sign flips per side below. + const yaw = THREE.MathUtils.degToRad(PROFILES[this.controllerType]?.gripMarkerYaw ?? 0); + // Band texture for the cylinder glow: uniform AROUND the tube (so the color + // wraps the whole circumference, not just one facing strip like the radial + // texture did), and a bright plateau filling MOST of the length with just a + // soft fade at the very ends. The Length slider scales the cylinder height, + // so this keeps the glow filling ~the whole cylinder (max Length ≈ the full + // handle) instead of only a short middle band. Maps v (0..1) → height. + if (!this._barGlowTexture) { + const c = document.createElement('canvas'); c.width = 4; c.height = 64; + const ctx = c.getContext('2d'); + const g = ctx.createLinearGradient(0, 0, 0, 64); + g.addColorStop(0.00, 'rgba(255,255,255,0)'); + g.addColorStop(0.10, 'rgba(255,255,255,0.95)'); + g.addColorStop(0.90, 'rgba(255,255,255,0.95)'); + g.addColorStop(1.00, 'rgba(255,255,255,0)'); + ctx.fillStyle = g; ctx.fillRect(0, 0, 4, 64); + this._barGlowTexture = new THREE.CanvasTexture(c); + } const markers = {}; for (const side of ['left', 'right']) { const spec = specs[side]; @@ -2196,20 +2261,26 @@ export class ControllerOverlay { const d = thin * 0.9; obj.scale.set(d, d, 1); } else { - // NORMAL blending (not additive): additive adds the glow to whatever's - // behind it, so it washed out over the bright white body but popped over - // the dark trackpads. Normal blending shows the grip color consistently - // regardless of what's behind/in front (fully solid at full brightness). + // Read as a soft GLOW that's still visible over the light handles. The + // band texture wraps the FULL circumference (color all the way around the + // tube, consistent from any angle) and fills most of the length, fading + // softly only at the very ends. NORMAL blending keeps + // it visible on any background. (Pure additive looked like a real glow + // but vanished over the bright body — additive adds to the destination, + // and there's no headroom over near-white, so it only showed over dark + // areas. A radial texture only lit one facing strip of the tube.) const mat = new THREE.MeshBasicMaterial({ + map: this._barGlowTexture, color: this._gripColor, transparent: true, opacity: 0, blending: THREE.NormalBlending, depthTest: false, depthWrite: false, }); // Cylinder: radius ≤ half the handle's thin cross-section; height ≤ the // handle's Z length. Built along Y, rotated so its axis runs - // front-to-back (model Z, the handle's long axis). + // front-to-back (model Z, the handle's long axis). Open-ended (no cap + // disks) so the band fades cleanly at the ends with no flat cap showing. const radius = (thin * 0.45) * (w / 5); const height = (spec.dimZ * 1.35) * (len / 5); // ~50% longer than before - const geo = new THREE.CylinderGeometry(radius, radius, height, 20); + const geo = new THREE.CylinderGeometry(radius, radius, height, 20, 1, true); // Lay the cylinder front-to-back (axis → Z), then tilt it so the back // (−Z) end rises ~1/3 of its length, angling it inline with the handle. // asin(2/3): with a centre pivot the end rises (height/2)·(2/3) = height/3. @@ -2221,6 +2292,10 @@ export class ControllerOverlay { obj.renderOrder = 10; // draw after the model so it sits on top obj.position.copy(spec.pos); obj.position.y += yLift; + // Angle the bumper/trigger end inward toward center. Sign mirrors per + // side: left handle (−X) yaws −, right handle (+X) yaws +. No-op on the + // circle sprite (it billboards). + obj.rotation.y = (spec.pos.x < 0 ? -1 : 1) * yaw; this.bodyGroup.add(obj); markers[side] = obj; } diff --git a/packages/visualizer/src/controller-profiles.js b/packages/visualizer/src/controller-profiles.js index 571a918..13361c5 100644 --- a/packages/visualizer/src/controller-profiles.js +++ b/packages/visualizer/src/controller-profiles.js @@ -416,10 +416,15 @@ export const PROFILES = { // sit over the controller handles where the hands grip, instead of bunched // near the center (PR #76 feedback). gripMarkerSideOffset: 0.14, + // Yaw each glow about the vertical axis (degrees) so its front (bumper/ + // trigger) end angles toward the controller's centerline — best seen in the + // Top view. Sign is applied per side in code (left/right mirror). + gripMarkerYaw: 18, // Two-tone (ceski/Larf SC2 look, issue #61): light body = shells only. // The grip-sense bars (left/right_gripsense) are deliberately NOT in a - // theme group — they're painted with the grip color so they read as the - // grip-sense indicator instead of disappearing into the body color (PR #76). + // theme group — they're driven on their own (a rest grey that lights to the + // grip color while that side is gripped) so they read as the grip-sense + // indicator instead of disappearing into the body color (PR #76, #74). // Everything else is dark accent — the trackpads, the system buttons // (view/menu/steam + the "…" quick-access), and the controls (sticks, face // buttons, dpad, shoulders, triggers, paddles).