MatteoScript commited on
Commit
47c4845
·
verified ·
1 Parent(s): d13a922

Update ar.html

Browse files
Files changed (1) hide show
  1. ar.html +146 -111
ar.html CHANGED
@@ -102,7 +102,7 @@
102
  }
103
  .loader-fill {
104
  height: 100%; background: var(--neon-green); width: 0%;
105
- transition: width 0.1s linear; /* Resa più fluida */
106
  box-shadow: 0 0 10px var(--neon-green);
107
  }
108
 
@@ -111,31 +111,23 @@
111
  padding-bottom: env(safe-area-inset-bottom, 20px);
112
  }
113
 
114
- /* FALLBACK VIDEO PLAYER */
115
- #fallback-player {
116
- position: fixed;
117
- top: 50%;
118
- left: 50%;
119
- transform: translate(-50%, -50%);
120
- width: 90%;
121
- max-width: 500px;
122
- z-index: 1000;
123
- background: rgba(0,0,0,0.95);
124
- border: 2px solid var(--neon-green);
125
- border-radius: 12px;
126
- padding: 20px;
127
  display: none;
128
- box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
129
- }
130
-
131
- #fallback-player video {
132
- position: relative !important;
133
- width: 100% !important;
134
- height: auto !important;
135
- min-width: auto !important;
136
- min-height: auto !important;
137
- border-radius: 8px;
138
- margin-bottom: 15px;
 
 
 
139
  }
140
  </style>
141
  </head>
@@ -164,16 +156,14 @@
164
  <span id="sys-status" class="text-[#00ff41] font-bold text-lg tracking-wider animate-pulse">SCANNING...</span>
165
  <span class="text-[8px] text-white/40 font-mono mt-1">FPS: <span id="fps-counter">60</span> | STABLE_MODE: ON</span>
166
  </div>
167
- <!-- Pulsante chiusura finto -->
168
  <div class="w-10 h-10 flex items-center justify-center border border-[#00ff41]/50 rounded-full bg-black/40 backdrop-blur-md opacity-50">
169
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#00ff41" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
170
  </div>
171
  </div>
172
 
173
- <!-- MIRINO QUADRATO (RIDOTTO) -->
174
  <div id="aim-overlay" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center justify-center transition-opacity duration-300">
175
- <!-- Frame QUADRATO RIDIMENSIONATO -->
176
- <!-- Prima era w-[70vw] max-w-[280px]. Ora è più piccolo. -->
177
  <div class="relative w-[50vw] h-[50vw] max-w-[200px] max-h-[200px] border border-white/20 rounded-xl">
