ak0mh commited on
Commit
bb3c028
·
1 Parent(s): ee043ed

Replace emoji icons with SVG graphics in the application menu

Browse files

Update menu icon definitions in script.js to use SVG elements instead of emoji characters for a more professional and scalable user interface.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 86baadf9-2b37-4547-9f33-81971013eb9f
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4c4be94b-6759-4e23-a55c-aa483a19366e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/99f3fde9-2694-474e-a80a-45981704d855/86baadf9-2b37-4547-9f33-81971013eb9f/WbnIKiO
Replit-Helium-Checkpoint-Created: true

Files changed (1) hide show
  1. keypad-mobile/js/script.js +77 -102
keypad-mobile/js/script.js CHANGED
@@ -5,18 +5,18 @@ const raycaster = new THREE.Raycaster();
5
  const mouse = new THREE.Vector2();
6
 
7
  // --- EMULATOR STATE ---
8
- let currentScreen = "HOME"; // HOME, MENU, APP_MESSAGES, APP_CONTACTS, APP_CAMERA
9
- let selectedIndex = 0; // For grid navigation
10
  const menuIcons = [
11
- { name: "Messages", icon: "✉️" },
12
- { name: "Contacts", icon: "👤" },
13
- { name: "Camera", icon: "📷" },
14
- { name: "Settings", icon: "⚙️" },
15
- { name: "Games", icon: "🎮" },
16
- { name: "Tools", icon: "🛠️" }
17
  ];
18
 
19
- // Canvas for Screen (KeyNX QVGA 240x320 feel)
20
  const screenCanvas = document.createElement('canvas');
21
  screenCanvas.width = 240; screenCanvas.height = 320;
22
  const ctx = screenCanvas.getContext('2d');
