Spaces:
Running
Running
| /** | |
| * 🌿 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; | |