178
  <div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 border-[#00ff41]"></div>
179
  <div class="absolute -top-1 -right-1 w-4 h-4 border-t-2 border-r-2 border-[#00ff41]"></div>
@@ -190,45 +180,30 @@
190
  </div>
191
  </div>
192
 
193
- <!-- BOTTOM CONTROLS (COMPATTI) -->
194
  <div class="w-full px-6 pb-6 pointer-events-auto flex flex-col items-center bg-gradient-to-t from-black/90 to-transparent">
195
 
196
- <!-- Pulsante Audio (Più piccolo) -->
197
  <button id="audio-btn" class="w-full max-w-xs h-12 bg-[#00ff41]/10 border border-[#00ff41] text-[#00ff41] font-bold text-sm uppercase tracking-[0.15em] rounded active:bg-[#00ff41] active:text-black transition-all mb-3 flex items-center justify-center gap-3 backdrop-blur-md">
198
  <span>🔊</span>
199
  <span id="audio-text">Attiva Audio</span>
200
  </button>
201
 
202
- <!-- Pulsante Fallback (Uguale altezza, più piccolo) -->
203
- <button id="fallback-btn" class="w-full max-w-xs h-12 bg-[#ff0055]/10 border border-[#ff0055] text-[#ff0055] font-bold text-sm uppercase tracking-[0.15em] rounded active:bg-[#ff0055] active:text-white transition-all mb-2 flex items-center justify-center gap-2 backdrop-blur-md">
204
  <span>⚠️</span>
205
- <span>Mostra Video</span>
206
  </button>
207
-
208
- <!-- Testo credits rimosso come richiesto -->
209
  </div>
210
  </div>
211
 
212
- <!-- 4. FALLBACK VIDEO PLAYER -->
213
- <div id="fallback-player">
214
- <div class="text-[#00ff41] font-bold text-xl mb-3 text-center">📹 FALLBACK MODE</div>
215
- <video id="fallback-video" controls playsinline webkit-playsinline>
216
- <source src="./img/video.mp4" type="video/mp4">
217
- </video>
218
- <button id="close-fallback" class="w-full py-3 bg-[#ff0055] text-white font-bold uppercase rounded active:scale-95 transition-transform">
219
- Chiudi
220
- </button>
221
- <p class="text-white/60 text-xs mt-3 text-center">Il marker non è stato trovato. Usa questo player manuale.</p>
222
- </div>
223
 
224
- <!-- 5. AR SCENE - STABILIZZAZIONE ESTREMA -->
225
- <!--
226
- MODIFICHE STABILITÀ:
227
- 1. filterBeta abbassato a 0.01 (Prima era 10000). Valori bassi riducono il jittering (tremolio) aumentando leggermente la latenza.
228
- 2. filterMinCF abbassato a 0.0001 per tagliare il rumore ad alta frequenza.
229
- -->
230
  <a-scene
231
- mindar-image="imageTargetSrc: img/targets.mind; filterMinCF:0.0001; filterBeta: 0.01; uiLoading: no; uiScanning: no; missTolerance: 20; warmupTolerance: 5; maxTrack: 1"
232
  color-space="sRGB"
233
  renderer="colorManagement: true, physicallyCorrectLights, highRefreshRate: true, antialias: true"
234
  vr-mode-ui="enabled: false"
@@ -240,35 +215,39 @@
240
 
241
  <a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
242
 
 
243
  <a-entity id="example-target" mindar-image-target="targetIndex: 0">
244
- <!-- VIDEO 16:9 con CSS smoothing extra -->
245
- <a-entity id="myVid"
246
- geometry="primitive: plane; width: 1.6; height: 0.9"
247
- material="src: #vid; shader: flat; transparent: true; opacity: 1"
248
- position="0 0 0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  </a-entity>
250
-
251
- <!-- Cornice Cyberpunk 16:9 -->
252
- <a-plane position="0 0.47 0" width="1.68" height="0.03" color="#00ff41" material="shader: flat">
253
- <a-animation attribute="opacity" from="1" to="0.7" dur="100" direction="alternate" repeat="indefinite"></a-animation>
254
- </a-plane>
255
- <a-plane position="0 -0.47 0" width="1.68" height="0.03" color="#00ff41" material="shader: flat"></a-plane>
256
- <a-plane position="-0.83 0 0" width="0.03" height="0.96" color="#00ff41" material="shader: flat"></a-plane>
257
- <a-plane position="0.83 0 0" width="0.03" height="0.96" color="#00ff41" material="shader: flat">
258
- <a-animation attribute="opacity" from="1" to="0.7" dur="150" direction="alternate" repeat="indefinite"></a-animation>
259
- </a-plane>
260
-
261
- <a-plane position="-0.77 0.41 0.01" width="0.15" height="0.03" color="white" material="shader: flat; opacity: 0.9"></a-plane>
262
- <a-plane position="0.77 -0.41 0.01" width="0.15" height="0.03" color="white" material="shader: flat; opacity: 0.9"></a-plane>
263
-
264
- <a-text value="STABLE_LINK" color="#00ff41" align="center" width="1.5" position="0 -0.6 0.01"
265
- font="https://cdn.aframe.io/fonts/Roboto-msdf.json">
266
- <a-animation attribute="opacity" from="0.3" to="1" dur="800" direction="alternate" repeat="indefinite"></a-animation>
267
- </a-text>
268
  </a-entity>
269
  </a-scene>
270
 
271
- <!-- 6. LOGIC -->
272
  <script>
273
  const video = document.querySelector("#vid");
274
  const target = document.querySelector("#example-target");
@@ -282,39 +261,48 @@
282
  const scene = document.querySelector("a-scene");
283
  const fpsCounter = document.querySelector("#fps-counter");
284
  const fallbackBtn = document.querySelector("#fallback-btn");
285
- const fallbackPlayer = document.querySelector("#fallback-player");
286
- const fallbackVideo = document.querySelector("#fallback-video");
287
- const closeFallback = document.querySelector("#close-fallback");
 
288
 
289
  let isAudioEnabled = false;
290
  let loadProgress = 0;
 
 
 
291
  let smoothPositionX = 0;
292
  let smoothPositionY = 0;
293
  let smoothPositionZ = 0;
294
-
295
- // MODIFICA STABILITÀ: Abbassato fattore di smoothing
296
- // 0.5 era troppo veloce (scattoso). 0.1 significa che si muove solo del 10% verso il target ogni frame.
297
- // Risultato: Movimento molto "pesante" e fluido che assorbe le vibrazioni della mano.
298
  const SMOOTHING_FACTOR = 0.1;
 
299
 
300
  // CUSTOM SMOOTHING LAYER
301
- const myVidEntity = document.querySelector("#myVid");
302
 
303
- // Applica smoothing custom ogni frame
304
  setInterval(() => {
305
- if(myVidEntity && target.object3D.visible) {
 
306
  const currentPos = target.object3D.position;
307
 
308
- // Lerp (Linear interpolation) pesante
309
- smoothPositionX += (currentPos.x - smoothPositionX) * SMOOTHING_FACTOR;
310
- smoothPositionY += (currentPos.y - smoothPositionY) * SMOOTHING_FACTOR;
311
- smoothPositionZ += (currentPos.z - smoothPositionZ) * SMOOTHING_FACTOR;
 
 
 
 
 
 
 
 
 
 
312
 
313
- // Applica la posizione smoothed al contenuto, NON al target stesso (che è controllato da MindAR)
314
- // Nota: In A-Frame il target controlla tutto il gruppo.
315
- // Per un vero smoothing visivo sovrascriviamo leggermente la posizione dei figli o usiamo una logica di "follow".
316
- // Qui stiamo applicando un trick: se il target si muove troppo velocemente, l'occhio lo nota.
317
- // Con MindAR filterBeta molto basso (0.01), il target stesso sarà stabile.
318
  }
319
  }, 16);
320
 
@@ -333,13 +321,12 @@
333
  ];
