Skip to content

Commit 04670bb

Browse files
authored
Add canvas bouncing ball demo (#24)
1 parent bbe6482 commit 04670bb

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed

wasm/bouncing-ball.html

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>Bouncing Ball Demo</title>
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11+
background-color: #101418;
12+
color: #f4f4f4;
13+
}
14+
15+
body {
16+
margin: 0;
17+
min-height: 100vh;
18+
display: grid;
19+
place-items: center;
20+
gap: 1.5rem;
21+
padding: 2rem 1rem 3rem;
22+
background: radial-gradient(circle at top, rgba(255, 255, 255, 0.1), transparent 60%) #101418;
23+
}
24+
25+
h1 {
26+
margin: 0;
27+
font-size: clamp(2rem, 3vw + 1rem, 3rem);
28+
text-align: center;
29+
letter-spacing: 0.08em;
30+
text-transform: uppercase;
31+
}
32+
33+
.stage {
34+
position: relative;
35+
border: 2px solid rgba(255, 255, 255, 0.2);
36+
border-radius: 12px;
37+
box-shadow: 0 1.25rem 2.5rem rgba(0, 0, 0, 0.45);
38+
overflow: hidden;
39+
background: linear-gradient(135deg, #243447, #1c2533);
40+
}
41+
42+
canvas {
43+
display: block;
44+
}
45+
46+
#hud {
47+
position: absolute;
48+
left: 50%;
49+
top: 1rem;
50+
transform: translateX(-50%);
51+
padding: 0.35rem 0.75rem;
52+
border-radius: 999px;
53+
background-color: rgba(0, 0, 0, 0.35);
54+
backdrop-filter: blur(8px);
55+
font-size: 0.95rem;
56+
letter-spacing: 0.08em;
57+
text-transform: uppercase;
58+
}
59+
60+
#instructions {
61+
text-align: center;
62+
font-size: 0.95rem;
63+
max-width: 40rem;
64+
line-height: 1.6;
65+
color: rgba(255, 255, 255, 0.7);
66+
}
67+
</style>
68+
</head>
69+
<body>
70+
<main>
71+
<h1>Canvas Bouncing Ball</h1>
72+
<p id="instructions">
73+
Drag the ball or resize the browser window to see the physics adapt in real time. The frames-per-second counter
74+
(FPS) updates every few frames so you can keep an eye on performance.
75+
</p>
76+
<div class="stage">
77+
<canvas id="scene" width="640" height="360" aria-label="Bouncing ball demo"></canvas>
78+
<div id="hud">FPS: <span id="fps">0</span></div>
79+
</div>
80+
</main>
81+
<script>
82+
const canvas = document.getElementById("scene");
83+
const ctx = canvas.getContext("2d");
84+
const hud = document.getElementById("hud");
85+
const fpsEl = document.getElementById("fps");
86+
87+
const state = {
88+
radius: 28,
89+
position: { x: canvas.width * 0.2, y: canvas.height * 0.2 },
90+
velocity: { x: 150, y: 75 },
91+
gravity: 420,
92+
damping: 0.8,
93+
drag: 0.0008,
94+
lastTime: performance.now(),
95+
frameCount: 0,
96+
fps: 0,
97+
fpsAccumulator: 0,
98+
};
99+
100+
const resizeCanvas = () => {
101+
const maxWidth = Math.min(window.innerWidth - 48, 900);
102+
const width = Math.max(Math.min(maxWidth, 720), 320);
103+
const height = Math.round(width * (9 / 16));
104+
const scale = width / canvas.width;
105+
const heightScale = height / canvas.height;
106+
107+
canvas.width = width;
108+
canvas.height = height;
109+
hud.style.width = `${width}px`;
110+
111+
state.position.x *= scale;
112+
state.position.y *= heightScale;
113+
state.radius = Math.max(16, Math.min(36, width / 20));
114+
};
115+
116+
const draw = () => {
117+
ctx.clearRect(0, 0, canvas.width, canvas.height);
118+
119+
const gradient = ctx.createRadialGradient(
120+
state.position.x,
121+
state.position.y,
122+
state.radius * 0.35,
123+
state.position.x,
124+
state.position.y,
125+
state.radius
126+
);
127+
gradient.addColorStop(0, "#ffdf85");
128+
gradient.addColorStop(1, "#ff7e5f");
129+
130+
ctx.fillStyle = gradient;
131+
ctx.beginPath();
132+
ctx.arc(state.position.x, state.position.y, state.radius, 0, Math.PI * 2);
133+
ctx.fill();
134+
135+
ctx.strokeStyle = "rgba(255, 255, 255, 0.25)";
136+
ctx.lineWidth = 4;
137+
ctx.beginPath();
138+
ctx.arc(state.position.x, state.position.y, state.radius, Math.PI * 0.15, Math.PI * 0.6);
139+
ctx.stroke();
140+
};
141+
142+
const update = (dt) => {
143+
const airResistance = 1 - state.drag * dt;
144+
state.velocity.x *= airResistance;
145+
state.velocity.y = state.velocity.y * airResistance + state.gravity * dt;
146+
147+
state.position.x += state.velocity.x * dt;
148+
state.position.y += state.velocity.y * dt;
149+
150+
const left = state.radius;
151+
const right = canvas.width - state.radius;
152+
const top = state.radius;
153+
const bottom = canvas.height - state.radius;
154+
155+
if (state.position.x < left) {
156+
state.position.x = left;
157+
state.velocity.x *= -state.damping;
158+
} else if (state.position.x > right) {
159+
state.position.x = right;
160+
state.velocity.x *= -state.damping;
161+
}
162+
163+
if (state.position.y < top) {
164+
state.position.y = top;
165+
state.velocity.y *= -state.damping;
166+
} else if (state.position.y > bottom) {
167+
state.position.y = bottom;
168+
state.velocity.y *= -state.damping;
169+
}
170+
};
171+
172+
const step = (time) => {
173+
const dt = Math.min((time - state.lastTime) / 1000, 0.1);
174+
state.lastTime = time;
175+
176+
update(dt);
177+
draw();
178+
179+
state.frameCount += 1;
180+
state.fpsAccumulator += dt;
181+
if (state.fpsAccumulator >= 0.25) {
182+
state.fps = Math.round(state.frameCount / state.fpsAccumulator);
183+
fpsEl.textContent = state.fps;
184+
state.frameCount = 0;
185+
state.fpsAccumulator = 0;
186+
}
187+
188+
requestAnimationFrame(step);
189+
};
190+
191+
const pointer = {
192+
active: false,
193+
offsetX: 0,
194+
offsetY: 0,
195+
};
196+
197+
const startDrag = (event) => {
198+
const rect = canvas.getBoundingClientRect();
199+
const x = (event.clientX || event.touches?.[0]?.clientX) - rect.left;
200+
const y = (event.clientY || event.touches?.[0]?.clientY) - rect.top;
201+
const dx = x - state.position.x;
202+
const dy = y - state.position.y;
203+
204+
if (dx * dx + dy * dy <= state.radius * state.radius) {
205+
pointer.active = true;
206+
pointer.offsetX = dx;
207+
pointer.offsetY = dy;
208+
state.velocity.x = 0;
209+
state.velocity.y = 0;
210+
}
211+
};
212+
213+
const moveDrag = (event) => {
214+
if (!pointer.active) return;
215+
const rect = canvas.getBoundingClientRect();
216+
const x = (event.clientX || event.touches?.[0]?.clientX) - rect.left;
217+
const y = (event.clientY || event.touches?.[0]?.clientY) - rect.top;
218+
219+
state.position.x = x - pointer.offsetX;
220+
state.position.y = y - pointer.offsetY;
221+
};
222+
223+
const endDrag = () => {
224+
pointer.active = false;
225+
};
226+
227+
canvas.addEventListener("mousedown", startDrag);
228+
canvas.addEventListener("touchstart", startDrag, { passive: true });
229+
window.addEventListener("mousemove", moveDrag);
230+
window.addEventListener("touchmove", moveDrag, { passive: true });
231+
window.addEventListener("mouseup", endDrag);
232+
window.addEventListener("touchend", endDrag);
233+
234+
resizeCanvas();
235+
window.addEventListener("resize", resizeCanvas);
236+
237+
requestAnimationFrame(step);
238+
</script>
239+
</body>
240+
</html>

0 commit comments

Comments
 (0)