A field guide · v0.6.0-beta

nw_wrld

An event-driven sequencer for triggering visuals using web technologies. Compose with a 16-step grid, a MIDI controller, an OSC stream, live audio, or an audio file. Modules are JavaScript files in a folder you own.

Stack · Electron · React · Three.js · p5 · D3 Node · ≥20 License · GPL-3.0

01Quick start · sixty seconds to your first trigger

If npm start is already running, two windows are on screen — Dashboard and Projector. The first launch will ask you to pick a project folder; let it scaffold one. Then:

  1. In the Dashboard, click CREATE TRACK and name it.
  2. Click + MODULE and pick HelloWorld.
  3. Click + CHANNEL. A row of 16 cells appears.
  4. Click cells 1, 5, 9, 13 in the grid. They turn red.
  5. Assign the channel a method — try show.
  6. Hit PLAY in the footer.

The playhead sweeps left to right. Every red cell fires show on every module attached to that track. The Projector window flashes Hello world on the beat. Stop, change HelloWorld to SpinningCube, swap the method to rotate, and you have an audiovisual loop with no external hardware.

Note The IAC Driver Bus 1 not found message in the terminal is benign — it’s the macOS virtual MIDI driver, only used when you switch to External MIDI mode. Sequencer mode ignores it.

02The mental model · one sentence

Core idea

Triggers fire methods on visual modules. Everything else is plumbing for what counts as a trigger and what counts as a method.

Two events drive the entire system:

EventPayloadEffect
track-selection {trackName, velocity} Switches the active track in the Projector. Old modules destroy, new ones initialize.
method-trigger {channelName, velocity} Fires every method that channel maps to, on every module on the active track.

Both events have several producers — the built-in sequencer, MIDI, OSC, live audio bands, file playback. Switching between them changes nothing about your tracks, modules, or method mappings. Only the trigger source changes.

03Architecture · two windows, three processes

Main process (Node) ├── BrowserWindow #1 · Dashboard ── React control surface ├── BrowserWindow #2 · Projector ── Visual output canvas │ └── BrowserView · Sandbox ── Your module code, isolated └── InputManager ── MIDI · OSC · Audio FFT

The Dashboard and Projector are two ordinary Electron renderer windows. The interesting piece is the third surface: a sandboxed BrowserView embedded in the Projector that runs your workspace JavaScript. It has its own preload, its own context isolation, and only the globals it asks for in its docblock.

Boot sequence

From src/main/mainProcess/entry.ts:

  1. setupApp() — registers the nw-sandbox:// and nw-assets:// URL schemes.
  2. registerIpcBridge() — wires JSON read/write, workspace, and input handlers.
  3. registerSandboxIpc() — sets up the RPC layer for sandboxed modules.
  4. registerWorkspaceSelectionIpc() — folder picker and scaffolder.
  5. app.whenReady() — pop the picker, scaffold if empty, spawn both windows.

The sandbox boundary

Workspace modules cannot require('fs') or hit arbitrary network endpoints. They see only what they declare in @nwWrld imports: a fixed set of SDK helpers, three globals (THREE, p5, d3), and the THREE.js loaders. Asset reads are scoped to your project’s assets/ directory via the nw-assets:// protocol — paths that try to escape return null.

Trust The sandbox limits, but does not eliminate, the risk of running untrusted JavaScript. Open project folders only from sources you trust.

04Project folders · the unit of portability

Every nw_wrld project is a self-contained folder. Copy it to share, drop it in Dropbox, version it in git. Everything needed to run a composition lives inside.

MyProject/ ├── modules/ Your visual modules (hot-reloaded) │ ├── HelloWorld.js │ ├── SpinningCube.js │ └── ... 14 more starters ├── assets/ Project resources, scoped │ ├── images/ blueprint.png │ ├── models/ cube.obj tetra.stl triangle.ply ... │ └── json/ meteor.json └── nw_wrld_data/ App state (auto-managed) └── json/ ├── userData.json Tracks, channels, mappings ├── appState.json UI state, active track ├── config.json Aspect ratio, background └── recordingData.json Optional recordings

