Spaces:
Running on Zero
Running on Zero
Enhance interactable features and visual labels in the scene. Updated index.html to add a new collection label, modified main.js to manage hover interactions for both vending machine and cassette collection, and adjusted style.css for consistent label styling. Improved world.js to include outlines for the cassette pile and walkman, enabling better user interaction.
Browse files- frontend/index.html +2 -1
- frontend/main.js +84 -26
- frontend/style.css +5 -5
- frontend/world.js +51 -8
frontend/index.html
CHANGED
|
@@ -21,7 +21,8 @@
|
|
| 21 |
<h1 id="title">LoFinity</h1>
|
| 22 |
<p id="subtitle">♪ chill beats, freshly vended</p>
|
| 23 |
</div>
|
| 24 |
-
<div id="vending-label"><span>♪ vend a vibe</span></div>
|
|
|
|
| 25 |
|
| 26 |
<div id="machine-modal" class="hidden">
|
| 27 |
<button id="modal-close" aria-label="close">×</button>
|
|
|
|
| 21 |
<h1 id="title">LoFinity</h1>
|
| 22 |
<p id="subtitle">♪ chill beats, freshly vended</p>
|
| 23 |
</div>
|
| 24 |
+
<div id="vending-label" class="hover-label"><span>♪ vend a vibe</span></div>
|
| 25 |
+
<div id="collection-label" class="hover-label"><span>♪ cassette collection</span></div>
|
| 26 |
|
| 27 |
<div id="machine-modal" class="hidden">
|
| 28 |
<button id="modal-close" aria-label="close">×</button>
|
frontend/main.js
CHANGED
|
@@ -125,17 +125,37 @@ world.bird.matrixAutoUpdate = true;
|
|
| 125 |
world.bird.userData.tail.matrixAutoUpdate = true;
|
| 126 |
|
| 127 |
// ---------------------------------------------------------------------------
|
| 128 |
-
//
|
| 129 |
// ---------------------------------------------------------------------------
|
| 130 |
|
| 131 |
const raycaster = new THREE.Raycaster();
|
| 132 |
const pointerNDC = new THREE.Vector2(-2, -2); // offscreen until first move
|
| 133 |
let pointerActive = false;
|
| 134 |
-
let
|
| 135 |
|
| 136 |
-
const vendingLabel = document.getElementById("vending-label");
|
| 137 |
const labelAnchor = new THREE.Vector3();
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
window.addEventListener("pointermove", (e) => {
|
| 141 |
pointerNDC.set(
|
|
@@ -148,42 +168,57 @@ document.addEventListener("pointerleave", () => {
|
|
| 148 |
pointerActive = false;
|
| 149 |
});
|
| 150 |
|
| 151 |
-
function
|
| 152 |
-
if (
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
-
function
|
| 160 |
if (intro.phase !== "idle" || view.mode !== "scene" || !pointerActive) {
|
| 161 |
-
|
| 162 |
return;
|
| 163 |
}
|
| 164 |
raycaster.setFromCamera(pointerNDC, camera);
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
if (
|
| 168 |
-
labelAnchor.copy(
|
| 169 |
-
labelAnchor.y +=
|
| 170 |
labelAnchor.project(camera);
|
| 171 |
-
|
| 172 |
-
|
| 173 |
}
|
| 174 |
}
|
| 175 |
|
| 176 |
// ---------------------------------------------------------------------------
|
| 177 |
-
//
|
|
|
|
| 178 |
// ---------------------------------------------------------------------------
|
| 179 |
|
| 180 |
// Frames the machine on the left so the modal card sits beside it.
|
| 181 |
const MACHINE_CAM = new THREE.Vector3(-2.3, 3.0, 10.8);
|
| 182 |
const MACHINE_LOOK = new THREE.Vector3(-2.9, 2.6, 4.2);
|
|
|
|
|
|
|
|
|
|
| 183 |
const ZOOM_MS = 1500;
|
| 184 |
|
| 185 |
const view = {
|
| 186 |
-
mode: "scene", // scene | zoom-in | machine | zoom-out
|
|
|
|
| 187 |
t0: 0,
|
| 188 |
camFrom: new THREE.Vector3(),
|
| 189 |
camTo: new THREE.Vector3(),
|
|
@@ -201,21 +236,38 @@ function startViewTransition(mode, camTo, lookTo) {
|
|
| 201 |
}
|
| 202 |
|
| 203 |
function zoomToMachine() {
|
| 204 |
-
|
|
|
|
| 205 |
startViewTransition("zoom-in", MACHINE_CAM, MACHINE_LOOK);
|
| 206 |
}
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
function closeMachineView() {
|
| 209 |
ui.closeModal();
|
| 210 |
startViewTransition("zoom-out", CAM_END, LOOK_SCENE);
|
| 211 |
}
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
canvas.addEventListener("click", () => {
|
| 214 |
-
if (
|
| 215 |
-
|
|
|
|
|
|
|
| 216 |
}
|
| 217 |
});
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
// Machine glow while generating
|
| 220 |
let machineBusy = false;
|
| 221 |
const SCREEN_BASE = new THREE.Color(0x1c2f28);
|
|
@@ -317,6 +369,12 @@ window.lofinityDebug = {
|
|
| 317 |
closeMachine() {
|
| 318 |
closeMachineView();
|
| 319 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
stats() {
|
| 321 |
return { ...renderer.info.render, ...renderer.info.memory };
|
| 322 |
},
|
|
@@ -392,8 +450,8 @@ function animate(now = 0) {
|
|
| 392 |
lookTarget.lerpVectors(view.lookFrom, view.lookTo, e);
|
| 393 |
if (t >= 1) {
|
| 394 |
if (view.mode === "zoom-in") {
|
| 395 |
-
view.mode =
|
| 396 |
-
ui.openModal();
|
| 397 |
} else {
|
| 398 |
view.mode = "scene";
|
| 399 |
intro.idleStart = elapsed; // sway ramps back in
|
|
@@ -415,7 +473,7 @@ function animate(now = 0) {
|
|
| 415 |
}
|
| 416 |
|
| 417 |
camera.lookAt(lookTarget);
|
| 418 |
-
|
| 419 |
renderer.render(scene, camera);
|
| 420 |
}
|
| 421 |
animate();
|
|
|
|
| 125 |
world.bird.userData.tail.matrixAutoUpdate = true;
|
| 126 |
|
| 127 |
// ---------------------------------------------------------------------------
|
| 128 |
+
// Interactables: hover outline + floating label, click hook
|
| 129 |
// ---------------------------------------------------------------------------
|
| 130 |
|
| 131 |
const raycaster = new THREE.Raycaster();
|
| 132 |
const pointerNDC = new THREE.Vector2(-2, -2); // offscreen until first move
|
| 133 |
let pointerActive = false;
|
| 134 |
+
let hoveredItem = null;
|
| 135 |
|
|
|
|
| 136 |
const labelAnchor = new THREE.Vector3();
|
| 137 |
+
|
| 138 |
+
// anchors are precomputed: nothing interactable ever moves
|
| 139 |
+
const collectionAnchor = world.collection.getWorldPosition(new THREE.Vector3());
|
| 140 |
+
|
| 141 |
+
const interactables = [
|
| 142 |
+
{
|
| 143 |
+
object: world.vending,
|
| 144 |
+
outlines: [world.vending.userData.outline],
|
| 145 |
+
label: document.getElementById("vending-label"),
|
| 146 |
+
anchor: world.vending.position.clone(),
|
| 147 |
+
labelOffsetY: 5.6, // above the machine top
|
| 148 |
+
onClick: () => zoomToMachine(),
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
object: world.collection,
|
| 152 |
+
outlines: world.collection.userData.outlines,
|
| 153 |
+
label: document.getElementById("collection-label"),
|
| 154 |
+
anchor: collectionAnchor,
|
| 155 |
+
labelOffsetY: 1.15, // above the bench backrest
|
| 156 |
+
onClick: () => zoomToBench(),
|
| 157 |
+
},
|
| 158 |
+
];
|
| 159 |
|
| 160 |
window.addEventListener("pointermove", (e) => {
|
| 161 |
pointerNDC.set(
|
|
|
|
| 168 |
pointerActive = false;
|
| 169 |
});
|
| 170 |
|
| 171 |
+
function setHoveredItem(item) {
|
| 172 |
+
if (item === hoveredItem) return;
|
| 173 |
+
if (hoveredItem) {
|
| 174 |
+
for (const outline of hoveredItem.outlines) outline.visible = false;
|
| 175 |
+
hoveredItem.label.classList.remove("visible");
|
| 176 |
+
}
|
| 177 |
+
hoveredItem = item;
|
| 178 |
+
if (item) {
|
| 179 |
+
for (const outline of item.outlines) outline.visible = true;
|
| 180 |
+
item.label.classList.add("visible");
|
| 181 |
+
}
|
| 182 |
+
canvas.style.cursor = item ? "pointer" : "";
|
| 183 |
}
|
| 184 |
|
| 185 |
+
function updateHover() {
|
| 186 |
if (intro.phase !== "idle" || view.mode !== "scene" || !pointerActive) {
|
| 187 |
+
setHoveredItem(null);
|
| 188 |
return;
|
| 189 |
}
|
| 190 |
raycaster.setFromCamera(pointerNDC, camera);
|
| 191 |
+
setHoveredItem(
|
| 192 |
+
interactables.find(
|
| 193 |
+
(item) => raycaster.intersectObject(item.object, true).length > 0
|
| 194 |
+
) ?? null
|
| 195 |
+
);
|
| 196 |
|
| 197 |
+
if (hoveredItem) {
|
| 198 |
+
labelAnchor.copy(hoveredItem.anchor);
|
| 199 |
+
labelAnchor.y += hoveredItem.labelOffsetY;
|
| 200 |
labelAnchor.project(camera);
|
| 201 |
+
hoveredItem.label.style.left = `${((labelAnchor.x + 1) / 2) * window.innerWidth}px`;
|
| 202 |
+
hoveredItem.label.style.top = `${((1 - labelAnchor.y) / 2) * window.innerHeight}px`;
|
| 203 |
}
|
| 204 |
}
|
| 205 |
|
| 206 |
// ---------------------------------------------------------------------------
|
| 207 |
+
// Zoom views: machine (modal while zoomed) and bench (a close look at the
|
| 208 |
+
// tapes). Click zooms in, close/Esc/click zooms back out.
|
| 209 |
// ---------------------------------------------------------------------------
|
| 210 |
|
| 211 |
// Frames the machine on the left so the modal card sits beside it.
|
| 212 |
const MACHINE_CAM = new THREE.Vector3(-2.3, 3.0, 10.8);
|
| 213 |
const MACHINE_LOOK = new THREE.Vector3(-2.9, 2.6, 4.2);
|
| 214 |
+
// Leans over the bench seat where the tapes and walkman rest.
|
| 215 |
+
const BENCH_CAM = new THREE.Vector3(1.3, 3.3, 8.4);
|
| 216 |
+
const BENCH_LOOK = new THREE.Vector3(1.3, 1.15, 4.4);
|
| 217 |
const ZOOM_MS = 1500;
|
| 218 |
|
| 219 |
const view = {
|
| 220 |
+
mode: "scene", // scene | zoom-in | machine | bench | zoom-out
|
| 221 |
+
target: "machine", // what the current zoom-in lands on
|
| 222 |
t0: 0,
|
| 223 |
camFrom: new THREE.Vector3(),
|
| 224 |
camTo: new THREE.Vector3(),
|
|
|
|
| 236 |
}
|
| 237 |
|
| 238 |
function zoomToMachine() {
|
| 239 |
+
setHoveredItem(null);
|
| 240 |
+
view.target = "machine";
|
| 241 |
startViewTransition("zoom-in", MACHINE_CAM, MACHINE_LOOK);
|
| 242 |
}
|
| 243 |
|
| 244 |
+
function zoomToBench() {
|
| 245 |
+
setHoveredItem(null);
|
| 246 |
+
view.target = "bench";
|
| 247 |
+
startViewTransition("zoom-in", BENCH_CAM, BENCH_LOOK);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
function closeMachineView() {
|
| 251 |
ui.closeModal();
|
| 252 |
startViewTransition("zoom-out", CAM_END, LOOK_SCENE);
|
| 253 |
}
|
| 254 |
|
| 255 |
+
function closeBenchView() {
|
| 256 |
+
startViewTransition("zoom-out", CAM_END, LOOK_SCENE);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
canvas.addEventListener("click", () => {
|
| 260 |
+
if (hoveredItem && view.mode === "scene" && intro.phase === "idle") {
|
| 261 |
+
hoveredItem.onClick();
|
| 262 |
+
} else if (view.mode === "bench") {
|
| 263 |
+
closeBenchView();
|
| 264 |
}
|
| 265 |
});
|
| 266 |
|
| 267 |
+
window.addEventListener("keydown", (e) => {
|
| 268 |
+
if (e.key === "Escape" && view.mode === "bench") closeBenchView();
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
// Machine glow while generating
|
| 272 |
let machineBusy = false;
|
| 273 |
const SCREEN_BASE = new THREE.Color(0x1c2f28);
|
|
|
|
| 369 |
closeMachine() {
|
| 370 |
closeMachineView();
|
| 371 |
},
|
| 372 |
+
openBench() {
|
| 373 |
+
this.skipIntro();
|
| 374 |
+
view.mode = "bench";
|
| 375 |
+
camera.position.copy(BENCH_CAM);
|
| 376 |
+
lookTarget.copy(BENCH_LOOK);
|
| 377 |
+
},
|
| 378 |
stats() {
|
| 379 |
return { ...renderer.info.render, ...renderer.info.memory };
|
| 380 |
},
|
|
|
|
| 450 |
lookTarget.lerpVectors(view.lookFrom, view.lookTo, e);
|
| 451 |
if (t >= 1) {
|
| 452 |
if (view.mode === "zoom-in") {
|
| 453 |
+
view.mode = view.target;
|
| 454 |
+
if (view.target === "machine") ui.openModal();
|
| 455 |
} else {
|
| 456 |
view.mode = "scene";
|
| 457 |
intro.idleStart = elapsed; // sway ramps back in
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
camera.lookAt(lookTarget);
|
| 476 |
+
updateHover();
|
| 477 |
renderer.render(scene, camera);
|
| 478 |
}
|
| 479 |
animate();
|
frontend/style.css
CHANGED
|
@@ -86,7 +86,7 @@ body {
|
|
| 86 |
}
|
| 87 |
}
|
| 88 |
|
| 89 |
-
|
| 90 |
position: fixed;
|
| 91 |
left: 0;
|
| 92 |
top: 0;
|
|
@@ -97,11 +97,11 @@ body {
|
|
| 97 |
transition: opacity 0.18s ease;
|
| 98 |
}
|
| 99 |
|
| 100 |
-
|
| 101 |
opacity: 1;
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
position: relative;
|
| 106 |
display: inline-block;
|
| 107 |
font-family: "Baloo 2", sans-serif;
|
|
@@ -115,7 +115,7 @@ body {
|
|
| 115 |
white-space: nowrap;
|
| 116 |
}
|
| 117 |
|
| 118 |
-
|
| 119 |
content: "";
|
| 120 |
position: absolute;
|
| 121 |
left: 50%;
|
|
@@ -128,7 +128,7 @@ body {
|
|
| 128 |
z-index: -1;
|
| 129 |
}
|
| 130 |
|
| 131 |
-
|
| 132 |
animation:
|
| 133 |
label-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both,
|
| 134 |
label-bob 2s ease-in-out 0.45s infinite alternate;
|
|
|
|
| 86 |
}
|
| 87 |
}
|
| 88 |
|
| 89 |
+
.hover-label {
|
| 90 |
position: fixed;
|
| 91 |
left: 0;
|
| 92 |
top: 0;
|
|
|
|
| 97 |
transition: opacity 0.18s ease;
|
| 98 |
}
|
| 99 |
|
| 100 |
+
.hover-label.visible {
|
| 101 |
opacity: 1;
|
| 102 |
}
|
| 103 |
|
| 104 |
+
.hover-label span {
|
| 105 |
position: relative;
|
| 106 |
display: inline-block;
|
| 107 |
font-family: "Baloo 2", sans-serif;
|
|
|
|
| 115 |
white-space: nowrap;
|
| 116 |
}
|
| 117 |
|
| 118 |
+
.hover-label span::after {
|
| 119 |
content: "";
|
| 120 |
position: absolute;
|
| 121 |
left: 50%;
|
|
|
|
| 128 |
z-index: -1;
|
| 129 |
}
|
| 130 |
|
| 131 |
+
.hover-label.visible span {
|
| 132 |
animation:
|
| 133 |
label-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both,
|
| 134 |
label-bob 2s ease-in-out 0.45s infinite alternate;
|
frontend/world.js
CHANGED
|
@@ -448,7 +448,27 @@ function buildCassettePile() {
|
|
| 448 |
if (i === placements.length - 1) cassette.rotation.z = 0.07;
|
| 449 |
g.add(cassette);
|
| 450 |
});
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
}
|
| 453 |
|
| 454 |
// --- walkman + vintage headphones ---------------------------------------------
|
|
@@ -472,7 +492,21 @@ function buildWalkman() {
|
|
| 472 |
button.position.set(0.19, 0.12, -0.1 + i * 0.1);
|
| 473 |
g.add(button);
|
| 474 |
});
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
}
|
| 477 |
|
| 478 |
function buildHeadphones() {
|
|
@@ -979,17 +1013,26 @@ export function buildWorld(scene) {
|
|
| 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(-
|
| 985 |
cassettes.rotation.y = -0.2;
|
| 986 |
-
|
| 987 |
|
| 988 |
// …with their walkman and headphones beside it
|
| 989 |
const walkman = buildWalkman();
|
| 990 |
-
walkman.position.set(0.
|
| 991 |
walkman.rotation.y = 0.35;
|
| 992 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 993 |
|
| 994 |
const headphones = buildHeadphones();
|
| 995 |
headphones.position.set(0.85, 1.05, -0.04);
|
|
@@ -1097,5 +1140,5 @@ export function buildWorld(scene) {
|
|
| 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 |
}
|
|
|
|
| 448 |
if (i === placements.length - 1) cassette.rotation.z = 0.07;
|
| 449 |
g.add(cassette);
|
| 450 |
});
|
| 451 |
+
shadows(g);
|
| 452 |
+
|
| 453 |
+
// Hover outline: one back-face hull per cassette so the highlight follows
|
| 454 |
+
// the silhouette of the pile (same trick as the vending machine).
|
| 455 |
+
const outline = new THREE.Group();
|
| 456 |
+
const outlineMaterial = new THREE.MeshBasicMaterial({
|
| 457 |
+
color: 0xffd84a,
|
| 458 |
+
side: THREE.BackSide,
|
| 459 |
+
});
|
| 460 |
+
placements.forEach(([x, y, z, yaw], i) => {
|
| 461 |
+
const hull = box(0.54, 0.16, 0.38, outlineMaterial);
|
| 462 |
+
hull.position.set(x, y + 0.04, z);
|
| 463 |
+
hull.rotation.y = yaw;
|
| 464 |
+
if (i === placements.length - 1) hull.rotation.z = 0.07;
|
| 465 |
+
outline.add(hull);
|
| 466 |
+
});
|
| 467 |
+
outline.visible = false;
|
| 468 |
+
g.add(outline);
|
| 469 |
+
g.userData.outline = outline;
|
| 470 |
+
|
| 471 |
+
return g;
|
| 472 |
}
|
| 473 |
|
| 474 |
// --- walkman + vintage headphones ---------------------------------------------
|
|
|
|
| 492 |
button.position.set(0.19, 0.12, -0.1 + i * 0.1);
|
| 493 |
g.add(button);
|
| 494 |
});
|
| 495 |
+
shadows(g);
|
| 496 |
+
|
| 497 |
+
// Hover outline hull (see buildCassettePile)
|
| 498 |
+
const outline = new THREE.Group();
|
| 499 |
+
const hull = box(0.6, 0.22, 0.46, new THREE.MeshBasicMaterial({
|
| 500 |
+
color: 0xffd84a,
|
| 501 |
+
side: THREE.BackSide,
|
| 502 |
+
}));
|
| 503 |
+
hull.position.y = 0.075;
|
| 504 |
+
outline.add(hull);
|
| 505 |
+
outline.visible = false;
|
| 506 |
+
g.add(outline);
|
| 507 |
+
g.userData.outline = outline;
|
| 508 |
+
|
| 509 |
+
return g;
|
| 510 |
}
|
| 511 |
|
| 512 |
function buildHeadphones() {
|
|
|
|
| 1013 |
bench.position.set(1.7, 0.3, 4.4);
|
| 1014 |
scene.add(bench);
|
| 1015 |
|
| 1016 |
+
// someone left their tape collection on the bench… (clickable as a group)
|
| 1017 |
+
const collection = new THREE.Group();
|
| 1018 |
+
collection.position.set(-0.4, 1.05, 0);
|
| 1019 |
+
bench.add(collection);
|
| 1020 |
+
|
| 1021 |
const cassettes = buildCassettePile();
|
| 1022 |
+
cassettes.position.set(-0.6, 0, -0.05);
|
| 1023 |
cassettes.rotation.y = -0.2;
|
| 1024 |
+
collection.add(cassettes);
|
| 1025 |
|
| 1026 |
// …with their walkman and headphones beside it
|
| 1027 |
const walkman = buildWalkman();
|
| 1028 |
+
walkman.position.set(0.55, 0, 0.02);
|
| 1029 |
walkman.rotation.y = 0.35;
|
| 1030 |
+
collection.add(walkman);
|
| 1031 |
+
|
| 1032 |
+
collection.userData.outlines = [
|
| 1033 |
+
cassettes.userData.outline,
|
| 1034 |
+
walkman.userData.outline,
|
| 1035 |
+
];
|
| 1036 |
|
| 1037 |
const headphones = buildHeadphones();
|
| 1038 |
headphones.position.set(0.85, 1.05, -0.04);
|
|
|
|
| 1140 |
const clouds = buildClouds();
|
| 1141 |
for (const cloud of clouds) scene.add(cloud);
|
| 1142 |
|
| 1143 |
+
return { vending, bench, collection, sign, lamp, bigTree, house, clouds, bird };
|
| 1144 |
}
|