← back to Ventura Bus 3d
public/scene-demo.html
156 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ventura Bus 3D — Scene Smoke Test</title>
<style>
html, body { margin: 0; padding: 0; height: 100%; background: #0a0a0a; color: #eee;
font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
#stage { position: fixed; inset: 0; }
.panel {
background: rgba(0,0,0,0.55); border-radius: 8px; backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px); padding: 10px 14px;
}
#hud { position: fixed; top: 10px; left: 10px; pointer-events: none; }
#hud b { color: #ffd66e; }
#fps { font-variant-numeric: tabular-nums; opacity: 0.85; }
#ctrl { position: fixed; top: 10px; right: 10px; min-width: 230px; display: flex; flex-direction: column; gap: 8px; }
#ctrl label { display: flex; flex-direction: column; gap: 3px; font-size: 12px; opacity: 0.85; }
#ctrl input[type=range] { width: 100%; accent-color: #ffd66e; }
#ctrl .row { display: flex; gap: 6px; flex-wrap: wrap; }
#ctrl .row button {
flex: 1 1 auto; background: rgba(255,255,255,0.08); color: #eee; border: 1px solid rgba(255,255,255,0.18);
border-radius: 6px; padding: 4px 8px; cursor: pointer; font-size: 11px;
}
#ctrl .row button:hover { border-color: #ffd66e; color: #ffd66e; }
#ctrl .row button.active { background: #ffd66e; color: #1a1100; border-color: #ffd66e; }
#clock { font-variant-numeric: tabular-nums; color: #ffd66e; }
</style>
</head>
<body>
<div id="stage"></div>
<div id="hud" class="panel">
<b>Ventura Bus 3D</b> · base scene (Task #3)<br/>
drag = orbit · scroll = zoom · right-drag = pan<br/>
<span id="fps">— fps</span>
</div>
<div id="ctrl" class="panel">
<label>Time of day · <span id="clock">12:00</span>
<input id="tod" type="range" min="0" max="1440" step="5" value="720" />
</label>
<div class="row" id="presets">
<button data-min="360">Dawn</button>
<button data-min="720" class="active">Noon</button>
<button data-min="1140">Dusk</button>
<button data-min="60">Night</button>
</div>
<label><input id="autocycle" type="checkbox" /> auto-cycle (×60)</label>
<label><input id="loadroute" type="checkbox" /> load /api/route into curve</label>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import { createBaseScene } from './js/scene.js';
import * as THREE from 'three';
const stage = document.getElementById('stage');
const todEl = document.getElementById('tod');
const clockEl = document.getElementById('clock');
const fpsEl = document.getElementById('fps');
const presets = document.getElementById('presets');
const autoEl = document.getElementById('autocycle');
const loadEl = document.getElementById('loadroute');
const ctx = createBaseScene({ container: stage, initialMinutes: Number(todEl.value) });
window.__scene = ctx;
function setClock(min) {
const m = ((min % 1440) + 1440) % 1440;
const hh = String(Math.floor(m / 60)).padStart(2, '0');
const mm = String(Math.floor(m % 60)).padStart(2, '0');
clockEl.textContent = `${hh}:${mm}`;
}
setClock(Number(todEl.value));
todEl.addEventListener('input', () => {
const v = Number(todEl.value);
ctx.setTimeOfDay(v);
setClock(v);
presets.querySelectorAll('button').forEach(b => b.classList.toggle('active', Number(b.dataset.min) === v));
});
presets.addEventListener('click', e => {
const btn = e.target.closest('button[data-min]');
if (!btn) return;
const v = Number(btn.dataset.min);
todEl.value = String(v);
ctx.setTimeOfDay(v);
setClock(v);
presets.querySelectorAll('button').forEach(b => b.classList.toggle('active', b === btn));
});
let lastT = performance.now();
let frames = 0; let frameAcc = 0;
ctx.onTick(() => {
const now = performance.now();
const dt = (now - lastT) / 1000;
lastT = now;
if (autoEl.checked) {
const advance = dt * 60; // 1 real-sec = 60 simulated-min
const next = (Number(todEl.value) + advance) % 1440;
todEl.value = String(next);
ctx.setTimeOfDay(next);
setClock(next);
}
frames += 1; frameAcc += dt;
if (frameAcc >= 0.5) {
fpsEl.textContent = `${Math.round(frames / frameAcc)} fps`;
frames = 0; frameAcc = 0;
}
});
async function loadAndApplyRoute() {
try {
const r = await fetch('/api/route').then(r => r.json());
if (!r?.stops?.length) return;
const lngs = r.stops.map(s => s.lng);
const lats = r.stops.map(s => s.lat);
const lngMin = Math.min(...lngs), lngMax = Math.max(...lngs);
const latMin = Math.min(...lats), latMax = Math.max(...lats);
const lngSpan = lngMax - lngMin || 1;
const latSpan = latMax - latMin || 1;
const targetLen = ctx.constants.ROAD_LENGTH * 0.95;
// +lng → +X (east), +lat → -Z (north)
const points = r.stops.map(s => new THREE.Vector3(
((s.lng - lngMin) / lngSpan - 0.5) * targetLen,
0,
-((s.lat - latMin) / latSpan - 0.5) * (ctx.constants.ROAD_LENGTH * 0.04),
));
ctx.setRouteWaypoints(points);
console.info('[scene-demo] applied /api/route waypoints', points);
} catch (err) {
console.warn('[scene-demo] failed to load /api/route', err);
}
}
loadEl.addEventListener('change', () => { if (loadEl.checked) loadAndApplyRoute(); });
ctx.start();
</script>
</body>
</html>