334
  let msgIndex = 0;
335
 
336
- // MODIFICA CARICAMENTO: Rallentato
337
  const loadInterval = setInterval(() => {
338
- // Incremento ridotto da 2 a 0.4 per rallentare il caricamento
339
- loadProgress += 0.4;
340
 
341
  if(loadProgress > 100) loadProgress = 100;
342
- loadingPercent.innerText = Math.floor(loadProgress) + "%"; // Arrotonda per estetica
343
  progressBar.style.width = loadProgress + "%";
344
 
345
  if(Math.floor(loadProgress) % 25 === 0 && msgIndex < nerdMessages.length) {
@@ -351,18 +338,27 @@
351
  clearInterval(loadInterval);
352
  document.querySelector('#load-msg').innerText = "System Ready.";
353
  }
354
- }, 30); // Esegue ogni 30ms
355
 
356
  // AR PRONTA
357
  scene.addEventListener("arReady", () => {
358
  console.log("✅ AR SYSTEM ONLINE");
359
- // Aspetta che la barra finisca visivamente se è ancora indietro
360
  const checkLoad = setInterval(() => {
361
  if(loadProgress >= 100) {
362
  clearInterval(checkLoad);
363
  setTimeout(() => {
364
  loader.style.opacity = "0";
365
  setTimeout(() => loader.style.display = "none", 500);
 
 
 
 
 
 
 
 
 
366
  }, 500);
367
  }
368
  }, 100);
@@ -370,28 +366,40 @@
370
 
371
  // TARGET FOUND
372
  target.addEventListener("targetFound", () => {
 
 
373
  video.play();
374
  statusText.innerText = "LOCKED";
375
  statusText.style.color = "#fff";
376
  statusText.classList.remove("animate-pulse");
377
  aimOverlay.style.opacity = "0";
 
 
 
 
378
  if(navigator.vibrate) navigator.vibrate(50);
379
  });
380
 
381
  // TARGET LOST
382
  target.addEventListener("targetLost", () => {
 
 
383
  video.pause();
384
  statusText.innerText = "SEARCHING...";
385
  statusText.style.color = "#00ff41";
386
  statusText.classList.add("animate-pulse");
387
  aimOverlay.style.opacity = "1";
 
 
 
 
 
388
  });
389
 
390
  // AUDIO TOGGLE
391
  audioBtn.addEventListener('click', () => {
392
  if (!isAudioEnabled) {
393
  video.muted = false;
394
- fallbackVideo.muted = false;
395
  isAudioEnabled = true;
396
  audioText.innerText = "AUDIO ON";
397
  audioBtn.classList.add("bg-[#00ff41]", "text-black");
@@ -406,17 +414,44 @@
406
  }
407
  });
408
 
409
- // FALLBACK MODE
410
  fallbackBtn.addEventListener('click', () => {
411
- fallbackPlayer.style.display = 'block';
412
- fallbackVideo.play();
413
- if(navigator.vibrate) navigator.vibrate(100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  });
415
 
416
- closeFallback.addEventListener('click', () => {
417
- fallbackPlayer.style.display = 'none';
418
- fallbackVideo.pause();
419
  });
 
420
  </script>
421
  </body>
422
  </html>
 
102
  }
103
  .loader-fill {
104
  height: 100%; background: var(--neon-green); width: 0%;
105
+ transition: width 0.1s linear;
106
  box-shadow: 0 0 10px var(--neon-green);
107
  }
108
 
 
111
  padding-bottom: env(safe-area-inset-bottom, 20px);
112
  }
113
 
114
+ /* Pulsante X per chiusura modalità manuale */
115
+ #manual-close-btn {
 
 
 
 
 
 
 
 
 
 
 
116
  display: none;
117
+ position: fixed;
118
+ top: 20px;
119
+ right: 20px;
120
+ z-index: 2000;
121
+ width: 40px;
122
+ height: 40px;
123
+ background: rgba(255, 0, 85, 0.8);
124
+ border: 1px solid #ff0055;
125
+ border-radius: 50%;
126
+ align-items: center;
127
+ justify-content: center;
128
+ color: white;
129
+ font-weight: bold;
130
+ box-shadow: 0 0 15px rgba(255, 0, 85, 0.5);
131
  }
