Compare commits
6 Commits
4902f04491
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f2346b28e | ||
|
|
3abd43bdfe | ||
|
|
ac7180ad8f | ||
|
|
e85b0fc51c | ||
|
|
a58177c8cd | ||
|
|
7ce94f78c4 |
358
dashcam.html
358
dashcam.html
@@ -12,39 +12,60 @@
|
|||||||
|
|
||||||
#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-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: 7px; height: 7px; border-radius: 50%; background: #fff; 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} }
|
||||||
|
|
||||||
#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-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; }
|
||||||
|
#speed-display:hover { background: rgba(255,255,255,0.12); }
|
||||||
#speed-val { font-size: 44px; font-weight: 700; color: #fff; line-height: 1; }
|
#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; }
|
#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; }
|
/* flash effect on snapshot */
|
||||||
.data-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
|
#flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; z-index: 30; transition: opacity 0.05s; }
|
||||||
.di { font-size: 10px; color: #ccc; display: flex; gap: 4px; align-items: center; }
|
#flash.on { opacity: 0.85; }
|
||||||
.dl { color: #666; font-size: 9px; letter-spacing: 0.4px; }
|
|
||||||
|
/* big speedo */
|
||||||
|
#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; }
|
||||||
|
|
||||||
|
/* info bar */
|
||||||
|
#gps-bar { position: absolute; bottom: 62px; left: 0; right: 0; background: rgba(0,0,0,0.55); padding: 7px 16px; display: none; }
|
||||||
|
.data-row { display: flex; gap: 21px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.di { font-size: 13px; color: #ccc; display: flex; gap: 5px; align-items: center; }
|
||||||
|
.dl { color: #666; font-size: 12px; letter-spacing: 0.4px; }
|
||||||
.dv { color: #fff; font-weight: 500; }
|
.dv { color: #fff; font-weight: 500; }
|
||||||
#gps-dot { width: 6px; height: 6px; border-radius: 50%; background: #555; flex-shrink: 0; }
|
#gps-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; flex-shrink: 0; }
|
||||||
#gps-dot.active { background: #1D9E75; animation: gpsblink 2s infinite; }
|
#gps-dot.active { background: #1D9E75; animation: gpsblink 2s infinite; }
|
||||||
@keyframes gpsblink { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
@keyframes gpsblink { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||||||
|
|
||||||
#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; }
|
#status-bar { position: absolute; bottom: 117px; 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; }
|
#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; }
|
/* button bar */
|
||||||
.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; }
|
#btn-bar { position: absolute; bottom: 0; left: 0; right: 0; display: flex; justify-content: center; align-items: center; gap: 10px; padding: 9px 13px; 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: 26px; padding: 7px 16px; font-size: 14px; 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:hover { background: rgba(255,255,255,0.2); }
|
||||||
.hud-btn:disabled { opacity: 0.3; cursor: default; }
|
.hud-btn:disabled { opacity: 0.3; cursor: default; }
|
||||||
.hud-btn.primary { background: rgba(29,158,117,0.7); border-color: #1D9E75; }
|
.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.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 { 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); }
|
.hud-btn.danger:hover { background: rgba(220,0,0,0.85); }
|
||||||
|
.hud-btn.active { background: rgba(186,117,23,0.7); border-color: rgba(186,117,23,0.9); }
|
||||||
|
canvas { display: none; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<video id="video" autoplay playsinline muted></video>
|
<video id="video" autoplay playsinline muted></video>
|
||||||
|
<canvas id="snap-canvas"></canvas>
|
||||||
|
<div id="flash"></div>
|
||||||
|
|
||||||
<div id="placeholder">
|
<div id="placeholder">
|
||||||
<div style="font-size:40px">🎥</div>
|
<div style="font-size:40px">🎥</div>
|
||||||
@@ -64,10 +85,27 @@
|
|||||||
<span id="rec-timer">00:00</span>
|
<span id="rec-timer">00:00</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="speed-display">
|
<div id="speed-display" onclick="openSpeedo()">
|
||||||
<div id="speed-val">--</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 id="speedo-overlay" onclick="closeSpeedo()">
|
||||||
|
<button id="speedo-close" onclick="closeSpeedo()">✕</button>
|
||||||
|
<div id="big-speed-val">--</div>
|
||||||
|
<div id="big-speed-unit">MPH</div>
|
||||||
|
<div id="big-gps-info">
|
||||||
|
<div class="big-data-row">
|
||||||
|
<div class="bdi"><span class="bdl">LAT</span><span class="bdv" id="b-lat">--</span></div>
|
||||||
|
<div class="bdi"><span class="bdl">LNG</span><span class="bdv" id="b-lng">--</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="big-data-row">
|
||||||
|
<div class="bdi"><span class="bdl">ALT</span><span class="bdv" id="b-alt">--</span></div>
|
||||||
|
<div class="bdi"><span class="bdl">ACC</span><span class="bdv" id="b-acc">--</span></div>
|
||||||
|
<div class="bdi"><span class="bdl">HDG</span><span class="bdv" id="b-hdg">--</span></div>
|
||||||
|
<div class="bdi"><span class="bdl">DIST</span><span class="bdv" id="b-dist">--</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="gps-bar">
|
<div id="gps-bar">
|
||||||
@@ -87,9 +125,11 @@
|
|||||||
<div id="btn-bar">
|
<div id="btn-bar">
|
||||||
<button class="hud-btn primary" id="start-btn" onclick="startDashcam()">START</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="flip-btn" onclick="switchCamera()" disabled>FLIP</button>
|
||||||
<button class="hud-btn" id="rec-btn" onclick="toggleRecord()" disabled>● RECORD</button>
|
<button class="hud-btn" id="snap-btn" onclick="takeSnapshot()" disabled>📷</button>
|
||||||
|
<button class="hud-btn" id="rec-btn" onclick="toggleRecord()" disabled>● REC</button>
|
||||||
|
<button class="hud-btn" id="info-btn" onclick="toggleInfo()" disabled>INFO</button>
|
||||||
<button class="hud-btn" id="unit-btn" onclick="toggleUnit()">MPH/KPH</button>
|
<button class="hud-btn" id="unit-btn" onclick="toggleUnit()">MPH/KPH</button>
|
||||||
<button class="hud-btn" id="fs-btn" onclick="toggleFullscreen()">⛶ FULL</button>
|
<button class="hud-btn" id="fs-btn" onclick="toggleFullscreen()">⛶</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,10 +139,14 @@ let stream = null, facingMode = 'environment';
|
|||||||
let watchId = null, clockInterval = null;
|
let watchId = null, clockInterval = null;
|
||||||
let useMph = true, maxSpeed = 0;
|
let useMph = true, maxSpeed = 0;
|
||||||
let lastPos = null, totalDist = 0;
|
let lastPos = null, totalDist = 0;
|
||||||
let mediaRecorder = null, recordedChunks = [], recInterval = null, recSeconds = 0;
|
let mediaRecorder = null, recordedChunks = [];
|
||||||
let isRecording = false;
|
let recInterval = null, recSeconds = 0, isRecording = false;
|
||||||
|
let speedoOpen = false, infoVisible = 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; }
|
function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; }
|
||||||
|
function cvtLabel() { return useMph ? 'mph' : 'kph'; }
|
||||||
|
|
||||||
function haversine(a, b) {
|
function haversine(a, b) {
|
||||||
const R = 6371000, r = Math.PI / 180;
|
const R = 6371000, r = Math.PI / 180;
|
||||||
@@ -116,7 +160,8 @@ function bearingLabel(deg) {
|
|||||||
return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8]+' '+Math.round(deg)+'\xb0';
|
return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8]+' '+Math.round(deg)+'\xb0';
|
||||||
}
|
}
|
||||||
|
|
||||||
function pad(v) { return String(v).padStart(2,'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() {
|
function updateClock() {
|
||||||
const n = new Date();
|
const n = new Date();
|
||||||
@@ -128,19 +173,178 @@ function updateClock() {
|
|||||||
|
|
||||||
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
|
function setStatus(msg) { document.getElementById('status-bar').textContent = msg; }
|
||||||
|
|
||||||
|
function toggleInfo() {
|
||||||
|
infoVisible = !infoVisible;
|
||||||
|
const bar = document.getElementById('gps-bar');
|
||||||
|
const btn = document.getElementById('info-btn');
|
||||||
|
bar.style.display = infoVisible ? 'block' : 'none';
|
||||||
|
btn.classList.toggle('active', infoVisible);
|
||||||
|
btn.textContent = infoVisible ? 'INFO ✓' : 'INFO';
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeSnapshot() {
|
||||||
|
if (!stream) return;
|
||||||
|
const video = document.getElementById('video');
|
||||||
|
const canvas = document.getElementById('snap-canvas');
|
||||||
|
canvas.width = video.videoWidth || 1280;
|
||||||
|
canvas.height = video.videoHeight || 720;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// stamp telemetry onto image
|
||||||
|
const n = new Date();
|
||||||
|
const dateStr = n.getFullYear()+'-'+pad(n.getMonth()+1)+'-'+pad(n.getDate())+' '+pad(n.getHours())+':'+pad(n.getMinutes())+':'+pad(n.getSeconds());
|
||||||
|
const spd = currentGPS.speed !== null ? Math.round(cvt(currentGPS.speed))+' '+cvtLabel().toUpperCase() : '--';
|
||||||
|
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 lines = [dateStr, spd+' '+bearingLabel(currentGPS.hdg), 'LAT '+lat+' LNG '+lng, 'ALT '+alt];
|
||||||
|
|
||||||
|
ctx.font = 'bold 20px monospace';
|
||||||
|
ctx.textBaseline = 'bottom';
|
||||||
|
const lineH = 26, pad8 = 10;
|
||||||
|
const boxH = lines.length * lineH + pad8 * 2;
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
||||||
|
ctx.fillRect(0, canvas.height - boxH, canvas.width, boxH);
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
lines.forEach((l, i) => {
|
||||||
|
ctx.fillText(l, pad8, canvas.height - boxH + pad8 + (i+1)*lineH);
|
||||||
|
});
|
||||||
|
|
||||||
|
// flash
|
||||||
|
const flash = document.getElementById('flash');
|
||||||
|
flash.classList.add('on');
|
||||||
|
setTimeout(() => flash.classList.remove('on'), 120);
|
||||||
|
|
||||||
|
// download
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const fname = 'dashcam_snap_'+n.getFullYear()+pad(n.getMonth()+1)+pad(n.getDate())+'_'+pad(n.getHours())+pad(n.getMinutes())+pad(n.getSeconds())+'.jpg';
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url; a.download = fname; a.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||||||
|
setStatus('Snapshot saved');
|
||||||
|
}, 'image/jpeg', 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
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), m=Math.floor((ms%3600000)/60000), s=Math.floor((ms%60000)/1000), mil=ms%1000;
|
||||||
|
return pad(h)+':'+pad(m)+':'+pad(s)+','+pad3(mil);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) : '--';
|
||||||
|
srtEntries.push({
|
||||||
|
startMs: elapsed, endMs: elapsed+1000,
|
||||||
|
lines: [
|
||||||
|
dateStr+' '+timeStr,
|
||||||
|
'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)=>(i+1)+'\n'+toSRTTime(e.startMs)+' --> '+toSRTTime(e.endMs)+'\n'+e.lines.join('\n')).join('\n\n')+'\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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 dl(filename, blob) {
|
||||||
|
const url=URL.createObjectURL(blob), a=document.createElement('a');
|
||||||
|
a.href=url; a.download=filename; a.click();
|
||||||
|
setTimeout(()=>URL.revokeObjectURL(url),5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRecord() {
|
||||||
|
if (!stream) return;
|
||||||
|
const btn = document.getElementById('rec-btn');
|
||||||
|
const ind = document.getElementById('rec-indicator');
|
||||||
|
if (!isRecording) {
|
||||||
|
recordedChunks=[]; srtEntries=[]; recStartTime=Date.now();
|
||||||
|
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=()=>{
|
||||||
|
const name=getFilename(), mime2=getBestMimeType(), 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;
|
||||||
|
ind.style.display='flex';
|
||||||
|
btn.innerHTML='▮▮ SAVE';
|
||||||
|
btn.classList.add('danger');
|
||||||
|
captureSRTFrame();
|
||||||
|
recInterval=setInterval(()=>{
|
||||||
|
recSeconds++;
|
||||||
|
document.getElementById('rec-timer').textContent=pad(Math.floor(recSeconds/60))+':'+pad(recSeconds%60);
|
||||||
|
captureSRTFrame();
|
||||||
|
},1000);
|
||||||
|
setStatus('Recording...');
|
||||||
|
} else {
|
||||||
|
mediaRecorder.stop(); isRecording=false;
|
||||||
|
clearInterval(recInterval);
|
||||||
|
ind.style.display='none';
|
||||||
|
btn.innerHTML='● REC';
|
||||||
|
btn.classList.remove('danger');
|
||||||
|
setStatus('Saving...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startGPS() {
|
function startGPS() {
|
||||||
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
|
if (!navigator.geolocation) { setStatus('GPS unavailable'); return; }
|
||||||
document.getElementById('gps-bar').style.display = 'flex';
|
|
||||||
watchId=navigator.geolocation.watchPosition(pos=>{
|
watchId=navigator.geolocation.watchPosition(pos=>{
|
||||||
const c=pos.coords;
|
const c=pos.coords;
|
||||||
const spd = c.speed !== null && c.speed >= 0 ? c.speed : 0;
|
currentGPS={lat:c.latitude,lng:c.longitude,alt:c.altitude,acc:c.accuracy,hdg:c.heading,speed:c.speed!==null?c.speed:0};
|
||||||
const conv = cvt(spd);
|
const conv=cvt(currentGPS.speed);
|
||||||
if(conv>maxSpeed) maxSpeed=conv;
|
if(conv>maxSpeed) maxSpeed=conv;
|
||||||
const cur={lat:c.latitude,lng:c.longitude};
|
const cur={lat:c.latitude,lng:c.longitude};
|
||||||
if(lastPos){const d=haversine(lastPos,cur);if(d<200)totalDist+=d;}
|
if(lastPos){const d=haversine(lastPos,cur);if(d<200)totalDist+=d;}
|
||||||
lastPos=cur;
|
lastPos=cur;
|
||||||
document.getElementById('speed-val').textContent=Math.round(conv);
|
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':'--';
|
||||||
@@ -148,6 +352,7 @@ function startGPS() {
|
|||||||
document.getElementById('hdg-val').textContent=bearingLabel(c.heading);
|
document.getElementById('hdg-val').textContent=bearingLabel(c.heading);
|
||||||
document.getElementById('dist-val').textContent=(totalDist/1000).toFixed(2)+'km';
|
document.getElementById('dist-val').textContent=(totalDist/1000).toFixed(2)+'km';
|
||||||
document.getElementById('gps-dot').className='active';
|
document.getElementById('gps-dot').className='active';
|
||||||
|
syncBigSpeedo();
|
||||||
setStatus('');
|
setStatus('');
|
||||||
}, err=>{
|
}, err=>{
|
||||||
setStatus('GPS: '+err.message);
|
setStatus('GPS: '+err.message);
|
||||||
@@ -155,89 +360,25 @@ function startGPS() {
|
|||||||
},{enableHighAccuracy:true,maximumAge:1000,timeout:15000});
|
},{enableHighAccuracy:true,maximumAge:1000,timeout:15000});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = '▮▮ SAVE';
|
|
||||||
btn.innerHTML = '▮▮ 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 = '● 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.textContent='...';
|
btn.disabled=true; btn.textContent='...';
|
||||||
if(isRecording) toggleRecord();
|
if(isRecording) toggleRecord();
|
||||||
|
closeSpeedo();
|
||||||
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,width:{ideal:1920},height:{ideal:1080}},audio:false});
|
||||||
video: { facingMode, width: { ideal: 1920 }, height: { ideal: 1080 } }, 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('top-left').style.display='flex';
|
document.getElementById('top-left').style.display='flex';
|
||||||
document.getElementById('flip-btn').disabled=false;
|
document.getElementById('flip-btn').disabled=false;
|
||||||
document.getElementById('rec-btn').disabled=false;
|
document.getElementById('rec-btn').disabled=false;
|
||||||
|
document.getElementById('snap-btn').disabled=false;
|
||||||
|
document.getElementById('info-btn').disabled=false;
|
||||||
btn.textContent='RESTART'; btn.disabled=false;
|
btn.textContent='RESTART'; btn.disabled=false;
|
||||||
if(watchId!==null){navigator.geolocation.clearWatch(watchId);watchId=null;}
|
if(watchId!==null){navigator.geolocation.clearWatch(watchId);watchId=null;}
|
||||||
maxSpeed=0; totalDist=0; lastPos=null;
|
maxSpeed=0; totalDist=0; lastPos=null;
|
||||||
|
currentGPS={lat:null,lng:null,alt:null,acc:null,hdg:null,speed:0};
|
||||||
startGPS();
|
startGPS();
|
||||||
if(clockInterval) clearInterval(clockInterval);
|
if(clockInterval) clearInterval(clockInterval);
|
||||||
clockInterval=setInterval(updateClock,1000);
|
clockInterval=setInterval(updateClock,1000);
|
||||||
@@ -249,34 +390,37 @@ async function startDashcam() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchCamera() {
|
function switchCamera() { facingMode=facingMode==='environment'?'user':'environment'; startDashcam(); }
|
||||||
facingMode = facingMode === 'environment' ? 'user' : 'environment';
|
|
||||||
startDashcam();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleUnit() {
|
function toggleUnit() {
|
||||||
useMph=!useMph;
|
useMph=!useMph;
|
||||||
maxSpeed=useMph?maxSpeed/3.6*2.23694:maxSpeed/2.23694*3.6;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wakeLock = null;
|
||||||
|
async function requestWakeLock() {
|
||||||
|
try { wakeLock = await navigator.wakeLock.request('screen'); } catch(e) {}
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') requestWakeLock();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => { startDashcam(); requestWakeLock(); });
|
||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
const app = document.getElementById('app');
|
const app=document.getElementById('app'), btn=document.getElementById('fs-btn');
|
||||||
const btn = document.getElementById('fs-btn');
|
|
||||||
if(!app.classList.contains('fullscreen')){
|
if(!app.classList.contains('fullscreen')){
|
||||||
if(app.requestFullscreen) app.requestFullscreen().catch(()=>{});
|
if(app.requestFullscreen) app.requestFullscreen().catch(()=>{});
|
||||||
app.classList.add('fullscreen');
|
app.classList.add('fullscreen'); btn.textContent='\u2715';
|
||||||
btn.textContent = '\u2715 EXIT';
|
|
||||||
} else {
|
} else {
|
||||||
if(document.exitFullscreen&&document.fullscreenElement) document.exitFullscreen().catch(()=>{});
|
if(document.exitFullscreen&&document.fullscreenElement) document.exitFullscreen().catch(()=>{});
|
||||||
app.classList.remove('fullscreen');
|
app.classList.remove('fullscreen'); btn.textContent='\u26f6';
|
||||||
btn.textContent = '\u26f6 FULL';
|
|
||||||
}
|
}
|
||||||
document.addEventListener('fullscreenchange',()=>{
|
document.addEventListener('fullscreenchange',()=>{
|
||||||
if (!document.fullscreenElement) {
|
if(!document.fullscreenElement){app.classList.remove('fullscreen');btn.textContent='\u26f6';}
|
||||||
app.classList.remove('fullscreen');
|
|
||||||
btn.textContent = '\u26f6 FULL';
|
|
||||||
}
|
|
||||||
},{once:true});
|
},{once:true});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
244
speedometer.html
Normal file
244
speedometer.html
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Speedometer</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #000;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #fff;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#speed-val {
|
||||||
|
font-size: clamp(100px, 22vh, 240px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#speed-unit {
|
||||||
|
font-size: clamp(16px, 4vh, 36px);
|
||||||
|
color: #aaa;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
margin-top: clamp(6px, 1vh, 12px);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gps-info {
|
||||||
|
margin-top: clamp(20px, 4vh, 48px);
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: clamp(8px, 1.5vh, 18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row {
|
||||||
|
display: flex;
|
||||||
|
gap: clamp(16px, 5vw, 36px);
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.di {
|
||||||
|
font-size: clamp(13px, 2.4vh, 24px);
|
||||||
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
gap: clamp(4px, 0.8vh, 8px);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dl { color: #555; font-size: clamp(10px, 1.8vh, 18px); letter-spacing: 0.5px; }
|
||||||
|
.dv { color: #fff; font-weight: 500; }
|
||||||
|
|
||||||
|
#gps-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #555;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#gps-dot.active { background: #1D9E75; animation: gpsblink 2s infinite; }
|
||||||
|
@keyframes gpsblink { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||||||
|
|
||||||
|
#clock {
|
||||||
|
position: fixed;
|
||||||
|
top: 14px;
|
||||||
|
left: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,0.07);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#unit-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 14px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 0.5px solid rgba(255,255,255,0.2);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 26px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
#unit-btn:hover { background: rgba(255,255,255,0.2); }
|
||||||
|
|
||||||
|
#fs-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 14px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 0.5px solid rgba(255,255,255,0.2);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 26px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
#fs-btn:hover { background: rgba(255,255,255,0.2); }
|
||||||
|
|
||||||
|
#status {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255,255,255,0.35);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="clock">--:--:--</div>
|
||||||
|
<button id="fs-btn" onclick="toggleFullscreen()">⛶</button>
|
||||||
|
<button id="unit-btn" onclick="toggleUnit()">MPH / KPH</button>
|
||||||
|
|
||||||
|
<div id="speed-val">--</div>
|
||||||
|
<div id="speed-unit">MPH</div>
|
||||||
|
|
||||||
|
<div id="gps-info">
|
||||||
|
<div class="data-row">
|
||||||
|
<div id="gps-dot"></div>
|
||||||
|
<div class="di"><span class="dl">LAT</span><span class="dv" id="lat-val">--</span></div>
|
||||||
|
<div class="di"><span class="dl">LNG</span><span class="dv" id="lng-val">--</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="data-row">
|
||||||
|
<div class="di"><span class="dl">ALT</span><span class="dv" id="alt-val">--</span></div>
|
||||||
|
<div class="di"><span class="dl">ACC</span><span class="dv" id="acc-val">--</span></div>
|
||||||
|
<div class="di"><span class="dl">HDG</span><span class="dv" id="hdg-val">--</span></div>
|
||||||
|
<div class="di"><span class="dl">DIST</span><span class="dv" id="dist-val">0.00km</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status">Acquiring GPS...</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let useMph = true, watchId = null;
|
||||||
|
let lastPos = null, totalDist = 0;
|
||||||
|
|
||||||
|
function pad(v) { return String(Math.floor(v)).padStart(2, '0'); }
|
||||||
|
|
||||||
|
function cvt(ms) { return useMph ? ms * 2.23694 : ms * 3.6; }
|
||||||
|
|
||||||
|
function haversine(a, b) {
|
||||||
|
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 bearingLabel(deg) {
|
||||||
|
if (deg === null || isNaN(deg)) return '--';
|
||||||
|
return ['N','NE','E','SE','S','SW','W','NW'][Math.round(deg/45)%8] + ' ' + Math.round(deg) + '\xb0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
const n = new Date();
|
||||||
|
document.getElementById('clock').textContent =
|
||||||
|
pad(n.getHours()) + ':' + pad(n.getMinutes()) + ':' + pad(n.getSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUnit() {
|
||||||
|
useMph = !useMph;
|
||||||
|
document.getElementById('speed-unit').textContent = useMph ? 'MPH' : 'KPH';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGPS() {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
document.getElementById('status').textContent = 'GPS unavailable';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
watchId = navigator.geolocation.watchPosition(pos => {
|
||||||
|
const c = pos.coords;
|
||||||
|
const speed = c.speed !== null ? c.speed : 0;
|
||||||
|
const conv = cvt(speed);
|
||||||
|
|
||||||
|
document.getElementById('speed-val').textContent = Math.round(conv);
|
||||||
|
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 = bearingLabel(c.heading);
|
||||||
|
|
||||||
|
const cur = { lat: c.latitude, lng: c.longitude };
|
||||||
|
if (lastPos) { const d = haversine(lastPos, cur); if (d < 200) totalDist += d; }
|
||||||
|
lastPos = cur;
|
||||||
|
document.getElementById('dist-val').textContent = (totalDist / 1000).toFixed(2) + 'km';
|
||||||
|
|
||||||
|
document.getElementById('gps-dot').className = 'active';
|
||||||
|
document.getElementById('status').textContent = '';
|
||||||
|
}, err => {
|
||||||
|
document.getElementById('status').textContent = 'GPS: ' + err.message;
|
||||||
|
document.getElementById('gps-dot').className = '';
|
||||||
|
}, { enableHighAccuracy: true, maximumAge: 1000, timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
const btn = document.getElementById('fs-btn');
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen().catch(() => {});
|
||||||
|
btn.textContent = '✕';
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen().catch(() => {});
|
||||||
|
btn.textContent = '⛶';
|
||||||
|
}
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
if (!document.fullscreenElement) btn.textContent = '⛶';
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock();
|
||||||
|
startGPS();
|
||||||
|
|
||||||
|
let wakeLock = null;
|
||||||
|
async function requestWakeLock() {
|
||||||
|
try { wakeLock = await navigator.wakeLock.request('screen'); } catch(e) {}
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') requestWakeLock();
|
||||||
|
});
|
||||||
|
requestWakeLock();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user