diff --git a/dashcam.html b/dashcam.html index 3234d85..4d8f2ab 100644 --- a/dashcam.html +++ b/dashcam.html @@ -14,12 +14,45 @@ #rec-dot { width: 7px; height: 7px; border-radius: 50%; background: #fff; animation: blink 1s infinite; } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} } -#speed-display { position: absolute; top: 12px; right: 12px; text-align: center; background: rgba(0,0,0,0.45); border-radius: 12px; padding: 6px 14px; } +/* SMALL speedo — top right */ +#speed-display { + position: absolute; top: 12px; right: 12px; + text-align: center; background: rgba(0,0,0,0.45); + border-radius: 12px; padding: 6px 14px; + pointer-events: all; cursor: pointer; + transition: opacity 0.2s; +} +#speed-display:hover { background: rgba(255,255,255,0.12); } #speed-val { font-size: 44px; font-weight: 700; color: #fff; line-height: 1; } #speed-unit { font-size: 10px; color: #aaa; letter-spacing: 1.5px; margin-top: 1px; } -#max-speed { font-size: 10px; color: #666; margin-top: 2px; } -#gps-bar { position: absolute; bottom: 48px; left: 0; right: 0; background: rgba(0,0,0,0.5); padding: 5px 12px; display: none; flex-direction: column; gap: 3px; } +/* BIG speedo overlay */ +#speedo-overlay { + position: absolute; inset: 0; + background: rgba(0,0,0,0.92); + display: none; flex-direction: column; + align-items: center; justify-content: center; + pointer-events: all; cursor: pointer; + z-index: 10; +} +#speedo-overlay.visible { display: flex; } +#speedo-close { + position: absolute; top: 12px; right: 14px; + color: rgba(255,255,255,0.5); font-size: 22px; + cursor: pointer; font-family: var(--font-sans); + pointer-events: all; line-height: 1; + background: none; border: none; +} +#speedo-close:hover { color: #fff; } +#big-speed-val { font-size: clamp(80px, 22vw, 160px); font-weight: 700; color: #fff; line-height: 1; text-align: center; } +#big-speed-unit { font-size: clamp(14px, 3vw, 22px); color: #aaa; letter-spacing: 3px; margin-top: 8px; text-align: center; } +#big-gps-info { margin-top: 28px; text-align: center; display: flex; flex-direction: column; gap: 8px; } +.big-data-row { display: flex; gap: 24px; justify-content: center; flex-wrap: wrap; } +.bdi { font-size: clamp(11px, 2vw, 15px); color: #ccc; display: flex; gap: 6px; align-items: center; } +.bdl { color: #555; font-size: clamp(9px, 1.5vw, 12px); letter-spacing: 0.5px; } +.bdv { color: #fff; font-weight: 500; } + +#gps-bar { position: absolute; bottom: 48px; left: 0; right: 0; background: rgba(0,0,0,0.5); padding: 5px 12px; display: none; } .data-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; } .di { font-size: 10px; color: #ccc; display: flex; gap: 4px; align-items: center; } .dl { color: #666; font-size: 9px; letter-spacing: 0.4px; } @@ -31,7 +64,7 @@ #status-bar { position: absolute; bottom: 90px; left: 12px; font-size: 10px; color: rgba(255,255,255,0.4); font-family: var(--font-sans); pointer-events: none; } #placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; gap: 10px; font-family: var(--font-sans); background: #000; } -#btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 10px; padding: 7px 12px; background: rgba(0,0,0,0.5); pointer-events: all; flex-wrap: wrap; } +#btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 10px; padding: 7px 12px; background: rgba(0,0,0,0.5); pointer-events: all; flex-wrap: wrap; z-index: 20; } .hud-btn { background: rgba(255,255,255,0.1); border: 0.5px solid rgba(255,255,255,0.2); color: #fff; border-radius: 20px; padding: 5px 13px; font-size: 11px; font-family: var(--font-sans); cursor: pointer; letter-spacing: 0.4px; white-space: nowrap; } .hud-btn:hover { background: rgba(255,255,255,0.2); } .hud-btn:disabled { opacity: 0.3; cursor: default; } @@ -43,26 +76,50 @@
+
🎥
Tap START to begin
Camera + location permissions required
+
--:--:--
---
+
REC 00:00
-
+ + +
--
MPH
-
MAX --
+ + +
+ +
--
+
MPH
+
+
+
LAT--
+
LNG--
+
+
+
ALT--
+
ACC--
+
HDG--
+
DIST--
+
+
+
+
@@ -74,7 +131,9 @@
DIST0.00km
+
+
@@ -92,11 +151,8 @@ let useMph = true, maxSpeed = 0; let lastPos = null, totalDist = 0; let mediaRecorder = null, recordedChunks = []; let recInterval = null, recSeconds = 0, isRecording = false; - -// SRT state -let srtEntries = []; -let srtInterval = null; -let recStartTime = null; +let speedoOpen = false; +let srtEntries = [], recStartTime = null; let currentGPS = { lat: null, lng: null, alt: null, acc: null, hdg: null, speed: 0 }; function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; } @@ -114,7 +170,7 @@ function bearingLabel(deg) { return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0'; } -function pad(v, n=2) { return String(Math.floor(v)).padStart(n,'0'); } +function pad(v) { return String(Math.floor(v)).padStart(2,'0'); } function pad3(v) { return String(Math.floor(v)).padStart(3,'0'); } function updateClock() { @@ -127,57 +183,61 @@ function updateClock() { function setStatus(msg) { document.getElementById('status-bar').textContent = msg; } -// Format ms offset as SRT timecode HH:MM:SS,mmm +function openSpeedo() { + if (isRecording) return; + speedoOpen = true; + document.getElementById('speedo-overlay').classList.add('visible'); + syncBigSpeedo(); +} + +function closeSpeedo() { + speedoOpen = false; + document.getElementById('speedo-overlay').classList.remove('visible'); +} + +function syncBigSpeedo() { + if (!speedoOpen) return; + const unit = useMph ? 'MPH' : 'KPH'; + document.getElementById('big-speed-val').textContent = document.getElementById('speed-val').textContent; + document.getElementById('big-speed-unit').textContent = unit; + document.getElementById('b-lat').textContent = document.getElementById('lat-val').textContent; + document.getElementById('b-lng').textContent = document.getElementById('lng-val').textContent; + document.getElementById('b-alt').textContent = document.getElementById('alt-val').textContent; + document.getElementById('b-acc').textContent = document.getElementById('acc-val').textContent; + document.getElementById('b-hdg').textContent = document.getElementById('hdg-val').textContent; + document.getElementById('b-dist').textContent = document.getElementById('dist-val').textContent; +} + function toSRTTime(ms) { - const h = Math.floor(ms / 3600000); - const m = Math.floor((ms % 3600000) / 60000); - const s = Math.floor((ms % 60000) / 1000); - const mil = ms % 1000; + const h = Math.floor(ms/3600000), m = Math.floor((ms%3600000)/60000); + const s = Math.floor((ms%60000)/1000), mil = ms%1000; return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil); } -// Capture one SRT frame every second during recording function captureSRTFrame() { const now = new Date(); const elapsed = Date.now() - recStartTime; const dateStr = now.getFullYear()+'-'+pad(now.getMonth()+1)+'-'+pad(now.getDate()); const timeStr = pad(now.getHours())+':'+pad(now.getMinutes())+':'+pad(now.getSeconds()); const spd = currentGPS.speed !== null ? Math.round(cvt(currentGPS.speed)) : '--'; - const lat = currentGPS.lat !== null ? currentGPS.lat.toFixed(6) : '--'; - const lng = currentGPS.lng !== null ? currentGPS.lng.toFixed(6) : '--'; - const alt = currentGPS.alt !== null ? Math.round(currentGPS.alt)+'m' : '--'; - const acc = currentGPS.acc !== null ? Math.round(currentGPS.acc)+'m' : '--'; - const hdg = currentGPS.hdg !== null ? bearingLabel(currentGPS.hdg) : '--'; - srtEntries.push({ - startMs: elapsed, - endMs: elapsed + 1000, + startMs: elapsed, endMs: elapsed + 1000, lines: [ dateStr + ' ' + timeStr, - 'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + hdg, - 'Lat: ' + lat + ' Lng: ' + lng, - 'Alt: ' + alt + ' Acc: ' + acc + 'Speed: ' + spd + ' ' + cvtLabel() + ' Hdg: ' + bearingLabel(currentGPS.hdg), + 'Lat: ' + (currentGPS.lat !== null ? currentGPS.lat.toFixed(6) : '--') + ' Lng: ' + (currentGPS.lng !== null ? currentGPS.lng.toFixed(6) : '--'), + 'Alt: ' + (currentGPS.alt !== null ? Math.round(currentGPS.alt)+'m' : '--') + ' Acc: ' + (currentGPS.acc !== null ? Math.round(currentGPS.acc)+'m' : '--') ] }); } function buildSRT() { - return srtEntries.map((e, i) => { - return (i+1) + '\n' + - toSRTTime(e.startMs) + ' --> ' + toSRTTime(e.endMs) + '\n' + - e.lines.join('\n'); - }).join('\n\n') + '\n'; + return srtEntries.map((e,i) => (i+1)+'\n'+toSRTTime(e.startMs)+' --> '+toSRTTime(e.endMs)+'\n'+e.lines.join('\n')).join('\n\n')+'\n'; } -function saveSRT(basename) { - if (srtEntries.length === 0) return; - const blob = new Blob([buildSRT()], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = basename + '.srt'; - a.click(); - setTimeout(() => URL.revokeObjectURL(url), 5000); +function getFilename() { + const n = new Date(); + return 'dashcam_'+n.getFullYear()+pad(n.getMonth()+1)+pad(n.getDate())+'_'+pad(n.getHours())+pad(n.getMinutes())+pad(n.getSeconds()); } function getBestMimeType() { @@ -186,58 +246,41 @@ function getBestMimeType() { return ''; } -function getFilename() { - const n = new Date(); - return 'dashcam_' + n.getFullYear() + pad(n.getMonth()+1) + pad(n.getDate()) + '_' + pad(n.getHours()) + pad(n.getMinutes()) + pad(n.getSeconds()); -} - -function saveVideo(basename) { - if (recordedChunks.length === 0) { setStatus('Nothing to save'); return; } - const mime = getBestMimeType(); - const ext = mime.includes('mp4') ? 'mp4' : 'webm'; - const blob = new Blob(recordedChunks, { type: mime }); +function dl(filename, blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; - a.download = basename + '.' + ext; - a.click(); + a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); - recordedChunks = []; } function toggleRecord() { if (!stream) return; const btn = document.getElementById('rec-btn'); const ind = document.getElementById('rec-indicator'); - if (!isRecording) { - recordedChunks = []; - srtEntries = []; - recStartTime = Date.now(); + recordedChunks = []; srtEntries = []; recStartTime = Date.now(); const mime = getBestMimeType(); - try { - mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {}); - } catch(e) { - mediaRecorder = new MediaRecorder(stream); - } + try { mediaRecorder = new MediaRecorder(stream, mime ? { mimeType: mime } : {}); } + catch(e) { mediaRecorder = new MediaRecorder(stream); } mediaRecorder.ondataavailable = e => { if (e.data && e.data.size > 0) recordedChunks.push(e.data); }; mediaRecorder.onstop = () => { const name = getFilename(); - saveVideo(name); - setTimeout(() => saveSRT(name), 800); + const mime2 = getBestMimeType(); + const ext = mime2.includes('mp4') ? 'mp4' : 'webm'; + dl(name+'.'+ext, new Blob(recordedChunks, { type: mime2 })); + setTimeout(() => dl(name+'.srt', new Blob([buildSRT()], { type: 'text/plain' })), 800); + recordedChunks = []; setStatus('Saved video + SRT'); }; mediaRecorder.start(1000); - isRecording = true; - recSeconds = 0; + isRecording = true; recSeconds = 0; ind.style.display = 'flex'; btn.innerHTML = '▮▮ SAVE'; btn.classList.add('danger'); captureSRTFrame(); recInterval = setInterval(() => { recSeconds++; - const m = pad(Math.floor(recSeconds/60)), s = pad(recSeconds%60); - document.getElementById('rec-timer').textContent = m+':'+s; + document.getElementById('rec-timer').textContent = pad(Math.floor(recSeconds/60))+':'+pad(recSeconds%60); captureSRTFrame(); }, 1000); setStatus('Recording...'); @@ -254,7 +297,7 @@ function toggleRecord() { function startGPS() { if (!navigator.geolocation) { setStatus('GPS unavailable'); return; } - document.getElementById('gps-bar').style.display = 'flex'; + document.getElementById('gps-bar').style.display = 'block'; watchId = navigator.geolocation.watchPosition(pos => { const c = pos.coords; currentGPS = { lat: c.latitude, lng: c.longitude, alt: c.altitude, acc: c.accuracy, hdg: c.heading, speed: c.speed !== null ? c.speed : 0 }; @@ -264,7 +307,6 @@ function startGPS() { if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; } lastPos = cur; document.getElementById('speed-val').textContent = Math.round(conv); - document.getElementById('max-speed').textContent = 'MAX ' + Math.round(maxSpeed); document.getElementById('lat-val').textContent = c.latitude.toFixed(5); document.getElementById('lng-val').textContent = c.longitude.toFixed(5); document.getElementById('alt-val').textContent = c.altitude !== null ? Math.round(c.altitude)+'m' : '--'; @@ -272,6 +314,7 @@ function startGPS() { document.getElementById('hdg-val').textContent = bearingLabel(c.heading); document.getElementById('dist-val').textContent = (totalDist/1000).toFixed(2)+'km'; document.getElementById('gps-dot').className = 'active'; + syncBigSpeedo(); setStatus(''); }, err => { setStatus('GPS: ' + err.message); @@ -283,6 +326,7 @@ async function startDashcam() { const btn = document.getElementById('start-btn'); btn.disabled = true; btn.textContent = '...'; if (isRecording) toggleRecord(); + closeSpeedo(); try { if (stream) stream.getTracks().forEach(t => t.stop()); stream = await navigator.mediaDevices.getUserMedia({ @@ -308,15 +352,14 @@ async function startDashcam() { } } -function switchCamera() { - facingMode = facingMode === 'environment' ? 'user' : 'environment'; - startDashcam(); -} +function switchCamera() { facingMode = facingMode === 'environment' ? 'user' : 'environment'; startDashcam(); } function toggleUnit() { useMph = !useMph; maxSpeed = useMph ? maxSpeed / 3.6 * 2.23694 : maxSpeed / 2.23694 * 3.6; - document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH'; + const unit = useMph ? 'MPH' : 'KPH'; + document.getElementById('speed-unit').textContent = unit; + document.getElementById('big-speed-unit').textContent = unit; } function toggleFullscreen() { @@ -332,10 +375,7 @@ function toggleFullscreen() { btn.textContent = '\u26f6 FULL'; } document.addEventListener('fullscreenchange', () => { - if (!document.fullscreenElement) { - app.classList.remove('fullscreen'); - btn.textContent = '\u26f6 FULL'; - } + if (!document.fullscreenElement) { app.classList.remove('fullscreen'); btn.textContent = '\u26f6 FULL'; } }, { once: true }); }