Files
web-dashcam/dashcam.html
2026-04-29 22:12:49 +01:00

188 lines
8.4 KiB
HTML

<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
#app { font-family: var(--font-mono); padding: 1rem 0; }
#viewfinder { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: var(--border-radius-lg); overflow: hidden; }
#video { width: 100%; height: 100%; object-fit: cover; display: block; }
#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; }
#speed-val { font-size: 38px; font-weight: 700; color: #fff; line-height: 1; }
#speed-unit { font-size: 11px; color: #aaa; letter-spacing: 1px; margin-top: 2px; }
#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-dot { width: 8px; height: 8px; border-radius: 50%; background: #e24b4a; animation: blink 1s infinite; }
@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; }
#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-row1 { display: flex; gap: 16px; align-items: center; }
#gps-row2 { display: flex; gap: 16px; align-items: center; }
.gps-item { font-size: 11px; color: #ccc; display: flex; gap: 5px; align-items: center; }
.gps-label { color: #888; font-size: 10px; letter-spacing: 0.5px; }
.gps-val { color: #fff; font-weight: 500; }
#gps-dot { width: 7px; height: 7px; border-radius: 50%; background: #888; flex-shrink: 0; }
#gps-dot.active { background: #1D9E75; animation: blink 2s infinite; }
#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); }
#controls { display: flex; gap: 10px; margin-top: 12px; }
#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; }
</style>
<div id="app">
<div id="viewfinder">
<video id="video" autoplay playsinline muted></video>
<div id="hud">
<div id="rec-badge"><div id="rec-dot"></div> REC</div>
<div id="clock"></div>
<div id="speed-display">
<div id="speed-val">0</div>
<div id="speed-unit">MPH</div>
</div>
<div id="gps-bar">
<div id="gps-row1">
<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="gps-item"><span class="gps-label">LNG</span><span class="gps-val" 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>
<div id="gps-row2">
<div class="gps-item"><span class="gps-label">ACC</span><span class="gps-val" id="acc-val">--</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 id="controls">
<button id="start-btn" onclick="startDashcam()">Start</button>
<button id="switch-btn" onclick="switchCamera()" disabled>Flip</button>
<button id="unit-btn" onclick="toggleUnit()">MPH/KPH</button>
</div>
<div id="status">Ready</div>
</div>
<script>
let stream = null;
let facingMode = 'environment';
let watchId = null;
let useMph = true;
let maxSpeed = 0;
let gpsActive = false;
function toMph(ms) { return ms * 2.23694; }
function toKph(ms) { return ms * 3.6; }
function convertSpeed(ms) {
return useMph ? toMph(ms) : toKph(ms);
}
function bearing(deg) {
if (deg === null || deg === undefined) return '--';
const dirs = ['N','NE','E','SE','S','SW','W','NW'];
return dirs[Math.round(deg / 45) % 8] + ' ' + Math.round(deg) + '°';
}
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2,'0');
const m = String(now.getMinutes()).padStart(2,'0');
const s = String(now.getSeconds()).padStart(2,'0');
document.getElementById('clock').textContent = h + ':' + m + ':' + s;
}
function startGPS() {
if (!navigator.geolocation) {
document.getElementById('status').textContent = 'GPS not available in this browser';
return;
}
document.getElementById('gps-bar').style.display = 'flex';
watchId = navigator.geolocation.watchPosition(pos => {
gpsActive = true;
document.getElementById('gps-dot').className = 'active';
const c = pos.coords;
const spd = c.speed !== null ? c.speed : 0;
const converted = convertSpeed(spd);
if (converted > maxSpeed) maxSpeed = converted;
document.getElementById('speed-val').textContent = Math.round(converted);
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH';
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' : '--';
document.getElementById('acc-val').textContent = Math.round(c.accuracy) + 'm';
document.getElementById('hdg-val').textContent = bearing(c.heading);
document.getElementById('max-val').textContent = Math.round(maxSpeed);
document.getElementById('status').textContent = 'GPS locked';
}, err => {
document.getElementById('status').textContent = 'GPS: ' + err.message;
document.getElementById('gps-dot').className = '';
}, {
enableHighAccuracy: true,
maximumAge: 1000,
timeout: 10000
});
}
async function startDashcam() {
const btn = document.getElementById('start-btn');
btn.disabled = true;
btn.textContent = 'Starting...';
try {
if (stream) stream.getTracks().forEach(t => t.stop());
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: facingMode, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false
});
document.getElementById('video').srcObject = stream;
document.getElementById('placeholder').style.display = 'none';
document.getElementById('rec-badge').style.display = 'flex';
document.getElementById('clock').style.display = 'block';
document.getElementById('switch-btn').disabled = false;
btn.textContent = 'Restart';
btn.disabled = false;
document.getElementById('status').textContent = 'Camera active — requesting GPS...';
if (watchId !== null) navigator.geolocation.clearWatch(watchId);
maxSpeed = 0;
startGPS();
setInterval(updateClock, 1000);
updateClock();
} catch(e) {
document.getElementById('status').textContent = 'Camera error: ' + e.message;
btn.textContent = 'Start';
btn.disabled = false;
}
}
function switchCamera() {
facingMode = facingMode === 'environment' ? 'user' : 'environment';
startDashcam();
}
function toggleUnit() {
useMph = !useMph;
maxSpeed = useMph ? maxSpeed * (2.23694 / 3.6) : maxSpeed * (3.6 / 2.23694);
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH';
document.getElementById('max-val').textContent = Math.round(maxSpeed);
}
</script>