@harvis/asteroids
v1.0.0
Published
Blow up any web page. A zero-dependency asteroids easter egg that turns DOM elements into targets. ~5kB gzipped, all vector art, no image assets.
Readme
@harvis/asteroids
Blow up any web page. Drop this in your project, wire it to the Konami code, and anyone who finds it gets a little spaceship that shoots the page apart — every paragraph, card and table cell is a target.
A modern rewrite of erkie/asteroids, the bookmarklet that has been destroying the web since 2010.
- Zero runtime dependencies. ~5 kB gzipped, ESM, tree-shakeable.
- No image assets. Every pixel is SVG path data drawn to a canvas — nothing to host, nothing to 404.
- Contained. Canvas and HUD live in a shadow root, so page CSS cannot touch them and they cannot leak.
- Reversible. Quitting puts the page back exactly as it was.
- SSR safe. Calling it without a DOM does nothing.
Install
npm install @harvis/asteroidsUse
import { asteroids, konami } from '@harvis/asteroids'
konami(() => asteroids())That is the whole easter egg. konami fires on ↑ ↑ ↓ ↓ ← → ← → B A and returns an unsubscribe function.
Prefer your own trigger? asteroids() starts a game from anywhere — a hidden menu item, an ?asteroids query
param, five clicks on the logo.
Without a bundler
<script src="https://unpkg.com/@harvis/asteroids/dist/asteroids.global.js"></script>
<script>
HarvisAsteroids.konami(() => HarvisAsteroids())
</script>The published package also contains dist/bookmarklet.txt — paste it into a bookmark to play on any site.
Load it only when it is needed
An easter egg should not cost your users anything until they find it:
import { konami } from '@harvis/asteroids'
konami(async () => {
const { asteroids } = await import('@harvis/asteroids')
asteroids()
})Controls
| Key | Action |
| ----------------- | ----------------------------------- |
| ↑ / W | Thrust |
| ← → / A D | Turn |
| ↓ / S | Brake |
| space | Fire |
| B (hold) | Outline everything that can be shot |
| Esc | Quit |
Fly off an edge and the page scrolls, so the whole document is playable.
Options
asteroids({
ship: 'classic', // 'classic' | 'dart' | 'kite' | your own SVG path data
size: 30, // ship size in pixels
hud: true, // score panel
hints: true, // key hints inside the score panel
scroll: true, // scroll the page when the ship leaves an edge
restoreOnQuit: true, // put every destroyed element back on exit
safe: '.keep-me', // selector for elements that cannot be shot
theme: {
ship: '#ffffff',
outline: '#101014',
bullet: '#ff4d2e',
particles: ['#ff4d2e', '#ffc400'],
reveal: '#ff4d2e',
},
onHit: (element, score) => {},
onQuit: (score) => {},
})Protecting elements
Bullets pass straight through anything matching the safe selector, and through anything carrying
data-asteroids-safe — including its children:
<nav data-asteroids-safe>…</nav>Use it for the checkout button, not for the marketing copy.
Custom ships
Ships are SVG path data in a 32×32 box with the nose pointing right:
asteroids({ ship: 'M30 16L4 2L12 16L4 30Z' })The same strings are exported for reuse in your own markup:
import { SHIPS, svgMarkup } from '@harvis/asteroids'
document.querySelector('.logo').innerHTML = svgMarkup(SHIPS.dart, { size: 24, fill: '#ff4d2e' })API
| Export | Description |
| ----------------------------- | ------------------------------------------------------------------------------ |
| asteroids(options?) | Starts a game, returns a handle. Returns the running game if one is already up. |
| game.score / game.running | Current state. |
| game.quit() | Stop, tear down, restore the page. |
| game.restore() | Put everything shot so far back, without stopping. |
| isPlaying() | Is a game running right now? |
| quit() | Stop the running game, if any. |
| konami(handler, options?) | ↑ ↑ ↓ ↓ ← → ← → B A. Returns an unsubscribe function. |
| sequence(codes, handler) | Same, for any KeyboardEvent.code sequence. |
| SHIPS, SHIP_PATH, ICONS | The artwork, as SVG path data. |
| svgMarkup, svgDataUri | Turn that path data into <svg> markup or a data URI. |
Only one game runs at a time; a second asteroids() call returns the game already in progress.
Notes
- The game takes over the arrow keys, space,
BandEscwhile it runs, and hands them back when it quits. - Elements are removed, not hidden, so scripts observing them will see them go.
restoreOnQuit(on by default) reinserts them in place on exit. - Rendering pauses in background tabs, like anything driven by
requestAnimationFrame. - TypeScript types are bundled.
Develop
npm run dev # demo page on http://localhost:5173
npm run build # dist/ + size reportReleasing
Publishing runs in CI (.github/workflows/publish.yml).
One-time setup: create an automation access token on npmjs.com (Access Tokens → Generate → Automation, so
it bypasses 2FA) and add it to the repository as the NPM_TOKEN secret.
To ship a version:
npm version patch # or minor / major — commits and tags
git push --follow-tagsThen publish a GitHub release for that tag. The workflow installs, builds, checks that the tag matches
package.json and that the version is not already on the registry, and publishes with
provenance attested to this repo and commit.
workflow_dispatch runs the same job manually and takes a dist-tag input, for next or beta releases.
Credits
Idea, game design and the original implementation: Erik Rothoff Andersson and Rootof Creations HB — erkie/asteroids. This is an altered version with a rewritten engine, distributed under the same zlib license. See LICENSE.
