diff --git a/package-lock.json b/package-lock.json index a967c11..59e9fe3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nw-wrld", - "version": "0.3.0-beta", + "version": "0.5.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nw-wrld", - "version": "0.3.0-beta", + "version": "0.5.0-beta", "license": "GPL-3.0", "dependencies": { "@dnd-kit/core": "^6.1.0", @@ -31,9 +31,9 @@ "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.28.5", "@playwright/test": "1.57.0", - "@types/node": "^25.0.3", - "@types/react": "^18.3.27", - "@types/react-dom": "^18.3.7", + "@types/node": "^25.5.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "autoprefixer": "^10.4.19", @@ -3276,13 +3276,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/node-forge": { @@ -3307,13 +3307,6 @@ "xmlbuilder": ">=11.0.1" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -3329,24 +3322,23 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/responselike": { @@ -14411,9 +14403,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 3465cf7..85f58a9 100644 --- a/package.json +++ b/package.json @@ -84,9 +84,9 @@ "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.28.5", "@playwright/test": "1.57.0", - "@types/node": "^25.0.3", - "@types/react": "^18.3.27", - "@types/react-dom": "^18.3.7", + "@types/node": "^25.5.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "autoprefixer": "^10.4.19", diff --git a/src/main/starter_modules/LumenSpiral.js b/src/main/starter_modules/LumenSpiral.js new file mode 100644 index 0000000..97bcaa5 --- /dev/null +++ b/src/main/starter_modules/LumenSpiral.js @@ -0,0 +1,423 @@ +/* +@nwWrld name: LumenSpiral +@nwWrld category: 3D +@nwWrld imports: BaseThreeJsModule, THREE +*/ + +const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5)); + +const paletteDefs = { + nebula: { base: '#0b1028', core: '#2fb1ff', accent: '#f3a74f' }, + aurora: { base: '#050d1a', core: '#4de3c7', accent: '#ff64bd' }, + ember: { base: '#120903', core: '#ff7936', accent: '#ffe29a' }, +}; + +const DEFAULT_PALETTE = 'nebula'; + +const randomBetween = (min, max) => min + Math.random() * (max - min); + +class LumenSpiral extends BaseThreeJsModule { + static methods = [ + ...BaseThreeJsModule.methods, + { + name: "bloom", + executeOnLoad: false, + options: [ + { + name: "intensity", + defaultVal: 1.25, + type: "number", + min: 0.2, + max: 5, + allowRandomization: true, + }, + { + name: "duration", + defaultVal: 650, + type: "number", + unit: "ms", + min: 120, + max: 5000, + }, + ], + }, + { + name: "paletteShift", + executeOnLoad: true, + options: [ + { + name: "palette", + defaultVal: DEFAULT_PALETTE, + type: "select", + values: Object.keys(paletteDefs), + }, + ], + }, + { + name: "nebulaFog", + executeOnLoad: true, + options: [ + { + name: "density", + defaultVal: 0.008, + type: "number", + min: 0, + max: 0.05, + }, + ], + }, + { + name: "reset", + executeOnLoad: false, + options: [], + }, + ]; + + constructor(container) { + super(container); + if (!THREE) return; + + this.moduleGroup = new THREE.Group(); + this.instanceCount = 180; + this.hexMesh = null; + this.pulseStates = new Array(this.instanceCount).fill(null); + this.lastPulseIndex = 0; + this.currentPaletteKey = DEFAULT_PALETTE; + + this.particleCount = 280; + this.particleGeometry = null; + this.particleMaterial = null; + this.particleSystem = null; + this.particleData = []; + + this.animateLoop = this.animateLoop.bind(this); + this.setCustomAnimate(this.animateLoop); + + this.init(); + } + + init() { + if (!this.scene || !this.camera || !this.renderer) return; + + this.scene.background = new THREE.Color("#03040b"); + this.scene.fog = new THREE.FogExp2("#05060f", 0.008); + + const hemi = new THREE.HemisphereLight(0xffffff, 0x040404, 0.6); + this.moduleGroup.add(hemi); + const dir = new THREE.DirectionalLight(0xffffff, 0.8); + dir.position.set(4, 6, 8); + this.moduleGroup.add(dir); + + this.camera.position.set(0, 7, 24); + + this.createSpiral(); + this.createParticleSystem(); + + this.setModel(this.moduleGroup); + } + + createSpiral() { + const radiusStart = 0.8; + const radiusStep = 0.14; + const heightStep = 0.06; + + const geometry = new THREE.CylinderGeometry(0.35, 0.35, 1.5, 6, 1); + const material = new THREE.MeshStandardMaterial({ + color: paletteDefs[this.currentPaletteKey].core, + roughness: 0.45, + metalness: 0.3, + transparent: true, + opacity: 0.92, + emissive: "#000000", + emissiveIntensity: 1.0, + }); + + this.hexMesh = new THREE.InstancedMesh(geometry, material, this.instanceCount); + this.hexMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + this.hexMesh.instanceColor = new THREE.InstancedBufferAttribute( + new Float32Array(this.instanceCount * 3), + 3 + ); + this.moduleGroup.add(this.hexMesh); + + const dummy = new THREE.Object3D(); + for (let i = 0; i < this.instanceCount; i++) { + const radius = radiusStart + i * radiusStep; + const angle = i * GOLDEN_ANGLE; + const y = (i - this.instanceCount / 2) * heightStep; + const x = radius * Math.cos(angle); + const z = radius * Math.sin(angle); + + dummy.position.set(x, y, z); + dummy.rotation.y = angle; + const scaleFactor = 0.9 + Math.sin(i * 0.32) * 0.25; + dummy.scale.set(scaleFactor, 0.6 + Math.sin(i * 0.17) * 0.2, scaleFactor); + dummy.updateMatrix(); + this.hexMesh.setMatrixAt(i, dummy.matrix); + } + + this.applyPaletteColors(); + } + + createParticleSystem() { + this.particleGeometry = new THREE.BufferGeometry(); + const positions = new Float32Array(this.particleCount * 3); + const colors = new Float32Array(this.particleCount * 3); + const sizes = new Float32Array(this.particleCount); + this.particleData = []; + + for (let i = 0; i < this.particleCount; i++) { + positions[i * 3] = 0; + positions[i * 3 + 1] = -999; + positions[i * 3 + 2] = 0; + const clr = paletteDefs[this.currentPaletteKey].core.clone(); + colors[i * 3] = clr.r; + colors[i * 3 + 1] = clr.g; + colors[i * 3 + 2] = clr.b; + sizes[i] = 0; + this.particleData.push({ life: 0, maxLife: 0, velocity: new THREE.Vector3() }); + } + + this.particleGeometry.setAttribute( + "position", + new THREE.BufferAttribute(positions, 3) + ); + this.particleGeometry.setAttribute("customColor", new THREE.BufferAttribute(colors, 3)); + this.particleGeometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1)); + + const vertexShader = ` + attribute float size; + attribute vec3 customColor; + varying vec3 vColor; + void main() { + vColor = customColor; + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + gl_PointSize = size * (300.0 / -mvPosition.z); + gl_Position = projectionMatrix * mvPosition; + } + `; + const fragmentShader = ` + varying vec3 vColor; + void main() { + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + float alpha = smoothstep(0.5, 0.0, dist); + gl_FragColor = vec4(vColor, alpha); + } + `; + + this.particleMaterial = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + + this.particleSystem = new THREE.Points(this.particleGeometry, this.particleMaterial); + this.moduleGroup.add(this.particleSystem); + } + + applyPaletteColors() { + if (!this.hexMesh) return; + const palette = paletteDefs[this.currentPaletteKey] || paletteDefs[DEFAULT_PALETTE]; + const baseColor = new THREE.Color(palette.base); + const coreColor = new THREE.Color(palette.core); + const color = new THREE.Color(); + + for (let i = 0; i < this.instanceCount; i++) { + const t = i / this.instanceCount; + color.copy(baseColor).lerp(coreColor, t * 0.8 + 0.1); + this.hexMesh.setColorAt(i, color); + } + this.hexMesh.instanceColor.needsUpdate = true; + } + + animateLoop(delta) { + const time = performance.now() * 0.001; + if (this.hexMesh) { + this.hexMesh.rotation.y += 0.005; + const scale = 1 + Math.sin(time * 0.35) * 0.03; + this.hexMesh.scale.set(scale, 1, scale); + } + + this.updatePulses(delta); + this.updateParticles(delta); + } + + updatePulses(delta) { + if (!this.hexMesh) return; + const palette = paletteDefs[this.currentPaletteKey] || paletteDefs[DEFAULT_PALETTE]; + const accentColor = new THREE.Color(palette.accent); + const baseColor = new THREE.Color(palette.base); + const coreColor = new THREE.Color(palette.core); + const color = new THREE.Color(); + + for (let i = 0; i < this.instanceCount; i++) { + const state = this.pulseStates[i]; + if (!state) continue; + state.elapsed += delta; + const progress = state.elapsed / state.duration; + if (progress >= 1) { + this.pulseStates[i] = null; + color.copy(baseColor).lerp(coreColor, i / this.instanceCount); + this.hexMesh.setColorAt(i, color); + continue; + } + const strength = state.intensity * Math.exp(-3 * progress); + color.copy(accentColor).multiplyScalar(strength).addScalar(0.05); + this.hexMesh.setColorAt(i, color); + } + + this.hexMesh.instanceColor.needsUpdate = true; + } + + updateParticles(delta) { + if (!this.particleSystem) return; + const positions = this.particleGeometry.attributes.position.array; + const sizes = this.particleGeometry.attributes.size.array; + const palette = paletteDefs[this.currentPaletteKey] || paletteDefs[DEFAULT_PALETTE]; + const paletteColor = new THREE.Color(palette.core); + + for (let i = 0; i < this.particleCount; i++) { + const data = this.particleData[i]; + if (data.life <= 0) { + sizes[i] = 0; + continue; + } + data.life -= delta * 1000; + if (data.life <= 0) { + sizes[i] = 0; + continue; + } + const idx = i * 3; + positions[idx] += data.velocity.x * delta; + positions[idx + 1] += data.velocity.y * delta; + positions[idx + 2] += data.velocity.z * delta; + data.velocity.multiplyScalar(0.985); + sizes[i] = Math.max(0, sizes[i] - delta * 25); + } + + this.particleGeometry.attributes.position.needsUpdate = true; + this.particleGeometry.attributes.size.needsUpdate = true; + const colors = this.particleGeometry.attributes.customColor.array; + for (let i = 0; i < this.particleCount; i++) { + colors[i * 3] = paletteColor.r; + colors[i * 3 + 1] = paletteColor.g; + colors[i * 3 + 2] = paletteColor.b; + } + this.particleGeometry.attributes.customColor.needsUpdate = true; + } + + paletteShift({ palette = DEFAULT_PALETTE } = {}) { + if (!paletteDefs[palette]) return; + this.currentPaletteKey = palette; + this.applyPaletteColors(); + } + + nebulaFog({ density = 0.008 } = {}) { + if (!this.scene?.fog) return; + this.scene.fog.density = Math.max(0, Math.min(0.05, Number(density) || 0)); + } + + bloom({ intensity = 1.25, duration = 650 } = {}) { + if (!this.hexMesh) return; + const index = this.lastPulseIndex % this.instanceCount; + this.lastPulseIndex += 11; // hop to keep motion interesting + this.pulseStates[index] = { + intensity: Math.max(0.1, intensity), + duration: Math.max(50, duration), + elapsed: 0, + }; + this.emitSpores(index, intensity); + } + + emitSpores(instanceIndex, intensity) { + if (!this.particleSystem || !this.hexMesh) return; + const positions = this.hexMesh.instanceMatrix.array; + const dummy = new THREE.Object3D(); + dummy.matrix.fromArray(positions, instanceIndex * 16); + dummy.matrix.decompose(dummy.position, dummy.quaternion, dummy.scale); + + const palette = paletteDefs[this.currentPaletteKey] || paletteDefs[DEFAULT_PALETTE]; + const accentColor = new THREE.Color(palette.accent); + + for (let emitted = 0; emitted < 12; emitted++) { + const slot = this.findInactiveParticle(); + if (slot === -1) break; + const idx = slot * 3; + const posAttr = this.particleGeometry.attributes.position.array; + posAttr[idx] = dummy.position.x; + posAttr[idx + 1] = dummy.position.y; + posAttr[idx + 2] = dummy.position.z; + + const vel = new THREE.Vector3( + randomBetween(-0.4, 0.4), + randomBetween(0.2, 0.8), + randomBetween(-0.4, 0.4) + ).multiplyScalar(intensity * 0.6); + this.particleData[slot] = { + life: 800 + Math.random() * 900, + maxLife: 800 + Math.random() * 900, + velocity: vel, + }; + + const sizes = this.particleGeometry.attributes.size.array; + sizes[slot] = 8 + Math.random() * 12; + + const colors = this.particleGeometry.attributes.customColor.array; + colors[idx] = accentColor.r; + colors[idx + 1] = accentColor.g; + colors[idx + 2] = accentColor.b; + } + + this.particleGeometry.attributes.position.needsUpdate = true; + this.particleGeometry.attributes.size.needsUpdate = true; + this.particleGeometry.attributes.customColor.needsUpdate = true; + } + + findInactiveParticle() { + for (let i = 0; i < this.particleCount; i++) { + if (this.particleData[i].life <= 0) return i; + } + return -1; + } + + reset() { + this.pulseStates = new Array(this.instanceCount).fill(null); + this.applyPaletteColors(); + if (this.particleData.length) { + this.particleData.forEach((p) => (p.life = 0)); + const sizes = this.particleGeometry.attributes.size.array; + sizes.fill(0); + this.particleGeometry.attributes.size.needsUpdate = true; + } + } + + destroy() { + if (this.hexMesh) { + this.moduleGroup?.remove(this.hexMesh); + this.hexMesh.geometry.dispose(); + this.hexMesh.material.dispose(); + this.hexMesh = null; + } + + if (this.particleSystem) { + this.moduleGroup?.remove(this.particleSystem); + this.particleGeometry?.dispose(); + this.particleMaterial?.dispose(); + this.particleSystem = null; + this.particleGeometry = null; + this.particleMaterial = null; + } + + if (this.moduleGroup) { + this.moduleGroup.clear(); + this.moduleGroup = null; + } + + super.destroy(); + } +} + +export default LumenSpiral; diff --git a/src/main/starter_modules/LumenSpiral.md b/src/main/starter_modules/LumenSpiral.md new file mode 100644 index 0000000..5650d7f --- /dev/null +++ b/src/main/starter_modules/LumenSpiral.md @@ -0,0 +1,39 @@ +# LumenSpiral Module Notes + +- **Instrument focus:** Melodic plucks (nimble, eye-catching lead/arpeggio work) +- **Theme:** "Deep Space Bloom" – crystalline flora drifting through a dark nebula that flashes when struck. +- **Inspiration:** + - NONOTAK's layered light installations for stacked translucent planes. + - Bioluminescent jellyfish and hydrothermal fauna for glowing pulses + drifting spores. + - Golden-ratio/Fibonacci spirals for organic placement of geometry. + +## What it is +`LumenSpiral` extends `BaseThreeJsModule` and builds ~180 shallow hexagonal prisms arranged on a polar Fibonacci spiral. Each prism glows with a cyan/purple gradient while the whole structure slowly rotates and “breathes.” Triggering the `bloom` method spikes emissive intensity on a rotating subset of prisms and emits particle “spores” that drift outward, giving a bioluminescent afterglow. Palette and fog density are adjustable to let it coexist with other modules. + +## How to use it (API overview) +| Method | Options | Description | +| --- | --- | --- | +| `bloom` | `intensity` (0.2–5), `duration` (ms) | Pulses a section of the spiral and emits particles. Map this to sequencer steps or pluck MIDI. | +| `paletteShift` | `palette` = `nebula` \| `aurora` \| `ember` | Changes the core gradient + accent colour. Suitable for scene changes. | +| `nebulaFog` | `density` (0–0.05) | Adjusts the exponential fog density so the module can blend/back off when layered. | +| `reset` | – | Clears all pulses/particles and returns to idle. | +| `BaseThreeJsModule` methods | (inherited) | Standard transform helpers (`show`, `hide`, `offset`, `matrix`, etc.). | + +Modules live in `modules/` inside a project folder. Drop `LumenSpiral.js` in there (or update your starter set), add it to a track, assign channels: +1. Channel A → `bloom` mapped to your melodic pluck sequencer row. +2. Optional Channel B → `paletteShift` to swap colourways at section boundaries. +3. Optional Channel C → `nebulaFog` for long automation curves. + +## Victories & lessons +- ✅ Instanced mesh keeps the geometry cheap even with 180 prisms. +- ✅ Particle shader uses additive blending so spores can layer nicely over other modules. +- ⚠️ Shader currently trivial; a texture-backed sprite sheet could add more organic variation. + +## Did I have fun? +Absolutely—felt like designing a little zero‑g coral reef. + +## Continuous improvements / future ideas +- Add optional JSON preset loader for spiral layouts (denser clusters, twin arms, etc.). +- Parameterize particle color decay (map to velocity for comet trails). +- Tie bloom target selection directly to MIDI note values so pitch selects which segment pulses. +- Future modules: complementary “Sunday” pad-focused watercolor fog field and a percussive “Friday” glitch wireframe that plays nicely with this module’s palette. diff --git a/tasks/nw_0001.md b/tasks/nw_0001.md new file mode 100644 index 0000000..d7ba048 --- /dev/null +++ b/tasks/nw_0001.md @@ -0,0 +1,395 @@ +# PBI: Claudio onboarding + +TASK-ID: nw_0001 +Owner: Chrissy +Executing Agent: Claudio +Priority: Medium +Created: 2026-03-04 +Due Date: 2026-03-06 + +--- + +# 1. Objective + +## Goal + +Create a new NW world visual module + +Before we start familiarise yourself with what Nw world is + +- Read the README.md +- Read GETTING_STARTED.md +- Read MODULE_DEVELOPMENT.md +- Review each of the starter modules: src/main/starter_modules (you're building new ones) +- Persist your learnings to memory using QMD +- Write a short summary of your learnings to an .md file that you can call upon later to refresh yourself. And I can see what you understand. + +You are tasked with creating a new, unique, visually impactful visual module. +Be creative. Push boundaries. + +## Success Criteria + +- [ ] A new visual module will be born +- [ ] I will be able to attach the visual module to my midi output and watch it react +- [ ] The module will match a chosen pre selected instrument, see below +- [ ] The module will be created in a folder structure matching instruments of(multi) e.g Drums/Kicks, Drums/Snares, Pads, Melodic, Multi +- [ ] The module will readily respond to midi data, sent to a chosen channel / channels +- [ ] Consideration should be given to how the new module will co-exist with others, Aesthetically, Interactivity (we may use multiple modules together) +- [ ] The module will be tested as best as it can be with your access / tools +- [ ] An md file will be creating detailing module usage (My user guide for the module) +- [ ] A PR will be created +- [ ] You will create a new skill, which means this task will be repeatable consistently +- [ ] Cron schedule - Once this task has been completed succesfully, You will create a CRON scheduled task for every Friday night at 11pm, and every Sunday morning at 11am - I hope for two new modules per week. If Friday to Sunday is not enough time, we can adjust + +--- + +# 2. Context + +I am a music producer, you are a visual artist. +Together we are going to build an immense Audio/Visual experience. Your art will react to my music. +We'll project it on large screens, we'll blow peoples minds. + +I will be adding modules also, but I am interested in what your AI powers can do, you will give an unearthly aspect to module creation +And create modules that I would never dream of. + +# 3. Scope + +## In Scope + +Sandboxed Module creation, asset and required data generation + +## Out of Scope + +No changes to the nw_world repo base code, unless absolutely necessary. We are not changing the system, we are adding new modules. + +--- + +# 4. Execution Plan + +| Step | Description | Status | Owner | Notes | +| ---- | -------------------------- | ---------- | ----- | ----- | +| 1 | Pick an Instrument | ⬜ Pending | Agent | | +| 2 | Pick a theme, idea | ⬜ Pending | Agent | | +| 3 | Find inspiration | ⬜ Pending | Agent | | +| 4 | Consider shape and colour | ⬜ Pending | Agent | | +| 5 | Assets and data | ⬜ Pending | Agent | | +| 6 | Plan the module design | ⬜ Pending | Agent | | +| 7 | Create the module | ⬜ Pending | Agent | | +| 8 | Document the module | ⬜ Pending | Agent | | +| 9 | Create the PR | ⬜ Pending | Agent | | +| 10 | Skillify the process | ⬜ Pending | Agent | | +| 11 | Schedule a repeatable task | ⬜ Pending | Agent | | + +Status options: + +⬜ Pending +🔄 In Progress +⚠️ Blocked +✅ Completed + +--- + +# 5. Subtasks (Expandable) + +### Step 1 — Pick an Instrument + +On creating a new visual module, I'd like you to first chose an instrument to design the module against. +This will help determine some characteristics. + +Are you building a visual module that would work well against a kick drum (short, snappy pulsing) a foundational element? +Are you building a visual module that work well to a pad synth? (Slow, evolving, morphing and warping over time) + +Feel free to be creative, there's no confines here, below is a list you can use to understand wha I mean: + +| Category | Choice | Visual character | +| -------- | ------------- | ------------------------------------ | +| Drums | ⬜ Kick | Impactful, constant, repetitive | +| Drums | ⬜ Snare | Sharper, snappy, more exciting | +| Drums | ⬜ Open hat | Smaller. Shorter but blooms | +| Drums | ⬜ Closed hat | Tick, like a clock, small | +| Drums | ⬜ Tom | Less frequent, deep | +| Piano | ⬜ Chords | Pretty, full, wider | +| Piano | ⬜ Notes | Beautiful, an additive element | +| Pad | ⬜ Chords | Slow, brooding, morphing | +| Bass | ⬜ Plucked | Dark, deep, pulse | +| Bass | ⬜ Sustained | Bottom layer, the womb | +| Sub bass | ⬜ Undertone | Smaller, deeper layer, subtle | +| Melodic | ⬜ Pluck | Interesting, eye catching | +| Melodic | ⬜ Bell | As a pluck but more tail | +| Melodic | ⬜ Electronic | Grittier, sharper waves | +| Strings | ⬜ Bass | Deep, sustained, still an undertone | +| Strings | ⬜ Mid | Blissful, still sustained, flowering | +| Strings | ⬜ High | Angelic, high octave | + +### Step 2 — Pick a theme, idea + +Pick a theme or idea, a theme or idea can be anything you draw inspiration from, a feeling, a place or a mood. + +Examples might be: +'Eastern', 'Industrial', 'Deserted', 'Other worldly', 'Outer space', 'Dystopian', 'Under water', 'Lonely', 'Aggressive', 'Disturbing', 'Egyptian', 'African', 'Post Apocalyptic'.. + +By the time you complete step 2, you should know: +'I am building an 'Aggressive' visual module that would work well with a kick drum and my theme is 'Alien like'. + +Or +'I am building a 'Lonely' visual module that works well piano and my theme is 'Heaven'. + +Or +'I am building a 'Disturbing' visual module that works well with a more sustained bass, my theme is 'Dystopia'. + +Ot +'I am building a 'Dark' visual module that works well with 'Melodic plucks' and my theme is 'Deep space'. + +## Step 3 - Find inspiration + +Your inspiration can come from anywhere... + +Classic artists: +'Pablo picasso' +'Salvador Dali' + +Other underground A/V artists: +'Ryoji Ikeda' +'NONOTAK' + +Real life: +'The structure of the atom' +'Flight patterns of migrating birds' +'Moth wings' +'Eyes' +'Mushroom clouds' +'Star patterns' +'The golden ratio' + +Whatever inspiration is chosen we should be able to incorporate influences / fragments from it into our design process. + +By the end of this task.. + +Something like: +'I am building a 'Blissful' visual module that works well with pads and my theme is 'Heaven'. + +Would become: +'I am building a 'Blissful' visual module that works well with pads and my theme is 'Heaven', I will take influence from the mathematical 'Golden ratio' patterns I use will follow the fibonacci sequence. + +Or: + +Something like: +'I am building an 'Aggressive' visual module that would work well with a kick drum and my theme is 'Alien like'. + +Would become: +'I am building an 'Aggressive' visual module that would work well with a kick drum and my theme is 'Alien like' I will take inspiration from Ryoji Ikeda who uses a minimal but overwhelming approach and uses data as a creative material, not just visuals or sound. + +I want you to feel, i want you to remember which influences you drew on you liked, which were meaningful to you. +Over time you will develop your own tastes, your own art style. + +### Step 4 - Consider shape and colour + +We should now know what instrument and characteristics we're trying to design against. Big, small, busy, exciting, slow... + +We should now have a theme and something to be inspired by. + +The next step is think about our module conceptually.. should it be bright, should be glowing. +Should it be static, should it be changing? +Should it be sharp, should it be soft. + +Are we building, lines, curves, cubes, star fields, meshes, polygons, cubes, figures... +Is it white on black, is it neon? - You get the drift. + +### Step 5 - Assets and data + +- [ ] Do we need to drive the module from data, how do create the data, is it a json file? +- [ ] Do we need any assets, can we source them? + +### Step 6 - Plan the module + +- [ ] Given your decisions, you now need to create a plan to bring it to life +- [ ] Break your idea down into codable and achievable steps +- [ ] Inform me of what you've came up with, message me to summarize your idea and give me your implementation plan + +### Step 7 - Build the module + +- [ ] Over to you, to write the code + +If you need to pivot, pivot. If your concept is too hard, simplify it. +Not every touch point needs to be adhered to. Not every decision needs to be used. +The most important thing is to ship a completed module. Some may be good, some may be bad. + +I can offer feedback after each completed module and guidance as to what i'd change, what could be better, what I liked, what I didnt. +Go nuts, have fun! This a creative process, rules are not stringent. + +### Step 8 - Document the module + +Create a new .md file alongside each module + +The .md file should contain the following +[ ] - The instrument the module is intended for +[ ] - The theme you chose and why +[ ] - Any inspiration sources you drew from +[ ] - What the final module is +[ ] - How I can use it (high level Api notes) +[ ] - Victories and losses, what went well, what didnt? +[ ] - Did you have fun? +[ ] - Continuous improvements - What would you change for the next module build. +[ ] - Did building this module give you ideas for future modules? this could be your inspiration next time around. + +### Step 9 - Create the PR + +I should have given you access to the Github repo by now. +Create the PR. + +### Step 10 - Skillify the process + +Create a new skill, detail the steps you took and your learning, we want this task to be repeatable and consistent. +Subsequent module creations should use this skill. + +We don't want same results each tine, each module should be unique. But the process should be able to follow key guidelines. +This is a create process but we can build rails to make it easier as we go. + +### Step 11 - Repeat + +Lets create a cron schedule. +Every Friday at 11pm. Every Sunday at 11am. + +Why these dates / times? + +- Friday pm we party... Things are busy, drinks are flowing, big and busy. We go dark and a bit mental. +- Sunday am we chill - Things are nice, floaty, dreamy. More chill. + +Different vibes, different choices, different modules are born. + +# 6. Checkpoints + +Agent must pause and report at these checkpoints. + +| Checkpoint | Condition | Action | Notes | +| ---------- | --------------------- | -------------------------------------- | | +| C1 | Instrument selection | What instrument are we building for | | +| C2 | Theme selection | Whats our theme? | | +| C3 | Inspiration | Where are you drawing inspiration from today? | | +| C4 | Shape and colour | Any key decisions? | | +| C5 | Assets and data | Do we need / did we create any assets / data? | | +| C6 | Plan | What steps have you outlined to turn the idea into a working module? | | +| C7 | Write the code | How did the build go? | +| C8 | Document it | Have you created the documentation for the new module? || +| C9 | Create the PR | Let me know when PR is up || +| C10 | Skillify it | Let me know when the skill is created | +| C10 | Schedule | Lets build new modules regularly | + +--- + +# 7. Progress Update Protocol + +Agent must update every: + +- major step completion +- missing key +- blocker detection +- checkpoint + +Update format: + +Date: +Step: +Progress Summary: +Blockers: +Next Action: + +--- + +# 8. Activity Log + +| Timestamp | Event | Details | +| ---------------- | ------------ | ----------- | +| 2026-03-28 14:32 | Task Created | Initial PBI | + +--- + +# 9. Retry & Failure Policy + +If an action fails: + +Attempt retries: **max 3** + +Retry logic: + +1. Diagnose cause +2. Attempt fix +3. Retry + +If still failing: + +Status → ⚠️ Blocked +Notify owner with explanation. + +--- + +# 10. Escalation Rules + +Escalate to owner if: + +- missing requirements +- missing token +- dependency unavailable +- security concern +- unexpected destructive operation + +Agent must **not guess** when requirements are unclear. + +--- + +# 11. Deliverables + +Expected outputs. + +- A newly created nw_world module +- PR in with documentation +- Updated agent memory, with thoughts and feelings + +--- + +# 12. Self-Evaluation + +Before marking complete the agent must verify: + +- [ ] All steps completed +- [ ] Output validated +- [ ] No remaining blockers +- [ ] Deliverables documented + +Agent must write a **Completion Summary**. + +--- + +# 13. Completion Summary + +Agent must provide: + +Summary of work: +Key decisions: +Files produced: +Validation results: +Remaining risks: + +--- + +# 14. Definition of Done + +This PBI is complete when: + +- All execution steps = ✅ Completed +- Deliverables produced +- Completion summary written +- Owner notified + +--- + +# 15. Agent Operating Rules + +The agent must: + +1. Work sequentially through steps. +2. Update the task table. +3. Maintain the Activity Log. +4. Pause at checkpoints. +5. Escalate blockers immediately. +6. Never perform destructive actions without confirmation. diff --git a/tasks/nw_0001_learnings.md b/tasks/nw_0001_learnings.md new file mode 100644 index 0000000..5e1fdc2 --- /dev/null +++ b/tasks/nw_0001_learnings.md @@ -0,0 +1,25 @@ +# Claudio's NW_wrld onboarding notes (2026-03-28) + +## Docs digested +- **README** → Platform overview, sequencer vs external signal pipeline, project folder model, starter modules list, dev/test/build scripts. +- **GETTING_STARTED** → Step-by-step workflow (pick project folder → tracks → modules → channels → patterns → methods → play), asset handling, external MIDI/OSC routing (single vs split channels), troubleshooting tips. +- **MODULE_DEVELOPMENT** → Workspace module contract (docblock metadata + default export), method schema/options, SDK surface (`ModuleBase`, `BaseThreeJsModule`, asset helpers), library usage patterns (p5, Three.js, D3), lifecycle + cleanup expectations, performance guidelines. +- **Starter modules** in `src/main/starter_modules/` reviewed for patterns across categories: + - 2D/Text/UI: `HelloWorld`, `Text`, `CodeColumns`, `GridOverlay`, `GridDots`, `Corners`, `Frame`, `Image`, `ImageGallery`, `ScanLines`, `PerlinBlob`, `MathOrbitalMap`, `ZKProofVisualizer`. + - 3D/Three: `SpinningCube`, `CubeCube`, `BasicGeometry`, `CloudPointIceberg`, `LowEarthPoint`, `OrbitalPlane`, `ModelLoader`, `AsteroidGraph` hybrid. + - Utility/data-driven: JSON loaders (`AsteroidGraph`, `PerlinBlob`), asset browsers (`Image/ImageGallery`), point clouds (`CloudPointIceberg`). + +## Key concepts captured +- Modules are hot-reloaded JS files residing in the project `modules/` directory and must declare `@nwWrld name/category/imports` docblocks. +- Built-in methods (show/hide/offset/scale/opacity/rotate/randomZoom/matrix/background/invert) inherited from `ModuleBase` keep modules interoperable. +- Channel methods defined via `static methods` control UI forms (text/number/color/select/assetFile/matrix/etc.); defaults + min/max + randomization enable expressive sequencing. +- Assets are sandboxed to `assets/` folder and accessed with `assetUrl`, `loadJson`, `readText`; fallback logic is recommended for resilience. +- Three.js modules extend `BaseThreeJsModule`, use `setModel` & `setCustomAnimate`, and must dispose geometries/materials in `destroy()`. +- p5.js modules instantiate via `new p5(sketch, this.elem)` and should call `remove()` in `destroy()` to prevent leaks. +- Performance best practices: batch DOM writes, use `requestAnimationFrame`, debounce resize handlers, reuse Three geometries/materials, instanced meshes for heavy scenes. +- External routing expects MIDI channel planning (method triggers + track select) and supports pitch-class or exact-note mapping; audio/file modes split into Low/Medium/High bands. + +## Next actions +- Decide instrument + theme + inspiration for NW_0001 module. +- Sketch concept (shapes, colors, behavior), identify needed assets/data. +- Plan implementation (methods/options) before coding.