132
  </style>
133
  </head>
 
156
  <span id="sys-status" class="text-[#00ff41] font-bold text-lg tracking-wider animate-pulse">SCANNING...</span>
157
  <span class="text-[8px] text-white/40 font-mono mt-1">FPS: <span id="fps-counter">60</span> | STABLE_MODE: ON</span>
158
  </div>
159
+ <!-- Pulsante chiusura (Decorativo o funzionale se necessario) -->
160
  <div class="w-10 h-10 flex items-center justify-center border border-[#00ff41]/50 rounded-full bg-black/40 backdrop-blur-md opacity-50">
161
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#00ff41" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
162
  </div>
163
  </div>
164
 
165
+ <!-- MIRINO QUADRATO -->
166
  <div id="aim-overlay" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center justify-center transition-opacity duration-300">
 
 
167
  <div class="relative w-[50vw] h-[50vw] max-w-[200px] max-h-[200px] border border-white/20 rounded-xl">
168
  <div class="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 border-[#00ff41]"></div>
169
  <div class="absolute -top-1 -right-1 w-4 h-4 border-t-2 border-r-2 border-[#00ff41]"></div>
 
180
  </div>
181
  </div>
182
 
183
+ <!-- BOTTOM CONTROLS -->
184
  <div class="w-full px-6 pb-6 pointer-events-auto flex flex-col items-center bg-gradient-to-t from-black/90 to-transparent">
185
 
 
186
  <button id="audio-btn" class="w-full max-w-xs h-12 bg-[#00ff41]/10 border border-[#00ff41] text-[#00ff41] font-bold text-sm uppercase tracking-[0.15em] rounded active:bg-[#00ff41] active:text-black transition-all mb-3 flex items-center justify-center gap-3 backdrop-blur-md">
