Command Palette

Search for a command to run...

Hectal
Course outline
Topic 3.7Beginner ~21 min · step 7 of 7

Build: The World Modules

In one line

Split the showroom's single main.js into small modules (renderer, scene, camera, lights, loop), add the stand module and the bike's seat tube, and make the canvas follow its container.

0/7 · 0%

Before you start

You will learn

  • Split the showroom into world modules with one job each
  • Run a loop with delta time and a resize that follows #app
  • Add the seat tube to the bike

You write code in

In your lumen-3d project: add or replace these files, then run npm run dev

Files you touch

  • index.html
  • src/main.js
  • src/world/renderer.js
  • src/world/scene.js
  • src/world/camera.js
  • src/world/lights.js
  • src/world/loop.js
  • src/objects/showroom.js
  • src/objects/bike.js

Think of it like this

A workshop where every tool has its place on the wall. At first you work with a few tools in a heap on the bench. As the job grows you hang each one in its spot, so anyone can find it and put it back. Today the showroom gets its wall.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

module
A JavaScript file that exports functions or values. Other files import them by path.
updatable
In this course: any object with a tick(delta) method that the loop calls once per frame.
turntable
A group holding the stand and the bike, so rotating one object turns them both.
seat tube
The frame tube that runs from the bottom bracket up to the seat. This phase's bike part.

Step by step

01Create the files

In lumen-3d/src/ make a folder world with renderer.js, scene.js, camera.js, lights.js and loop.js, and put showroom.js next to bike.js in objects. Paste each file below. The code is the Phase 2 main.js, cut at its seams. index.html is listed so you have the whole project: it is the same as in Phase 1.

02Replace main.js

The new src/main.js imports the creators, checks WebGL 2 (from Phase 1), creates the pieces, groups the stand and bike on a turntable, starts the loop and observes the container.

03Update bike.js

createBike() adds one more call to tubeBetween: the seat tube, named "seat-tube". Open the scene tree and look for it under bike.

04Run it

npm run dev, open the address and resize the window. The bike turns on the stand and stays in proportion. The live example below shows 5 draw calls: the stand, its mark and three tubes.

Try it yourself

  1. 1

    Add a second updatable

    In the live example add loop.add({ tick: () => { camera.position.y = 1.2 + Math.sin(performance.now() / 1000) * 0.1; camera.lookAt(0, 0.4, 0); } }); after loop.add(turntable);. The camera bobs gently: anything with a tick(delta) can join the loop without a new loop.

  2. 2

    Stop the loop

    After loop.start(); add setTimeout(() => loop.stop(), 500); and two lines that log renderer.info.render.frame after 800 ms and after 1200 ms. Both print the same number: stop() called setAnimationLoop(null), so no frame is drawn any more and the stand freezes. Use it when the canvas scrolls out of view.

Code & diagrams

Build: the showroom after Phase 3 (all files in one example) New tab

Local imports cannot run in the preview, so this example has the code of every project file inline, each under a comment with its path. The files below are the same code, split.

idle

Console

Run the scene to see its console.

Expected console output

bike parts: down-tube, top-tube, seat-tube
draw calls: 5 | triangles: 460

Your project files

Your lumen-3d project after this topic. Click a file to see it in full.

index.htmlhtml

The whole file after this topic. If yours differs, you can copy this one and carry on.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lumen Bikes · 3D showroom</title>
    <style>
      html, body { margin: 0; height: 100%; background: #0b1118; color: #e6edf6; font-family: system-ui, sans-serif; }
      #app { position: fixed; inset: 0; }
      canvas { display: block; }
      #fallback { position: fixed; inset: 0; display: grid; place-content: center; justify-items: center; gap: 12px; padding: 16px; text-align: center; background: #0b1118; }
      #fallback[hidden] { display: none; }
      #fallback img { max-width: min(100%, 420px); max-height: 60%; border-radius: 8px; }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <div id="fallback" hidden>
      <p>Your browser can't draw 3D here, so here is a picture of the Lumen bike instead.</p>
      <img src="/data/3d/showroom-poster.png" alt="The Lumen Bikes showroom: an orange e-bike on a round display stand" />
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A missing export

In your src/world/camera.js remove the word export in front of resizeCamera, then save.

terminal
$ npm run dev (browser console)
── what you'll see ──
Uncaught SyntaxError: The requested module '/src/world/camera.js' does not provide an export named 'resizeCamera'
Vite's overlay shows a similar message. The page stays blank.

Myth vs fact

Myth

More files make the code slower.

Fact

Vite joins modules for the browser in production, and the code that runs is the same. Files are for people.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Keep creators free of side effects beyond what they say: createRenderer(container) appends the canvas (its job); createLights(scene) adds to the scene (its job). A creator that also starts the loop or reads window makes tests harder (Phase 19).

  • ▸

    updatables is a plain array: add pushes, and there is no remove (the practice question adds one). Add it only when something must leave the loop.

Remember this

  1. 1

    The problem this solves: the story's trouble came from one file where nothing had an owner. Kofi could not change the pixel ratio without reading the loop. Every later phase adds more (controls, a panel, loading); without a split main.js would become a thousand lines nobody dares touch.

  2. 2

    What this phase adds. In src/world/: renderer.js (createRenderer, resizeRenderer), scene.js, camera.js (createCamera, resizeCamera), lights.js and loop.js (createLoop returning start(), stop(), add(updatable)); src/objects/showroom.js (createStand); and the bike's seat tube, from the bottom bracket (0, 0.30, 0) to the seat cluster (−0.10, 0.82, 0), radius 0.020, named "seat-tube". main.js becomes wiring.

  3. 3

    How it plugs in. The stand moves into showroom.js unchanged. tubeBetween, the down-tube and the top-tube keep their points, which now sit inside createBike as local constants instead of Phase 2's exported POINTS (nothing imports it). Phase 1's supportsWebGL2(), #fallback and startShowroom() stay. The stand and bike share a turntable group whose tick(delta) turns 0.25 radians a second: one turn in about 25 s, the same speed as Phase 2's time / 4000, now at any refresh rate.

  4. 4

    Resize and delta. The loop uses a connected THREE.Timer (Topic 3.2) and passes delta to everything added with loop.add. main.js observes #app with a ResizeObserver (Topic 3.3), skips a 0-pixel box, and the pixel ratio is capped at 2.

  5. 5

    What the showroom can do now: every part has a home; the loop reports a real delta; the canvas keeps its proportions in any container; a hidden tab does not make the stand jump. Still missing: the rear triangle (Phase 4), real materials and light (Phase 5), wheels (Phases 6–7), controls (Phase 8). Today the frame is three orange tubes on a stand.

Explain it without notes

01

Why do renderer.js and camera.js each export a resize function instead of one shared resize()?

02

What does the loop's add(updatable) buy compared with calling stand.rotation.y += ... in the loop itself?

Practice

01

Give createLoop a way to remove an updatable. Write the new method.

02

Make the turntable turn at one full turn per 10 seconds.

Done when you can

  • Your project has the five src/world/ files, showroom.js and the updated bike.js

  • The stand and bike turn together at the same speed on any screen

  • The canvas keeps its proportions when you resize or hide and show it

  • The scene tree shows down-tube, top-tube and seat-tube under bike