State files use atomic writes with a .backup fallback. They’re plain JSON — diffable, mergeable, recoverable. Lose your project? nw_wrld will detect the missing folder on launch and prompt you to reselect or create one.

Switching projects

Each project is independent. Open Settings to switch — your current state saves first. Closing and re-opening the app remembers your last project.

Test isolation

Set NW_WRLD_TEST_PROJECT_DIR=/path/to/scratch before launching to boot directly into a known folder, useful for E2E tests and ephemeral demos.

05Composition · tracks, modules, channels

Three nested concepts. Internalize these and the Dashboard becomes obvious.

Track

A scene. A track holds one or more visual modules that render together, plus the channels that drive them. Switching tracks tears down the active modules and brings up the next set. Use tracks like slides, songs, or sections of a performance.

Module

A visual element — a spinning cube, a grid of dots, a piece of text. Modules are JavaScript classes you write or borrow. A track can host multiple modules; they layer in the Projector.

Channel

A row in the sequencer grid. Each channel maps to one or more methods on the track’s modules. A channel is the bridge between “something fired” and “something visual happens.”

Track "Opening" ├── Module · Text ── shows the title ├── Module · GridOverlay ── background ├── Module · SpinningCube │ ├── Channel "kick" ──→ [SpinningCube.rotate] ├── Channel "snare" ──→ [Text.color, GridOverlay.invert] └── Channel "hihat" ──→ [SpinningCube.scale]

One channel can fire many methods at once. One method can be wired to many channels. This is the composability that makes nw_wrld feel like an instrument once you stop thinking of it as a slideshow.

Per-channel signal settings

For audio and file modes, each channel has its own threshold (how loud counts as a trigger) and cooldown (how soon after firing it can fire again). Tune these to make a channel sensitive to kicks but ignore the snare on the same band.

Module enable / disable

Toggle a module off without removing it. Keeps the configuration around for one-click revival — useful for A/B’ing two visuals over the same channels.

06The 16-step sequencer · default signal source

The default mode. A continuously looping 16-step pattern fires channels on each active step. No external hardware required.

BPM 120 · 16n · running 4 channels · 4 bars
kick
snare
hihat
accent

Tempo

Adjustable in Settings, range 60–130 BPM. The clock is Tone.Transport running in the Projector renderer at sixteenth-note (16n) resolution. Patterns loop continuously and persist with the track.

Where the clock lives

Important detail: the playhead is not a main-process timer broadcasting steps over IPC. It’s entirely in the Projector renderer (src/shared/sequencer/SequencerPlayback.ts). This keeps it in sync with the visuals it’s driving and avoids round-trip jitter.

Patterns persist

What you click into the grid is part of the track’s data. Save, switch tracks, come back — your pattern is intact.

07Built-in methods · what you get for free

Every class extending ModuleBase inherits these triggerable methods. Wire any of them to a channel without writing custom code.

MethodOptionsEffect
showduration (ms)Fade in to visible.
hideduration (ms)Fade out to invisible.
offsetx, y (%)Translate the module within its container.
scalescale (×)Uniform scale.
opacityopacity (%)Set transparency.
rotatedirection, speed, durationSpin the module. Speed 0.1–100.
randomZoomscaleFrom, scaleTo, positionRandom zoom-and-recenter.
matrixrows, cols, excludedCellsPosition via a grid layout.
viewportLinex, y, length, opacityDraw a line on the viewport.
backgroundcolorSet module background color.
invertdurationColor invert flash.

These compose. Wire kickscale + invert for a percussive burst. Wire snarerotate + offset for a syncopated dance.

08Signal sources · what counts as a trigger

Switch in Settings → Signal Source. Tracks, modules, and method mappings are unaffected by the switch — only the source of triggers changes.

Sequencer (default)
Built-in 16-step grid. 60–130 BPM. No hardware needed. Patterns save with each track.
MIDI · Pitch Class
Map C..B, octave-agnostic. Avoids the “G7 vs G8” mismatch between DAWs.
MIDI · Exact Note
Map full MIDI note numbers 0–127. Octave-specific triggers.
OSC
UDP listener. Map OSC addresses (e.g. /track/select/2) to track-selection or method-trigger events.
External Audio
Live capture (mic, loopback, virtual interface). Multi-band FFT splits Low/Mid/High; each band is a channel.
File Upload
Drop an MP3/WAV per track. Same Low/Mid/High band split during playback.

