import scene3d
scene = scene3d.Scene()
scene.set_sky('#1a1a2e')
scene.set_ground(length=10, width=10)
sphere = scene3d.Shapes.Sphere(diameter=1.5, segments=16)
sphere.set_color('#4488ff')
sphere.set_position(0, 0.75, 0)
scene.add(sphere)
ctx = scene.get_context('2d')
ctx.font = '16px sans-serif'
ctx.fill_style = '#e94560'
ctx.fill_text('(0, 0) — top-left', 8, 20)
ctx.fill_style = '#f5a623'
ctx.fill_text('x increases to the right →', 8, 50)
ctx.fill_style = '#44cc88'
ctx.fill_text('y increases downward ↓', 8, 80)
ctx.fill_style = '#ffffff'
ctx.fill_text('This text is at x=8, y=260', 8, 260)HUD — Heads-Up Display
A HUD (Heads-Up Display) is the information layer drawn on top of a game or simulation — score, health, timer, mini-map. You’ve already seen it used briefly in the Animations and On Click notebooks, where a single line of text appeared in the corner.
In scene3d, every scene has a 2D canvas that floats above the 3D world. In this notebook you’ll learn how that canvas works, how to draw shapes on it, and how to build a real, live-updating HUD.
The 2D Canvas Overlay
scene.get_context('2d') returns a canvas that sits transparently on top of the 3D scene. Everything you draw on it appears in front of the 3D world.
The canvas uses the same coordinate system as every browser canvas:
| Corner | Coordinates |
|---|---|
| Top-left | (0, 0) |
| Right | x increases → |
| Down | y increases ↓ |
The scene below draws labels at different positions to make this concrete. Run it and read the labels — notice where each one appears.
How fill_text Places Text
ctx.fill_text('Score: 0', 8, 20)
# x yThe x and y values position the bottom-left of the first character. So y=20 means the baseline of the text is 20 pixels from the top edge.
ctx.font sets the size and typeface: '20px sans-serif', '14px monospace'. Change it before each fill_text call to use different sizes in the same HUD.
{ “question_type”: “multiple_choice”, “question”: “In the 2D overlay canvas, where is the point (0, 0)?”, “options”: [ { “key”: “a”, “text”: “Center of the canvas” }, { “key”: “b”, “text”: “Bottom-left corner” }, { “key”: “c”, “text”: “Top-left corner” }, { “key”: “d”, “text”: “Top-right corner” } ], “answer”: “c”, “submitted_answer”: “” }
Drawing Rectangles
Text alone isn’t enough for a real HUD — you also need solid shapes like panels and bars. The canvas has two rectangle methods:
ctx.fill_rect(x, y, width, height) # solid filled rectangle
ctx.stroke_rect(x, y, width, height) # outlined rectangle (no fill)The first two arguments are the top-left corner of the rectangle. fill_rect uses fill_style for its color; stroke_rect uses stroke_style.
Run the scene below to see both in action.
import scene3d
scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=10, width=10)
box = scene3d.Shapes.Box(width=1.5, height=1.5, depth=1.5)
box.set_color('#cc44ff')
box.set_position(0, 0.75, 0)
scene.add(box)
ctx = scene.get_context('2d')
# Filled rectangle
ctx.fill_style = '#e94560'
ctx.fill_rect(10, 10, 160, 40)
# Outlined rectangle
ctx.stroke_style = '#44cc88'
ctx.line_width = 3
ctx.stroke_rect(10, 60, 160, 40)
# Labels
ctx.fill_style = '#ffffff'
ctx.font = '15px sans-serif'
ctx.fill_text('fill_rect — solid', 18, 36)
ctx.fill_text('stroke_rect — outline', 18, 86)Rectangle Parameters
ctx.fill_rect(x, y, width, height)
# | | | |
# | | | height in pixels
# | | width in pixels
# | y of top-left corner
# x of top-left cornerFor stroke_rect, set ctx.stroke_style (a color) and ctx.line_width (pixels) before calling it — otherwise you get the default black 1-pixel border.
{ “question_type”: “true_false”, “question”: “ctx.fill_rect(10, 20, 100, 40) draws a rectangle whose top-left corner is at x=10, y=20.”, “answer”: “True”, “submitted_answer”: “” }
Building a Progress Bar
A progress bar is the most common HUD element — health, energy, loading, time remaining. It’s built from two rectangles stacked in the same position:
- A background rectangle (the empty bar)
- A fill rectangle drawn on top, whose width is proportional to a value
The key formula:
fill_width = bar_max_width * (value / max_value)If bar_max_width = 200 and health is 70 / 100, the fill is 200 * 0.70 = 140 pixels wide.
Run the cell to see a health bar drawn at 70%.
import scene3d
scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=10, width=10)
sphere = scene3d.Shapes.Sphere(diameter=1.5, segments=16)
sphere.set_color('#e94560')
sphere.set_position(0, 0.75, 0)
scene.add(sphere)
health = 70
max_health = 100
bar_x, bar_y = 10, 10
bar_max_width = 220
bar_height = 26
ctx = scene.get_context('2d')
# Background (empty bar)
ctx.fill_style = '#333333'
ctx.fill_rect(bar_x, bar_y, bar_max_width, bar_height)
# Fill (proportional to health)
fill_width = bar_max_width * (health / max_health)
ctx.fill_style = '#44cc88'
ctx.fill_rect(bar_x, bar_y, fill_width, bar_height)
# Label below the bar
ctx.fill_style = '#ffffff'
ctx.font = '17px sans-serif'
ctx.fill_text(f'Health: {health} / {max_health}', bar_x, bar_y + bar_height + 20)
scene.run()Why the Formula Works
fill_width = bar_max_width * (value / max_value)value / max_value is always between 0.0 and 1.0 — it’s the fraction of the bar that should be filled. Multiplying by bar_max_width converts that fraction to pixels.
| Health | value / max_value |
Pixels filled (out of 220) |
|---|---|---|
| 100 | 1.00 | 220 |
| 70 | 0.70 | 154 |
| 25 | 0.25 | 55 |
| 0 | 0.00 | 0 |
Try changing health = 70 to other values in the cell above and re-running.
{ “question_type”: “multiple_choice”, “question”: “A health bar has bar_max_width = 200. The player has 45 out of 100 health. How wide should the filled bar be?”, “options”: [ { “key”: “a”, “text”: “45 pixels” }, { “key”: “b”, “text”: “90 pixels” }, { “key”: “c”, “text”: “155 pixels” }, { “key”: “d”, “text”: “200 pixels” } ], “answer”: “b”, “submitted_answer”: “” }
A Live, Updating HUD
So far the HUD has been drawn once at startup. In a real game it needs to update every frame.
The pattern is: 1. Write a draw_hud() helper that calls ctx.clear() first, then redraws everything 2. Call draw_hud() inside your @scene.on_frame callback
ctx.clear() wipes the entire 2D canvas. Without it, every frame’s drawing stacks on top of the previous one and the screen turns into a mess. Always clear before redrawing.
The scene below has an animated sphere and a countdown timer bar. Click Stop (■) when you’ve watched the timer run.
import scene3d
import math
scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=12, width=12)
sphere = scene3d.Shapes.Sphere(diameter=1, segments=16)
sphere.set_color('#e94560')
sphere.set_position(0, 1, 0)
scene.add(sphere)
ctx = scene.get_context('2d')
t = 0.0
duration = 20.0
def draw_hud(elapsed):
remaining = max(0.0, duration - elapsed)
fraction = remaining / duration
ctx.clear() # wipe the previous frame's drawing first
# Timer bar background
ctx.fill_style = '#333333'
ctx.fill_rect(10, 10, 240, 22)
# Timer bar fill
ctx.fill_style = '#f5a623'
ctx.fill_rect(10, 10, 240 * fraction, 22)
# Timer label
ctx.fill_style = '#ffffff'
ctx.font = '18px sans-serif'
ctx.fill_text(f'Time remaining: {remaining:.1f}s', 10, 52)
@scene.on_frame
def animate(dt):
global t
t += dt
x = math.sin(t * 1.5) * 4
sphere.set_position(x, 1, 0)
draw_hud(t)
scene.run()The Clear-and-Redraw Pattern
def draw_hud(elapsed):
ctx.clear() # step 1: wipe everything
ctx.fill_rect(...) # step 2: draw background elements
ctx.fill_text(...) # step 3: draw text on top
@scene.on_frame
def animate(dt):
...
draw_hud(t) # called every frameSeparating draw_hud into its own function keeps the animation callback clean and makes it easy to add more HUD elements later — just add lines inside draw_hud.
{ “question_type”: “multiple_choice”, “question”: “Why does draw_hud() call ctx.clear() at the start of every frame?”, “options”: [ { “key”: “a”, “text”: “To save memory” }, { “key”: “b”, “text”: “To prevent old drawn elements from staying on screen” }, { “key”: “c”, “text”: “Because fill_rect only works after clear()” }, { “key”: “d”, “text”: “To reset the canvas coordinate system” } ], “answer”: “b”, “submitted_answer”: “” }
Try It Yourself
Click the spheres to score points before the timer runs out. The HUD shows your score, a countdown bar, and the time remaining. The bar turns red when less than 40% of the time is left.
Use the slider to choose how long the round lasts.
import scene3d
import random
TIMER = 20 #@param {type:"slider", min:10, max:30, step:5}
scene = scene3d.Scene()
scene.set_sky('#0f3460')
scene.set_ground(length=16, width=10)
ctx = scene.get_context('2d')
t = 0.0
score = 0
palette = ['#e94560', '#f5a623', '#4488ff', '#44cc88', '#cc44ff']
def draw_hud():
remaining = max(0.0, TIMER - t)
fraction = remaining / TIMER
ctx.clear()
# Score
ctx.fill_style = '#ffffff'
ctx.font = '22px sans-serif'
ctx.fill_text(f'Score: {score}', 10, 30)
# Timer bar background
ctx.fill_style = '#333333'
ctx.fill_rect(10, 42, 260, 22)
# Timer bar fill — green when plenty of time, red when running low
ctx.fill_style = '#44cc88' if fraction > 0.4 else '#e94560'
ctx.fill_rect(10, 42, 260 * fraction, 22)
ctx.fill_style = '#ffffff'
ctx.font = '16px sans-serif'
ctx.fill_text(f'{remaining:.0f}s remaining', 10, 84)
spheres = []
def make_handler(idx):
def handler():
global score
score += 1
spheres[idx].set_color(random.choice(palette))
draw_hud()
return handler
for i in range(5):
s = scene3d.Shapes.Sphere(diameter=1.2, segments=16)
s.set_color(palette[i])
s.set_position(i * 3 - 6, 0.7, 0)
s.on_click(make_handler(i))
scene.add(s)
spheres.append(s)
@scene.on_frame
def animate(dt):
global t
t += dt
draw_hud()
draw_hud()
scene.run()Think about a game or app you use that has a HUD. What information does it show?
If you were designing your own game, what would you put on the HUD — health, score, a map, ammo, time? Describe the layout: where on screen would each element go, and how would you use fill_rect and fill_text to build it?