187
  <span>🔊</span>
188
  <span id="audio-text">Attiva Audio</span>
189
  </button>
190
 
191
+ <!-- Pulsante Fallback (Nascosto inizialmente) -->
192
+ <button id="fallback-btn" class="hidden w-full max-w-xs h-12 bg-[#ff0055]/10 border border-[#ff0055] text-[#ff0055] font-bold text-sm uppercase tracking-[0.15em] rounded active:bg-[#ff0055] active:text-white transition-all mb-2 flex items-center justify-center gap-2 backdrop-blur-md animate-fade-in">
193
  <span>⚠️</span>
194
+ <span>NON TROVO MARKER</span>
195
  </button>
 
 
196
  </div>
197
  </div>
198
 
199
+ <!-- Bottone CHIUDI per modalità manuale -->
200
+ <button id="manual-close-btn" class="pointer-events-auto">
201
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M18 6L6 18M6 6l12 12"/></svg>
202
+ </button>
 
 
 
 
 
 
 
203
 
204
+ <!-- AR SCENE -->
 
 
 
 
 
205
  <a-scene
206
+ mindar-image="imageTargetSrc: img/targets.mind; filterMinCF:0.0001; filterBeta: 0.005; uiLoading: no; uiScanning: no; missTolerance: 20; warmupTolerance: 5; maxTrack: 1"
207
  color-space="sRGB"
208
  renderer="colorManagement: true, physicallyCorrectLights, highRefreshRate: true, antialias: true"
209
  vr-mode-ui="enabled: false"
 
215
 
216
  <a-camera position="0 0 0" look-controls="enabled: false"></a-camera>
217
 
218
+ <!-- TARGET MINDAR -->
219
  <a-entity id="example-target" mindar-image-target="targetIndex: 0">
220
+ <!-- GRUPPO CONTENUTO: Mettiamo tutto qui dentro per poterlo spostare facilmente -->
221
+ <a-entity id="ar-content-group">
222
+ <!-- VIDEO -->
223
+ <a-entity id="myVid"
224
+ geometry="primitive: plane; width: 1.6; height: 0.9"
225
+ material="src: #vid; shader: flat; transparent: true; opacity: 1"
226
+ position="0 0 0">
227
+ </a-entity>
228
+
229
+ <!-- CORNICI GRAFICHE -->
230
+ <a-plane position="0 0.47 0" width="1.68" height="0.03" color="#00ff41" material="shader: flat">
231
+ <a-animation attribute="opacity" from="1" to="0.7" dur="100" direction="alternate" repeat="indefinite"></a-animation>
232
+ </a-plane>
233
+ <a-plane position="0 -0.47 0" width="1.68" height="0.03" color="#00ff41" material="shader: flat"></a-plane>
234
+ <a-plane position="-0.83 0 0" width="0.03" height="0.96" color="#00ff41" material="shader: flat"></a-plane>
235
+ <a-plane position="0.83 0 0" width="0.03" height="0.96" color="#00ff41" material="shader: flat">
236
+ <a-animation attribute="opacity" from="1" to="0.7" dur="150" direction="alternate" repeat="indefinite"></a-animation>
237
+ </a-plane>
238
+
239
+ <a-plane position="-0.77 0.41 0.01" width="0.15" height="0.03" color="white" material="shader: flat; opacity: 0.9"></a-plane>
240
+ <a-plane position="0.77 -0.41 0.01" width="0.15" height="0.03" color="white" material="shader: flat; opacity: 0.9"></a-plane>
241
+
242
+ <a-text value="STABLE_LINK" color="#00ff41" align="center" width="1.5" position="0 -0.6 0.01"
243
+ font="https://cdn.aframe.io/fonts/Roboto-msdf.json">
244
+ <a-animation attribute="opacity" from="0.3" to="1" dur="800" direction="alternate" repeat="indefinite"></a-animation>
245
+ </a-text>
246
  </a-entity>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  </a-entity>