Two MIDI channels are configurable independently:

  • Method Triggers MIDI Channel — which channel fires module methods.
  • Track Select MIDI Channel — which channel switches tracks.

Set both to channel 1 for the simplest DAW workflow, or split them onto channels 1 and 2 for clean separation.

09MIDI · DAW & Strudel walkthrough

Virtual MIDI routing

  • macOS — open Audio MIDI Setup → Show MIDI Studio, double-click IAC Driver, enable Bus 1.
  • Windows — install loopMIDI, create a virtual port.
  • Linux — ALSA ‘through’ ports work; some distros need snd-virmidi.

Configure in nw_wrld

  1. Settings → Signal Source → choose MIDI (Pitch Class) or MIDI (Exact Note).
  2. Select your virtual port (or hardware controller) as the input device.
  3. Settings → Configure Mappings — assign which notes trigger track-selection vs which trigger methods.

From a DAW (Ableton, Logic, FL Studio, …)

Most DAWs default to MIDI Channel 1. Two recommended setups:

Option A — single channel

Keep both Method Triggers and Track Select on channel 1. Use Configure Mappings to split which notes do which.

Option B — split channels

In your DAW, route method triggers to ch.1 and track-select to ch.2 (separate MIDI tracks/devices). In nw_wrld set Method Triggers = 1, Track Select = 2.

From Strudel

Strudel can drive nw_wrld over the same virtual MIDI port. Keep local audio playing while sending MIDI by reusing the same rhythmic pattern for two outputs:

// 1. Declare the rhythm once
const r = "c4 ~ e4 g4 ~ e4 ~";

// 2. Local audio (Strudel sound)
$: note(r).s("piano");

// 3. Same pattern → MIDI to nw_wrld
$: note(r).midi("IAC Driver Bus 1").midichan(1);

Single brackets around the port name. Sending a pattern to .midi() alone silences its local audio — duplicate the pattern for both outputs if you want the sound and the visuals together.

Windows quirk If MIDI works once and then stops, ensure nw_wrld fully closed (no background process) and restart the virtual port app.

10OSC · live audio · file upload

OSC

nw_wrld listens for OSC messages on a configurable UDP port. Map any OSC address to track-selection or method-trigger:

  • /track/select/2 → switch to track 2
  • /trigger/kick → fire the channel named “kick”

Senders include TouchOSC, Open Stage Control, Max/MSP, Pure Data, SuperCollider, custom scripts. Velocity / value can ride in the OSC argument.

External audio

Pick an input device (hardware interface, BlackHole loopback on macOS, VB-Audio Cable on Windows). nw_wrld runs a local FFT and splits the spectrum into three bands — Low, Mid, High — each emitting trigger events when amplitude crosses the per-channel threshold.

Per-channel knobs:

  • Threshold — minimum amplitude to count as a trigger.
  • Cooldown — how long after firing before this channel can fire again. Tune up to suppress double-triggers; tune down to let fast hi-hats through.

File upload

Per-track MP3 or WAV. Same Low/Mid/High band-split logic, but driven by the file’s own playback. Useful for prepared sets, click-tracks, or pieces where the audio is the source of truth.

Privacy Audio analysis runs entirely locally. Nothing leaves your machine.

11Module anatomy · the docblock contract

A module is a JavaScript class in modules/YourName.js. The filename is its identity. The docblock declares its dependencies. The default export is required.

/*
@nwWrld name: PulsingCircle
@nwWrld category: 2D
@nwWrld imports: ModuleBase
*/

class PulsingCircle extends ModuleBase {
  static methods = [
    {
      name: "pulse",
      executeOnLoad: false,
      options: [
        { name: "intensity", defaultVal: 1.5, type: "number" },
        { name: "duration", defaultVal: 500, type: "number", unit: "ms" }
      ]
    }
  ];

