npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@emu198x/zx-spectrum

v0.7.0

Published

ZX Spectrum in the browser, published to npm as @emu198x/zx-spectrum

Readme

@emu198x/zx-spectrum

A cycle-accurate ZX Spectrum 48K for the browser. The same emulator core as the native Emu198x application, compiled to WebAssembly — its output is checked pixel-for-pixel against the native build on every change.

The 48K ROM travels inside the package, so a page needs no firmware of its own.

Install

npm install @emu198x/zx-spectrum

Use

import init, { Spectrum } from '@emu198x/zx-spectrum';

await init();
const spectrum = await Spectrum.createBundled(document.querySelector('canvas'));

let last = performance.now();
function frame(now) {
  spectrum.tick(now - last);
  last = now;
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Pass elapsed real time to tick, not one frame per callback. The Spectrum runs at 50.08 Hz and a display usually refreshes at 60 Hz or more; tick converts elapsed time into whole machine frames, so the machine runs at its own speed rather than the monitor's.

The canvas drawing buffer is resized to the machine's picture (352×296) and the page controls the displayed size with CSS. Add image-rendering: pixelated or the browser will blur the pixels.

API

| Method | Purpose | |---|---| | Spectrum.createBundled(canvas) | Build a 48K on the ROM in this package. | | Spectrum.create(canvas, rom) | Build a 48K on a ROM you supply. | | tick(elapsedMs) | Run elapsed real time and draw. Returns frames run. | | loadSnapshot(bytes, format) | Load a .sna or .z80 snapshot. | | load(slot, kind, bytes) | Load media — for example a tape into tape-1. | | autoload(maxBootFrames) | Load the tape in tape-1: type LOAD "" and press play. A machine that has already run is reset first; the tape stays in. | | transport(slot, playing) | Press the tape deck's PLAY (true) or STOP (false). STOP keeps the tape's place; PLAY carries on from it. query('tape.playing') and query('tape.progress') report the state. | | rewind(slot) | Press the tape deck's REWIND: back to the start of the tape, stopped. Then transport(slot, true) plays it again. | | previousBlock(slot) / nextBlock(slot) | Search the tape for the previous or next block, by the gaps between blocks. Previous goes to the start of the block playing first. Returns whether it moved; query('tape.block') and query('tape.blocks') give the position. | | keyDown(code) / keyUp(code) | Feed a DOM KeyboardEvent.code. | | setAudioEnabled(on) | Start or stop audio. | | configureAudio(rate, channels, capacity) | Match the page's AudioContext. | | audioDrain() | Take buffered samples to feed a worklet. | | frameRgba() / frameSize() | The picture, for presenting it yourself. | | mediaSlots() | Slot names this machine accepts. | | resize(width, height) | Tell it the canvas changed size. |

createBundled and create are async because the API keeps room for a GPU renderer, which needs an adapter.

Keys are mapped from KeyboardEvent.code, the physical key, so a learner on an AZERTY or Dvorak layout presses the key that sits where the Spectrum's does. Shift reaches CapsShift; Control and Alt reach SymbolShift. Cursor keys expand to the chord the hardware actually uses — the Spectrum has no cursor keys, and Up is CapsShift+7.

ROM copyright

Amstrad have kindly given their permission for the redistribution of their copyrighted material but retain that copyright.

The permission is Cliff Lawson's, for Amstrad plc, on comp.sys.sinclair, 31 August 1999: https://web.archive.org/web/20180828125931/http://www.worldofspectrum.org/permits/amstrad-roms.txt

The ROM image is included unmodified, and no charge is made for it. Versions 0.1.0 to 0.4.0 carried a 48K image with 272 bytes changed in the ROM's unused area, from address 0x386E; use 0.4.1 or later, which carries the unmodified image (SHA1 5ea7c2b824672e914525d1d5c419d71b84a426a2). The permission covers the Sinclair 48K and 128K ROMs and Amstrad's +2/+2A/+3 machines. It does not extend to the ZX80, ZX81, Interface 1 or 2, Timex machines, or Spectrum clones.

Licence

The emulator is licensed under the terms in the Emu198x repository. The ROM is copyright Amstrad plc and is redistributed under the permission above; it is not covered by that licence.

Running inside a Web Worker

Spectrum.createHeadless(rom) and, in a firmware-bundled build, Spectrum.createHeadlessBundled() create the same 48K machine without a DOM canvas. Run autoload() and tick(elapsedMs) in the worker, then transfer the frameRgba() buffer and frameSize() dimensions to the page for presentation. The existing canvas-based constructors remain available.

Boot and turbo tape loading are synchronous operations. Keeping them in a worker prevents those operations from blocking editing and other page interactions; it does not remove their emulation cost. Keep at most one tick request in flight, terminate the previous worker when starting a new run, and pause requests when the page is hidden. The caller owns worker lifecycle, frame transfer and audio presentation. No snapshot injection or tape-loading shortcut is implied.

Numbered BASIC listings

The additive basicTape(source, name) export converts a numbered ASCII listing into a self-starting BASIC TAP. It uses the format crate's tokenise_listing path and the shared TAP writer, preserving expression text rather than passing it through the analysis AST. Line numbers are sorted; duplicate line numbers, empty programs, unsupported characters, unterminated strings and out-of-range numbers return errors. This is tokenisation, not a BASIC grammar validator: the ROM still reports execution and syntax errors when the tape runs.

This route is currently a bounded Code198x browser trial (Meet BASIC greeting and Sonar). DEF FN parameter markers, embedded graphics/control codes and broader source-notation compatibility remain outside it. The listing and headless worker APIs are available from npm version 0.4.0.

Direct program execution

On a fresh 48K instance, runBasic(source) installs the tokenised listing in RAM through the shared native BASIC loader, updates its system variables, and types RUN through the ROM. It mounts no tape. basicTape(source, name) remains the independent download/export path.

runCode(bytes, origin, entry) boots the ROM, installs the machine code and a small BASIC launcher, then executes CLEAR followed by RANDOMIZE USR. This preserves ROM services and a return stack. Code must start at or above 24576, fit below 65536, and contain its entry point. Use a fresh instance for each run. Both APIs are synchronous: worker hosts keep boot work off the page thread. These APIs are available from npm version 0.4.0.

readMemory(address, length) reads the current visible address space for lesson inspectors. It is read-only and rejects ranges outside the 64 KiB address space. This API is available from npm version 0.4.0.

For lesson replay, call enableScreenWriteTrace(address, length) on a fresh machine before runCode(), then read screenWriteTrace(). Choose a narrow bitmap range to avoid filling the capture with ROM clearing. It returns captured writes within that range (including ROM writes) and a full flag when the shared 8192-record cap is reached. The caller can filter by program PC range. A replay is a recording, not live stepping; a full capture must not be presented as complete. These APIs are available from npm version 0.4.0.

For the bounded routine lesson, call enableRoutineTrace(stop) before runCode(), then routineTrace(). The JSON recording contains actual unconditional CALL/RET transitions and bitmap writes in $4000–$47FF, with before/after register snapshots. Existing debugger stepping stops at the supplied PC, on leaving the program, or after 4096 steps. Check complete: only reaching the supplied stop address makes it true. The intended stop is the lesson's hold label. This is bounded teaching instrumentation, not a general instruction trace. It is available from npm version 0.4.0.

Debugger controls

debugState() serialises shared debugger CPU state and disassembly/bytes at PC. debugStep() invokes the existing bounded native step; debugRunTo(address) runs to an instruction boundary with a fixed 14-million-half-cycle budget and returns whether it reached the address. Invalid addresses are rejected. Both execution methods deliver queued input first. Suspend normal frame ticks before using them; no implicit rendering or real-time playback runs while paused. A failed run-to is not a breakpoint hit, and a step at HALT can remain waiting. These exports are available from npm version 0.4.0.