248
  </a-scene>
249
 
250
+ <!-- LOGIC -->
251
  <script>
252
  const video = document.querySelector("#vid");
253
  const target = document.querySelector("#example-target");
 
261
  const scene = document.querySelector("a-scene");
262
  const fpsCounter = document.querySelector("#fps-counter");
263
  const fallbackBtn = document.querySelector("#fallback-btn");
264
+
265
+ const contentGroup = document.querySelector("#ar-content-group");
266
+ const camera = document.querySelector("a-camera");
267
+ const manualCloseBtn = document.querySelector("#manual-close-btn");
268
 
269
  let isAudioEnabled = false;
270
  let loadProgress = 0;
271
+ let isManualMode = false;
272
+
273
+ // Smoothing Variables
274
  let smoothPositionX = 0;
275
  let smoothPositionY = 0;
276
  let smoothPositionZ = 0;
 
 
 
 
277
  const SMOOTHING_FACTOR = 0.1;
278
+ const MOVEMENT_THRESHOLD = 0.002; // Deadzone: se ti muovi meno di questo, non aggiorno (rimuove il tremolio da fermo)
279
 
280
  // CUSTOM SMOOTHING LAYER
281
+ const myVidEntity = document.querySelector("#myVid"); // Riferimento per controllo esistenza
282
 
 
283
  setInterval(() => {
284
+ // Applica solo se in modalità AR normale (target visible)
285
+ if(myVidEntity && target.object3D.visible && !isManualMode) {
286
  const currentPos = target.object3D.position;
287
 
288
+ // Calcola la differenza
289
+ let dx = currentPos.x - smoothPositionX;
290
+ let dy = currentPos.y - smoothPositionY;
291
+ let dz = currentPos.z - smoothPositionZ;
292
+
293
+ // DEADZONE CHECK: Se il movimento è minuscolo (tremolio), ignoralo
294
+ if (Math.abs(dx) < MOVEMENT_THRESHOLD) dx = 0;
295
+ if (Math.abs(dy) < MOVEMENT_THRESHOLD) dy = 0;
296
+ if (Math.abs(dz) < MOVEMENT_THRESHOLD) dz = 0;
297
+
298
+ // Applica smoothing solo se c'è movimento significativo
299
+ smoothPositionX += dx * SMOOTHING_FACTOR;
300
+ smoothPositionY += dy * SMOOTHING_FACTOR;
301
+ smoothPositionZ += dz * SMOOTHING_FACTOR;
302
 
303
+ // Nota: In MindAR puro non possiamo facilmente sovrascrivere la posizione del target senza rompere il tracking,
304
+ // ma questa logica è pronta per essere applicata a un figlio "ghost" se necessario.
305
+ // Per ora, ci affidiamo al filterBeta bassissimo (0.005) impostato nell'HTML per la stabilità principale.
 
 
306
  }
307
  }, 16);
308
 
 
321
  ];
322
  let msgIndex = 0;
323
 
324
+ // MODIFICA CARICAMENTO: Velocizzato del 20% circa (0.5 invece di 0.4)
325
  const loadInterval = setInterval(() => {
326
+ loadProgress += 0.55;
 
327
 
328
  if(loadProgress > 100) loadProgress = 100;
329
+ loadingPercent.innerText = Math.floor(loadProgress) + "%";
330
  progressBar.style.width = loadProgress + "%";
331
 
332
  if(Math.floor(loadProgress) % 25 === 0 && msgIndex < nerdMessages.length) {
 
338
  clearInterval(loadInterval);
339
  document.querySelector('#load-msg').innerText = "System Ready.";
340
  }
341
+ }, 30);
342
 
343
  // AR PRONTA
