From 2becc5b38043e4926ec532d8bffbc8c8406763ca Mon Sep 17 00:00:00 2001 From: LegionaryLeader Date: Sat, 11 Jul 2026 01:45:00 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(keys3d):=20Better=20Key=20Sizes=20?= =?UTF-8?q?=E2=80=94=20deeper,=20edge-shaded,=20trapezoid=20piano=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape the Keys Highway 3D piano so key presses read clearly, as the default look (no toggle): - Taller/longer key bodies for more visible key-depress travel. - Baked vertex-color edge shading on every key wall (_shadeKeyEdges). - Thin real-piano side gap on every key (render inset only; positions and lane widths untouched, so falling notes still line up). - Trapezoid sharps: top-edge X taper (_taperKeyX) + front-face slant (_slantFrontFace). Sharp back edge anchored to the hit-line so sharps no longer dive into the highway and their length is independent of the natural-key length. Bump plugin 0.2.1 -> 0.2.2. Co-Authored-By: Claude Opus 4.8 Signed-off-by: LegionaryLeader --- plugins/keys_highway_3d/plugin.json | 2 +- plugins/keys_highway_3d/screen.js | 104 ++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/plugins/keys_highway_3d/plugin.json b/plugins/keys_highway_3d/plugin.json index 1a9fe303..b5f2fd61 100644 --- a/plugins/keys_highway_3d/plugin.json +++ b/plugins/keys_highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "keys_highway_3d", "name": "Keys Highway 3D", - "version": "0.2.1", + "version": "0.2.2", "description": "RS+-style 3D falling-note piano highway fed by the Sloppak Notation Format, with Web MIDI input scoring.", "type": "visualization", "bundled": true, diff --git a/plugins/keys_highway_3d/screen.js b/plugins/keys_highway_3d/screen.js index 62779949..9ac6d3c2 100644 --- a/plugins/keys_highway_3d/screen.js +++ b/plugins/keys_highway_3d/screen.js @@ -1854,8 +1854,15 @@ return { layout, whiteCount: whiteIndex }; } - const WHITE_W = 12 * K, WHITE_L = 46 * K, WHITE_H = 5 * K; - const BLACK_W = 6.4 * K, BLACK_L = 28 * K, BLACK_H = 6.5 * K; + // Key WIDTHS are fixed — the horizontal lane tiling (naturals/sharps) + // is derived from them, so they must not change. + const WHITE_W = 12 * K, BLACK_W = 6.4 * K; + // Key LENGTH (depth toward the player) + HEIGHT: taller/longer bodies so + // the key-depress tip has room to read and the edge-shaded side walls + // (see buildKeyboardAndHighway) give each key a defined border. Every + // consumer reads these at build-/frame-time. + const WHITE_L = 49.5 * K, WHITE_H = 9.5 * K; + const BLACK_L = 33 * K, BLACK_H = 13 * K; const HIGHWAY_LEN = 1150 * K; // longer runway → ~8.8s of lookahead visible // 'flat' piano-shaped-lane geometry (see laneSpanFlat). Zero-overlap tiling: // white lanes are trimmed by FLAT_SHARP_HALF where they meet a sharp and @@ -2516,6 +2523,67 @@ return g; } + // Bake a vertical shade ramp into a key geometry's vertex colors so the + // (taller) side walls darken toward the base while the top face stays + // full-bright. This gives every key a defined edge and turns the seam + // between neighbours into a soft shadow line — the "smidge of darkness" + // that makes adjacent keys easy to tell apart. The material multiplies + // its base color by these (vertexColors), same trick the note gems use. + function _shadeKeyEdges(geo, botShade) { + geo.computeBoundingBox(); + const y0 = geo.boundingBox.min.y, yr = (geo.boundingBox.max.y - y0) || 1; + const pos = geo.attributes.position; + const cols = new Float32Array(pos.count * 3); + for (let i = 0; i < pos.count; i++) { + const t = (pos.getY(i) - y0) / yr; // 0 base .. 1 top + const v = botShade + (1 - botShade) * (t * t * (3 - 2 * t)); + cols[i * 3] = cols[i * 3 + 1] = cols[i * 3 + 2] = v; + } + geo.setAttribute('color', new T.BufferAttribute(cols, 3)); + } + + // Contour a stood-up key geometry (height == +Y) by tapering its + // horizontal (X) footprint toward the TOP: the base keeps full width and + // the top narrows to `topScale`, giving a trapezoid/chiseled-wedge profile + // that reads from the camera's top-down angle (used for the sharp keys). + // Left/right edges only — depth (Z) toward the highway is never + // touched. Cheap: a one-time vertex pass at build; recomputes normals so + // the sloped walls light correctly. + function _taperKeyX(geo, topScale) { + geo.computeBoundingBox(); + const y0 = geo.boundingBox.min.y, yr = (geo.boundingBox.max.y - y0) || 1; + const pos = geo.attributes.position; + for (let i = 0; i < pos.count; i++) { + const t = (pos.getY(i) - y0) / yr; // 0 base .. 1 top + const s = 1 + (topScale - 1) * t; // full at base, topScale at top + pos.setX(i, pos.getX(i) * s); + } + pos.needsUpdate = true; + geo.computeVertexNormals(); + } + + // Slant a stood-up key's FRONT face (the edge toward the player) back by + // `degrees` from vertical, so the side profile reads as a trapezoid/wedge: + // the front-bottom edge recedes while the top edge stays, tilting a 90° + // front to (90 - degrees)°. Only the front half is sheared (weighted by + // depth) so the BACK edge that meets the highway is never moved. One-time + // vertex pass; recomputes normals. Sharp keys only. + function _slantFrontFace(geo, degrees) { + geo.computeBoundingBox(); + const bb = geo.boundingBox; + const y0 = bb.min.y, yr = (bb.max.y - y0) || 1; + const zMid = (bb.min.z + bb.max.z) / 2, zr = (bb.max.z - zMid) || 1; + const amt = (bb.max.y - bb.min.y) * Math.tan(degrees * Math.PI / 180); + const pos = geo.attributes.position; + for (let i = 0; i < pos.count; i++) { + const t = (pos.getY(i) - y0) / yr; // 0 base .. 1 top + const fw = Math.max(0, (pos.getZ(i) - zMid) / zr); // 0 back-half .. 1 front + pos.setZ(i, pos.getZ(i) - (1 - t) * fw * amt); // recede the front-base + } + pos.needsUpdate = true; + geo.computeVertexNormals(); + } + function _noteGeometry(w, len) { const key = w.toFixed(4) + '|' + len.toFixed(2); let geo = _noteGeoCache.get(key); @@ -2968,10 +3036,27 @@ // is translated to the key's BACK edge so the live MIDI key- // depress animation can pivot there (rotation.x > 0 tips the // front of the key down, like a real piano action). - const whiteGeo = _roundedBoxGeo(WHITE_W * 0.94, WHITE_H, WHITE_L, WHITE_W * 0.05, WHITE_H * 0.1); + // Left/right inset so a thin real-piano gap opens between neighbours and + // each edge reads clearly. Naturals get the wider inset (their long walls + // sit flush, so this is what actually separates them); sharps keep a + // slimmer one. Inset is left/right ONLY — the back edge that meets the + // highway never moves, and positions + lane widths are untouched so + // falling notes still line up with their key. + const whiteGap = 1.0 * K; // per side + const blackGap = 0.7 * K; // per side + const whiteBodyW = WHITE_W - 2 * whiteGap; + const blackBodyW = BLACK_W - 2 * blackGap; + const whiteGeo = _roundedBoxGeo(whiteBodyW, WHITE_H, WHITE_L, WHITE_W * 0.05, WHITE_H * 0.1); whiteGeo.translate(0, 0, WHITE_L / 2); - const blackGeo = _roundedBoxGeo(BLACK_W, BLACK_H, BLACK_L, BLACK_W * 0.08, BLACK_H * 0.06); + const blackGeo = _roundedBoxGeo(blackBodyW, BLACK_H, BLACK_L, BLACK_W * 0.08, BLACK_H * 0.06); blackGeo.translate(0, 0, BLACK_L / 2); + // Contour the sharps into a trapezoid: pull the TOP edges inward + // (left/right only) while the base stays full width, so each black + // key reads as a chiseled wedge from the camera's top-down angle. + _taperKeyX(blackGeo, 0.84); + _slantFrontFace(blackGeo, 10); // front face 90° -> ~80° (trapezoid side profile) + _shadeKeyEdges(whiteGeo, 0.72); // white walls darken to 72% at the base + _shadeKeyEdges(blackGeo, 0.60); // blacks a touch stronger for form const whiteGlyphGeo = new T.PlaneGeometry(WHITE_W * 0.72, WHITE_W * 0.72); const blackGlyphGeo = new T.PlaneGeometry(BLACK_W * 0.96, BLACK_W * 0.96); for (const [midi, entry] of layout) { @@ -2979,6 +3064,9 @@ const inRange = midi >= range.activeLow && midi <= range.activeHigh; const material = new T.MeshStandardMaterial({ color: black ? 0x070708 : 0xe8e8ee, + // Edge shading is baked into the geometry's vertex colors + // (_shadeKeyEdges); the material multiplies its base color by them. + vertexColors: true, // Pitch-class color preset on emissive but OFF at rest — the key // is neutral until a note approaches, when updateScene ramps the // intensity up by proximity. @@ -2991,12 +3079,14 @@ envMapIntensity: black ? 1.3 : 0.3, }); const mesh = new T.Mesh(black ? blackGeo : whiteGeo, material); - // Positions place the geometry where the old centred boxes - // sat: the mesh origin is the back-edge centre. + // The mesh origin is the back-edge centre. Both key types anchor + // their BACK edge to the hit-line (-WHITE_L/2), so the sharps sit + // flush with the naturals instead of diving backward into the + // highway, and their visible length is independent of WHITE_L. mesh.position.set( keyX(entry, whiteCount), black ? BLACK_H / 2 + WHITE_H * 0.1 : WHITE_H / 2, - black ? (WHITE_L - BLACK_L) / 2 - WHITE_L / 2 - BLACK_L / 2 - 4 * K : -WHITE_L / 2, + -WHITE_L / 2, ); mesh.userData.midi = midi; // Wrong-note flash restore state. From bfd962abe68a381b89e165c4e93435e395b78ce5 Mon Sep 17 00:00:00 2001 From: LegionaryLeader Date: Sat, 11 Jul 2026 02:44:43 -0400 Subject: [PATCH 2/3] feat(keys3d): tune floating-layout note planes for the reshaped keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The taller, edge-shaded keys pushed the floating-layout sharp note gems far above their keys, and on the low-down camera the natural gems sat a touch high as they came in. Decouple the floating note planes from the (now tall) black-key height: - Naturals: drop the entry plane 1K so they read cleanly on the low cam. - Sharps: sit +2K over the natural base so they still read as raised, independent of BLACK_H (no longer pinned to the tall black key). Floating layout only — flat/realistic keep their shared WHITE_H plane. No key geometry, positions, lane widths, or plugin version change. Signed-off-by: LegionaryLeader Co-Authored-By: Claude Opus 4.8 --- plugins/keys_highway_3d/screen.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/keys_highway_3d/screen.js b/plugins/keys_highway_3d/screen.js index 9ac6d3c2..a8d171d1 100644 --- a/plugins/keys_highway_3d/screen.js +++ b/plugins/keys_highway_3d/screen.js @@ -3174,7 +3174,11 @@ } else { w = (entry.black ? BLACK_W : WHITE_W * 0.94) * 0.9; x = keyX(entry, whiteCount); - y = (entry.black ? BLACK_H + WHITE_H : WHITE_H) + NOTE_H / 2 + 0.5 * K; + // Floating note planes (independent of the tall black-key height): + // naturals dropped 1K so they read better on the low-down cam as + // they come in; sharps sit +2K over the base so they stay raised + // (nets the same height they had before the natural was lowered). + y = WHITE_H + NOTE_H / 2 + 0.5 * K + (entry.black ? 2 * K : -1 * K); } // Clone per note so each can glow independently while being consumed. const mesh = new T.Mesh(_noteGeometry(w, len), _noteMaterial(note.midi, note.hand).clone()); From 70427ecfe3d6613c34103089e9357fe78e6bf555 Mon Sep 17 00:00:00 2001 From: LegionaryLeader Date: Sat, 11 Jul 2026 03:10:01 -0400 Subject: [PATCH 3/3] docs(changelog): note the keys3d realistic-key-sizes changes Add an [Unreleased] > Changed entry for the reshaped Keys Highway 3D keys and the floating-layout note-plane tuning, per the per-PR changelog guideline. Signed-off-by: LegionaryLeader Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de41338..8c76d79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- **Keys Highway 3D — reshaped piano keys so key presses read clearly (default look, no + toggle).** Taller/deeper key bodies with baked vertex-color edge shading and a thin + real-piano side gap give every key a defined border and make adjacent keys easy to tell + apart; sharps are contoured into a top-tapered, front-slanted trapezoid and anchored to + the hit-line so they no longer dive backward into the highway. Floating-layout falling + notes are re-planed to suit: naturals sit a touch lower for the low-down camera and + sharps ride just above them, decoupled from the black-key height. Key positions, lane + widths, and note X-alignment are unchanged, and the `flat`/`realistic` layouts are + untouched. (keys_highway_3d `0.2.1` → `0.2.2`) + ### Fixed - **The packaged desktop app could not start (`ModuleNotFoundError: No module named 'appstate'`).** feedback-desktop's `scripts/bundle-slopsmith.sh` copies a *hardcoded