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.
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:
- In the Dashboard, click CREATE TRACK and name it.
- Click + MODULE and pick
HelloWorld. - Click + CHANNEL. A row of 16 cells appears.
- Click cells 1, 5, 9, 13 in the grid. They turn red.
- Assign the channel a method — try
show. - 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.
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
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:
| Event | Payload | Effect |
|---|---|---|
| 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
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:
setupApp()— registers thenw-sandbox://andnw-assets://URL schemes.registerIpcBridge()— wires JSON read/write, workspace, and input handlers.registerSandboxIpc()— sets up the RPC layer for sandboxed modules.registerWorkspaceSelectionIpc()— folder picker and scaffolder.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.
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.
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.”
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.
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.
| Method | Options | Effect |
|---|---|---|
| show | duration (ms) | Fade in to visible. |
| hide | duration (ms) | Fade out to invisible. |
| offset | x, y (%) | Translate the module within its container. |
| scale | scale (×) | Uniform scale. |
| opacity | opacity (%) | Set transparency. |
| rotate | direction, speed, duration | Spin the module. Speed 0.1–100. |
| randomZoom | scaleFrom, scaleTo, position | Random zoom-and-recenter. |
| matrix | rows, cols, excludedCells | Position via a grid layout. |
| viewportLine | x, y, length, opacity | Draw a line on the viewport. |
| background | color | Set module background color. |
| invert | duration | Color invert flash. |
These compose. Wire kick → scale + invert for a percussive burst. Wire snare → rotate + 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.
/track/select/2) to track-selection or method-trigger events.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
- Settings → Signal Source → choose MIDI (Pitch Class) or MIDI (Exact Note).
- Select your virtual port (or hardware controller) as the input device.
- 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.
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.
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
- SDK —
ModuleBase,BaseThreeJsModule,assetUrl,readText,loadJson - Globals —
THREE,p5,d3,Noise - THREE.js loaders —
OBJLoader,PLYLoader,PCDLoader,GLTFLoader,STLLoader
Lifecycle
- Construct — call
super(container)first; this gives youthis.elem, transformation state, and starts hidden. - Init — build DOM, create canvases, kick off async asset loads. Keep it fast.
- Method execution — channels call your methods with the options object. Methods marked
executeOnLoad: truealso run once after init. - Destroy — cancel
requestAnimationFrame, remove listeners, dispose Three.js resources, thensuper.destroy().
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.
| type | UI control | Example |
|---|---|---|
| text | Text input | { name: "msg", defaultVal: "Hello", type: "text" } |
| number | Number input | { name: "size", type: "number", min: 10, max: 200, unit: "px" } |
| color | Hex picker | { name: "color", defaultVal: "#FF0000", type: "color" } |
| boolean | Toggle | { name: "enabled", defaultVal: true, type: "boolean" } |
| select | Dropdown | { name: "mode", values: ["bounce","slide"], type: "select" } |
| matrix | Grid editor | { name: "pos", defaultVal: { rows: 3, cols: 3, excludedCells: [] }, type: "matrix" } |
| assetFile | File picker | { name: "img", type: "assetFile", assetBaseDir: "images", assetExtensions: [".png"] } |
| assetDir | Folder 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.
| helper | signature | returns |
|---|---|---|
| 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
| Input | Result |
|---|---|
"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
| HelloWorld | Minimal possible module — 50 lines, single text method. |
| Text | Configurable display text, font, position. The DOM-based pattern. |
| Corners | Four corner UI elements, useful as a frame. |
| Frame | Border overlay with adjustable inset. |
| CodeColumns | Animated scrolling code/text, Matrix-style. |
2D graphics
| GridOverlay | Canvas grid lines — good base for layered visuals. |
| GridDots | Animated dot grid — p5.js example. |
| Image | Asset-loaded image with fit modes — uses assetUrl(). |
| ImageGallery | Cycle through assets/images/. |
| ScanLines | CRT-style scanline overlay. |
3D
| SpinningCube | Simplest BaseThreeJsModule example. |
| BasicGeometry | Multiple primitives composed. |
| CubeCube | Nested cubes, rotation chains. |
| CubeGrowth | Procedural growing structure. |
| OrbitalPlane | Orbital mechanics — rings, planets. |
| LowEarthPoint | Low-earth-orbit visualization. |
| PerlinBlob | Noise-driven blob — uses the Noise import. |
| ModelLoader | OBJ / STL / PLY / PCD / GLTF loader. |
Data viz
| AsteroidGraph | p5 + JSON asset (meteor.json) — the canonical data-loading example. |
| MathOrbitalMap | Mathematical orbit map. |
| CloudPointIceberg | 3D point cloud. |
| ZKProofVisualizer | Most 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.
What you can change without restart
- Method bodies, options, defaults.
static methods— the new methods appear in the assignment dropdown.- Imports — adding
p5to the docblock makesp5available 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
| Command | Output |
|---|---|
npm run dist:mac | Universal DMG (Intel + Apple Silicon) |
npm run dist:mac:split | Two smaller DMGs, one per arch |
npm run dist:mac:arm64 | Apple Silicon only |
npm run dist:mac:x64 | Intel only |
npm run dist:win | Portable Windows .exe |
npm run dist:linux | AppImage + .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 typecheck | TypeScript across the renderer + core lanes. |
npm run test:unit | Node test runner against runtime build. |
npm run test:e2e | Playwright drives the real Electron app. |
npm run all:test | Everything plus lint. |
18Troubleshooting · the usual suspects
| Symptom | Where to look |
|---|---|
| Project folder missing on launch | Reselect via the picker, or point at a fresh empty folder. |
| Module doesn’t appear in dropdown | Filename letters/numbers only · docblock has all three @nwWrld fields · file ends with export default. |
| Module loads but nothing visible | Trigger show() from a channel — or set a method executeOnLoad: true that calls this.show(). |
| Asset returns null | Path must be relative, no leading slash, no ../. Case-sensitive. |
| Pattern playing but no visuals | Channel needs a method assigned. Module must be on the active track. |
| No MIDI device shown | macOS — 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 firing | File saved? Saved in this project’s modules/, not another? Check Projector dev tools for syntax errors. |
| Dev server fails to start | Port 9000 is busy. Close other dev servers. Re-run npm install if dependencies look stale. |
| libasound error on WSL | Audio 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 (
InstancedMeshfor many copies of the same mesh). - Use
requestAnimationFrame, neversetInterval, for render loops. - Always
cancelAnimationFrameand dispose Three resources indestroy(). - 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.ts—start()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— theModuleBasesuperclass.src/projector/helpers/threeBase.ts—BaseThreeJsModule.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.