Compare commits

..

3 Commits

Author SHA1 Message Date
Jon
4902f04491 Added recording option 2026-04-29 22:14:09 +01:00
Jon
eb624eef06 Added sensors for alt acc 2026-04-29 22:13:48 +01:00
Jon
1473e6e0fd Added full screen button 2026-04-29 22:13:10 +01:00

View File

@@ -1,175 +1,251 @@
<style> <style>
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
#app { font-family: var(--font-mono); padding: 1rem 0; } #app { font-family: var(--font-mono); background: #000; border-radius: var(--border-radius-lg); overflow: hidden; position: relative; width: 100%; height: 56vw; min-height: 300px; max-height: 92vh; }
#viewfinder { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: var(--border-radius-lg); overflow: hidden; } #app.fullscreen { border-radius: 0; height: 100vh; width: 100vw; position: fixed; inset: 0; z-index: 9999; }
#video { width: 100%; height: 100%; object-fit: cover; display: block; } #video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
#hud { position: absolute; inset: 0; pointer-events: none; } #hud { position: absolute; inset: 0; pointer-events: none; }
#speed-display { position: absolute; top: 12px; right: 12px; text-align: center; background: rgba(0,0,0,0.55); border-radius: 10px; padding: 6px 14px; } #top-left { position: absolute; top: 12px; left: 12px; display: none; flex-direction: column; gap: 3px; }
#speed-val { font-size: 38px; font-weight: 700; color: #fff; line-height: 1; } #clock { font-size: 16px; color: #fff; background: rgba(0,0,0,0.45); border-radius: 8px; padding: 4px 10px; letter-spacing: 1px; }
#speed-unit { font-size: 11px; color: #aaa; letter-spacing: 1px; margin-top: 2px; } #dateline { font-size: 10px; color: #aaa; background: rgba(0,0,0,0.45); border-radius: 8px; padding: 3px 10px; letter-spacing: 0.5px; }
#rec-badge { position: absolute; top: 12px; left: 12px; display: none; align-items: center; gap: 6px; background: rgba(0,0,0,0.55); border-radius: 20px; padding: 5px 12px; color: #fff; font-size: 12px; } #rec-indicator { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); background: rgba(180,0,0,0.75); border-radius: 20px; padding: 4px 12px; color: #fff; font-size: 11px; letter-spacing: 1px; display: none; align-items: center; gap: 6px; white-space: nowrap; }
#rec-dot { width: 8px; height: 8px; border-radius: 50%; background: #e24b4a; animation: blink 1s infinite; } #rec-dot { width: 7px; height: 7px; border-radius: 50%; background: #fff; animation: blink 1s infinite; }
#rec-timer { font-size: 11px; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
#clock { position: absolute; top: 44px; left: 12px; background: rgba(0,0,0,0.55); border-radius: 20px; padding: 4px 12px; color: #fff; font-size: 12px; display: none; } #speed-display { position: absolute; top: 12px; right: 12px; text-align: center; background: rgba(0,0,0,0.45); border-radius: 12px; padding: 6px 14px; }
#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: 0; left: 0; right: 0; background: rgba(0,0,0,0.6); padding: 6px 12px; display: none; flex-direction: column; gap: 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; }
#gps-row1 { display: flex; gap: 16px; align-items: center; } .data-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
#gps-row2 { display: flex; gap: 16px; align-items: center; } .di { font-size: 10px; color: #ccc; display: flex; gap: 4px; align-items: center; }
.gps-item { font-size: 11px; color: #ccc; display: flex; gap: 5px; align-items: center; } .dl { color: #666; font-size: 9px; letter-spacing: 0.4px; }
.gps-label { color: #888; font-size: 10px; letter-spacing: 0.5px; } .dv { color: #fff; font-weight: 500; }
.gps-val { color: #fff; font-weight: 500; } #gps-dot { width: 6px; height: 6px; border-radius: 50%; background: #555; flex-shrink: 0; }
#gps-dot { width: 7px; height: 7px; border-radius: 50%; background: #888; flex-shrink: 0; } #gps-dot.active { background: #1D9E75; animation: gpsblink 2s infinite; }
#gps-dot.active { background: #1D9E75; animation: blink 2s infinite; } @keyframes gpsblink { 0%,100%{opacity:1} 50%{opacity:0.4} }
#placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; font-size: 14px; gap: 8px; font-family: var(--font-sans); } #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; }
#controls { display: flex; gap: 10px; margin-top: 12px; } #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; }
#start-btn { flex: 1; padding: 10px; border-radius: var(--border-radius-md); border: none; background: #1D9E75; color: #fff; font-size: 14px; font-weight: 500; cursor: pointer; }
#start-btn:hover { background: #0F6E56; }
#switch-btn { padding: 10px 16px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-secondary); background: var(--color-background-secondary); color: var(--color-text-primary); font-size: 14px; cursor: pointer; }
#switch-btn:disabled { opacity: 0.4; cursor: default; }
#unit-btn { padding: 10px 16px; border-radius: var(--border-radius-md); border: 0.5px solid var(--color-border-secondary); background: var(--color-background-secondary); color: var(--color-text-primary); font-size: 13px; cursor: pointer; }
#status { font-size: 12px; color: var(--color-text-secondary); margin-top: 8px; font-family: var(--font-sans); min-height: 18px; } #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; }
.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; }
.hud-btn.primary { background: rgba(29,158,117,0.7); border-color: #1D9E75; }
.hud-btn.primary:hover { background: rgba(29,158,117,0.9); }
.hud-btn.danger { background: rgba(180,0,0,0.7); border-color: rgba(220,0,0,0.9); }
.hud-btn.danger:hover { background: rgba(220,0,0,0.85); }
</style> </style>
<div id="app"> <div id="app">
<div id="viewfinder">
<video id="video" autoplay playsinline muted></video> <video id="video" autoplay playsinline muted></video>
<div id="placeholder">
<div style="font-size:40px">&#127909;</div>
<div style="color:#aaa;font-family:var(--font-sans)">Tap START to begin</div>
<div style="font-size:11px;color:#555;font-family:var(--font-sans)">Camera + location permissions required</div>
</div>
<div id="hud"> <div id="hud">
<div id="rec-badge"><div id="rec-dot"></div> REC</div> <div id="top-left">
<div id="clock"></div> <div id="clock">--:--:--</div>
<div id="dateline">---</div>
</div>
<div id="rec-indicator">
<div id="rec-dot"></div>
<span>REC</span>
<span id="rec-timer">00:00</span>
</div>
<div id="speed-display"> <div id="speed-display">
<div id="speed-val">0</div> <div id="speed-val">--</div>
<div id="speed-unit">MPH</div> <div id="speed-unit">MPH</div>
<div id="max-speed">MAX --</div>
</div> </div>
<div id="gps-bar"> <div id="gps-bar">
<div id="gps-row1"> <div class="data-row">
<div id="gps-dot"></div> <div id="gps-dot"></div>
<div class="gps-item"><span class="gps-label">LAT</span><span class="gps-val" id="lat-val">--</span></div> <div class="di"><span class="dl">LAT</span><span class="dv" id="lat-val">--</span></div>
<div class="gps-item"><span class="gps-label">LNG</span><span class="gps-val" id="lng-val">--</span></div> <div class="di"><span class="dl">LNG</span><span class="dv" id="lng-val">--</span></div>
<div class="gps-item"><span class="gps-label">ALT</span><span class="gps-val" id="alt-val">--</span></div> <div class="di"><span class="dl">ALT</span><span class="dv" id="alt-val">--</span></div>
</div> <div class="di"><span class="dl">ACC</span><span class="dv" id="acc-val">--</span></div>
<div id="gps-row2"> <div class="di"><span class="dl">HDG</span><span class="dv" id="hdg-val">--</span></div>
<div class="gps-item"><span class="gps-label">ACC</span><span class="gps-val" id="acc-val">--</span></div> <div class="di"><span class="dl">DIST</span><span class="dv" id="dist-val">0.00km</span></div>
<div class="gps-item"><span class="gps-label">HDG</span><span class="gps-val" id="hdg-val">--</span></div>
<div class="gps-item"><span class="gps-label">MAX</span><span class="gps-val" id="max-val">0</span></div>
</div>
</div>
</div>
<div id="placeholder">
<div style="font-size:36px">&#127909;</div>
<div>Tap "Start" to begin</div>
<div style="font-size:12px;color:#666">Camera + GPS permissions needed</div>
</div> </div>
</div> </div>
<div id="controls"> <div id="status-bar"></div>
<button id="start-btn" onclick="startDashcam()">Start</button>
<button id="switch-btn" onclick="switchCamera()" disabled>Flip</button> <div id="btn-bar">
<button id="unit-btn" onclick="toggleUnit()">MPH/KPH</button> <button class="hud-btn primary" id="start-btn" onclick="startDashcam()">START</button>
<button class="hud-btn" id="flip-btn" onclick="switchCamera()" disabled>FLIP</button>
<button class="hud-btn" id="rec-btn" onclick="toggleRecord()" disabled>&#9679; RECORD</button>
<button class="hud-btn" id="unit-btn" onclick="toggleUnit()">MPH/KPH</button>
<button class="hud-btn" id="fs-btn" onclick="toggleFullscreen()">&#x26F6; FULL</button>
</div>
</div> </div>
<div id="status">Ready</div>
</div> </div>
<script> <script>
let stream = null; let stream = null, facingMode = 'environment';
let facingMode = 'environment'; let watchId = null, clockInterval = null;
let watchId = null; let useMph = true, maxSpeed = 0;
let useMph = true; let lastPos = null, totalDist = 0;
let maxSpeed = 0; let mediaRecorder = null, recordedChunks = [], recInterval = null, recSeconds = 0;
let gpsActive = false; let isRecording = false;
function toMph(ms) { return ms * 2.23694; } function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; }
function toKph(ms) { return ms * 3.6; }
function convertSpeed(ms) { function haversine(a, b) {
return useMph ? toMph(ms) : toKph(ms); const R = 6371000, r = Math.PI / 180;
const dLat = (b.lat - a.lat) * r, dLng = (b.lng - a.lng) * r;
const x = Math.sin(dLat/2)**2 + Math.cos(a.lat*r)*Math.cos(b.lat*r)*Math.sin(dLng/2)**2;
return R * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x));
} }
function bearing(deg) { function bearingLabel(deg) {
if (deg === null || deg === undefined) return '--'; if (deg === null || isNaN(deg)) return '--';
const dirs = ['N','NE','E','SE','S','SW','W','NW']; return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0';
return dirs[Math.round(deg / 45) % 8] + ' ' + Math.round(deg) + '°';
} }
function pad(v) { return String(v).padStart(2,'0'); }
function updateClock() { function updateClock() {
const now = new Date(); const n = new Date();
const h = String(now.getHours()).padStart(2,'0'); document.getElementById('clock').textContent = pad(n.getHours())+':'+pad(n.getMinutes())+':'+pad(n.getSeconds());
const m = String(now.getMinutes()).padStart(2,'0'); const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const s = String(now.getSeconds()).padStart(2,'0'); const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
document.getElementById('clock').textContent = h + ':' + m + ':' + s; document.getElementById('dateline').textContent = days[n.getDay()]+' '+pad(n.getDate())+' '+months[n.getMonth()]+' '+n.getFullYear();
} }
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
function startGPS() { function startGPS() {
if (!navigator.geolocation) { if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
document.getElementById('status').textContent = 'GPS not available in this browser';
return;
}
document.getElementById('gps-bar').style.display = 'flex'; document.getElementById('gps-bar').style.display = 'flex';
watchId = navigator.geolocation.watchPosition(pos => { watchId = navigator.geolocation.watchPosition(pos => {
gpsActive = true;
document.getElementById('gps-dot').className = 'active';
const c = pos.coords; const c = pos.coords;
const spd = c.speed !== null ? c.speed : 0; const spd = c.speed !== null && c.speed >= 0 ? c.speed : 0;
const converted = convertSpeed(spd); const conv = cvt(spd);
if (converted > maxSpeed) maxSpeed = converted; if (conv > maxSpeed) maxSpeed = conv;
const cur = { lat: c.latitude, lng: c.longitude };
document.getElementById('speed-val').textContent = Math.round(converted); if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; }
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH'; 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('lat-val').textContent = c.latitude.toFixed(5);
document.getElementById('lng-val').textContent = c.longitude.toFixed(5); document.getElementById('lng-val').textContent = c.longitude.toFixed(5);
document.getElementById('alt-val').textContent = c.altitude !== null ? Math.round(c.altitude) + 'm' : '--'; document.getElementById('alt-val').textContent = c.altitude !== null ? Math.round(c.altitude)+'m' : '--';
document.getElementById('acc-val').textContent = Math.round(c.accuracy) + 'm'; document.getElementById('acc-val').textContent = Math.round(c.accuracy)+'m';
document.getElementById('hdg-val').textContent = bearing(c.heading); document.getElementById('hdg-val').textContent = bearingLabel(c.heading);
document.getElementById('max-val').textContent = Math.round(maxSpeed); document.getElementById('dist-val').textContent = (totalDist/1000).toFixed(2)+'km';
document.getElementById('status').textContent = 'GPS locked'; document.getElementById('gps-dot').className = 'active';
setStatus('');
}, err => { }, err => {
document.getElementById('status').textContent = 'GPS: ' + err.message; setStatus('GPS: ' + err.message);
document.getElementById('gps-dot').className = ''; document.getElementById('gps-dot').className = '';
}, { }, { enableHighAccuracy: true, maximumAge: 1000, timeout: 15000 });
enableHighAccuracy: true, }
maximumAge: 1000,
timeout: 10000 function getBestMimeType() {
}); const types = [
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
'video/mp4'
];
for (const t of types) { if (MediaRecorder.isTypeSupported(t)) return t; }
return '';
}
function saveRecording() {
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 });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const now = new Date();
a.href = url;
a.download = 'dashcam_' + now.getFullYear() + pad(now.getMonth()+1) + pad(now.getDate()) + '_' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds()) + '.' + ext;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 5000);
recordedChunks = [];
setStatus('Saved!');
}
function toggleRecord() {
if (!stream) return;
const btn = document.getElementById('rec-btn');
const ind = document.getElementById('rec-indicator');
if (!isRecording) {
recordedChunks = [];
const mime = getBestMimeType();
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 = saveRecording;
mediaRecorder.start(1000);
isRecording = true;
recSeconds = 0;
ind.style.display = 'flex';
btn.textContent = '&#9646;&#9646; SAVE';
btn.innerHTML = '&#9646;&#9646; SAVE';
btn.classList.add('danger');
recInterval = setInterval(() => {
recSeconds++;
const m = pad(Math.floor(recSeconds/60)), s = pad(recSeconds%60);
document.getElementById('rec-timer').textContent = m+':'+s;
}, 1000);
setStatus('Recording...');
} else {
mediaRecorder.stop();
isRecording = false;
clearInterval(recInterval);
ind.style.display = 'none';
btn.innerHTML = '&#9679; RECORD';
btn.classList.remove('danger');
setStatus('Saving...');
}
} }
async function startDashcam() { async function startDashcam() {
const btn = document.getElementById('start-btn'); const btn = document.getElementById('start-btn');
btn.disabled = true; btn.disabled = true; btn.textContent = '...';
btn.textContent = 'Starting...'; if (isRecording) toggleRecord();
try { try {
if (stream) stream.getTracks().forEach(t => t.stop()); if (stream) stream.getTracks().forEach(t => t.stop());
stream = await navigator.mediaDevices.getUserMedia({ stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: facingMode, width: { ideal: 1280 }, height: { ideal: 720 } }, video: { facingMode, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false
audio: false
}); });
document.getElementById('video').srcObject = stream; document.getElementById('video').srcObject = stream;
document.getElementById('placeholder').style.display = 'none'; document.getElementById('placeholder').style.display = 'none';
document.getElementById('rec-badge').style.display = 'flex'; document.getElementById('top-left').style.display = 'flex';
document.getElementById('clock').style.display = 'block'; document.getElementById('flip-btn').disabled = false;
document.getElementById('switch-btn').disabled = false; document.getElementById('rec-btn').disabled = false;
btn.textContent = 'Restart'; btn.textContent = 'RESTART'; btn.disabled = false;
btn.disabled = false; if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }
document.getElementById('status').textContent = 'Camera active — requesting GPS...'; maxSpeed = 0; totalDist = 0; lastPos = null;
if (watchId !== null) navigator.geolocation.clearWatch(watchId);
maxSpeed = 0;
startGPS(); startGPS();
setInterval(updateClock, 1000); if (clockInterval) clearInterval(clockInterval);
clockInterval = setInterval(updateClock, 1000);
updateClock(); updateClock();
setStatus('Acquiring GPS...');
} catch(e) { } catch(e) {
document.getElementById('status').textContent = 'Camera error: ' + e.message; setStatus('Camera: ' + e.message);
btn.textContent = 'Start'; btn.textContent = 'START'; btn.disabled = false;
btn.disabled = false;
} }
} }
@@ -180,8 +256,27 @@ function switchCamera() {
function toggleUnit() { function toggleUnit() {
useMph = !useMph; useMph = !useMph;
maxSpeed = useMph ? maxSpeed * (2.23694 / 3.6) : maxSpeed * (3.6 / 2.23694); maxSpeed = useMph ? maxSpeed / 3.6 * 2.23694 : maxSpeed / 2.23694 * 3.6;
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH'; document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH';
document.getElementById('max-val').textContent = Math.round(maxSpeed); }
function toggleFullscreen() {
const app = document.getElementById('app');
const btn = document.getElementById('fs-btn');
if (!app.classList.contains('fullscreen')) {
if (app.requestFullscreen) app.requestFullscreen().catch(()=>{});
app.classList.add('fullscreen');
btn.textContent = '\u2715 EXIT';
} else {
if (document.exitFullscreen && document.fullscreenElement) document.exitFullscreen().catch(()=>{});
app.classList.remove('fullscreen');
btn.textContent = '\u26f6 FULL';
}
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
app.classList.remove('fullscreen');
btn.textContent = '\u26f6 FULL';
}
}, { once: true });
} }
</script> </script>