  constructor(container) {
    super(container);     // must be first; gives you this.elem
    this.init();
  }

  init() {
    // build DOM, set up canvases, load assets
  }

  pulse({ intensity = 1.5, duration = 500 } = {}) {
    // the channel triggers this
  }

  destroy() {
    // stop animations, remove listeners
    super.destroy();
  }
}

export default PulsingCircle;

Required docblock fields

  • @nwWrld name — display name in the Dashboard dropdown.
  • @nwWrld category — grouping label (2D, 3D, Text, Data, … you choose).
  • @nwWrld imports — comma-separated dependency tokens. Must include at least one.

Allowed imports

  • SDKModuleBase, BaseThreeJsModule, assetUrl, readText, loadJson
  • GlobalsTHREE, p5, d3, Noise
  • THREE.js loadersOBJLoader, PLYLoader, PCDLoader, GLTFLoader, STLLoader

Lifecycle

  1. Construct — call super(container) first; this gives you this.elem, transformation state, and starts hidden.
  2. Init — build DOM, create canvases, kick off async asset loads. Keep it fast.
  3. Method execution — channels call your methods with the options object. Methods marked executeOnLoad: true also run once after init.
  4. Destroy — cancel requestAnimationFrame, remove listeners, dispose Three.js resources, then super.destroy().
Don’t Don’t use import ... from ... path imports inside workspace modules — they’re loaded at runtime from your project folder, not bundled.

12Method options · the eight types

Each entry in static methods[].options[] drives both the Dashboard input control and the keys passed to your method at runtime.

typeUI controlExample
textText input { name: "msg", defaultVal: "Hello", type: "text" }
numberNumber input { name: "size", type: "number", min: 10, max: 200, unit: "px" }
colorHex picker { name: "color", defaultVal: "#FF0000", type: "color" }
booleanToggle { name: "enabled", defaultVal: true, type: "boolean" }
selectDropdown { name: "mode", values: ["bounce","slide"], type: "select" }
matrixGrid editor { name: "pos", defaultVal: { rows: 3, cols: 3, excludedCells: [] }, type: "matrix" }
assetFileFile picker { name: "img", type: "assetFile", assetBaseDir: "images", assetExtensions: [".png"] }
assetDirFolder picker { name: "dir", type: "assetDir", assetBaseDir: "images", allowCustom: true }

Cross-cutting fields

  • defaultVal — the value the option starts at when the method is created.
  • min / max — for numbers; the UI clamps within these bounds.
  • unit — UI label (ms, %, ×, …); does not affect runtime.
  • allowRandomization — adds a dice button next to the input.
  • allowCustom — for asset pickers, lets users type a custom path.

Conventions

  • Durations in milliseconds, labelled unit: "ms".
  • Percent values labelled unit: "%".
  • Multipliers labelled unit: "×".

13SDK · loading project assets

Three helpers, declared in your docblock, give you scoped access to the project’s assets/ folder. Paths are relative to assets/; absolute paths and ../ escapes return null.

helpersignaturereturns
assetUrl assetUrl(path) string | null — an nw-assets:// URL safe to drop into img.src or fetch().
loadJson await loadJson(path) Promise<object | null> — parsed JSON.
readText await readText(path) Promise<string | null> — raw text contents.
// docblock: @nwWrld imports: ModuleBase, assetUrl, loadJson, readText

async init() {
  this.img.src = assetUrl("images/blueprint.png");
  this.data = await loadJson("json/meteor.json");
  this.poem = await readText("data/poem.txt");
}

Path safety

InputResult
"images/x.png"Resolved against project assets/
"/images/x.png"null — leading slash rejected
"../other/x.png"null — escapes assets/
"http://example.com/x.png"null — no external URLs through this helper

Patterns

  • Always check for null; provide a fallback so a missing asset doesn’t crash a live performance.
  • Load once in init() and reuse — don’t re-fetch JSON every method call.
  • For 3D models, pair assetUrl() with the right loader (OBJLoader, GLTFLoader, etc.) declared in your imports.

14Three.js modules · 3D out of the box

