diff --git a/src/phone.tsx b/src/phone.tsx index 0bcf342..0793aa4 100644 --- a/src/phone.tsx +++ b/src/phone.tsx @@ -44,6 +44,29 @@ const sfx = { // ---- 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[] } export function buildPhone(): Built { @@ -81,21 +104,29 @@ export function buildPhone(): Built { lid.add(glass) // 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) k.position.set(x, y, 0.10) k.userData = { key: id, baseZ: 0.10 } k.castShadow = true keypad.add(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, 'end') // right soft key - key(0.20, 0.16, 0, 0.22, 'nav') // nav pad + key(0.16, 0.10, -0.20, 0.40, 'call', 'do') // left 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 const rows = [0.04, -0.10, -0.24, -0.38] const cols = [-0.20, 0, 0.20] 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 // 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.dispose() 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() } }) } }, [])