344
  scene.addEventListener("arReady", () => {
345
  console.log("✅ AR SYSTEM ONLINE");
346
+
347
  const checkLoad = setInterval(() => {
348
  if(loadProgress >= 100) {
349
  clearInterval(checkLoad);
350
  setTimeout(() => {
351
  loader.style.opacity = "0";
352
  setTimeout(() => loader.style.display = "none", 500);
353
+
354
+ // TIMER 10 SECONDI PER IL BOTTONE FALLBACK
355
+ setTimeout(() => {
356
+ if (!target.object3D.visible && !isManualMode) {
357
+ fallbackBtn.classList.remove('hidden');
358
+ fallbackBtn.style.display = 'flex'; // Tailwind hidden usa display:none
359
+ }
360
+ }, 10000);
361
+
362
  }, 500);
363
  }
364
  }, 100);
 
366
 
367
  // TARGET FOUND
368
  target.addEventListener("targetFound", () => {
369
+ if(isManualMode) return; // Ignora se in manual mode
370
+
371
  video.play();
372
  statusText.innerText = "LOCKED";
373
  statusText.style.color = "#fff";
374
  statusText.classList.remove("animate-pulse");
375
  aimOverlay.style.opacity = "0";
376
+
377
+ // Nascondi bottone fallback se appare mentre trovi il target
378
+ fallbackBtn.style.display = 'none';
379
+
380
  if(navigator.vibrate) navigator.vibrate(50);
381
  });
382
 
383
  // TARGET LOST
384
  target.addEventListener("targetLost", () => {
385
+ if(isManualMode) return;
386
+
387
  video.pause();
388
  statusText.innerText = "SEARCHING...";
389
  statusText.style.color = "#00ff41";
390
  statusText.classList.add("animate-pulse");
391
  aimOverlay.style.opacity = "1";
392
+
393
+ // Il bottone fallback riappare se sono passati i 10 sec?
394
+ // Per semplicità lo facciamo riapparire solo se il timer originale è scattato, ma qui lo mostriamo subito se l'utente lo aveva già visto.
395
+ // Una logica più pulita: lasciamo che il timer decida o lo mostriamo dopo un po' di "searching".
396
+ // Per ora seguiamo la logica base: se perdi il target, riparte la ricerca.
397
  });
398
 
399
  // AUDIO TOGGLE
400
  audioBtn.addEventListener('click', () => {
401
  if (!isAudioEnabled) {
402
  video.muted = false;
 
403
  isAudioEnabled = true;
404
  audioText.innerText = "AUDIO ON";
405
  audioBtn.classList.add("bg-[#00ff41]", "text-black");
 
414
  }
415
  });
416
 
417
+ // MODALITÀ MANUALE (SENZA MARKER)
418
  fallbackBtn.addEventListener('click', () => {
419
+ isManualMode = true;
420
+ console.log("🛠 MANUAL OVERRIDE ENGAGED");
421
+
422
+ // 1. Sposta il contenuto AR dentro la camera (HUD)
423
+ // Stacchiamo il gruppo dal target
424
+ contentGroup.parentNode.removeChild(contentGroup);
425
+ // Attacchiamo alla camera
426
+ camera.appendChild(contentGroup);
427
+
428
+ // 2. Posiziona il contenuto di fronte alla camera
429
+ contentGroup.setAttribute("position", "0 0 -1.5"); // 1.5 metri avanti
430
+ contentGroup.setAttribute("scale", "0.5 0.5 0.5"); // Scaliamo perché è vicino
431
+ contentGroup.setAttribute("rotation", "0 0 0");
432
+
433
+ // 3. Nascondi UI di ricerca
434
+ aimOverlay.style.display = "none";
435
+ fallbackBtn.style.display = "none";
436
+ statusText.innerText = "MANUAL_LINK";
437
+ statusText.style.color = "#ff0055";
438
+ statusText.classList.remove("animate-pulse");
439
+
440
+ // 4. Mostra bottone chiusura X
441
+ manualCloseBtn.style.display = "flex";
442
+
443
+ // 5. Avvia video
444
+ video.play();
445
+ if(video.muted && isAudioEnabled) video.muted = false;
446
+
447
+ if(navigator.vibrate) navigator.vibrate([50, 50, 50]);
448
  });
449
 
450
+ // CHIUSURA MODALITÀ MANUALE
451
+ manualCloseBtn.addEventListener('click', () => {
452
+ location.reload(); // Il modo più pulito per resettare MindAR e il DOM
453
  });
454
+
455
  </script>
456
  </body>
457
  </html>