<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bird vs Dr. J — Three.js Court</title>
<style>
:root {
--ink: #f5e8cf;
--red: #d84b31;
--panel: rgba(12, 16, 23, .78);
}
* { box-sizing: border-box; }
body {
margin: 0;
overflow: hidden;
background: #080c12;
color: var(--ink);
font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
}
canvas { display: block; }
#hud {
position: fixed;
z-index: 2;
top: 18px;
left: 18px;
width: min(360px, calc(100vw - 36px));
padding: 16px 18px;
border: 1px solid rgba(245,232,207,.35);
background: var(--panel);
box-shadow: 0 12px 35px #0008;
backdrop-filter: blur(8px);
}
h1 {
margin: 0 0 5px;
color: #fff3db;
font-size: clamp(23px, 4vw, 37px);
letter-spacing: 1px;
line-height: .95;
}
#status {
margin: 8px 0 12px;
color: #e8d8be;
font-family: system-ui, sans-serif;
font-size: 13px;
line-height: 1.4;
}
#scoreboard {
display: flex;
overflow: hidden;
border: 2px solid #dbc49d;
background: #0b0b0b;
}
.score {
flex: 1;
padding: 8px;
text-align: center;
color: #ff563c;
font-size: 27px;
line-height: .9;
text-shadow: 0 0 10px #f33;
}
.score + .score { border-left: 2px solid #dbc49d; }
.name {
display: block;
color: #e7dcc5;
font-family: system-ui, sans-serif;
font-size: 10px;
font-weight: 800;
letter-spacing: 1px;
text-shadow: none;
}
button {
margin-top: 12px;
border: 1px solid #f8d9a7;
padding: 10px 13px;
color: #15100e;
background: #f0c27a;
cursor: pointer;
font: 800 13px system-ui, sans-serif;
}
button:hover { background: #ffe0a9; }
#caption {
position: fixed;
z-index: 2;
bottom: 22px;
left: 50%;
width: min(650px, calc(100vw - 40px));
transform: translateX(-50%);
padding: 11px 16px;
text-align: center;
background: rgba(0,0,0,.54);
border: 1px solid rgba(255,255,255,.18);
color: #fff4dc;
font-size: clamp(16px, 2.6vw, 26px);
letter-spacing: .7px;
text-shadow: 2px 2px #000;
opacity: 0;
transition: opacity .2s;
}
#hint {
position: fixed;
right: 18px;
bottom: 18px;
color: #e9dbc0;
opacity: .72;
font: 12px system-ui, sans-serif;
text-align: right;
}
</style>
</head>
<body>
<div id="hud">
<h1>BIRD vs DR. J</h1>
<div id="scoreboard">
<div class="score"><span class="name">LARRY</span><span id="birdScore">00</span></div>
<div class="score"><span class="name">DR. J</span><span id="jScore">00</span></div>
</div>
<p id="status">Click Start to enable court sounds. Spacebar: break the window.</p>
<button id="start">START SCENE + SOUND</button>
</div>
<div id="caption"></div>
<div id="hint">Drag to orbit • Wheel to zoom<br>Space = smash glass</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.174.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.174.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// ------------------------------------------------------------
// Basic scene
// ------------------------------------------------------------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x101720);
scene.fog = new THREE.Fog(0x101720, 25, 70);
const camera = new THREE.PerspectiveCamera(
48, innerWidth / innerHeight, 0.1, 150
);
camera.position.set(20, 13, 24);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 3.2, -2);
controls.enableDamping = true;
controls.minDistance = 12;
controls.maxDistance = 43;
controls.maxPolarAngle = Math.PI * 0.48;
scene.add(new THREE.HemisphereLight(0xb9d8ff, 0x423020, 2.3));
const keyLight = new THREE.DirectionalLight(0xffe3b4, 3.6);
keyLight.position.set(-10, 16, 9);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048, 2048);
scene.add(keyLight);
const rimLight = new THREE.PointLight(0xff6e48, 20, 32, 2);
rimLight.position.set(4, 8, -10);
scene.add(rimLight);
// ------------------------------------------------------------
// Materials and helpers
// ------------------------------------------------------------
const wood = new THREE.MeshStandardMaterial({
color: 0xb87437, roughness: 0.7, metalness: 0.05
});
const paint = new THREE.MeshStandardMaterial({
color: 0xd7b16e, roughness: 0.85
});
const black = new THREE.MeshStandardMaterial({
color: 0x16191e, roughness: 0.65
});
const orange = new THREE.MeshStandardMaterial({
color: 0xd76524, roughness: 0.75
});
const white = new THREE.MeshStandardMaterial({
color: 0xf4ead8, roughness: 0.5
});
const red = new THREE.MeshStandardMaterial({
color: 0xc9342f, roughness: 0.62
});
const blue = new THREE.MeshStandardMaterial({
color: 0x28457e, roughness: 0.63
});
const skinLight = new THREE.MeshStandardMaterial({
color: 0xd3976d, roughness: 0.8
});
const skinDark = new THREE.MeshStandardMaterial({
color: 0x704432, roughness: 0.82
});
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0x94cff0,
transparent: true,
opacity: 0.28,
roughness: 0.05,
metalness: 0.15,
side: THREE.DoubleSide
});
function box(w, h, d, material, x=0, y=0, z=0) {
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
m.position.set(x, y, z);
m.castShadow = true;
m.receiveShadow = true;
scene.add(m);
return m;
}
function cylinder(r1, r2, h, material, x=0, y=0, z=0) {
const m = new THREE.Mesh(new THREE.CylinderGeometry(r1, r2, h, 16), material);
m.position.set(x, y, z);
m.castShadow = true;
m.receiveShadow = true;
scene.add(m);
return m;
}
function textSprite(text, color = "#ffffff") {
const canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 128;
const c = canvas.getContext("2d");
c.clearRect(0, 0, canvas.width, canvas.height);
c.font = "bold 52px Arial";
c.textAlign = "center";
c.textBaseline = "middle";
c.fillStyle = "#000";
c.fillText(text, 258, 68);
c.fillStyle = color;
c.fillText(text, 256, 64);
const tex = new THREE.CanvasTexture(canvas);
const sprite = new THREE.Sprite(
new THREE.SpriteMaterial({ map: tex, transparent: true })
);
sprite.scale.set(5.5, 1.38, 1);
return sprite;
}
// ------------------------------------------------------------
// Gym floor and walls
// ------------------------------------------------------------
const floor = box(34, 0.25, 26, wood, 0, -0.15, 0);
floor.receiveShadow = true;
// Wood floor stripes
for (let z = -12; z <= 12; z += 2) {
const stripe = box(33.6, 0.012, 0.035, paint, 0, 0.01, z);
stripe.material = new THREE.MeshStandardMaterial({ color: 0x8b5128 });
}
const lineMat = new THREE.MeshBasicMaterial({ color: 0xf2e1bc });
function courtLine(w, d, x, z) {
const line = new THREE.Mesh(new THREE.BoxGeometry(w, 0.035, d), lineMat);
line.position.set(x, 0.04, z);
scene.add(line);
}
courtLine(0.11, 22, 0, 0);
courtLine(30, 0.11, 0, 0);
courtLine(0.11, 20, -14, 0);
courtLine(0.11, 20, 14, 0);
courtLine(28, 0.11, 0, -10);
courtLine(28, 0.11, 0, 10);
// Back wall
box(34, 13, 0.45, black, 0, 6.4, -13.4);
// Brick-style wall lines
const wallLineMat = new THREE.MeshBasicMaterial({ color: 0x3c4149 });
for (let y = 1; y < 12; y += 1.15) {
const l = new THREE.Mesh(new THREE.BoxGeometry(33, 0.035, 0.025), wallLineMat);
l.position.set(0, y, -13.14);
scene.add(l);
}
// Ceiling lights
for (const x of [-11, 0, 11]) {
const fixture = box(4.2, 0.18, 1.3, white, x, 11.7, -2);
const light = new THREE.PointLight(0xe9efff, 8, 20, 2);
light.position.set(x, 10.8, -2);
scene.add(light);
}
// ------------------------------------------------------------
// Hoop
// ------------------------------------------------------------
const hoop = new THREE.Group();
hoop.position.set(0, 0, -9.8);
scene.add(hoop);
const pole = new THREE.Mesh(
new THREE.CylinderGeometry(.21, .28, 7, 12),
black
);
pole.position.set(0, 3.5, -1.4);
pole.castShadow = true;
hoop.add(pole);
const arm = new THREE.Mesh(new THREE.BoxGeometry(.25, .25, 2.2), black);
arm.position.set(0, 6.55, -0.45);
hoop.add(arm);
const board = new THREE.Mesh(new THREE.BoxGeometry(4.2, 2.7, .16), white);
board.position.set(0, 6.2, 0);
board.castShadow = true;
hoop.add(board);
const boardSquare = new THREE.Mesh(
new THREE.BoxGeometry(1.05, .82, .02),
new THREE.MeshBasicMaterial({ color: 0x222222 })
);
boardSquare.position.set(0, 6.15, 0.095);
hoop.add(boardSquare);
const rim = new THREE.Mesh(
new THREE.TorusGeometry(.72, .06, 10, 32),
red
);
rim.rotation.x = Math.PI / 2;
rim.position.set(0, 5.25, .48);
hoop.add(rim);
// Net
const netMat = new THREE.MeshBasicMaterial({ color: 0xf5f0df });
for (let i = 0; i < 10; i++) {
const a = (i / 10) * Math.PI * 2;
const top = new THREE.Vector3(Math.cos(a)*.7, 5.2, .48 + Math.sin(a)*.7);
const bottom = new THREE.Vector3(Math.cos(a)*.43, 4.5, .48 + Math.sin(a)*.43);
const mid = top.clone().lerp(bottom, .5);
const len = top.distanceTo(bottom);
const strand = new THREE.Mesh(
new THREE.CylinderGeometry(.012, .012, len, 5), netMat
);
strand.position.copy(mid);
strand.lookAt(bottom);
strand.rotateX(Math.PI / 2);
hoop.add(strand);
}
// ------------------------------------------------------------
// Window + breakable shards
// ------------------------------------------------------------
const windowGroup = new THREE.Group();
windowGroup.position.set(-10.5, 7.2, -13.08);
scene.add(windowGroup);
const frameMat = new THREE.MeshStandardMaterial({
color: 0x30353c, metalness: 0.45, roughness: 0.45
});
box(7.1, .22, .22, frameMat, -10.5, 10.15, -13.0);
box(7.1, .22, .22, frameMat, -10.5, 4.25, -13.0);
box(.22, 6.1, .22, frameMat, -14.0, 7.2, -13.0);
box(.22, 6.1, .22, frameMat, -7.0, 7.2, -13.0);
const intactPane = new THREE.Mesh(
new THREE.PlaneGeometry(6.65, 5.65), glassMaterial
);
intactPane.position.set(-10.5, 7.2, -12.94);
scene.add(intactPane);
// Window mullions
for (let i = 1; i < 3; i++) {
box(.08, 5.65, .1, frameMat, -14 + i * 2.33, 7.2, -12.9);
}
box(6.65, .08, .1, frameMat, -10.5, 7.2, -12.9);
const shards = [];
function makeShards() {
for (let i = 0; i < 46; i++) {
const shard = new THREE.Mesh(
new THREE.TetrahedronGeometry(0.08 + Math.random() * 0.15),
new THREE.MeshPhysicalMaterial({
color: 0xa4e4ff,
transparent: true,
opacity: .7,
roughness: .05,
metalness: .2
})
);
shard.visible = false;
shard.userData.velocity = new THREE.Vector3();
scene.add(shard);
shards.push(shard);
}
}
makeShards();
let glassBroken = false;
// ------------------------------------------------------------
// Characters
// ------------------------------------------------------------
function makePlayer({ name, jersey, skin, hair, x, z }) {
const g = new THREE.Group();
g.position.set(x, 0, z);
scene.add(g);
const torso = new THREE.Mesh(
new THREE.CylinderGeometry(.63, .78, 1.85, 12),
jersey
);
torso.position.y = 3.15;
torso.castShadow = true;
g.add(torso);
const shorts = new THREE.Mesh(
new THREE.BoxGeometry(1.37, .64, .82),
jersey
);
shorts.position.y = 2.14;
shorts.castShadow = true;
g.add(shorts);
const neck = new THREE.Mesh(
new THREE.CylinderGeometry(.19, .19, .3, 10), skin
);
neck.position.y = 4.18;
g.add(neck);
const head = new THREE.Mesh(
new THREE.SphereGeometry(.49, 16, 12), skin
);
head.position.y = 4.67;
head.castShadow = true;
g.add(head);
if (hair) {
const hairMesh = new THREE.Mesh(
new THREE.SphereGeometry(.51, 16, 12, 0, Math.PI * 2, 0, Math.PI * .48),
hair
);
hairMesh.position.y = 4.83;
g.add(hairMesh);
}
function limb(radius, length, material, px, py, pz) {
const limb = new THREE.Mesh(
new THREE.CylinderGeometry(radius, radius * .88, length, 10),
material
);
limb.position.set(px, py, pz);
limb.castShadow = true;
g.add(limb);
return limb;
}
const leftArm = limb(.16, 1.55, skin, -.78, 3.38, 0);
const rightArm = limb(.16, 1.55, skin, .78, 3.38, 0);
const leftLeg = limb(.23, 1.75, black, -.38, 1.12, 0);
const rightLeg = limb(.23, 1.75, black, .38, 1.12, 0);
const shoeL = new THREE.Mesh(new THREE.BoxGeometry(.52,.2,.95), white);
shoeL.position.set(-.38, .18, .13);
g.add(shoeL);
const shoeR = shoeL.clone();
shoeR.position.x = .38;
g.add(shoeR);
const label = textSprite(name, "#fff5d8");
label.position.set(0, 6.1, 0);
g.add(label);
return { group: g, torso, leftArm, rightArm, leftLeg, rightLeg };
}
const bird = makePlayer({
name: "LARRY BIRD", jersey: new THREE.MeshStandardMaterial({ color: 0x2b8b55 }),
skin: skinLight,
hair: new THREE.MeshStandardMaterial({ color: 0xd8b55d }),
x: -3.7, z: 1.8
});
const drj = makePlayer({
name: "DR. J", jersey: new THREE.MeshStandardMaterial({ color: 0xffffff }),
skin: skinDark,
hair: new THREE.MeshStandardMaterial({ color: 0x17110f }),
x: 2.3, z: .4
});
// Janitor
const janitor = new THREE.Group();
janitor.position.set(-11.1, 0, -8.8);
scene.add(janitor);
const janitorBody = new THREE.Mesh(
new THREE.CylinderGeometry(.52, .65, 1.75, 12),
blue
);
janitorBody.position.y = 2.7;
janitor.add(janitorBody);
const janitorHead = new THREE.Mesh(
new THREE.SphereGeometry(.42, 14, 12),
skinDark
);
janitorHead.position.y = 4.05;
janitor.add(janitorHead);
const cap = new THREE.Mesh(
new THREE.CylinderGeometry(.45,.45,.16,16),
black
);
cap.position.y = 4.4;
janitor.add(cap);
const mop = new THREE.Group();
mop.position.set(.9, 1.6, .2);
janitor.add(mop);
const mopHandle = new THREE.Mesh(
new THREE.CylinderGeometry(.045,.045,3.6,8),
new THREE.MeshStandardMaterial({ color: 0x9c6d3a })
);
mopHandle.rotation.z = -.36;
mopHandle.position.y = 1.25;
mop.add(mopHandle);
const mopHead = new THREE.Mesh(
new THREE.BoxGeometry(1.35,.13,.42),
new THREE.MeshStandardMaterial({ color: 0xd2bd77 })
);
mopHead.position.set(-.62, -.38, 0);
mop.add(mopHead);
const janitorLabel = textSprite("JANITOR", "#ffe7a3");
janitorLabel.position.y = 5.05;
janitor.add(janitorLabel);
// ------------------------------------------------------------
// Basketball
// ------------------------------------------------------------
const ball = new THREE.Mesh(
new THREE.SphereGeometry(.32, 20, 16),
orange
);
ball.castShadow = true;
ball.position.set(-3.3, 1.25, 1.7);
scene.add(ball);
const seamMat = new THREE.MeshBasicMaterial({ color: 0x47200c });
const seam1 = new THREE.Mesh(new THREE.TorusGeometry(.326,.012,5,24), seamMat);
ball.add(seam1);
const seam2 = seam1.clone();
seam2.rotation.x = Math.PI / 2;
ball.add(seam2);
// ------------------------------------------------------------
// Sound effects: procedural, no external audio files
// ------------------------------------------------------------
let audioCtx = null;
let soundEnabled = false;
function initAudio() {
if (!audioCtx) audioCtx = new AudioContext();
audioCtx.resume();
soundEnabled = true;
}
function tone(freq, duration, type = "sine", gain = .08, slide = 1) {
if (!soundEnabled) return;
const osc = audioCtx.createOscillator();
const amp = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(
Math.max(10, freq * slide), audioCtx.currentTime + duration
);
amp.gain.setValueAtTime(gain, audioCtx.currentTime);
amp.gain.exponentialRampToValueAtTime(.001, audioCtx.currentTime + duration);
osc.connect(amp).connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function noise(duration, gain = .12, highpass = 500) {
if (!soundEnabled) return;
const size = Math.floor(audioCtx.sampleRate * duration);
const buffer = audioCtx.createBuffer(1, size, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < size; i++) data[i] = Math.random() * 2 - 1;
const source = audioCtx.createBufferSource();
const filter = audioCtx.createBiquadFilter();
const amp = audioCtx.createGain();
filter.type = "highpass";
filter.frequency.value = highpass;
amp.gain.setValueAtTime(gain, audioCtx.currentTime);
amp.gain.exponentialRampToValueAtTime(.001, audioCtx.currentTime + duration);
source.buffer = buffer;
source.connect(filter).connect(amp).connect(audioCtx.destination);
source.start();
}
function bounceSound() { tone(105, .08, "sine", .13, .46); }
function swishSound() { noise(.22, .075, 1800); tone(780, .12, "sine", .025, .55); }
function glassSound() {
noise(.7, .28, 1200);
for (let i = 0; i < 8; i++) {
setTimeout(() => tone(1000 + Math.random()*2300, .18, "triangle", .035, .55), i * 38);
}
}
// ------------------------------------------------------------
// Sequence / action
// ------------------------------------------------------------
const birdScoreEl = document.querySelector("#birdScore");
const jScoreEl = document.querySelector("#jScore");
const captionEl = document.querySelector("#caption");
const statusEl = document.querySelector("#status");
let birdScore = 0;
let sceneStarted = false;
let startTime = 0;
let lastBounce = -1;
let scoredThisRound = false;
function caption(message, seconds = 1.8) {
captionEl.textContent = message;
captionEl.style.opacity = 1;
clearTimeout(captionEl._timer);
captionEl._timer = setTimeout(() => captionEl.style.opacity = 0, seconds * 1000);
}
function resetPlay() {
bird.group.position.set(-3.7, 0, 1.8);
drj.group.position.set(2.3, 0, .4);
ball.position.set(-3.2, 1.15, 1.5);
scoredThisRound = false;
}
function breakGlass() {
if (glassBroken) return;
glassBroken = true;
intactPane.visible = false;
glassSound();
caption("THE JANITOR HAS SEEN ENOUGH.", 2.8);
shards.forEach((shard, i) => {
shard.visible = true;
shard.position.set(
-10.5 + (Math.random() - .5) * 5.6,
7.3 + (Math.random() - .5) * 4.8,
-12.75 + Math.random() * .22
);
shard.userData.velocity.set(
(Math.random() - .5) * 7,
Math.random() * 5 + 1,
Math.random() * 3 + .5
);
shard.rotation.set(Math.random()*5, Math.random()*5, Math.random()*5);
});
janitor.rotation.z = -.22;
mop.rotation.z = .65;
}
function animatePlay(elapsed, dt) {
const cycle = elapsed % 10;
// Reset / possession
if (cycle < .15 && elapsed > .15) resetPlay();
// Bird dribbles and drives first
if (cycle < 3.2) {
const p = cycle / 3.2;
bird.group.position.x = -3.7 + p * 2.1;
bird.group.position.z = 1.8 - p * 2.0;
drj.group.position.x = 2.3 - p * 1.45;
drj.group.position.z = .4 + Math.sin(p * Math.PI) * .6;
const b = Math.abs(Math.sin(cycle * 7.5));
ball.position.set(
bird.group.position.x + .5,
.33 + b * 1.1,
bird.group.position.z + .2
);
bird.rightArm.rotation.z = -.55 + Math.sin(cycle*7.5)*.32;
drj.leftArm.rotation.z = .55;
const tick = Math.floor(cycle * 7.5 / Math.PI);
if (tick !== lastBounce) {
bounceSound();
lastBounce = tick;
}
}
// Shot trajectory to basket
if (cycle >= 3.2 && cycle < 4.8) {
const p = (cycle - 3.2) / 1.6;
const start = new THREE.Vector3(-1.55, 1.6, -.2);
const end = new THREE.Vector3(0, 5.18, -9.2);
ball.position.lerpVectors(start, end, p);
ball.position.y += Math.sin(p * Math.PI) * 3.1;
ball.rotation.x += .32;
bird.rightArm.rotation.z = -1.55 + p*.4;
drj.leftArm.rotation.z = .8 + p*.7;
}
// Swish then window break
if (cycle >= 4.8 && cycle < 5.3 && !scoredThisRound) {
scoredThisRound = true;
birdScore++;
birdScoreEl.textContent = String(birdScore).padStart(2, "0");
swishSound();
caption("BIRD FOR THE WIN.", 1.8);
}
if (cycle > 5.7 && !glassBroken) breakGlass();
// Dr J gets a brief response pose afterward
if (cycle >= 6.4) {
drj.group.position.x = 1.1 + Math.sin(cycle * 1.7) * .35;
drj.rightArm.rotation.z = -.9;
ball.position.set(0, 5.2, -9.1);
}
// Ambient character motion
bird.torso.rotation.y = Math.sin(elapsed*2.0) * .08;
drj.torso.rotation.y = Math.sin(elapsed*1.5+.7) * .08;
janitor.position.x = -11.1 + Math.sin(elapsed*.55)*.22;
janitor.rotation.y = .22 + Math.sin(elapsed*.55)*.12;
// Shards fall after glass break
if (glassBroken) {
shards.forEach(shard => {
if (!shard.visible) return;
shard.userData.velocity.y -= 11.5 * dt;
shard.position.addScaledVector(shard.userData.velocity, dt);
shard.rotation.x += dt * 8;
shard.rotation.y += dt * 11;
if (shard.position.y < .15) {
shard.position.y = .15;
shard.userData.velocity.y *= -.22;
shard.userData.velocity.x *= .72;
shard.userData.velocity.z *= .72;
}
});
}
}
// ------------------------------------------------------------
// UI
// ------------------------------------------------------------
document.querySelector("#start").addEventListener("click", () => {
initAudio();
if (!sceneStarted) {
sceneStarted = true;
startTime = performance.now() / 1000;
statusEl.textContent = "Bird drives left. Dr. J contests. The janitor watches the window.";
caption("ONE-ON-ONE. NO EXCUSES.", 2.2);
document.querySelector("#start").textContent = "SOUND ENABLED";
}
});
addEventListener("keydown", (event) => {
if (event.code === "Space") {
event.preventDefault();
initAudio();
breakGlass();
}
});
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// ------------------------------------------------------------
// Render loop
// ------------------------------------------------------------
let previous = performance.now() / 1000;
function render() {
requestAnimationFrame(render);
const now = performance.now() / 1000;
const dt = Math.min(.04, now - previous);
previous = now;
if (sceneStarted) animatePlay(now - startTime, dt);
controls.update();
renderer.render(scene, camera);
}
render();
</script>
</body>
</html>