forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
120 lines
4.1 KiB
HTML
120 lines
4.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Timmy's Workshop</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
</head>
|
|
<body>
|
|
<div id="overlay">
|
|
<div id="status">
|
|
<div class="name">Timmy</div>
|
|
<div class="mood" id="mood-text">focused</div>
|
|
</div>
|
|
<div id="connection-dot"></div>
|
|
<div id="speech-area">
|
|
<div class="bubble" id="speech-bubble"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
|
|
}
|
|
}
|
|
</script>
|
|
<script type="module">
|
|
import * as THREE from "three";
|
|
import { buildRoom } from "./scene.js";
|
|
import { createWizard } from "./wizard.js";
|
|
import { createFamiliar } from "./familiar.js";
|
|
import { setupControls } from "./controls.js";
|
|
import { StateReader } from "./state.js";
|
|
|
|
// --- Renderer ---
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 0.8;
|
|
document.body.prepend(renderer.domElement);
|
|
|
|
// --- Scene ---
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x0a0a14);
|
|
scene.fog = new THREE.Fog(0x0a0a14, 5, 12);
|
|
|
|
// --- Camera (visitor at the door) ---
|
|
const camera = new THREE.PerspectiveCamera(
|
|
55, window.innerWidth / window.innerHeight, 0.1, 50
|
|
);
|
|
camera.position.set(0, 2.0, 4.5);
|
|
|
|
// --- Build scene elements ---
|
|
const { crystalBall, crystalLight, fireLight, candleLights } = buildRoom(scene);
|
|
const wizard = createWizard();
|
|
scene.add(wizard.group);
|
|
const familiar = createFamiliar();
|
|
scene.add(familiar.group);
|
|
|
|
// --- Controls ---
|
|
const controls = setupControls(camera, renderer.domElement);
|
|
|
|
// --- State ---
|
|
const stateReader = new StateReader();
|
|
const moodEl = document.getElementById("mood-text");
|
|
stateReader.onChange((state) => {
|
|
if (moodEl) {
|
|
moodEl.textContent = state.timmyState.mood;
|
|
}
|
|
});
|
|
stateReader.connect();
|
|
|
|
// --- Resize ---
|
|
window.addEventListener("resize", () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
// --- Animation loop ---
|
|
const clock = new THREE.Clock();
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
const dt = clock.getDelta();
|
|
|
|
// Update scene elements
|
|
wizard.update(dt);
|
|
familiar.update(dt);
|
|
controls.update();
|
|
|
|
// Crystal ball subtle rotation + pulsing glow
|
|
crystalBall.rotation.y += dt * 0.3;
|
|
const pulse = 0.3 + Math.sin(Date.now() * 0.002) * 0.15;
|
|
crystalLight.intensity = pulse;
|
|
crystalBall.material.emissiveIntensity = pulse * 0.5;
|
|
|
|
// Fireplace flicker
|
|
fireLight.intensity = 1.2 + Math.sin(Date.now() * 0.005) * 0.15
|
|
+ Math.sin(Date.now() * 0.013) * 0.1;
|
|
|
|
// Candle flicker — each offset slightly for variety
|
|
const now = Date.now();
|
|
for (let i = 0; i < candleLights.length; i++) {
|
|
candleLights[i].intensity = 0.4
|
|
+ Math.sin(now * 0.007 + i * 2.1) * 0.1
|
|
+ Math.sin(now * 0.019 + i * 1.3) * 0.05;
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html>
|