This guide covers creating custom visual modules for nw_wrld, including the workspace (project folder) module contract and the runtime-injected SDK surface.
- Workspace Modules
- Module Architecture
- Your First Module
- Module Lifecycle
- Working with Methods
- Option Types Reference
- SDK API Reference
- Working with Assets
- Using Libraries
- Starter Modules Reference
- Advanced Patterns
- Debugging Modules
- Best Practices
- Performance Tips
Modules live in your project's modules/ folder, not in the application source code. This architecture enables:
- ✅ Edit modules without touching application code
- ✅ Hot-reload: changes apply immediately when you save
- ✅ Portability: share project folders with modules intact
- ✅ Trusted project code: modules are JavaScript files executed by nw_wrld
- ✅ No build step: pure JavaScript, runs directly
Trust note: Only open project folders you trust. Workspace modules are code.
MyProject/
└── modules/ # ← Your modules go here
├── Text.js # Included starter module
├── HelloWorld.js # Included starter module
└── MyModule.js # Your custom module
- Navigate to your project folder
- Open the
modules/directory - Create
MyModule.js - Save → nw_wrld detects and loads it automatically
- The module appears in the Dashboard dropdown
All workspace modules must follow the docblock contract:
- File identity:
modules/Foo.js→ module idFoo(letters/numbers only; must start with a letter) - Required docblock fields:
@nwWrld name: ...(display name in the UI)@nwWrld category: ...(how it groups in the UI)@nwWrld imports: ...(comma-separated dependency tokens; must include at least one)
- Default export: the module must end with
export default YourClass;
Allowed @nwWrld imports:
- SDK:
ModuleBase,BaseThreeJsModule,assetUrl,readText,loadJson - Global libs:
THREE,p5,d3,Noise - THREE.js Loaders:
OBJLoader,PLYLoader,PCDLoader,GLTFLoader,STLLoader
/*
@nwWrld name: MyModule
@nwWrld category: 2D
@nwWrld imports: ModuleBase
*/
class MyModule extends ModuleBase {
constructor(container) {
super(container);
this.init();
}
init() {
// setup code
}
destroy() {
// cleanup code
super.destroy();
}
}
export default MyModule;Important: Do not use path-based import ... from ... inside workspace modules. Workspace modules are loaded from your project folder at runtime.
Signal Flow: Trigger (Sequencer/MIDI/OSC) → Dashboard (maps trigger to method) → Projector (calls method on module) → Module (updates visuals)
Inheritance: All modules extend ModuleBase (provides this.elem, built-in methods like show/hide, transformations, cleanup). For 3D graphics, extend BaseThreeJsModule instead.
Hot Reloading: When you save a module file in your workspace, nw_wrld detects the change, reloads the module definition, and updates the Dashboard. If the module is currently active in a track, it will be reloaded in the Projector.
Let's create a simple pulsing circle module in your workspace.
Navigate to your project folder and create modules/PulsingCircle.js:
/*
@nwWrld name: PulsingCircle
@nwWrld category: 2D
@nwWrld imports: ModuleBase
*/
class PulsingCircle extends ModuleBase {
// Define available methods
static methods = [
{
name: "pulse",
executeOnLoad: false,
options: [
{
name: "intensity",
defaultVal: 1.5,
type: "number",
},
{
name: "duration",
defaultVal: 500,
type: "number",
},
],
},
{
name: "color",
executeOnLoad: true,
options: [
{
name: "color",
defaultVal: "#00FF00",
type: "color",
},
],
},
];
constructor(container) {
super(container);
this.canvas = null;
this.ctx = null;
this.circleScale = 1;
this.color = "#00FF00";
this.init();
}
init() {
// Create canvas
this.canvas = document.createElement("canvas");
this.canvas.width = this.elem.offsetWidth;
this.canvas.height = this.elem.offsetHeight;
this.elem.appendChild(this.canvas);
this.ctx = this.canvas.getContext("2d");
// Draw initial circle
this.draw();
}
draw() {
const ctx = this.ctx;
const width = this.canvas.width;
const height = this.canvas.height;
const radius = (Math.min(width, height) / 4) * this.circleScale;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw circle
ctx.beginPath();
ctx.arc(width / 2, height / 2, radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
pulse({ intensity = 1.5, duration = 500 }) {
// Animate scale from 1 to intensity and back
const startScale = this.circleScale;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = elapsed / duration;
if (progress < 0.5) {
// Growing phase
this.circleScale =
startScale + (intensity - startScale) * (progress * 2);
} else if (progress < 1) {
// Shrinking phase
this.circleScale =
intensity - (intensity - startScale) * ((progress - 0.5) * 2);
} else {
// Animation complete
this.circleScale = startScale;
this.draw();
return;
}
this.draw();
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
color({ color = "#00FF00" }) {
this.color = color;
this.draw();
}
destroy() {
if (this.canvas && this.canvas.parentNode === this.elem) {
this.elem.removeChild(this.canvas);
this.canvas = null;
this.ctx = null;
}
super.destroy();
}
}
export default PulsingCircle;- Save the file → nw_wrld automatically detects and loads it
- In the Dashboard, create a track
- Click [+ MODULE] → Select "PulsingCircle" from the dropdown
- Add a channel and program a pattern in the sequencer grid
- Assign the
pulsemethod to the channel - Click [PLAY] to test your module
Hot Reload: Any changes you make to the file will be detected automatically when you save. No need to restart the app.
For live performance testing, switch to External Mode in Settings and trigger from your DAW.
constructor(container) {
super(container); // Call this first
// Initialize your instance variables
this.myVariable = null;
// Call your init method
this.init();
}super() sets up this.elem, transformation states, and hides the module by default.
init() {
// Create DOM elements
// Set up canvases
// Initialize libraries (p5, Three.js, etc.)
// Load assets
}Keep initialization fast. Set defaults but don't start animations.
myMethod({ param1 = defaultValue, param2 = defaultValue }) {
// Validate parameters
// Update visual state
// Trigger animations
// Redraw if needed
}Methods with executeOnLoad: true run automatically after init() for initial setup. Methods with executeOnLoad: false wait for sequencer or external triggers. Always use default parameters.
destroy() {
// Stop animations
// Remove event listeners
// Clean up DOM elements
// Destroy library instances
super.destroy(); // Call this last
}Critical for preventing memory leaks and stopping background processes.
static methods = [
{
name: "methodName", // Must match function name
executeOnLoad: true, // Run on module load?
options: [ // Parameters
{
name: "paramName", // Parameter name
defaultVal: "value", // Default value
type: "text", // UI control type
min: 0, // (optional) for numbers
max: 100, // (optional) for numbers
values: ["a", "b"], // (optional) for selects
allowRandomization: true, // (optional) add randomize button
},
],
},
];executeOnLoad: true - Runs automatically when module loads (for initial setup: colors, sizes, text, positions)
executeOnLoad: false - Waits for sequencer or external trigger (for animations, effects, state changes)
- Use camelCase:
myMethod,color - Be descriptive
- Method name in
static methodsmust match function name exactly
| Type | Description | Example |
|---|---|---|
text |
Text input | { name: "message", defaultVal: "Hello", type: "text" } |
number |
Numeric input | { name: "size", defaultVal: 50, type: "number", min: 10, max: 200 } |
color |
Color picker (hex) | { name: "color", defaultVal: "#FF0000", type: "color" } |
boolean |
Toggle switch | { name: "enabled", defaultVal: true, type: "boolean" } |
select |
Dropdown menu | { name: "mode", defaultVal: "bounce", type: "select", values: ["bounce", "slide"] } |
matrix |
Grid position | { name: "position", defaultVal: { rows: 1, cols: 1, excludedCells: [] }, type: "matrix" } |
All options create UI controls in the Dashboard and pass values to your methods.
nw_wrld injects the identifiers you request in @nwWrld imports so your module code can use them directly (ModuleBase, assetUrl, THREE, etc.).
Internally, these are provided by the runtime; libraries are available as globalThis.THREE/p5/d3.
// Recommended: declare imports in the docblock and use the injected identifiers.
/*
@nwWrld name: Uses SDK
@nwWrld category: 2D
@nwWrld imports: ModuleBase, assetUrl, readText, loadJson
*/The foundation for all 2D and DOM-based modules.
/*
@nwWrld name: MyModule
@nwWrld category: 2D
@nwWrld imports: ModuleBase
*/
class MyModule extends ModuleBase {
static methods = [
// your methods
];
constructor(container) {
super(container);
// super() provides this.elem
this.init();
}
}Inherited Properties:
this.elem- The container DOM element for your modulethis.externalElements- Array for tracking external DOM elements
Inherited Methods:
show()- Make module visiblehide()- Hide moduleoffset({ x, y })- Reposition modulescale({ scale })- Scale moduleopacity({ opacity })- Set opacityrotate({ degrees })- Rotate modulerandomZoom()- Random zoom effectmatrix({ position })- Position using matrix grid
Base class for Three.js 3D modules (extends ModuleBase).
/*
@nwWrld name: My3DModule
@nwWrld category: 3D
@nwWrld imports: BaseThreeJsModule, THREE
*/
class My3DModule extends BaseThreeJsModule {
static methods = [
// your methods
];
constructor(container) {
super(container);
// super() provides this.scene, this.camera, this.renderer
this.init();
}
init() {
// Add objects to this.scene
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
this.scene.add(cube);
}
}Additional Inherited Properties:
this.scene- Three.js scenethis.camera- Three.js camerathis.renderer- Three.js rendererthis.controls- Orbit controls (if enabled)
Get a nw-assets:// URL for a workspace asset (scoped to the current project).
const imageUrl = assetUrl("images/blueprint.png");
if (imageUrl) {
this.img.src = imageUrl;
}Parameters:
path(string) - Relative path fromassets/folder
Returns: string | null - Asset URL or null if invalid
Path safety: Paths are constrained to the assets/ folder. Attempts to access files outside the project assets will return null.
Load and parse a JSON file from workspace assets.
const data = await loadJson("json/meteor.json");
if (data) {
this.processData(data);
}Parameters:
path(string) - Relative path fromassets/folder
Returns: Promise<object | null> - Parsed JSON or null if error
Example with fallback:
async loadData() {
const data = await loadJson("json/data.json");
if (data && Array.isArray(data)) {
this.dataset = data;
} else {
// Fallback to hardcoded data
this.dataset = [{ id: 1, value: 100 }];
}
}Read a text file from workspace assets.
const text = await readText("data/poem.txt");
if (text) {
this.displayText(text);
}Parameters:
path(string) - Relative path fromassets/folder
Returns: Promise<string | null> - File contents or null if error
| Method | Purpose | Returns |
|---|---|---|
assetUrl(path) |
Get nw-assets:// URL for asset |
string | null |
loadJson(path) |
Load & parse JSON file | Promise<object | null> |
readText(path) |
Read text file | Promise<string | null> |
Your project folder includes an assets/ directory for images, JSON data, and other resources that modules can load.
MyProject/
└── assets/
├── images/ # Images (PNG, JPG, GIF, etc.)
│ └── blueprint.png
├── models/ # 3D models (OBJ, PLY, PCD, GLTF/GLB, STL)
│ └── cube.obj
└── json/ # JSON data files
└── meteor.json
- Navigate to your project folder
- Open the
assets/directory - Add files to
images/orjson/subdirectories - Reference them in your modules using the SDK
/*
@nwWrld name: ImageModule
@nwWrld category: 2D
@nwWrld imports: ModuleBase, assetUrl
*/
class ImageModule extends ModuleBase {
static methods = [
{
name: "image",
executeOnLoad: true,
options: [
{
name: "path",
defaultVal: "images/blueprint.png",
type: "text",
},
],
},
];
constructor(container) {
super(container);
this.img = null;
this.init();
}
init() {
this.img = document.createElement("img");
this.img.style.cssText = "width: 100%; height: 100%; object-fit: contain;";
this.elem.appendChild(this.img);
}
image({ path = "images/blueprint.png" } = {}) {
const url = assetUrl(path);
if (this.img && url) {
this.img.src = url;
}
this.show();
}
destroy() {
if (this.img && this.img.parentNode === this.elem) {
this.elem.removeChild(this.img);
}
this.img = null;
super.destroy();
}
}
export default ImageModule;/*
@nwWrld name: DataViz
@nwWrld category: 2D
@nwWrld imports: ModuleBase, loadJson
*/
class DataViz extends ModuleBase {
static methods = [
{
name: "loadData",
executeOnLoad: true,
options: [
{
name: "count",
defaultVal: 5,
type: "number",
},
],
},
];
constructor(container) {
super(container);
this.dataset = null;
this.init();
}
init() {
// Setup canvas or DOM elements
}
async loadData({ count = 5 } = {}) {
// Try to load from workspace
const data = await loadJson("json/meteor.json");
if (data && Array.isArray(data)) {
// Use workspace data
this.dataset = data.slice(0, count);
} else {
// Fallback to generated data
this.dataset = this.generateFallbackData(count);
}
this.render();
}
generateFallbackData(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
value: Math.random() * 100,
}));
}
render() {
// Render the dataset
}
}
export default DataViz;async loadPoem() {
const text = await readText("data/poem.txt");
if (text) {
this.displayText(text);
} else {
this.displayText('Default text');
}
}✅ Correct:
assetUrl("images/photo.png");
loadJson("json/data.json");
readText("data/text.txt");❌ Incorrect:
assetUrl("/images/photo.png"); // Don't start with /
assetUrl("../other/file.png"); // Can't escape assets folder
assetUrl("/etc/passwd"); // Path safety: constrained to assets/
assetUrl("http://example.com/file.obj"); // No external URLsNew projects include two starter assets:
assets/images/blueprint.png- Example image (used by Image module)assets/json/meteor.json- Example dataset (used by AsteroidGraph module)assets/models/cube.obj- Example OBJ model (used by ModelLoader)assets/models/tetra.stl- Example STL model (used by ModelLoader)assets/models/triangle.ply- Example PLY model (used by ModelLoader)assets/models/points.pcd- Example PCD point cloud (used by ModelLoader)assets/models/triangle.gltf- Example glTF model (used by ModelLoader)
Study the Image and AsteroidGraph starter modules to see asset loading patterns in action.
p5.js is available globally in workspace modules.
/*
@nwWrld name: MyP5Module
@nwWrld category: 2D
@nwWrld imports: ModuleBase, p5
*/
class MyP5Module extends ModuleBase {
static methods = [];
constructor(container) {
super(container);
this.myp5 = null;
this.init();
}
init() {
if (!p5) return;
const sketch = (p) => {
p.setup = () => {
p.createCanvas(this.elem.offsetWidth, this.elem.offsetHeight);
p.background(0);
};
p.draw = () => {
// Your drawing code
p.fill(255);
p.ellipse(p.mouseX, p.mouseY, 50, 50);
};
};
this.myp5 = new p5(sketch, this.elem);
}
destroy() {
if (this.myp5) {
this.myp5.remove();
this.myp5 = null;
}
super.destroy();
}
}
export default MyP5Module;See the GridDots and AsteroidGraph starter modules for complete p5.js examples.
Extend BaseThreeJsModule instead of ModuleBase for Three.js projects.
/*
@nwWrld name: My3DModule
@nwWrld category: 3D
@nwWrld imports: BaseThreeJsModule, THREE
*/
class My3DModule extends BaseThreeJsModule {
static methods = [];
constructor(container) {
super(container);
this.cube = null;
this.init();
}
init() {
if (!THREE) return;
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() {
if (this.cube) {
this.cube.rotation.x += 0.01;
this.cube.rotation.y += 0.01;
}
}
destroy() {
if (this.cube) {
this.scene.remove(this.cube);
this.cube.geometry.dispose();
this.cube.material.dispose();
this.cube = null;
}
super.destroy();
}
}
export default My3DModule;See the SpinningCube, CubeCube, and OrbitalPlane starter modules for complete Three.js examples.
Use assetUrl() to safely reference models from your project’s assets/ folder, then load them with the THREE.js loader classes.
/*
@nwWrld name: MyModelLoader
@nwWrld category: 3D
@nwWrld imports: BaseThreeJsModule, THREE, assetUrl, OBJLoader, GLTFLoader
*/
class MyModelLoader extends BaseThreeJsModule {
loadModel({ modelPath = "models/cube.obj" } = {}) {
const url = assetUrl(modelPath);
if (!url) return;
const ext = String(modelPath).split(".").pop().toLowerCase();
const loader =
ext === "obj"
? new OBJLoader()
: ext === "glb" || ext === "gltf"
? new GLTFLoader()
: null;
if (!loader) return;
loader.load(url, (result) => {
const root = result?.scene || result;
this.setModel(root);
});
}
}
export default MyModelLoader;D3.js is available globally in workspace modules.
/*
@nwWrld name: MyD3Module
@nwWrld category: 2D
@nwWrld imports: ModuleBase, d3
*/
class MyD3Module extends ModuleBase {
static methods = [];
constructor(container) {
super(container);
this.svg = null;
this.init();
}
init() {
if (!d3) return;
this.svg = d3
.select(this.elem)
.append("svg")
.attr("width", "100%")
.attr("height", "100%");
// Create your visualization
const data = [10, 20, 30, 40, 50];
this.svg
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", (d, i) => i * 100 + 50)
.attr("cy", 100)
.attr("r", (d) => d);
}
destroy() {
if (this.svg) {
this.svg.remove();
this.svg = null;
}
super.destroy();
}
}
export default MyD3Module;The following libraries are available globally in workspace modules:
- p5 -
globalThis.p5- Creative coding and canvas drawing - THREE -
globalThis.THREE- 3D graphics - d3 -
globalThis.d3- Data visualization
Every new project includes 16 starter modules that demonstrate different techniques and patterns. Study these modules to learn best practices.
File: modules/HelloWorld.js
Purpose: Minimal working example - the simplest possible module
Techniques: Basic DOM manipulation, text display
Good for: Understanding the minimum module structure
File: modules/Text.js
Purpose: Configurable text display and manipulation
Techniques: DOM text rendering, CSS styling, method options
Good for: Learning method parameters and text handling
File: modules/Corners.js
Purpose: DOM-based corner UI elements
Techniques: Fixed positioning, UI overlays
Good for: Creating persistent UI elements
File: modules/GridOverlay.js
Purpose: Canvas-based grid overlay
Techniques: Canvas 2D API, responsive sizing
Good for: Learning canvas drawing basics
File: modules/GridDots.js
Purpose: Animated dot grid patterns with p5.js
Techniques: p5.js sketches, animation loops, noise
Good for: Learning p5.js integration
File: modules/Frame.js
Purpose: Border frame overlay
Techniques: CSS borders, responsive containers
Good for: Simple visual framing
File: modules/Image.js
Purpose: Load and display images from workspace assets
Techniques: assetUrl(), image loading
Good for: Learning asset loading patterns
File: modules/CodeColumns.js
Purpose: Matrix-style animated text columns
Techniques: DOM animation, text effects
Good for: Creative text animations
File: modules/SpinningCube.js
Purpose: Basic Three.js cube with rotation
Techniques: BaseThreeJsModule, basic 3D objects, animation
Good for: Learning Three.js basics
File: modules/CubeCube.js
Purpose: Nested cube visualization
Techniques: Complex Three.js scenes, multiple objects
Good for: Intermediate Three.js patterns
File: modules/OrbitalPlane.js
Purpose: Orbital mechanics simulation
Techniques: Mathematical animation, 3D transformations
Good for: Physics-based 3D visuals
File: modules/LowEarthPoint.js
Purpose: Low earth orbit visualization
Techniques: Orbital calculations, particle systems
Good for: Advanced 3D techniques
File: modules/AsteroidGraph.js
Purpose: p5.js visualization with workspace JSON data
Techniques: loadJson(), data processing, p5.js graphs
Good for: Learning asset loading and data visualization
File: modules/MathOrbitalMap.js
Purpose: Mathematical orbit mapping
Techniques: Mathematical visualization, coordinate systems
Good for: Mathematical graphics
File: modules/CloudPointIceberg.js
Purpose: 3D point cloud visualization
Techniques: Three.js points, particle systems
Good for: Point cloud rendering
File: modules/ZKProofVisualizer.js
Purpose: Zero-knowledge proof visualization
Techniques: Complex animations, state visualization
Good for: Advanced visualization patterns
- Start with HelloWorld - Understand the bare minimum structure
- Study Text - Learn method parameters and configuration
- Try Image or AsteroidGraph - Learn asset loading
- Explore SpinningCube - Get into 3D basics
- Copy and modify - Duplicate a module and change it to learn
All starter modules are fully editable in your project's modules/ folder. Experiment freely!
Use requestAnimationFrame for smooth animations. Always store the ID and cancel it in destroy().
init() {
this.animationId = null;
this.startAnimation();
}
startAnimation() {
const animate = () => {
// Update your visuals
this.render();
this.animationId = requestAnimationFrame(animate);
};
this.animationId = requestAnimationFrame(animate);
}
destroy() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
super.destroy();
}For DOM elements created outside this.elem, add them to this.externalElements array for automatic cleanup.
init() {
const overlay = document.createElement('div');
overlay.style.cssText = 'position: fixed; top: 0; left: 0;';
document.body.appendChild(overlay);
// Track for automatic cleanup
this.externalElements.push(overlay);
}
// ModuleBase.destroy() will remove all externalElements automaticallyAdd allowRandomization: true to any option to enable a randomization button in the UI (select options also support randomization in the Dashboard).
options: [
{
name: "color",
defaultVal: "#FF0000",
type: "color",
allowRandomization: true, // Adds random button in UI
},
];If your module needs to load assets during initialization, use an async pattern:
constructor(container) {
super(container);
this.dataset = null;
this.init();
}
init() {
// Synchronous setup
this.canvas = document.createElement('canvas');
this.elem.appendChild(this.canvas);
// Load assets asynchronously
this.loadAssets();
}
async loadAssets() {
try {
this.dataset = await loadJson("json/data.json");
if (this.dataset) {
this.render();
}
} catch (error) {
console.error('Failed to load assets:', error);
}
}For complex modules with multiple states, create a clear state management pattern:
constructor(container) {
super(container);
this.state = {
mode: 'idle',
speed: 1.0,
paused: false,
};
this.init();
}
updateState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}
togglePause() {
this.updateState({ paused: !this.state.paused });
}Handle window resize events for responsive modules:
init() {
this.onResize = this.onResize.bind(this);
window.addEventListener('resize', this.onResize);
this.resize();
}
resize() {
const width = this.elem.offsetWidth;
const height = this.elem.offsetHeight;
// Update canvas, Three.js renderer, etc.
}
onResize() {
// Debounce for performance
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.resizeTimeout = setTimeout(() => {
this.resize();
}, 250);
}
destroy() {
window.removeEventListener('resize', this.onResize);
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
super.destroy();
}Open the Developer Console in the Projector window:
- Mac:
Cmd + Option + I - Windows:
Ctrl + Shift + I
The console shows:
- JavaScript errors in your modules
console.log()output- Asset loading failures
- Module lifecycle events
Add debug logging to understand your module's behavior:
init() {
console.log('[MyModule] Initializing...');
// setup code
console.log('[MyModule] Initialized successfully');
}
myMethod({ param = 'default' }) {
console.log('[MyModule] myMethod called with:', param);
// method code
}When you save a module file, watch the Dashboard for feedback:
- ✅ Success: Module reloads silently
⚠️ Warning: Yellow badge shows broken module (check console)- ❌ Error: Module missing entirely (check filename and export)
| Error | Cause | Fix |
|---|---|---|
| "Module does not have an 'elem' property" | Forgot super() |
Call super(container) first in constructor |
| "Cannot read property 'appendChild' of null" | this.elem not ready |
Ensure super() is called before accessing this.elem |
| "Method not found" | Name mismatch | Method name in static methods must match function name exactly |
| "Module doesn't appear in Dashboard" | Contract issues | Ensure docblock has @nwWrld name/category/imports and file ends with export default |
| "ModuleBase is not defined" | Missing import | Add ModuleBase to @nwWrld imports |
| "Asset failed to load" | Invalid path | Verify path is relative to assets/ folder, no leading / |
| "Module loads but nothing visible" | Module hidden | Call this.show() or set a method with executeOnLoad: true |
To see if your module file is being detected:
- Open Dashboard
- Create or select a track
- Click [+ MODULE]
- Your module should appear in the dropdown
If it doesn't appear:
- Check that the file is saved in
modules/folder - Verify filename ends with
.js - Check that
export default ClassNameexists - Open console in Dashboard (
Cmd+Option+I) for errors
To debug asset loading issues:
async testAssetLoading() {
const imageUrl = assetUrl("images/test.png");
console.log('Image URL:', imageUrl);
const data = await loadJson("json/data.json");
console.log('Loaded data:', data);
}- Edit module file in your text editor
- Save the file
- Check Dashboard - module should reload
- Test by adding to a track and triggering methods
- Debug using console if issues arise
- Iterate - repeat steps 1-5
-
Use docblock imports and rely on the injected identifiers:
/* @nwWrld name: MyModule @nwWrld category: 2D @nwWrld imports: ModuleBase */
-
Always call
super(container)first in constructor:constructor(container) { super(container); // Must be first this.myProperty = null; this.init(); }
-
Use descriptive names:
- Display name:
@nwWrld name: ... - Category:
@nwWrld category: ... - Method names: Clear, action-oriented (
loadData,color,animate)
- Display name:
-
Always use default parameters:
myMethod({ color = "#FFFFFF", size = 50 } = {}) { // Now safe if called with no arguments }
-
Validate input where needed:
setCount({ count = 10 } = {}) { const safeCount = Math.max(1, Math.min(100, Number(count) || 10)); this.count = safeCount; }
-
Use
executeOnLoadappropriately:executeOnLoad: truefor setup methods (colors, text, initial state)executeOnLoad: falsefor trigger methods (animations, effects)
-
Always check for null returns:
const data = await loadJson("json/data.json"); if (data && Array.isArray(data)) { // Use data } else { // Fallback }
-
Provide fallbacks for missing assets:
async loadData() { const data = await loadJson("json/data.json"); this.dataset = data || this.generateDefaultData(); }
-
Use relative paths (no leading slash):
- ✅
'images/photo.png' - ❌
'/images/photo.png'
- ✅
-
Always clean up in
destroy():destroy() { // Stop animations if (this.animationId) { cancelAnimationFrame(this.animationId); } // Remove listeners window.removeEventListener('resize', this.onResize); // Dispose Three.js resources if (this.geometry) this.geometry.dispose(); if (this.material) this.material.dispose(); // Remove DOM elements if (this.canvas && this.elem.contains(this.canvas)) { this.elem.removeChild(this.canvas); } // Call parent cleanup super.destroy(); }
-
Null out references after cleanup:
this.canvas = null; this.myp5 = null; this.geometry = null;
-
Use
this.externalElementsfor DOM elements outsidethis.elem
- Keep methods focused - Each method does one thing well
- Extract reusable logic into private methods
- Comment non-obvious code - Explain "why", not "what"
- Use consistent formatting - Follow the starter module style
-
Fail gracefully:
init() { try { this.setupComplexFeature(); } catch (error) { console.error('[MyModule] Setup failed:', error); this.setupFallback(); } }
-
Log useful information:
console.log("[MyModule] Loading asset:", path); console.error("[MyModule] Failed to load:", path, error);
-
Check library availability:
init() { if (!p5) { console.error('[MyModule] p5.js not available'); return; } // Continue with p5 setup }
-
Batch DOM updates - Minimize reflows and repaints:
// Bad: Multiple style updates this.elem.style.width = "100px"; this.elem.style.height = "100px"; this.elem.style.opacity = "0.5"; // Good: Single cssText update this.elem.style.cssText = "width: 100px; height: 100px; opacity: 0.5;";
-
Use requestAnimationFrame for animations (not
setIntervalorsetTimeout):// Bad setInterval(() => this.render(), 16); // Good const animate = () => { this.render(); this.animationId = requestAnimationFrame(animate); }; animate();
-
Debounce expensive operations:
onResize() { clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(() => { this.resize(); }, 250); }
-
Clear only what you need:
// If full clear needed ctx.clearRect(0, 0, canvas.width, canvas.height); // If partial clear possible ctx.clearRect(x, y, width, height);
-
Use offscreen canvas for complex rendering:
init() { this.offscreen = document.createElement('canvas'); this.offscreenCtx = this.offscreen.getContext('2d'); // Render to offscreen, then copy to visible canvas }
-
Cache unchanging elements:
// Draw static background once this.cachedBackground = this.renderBackground(); render() { // Use cached background ctx.drawImage(this.cachedBackground, 0, 0); // Draw dynamic elements on top }
-
Reuse geometries and materials:
// Bad: New geometry/material for each object for (let i = 0; i < 100; i++) { const geo = new THREE.BoxGeometry(1, 1, 1); const mat = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const mesh = new THREE.Mesh(geo, mat); } // Good: Shared geometry/material const geo = new THREE.BoxGeometry(1, 1, 1); const mat = new THREE.MeshBasicMaterial({ color: 0xff0000 }); for (let i = 0; i < 100; i++) { const mesh = new THREE.Mesh(geo, mat); // Position mesh differently }
-
Limit object counts:
setCount({ count = 100 } = {}) { // Cap at reasonable maximum const safeCount = Math.min(count, 1000); this.createObjects(safeCount); }
-
Use instanced meshes for many identical objects:
const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000); this.scene.add(instancedMesh);
-
Dispose resources properly:
destroy() { if (this.mesh) { this.scene.remove(this.mesh); this.mesh.geometry.dispose(); this.mesh.material.dispose(); } super.destroy(); }
-
Avoid unnecessary redraws:
p.setup = () => { p.createCanvas(width, height); p.noLoop(); // Don't redraw unless needed }; myMethod() { // Update state, then redraw once this.updateState(); this.myp5.redraw(); }
-
Use p5 rendering modes:
p.setup = () => { p.createCanvas(width, height, p.WEBGL); // Hardware accelerated };
-
Avoid memory leaks:
- Remove event listeners in
destroy() - Cancel animation frames
- Clear intervals/timeouts
- Dispose Three.js resources
- Remove p5 instances
- Remove event listeners in
-
Monitor memory usage:
- Use Chrome DevTools Memory profiler
- Test loading/unloading modules repeatedly
- Check for increasing memory over time
-
Load assets once, reuse:
async init() { // Load once during init this.dataset = await loadJson("json/data.json"); } myMethod() { // Reuse loaded dataset this.processData(this.dataset); }
-
Consider asset size:
- Optimize images before adding to workspace
- Compress JSON data where possible
- Limit texture sizes for Three.js
- Module appears in Dashboard dropdown
- All methods appear in method selector
- All method options render correctly (text, number, color, etc.)
-
executeOnLoadbehavior works as expected - Methods trigger correctly from sequencer
- Visual output appears in Projector
- Hot reload works when file is saved
- No console errors on load or execution
- Module cleans up properly (check
destroy())
-
Test each method individually:
- Create a track, add your module
- Add a channel, assign one method
- Trigger the method and verify behavior
- Repeat for each method
-
Test with different parameters:
- Try minimum and maximum values
- Test with invalid inputs
- Verify default values work
-
Test hot reload:
- Make a small change (e.g., change a color)
- Save the file
- Verify the module reloads
- Check that the change appears
- Check console for warnings
- Monitor FPS in Projector window
- Test with multiple instances (add module to multiple tracks)
- Test long-running (let it run for several minutes)
- Create a track and add your module
- Remove the module from the track
- Check console for errors
- Add the module again
- Repeat several times to check for memory leaks
The 16 starter modules in your project's modules/ folder are your best learning resource:
- HelloWorld.js - Simplest structure
- Text.js - Method options and DOM
- Image.js - Asset loading
- AsteroidGraph.js - Data visualization with assets
- SpinningCube.js - Three.js basics
- GridDots.js - p5.js patterns
- p5.js Reference - Creative coding and canvas
- Three.js Documentation - 3D graphics
- D3.js Documentation - Data visualization
- GitHub Discussions - Ask questions, share modules
- GitHub Issues - Report bugs, request features
Happy module development! 🎨