Mi Campo — Vídeo tutorial
Mi Campo · Vídeo tutorial
Recorrido guiado en español
1 / 15
9:41 📶🔋
Pulsa Reproducir para empezar el tutorial.
0:00
0:00
Narración con la voz del navegador (es-ES) · Ver dossier escrito · Abrir la app

Vídeo tutorial de Mi Campo

Un recorrido visual de 3 minutos por las funciones de la app, narrado en español.

Pulsa para empezar (necesario para activar la voz)

TML = sc.html // Captions $('caption-text').innerHTML = sc.narration // Counter $('sc-counter').textContent = (currentScene+1) + ' / ' + SCENES.length // Total duration shown in timeline (suma global) $('t-total').textContent = fmtTime(totalDuration) // Pulsos animados (taps) si existen if(sc.taps && sc.taps.length){ sc.taps.forEach(tap => { setTimeout(() => { if(currentScene !== SCENES.indexOf(sc)) return const el = $('fake-tap') const phone = document.querySelector('.phone-screen') const rect = phone.getBoundingClientRect() el.style.left = (tap.x*rect.width/100) + 'px' el.style.top = (tap.y*rect.height/100) + 'px' el.classList.remove('show') // forzar reflow void el.offsetWidth el.classList.add('show') }, tap.at*1000) }) } } function speakNarration(text, onEnd){ if(!window.speechSynthesis || isMuted){ onEnd && setTimeout(onEnd, 200); return null } // Cancelar cualquier locución previa speechSynthesis.cancel() const u = new SpeechSynthesisUtterance(text.replace(/<[^>]+>/g,'')) u.lang = 'es-ES' u.rate = 1.0 u.pitch = 1.0 u.volume = 1.0 if(chosenVoice) u.voice = chosenVoice u.onend = () => onEnd && onEnd() u.onerror = () => onEnd && onEnd() // Algunos navegadores se "duermen" — ping cada 10s speechSynthesis.speak(u) return u } function startScene(){ renderScene() sceneStart = Date.now() - sceneElapsed*1000 const sc = SCENES[currentScene] // Empezar voz (si no muteada). El timer es el FALLBACK por si la voz falla. let advanced = false const advance = () => { if(advanced) return advanced = true if(isPlaying) nextScene(true) } if(!isMuted){ speakNarration(sc.narration, advance) } // Timer fallback en función de la duración estimada const remainingMs = Math.max(1500, (sc.duration - sceneElapsed)*1000) if(sceneTimer) clearTimeout(sceneTimer) sceneTimer = setTimeout(advance, remainingMs + 800) // +800ms gracia // Update progress bar if(progressInterval) clearInterval(progressInterval) progressInterval = setInterval(updateProgress, 250) } function updateProgress(){ if(!isPlaying){ return } // Tiempo total transcurrido = suma de escenas previas + actual let prev = 0 for(let i=0;i { if(isPlaying) nextScene(true) }, remainingMs + 800) } else { startScene() } } function nextScene(auto){ if(currentScene >= SCENES.length-1){ // Final del vídeo pauseVideo() currentScene = SCENES.length - 1 sceneElapsed = SCENES[currentScene].duration updateProgress() if(window.speechSynthesis) speechSynthesis.cancel() return } currentScene++ sceneElapsed = 0 if(isPlaying) startScene() else { renderScene(); updateProgress() } } function prevScene(){ if(currentScene <= 0) return currentScene-- sceneElapsed = 0 if(window.speechSynthesis) speechSynthesis.cancel() if(isPlaying) startScene() else { renderScene(); updateProgress() } } function restart(){ if(window.speechSynthesis) speechSynthesis.cancel() currentScene = 0 sceneElapsed = 0 if(isPlaying) startScene() else { renderScene(); updateProgress() } } function toggleMute(){ isMuted = !isMuted $('mute-btn').textContent = isMuted ? '🔇' : '🔊' if(window.speechSynthesis){ if(isMuted) speechSynthesis.cancel() else if(isPlaying){ // Reanudar narración de la escena actual const sc = SCENES[currentScene] speakNarration(sc.narration) } } } function toggleCaptions(){ captionsOn = !captionsOn $('captions').classList.toggle('hidden', !captionsOn) $('cap-btn').style.opacity = captionsOn ? '1' : '.45' } function seekFromClick(e){ const track = $('prog-track') const rect = track.getBoundingClientRect() const pct = (e.clientX - rect.left) / rect.width const targetT = pct * totalDuration // Encontrar la escena correspondiente let acc = 0 for(let i=0;i targetT){ currentScene = i sceneElapsed = targetT - acc break } acc += SCENES[i].duration } if(window.speechSynthesis) speechSynthesis.cancel() if(isPlaying) startScene() else { renderScene(); updateProgress() } } function startVideo(){ $('start-overlay').classList.add('hidden') isPlaying = true $('play-btn').textContent = '⏸' // Las voces a veces tardan en cargar — si aún no hay voz elegida, intentamos otra vez if(!voicesReady) loadVoices() startScene() } function goBack(){ if(window.speechSynthesis) speechSynthesis.cancel() // Intentar volver a la app o al dossier if(window.history.length > 1) window.history.back() else window.location.href = 'index.html' } // Render inicial (la primera escena se ve antes de pulsar play) renderScene() updateProgress() // Atajos de teclado document.addEventListener('keydown', e => { if($('start-overlay').classList.contains('hidden')){ if(e.key === ' '){ e.preventDefault(); togglePlay() } else if(e.key === 'ArrowRight'){ nextScene() } else if(e.key === 'ArrowLeft'){ prevScene() } else if(e.key === 'm' || e.key === 'M'){ toggleMute() } else if(e.key === 'c' || e.key === 'C'){ toggleCaptions() } else if(e.key === 'Escape'){ goBack() } } else if(e.key === ' ' || e.key === 'Enter'){ e.preventDefault(); startVideo() } })