Label the phone keys with runtime-drawn legends
The keypad keys had no glyphs. Stamp each key with a legend rendered into a canvas (THREE.CanvasTexture) on a thin unlit plane just proud of the key face, so it reads crisply and rides the key-press dip — no texture assets needed. The canvas aspect matches the key face and the font auto-fits, so a word sits as neatly as a digit. Digits/*/# show their glyph; call/end/nav read do/don't/?. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+37
-6
@@ -44,6 +44,29 @@ const sfx = {
|
|||||||
|
|
||||||
// ---- scene --------------------------------------------------------------------
|
// ---- scene --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw a key legend into a canvas and hand it back as a texture. The canvas aspect
|
||||||
|
// matches the key face (w:h) so glyphs aren't stretched when mapped onto the plane;
|
||||||
|
// the font shrinks to fit so words ("don't") sit as comfortably as single digits.
|
||||||
|
function makeKeyLabel(text: string, w: number, h: number): THREE.Texture {
|
||||||
|
const H = 220
|
||||||
|
const W = Math.max(64, Math.round(H * (w / h)))
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = W; canvas.height = H
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
ctx.fillStyle = '#0e140a'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
let size = Math.round(H * 0.66)
|
||||||
|
const setFont = () => { ctx.font = `700 ${size}px "IBM Plex Mono", ui-monospace, monospace` }
|
||||||
|
setFont()
|
||||||
|
while (ctx.measureText(text).width > W * 0.84 && size > 12) { size -= 4; setFont() }
|
||||||
|
ctx.fillText(text, W / 2, H / 2 + size * 0.04)
|
||||||
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace
|
||||||
|
texture.anisotropy = 4
|
||||||
|
return texture
|
||||||
|
}
|
||||||
|
|
||||||
type Built = { group: THREE.Group; hinge: THREE.Group; keys: THREE.Mesh[] }
|
type Built = { group: THREE.Group; hinge: THREE.Group; keys: THREE.Mesh[] }
|
||||||
|
|
||||||
export function buildPhone(): Built {
|
export function buildPhone(): Built {
|
||||||
@@ -81,21 +104,29 @@ export function buildPhone(): Built {
|
|||||||
lid.add(glass)
|
lid.add(glass)
|
||||||
|
|
||||||
// Chunky protruding keys (LOCAL to the keypad, whose body spans local y [-0.46,0.46]).
|
// Chunky protruding keys (LOCAL to the keypad, whose body spans local y [-0.46,0.46]).
|
||||||
const key = (w: number, h: number, x: number, y: number, id: string) => {
|
// `label` is stamped as a thin, unlit legend plane just proud of the key's +z face,
|
||||||
|
// so it reads crisply and rides along with the key-press dip.
|
||||||
|
const key = (w: number, h: number, x: number, y: number, id: string, label?: string) => {
|
||||||
const k = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.09), keyMat)
|
const k = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.09), keyMat)
|
||||||
k.position.set(x, y, 0.10)
|
k.position.set(x, y, 0.10)
|
||||||
k.userData = { key: id, baseZ: 0.10 }
|
k.userData = { key: id, baseZ: 0.10 }
|
||||||
k.castShadow = true
|
k.castShadow = true
|
||||||
keypad.add(k)
|
keypad.add(k)
|
||||||
keys.push(k)
|
keys.push(k)
|
||||||
|
if (label) {
|
||||||
|
const legend = new THREE.Mesh(new THREE.PlaneGeometry(w * 0.82, h * 0.82),
|
||||||
|
new THREE.MeshBasicMaterial({ map: makeKeyLabel(label, w, h), transparent: true }))
|
||||||
|
legend.position.z = 0.047 // half-depth is 0.045; sit just above to avoid z-fighting
|
||||||
|
k.add(legend)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
key(0.16, 0.10, -0.20, 0.40, 'call') // left soft key
|
key(0.16, 0.10, -0.20, 0.40, 'call', 'do') // left soft key
|
||||||
key(0.16, 0.10, 0.20, 0.40, 'end') // right soft key
|
key(0.16, 0.10, 0.20, 0.40, 'end', "don't") // right soft key
|
||||||
key(0.20, 0.16, 0, 0.22, 'nav') // nav pad
|
key(0.20, 0.16, 0, 0.22, 'nav', '?') // nav pad
|
||||||
const rows = [0.04, -0.10, -0.24, -0.38]
|
const rows = [0.04, -0.10, -0.24, -0.38]
|
||||||
const cols = [-0.20, 0, 0.20]
|
const cols = [-0.20, 0, 0.20]
|
||||||
const digits = [['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['*', '0', '#']]
|
const digits = [['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['*', '0', '#']]
|
||||||
rows.forEach((y, r) => cols.forEach((x, c) => key(0.15, 0.10, x, y, digits[r][c])))
|
rows.forEach((y, r) => cols.forEach((x, c) => key(0.15, 0.10, x, y, digits[r][c], digits[r][c])))
|
||||||
|
|
||||||
// Hinge barrel: parented to the hinge group at its local origin, so it sits
|
// Hinge barrel: parented to the hinge group at its local origin, so it sits
|
||||||
// EXACTLY on the rotation axis (the pivot passes through the cylinder centre).
|
// EXACTLY on the rotation axis (the pivot passes through the cylinder centre).
|
||||||
@@ -221,7 +252,7 @@ function PhoneDevice({ open, onKey }: { open: boolean; onKey: (k: string) => voi
|
|||||||
renderer.domElement.removeEventListener('pointerdown', onPointer)
|
renderer.domElement.removeEventListener('pointerdown', onPointer)
|
||||||
renderer.dispose()
|
renderer.dispose()
|
||||||
renderer.domElement.remove()
|
renderer.domElement.remove()
|
||||||
scene.traverse(obj => { if (obj instanceof THREE.Mesh) { obj.geometry.dispose(); (obj.material as THREE.Material).dispose() } })
|
scene.traverse(obj => { if (obj instanceof THREE.Mesh) { obj.geometry.dispose(); const material = obj.material as THREE.Material & { map?: THREE.Texture | null }; material.map?.dispose(); material.dispose() } })
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user