A working Stargate you can operate in a browser.
In Stargate the device is a standing ring carved with symbols. You dial a destination by turning the ring until each symbol in turn lines up under a fixed chevron at the top, which locks; when the whole address is locked the gate opens onto a shimmering event horizon, and an armoured iris can be closed over it to block anything coming through.
This project is that machine as a web page. Pick eight symbols from the keypad — with the mouse, by typing their numbers, or from the address book — and the ring turns to each one in order, locks a chevron, and lights the symbol blue on both the ring and the pad. With all eight locked, RUN opens the gate onto a shifting event horizon. IRIS closes twenty-four blades over it. CLS spins the ring down and releases the address.
Or switch AUTO off and dial it by hand, one symbol at a time, forwards or back.
Not every address goes somewhere. Seven chevrons will lock over any symbols at all, but the eighth only engages if the eight together are a place. And the gate can ring: press INC and someone dials you — at which point the iris stops being a toggle and becomes a decision, since whether it is shut when the wormhole connects is the whole of it.
The symbols are not the ones from the show. They are a 64-symbol alphabet I designed — each one a 5 × 5 arrangement of 2 × 2 blocks — and an address is eight of them rather than the show's seven-of-thirty-nine. The ring had to be redivided into 64 cells to hold them.
It is built with no framework and no runtime dependencies: one page, one stylesheet, and
about 750 lines of hand-written JavaScript, assembled from its parts by a dependency-free
sixty-line build script. Every animation is
SMIL —
<animateTransform> elements constructed in JavaScript and triggered with beginElement().
That was the point of the exercise: to find out how far SVG could be pushed with no canvas and
no game engine.
| Button | Key | What it does |
|---|---|---|
| any symbol | digits, then Enter |
Adds it to the address. Click a symbol again to remove it — but only if the ring has not locked it in yet. |
| AUTO | A |
On by default. The ring chases your selections as you make them. Turn it off and the ring stops; REV and FWD come alive. |
| RUN | Enter |
With all eight symbols locked, opens the gate. With an incomplete address, blinks red once for every symbol still missing. |
| IRIS | I |
Closes or opens the 24-blade iris over the event horizon. |
| REV | ← |
Gives the last chevron back and turns the ring to the symbol before it. Only while AUTO is off. |
| FWD | → |
Dials the next symbol in the address and locks it — one symbol, then it stops. Only while AUTO is off. |
| SLOW / NORM / SNAP | S |
Dial speed. SLOW is the original 2021 timing, NORM is the default and half of it, SNAP is nearly instant. Starts on SNAP if your system asks for reduced motion. |
| DHD | D |
Swaps the keypad for a dialling pedestal, and back. Both are live the whole time — they are two views of one address, so whichever you are not looking at is already up to date when you come back. |
| ⚙ | Settings, in the top corner of the page — sound on/off and volume. Outside the console, because the control row is full and a preference is not a thing you do to the gate. | |
| CLS | Esc |
Spins the ring down and releases the address. While the gate is open it arms a clear instead, which fires when the gate closes. |
| Destination | Picks a known address and dials it. Built from the address book in model.js, so the menu and the gate can never disagree. |
|
| INC | W |
Someone dials in. The ring dials an address that is not yours and the chevrons lock blue; shut the iris before it connects, or let it through. |
Backspace takes the last symbol back — or deletes a digit, if you are part-way through typing
one. The number you are typing shows in the readout under the keypad, and the line above it
says what the gate is doing.
Nothing here talks to the dial directly. The keypad, the pedestal, the keyboard and the destination menu are input devices, and all four go through one command surface of ten verbs — see v0.8. It is why there are two pedestals rather than one, and why the built page contains no inline event handler at all.
Active, and being developed. Current release: v0.6. The core works and has always worked: it loads with no console errors and no failed requests, and every control does what it says.
Each release since picking the 2021 project back up has had one job. v0.2 was a correctness pass over every defect that stood against v0.1. v0.3 wrote the controls that were never written — the manual dial that turning AUTO off has always implied, a speed control, keyboard operation, and a keypad that fits a phone. v0.4 made the thing maintainable: one copy of each drawing, tests, and a project layout that says what is a source and what is output. v0.5 gave it a point — somewhere to dial, and something that can dial you. v0.6 made the gate draw itself: the ring of symbols and the iris are computed now, not drawn. Measured before and after (CHANGELOG.md):
| v0.1 | v0.2 | v0.3 | v0.4.x | |
|---|---|---|---|---|
| symbols ever rendered red as CLS releases them | 0 | 8 | 8 | 8 |
| fill attributes written to the DOM per clear | 22,243 | 364 | 364 | 364 |
| sweep to reach symbol 1 from rest | +357.2° (10.9 s) | −2.8° (1.1 s) | −2.8° | −2.8° |
| turning AUTO off actually stops the dial | no | yes | yes | yes |
| …and there is a way to carry on by hand | no | no | yes | yes |
| a full eight-symbol dial | 34.2 s | 24.0 s | 12.1 s (1.4 s on SNAP) | 12.1 s |
prefers-reduced-motion honoured |
no | no | yes | yes |
| operable from the keyboard | no | no | yes | yes |
| keypad overflow on a 320 px phone | — | 88 px | 0 | 0 |
| copies of the gate drawing | 2, drifted | 2, drifted | 2, drifted | 1 |
| hand-maintained HTML | 539 KiB | 539 KiB | 539 KiB | 3 KiB |
| keypad buttons a screen reader can name | 0 / 64 | 0 / 64 | 0 / 64 | 64 / 64 |
| control keys that render the same in Chromium and Firefox | no | no | no | yes |
| tests | 0 | 0 | 0 | 68, in CI |
| addresses that open the gate | all | all | all | 7 (v0.5) |
| the gate can be dialled at you | no | no | no | yes (v0.5) |
| a shut iris seals the event horizon | 99.3% | 99.3% | 99.3% | 100% (v0.6) |
| blade tips visible in the star | ~12 / 24 | ~12 / 24 | ~12 / 24 | 24 / 24 (v0.6) |
| copies of the 64-symbol alphabet | 2 | 2 | 2 | 1 (v0.6) |
| a closed iris you can see | no | no | no | yes (v0.4.1) |
| …that survives a speed change | n/a | n/a | no | yes (v0.4.1) |
Nothing is open — every issue raised since v0.2 is measured and closed. v0.7 made the gate plural: adding a Pegasus or Universe ring is a folder and a manifest, not an edit to every tool. v0.8 separated the input device from the dial, and the DHD it made possible is here. v0.9 gave the chevrons a sound — synthesised in the page, because there is not an audio file in this repository. Next is v1.0, which adds nothing and checks everything.
- ROADMAP.md — what is planned, in order.
- KNOWNISSUES.md — nothing open; eighteen closed since v0.2, each reproduced and measured rather than guessed at, with their numbers.
- CHANGELOG.md — what changed per release.
- test/README.md — what is tested, and the three different questions.
Written over eight days in April 2021, in six commits. Picked up again in 2026: v0.1 was a tidy
pass that changed no behaviour — dead code and a committed copy of jQuery that nothing
imported removed, 65 stray <?xml?> declarations and 68 duplicate element ids cleaned out of
the markup, spelling fixed in the JavaScript API, and the position: absolute; left: 500px
layout replaced with flexbox. v0.2 then fixed the defects that tidy had catalogued, v0.3
built the controls the original never had, and v0.4 gave it a build, tests and a structure. The 2021 state is tagged v0.1-original; the full
list is in CHANGELOG.md.
git clone https://github.com/Dimitriuses/Stargate.git
cd Stargate
npm run build # assembles dist/ — no dependencies needed for this
npm run serve # or serve dist/ with anything you likeThen open http://localhost:8000/. Opening dist/index.html directly works too — every path
is relative, so it loads under file:// as well (checked in Chromium: 0 failed requests).
The page has no runtime dependencies: dist/ is plain HTML, CSS, JS and SVG. The build
itself needs nothing installed either — it is plain Node with no imports beyond fs and path.
Only the tests need npm install.
The gate is assembled from parts, so each piece is small enough to actually open and edit. Every part is a standalone SVG that renders on its own.
Everything about one gate lives in one folder, and nothing about it lives anywhere else.
src/index.html the page skeleton, and its build markers 3 KiB
src/gates/milkyway/
gate.js the manifest: 64 symbols, an address of 8, every dimension 4 KiB
gate.svg the shell: root <svg>, paint order, markers 2 KiB
gate.css every class the drawing uses — one copy 1 KiB
portal.svg the event horizon and its gradient 1 KiB
chassis.svg the ring casing and the nine chevrons 329 KiB
glyphs/S1..S64.svg the alphabet, one file per symbol
tools/geometry.js computes the iris and the ring of symbols
↓ npm run build
dist/ the whole site
Painting order matters and is set by the markers in gate.svg: the portal has to be behind the
iris, or the event horizon covers the blades meant to block it.
Two of those parts are computed, not drawn. The symbols on the ring are generated from the
same glyphs/ files as the keypad, so the alphabet exists once; and the iris blades are
generated so their geometry can be made to seal exactly. tools/geometry.js holds both, and
test/geometry.test.js measures them — the blade parameters were chosen by sweeping them and
measuring coverage, not by eye.
gate.js is where the numbers are. Nothing in tools/ or test/ knows how many symbols a
gate has: the manifest says, and the generators, the model and the tests all ask. Adding a gate
is a new folder under src/gates/ — tools/gates.js finds it by looking, npm run build gives
it dist/<id>/, and all three test suites pick it up with no edit anywhere. That is checked
rather than asserted: a testbed fixture gate with 36 symbols, an address of six and 18 blades
is under test on every run and is skipped by the deploy build.
The build refuses to run if a part carries its own <style>, or uses a class gate.css does
not define — so there stays exactly one copy of the drawing's styles, and a typo in a class name
is a build failure rather than an invisible shape.
Before v0.4 the gate was pasted into index.html and kept as a separate file, and the two
copies had already drifted apart (#9).
design/ still holds the Illustrator sources, but the build never reads them.
npm test # 82 unit tests, pure model + geometry — nothing to install, under a second
npm run test:e2e # 47 browser tests in Chromium (builds first)
npm run test:all # both suitesFull detail in test/README.md; the short version is that there are three kinds of question here and they need three different answers.
model.js is the half of the dial logic with no document in it, so it tests directly under
Node. Several of those tests exist to hold an invariant that CLAUDE.md otherwise
only states in prose — that no duration is ever zero, that SLOW is still the 2021 timing to
the millisecond.
behaviour.test.js drives the real page. Every test carrying an issue number is there because
that issue was once real, and fails against the build that shipped before the fix.
Neither of those answers did anything else move, which for an animation-heavy page is the
question that matters most. That is test/checkpoints.js: a fixed ten-stop session that dumps
the model state, the ring angle, the chevron classes, the address slots and the fill of every
ring and keypad symbol at each stop. SMIL uses fill="freeze", so snapshots taken at
settled moments are deterministic. It prints a diff for a human to read rather than a verdict,
because what counts as a wanted change differs per release.
The record so far:
| release | checkpoints vs the release before | what moved |
|---|---|---|
| v0.1 tidy | 10 / 10 identical | nothing — that was the point |
| v0.2 | 10 / 10 but for two fields | both intended, both #6 |
| v0.3 | 10 / 10 identical, at the new default speed and at SLOW | nothing settled; only durations |
| v0.4 | 10 / 10 identical, plus the keypad to the same SHA-256 | nothing — a generated page that renders like the hand-written one |
| v0.5 | 10 / 10 but for the address slots | intended — the strip shows the symbols now |
| v0.6 | 10 / 10 identical | nothing settled; the ring and iris are generated drawings of the same thing |
| v0.7 | 10 / 10 identical | nothing — the gate became plural and the Milky Way did not move |
| v0.8 | 10 / 10 identical, on both gates, plus the keypad to the same SHA-256 | nothing — input came apart from the dial and the dial did not notice |
| v0.9 | 10 / 10 identical, on both gates | nothing — the gate got a voice and the checkpoints are deaf, which is the harness working |
Two blind spots, both by design: a missing src and an empty one dump the same value, so it is
blind to #5; and sampling
at rest cannot see timing at all, which is why v0.3 could halve every duration and still score
10/10.
The 64-symbol alphabet is mine (design/synvols.ai), and so is the 24-blade iris
(design/Diaphragm.ai).
The gate ring is not. It is a hand redraw of a Wikimedia Commons file by Max Willis and User:M.A.D.company, licensed CC BY-SA 3.0. None of the upstream path data survives here — a rotation-aligned raster comparison of the two ring chassis scores an IoU of 0.211 where a re-export would score about 0.95 — but it is recognisably the same design, so it is credited as a derivative and the artwork carries the upstream licence. The full measurement is in ATTRIBUTION.md.
src/ everything that becomes the site
index.html the page skeleton, with two build markers
index.js dialing logic and every SMIL animation, built in JS
model.js the same logic minus the DOM: geometry, address, speed table
index.css layout
favicon.svg
console.js the command surface: ten verbs in, one state snapshot out
sound.js twelve synthesised voices — no audio files anywhere
settings.js the panel outside the console
devices/keypad.js the pad and the control row
devices/dhd.js the dialling pedestal
devices/keyboard.js digits, Enter, Esc, arrows
devices/switcher.js which pedestal is plugged in
gates/<id>/ one gate: manifest, drawing parts, stylesheet, alphabet
gate.js what this gate is — the only registry there is
glyphs/S1..SN.svg its alphabet, one file per symbol
dist/ the built site — generated, gitignored, deployed by CI
tools/gates.js finds and validates the gates; nothing is listed twice
tools/build.js assembles dist/, one folder per gate; no dependencies
tools/geometry.js the ring and iris generators — no numbers of its own
tools/screenshots.js regenerates docs/*.png from the real page
test/model.test.js unit tests on the address model, per gate
test/geometry.test.js unit tests on the ring and iris geometry, per gate
test/behaviour.test.js 47 browser tests in Chromium
test/checkpoints.js the ten-checkpoint session — a diff, not a verdict
test/lib/harness.js shared browser plumbing and the checkpoint dump
design/synvols.ai Illustrator source for the alphabet (mine)
design/Diaphragm.ai Illustrator source for the 24-blade iris (mine)
design/StarGate2.ai Illustrator source for the ring redraw (derivative)
CLAUDE.md architecture, invariants and gotchas for maintainers
docs/ screenshots
Issues and pull requests are welcome. Each entry in KNOWNISSUES.md says how its issue was reproduced and what the fix looked like, which is the house style: measure it before and after, and say what the number was.
If you touch the artwork, edit the parts under src/gates/<id>/, then run npm run build; the
build refuses a part that carries its own <style> or uses a class its stylesheet does not
define. If you change dialing behaviour,
run npm run test:all, and say what test/checkpoints.js reported against the previous build.
Code — MIT. Gate ring artwork — CC BY-SA 3.0, derived from work by Max Willis and User:M.A.D.company. Stargate is a trademark of Metro-Goldwyn-Mayer; this is a non-commercial fan project with no affiliation to the rights holder.













