Skip to content

Commit 664f944

Browse files
authored
feat: Add an interactive web example gallery deployed to GitHub Pages (#117)
Stacked on #116 (which is stacked on #115). ## What The old browser examples return as a modern, single-page demo gallery running on the WebAssembly backend, with a GitHub Pages deployment. **Scenes** (all draggable with the pointer via a mouse joint): - **Pyramid** and **Domino tower** (a bullet ball topples it) - **Ball cage** (zero gravity, kinematic spinning paddle, chain-loop cage) - **Circle stress** (320 balls in a container) - **Blob** (a soft body of distance-joint springs) - **Bridge** (revolute-joint planks under load) - **Racer** (wheel-joint car on rolling chain terrain, arrow keys to drive, following camera) **Presentation**: dark theme, palette-tinted shapes through Box2D v3's per-shape custom colors, pill navigation with URL-hash deep links, hint bubble per scene, device-pixel-ratio aware canvas, fixed-timestep loop with an accumulator. Rendering goes through the public `DebugDraw` interface, so the gallery doubles as a demonstration of it. **Deploy**: `deploy-examples.yml` builds with `build_runner --release` and publishes to GitHub Pages on pushes to main. One-time repo setup: enable the GitHub Actions source under Settings > Pages. ## Testing Built and verified locally in headless Chrome: all scenes render and simulate on the wasm backend (screenshots in the PR conversation), `melos run format-check` and `dart analyze --fatal-infos` green.
1 parent 409b275 commit 664f944

8 files changed

Lines changed: 1117 additions & 3 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: deploy-examples
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- "packages/forge2d/**"
9+
- ".github/workflows/deploy-examples.yml"
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: write
14+
15+
concurrency:
16+
group: pages
17+
cancel-in-progress: true
18+
19+
jobs:
20+
deploy:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: subosito/flutter-action@v2
25+
- uses: bluefireteam/melos-action@v3
26+
- name: Build the examples
27+
working-directory: packages/forge2d/example
28+
run: |
29+
dart run build_runner build --release --output build
30+
# The output's packages directory is a symlink; materialize it so
31+
# the published branch carries real files.
32+
cp -rL build/web site
33+
- name: Publish to the gh-pages branch
34+
working-directory: packages/forge2d/example/site
35+
run: |
36+
touch .nojekyll
37+
git init -b gh-pages
38+
git config user.name "github-actions[bot]"
39+
git config user.email "github-actions[bot]@users.noreply.github.com"
40+
git add -A
41+
git commit -m "Deploy forge2d examples for $GITHUB_SHA"
42+
git push --force \
43+
"https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" \
44+
gh-pages

packages/forge2d/example/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# Examples for Forge2D
22

3-
Runnable console examples of the native-backed API:
3+
An interactive gallery of physics demos running on the WebAssembly backend,
4+
deployed to GitHub Pages from main. Run it locally with:
5+
6+
```sh
7+
dart run build_runner serve web:8080 --release
8+
```
9+
10+
The console examples of the native backend still live in `bin/`:
411

512
```sh
613
dart run bin/hello_world.dart
714
dart run bin/car.dart
815
dart run bin/domino_tower.dart
916
```
10-
11-
Browser examples return when the WebAssembly backend lands.

packages/forge2d/example/pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ environment:
1010

1111
dependencies:
1212
forge2d: ^0.14.2+1
13+
web: ^1.1.1
1314

1415
dev_dependencies:
16+
build_runner: ^2.6.0
17+
build_web_compilers: ^4.2.0
1518
flame_lint: ^1.4.1
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import 'dart:js_interop';
2+
import 'dart:math' as math;
3+
4+
import 'package:forge2d/forge2d.dart';
5+
import 'package:web/web.dart';
6+
7+
/// Renders the world onto a canvas through the [DebugDraw] interface, with
8+
/// a soft glow-on-dark styling.
9+
class CanvasDebugDraw extends DebugDraw {
10+
CanvasDebugDraw(this._context);
11+
12+
final CanvasRenderingContext2D _context;
13+
14+
/// The world point at the center of the view.
15+
Vector2 center = Vector2.zero();
16+
17+
/// How many world meters are visible vertically.
18+
double viewHeight = 40;
19+
20+
double _scale = 20;
21+
double _canvasWidth = 0;
22+
double _canvasHeight = 0;
23+
24+
/// Prepares the transform for a frame and clears the canvas.
25+
void beginFrame(double canvasWidth, double canvasHeight) {
26+
_canvasWidth = canvasWidth;
27+
_canvasHeight = canvasHeight;
28+
_scale = canvasHeight / viewHeight;
29+
_context.clearRect(0, 0, canvasWidth, canvasHeight);
30+
}
31+
32+
double _x(double worldX) => _canvasWidth / 2 + (worldX - center.x) * _scale;
33+
double _y(double worldY) => _canvasHeight / 2 - (worldY - center.y) * _scale;
34+
35+
/// Converts a canvas point to world coordinates.
36+
Vector2 toWorld(double canvasX, double canvasY) => Vector2(
37+
center.x + (canvasX - _canvasWidth / 2) / _scale,
38+
center.y - (canvasY - _canvasHeight / 2) / _scale,
39+
);
40+
41+
String _rgba(int color, double alpha) {
42+
final r = (color >> 16) & 0xFF;
43+
final g = (color >> 8) & 0xFF;
44+
final b = color & 0xFF;
45+
return 'rgba($r, $g, $b, $alpha)';
46+
}
47+
48+
void _stroke(int color) {
49+
_context
50+
..strokeStyle = _rgba(color, 0.95).toJS
51+
..lineWidth = 1.6
52+
..stroke();
53+
}
54+
55+
void _fill(int color) {
56+
_context
57+
..fillStyle = _rgba(color, 0.28).toJS
58+
..fill();
59+
}
60+
61+
void _polygonPath(List<Vector2> vertices) {
62+
_context.beginPath();
63+
for (var i = 0; i < vertices.length; i++) {
64+
final vertex = vertices[i];
65+
if (i == 0) {
66+
_context.moveTo(_x(vertex.x), _y(vertex.y));
67+
} else {
68+
_context.lineTo(_x(vertex.x), _y(vertex.y));
69+
}
70+
}
71+
_context.closePath();
72+
}
73+
74+
@override
75+
void drawPolygon(List<Vector2> vertices, int color) {
76+
_polygonPath(vertices);
77+
_stroke(color);
78+
}
79+
80+
@override
81+
void drawSolidPolygon(
82+
Transform transform,
83+
List<Vector2> vertices,
84+
double radius,
85+
int color,
86+
) {
87+
_polygonPath([for (final vertex in vertices) transform.apply(vertex)]);
88+
_fill(color);
89+
_stroke(color);
90+
}
91+
92+
@override
93+
void drawCircle(Vector2 center, double radius, int color) {
94+
_context.beginPath();
95+
_context.arc(_x(center.x), _y(center.y), radius * _scale, 0, 2 * math.pi);
96+
_stroke(color);
97+
}
98+
99+
@override
100+
void drawSolidCircle(Transform transform, double radius, int color) {
101+
final position = transform.position;
102+
_context.beginPath();
103+
_context.arc(
104+
_x(position.x),
105+
_y(position.y),
106+
radius * _scale,
107+
0,
108+
2 * math.pi,
109+
);
110+
_fill(color);
111+
_stroke(color);
112+
// A radius line makes rotation visible.
113+
final edge = transform.apply(Vector2(radius, 0));
114+
_context.beginPath();
115+
_context.moveTo(_x(position.x), _y(position.y));
116+
_context.lineTo(_x(edge.x), _y(edge.y));
117+
_stroke(color);
118+
}
119+
120+
@override
121+
void drawSolidCapsule(
122+
Vector2 point1,
123+
Vector2 point2,
124+
double radius,
125+
int color,
126+
) {
127+
final axis = point2 - point1;
128+
final angle = math.atan2(axis.y, axis.x);
129+
_context.beginPath();
130+
_context.arc(
131+
_x(point1.x),
132+
_y(point1.y),
133+
radius * _scale,
134+
-angle + math.pi / 2,
135+
-angle + 3 * math.pi / 2,
136+
);
137+
_context.arc(
138+
_x(point2.x),
139+
_y(point2.y),
140+
radius * _scale,
141+
-angle - math.pi / 2,
142+
-angle + math.pi / 2,
143+
);
144+
_context.closePath();
145+
_fill(color);
146+
_stroke(color);
147+
}
148+
149+
@override
150+
void drawSegment(Vector2 point1, Vector2 point2, int color) {
151+
_context.beginPath();
152+
_context.moveTo(_x(point1.x), _y(point1.y));
153+
_context.lineTo(_x(point2.x), _y(point2.y));
154+
_stroke(color);
155+
}
156+
157+
@override
158+
void drawPoint(Vector2 point, double size, int color) {
159+
_context.beginPath();
160+
_context.arc(_x(point.x), _y(point.y), size / 2, 0, 2 * math.pi);
161+
_context.fillStyle = _rgba(color, 0.9).toJS;
162+
_context.fill();
163+
}
164+
165+
@override
166+
void drawString(Vector2 point, String text, int color) {
167+
_context
168+
..fillStyle = _rgba(0xE5E9F5, 0.8).toJS
169+
..font = '12px ui-sans-serif, system-ui'
170+
..fillText(text, _x(point.x), _y(point.y));
171+
}
172+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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>Forge2D examples</title>
7+
<link rel="icon" type="image/svg+xml" href="logo.svg">
8+
<script defer src="main.dart.js"></script>
9+
<style>
10+
:root {
11+
--bg: #0b1020;
12+
--panel: #151b30;
13+
--text: #e5e9f5;
14+
--muted: #8b93ad;
15+
--accent: #7c9cff;
16+
}
17+
* { box-sizing: border-box; }
18+
html, body {
19+
margin: 0;
20+
height: 100%;
21+
background: var(--bg);
22+
color: var(--text);
23+
font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
24+
overflow: hidden;
25+
}
26+
header {
27+
display: flex;
28+
align-items: center;
29+
gap: 1rem;
30+
padding: 0.6rem 1rem;
31+
background: var(--panel);
32+
border-bottom: 1px solid #232b47;
33+
}
34+
header h1 {
35+
display: flex;
36+
align-items: center;
37+
gap: 0.5rem;
38+
font-size: 1rem;
39+
font-weight: 600;
40+
margin: 0;
41+
letter-spacing: 0.02em;
42+
}
43+
header h1 img { height: 1.7rem; width: 1.7rem; }
44+
header h1 span { color: var(--accent); }
45+
nav {
46+
display: flex;
47+
gap: 0.4rem;
48+
flex-wrap: wrap;
49+
}
50+
nav button {
51+
background: transparent;
52+
color: var(--muted);
53+
border: 1px solid #2a3355;
54+
border-radius: 999px;
55+
padding: 0.3rem 0.9rem;
56+
font-size: 0.85rem;
57+
cursor: pointer;
58+
transition: all 0.15s ease;
59+
}
60+
nav button:hover { color: var(--text); border-color: var(--accent); }
61+
nav button.active {
62+
background: var(--accent);
63+
border-color: var(--accent);
64+
color: #0b1020;
65+
font-weight: 600;
66+
}
67+
#reset {
68+
background: transparent;
69+
color: var(--text);
70+
border: 1px solid #2a3355;
71+
border-radius: 999px;
72+
padding: 0.3rem 0.9rem;
73+
font-size: 0.85rem;
74+
cursor: pointer;
75+
transition: all 0.15s ease;
76+
}
77+
#reset:hover { border-color: var(--accent); color: var(--accent); }
78+
#canvas { display: block; width: 100vw; height: calc(100vh - 53px); }
79+
#hint {
80+
position: fixed;
81+
bottom: 0.8rem;
82+
left: 50%;
83+
transform: translateX(-50%);
84+
color: var(--muted);
85+
font-size: 0.8rem;
86+
background: color-mix(in srgb, var(--panel) 80%, transparent);
87+
padding: 0.35rem 0.9rem;
88+
border-radius: 999px;
89+
pointer-events: none;
90+
white-space: nowrap;
91+
}
92+
#loading {
93+
position: fixed;
94+
inset: 0;
95+
display: grid;
96+
place-items: center;
97+
color: var(--muted);
98+
font-size: 0.95rem;
99+
}
100+
a { color: var(--accent); text-decoration: none; }
101+
.spacer { flex: 1; }
102+
.gh { color: var(--muted); font-size: 0.85rem; }
103+
</style>
104+
</head>
105+
<body>
106+
<header>
107+
<h1><img src="logo.svg" alt="Forge2D logo">Forge2D <span>examples</span></h1>
108+
<nav id="nav"></nav>
109+
<div class="spacer"></div>
110+
<button id="reset" title="Restart the scene">&#8635; Reset</button>
111+
<a class="gh" href="https://github.com/flame-engine/forge2d">GitHub</a>
112+
</header>
113+
<canvas id="canvas"></canvas>
114+
<div id="hint"></div>
115+
<div id="loading">Loading Box2D…</div>
116+
</body>
117+
</html>
Lines changed: 15 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)