Added full screen button
This commit is contained in:
207
dashcam.html
207
dashcam.html
@@ -1,54 +1,58 @@
|
|||||||
|
|
||||||
<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; }
|
#speed-display { position: absolute; top: 14px; right: 14px; text-align: center; background: rgba(0,0,0,0.45); border-radius: 12px; padding: 8px 16px; pointer-events: none; }
|
||||||
#speed-val { font-size: 38px; font-weight: 700; color: #fff; line-height: 1; }
|
#speed-val { font-size: 48px; font-weight: 700; color: #fff; line-height: 1; }
|
||||||
#speed-unit { font-size: 11px; color: #aaa; letter-spacing: 1px; margin-top: 2px; }
|
#speed-unit { font-size: 11px; color: #aaa; letter-spacing: 1.5px; margin-top: 2px; }
|
||||||
|
#max-speed { font-size: 10px; color: #888; margin-top: 3px; 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; }
|
#clock { position: absolute; top: 14px; left: 14px; background: rgba(0,0,0,0.45); border-radius: 20px; padding: 5px 12px; color: #fff; font-size: 13px; display: none; }
|
||||||
#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: 52px; left: 0; right: 0; background: rgba(0,0,0,0.5); padding: 5px 14px; display: none; flex-direction: column; gap: 3px; }
|
||||||
|
#gps-row1, #gps-row2 { display: flex; gap: 18px; align-items: center; }
|
||||||
#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-item { font-size: 10px; color: #ccc; display: flex; gap: 4px; align-items: center; }
|
||||||
#gps-row1 { display: flex; gap: 16px; align-items: center; }
|
.gps-label { color: #777; font-size: 9px; letter-spacing: 0.5px; }
|
||||||
#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-val { color: #fff; font-weight: 500; }
|
||||||
#gps-dot { width: 7px; height: 7px; border-radius: 50%; background: #888; flex-shrink: 0; }
|
#gps-dot { width: 6px; height: 6px; border-radius: 50%; background: #555; flex-shrink: 0; }
|
||||||
#gps-dot.active { background: #1D9E75; animation: blink 2s infinite; }
|
#gps-dot.active { background: #1D9E75; animation: gpsblink 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); }
|
#placeholder { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #888; font-size: 14px; gap: 10px; font-family: var(--font-sans); background: #000; }
|
||||||
|
|
||||||
#controls { display: flex; gap: 10px; margin-top: 12px; }
|
#btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 20px; padding: 8px 16px; background: rgba(0,0,0,0.45); pointer-events: all; }
|
||||||
#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; }
|
.hud-btn { background: rgba(255,255,255,0.12); border: 0.5px solid rgba(255,255,255,0.2); color: #fff; border-radius: 20px; padding: 5px 14px; font-size: 11px; font-family: var(--font-sans); cursor: pointer; letter-spacing: 0.5px; transition: background 0.15s; }
|
||||||
|
.hud-btn:hover { background: rgba(255,255,255,0.22); }
|
||||||
|
.hud-btn:disabled { opacity: 0.3; cursor: default; }
|
||||||
|
.hud-btn.primary { background: rgba(29,158,117,0.7); border-color: rgba(29,158,117,0.9); }
|
||||||
|
.hud-btn.primary:hover { background: rgba(29,158,117,0.9); }
|
||||||
|
|
||||||
|
#status-bar { position: absolute; bottom: 40px; left: 14px; font-size: 10px; color: rgba(255,255,255,0.5); font-family: var(--font-sans); pointer-events: none; display: none; }
|
||||||
</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:42px">🎥</div>
|
||||||
|
<div style="color:#aaa">Tap Start to begin</div>
|
||||||
|
<div style="font-size:11px;color:#555">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="clock"></div>
|
<div id="clock"></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">
|
||||||
@@ -61,24 +65,19 @@
|
|||||||
<div id="gps-row2">
|
<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">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">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 class="gps-item"><span class="gps-label">DIST</span><span class="gps-val" id="dist-val">0.0km</span></div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="placeholder">
|
|
||||||
<div style="font-size:36px">🎥</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" id="status"></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="unit-btn" onclick="toggleUnit()">MPH/KPH</button>
|
||||||
|
<button class="hud-btn" id="fs-btn" onclick="toggleFullscreen()">⛶ FULL</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="status">Ready</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -87,88 +86,98 @@ let facingMode = 'environment';
|
|||||||
let watchId = null;
|
let watchId = null;
|
||||||
let useMph = true;
|
let useMph = true;
|
||||||
let maxSpeed = 0;
|
let maxSpeed = 0;
|
||||||
let gpsActive = false;
|
let lastPos = null;
|
||||||
|
let totalDist = 0;
|
||||||
|
let clockInterval = null;
|
||||||
|
|
||||||
function toMph(ms) { return ms * 2.23694; }
|
function ms2mph(ms){ return ms * 2.23694; }
|
||||||
function toKph(ms) { return ms * 3.6; }
|
function ms2kph(ms){ return ms * 3.6; }
|
||||||
|
function cvt(ms){ return useMph ? ms2mph(ms) : ms2kph(ms); }
|
||||||
|
|
||||||
function convertSpeed(ms) {
|
function haversine(a, b) {
|
||||||
return useMph ? toMph(ms) : toKph(ms);
|
const R = 6371000;
|
||||||
|
const dLat = (b.lat - a.lat) * Math.PI / 180;
|
||||||
|
const dLng = (b.lng - a.lng) * Math.PI / 180;
|
||||||
|
const x = Math.sin(dLat/2)**2 + Math.cos(a.lat*Math.PI/180)*Math.cos(b.lat*Math.PI/180)*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 || deg === undefined || isNaN(deg)) return '--';
|
||||||
const dirs = ['N','NE','E','SE','S','SW','W','NW'];
|
const dirs = ['N','NE','E','SE','S','SW','W','NW'];
|
||||||
return dirs[Math.round(deg / 45) % 8] + ' ' + Math.round(deg) + '°';
|
return dirs[Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
const now = new Date();
|
const n = new Date();
|
||||||
const h = String(now.getHours()).padStart(2,'0');
|
const pad = v => String(v).padStart(2,'0');
|
||||||
const m = String(now.getMinutes()).padStart(2,'0');
|
document.getElementById('clock').textContent = pad(n.getHours())+':'+pad(n.getMinutes())+':'+pad(n.getSeconds());
|
||||||
const s = String(now.getSeconds()).padStart(2,'0');
|
}
|
||||||
document.getElementById('clock').textContent = h + ':' + m + ':' + s;
|
|
||||||
|
function setStatus(msg) {
|
||||||
|
const el = document.getElementById('status-bar');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.display = msg ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
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 converted = cvt(spd);
|
||||||
if (converted > maxSpeed) maxSpeed = converted;
|
if (converted > maxSpeed) maxSpeed = converted;
|
||||||
|
|
||||||
|
const cur = { lat: c.latitude, lng: c.longitude };
|
||||||
|
if (lastPos) {
|
||||||
|
const d = haversine(lastPos, cur);
|
||||||
|
if (d < 200) totalDist += d;
|
||||||
|
}
|
||||||
|
lastPos = cur;
|
||||||
|
|
||||||
document.getElementById('speed-val').textContent = Math.round(converted);
|
document.getElementById('speed-val').textContent = Math.round(converted);
|
||||||
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH';
|
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 'Starting...';
|
btn.textContent = '...';
|
||||||
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: 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('clock').style.display = 'block';
|
document.getElementById('clock').style.display = 'block';
|
||||||
document.getElementById('switch-btn').disabled = false;
|
document.getElementById('flip-btn').disabled = false;
|
||||||
btn.textContent = 'Restart';
|
btn.textContent = 'RESTART';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
document.getElementById('status').textContent = 'Camera active — requesting GPS...';
|
if (watchId !== null) { navigator.geolocation.clearWatch(watchId); watchId = null; }
|
||||||
if (watchId !== null) navigator.geolocation.clearWatch(watchId);
|
maxSpeed = 0; totalDist = 0; lastPos = null;
|
||||||
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 +189,32 @@ 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');
|
||||||
|
const isFs = app.classList.contains('fullscreen');
|
||||||
|
if (!isFs) {
|
||||||
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user