Extend BaseThreeJsModule instead of ModuleBase to get a scene, camera, renderer, and OrbitControls preconfigured.

/*
@nwWrld name: SpinningCube
@nwWrld category: 3D
@nwWrld imports: BaseThreeJsModule, THREE
*/

class SpinningCube extends BaseThreeJsModule {
  init() {
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    this.cube = new THREE.Mesh(geometry, material);
    this.scene.add(this.cube);
    this.camera.position.z = 5;
  }

  animate() {
    this.cube.rotation.x += 0.01;
    this.cube.rotation.y += 0.01;
  }
}

export default SpinningCube;

Inherited from BaseThreeJsModule

  • this.scene, this.camera, this.renderer, this.controls
  • Render loop wired automatically; override animate() to advance state per frame.
  • Resize handling tied to the projector window.

Loading models

Combine assetUrl() with a loader matched to the file extension. The starter project ships cube.obj, tetra.stl, triangle.ply, points.pcd, and triangle.gltf for quick testing — see ModelLoader.js.

Disposal matters

In destroy(), call geometry.dispose() and material.dispose() on everything you created. Three.js does not garbage-collect WebGL resources; long performances will leak otherwise.

15Starter modules · twenty-two examples to learn from

Every new project scaffolds a starter library in modules/. Pick patterns from these instead of reinventing.

Text & UI

HelloWorldMinimal possible module — 50 lines, single text method.
TextConfigurable display text, font, position. The DOM-based pattern.
CornersFour corner UI elements, useful as a frame.
FrameBorder overlay with adjustable inset.
CodeColumnsAnimated scrolling code/text, Matrix-style.

2D graphics

GridOverlayCanvas grid lines — good base for layered visuals.
GridDotsAnimated dot grid — p5.js example.
ImageAsset-loaded image with fit modes — uses assetUrl().
ImageGalleryCycle through assets/images/.
ScanLinesCRT-style scanline overlay.

3D

SpinningCubeSimplest BaseThreeJsModule example.
BasicGeometryMultiple primitives composed.
CubeCubeNested cubes, rotation chains.
CubeGrowthProcedural growing structure.
OrbitalPlaneOrbital mechanics — rings, planets.
LowEarthPointLow-earth-orbit visualization.
PerlinBlobNoise-driven blob — uses the Noise import.
ModelLoaderOBJ / STL / PLY / PCD / GLTF loader.

Data viz

AsteroidGraphp5 + JSON asset (meteor.json) — the canonical data-loading example.
MathOrbitalMapMathematical orbit map.
CloudPointIceberg3D point cloud.
ZKProofVisualizerMost complex starter — abstract knowledge graph viz.

Open any of these in your editor while the app is running. Tweak a color, change a number, save — the Projector reflects the change without restart. Learning the system is mostly editing these in place.

16Hot reload · the editing loop

nw_wrld watches your project’s modules/ directory with fs.watch (350ms debounce). Any save broadcasts workspace:modulesChanged over IPC; both windows invalidate their module-class cache and re-instantiate.

[ editor ] save MyModule.js │ ▼ [ fs.watch ] 350ms debounce │ ▼ [ workspace.ts ] broadcast → Dashboard + Projector │ ▼ moduleClassCache.invalidate(name) │ ▼ Projector re-instantiates if module is on the active track

What you can change without restart

  • Method bodies, options, defaults.
  • static methods — the new methods appear in the assignment dropdown.
  • Imports — adding p5 to the docblock makes p5 available next reload.

What needs a restart

  • Application source (anything outside your project folder).
  • Adding a new dependency to package.json.
  • Changes to TypeScript runtime files (rebuilt by npm run build:runtime).

Debugging

Open the Projector dev tools — Cmd+Option+I on macOS, Ctrl+Shift+I on Windows / Linux — to see console.log output and stack traces from your module code. Errors during load surface as a yellow badge in the Dashboard.

Built-in editor

If you’d rather not Alt-Tab to your editor, the Dashboard ships a Monaco-based code view. Settings → Module Editor. Useful for quick tweaks; for serious work, your normal editor wins.

