/** * 🌿 Ivy's Creative Studio * Tab 7: p5.js Art Renderer * * Creative coding with p5.js * 10 modes, 10 palettes, brushes, symmetry, glow effects! */ class P5JSRenderer { constructor() { this.p5Instance = null; this.container = null; this.isActive = false; // Parameters this.params = { mode: "flowfield", density: 50, speed: 1.0, palette: "sunset", brushSize: 20, trails: true, glow: false, symmetry: false }; // Audio this.audioEnabled = false; this.mic = null; this.fft = null; this.amplitude = null; } init(container) { this.container = container; } start() { this.isActive = true; this.container.classList.remove("hidden"); this.container.innerHTML = ""; // Create p5 instance this.createSketch(); } stop() { this.isActive = false; this.container.classList.add("hidden"); if (this.p5Instance) { this.p5Instance.remove(); this.p5Instance = null; } if (this.mic) { this.mic.stop(); this.mic = null; } } reset() { if (this.p5Instance) { this.p5Instance.remove(); } this.createSketch(); } setMode(mode) { this.params.mode = mode; this.reset(); // Show/hide audio button const audioBtn = document.getElementById("p5-audio-btn"); if (audioBtn) { audioBtn.style.display = mode === "audio" ? "block" : "none"; } } setDensity(density) { this.params.density = density; } setSpeed(speed) { this.params.speed = speed; } setPalette(palette) { this.params.palette = palette; } setBrushSize(size) { this.params.brushSize = size; } setTrails(enabled) { this.params.trails = enabled; } setGlow(enabled) { this.params.glow = enabled; } setSymmetry(enabled) { this.params.symmetry = enabled; } async enableAudio() { if (this.audioEnabled) return; try { // p5.sound needs user interaction if (typeof p5 !== "undefined" && p5.prototype.getAudioContext) { const audioContext = p5.prototype.getAudioContext(); if (audioContext.state !== "running") { await audioContext.resume(); } } this.audioEnabled = true; this.reset(); } catch (err) { console.error("Failed to enable audio:", err); } } getPalette() { const palettes = { ivy: ["#22c55e", "#16a34a", "#4ade80", "#86efac", "#166534"], forest: ["#2d6a4f", "#40916c", "#52b788", "#74c69d", "#95d5b2"], sunset: ["#ff6b6b", "#feca57", "#ff9ff3", "#54a0ff", "#5f27cd"], ocean: ["#0077b6", "#00b4d8", "#90e0ef", "#caf0f8", "#023e8a"], fire: ["#ff4500", "#ff6600", "#ff8800", "#ffaa00", "#ffcc00"], candy: ["#ff70a6", "#ff9770", "#ffd670", "#e9ff70", "#70d6ff"], neon: ["#ff00ff", "#00ffff", "#ff00aa", "#00ff00", "#ffff00"], pastel: ["#ffadad", "#ffd6a5", "#fdffb6", "#caffbf", "#a0c4ff"], cosmic: ["#240046", "#5a189a", "#9d4edd", "#c77dff", "#e0aaff"], noir: ["#ffffff", "#cccccc", "#888888", "#444444", "#000000"] }; return palettes[this.params.palette] || palettes.forest; } createSketch() { const self = this; const params = this.params; const sketch = p => { let particles = []; let flowField = []; let cols, rows; const scale = 20; let zoff = 0; // Tree fractal let angle = 0; let treeLen = 0; // Stars let stars = []; // Audio visualization let fft, amplitude; // Paint mode let paintStrokes = []; // Matrix rain let rainDrops = []; // Mandala let mandalaAngle = 0; p.setup = function () { const canvas = p.createCanvas(self.container.clientWidth, self.container.clientHeight); canvas.parent(self.container); p.colorMode(p.HSB, 360, 100, 100, 100); if (params.mode === "flowfield") { setupFlowField(); } else if (params.mode === "circles") { // No setup needed } else if (params.mode === "tree") { // No setup needed } else if (params.mode === "starfield") { setupStarfield(); } else if (params.mode === "audio") { setupAudio(); } else if (params.mode === "ivy") { setupIvy(); } else if (params.mode === "rain") { setupRain(); } else if (params.mode === "paint") { // No setup needed } else if (params.mode === "spiral") { // No setup needed } else if (params.mode === "mandala") { // No setup needed } // Initialize ivy vines array for ivy mode if (params.mode !== "ivy") { ivyVines = []; } }; function setupFlowField() { cols = p.floor(p.width / scale); rows = p.floor(p.height / scale); flowField = new Array(cols * rows); for (let i = 0; i < params.density * 10; i++) { particles.push(new FlowParticle()); } } function setupStarfield() { for (let i = 0; i < params.density * 5; i++) { stars.push({ x: p.random(-p.width / 2, p.width / 2), y: p.random(-p.height / 2, p.height / 2), z: p.random(p.width), pz: 0 }); } } function setupAudio() { if (self.audioEnabled && typeof p5.FFT !== "undefined") { fft = new p5.FFT(0.8, 128); amplitude = new p5.Amplitude(); // Use mic self.mic = new p5.AudioIn(); self.mic.start(); fft.setInput(self.mic); } } function setupRain() { const cols = p.floor(p.width / 20); for (let i = 0; i < cols; i++) { rainDrops.push({ x: i * 20, y: p.random(-500, 0), speed: p.random(5, 15), chars: [], len: p.floor(p.random(5, 20)) }); // Generate random characters for (let j = 0; j < rainDrops[i].len; j++) { rainDrops[i].chars.push(String.fromCharCode(0x30a0 + p.random(96))); } } } p.draw = function () { const palette = self.getPalette(); const speed = params.speed; // Background with trails if (params.trails) { p.push(); p.colorMode(p.RGB); p.fill(10, 10, 15, 25); p.noStroke(); p.rect(0, 0, p.width, p.height); p.pop(); } else { p.background(10, 10, 15); } if (params.mode === "flowfield") { drawFlowField(palette, speed); } else if (params.mode === "circles") { drawCircles(palette, speed); } else if (params.mode === "tree") { drawTree(palette, speed); } else if (params.mode === "starfield") { drawStarfield(palette, speed); } else if (params.mode === "audio") { drawAudio(palette, speed); } else if (params.mode === "ivy") { drawIvy(palette, speed); } else if (params.mode === "spiral") { drawSpiral(palette, speed); } else if (params.mode === "rain") { drawRain(palette, speed); } else if (params.mode === "paint") { drawPaint(palette, speed); } else if (params.mode === "mandala") { drawMandala(palette, speed); } // Apply glow effect if (params.glow) { applyGlow(); } }; function drawFlowField(palette, speed) { zoff += 0.003 * speed; // Update flow field let yoff = 0; for (let y = 0; y < rows; y++) { let xoff = 0; for (let x = 0; x < cols; x++) { const index = x + y * cols; const angle = p.noise(xoff, yoff, zoff) * p.TWO_PI * 2; const v = p5.Vector.fromAngle(angle); v.setMag(1); flowField[index] = v; xoff += 0.1; } yoff += 0.1; } // Update and draw particles for (const particle of particles) { particle.follow(flowField); particle.update(speed); particle.edges(); particle.show(palette); } } function drawCircles(palette, speed) { p.translate(p.width / 2, p.height / 2); const time = p.frameCount * 0.02 * speed; const count = params.density; for (let i = 0; i < count; i++) { const angle = (i / count) * p.TWO_PI + time; const radius = 50 + p.sin(time * 2 + i * 0.5) * 100; const x = p.cos(angle) * radius; const y = p.sin(angle) * radius; const size = 20 + p.sin(time * 3 + i) * 15; const colorIndex = i % palette.length; p.push(); p.colorMode(p.RGB); const c = p.color(palette[colorIndex]); c.setAlpha(150); p.fill(c); p.noStroke(); p.ellipse(x, y, size, size); p.pop(); } // Mouse interaction if (p.mouseIsPressed) { for (let i = 0; i < 5; i++) { const angle = p.random(p.TWO_PI); const radius = p.random(50, 150); const x = p.mouseX - p.width / 2 + p.cos(angle) * radius; const y = p.mouseY - p.height / 2 + p.sin(angle) * radius; p.push(); p.colorMode(p.RGB); const c = p.color(palette[p.floor(p.random(palette.length))]); c.setAlpha(100); p.fill(c); p.noStroke(); p.ellipse(x, y, p.random(10, 40)); p.pop(); } } } function drawTree(palette, speed) { p.background(10, 10, 15); angle = p.map(p.sin(p.frameCount * 0.02 * speed), -1, 1, p.PI / 8, p.PI / 3); treeLen = p.map(params.density, 10, 100, 80, 180); p.translate(p.width / 2, p.height); p.push(); p.colorMode(p.RGB); p.stroke(palette[0]); p.strokeWeight(2); branch(treeLen, 0, palette); p.pop(); } function branch(len, depth, palette) { const colorIndex = depth % palette.length; p.push(); p.colorMode(p.RGB); const c = p.color(palette[colorIndex]); p.stroke(c); p.strokeWeight(p.map(len, 2, 150, 1, 8)); p.pop(); p.line(0, 0, 0, -len); p.translate(0, -len); if (len > 4) { p.push(); p.rotate(angle); branch(len * 0.67, depth + 1, palette); p.pop(); p.push(); p.rotate(-angle); branch(len * 0.67, depth + 1, palette); p.pop(); // Extra branch if (len > 20 && depth < 5) { p.push(); p.rotate(angle * 0.5); branch(len * 0.5, depth + 1, palette); p.pop(); } } } function drawStarfield(palette, speed) { p.background(10, 10, 15); p.translate(p.width / 2, p.height / 2); for (const star of stars) { star.z -= 5 * speed; if (star.z < 1) { star.z = p.width; star.x = p.random(-p.width / 2, p.width / 2); star.y = p.random(-p.height / 2, p.height / 2); star.pz = star.z; } const sx = p.map(star.x / star.z, 0, 1, 0, p.width / 2); const sy = p.map(star.y / star.z, 0, 1, 0, p.height / 2); const r = p.map(star.z, 0, p.width, 8, 0); const colorIndex = p.floor(p.map(star.z, 0, p.width, 0, palette.length)); p.push(); p.colorMode(p.RGB); const c = p.color(palette[colorIndex % palette.length]); p.fill(c); p.noStroke(); p.ellipse(sx, sy, r); // Trail const px = p.map(star.x / star.pz, 0, 1, 0, p.width / 2); const py = p.map(star.y / star.pz, 0, 1, 0, p.height / 2); p.stroke(c); p.strokeWeight(r * 0.5); p.line(px, py, sx, sy); p.pop(); star.pz = star.z; } } function drawAudio(palette, speed) { if (!fft || !self.audioEnabled) { // Show message p.push(); p.colorMode(p.RGB); p.fill(255); p.textAlign(p.CENTER, p.CENTER); p.textSize(20); p.text('🎤 Click "Enable Audio" to start', p.width / 2, p.height / 2); p.pop(); return; } const spectrum = fft.analyze(); const waveform = fft.waveform(); const level = amplitude ? amplitude.getLevel() : 0; p.translate(p.width / 2, p.height / 2); // Circular visualization const numBars = params.density; for (let i = 0; i < numBars; i++) { const angle = p.map(i, 0, numBars, 0, p.TWO_PI); const specIndex = p.floor(p.map(i, 0, numBars, 0, spectrum.length * 0.5)); const amp = spectrum[specIndex] / 255; const r1 = 50 + level * 100; const r2 = r1 + amp * 150 * speed; const x1 = p.cos(angle) * r1; const y1 = p.sin(angle) * r1; const x2 = p.cos(angle) * r2; const y2 = p.sin(angle) * r2; const colorIndex = p.floor(p.map(amp, 0, 1, 0, palette.length)); p.push(); p.colorMode(p.RGB); const c = p.color(palette[colorIndex % palette.length]); p.stroke(c); p.strokeWeight(3); p.line(x1, y1, x2, y2); p.pop(); } // Center circle const centerSize = 30 + level * 50; p.push(); p.colorMode(p.RGB); p.fill(palette[0]); p.noStroke(); p.ellipse(0, 0, centerSize); p.pop(); } // 🌿 IVY MODE - Growing vine animation! let ivyVines = []; function setupIvy() { ivyVines = []; const numVines = 5; for (let i = 0; i < numVines; i++) { ivyVines.push({ x: p.random(p.width * 0.1, p.width * 0.9), y: p.height, points: [], targetY: p.random(p.height * 0.1, p.height * 0.4), growthSpeed: p.random(0.5, 1.5), waveOffset: p.random(1000), thickness: p.random(3, 6) }); } } function drawIvy(palette, speed) { p.background(10, 20, 15); const time = p.frameCount * 0.02 * speed; // Grow vines for (let vine of ivyVines) { // Add new point if not fully grown if (vine.points.length === 0 || vine.points[vine.points.length - 1].y > vine.targetY) { const lastY = vine.points.length > 0 ? vine.points[vine.points.length - 1].y : vine.y; const lastX = vine.points.length > 0 ? vine.points[vine.points.length - 1].x : vine.x; // Slight horizontal wave const waveX = p.sin(lastY * 0.02 + vine.waveOffset) * 30; vine.points.push({ x: lastX + waveX * 0.1 + p.random(-2, 2), y: lastY - vine.growthSpeed * speed, hasLeaf: p.random() > 0.7, leafSide: p.random() > 0.5 ? 1 : -1, leafSize: p.random(15, 30), leafAngle: p.random(-0.5, 0.5) }); } } // Draw vines for (let vine of ivyVines) { if (vine.points.length < 2) continue; // Draw vine stem p.push(); p.colorMode(p.RGB); p.stroke(34, 100, 50); p.strokeWeight(vine.thickness); p.noFill(); p.beginShape(); p.curveVertex(vine.x, vine.y); for (let pt of vine.points) { p.curveVertex(pt.x, pt.y); } if (vine.points.length > 0) { const last = vine.points[vine.points.length - 1]; p.curveVertex(last.x, last.y); } p.endShape(); p.pop(); // Draw leaves for (let pt of vine.points) { if (pt.hasLeaf) { const leafWave = p.sin(time * 2 + pt.y * 0.1) * 0.1; p.push(); p.translate(pt.x, pt.y); p.rotate(pt.leafAngle + leafWave + pt.leafSide * 0.5); // Leaf shape (heart-like) p.colorMode(p.RGB); const greenVar = p.map(pt.y, p.height, 0, 0.5, 1); p.fill(34 + p.random(-10, 10), 150 + p.random(-20, 20) * greenVar, 60 + p.random(-10, 10)); p.noStroke(); p.beginShape(); const ls = pt.leafSize * pt.leafSide; p.vertex(0, 0); p.bezierVertex(ls * 0.5, -ls * 0.3, ls * 0.8, -ls * 0.8, 0, -ls * 1.2); p.bezierVertex(-ls * 0.8, -ls * 0.8, -ls * 0.5, -ls * 0.3, 0, 0); p.endShape(); // Leaf vein p.stroke(34, 100, 40); p.strokeWeight(1); p.line(0, 0, 0, -ls * 0.9); p.pop(); } } } // Floating sparkles for (let i = 0; i < 20; i++) { const sparkleX = (p.noise(i * 100 + time * 0.5) - 0.5) * p.width * 1.5 + p.width * 0.25; const sparkleY = (p.noise(i * 100 + 500 + time * 0.3) - 0.5) * p.height * 1.5 + p.height * 0.25; const sparkleSize = p.noise(i * 100 + time) * 5; p.push(); p.colorMode(p.RGB); p.noStroke(); p.fill(255, 255, 200, 150); p.ellipse(sparkleX, sparkleY, sparkleSize); p.pop(); } } // === NEW MODES === function drawSpiral(palette, speed) { p.translate(p.width / 2, p.height / 2); const time = p.frameCount * 0.01 * speed; const arms = 6; const pointsPerArm = params.density * 2; for (let arm = 0; arm < arms; arm++) { const armOffset = (arm / arms) * p.TWO_PI; for (let i = 0; i < pointsPerArm; i++) { const t = i / pointsPerArm; const angle = armOffset + t * p.TWO_PI * 3 + time; const radius = t * p.min(p.width, p.height) * 0.4; const x = p.cos(angle) * radius; const y = p.sin(angle) * radius; const size = (1 - t) * 15 + 3; p.push(); p.colorMode(p.RGB); const c = p.color(palette[(arm + i) % palette.length]); c.setAlpha(200 - t * 150); p.fill(c); p.noStroke(); p.ellipse(x, y, size); p.pop(); } } } function drawRain(palette, speed) { p.background(0, 10, 15); p.textSize(18); p.textFont("monospace"); for (let drop of rainDrops) { drop.y += drop.speed * speed; if (drop.y > p.height + drop.len * 20) { drop.y = p.random(-200, 0); drop.speed = p.random(5, 15); } for (let i = 0; i < drop.len; i++) { const yPos = drop.y - i * 20; if (yPos > 0 && yPos < p.height) { const alpha = p.map(i, 0, drop.len, 255, 0); p.push(); p.colorMode(p.RGB); const c = p.color(palette[0]); c.setAlpha(alpha); p.fill(c); p.noStroke(); // Randomly change characters if (p.random() > 0.95) { drop.chars[i] = String.fromCharCode(0x30a0 + p.random(96)); } p.text(drop.chars[i], drop.x, yPos); p.pop(); } } // Bright head if (drop.y > 0 && drop.y < p.height) { p.push(); p.colorMode(p.RGB); p.fill(255); p.text(drop.chars[0], drop.x, drop.y); p.pop(); } } } function drawPaint(palette, speed) { // Draw existing strokes for (let stroke of paintStrokes) { if (stroke.points.length < 2) continue; p.push(); p.colorMode(p.RGB); p.stroke(stroke.color); p.strokeWeight(stroke.size); p.noFill(); p.beginShape(); for (let pt of stroke.points) { p.curveVertex(pt.x, pt.y); } p.endShape(); p.pop(); } // Add points while mouse is pressed if (p.mouseIsPressed && p.mouseX > 0 && p.mouseX < p.width) { if (paintStrokes.length === 0 || !paintStrokes[paintStrokes.length - 1].active) { paintStrokes.push({ points: [], color: palette[p.floor(p.random(palette.length))], size: params.brushSize, active: true }); } const currentStroke = paintStrokes[paintStrokes.length - 1]; currentStroke.points.push({ x: p.mouseX, y: p.mouseY }); // Symmetry if (params.symmetry) { const mirrorX = p.width - p.mouseX; // Find or create mirror stroke if (currentStroke.mirror === undefined) { paintStrokes.push({ points: [], color: currentStroke.color, size: currentStroke.size, active: true, isMirror: true }); currentStroke.mirror = paintStrokes.length - 1; } if (paintStrokes[currentStroke.mirror]) { paintStrokes[currentStroke.mirror].points.push({ x: mirrorX, y: p.mouseY }); } } } } function drawMandala(palette, speed) { p.translate(p.width / 2, p.height / 2); mandalaAngle += 0.002 * speed; const segments = 12; const layers = p.floor(params.density / 10); for (let layer = 0; layer < layers; layer++) { const radius = 50 + layer * 30; for (let i = 0; i < segments; i++) { const angle = (i / segments) * p.TWO_PI + mandalaAngle * (layer % 2 === 0 ? 1 : -1); p.push(); p.rotate(angle); p.colorMode(p.RGB); const c = p.color(palette[(layer + i) % palette.length]); c.setAlpha(180); p.fill(c); p.noStroke(); // Draw petal shape const petalSize = 20 + layer * 5; p.ellipse(radius, 0, petalSize, petalSize * 2); // Inner detail c.setAlpha(100); p.fill(c); p.ellipse(radius - 10, 0, petalSize * 0.5, petalSize); p.pop(); } } // Center p.push(); p.colorMode(p.RGB); p.fill(palette[0]); p.noStroke(); p.ellipse(0, 0, 40); p.pop(); } function applyGlow() { // Simple glow simulation via overlay p.push(); p.blendMode(p.ADD); p.filter(p.BLUR, 2); p.blendMode(p.BLEND); p.pop(); } p.mouseReleased = function () { // End active paint strokes for (let stroke of paintStrokes) { stroke.active = false; } }; // Flow particle class class FlowParticle { constructor() { this.pos = p.createVector(p.random(p.width), p.random(p.height)); this.vel = p.createVector(0, 0); this.acc = p.createVector(0, 0); this.maxSpeed = 4; this.prevPos = this.pos.copy(); } follow(flowField) { const x = p.floor(this.pos.x / scale); const y = p.floor(this.pos.y / scale); const index = p.constrain(x + y * cols, 0, flowField.length - 1); const force = flowField[index]; if (force) { this.applyForce(force); } } applyForce(force) { this.acc.add(force); } update(speed) { this.vel.add(this.acc); this.vel.limit(this.maxSpeed * speed); this.pos.add(this.vel); this.acc.mult(0); } edges() { if (this.pos.x > p.width) { this.pos.x = 0; this.prevPos.x = 0; } if (this.pos.x < 0) { this.pos.x = p.width; this.prevPos.x = p.width; } if (this.pos.y > p.height) { this.pos.y = 0; this.prevPos.y = 0; } if (this.pos.y < 0) { this.pos.y = p.height; this.prevPos.y = p.height; } } show(palette) { const colorIndex = p.floor(p.map(this.pos.x, 0, p.width, 0, palette.length)); p.push(); p.colorMode(p.RGB); const c = p.color(palette[colorIndex % palette.length]); c.setAlpha(50); p.stroke(c); p.strokeWeight(1); p.line(this.pos.x, this.pos.y, this.prevPos.x, this.prevPos.y); p.pop(); this.prevPos = this.pos.copy(); } } p.windowResized = function () { p.resizeCanvas(self.container.clientWidth, self.container.clientHeight); }; }; this.p5Instance = new p5(sketch); } dispose() { this.stop(); } } // Export window.P5JSRenderer = P5JSRenderer;