ivy-gpu-art-studio / js /p5js-renderer.js
Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 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;