17Building & releases · shipping a binary

Production renderer bundle

npm run build

Per-platform installers

CommandOutput
npm run dist:macUniversal DMG (Intel + Apple Silicon)
npm run dist:mac:splitTwo smaller DMGs, one per arch
npm run dist:mac:arm64Apple Silicon only
npm run dist:mac:x64Intel only
npm run dist:winPortable Windows .exe
npm run dist:linuxAppImage + .deb

Outputs land in release/. Universal mac builds bundle both architectures and roughly double the download size — prefer :split for distribution.

Automated GitHub releases

Tag a version, push the tag — the workflow at .github/workflows/release.yml builds all targets and attaches them, plus a SHA256SUMS file:

git tag v1.0.0
git push origin v1.0.0

Test suites

npm run typecheckTypeScript across the renderer + core lanes.
npm run test:unitNode test runner against runtime build.
npm run test:e2ePlaywright drives the real Electron app.
npm run all:testEverything plus lint.

18Troubleshooting · the usual suspects

SymptomWhere to look
Project folder missing on launchReselect via the picker, or point at a fresh empty folder.
Module doesn’t appear in dropdownFilename letters/numbers only · docblock has all three @nwWrld fields · file ends with export default.
Module loads but nothing visibleTrigger show() from a channel — or set a method executeOnLoad: true that calls this.show().
Asset returns nullPath must be relative, no leading slash, no ../. Case-sensitive.
Pattern playing but no visualsChannel needs a method assigned. Module must be on the active track.
No MIDI device shownmacOS — enable IAC Bus 1. Windows — start loopMIDI before launching nw_wrld. Linux — aconnect -l to verify.
MIDI works once then dies (Windows)nw_wrld may have a background process. Force-quit, restart your virtual port app, then re-launch.
Hot reload not firingFile saved? Saved in this project’s modules/, not another? Check Projector dev tools for syntax errors.
Dev server fails to startPort 9000 is busy. Close other dev servers. Re-run npm install if dependencies look stale.
libasound error on WSLAudio capture isn’t supported under WSL by default. Use Sequencer or File Upload modes, or run on native Linux.

Performance hygiene

  • Cap object counts in 3D modules (InstancedMesh for many copies of the same mesh).
  • Use requestAnimationFrame, never setInterval, for render loops.
  • Always cancelAnimationFrame and dispose Three resources in destroy().
  • Test on the hardware you’ll perform on; integrated GPUs hit limits earlier than discrete.

19File map · where things live in the source

If you’re reading the codebase, start here.

Main process · Node

  • src/main/mainProcess/entry.tsstart() and boot sequence.
  • src/main/mainProcess/windows.ts — Dashboard + Projector window creation.
  • src/main/mainProcess/workspace.ts — project folder picker, scaffold, file watcher.
  • src/main/mainProcess/sandbox.ts — token-based sandbox access control.
  • src/main/mainProcess/ipcBridge/ — JSON, workspace, input, sandbox handlers.
  • src/main/InputManager.ts — MIDI / OSC / audio fan-out.

Projector · renderer

  • src/projector/Projector.ts — main projector logic.
  • src/projector/helpers/moduleBase.ts — the ModuleBase superclass.
  • src/projector/helpers/threeBase.tsBaseThreeJsModule.
  • src/projector/internal/sandbox/ — sandbox host inside the projector.
  • src/projector/internal/inputListener.ts — receives broadcast input events.

Dashboard · renderer

  • src/dashboard/Dashboard.js — top-level UI logic.
  • src/dashboard/views/, modals/, components/ — React UI surface.

Shared

  • src/shared/sequencer/SequencerPlayback.ts — Tone.js transport, 16n tick.
  • src/shared/json/ — atomic write, backup-fallback read.
  • src/shared/midi/, src/shared/audio/ — input utilities.
  • src/shared/validation/ — runtime validation of user data.

Workspace seeds

  • src/main/starter_modules/ — the 22 modules copied into a new project.
  • src/main/workspaceStarterAssets.ts — sample images, JSON, models.
  • src/main/workspaceStarterModules.ts — registry of which starters seed.