Add bird animation and cassette pile to the scene. Updated main.js to include bird movement and tail flicking behavior, and enhanced world.js with new cassette pile, walkman, and headphones models. Integrated cable geometry for visual detail and adjusted bird positioning on the bench.
Browse files- frontend/main.js +26 -0
- frontend/world.js +167 -1
frontend/main.js
CHANGED
|
@@ -121,6 +121,8 @@ scene.traverse((obj) => {
|
|
| 121 |
obj.matrixAutoUpdate = false;
|
| 122 |
});
|
| 123 |
for (const cloud of world.clouds) cloud.matrixAutoUpdate = true;
|
|
|
|
|
|
|
| 124 |
|
| 125 |
// ---------------------------------------------------------------------------
|
| 126 |
// Vending machine interaction: hover outline + floating label, click hook
|
|
@@ -318,6 +320,21 @@ window.lofinityDebug = {
|
|
| 318 |
stats() {
|
| 319 |
return { ...renderer.info.render, ...renderer.info.memory };
|
| 320 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
};
|
| 322 |
|
| 323 |
// ---------------------------------------------------------------------------
|
|
@@ -347,6 +364,15 @@ function animate(now = 0) {
|
|
| 347 |
if (cloud.position.x > 170) cloud.position.x = -170;
|
| 348 |
}
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
if (intro.phase === "loading" || intro.phase === "held") {
|
| 351 |
// Barely-there float while gazing at the sky
|
| 352 |
camera.position.y = CAM_START.y + Math.sin(elapsed * 0.5) * 0.08;
|
|
|
|
| 121 |
obj.matrixAutoUpdate = false;
|
| 122 |
});
|
| 123 |
for (const cloud of world.clouds) cloud.matrixAutoUpdate = true;
|
| 124 |
+
world.bird.matrixAutoUpdate = true;
|
| 125 |
+
world.bird.userData.tail.matrixAutoUpdate = true;
|
| 126 |
|
| 127 |
// ---------------------------------------------------------------------------
|
| 128 |
// Vending machine interaction: hover outline + floating label, click hook
|
|
|
|
| 320 |
stats() {
|
| 321 |
return { ...renderer.info.render, ...renderer.info.memory };
|
| 322 |
},
|
| 323 |
+
goto(px, py, pz, lx, ly, lz) {
|
| 324 |
+
this.skipIntro();
|
| 325 |
+
camera.position.set(px, py, pz);
|
| 326 |
+
lookTarget.set(lx, ly, lz);
|
| 327 |
+
view.mode = "machine"; // parks the camera (no idle sway)
|
| 328 |
+
},
|
| 329 |
+
birdInfo() {
|
| 330 |
+
const b = world.bird;
|
| 331 |
+
return {
|
| 332 |
+
rotY: b.rotation.y,
|
| 333 |
+
posY: b.position.y,
|
| 334 |
+
auto: b.matrixAutoUpdate,
|
| 335 |
+
matrixRotY: Math.atan2(b.matrix.elements[8], b.matrix.elements[10]),
|
| 336 |
+
};
|
| 337 |
+
},
|
| 338 |
};
|
| 339 |
|
| 340 |
// ---------------------------------------------------------------------------
|
|
|
|
| 364 |
if (cloud.position.x > 170) cloud.position.x = -170;
|
| 365 |
}
|
| 366 |
|
| 367 |
+
// the bench bird looks around, hops now and then, flicks its tail
|
| 368 |
+
const bird = world.bird;
|
| 369 |
+
bird.rotation.y =
|
| 370 |
+
-0.5 + Math.sin(elapsed * 0.4) * 0.4 + Math.sin(elapsed * 1.1) * 0.12;
|
| 371 |
+
bird.position.y =
|
| 372 |
+
bird.userData.baseY + Math.pow(Math.max(0, Math.sin(elapsed * 1.9)), 12) * 0.06;
|
| 373 |
+
bird.userData.tail.rotation.x =
|
| 374 |
+
-0.35 + Math.pow(Math.max(0, Math.sin(elapsed * 1.3 + 1)), 8) * 0.5;
|
| 375 |
+
|
| 376 |
if (intro.phase === "loading" || intro.phase === "held") {
|
| 377 |
// Barely-there float while gazing at the sky
|
| 378 |
camera.position.y = CAM_START.y + Math.sin(elapsed * 0.5) * 0.08;
|
frontend/world.js
CHANGED
|
@@ -413,6 +413,136 @@ function buildBench() {
|
|
| 413 |
return shadows(g);
|
| 414 |
}
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
// --- lamp post ---------------------------------------------------------------
|
| 417 |
|
| 418 |
function buildLamp() {
|
|
@@ -849,6 +979,42 @@ export function buildWorld(scene) {
|
|
| 849 |
bench.position.set(1.7, 0.3, 4.4);
|
| 850 |
scene.add(bench);
|
| 851 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
const sign = buildSign();
|
| 853 |
sign.position.set(-8.3, 0.3, 4.2);
|
| 854 |
sign.rotation.y = 0.12;
|
|
@@ -931,5 +1097,5 @@ export function buildWorld(scene) {
|
|
| 931 |
const clouds = buildClouds();
|
| 932 |
for (const cloud of clouds) scene.add(cloud);
|
| 933 |
|
| 934 |
-
return { vending, bench, sign, lamp, bigTree, house, clouds };
|
| 935 |
}
|
|
|
|
| 413 |
return shadows(g);
|
| 414 |
}
|
| 415 |
|
| 416 |
+
// --- cassette pile -----------------------------------------------------------
|
| 417 |
+
|
| 418 |
+
function buildCassette(color) {
|
| 419 |
+
const g = new THREE.Group();
|
| 420 |
+
const shell = box(0.46, 0.075, 0.3, lambert(color));
|
| 421 |
+
shell.position.y = 0.0375;
|
| 422 |
+
g.add(shell);
|
| 423 |
+
const label = box(0.32, 0.012, 0.2, lambert(0xf4ecd8));
|
| 424 |
+
label.position.y = 0.078;
|
| 425 |
+
g.add(label);
|
| 426 |
+
const tapeWindow = box(0.16, 0.013, 0.08, lambert(0x1d232f));
|
| 427 |
+
tapeWindow.position.y = 0.079;
|
| 428 |
+
g.add(tapeWindow);
|
| 429 |
+
return g;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
function buildCassettePile() {
|
| 433 |
+
const g = new THREE.Group();
|
| 434 |
+
const colors = [0x2e3440, 0xf2e8d5, 0xd95d4e, 0x7cc4ea, 0xf2c84b, 0x4a3b2c];
|
| 435 |
+
// [x, y, z, yaw] — three flat, two crossed on top, one tilted at the summit
|
| 436 |
+
const placements = [
|
| 437 |
+
[0, 0, 0, 0.15],
|
| 438 |
+
[0.5, 0, 0.05, -0.25],
|
| 439 |
+
[-0.46, 0, -0.04, 0.4],
|
| 440 |
+
[0.22, 0.088, 0.02, 1.35],
|
| 441 |
+
[-0.28, 0.088, -0.03, -1.15],
|
| 442 |
+
[-0.02, 0.176, 0, 0.55],
|
| 443 |
+
];
|
| 444 |
+
placements.forEach(([x, y, z, yaw], i) => {
|
| 445 |
+
const cassette = buildCassette(colors[i % colors.length]);
|
| 446 |
+
cassette.position.set(x, y, z);
|
| 447 |
+
cassette.rotation.y = yaw;
|
| 448 |
+
if (i === placements.length - 1) cassette.rotation.z = 0.07;
|
| 449 |
+
g.add(cassette);
|
| 450 |
+
});
|
| 451 |
+
return shadows(g);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// --- walkman + vintage headphones ---------------------------------------------
|
| 455 |
+
|
| 456 |
+
function buildWalkman() {
|
| 457 |
+
const g = new THREE.Group();
|
| 458 |
+
const body = box(0.5, 0.12, 0.36, lambert(0xc7cdd4));
|
| 459 |
+
body.position.y = 0.06;
|
| 460 |
+
g.add(body);
|
| 461 |
+
|
| 462 |
+
const tapeWindow = box(0.3, 0.015, 0.2, lambert(0x1d232f));
|
| 463 |
+
tapeWindow.position.set(-0.06, 0.125, 0);
|
| 464 |
+
g.add(tapeWindow);
|
| 465 |
+
const tapeLabel = box(0.14, 0.012, 0.1, lambert(0xf4ecd8));
|
| 466 |
+
tapeLabel.position.set(-0.06, 0.133, 0);
|
| 467 |
+
g.add(tapeLabel);
|
| 468 |
+
|
| 469 |
+
// transport buttons along the edge, the red one is record
|
| 470 |
+
[0x3a4254, 0x3a4254, 0xd95d4e].forEach((color, i) => {
|
| 471 |
+
const button = box(0.05, 0.05, 0.06, lambert(color));
|
| 472 |
+
button.position.set(0.19, 0.12, -0.1 + i * 0.1);
|
| 473 |
+
g.add(button);
|
| 474 |
+
});
|
| 475 |
+
return shadows(g);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
function buildHeadphones() {
|
| 479 |
+
const g = new THREE.Group();
|
| 480 |
+
const band = new THREE.Mesh(
|
| 481 |
+
new THREE.TorusGeometry(0.2, 0.018, 8, 18, Math.PI),
|
| 482 |
+
lambert(0x6b7280)
|
| 483 |
+
);
|
| 484 |
+
band.rotation.x = -Math.PI / 2;
|
| 485 |
+
band.position.y = 0.035;
|
| 486 |
+
g.add(band);
|
| 487 |
+
for (const side of [-1, 1]) {
|
| 488 |
+
const cup = cyl(0.085, 0.085, 0.05, 12, lambert(0x3a4254));
|
| 489 |
+
cup.position.set(side * 0.2, 0.03, 0);
|
| 490 |
+
g.add(cup);
|
| 491 |
+
const foam = cyl(0.08, 0.08, 0.026, 12, lambert(0xe8893c));
|
| 492 |
+
foam.position.set(side * 0.2, 0.068, 0);
|
| 493 |
+
g.add(foam);
|
| 494 |
+
}
|
| 495 |
+
return shadows(g);
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// --- little bird ---------------------------------------------------------------
|
| 499 |
+
|
| 500 |
+
function buildBird() {
|
| 501 |
+
const g = new THREE.Group();
|
| 502 |
+
const cream = lambert(0xf5efe0, { flatShading: true });
|
| 503 |
+
const featherMaterial = lambert(0xd9cdb4, { flatShading: true });
|
| 504 |
+
|
| 505 |
+
const body = ico(0.13, 1, cream);
|
| 506 |
+
body.scale.set(1, 0.95, 1.2);
|
| 507 |
+
body.position.y = 0.12;
|
| 508 |
+
g.add(body);
|
| 509 |
+
|
| 510 |
+
const breast = ico(0.085, 1, lambert(0xe8893c, { flatShading: true }));
|
| 511 |
+
breast.position.set(0, 0.07, 0.07);
|
| 512 |
+
g.add(breast);
|
| 513 |
+
|
| 514 |
+
const beak = new THREE.Mesh(new THREE.ConeGeometry(0.025, 0.07, 6), lambert(0xd9a13c));
|
| 515 |
+
beak.rotation.x = Math.PI / 2;
|
| 516 |
+
beak.position.set(0, 0.15, 0.17);
|
| 517 |
+
g.add(beak);
|
| 518 |
+
|
| 519 |
+
for (const side of [-1, 1]) {
|
| 520 |
+
const eye = ico(0.018, 0, lambert(0x1d232f));
|
| 521 |
+
eye.position.set(side * 0.055, 0.17, 0.115);
|
| 522 |
+
g.add(eye);
|
| 523 |
+
|
| 524 |
+
const wing = ico(0.07, 1, featherMaterial);
|
| 525 |
+
wing.scale.set(0.5, 0.7, 1.3);
|
| 526 |
+
wing.position.set(side * 0.115, 0.12, -0.01);
|
| 527 |
+
g.add(wing);
|
| 528 |
+
|
| 529 |
+
const leg = cyl(0.012, 0.012, 0.05, 5, lambert(0xb3823c));
|
| 530 |
+
leg.position.set(side * 0.04, 0.025, 0.02);
|
| 531 |
+
g.add(leg);
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
const tail = new THREE.Group();
|
| 535 |
+
const feathers = box(0.07, 0.02, 0.14, featherMaterial);
|
| 536 |
+
feathers.position.z = -0.07;
|
| 537 |
+
tail.add(feathers);
|
| 538 |
+
tail.position.set(0, 0.12, -0.13);
|
| 539 |
+
tail.rotation.x = -0.35;
|
| 540 |
+
g.add(tail);
|
| 541 |
+
g.userData.tail = tail;
|
| 542 |
+
|
| 543 |
+
return shadows(g);
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
// --- lamp post ---------------------------------------------------------------
|
| 547 |
|
| 548 |
function buildLamp() {
|
|
|
|
| 979 |
bench.position.set(1.7, 0.3, 4.4);
|
| 980 |
scene.add(bench);
|
| 981 |
|
| 982 |
+
// someone left their tape collection on the bench…
|
| 983 |
+
const cassettes = buildCassettePile();
|
| 984 |
+
cassettes.position.set(-1.0, 1.05, -0.05);
|
| 985 |
+
cassettes.rotation.y = -0.2;
|
| 986 |
+
bench.add(cassettes);
|
| 987 |
+
|
| 988 |
+
// …with their walkman and headphones beside it
|
| 989 |
+
const walkman = buildWalkman();
|
| 990 |
+
walkman.position.set(0.15, 1.05, 0.02);
|
| 991 |
+
walkman.rotation.y = 0.35;
|
| 992 |
+
bench.add(walkman);
|
| 993 |
+
|
| 994 |
+
const headphones = buildHeadphones();
|
| 995 |
+
headphones.position.set(0.85, 1.05, -0.04);
|
| 996 |
+
headphones.rotation.y = -0.6;
|
| 997 |
+
bench.add(headphones);
|
| 998 |
+
|
| 999 |
+
const cableCurve = new THREE.CatmullRomCurve3([
|
| 1000 |
+
new THREE.Vector3(0.38, 1.1, 0.1),
|
| 1001 |
+
new THREE.Vector3(0.52, 1.07, 0.22),
|
| 1002 |
+
new THREE.Vector3(0.685, 1.08, 0.07),
|
| 1003 |
+
]);
|
| 1004 |
+
const cable = new THREE.Mesh(
|
| 1005 |
+
new THREE.TubeGeometry(cableCurve, 16, 0.01, 5),
|
| 1006 |
+
lambert(0x2b3138)
|
| 1007 |
+
);
|
| 1008 |
+
cable.castShadow = true;
|
| 1009 |
+
bench.add(cable);
|
| 1010 |
+
|
| 1011 |
+
// a little bird perches on the backrest
|
| 1012 |
+
const bird = buildBird();
|
| 1013 |
+
bird.position.set(1.15, 2.11, -0.62);
|
| 1014 |
+
bird.rotation.y = -0.5;
|
| 1015 |
+
bird.userData.baseY = bird.position.y;
|
| 1016 |
+
bench.add(bird);
|
| 1017 |
+
|
| 1018 |
const sign = buildSign();
|
| 1019 |
sign.position.set(-8.3, 0.3, 4.2);
|
| 1020 |
sign.rotation.y = 0.12;
|
|
|
|
| 1097 |
const clouds = buildClouds();
|
| 1098 |
for (const cloud of clouds) scene.add(cloud);
|
| 1099 |
|
| 1100 |
+
return { vending, bench, sign, lamp, bigTree, house, clouds, bird };
|
| 1101 |
}
|