Course outline
Build: A Support Check
In one line
The showroom now asks the browser whether it can draw 3D before it builds anything, and shows a message and a still picture when it can't. It also gets its first bike part: the orange down tube, placed with exact numbers.
Before you start
You will learn
- Check WebGL 2 before building a scene
- Show a message and a still image as the fallback
- Place a bike part from exact end points
You write code in
In your lumen-3d project: replace index.html and src/main.js, put showroom-poster.png in public/data/3d/, then run npm run dev
Files you touch
- index.html
- src/main.js
- public/data/3d/showroom-poster.png
Think of it like this
A shop that sells by video call first checks that the customer's camera works. If it doesn't, the shop does not leave a blank screen: it says so and sends photos instead. Our support check is that first question, and the poster is the photos.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- fallback
- What the page shows when the 3D part can't run: here a message and a still picture.
- hidden
- An HTML attribute that hides an element. In JavaScript,
element.hidden = falseshows it again. - down tube
- The frame tube that runs from the pedals' axle (bottom bracket) up to the front of the frame (head tube).
- radian
- The angle unit three.js uses. 180 degrees is π radians, so −0.8657 radians is about −49.6 degrees.
Step by step
01index.html: the fallback block
Add <div id="fallback" hidden> after #app. It holds one paragraph and an <img> with alt text that describes the picture, so a screen reader user gets the same information. The CSS centres it over the page, and #fallback[hidden] { display: none; } makes sure the hidden attribute wins over display: grid.
02supportsWebGL2() runs first
The function makes a canvas that is not on the page, asks for "webgl2" and returns whether it got one. try / catch makes it return false instead of an error in a locked-down browser. When false, the code shows the fallback, writes a console warning for you as a developer, and stops: nothing else runs.
03startShowroom() holds the scene
All the Phase 0 code now sits in a function that is called only when the check passes. Nothing creates a renderer on a device that has no WebGL 2, so there is no crash and no uncaught error.
04The down tube
A cylinder of radius 0.022 m and length 0.6172 m, in Lumen orange, named "down-tube" so tests and the scene tree can find it. position.set(0.235, 0.5, 0) is the midpoint of the two end points. rotation.z = -0.8657 leans it from the bottom bracket up and forward to the head tube.
05See and test both paths
The first live example runs the real file. The second adds a few lines at the top that make every "webgl2" request fail, as on a device with no WebGL 2, and you see the fallback appear. Test the second path whenever you change the first.
Try it yourself
- 1
Check the ends of the tube
In the first example, add this at the end of
startShowroom():downTube.updateMatrixWorld(); console.log(new THREE.Vector3(0, 0.3086, 0).applyMatrix4(downTube.matrixWorld).toArray().map((n) => n.toFixed(3)).join(", "));. It prints the tube's top end:0.470, 0.700, 0.000, the bottom of the head tube. Change the0.3086to-0.3086and it prints-0.000, 0.300, 0.000(a tiny negative zero from rounding), the bottom bracket. - 2
Stand the tube upright
Set
downTube.rotation.z = 0. The tube now stands straight up at (0.235, 0.5, 0) like a flagpole instead of leaning. This is what "a cylinder starts upright" means.
Code & diagrams
This is the exact src/main.js below with the page's #app and #fallback elements. Open the scene tree: the stand (with its orange mark inside), the down-tube and the two lights.
Console
Run the scene to see its console.Five lines at the top pretend that every request for a webgl2 context fails. The page shows the message and the poster, and no 3D code runs.
Console
Run the scene to see its console.Your project files
Your lumen-3d project after this topic. Click a file to see it in full.
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
Remove the fallback block from the page
In the second example, delete the #fallback element from the HTML (in the project, delete the <div id="fallback" hidden> block from index.html).
Myth vs fact
Myth
If WebGL 2 is missing, the scene shows nothing and that is fine.
Fact
A blank page looks like a broken site. Customers leave and testers file bugs with no clue. A sentence and a picture cost a few lines.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A pure
<img>fallback is the simplest, and it is also the version search engines and screen readers can use. The model'salttext is part of the product, not decoration. Phase 18 builds the full text alternative. - ▸
The fallback has its own failure: if
showroom-poster.pngis missing or the path is wrong, Chrome shows a broken-image icon with thealttext beside it (the text can be cut short by the image's size limits). The paragraph above it still reaches the visitor. Test it by renaming the file. Recall from Topic 1.5:getContext("webgl2")returnsnullfor "can't", neverundefined, so the!== nulltest is the right one. - ▸
The check does not guard against a lost context after the page starts (the GPU can reset later), or against a software renderer that is technically WebGL 2 but very slow. Phase 18 handles
webglcontextlostand Phase 17 measures frame time. - ▸
The down tube's numbers are copied from a cylinder's own rules, not made up: a cylinder points along +y, so the tube is turned by the angle between +y and the direction (0.47, 0.40, 0) that points from the bottom bracket to the head tube. That angle is
atan2(0.47, 0.40)= 49.6°, and turning around z by a positive angle turns +y towards −x, hence the minus sign.
Remember this
- 1
The problem this solves: Lena's old phone showed a white square, with no message. With the check, a device that can't draw 3D gets a clear sentence and a picture of the bike, and the 3D code never runs on a machine that cannot run it. This closes the story: the showroom never leaves a blank page again.
- 2
What the build adds.
index.htmlgains a<div id="fallback" hidden>with a message and a still image,/data/3d/showroom-poster.png(a picture of the bike; copy it into your project from this site, or make your own).src/main.jsstarts withsupportsWebGL2()and, when it returns false, un-hides the fallback instead of starting the scene. The rest of the scene moves intostartShowroom(). - 3
How it plugs into what exists. The Phase 0 scene is unchanged: scene, camera, renderer, stand, lights, loop, resize. They now sit inside
startShowroom(), which only runs when WebGL 2 works. The camera also stands a little closer and lower (3.2 m from the middle instead of 4, and 1.2 m high instead of 1.6, looking at a point 0.4 m up) so the bike frame fills the view. - 4
The first bike part: the down tube. It is the tube from the bottom bracket (where the pedals turn) at (0, 0.30, 0) up to the bottom of the head tube (where the fork joins) at (0.47, 0.70, 0). A
CylinderGeometryis built upright around its own centre, so it is placed halfway between the two points, (0.235, 0.5, 0), and turned. The length is √(0.47² + 0.40²) = 0.6172 m. The turn isrotation.z = -0.8657radians, about −49.6°: the tube leans forward towards +x, and a negative z rotation tips the top towards +x. Phase 2 shows how to work these numbers out; here you use them. - 5
What the showroom can do now that it could not before: show a clear message and a picture on a device with no WebGL 2; show one real bike part standing on the stand. What is still missing: the other frame tubes (Phase 2 adds the top tube and groups them into
createBike()), the world split into modules (Phase 3), and a ResizeObserver, controls, colours and everything else in later phases.
Explain it without notes
Why does main.js ask supportsWebGL2() before it creates a renderer?
Where do the down tube's position (0.235, 0.5, 0) and angle −0.8657 come from?
Practice
Make the poster text different for a device that has WebGL 2 turned off in settings (you can't tell this from "no GPU"). Why is one message for both the right call?
Add a second tube to the scene: the top tube, from (−0.10, 0.82, 0) to (0.44, 0.84, 0), radius 0.018. What are its centre and length?
Done when you can
Your lumen-3d page shows the fallback when WebGL 2 is unavailable (test by faking it)
You can explain where each of the down tube's numbers comes from
Your index.html and src/main.js match this topic
The poster is in public/data/3d/ and loads at /data/3d/showroom-poster.png