@@ -57,31 +57,47 @@ function drawHomeScreen() {
57
  ctx.fillText('Menu', 120, 310);
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
60
  function drawMenu() {
61
  ctx.fillStyle = '#000';
62
  ctx.fillRect(0, 25, 240, 295);
63
 
64
  const cols = 3;
65
  const padding = 20;
66
- const size = 60;
67
 
68
  menuIcons.forEach((item, i) => {
69
  const r = Math.floor(i / cols);
70
  const c = i % cols;
71
- const x = padding + c * (size + 15) + size/2;
72
- const y = 60 + r * (size + 30);
73
 
74
  if (i === selectedIndex) {
75
  ctx.strokeStyle = '#7c3aed';
76
- ctx.lineWidth = 3;
77
- ctx.strokeRect(x - size/2 - 5, y - size/2 - 5, size + 10, size + 10);
78
  }
79
 
80
- ctx.font = '30px Arial';
81
- ctx.fillText(item.icon, x, y);
82
- ctx.font = '10px Arial';
 
 
 
 
 
83
  ctx.fillStyle = (i === selectedIndex) ? '#7c3aed' : 'white';
84
- ctx.fillText(item.name, x, y + size/2 + 10);
85
  });
86
  }
87
 
@@ -98,18 +114,35 @@ function updateDisplay() {
98
  ctx.fillStyle = 'white';
99
  ctx.font = '16px Arial';
100
  ctx.textAlign = 'center';
101
- ctx.fillText(currentScreen.replace("APP_", ""), 120, 160);
102
- ctx.font = '12px Arial';
103
- ctx.fillText('Coming Soon...', 120, 190);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
 
106
  if (screenMesh && screenMesh.material.map) screenMesh.material.map.needsUpdate = true;
107
  }
108
 
109
- // --- NAVIGATION LOGIC ---
110
  function navigate(key) {
111
  if (currentScreen === "HOME") {
112
- if (key === "FLASHLIGHT" || key === "-") {
113
  currentScreen = "MENU";
114
  selectedIndex = 0;
115
  }
@@ -136,6 +169,8 @@ function createRoundedRectShape(w, h, r) {
136
  shape.lineTo(w/2, h/2 - r);
137
  shape.quadraticCurveTo(w/2, h/2, w/2 - r, h/2);
138
  shape.lineTo(-w/2 + r, h/2);
 
 
139
  shape.quadraticCurveTo(-w/2, h/2, -w/2, h/2 - r);
140
  shape.lineTo(-w/2, -h/2 + r);
141
  shape.quadraticCurveTo(-w/2, -h/2, -w/2 + r, -h/2);
@@ -144,35 +179,23 @@ function createRoundedRectShape(w, h, r) {
144
 
145
  function createPill(w, h, label, color, x, y, isCircle = false, isDpad = false, isNav = false) {
146
  const group = new THREE.Group();
147
- const mat = new THREE.MeshStandardMaterial({
148
- color: color,
149
- roughness: 0.4,
150
- metalness: 0.1,
151
- });
152
  let geom;
153
- const depth = 0.08;
154
- const bevelSize = 0.01;
155
- const bevelThickness = 0.01;
156
  if (isCircle) {
157
- geom = new THREE.CylinderGeometry(w/2, w/2, depth, 32);
158
  } else {
159
  const r = Math.min(w, h) * 0.25;
160
- const shape = createRoundedRectShape(w, h, r);
161
- geom = new THREE.ExtrudeGeometry(shape, {
162
- depth: depth, bevelEnabled: true, bevelSegments: 2,
163
- steps: 1, bevelSize: bevelSize, bevelThickness: bevelThickness
164
- });
165
  }
166
  const mesh = new THREE.Mesh(geom, mat);
167
  if (isCircle) mesh.rotation.x = Math.PI/2;
168
  const borderMat = new THREE.MeshBasicMaterial({ color: 0xcccccc });
169
  let borderGeom;
170
  if (isCircle) {
171
- borderGeom = new THREE.CylinderGeometry(w/2 + 0.005, w/2 + 0.005, depth - 0.01, 32);
172
  } else {
173
  const br = Math.min(w + 0.01, h + 0.01) * 0.25;
174
- const bShape = createRoundedRectShape(w + 0.01, h + 0.01, br);
175
- borderGeom = new THREE.ExtrudeGeometry(bShape, { depth: depth - 0.01, bevelEnabled: false });
176
  }
177
  const borderMesh = new THREE.Mesh(borderGeom, borderMat);
178
  if (isCircle) borderMesh.rotation.x = Math.PI/2;
@@ -184,21 +207,18 @@ function createPill(w, h, label, color, x, y, isCircle = false, isDpad = false,
184
  if (tctx) {
185
  tctx.textAlign = 'center';
186
  if (label === 'FLASHLIGHT') {
187
- tctx.fillStyle = '#333'; tctx.font = 'bold 80px Arial';
188
- tctx.fillText('🔦', 64, 90);
189
  } else {
190
- tctx.font = (isNav) ? 'bold 55px Arial' : 'bold 50px Arial';
191
- tctx.fillStyle = (label === '📞') ? '#22c55e' : (label === '❌') ? '#ef4444' : '#333';
192
  tctx.fillText(label, 64, 75);
193
- const subtext = {'2': 'ABC', '3': 'DEF', '4': 'GHI', '5': 'JKL', '6': 'MNO', '7': 'PQRS', '8': 'TUV', '9': 'WXYZ'};
194
- if (subtext[label]) {
195
- tctx.font = '28px Arial'; tctx.fillText(subtext[label], 64, 100);
196
- }
197
  }
198
  }
199
  const tMat = new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(tCan), transparent: true });
200
  const tMesh = new THREE.Mesh(new THREE.PlaneGeometry(w*0.8, h*0.8), tMat);
201
- tMesh.position.z = depth + bevelThickness + 0.01;
202
  group.add(mesh); group.add(tMesh);
203
  group.position.set(x, y, 0.05);
204
  group.userData = { isBtn: true, val: label };
@@ -210,11 +230,7 @@ function init() {
210
  camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
211
  camera.position.z = 15;
212
  const container = document.getElementById('canvas-container');
213
- try {
214
- renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
215
- } catch (e) {
216
- renderer = new THREE.WebGLRenderer({ alpha: true });
217
- }
218
  renderer.setSize(window.innerWidth, window.innerHeight);
219
  renderer.setPixelRatio(window.devicePixelRatio || 1);
220
  container.appendChild(renderer.domElement);
@@ -234,24 +250,15 @@ function init() {
234
  phone.add(bezel);
235
 
236
  updateDisplay();
237
- screenMesh = new THREE.Mesh(
238
- new THREE.PlaneGeometry(4.1, 4.3),
239
- new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(screenCanvas) })
240
- );
241
  screenMesh.position.set(0, 2.05, 0.46);
242
  phone.add(screenMesh);
243
 
244
  const brandCanvas = document.createElement('canvas');
245
  brandCanvas.width = 512; brandCanvas.height = 128;
246
  const bctx = brandCanvas.getContext('2d');
247
- if (bctx) {
248
- bctx.fillStyle = '#444'; bctx.font = 'bold 50px Arial'; bctx.textAlign = 'center';
249
- bctx.fillText('KeyNX', 256, 80);
250
- }
251
- const brand = new THREE.Mesh(
252
- new THREE.PlaneGeometry(2.0, 0.5),
253
- new THREE.MeshBasicMaterial({map: new THREE.CanvasTexture(brandCanvas), transparent: true})
254
- );
255
  brand.position.set(0, -0.4, 0.46);
256
  phone.add(brand);
257
 
@@ -260,7 +267,6 @@ function init() {
260
  phone.add(buttonsContainer);
261
 
262
  const btnColor = 0xe0e0e0;
263
-
264
  const dpadY = 1.0; const baseCenterSize = 0.85;
265
  buttonsContainer.add(createPill(baseCenterSize, baseCenterSize, "FLASHLIGHT", 0xffffff, 0, dpadY, false, true));
266
  const slimSize = 0.08; const hubSize = baseCenterSize + slimSize * 2;
@@ -278,15 +284,12 @@ function init() {
278
  const keys = [['1','2','3'],['4','5','6'],['7','8','9'],['*','0','#']];
279
  const startY = -0.1; const spacingX = 1.35; const spacingY = 0.55;
280
  keys.forEach((row, ri) => {
281
- row.forEach((key, ci) => {
282
- buttonsContainer.add(createPill(1.15, 0.4, key, btnColor, -spacingX + ci*spacingX, startY - ri*spacingY));
283
- });
284
  });
285
 
286
  const backGroup = new THREE.Group();
287
  backGroup.position.z = -0.41;
288
  phone.add(backGroup);
289
-
290
  const camHousingGeom = new THREE.ExtrudeGeometry(createRoundedRectShape(1.8, 1.8, 0.4), { depth: 0.1, bevelEnabled: false });
291
  const camHousing = new THREE.Mesh(camHousingGeom, bezelMat);
292
  camHousing.position.set(0.8, 2.5, 0);
@@ -296,9 +299,7 @@ function init() {
296
  const camCanvas = document.createElement('canvas');
297
  camCanvas.width = 512; camCanvas.height = 512;
298
  const cctx = camCanvas.getContext('2d');
299
- cctx.fillStyle = '#0a0a0a';
300
- cctx.fillRect(0, 0, 512, 512);
301
-
302
  const lensY = 150;
303
  cctx.strokeStyle = '#333'; cctx.lineWidth = 12;
304
  cctx.beginPath(); cctx.arc(160, lensY, 80, 0, Math.PI*2); cctx.stroke();
@@ -306,33 +307,12 @@ function init() {
306
  cctx.fillStyle = '#111';
307
  cctx.beginPath(); cctx.arc(160, lensY, 70, 0, Math.PI*2); cctx.fill();
308
  cctx.beginPath(); cctx.arc(352, lensY, 70, 0, Math.PI*2); cctx.fill();
309
- cctx.fillStyle = 'rgba(255,255,255,0.1)';
310
- cctx.beginPath(); cctx.arc(140, lensY-20, 20, 0, Math.PI*2); cctx.fill();
311
- cctx.beginPath(); cctx.arc(332, lensY-20, 20, 0, Math.PI*2); cctx.fill();
312
-
313
  cctx.fillStyle = 'white'; cctx.font = 'bold 64px Arial'; cctx.textAlign = 'center';
314
  cctx.fillText('VGA CAM', 256, 420);
315
-
316
- const camTex = new THREE.CanvasTexture(camCanvas);
317
- camTex.anisotropy = 16;
318
- const camLabel = new THREE.Mesh(
319
- new THREE.PlaneGeometry(1.6, 1.6),
320
- new THREE.MeshBasicMaterial({ map: camTex })
321
- );
322
  camLabel.position.set(0.8, 2.5, -0.11);
323
  camLabel.rotation.y = Math.PI;
324
  backGroup.add(camLabel);
325
-
326
- const grillLines = 5;
327
- for(let i=0; i<grillLines; i++) {
328
- const line = new THREE.Mesh(
329
- new THREE.PlaneGeometry(0.8, 0.05),
330
- new THREE.MeshBasicMaterial({ color: 0x333333 })
331
- );
332
- line.position.set(-0.8, 2.8 + i*0.15, -0.01);
333
- line.rotation.y = Math.PI;
334
- backGroup.add(line);
335
- }
336
  animate();
337
  }
338
 
@@ -346,11 +326,7 @@ function handleInput(x, y, isDown) {
346
  let obj = hits[0].object;
347
  while(obj.parent && !obj.userData.isBtn) obj = obj.parent;
348
  if (obj.userData.isBtn && isDown) {
349
- const v = obj.userData.val;
350
-
351
- // Interaction logic
352
- navigate(v);
353
-
354
  const originalZ = obj.position.z;
355
  obj.position.z -= 0.04;
356
  setTimeout(() => obj.position.z = originalZ, 100);
@@ -362,7 +338,6 @@ function handleInput(x, y, isDown) {
362
  function animate() {
363
  requestAnimationFrame(animate);
364
  if (phone && !isDragging) {
365
- // Auto-rotation disabled as requested
366
  phone.rotation.x = THREE.MathUtils.lerp(phone.rotation.x, 0, 0.05);
367
  phone.rotation.y = THREE.MathUtils.lerp(phone.rotation.y, 0, 0.05);
368
  }
 
5
  const mouse = new THREE.Vector2();
6
 
7
  // --- EMULATOR STATE ---
8
+ let currentScreen = "HOME";
9
+ let selectedIndex = 0;
10
  const menuIcons = [
11
+ { name: "Messages", svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="m22 6-10 7L2 6"/></svg>` },
12
+ { name: "Contacts", svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>` },
13
+ { name: "Camera", svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>` },
14
+ { name: "Settings", svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>` },
15
+ { name: "Games", svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 12h4m-2-2v4m7-1h.01m2-2h.01"/></svg>` },
16
+ { name: "Tools", svg: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a2 2 0 0 1-2.83-2.83l-3.94 3.6z"/><path d="m10.11 10.11-6.17 6.17a2 2 0 0 0 2.83 2.83l6.17-6.17"/><path d="m9.2 14.8-1 1"/><path d="m11.8 12.2-1 1"/><path d="m14 10-1 1"/></svg>` }
17
  ];
18
 
19
+ // Canvas for Screen
20
  const screenCanvas = document.createElement('canvas');
21
  screenCanvas.width = 240; screenCanvas.height = 320;
22
  const ctx = screenCanvas.getContext('2d');
 
57
  ctx.fillText('Menu', 120, 310);
58
  }
59
 
60
+ function drawSvg(svgStr, x, y, size) {
61
+ const img = new Image();
62
+ const svgBlob = new Blob([svgStr], {type: 'image/svg+xml;charset=utf-8'});
63
+ const url = URL.createObjectURL(svgBlob);
64
+ img.onload = function() {
65
+ ctx.drawImage(img, x - size/2, y - size/2, size, size);
66
+ URL.revokeObjectURL(url);
67
+ };
68
+ img.src = url;
69
+ }
70
+
71
  function drawMenu() {
72
  ctx.fillStyle = '#000';
73
  ctx.fillRect(0, 25, 240, 295);
74
 
75
  const cols = 3;
76
  const padding = 20;
77
+ const size = 40;
78
 
79
  menuIcons.forEach((item, i) => {
80
  const r = Math.floor(i / cols);
81
  const c = i % cols;
82
+ const x = padding + c * (size + 35) + size/2;
83
+ const y = 80 + r * (size + 50);
84
 
85
  if (i === selectedIndex) {
86
  ctx.strokeStyle = '#7c3aed';
87
+ ctx.lineWidth = 2;
88
+ ctx.strokeRect(x - size/2 - 10, y - size/2 - 10, size + 20, size + 20);
89
  }
90
 
91
+ // Using temporary images for SVGs
92
+ const svgUri = 'data:image/svg+xml;base64,' + btoa(item.svg.replace('currentColor', i === selectedIndex ? '#7c3aed' : 'white'));
93
+ const img = new Image();
94
+ img.src = svgUri;
95
+ ctx.drawImage(img, x - size/2, y - size/2, size, size);
96
+
97
+ ctx.font = '12px Arial';
98
+ ctx.textAlign = 'center';
99
  ctx.fillStyle = (i === selectedIndex) ? '#7c3aed' : 'white';
100
+ ctx.fillText(item.name, x, y + size/2 + 20);
101
  });
102
  }
103
 
 
114
  ctx.fillStyle = 'white';
115
  ctx.font = '16px Arial';
116
  ctx.textAlign = 'center';
117
+ ctx.fillText(currentScreen.replace("APP_", ""), 120, 140);
118
+
119
+ // Simple functional apps content
120
+ if(currentScreen === "APP_MESSAGES") {
121
+ ctx.font = '12px Arial';
122
+ ctx.fillText("1. New Message", 120, 180);
123
+ ctx.fillText("2. Inbox", 120, 200);
124
+ ctx.fillText("3. Sent", 120, 220);
125
+ } else if(currentScreen === "APP_CONTACTS") {
126
+ ctx.font = '12px Arial';
127
+ ctx.fillText("fyn Developer", 120, 180);
128
+ ctx.fillText("+880 1XXX-XXXXXX", 120, 200);
129
+ } else if(currentScreen === "APP_SETTINGS") {
130
+ ctx.font = '12px Arial';
131
+ ctx.fillText("Display: KeyNX High", 120, 180);
132
+ ctx.fillText("Sound: Polyphonic", 120, 200);
133
+ ctx.fillText("Network: 2G/GSM", 120, 220);
134
+ } else {
135
+ ctx.font = '12px Arial';
136
+ ctx.fillText('Coming Soon...', 120, 180);
137
+ }
138
  }
139
 
140
  if (screenMesh && screenMesh.material.map) screenMesh.material.map.needsUpdate = true;
141
  }
142
 
 
143
  function navigate(key) {
144
  if (currentScreen === "HOME") {
145
+ if (key === "FLASHLIGHT" || key === "-" || key === "▲" || key === "▼" || key === "◀" || key === "▶") {
146
  currentScreen = "MENU";
147
  selectedIndex = 0;
148
  }
 
169
  shape.lineTo(w/2, h/2 - r);
170
  shape.quadraticCurveTo(w/2, h/2, w/2 - r, h/2);
171
  shape.lineTo(-w/2 + r, h/2);
172
+ shape.quadraticCurveTo(w/2, h/2, w/2 - r, h/2);
173
+ shape.lineTo(-w/2 + r, h/2);
174
  shape.quadraticCurveTo(-w/2, h/2, -w/2, h/2 - r);
175
  shape.lineTo(-w/2, -h/2 + r);
176
  shape.quadraticCurveTo(-w/2, -h/2, -w/2 + r, -h/2);
 
179
 
180
  function createPill(w, h, label, color, x, y, isCircle = false, isDpad = false, isNav = false) {
181
  const group = new THREE.Group();
182
+ const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.4, metalness: 0.1 });
 
 
 
 
183
  let geom;
 
 
 
184
  if (isCircle) {
185
+ geom = new THREE.CylinderGeometry(w/2, w/2, 0.08, 32);
186
  } else {
187
  const r = Math.min(w, h) * 0.25;
188
+ geom = new THREE.ExtrudeGeometry(createRoundedRectShape(w, h, r), { depth: 0.08, bevelEnabled: true, bevelSegments: 2, steps: 1, bevelSize: 0.01, bevelThickness: 0.01 });
 
 
 
 
189
  }
190
  const mesh = new THREE.Mesh(geom, mat);
191
  if (isCircle) mesh.rotation.x = Math.PI/2;
192
  const borderMat = new THREE.MeshBasicMaterial({ color: 0xcccccc });
193
  let borderGeom;
194
  if (isCircle) {
195
+ borderGeom = new THREE.CylinderGeometry(w/2 + 0.005, w/2 + 0.005, 0.07, 32);
196
  } else {
197
  const br = Math.min(w + 0.01, h + 0.01) * 0.25;
198
+ borderGeom = new THREE.ExtrudeGeometry(createRoundedRectShape(w + 0.01, h + 0.01, br), { depth: 0.07, bevelEnabled: false });
 
199
  }
200
  const borderMesh = new THREE.Mesh(borderGeom, borderMat);
201
  if (isCircle) borderMesh.rotation.x = Math.PI/2;
 
207
  if (tctx) {
208
  tctx.textAlign = 'center';
209
  if (label === 'FLASHLIGHT') {
210
+ tctx.fillStyle = '#333'; tctx.font = 'bold 80px Arial'; tctx.fillText('🔦', 64, 90);
 
211
  } else {
212
+ tctx.font = isNav ? 'bold 55px Arial' : 'bold 50px Arial';
213
+ tctx.fillStyle = label === '📞' ? '#22c55e' : label === '❌' ? '#ef4444' : '#333';
214
  tctx.fillText(label, 64, 75);
215
+ const sub = {'2':'ABC','3':'DEF','4':'GHI','5':'JKL','6':'MNO','7':'PQRS','8':'TUV','9':'WXYZ'};
216
+ if (sub[label]) { tctx.font = '28px Arial'; tctx.fillText(sub[label], 64, 100); }
 
 
217
  }
218
  }
219
  const tMat = new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(tCan), transparent: true });
220
  const tMesh = new THREE.Mesh(new THREE.PlaneGeometry(w*0.8, h*0.8), tMat);
221
+ tMesh.position.z = 0.1;
222
  group.add(mesh); group.add(tMesh);
223
  group.position.set(x, y, 0.05);
224
  group.userData = { isBtn: true, val: label };
 
230
  camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000);
231
  camera.position.z = 15;
232
  const container = document.getElementById('canvas-container');
233
+ try { renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); } catch (e) { renderer = new THREE.WebGLRenderer({ alpha: true }); }
 
 
 
 
234
  renderer.setSize(window.innerWidth, window.innerHeight);
235
  renderer.setPixelRatio(window.devicePixelRatio || 1);
236
  container.appendChild(renderer.domElement);
 
250
  phone.add(bezel);
251
 
252
  updateDisplay();
253
+ screenMesh = new THREE.Mesh(new THREE.PlaneGeometry(4.1, 4.3), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(screenCanvas) }));
 
 
 
254
  screenMesh.position.set(0, 2.05, 0.46);
255
  phone.add(screenMesh);
256
 
257
  const brandCanvas = document.createElement('canvas');
258
  brandCanvas.width = 512; brandCanvas.height = 128;
259
  const bctx = brandCanvas.getContext('2d');
260
+ if (bctx) { bctx.fillStyle = '#444'; bctx.font = 'bold 50px Arial'; bctx.textAlign = 'center'; bctx.fillText('KeyNX', 256, 80); }
261
+ const brand = new THREE.Mesh(new THREE.PlaneGeometry(2.0, 0.5), new THREE.MeshBasicMaterial({map: new THREE.CanvasTexture(brandCanvas), transparent: true}));
 
 
 
 
 
 
262
  brand.position.set(0, -0.4, 0.46);
263
  phone.add(brand);
264
 
 
267
  phone.add(buttonsContainer);
268
 
269
  const btnColor = 0xe0e0e0;
 
270
  const dpadY = 1.0; const baseCenterSize = 0.85;
271
  buttonsContainer.add(createPill(baseCenterSize, baseCenterSize, "FLASHLIGHT", 0xffffff, 0, dpadY, false, true));
272
  const slimSize = 0.08; const hubSize = baseCenterSize + slimSize * 2;
 
284
  const keys = [['1','2','3'],['4','5','6'],['7','8','9'],['*','0','#']];
285
  const startY = -0.1; const spacingX = 1.35; const spacingY = 0.55;
286
  keys.forEach((row, ri) => {
287
+ row.forEach((key, ci) => { buttonsContainer.add(createPill(1.15, 0.4, key, btnColor, -spacingX + ci*spacingX, startY - ri*spacingY)); });
 
 
288
  });
289
 
290
  const backGroup = new THREE.Group();
291
  backGroup.position.z = -0.41;
292
  phone.add(backGroup);
 
293
  const camHousingGeom = new THREE.ExtrudeGeometry(createRoundedRectShape(1.8, 1.8, 0.4), { depth: 0.1, bevelEnabled: false });
294
  const camHousing = new THREE.Mesh(camHousingGeom, bezelMat);
295
  camHousing.position.set(0.8, 2.5, 0);
 
299
  const camCanvas = document.createElement('canvas');
300
  camCanvas.width = 512; camCanvas.height = 512;
301
  const cctx = camCanvas.getContext('2d');
302
+ cctx.fillStyle = '#0a0a0a'; cctx.fillRect(0, 0, 512, 512);
 
 
303
  const lensY = 150;
304
  cctx.strokeStyle = '#333'; cctx.lineWidth = 12;
305
  cctx.beginPath(); cctx.arc(160, lensY, 80, 0, Math.PI*2); cctx.stroke();
 
307
  cctx.fillStyle = '#111';
308
  cctx.beginPath(); cctx.arc(160, lensY, 70, 0, Math.PI*2); cctx.fill();
309
  cctx.beginPath(); cctx.arc(352, lensY, 70, 0, Math.PI*2); cctx.fill();
 
 
 
 
310
  cctx.fillStyle = 'white'; cctx.font = 'bold 64px Arial'; cctx.textAlign = 'center';
311
  cctx.fillText('VGA CAM', 256, 420);
312
+ const camLabel = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 1.6), new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(camCanvas) }));
 
 
 
 
 
 
313
  camLabel.position.set(0.8, 2.5, -0.11);
314
  camLabel.rotation.y = Math.PI;
315
  backGroup.add(camLabel);
 
 
 
 
 
 
 
 
 
 
 
316
  animate();
317
  }
318
 
 
326
  let obj = hits[0].object;
327
  while(obj.parent && !obj.userData.isBtn) obj = obj.parent;
328
  if (obj.userData.isBtn && isDown) {
329
+ navigate(obj.userData.val);
 
 
 
 
330
  const originalZ = obj.position.z;
331
  obj.position.z -= 0.04;
332
  setTimeout(() => obj.position.z = originalZ, 100);
 
338
  function animate() {
339
  requestAnimationFrame(animate);
340
  if (phone && !isDragging) {
 
341
  phone.rotation.x = THREE.MathUtils.lerp(phone.rotation.x, 0, 0.05);
342
  phone.rotation.y = THREE.MathUtils.lerp(phone.rotation.y, 0, 0.05);
343
  }