← 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>