Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 Ivy's Creative Studio
* Main Application Controller
*
* Handles tab switching, UI controls, and renderer management
* WebGPU + Three.js + p5.js
*/
(async function () {
"use strict";
// DOM Elements
const canvas = document.getElementById("gpuCanvas");
const threeCanvas = document.getElementById("threeCanvas");
const p5Container = document.getElementById("p5Container");
const p5AudioContainer = document.getElementById("p5AudioContainer");
const errorMessage = document.getElementById("webgpu-error");
const loadingMessage = document.getElementById("loading");
const tabs = document.querySelectorAll(".tab");
const controlSections = document.querySelectorAll(".controls-section");
// Renderers
let device, context, format;
let fractalsRenderer, fluidRenderer, particlesRenderer, patternsRenderer, audioRenderer;
let threejsRenderer, p5jsRenderer, p5audioRenderer;
let activeRenderer = null;
let activeTab = "particles";
let webgpuSupported = true;
// Show loading
loadingMessage.classList.remove("hidden");
// Initialize WebGPU
try {
const result = await WebGPUUtils.initWebGPU(canvas);
device = result.device;
context = result.context;
format = result.format;
console.log("🌿 WebGPU initialized successfully!");
// Initialize all renderers
fractalsRenderer = new FractalsRenderer();
fluidRenderer = new FluidRenderer();
particlesRenderer = new ParticlesRenderer();
patternsRenderer = new PatternsRenderer();
audioRenderer = new AudioRenderer();
await fractalsRenderer.init(device, context, format, canvas);
await fluidRenderer.init(device, context, format, canvas);
await particlesRenderer.init(device, context, format, canvas);
await patternsRenderer.init(device, context, format, canvas);
await audioRenderer.init(device, context, format, canvas);
// Hide loading, start default renderer
loadingMessage.classList.add("hidden");
} catch (error) {
console.error("WebGPU initialization failed:", error);
loadingMessage.classList.add("hidden");
webgpuSupported = false;
// Don't show error - Three.js and p5.js still work!
console.log("🟡 WebGPU not available, but Three.js and p5.js are ready!");
}
// Initialize Three.js renderer (always works)
threejsRenderer = new ThreeJSRenderer();
threejsRenderer.init(threeCanvas);
// Initialize p5.js renderer (always works)
p5jsRenderer = new P5JSRenderer();
p5jsRenderer.init(p5Container);
// Initialize p5.js Audio renderer (always works)
p5audioRenderer = new P5AudioRenderer();
p5audioRenderer.init(p5AudioContainer);
// Start default tab (Particles = first tab!)
switchTab(webgpuSupported ? "particles" : "threejs");
// Tab switching
function switchTab(tabName) {
// Check if WebGPU tab selected but not supported
const webgpuTabs = ["fractals", "fluid", "particles", "patterns", "audio"];
if (webgpuTabs.includes(tabName) && !webgpuSupported) {
console.warn("WebGPU not supported, switching to Three.js");
tabName = "threejs";
}
// Stop active renderer
if (activeRenderer) {
activeRenderer.stop();
}
// Hide all canvases first
canvas.classList.add("hidden");
threeCanvas.classList.add("hidden");
p5Container.classList.add("hidden");
p5AudioContainer.classList.add("hidden");
// Update tab UI
tabs.forEach(tab => {
tab.classList.toggle("active", tab.dataset.tab === tabName);
});
// Update controls UI
controlSections.forEach(section => {
const sectionId = section.id.replace("controls-", "");
section.classList.toggle("active", sectionId === tabName);
section.classList.toggle("hidden", sectionId !== tabName);
});
// Start new renderer
activeTab = tabName;
switch (tabName) {
case "fractals":
canvas.classList.remove("hidden");
activeRenderer = fractalsRenderer;
break;
case "fluid":
canvas.classList.remove("hidden");
activeRenderer = fluidRenderer;
break;
case "particles":
canvas.classList.remove("hidden");
activeRenderer = particlesRenderer;
break;
case "patterns":
canvas.classList.remove("hidden");
activeRenderer = patternsRenderer;
break;
case "audio":
canvas.classList.remove("hidden");
activeRenderer = audioRenderer;
break;
case "threejs":
threeCanvas.classList.remove("hidden");
activeRenderer = threejsRenderer;
break;
case "p5js":
p5Container.classList.remove("hidden");
activeRenderer = p5jsRenderer;
break;
case "p5audio":
p5AudioContainer.classList.remove("hidden");
activeRenderer = p5audioRenderer;
break;
}
if (activeRenderer) {
activeRenderer.start();
}
}
// Tab click handlers
tabs.forEach(tab => {
tab.addEventListener("click", () => {
switchTab(tab.dataset.tab);
});
});
// ============================================
// Fractals Controls
// ============================================
const fractalType = document.getElementById("fractal-type");
const fractalIterations = document.getElementById("fractal-iterations");
const iterationsValue = document.getElementById("iterations-value");
const fractalPalette = document.getElementById("fractal-palette");
const fractalPower = document.getElementById("fractal-power");
const powerValue = document.getElementById("power-value");
const fractalColorshift = document.getElementById("fractal-colorshift");
const colorshiftValue = document.getElementById("colorshift-value");
const juliaReal = document.getElementById("julia-real");
const juliaRealValue = document.getElementById("julia-real-value");
const juliaImag = document.getElementById("julia-imag");
const juliaImagValue = document.getElementById("julia-imag-value");
const fractalAnimate = document.getElementById("fractal-animate");
const fractalSmooth = document.getElementById("fractal-smooth");
const resetFractals = document.getElementById("reset-fractals");
if (fractalType) {
fractalType.addEventListener("change", () => {
fractalsRenderer.setType(fractalType.value);
// Show/hide Julia parameters based on type
const juliaParams = document.querySelectorAll(".julia-param");
const isJulia = fractalType.value === "julia" || fractalType.value === "phoenix";
juliaParams.forEach(el => {
el.style.display = isJulia ? "block" : "none";
});
// Reset view for Ivy fractal (it looks best centered at origin)
if (fractalType.value === "ivy" || fractalType.value === "newton") {
fractalsRenderer.params.centerX = 0;
fractalsRenderer.params.centerY = 0;
fractalsRenderer.params.zoom = 1.0;
}
});
}
if (fractalIterations) {
fractalIterations.addEventListener("input", () => {
const value = parseInt(fractalIterations.value);
if (iterationsValue) iterationsValue.textContent = value;
fractalsRenderer.setIterations(value);
});
}
if (fractalPalette) {
fractalPalette.addEventListener("change", () => {
fractalsRenderer.setPalette(fractalPalette.value);
});
}
if (fractalPower) {
fractalPower.addEventListener("input", () => {
const value = parseFloat(fractalPower.value);
powerValue.textContent = value.toFixed(1);
fractalsRenderer.setPower(value);
});
}
if (fractalColorshift) {
fractalColorshift.addEventListener("input", () => {
const value = parseFloat(fractalColorshift.value);
colorshiftValue.textContent = value.toFixed(2);
fractalsRenderer.setColorShift(value);
});
}
if (juliaReal) {
juliaReal.addEventListener("input", () => {
const value = parseFloat(juliaReal.value);
if (juliaRealValue) juliaRealValue.textContent = value.toFixed(2);
fractalsRenderer.setJuliaParams(value, parseFloat(juliaImag?.value || 0));
});
}
if (juliaImag) {
juliaImag.addEventListener("input", () => {
const value = parseFloat(juliaImag.value);
if (juliaImagValue) juliaImagValue.textContent = value.toFixed(2);
fractalsRenderer.setJuliaParams(parseFloat(juliaReal?.value || 0), value);
});
}
if (fractalAnimate) {
fractalAnimate.addEventListener("change", () => {
fractalsRenderer.setAnimate(fractalAnimate.checked);
});
}
if (fractalSmooth) {
fractalSmooth.addEventListener("change", () => {
fractalsRenderer.setSmoothColoring(fractalSmooth.checked);
});
}
if (resetFractals) {
resetFractals.addEventListener("click", () => {
fractalsRenderer.reset();
});
}
// ============================================
// Fluid Controls
// ============================================
const fluidStyle = document.getElementById("fluid-style");
const fluidPalette = document.getElementById("fluid-palette");
const fluidViscosity = document.getElementById("fluid-viscosity");
const viscosityValue = document.getElementById("viscosity-value");
const fluidDiffusion = document.getElementById("fluid-diffusion");
const diffusionValue = document.getElementById("diffusion-value");
const fluidForce = document.getElementById("fluid-force");
const forceValue = document.getElementById("force-value");
const fluidCurl = document.getElementById("fluid-curl");
const curlValue = document.getElementById("curl-value");
const fluidPressure = document.getElementById("fluid-pressure");
const pressureValue = document.getElementById("pressure-value");
const fluidBloom = document.getElementById("fluid-bloom");
const fluidVortex = document.getElementById("fluid-vortex");
const resetFluid = document.getElementById("reset-fluid");
if (fluidStyle) {
fluidStyle.addEventListener("change", () => {
fluidRenderer.setStyle(fluidStyle.value);
});
}
if (fluidPalette) {
fluidPalette.addEventListener("change", () => {
fluidRenderer.setPalette(fluidPalette.value);
});
}
if (fluidViscosity) {
fluidViscosity.addEventListener("input", () => {
const value = parseFloat(fluidViscosity.value);
if (viscosityValue) viscosityValue.textContent = value.toFixed(2);
fluidRenderer.setViscosity(value);
});
}
if (fluidDiffusion) {
fluidDiffusion.addEventListener("input", () => {
const value = parseFloat(fluidDiffusion.value);
if (diffusionValue) diffusionValue.textContent = value.toFixed(5);
fluidRenderer.setDiffusion(value);
});
}
if (fluidForce) {
fluidForce.addEventListener("input", () => {
const value = parseInt(fluidForce.value);
if (forceValue) forceValue.textContent = value;
fluidRenderer.setForce(value);
});
}
if (fluidCurl) {
fluidCurl.addEventListener("input", () => {
const value = parseInt(fluidCurl.value);
curlValue.textContent = value;
fluidRenderer.setCurl(value);
});
}
if (fluidPressure) {
fluidPressure.addEventListener("input", () => {
const value = parseFloat(fluidPressure.value);
pressureValue.textContent = value.toFixed(2);
fluidRenderer.setPressure(value);
});
}
if (fluidBloom) {
fluidBloom.addEventListener("change", () => {
fluidRenderer.setBloom(fluidBloom.checked);
});
}
if (fluidVortex) {
fluidVortex.addEventListener("change", () => {
fluidRenderer.setVortex(fluidVortex.checked);
});
}
if (resetFluid) {
resetFluid.addEventListener("click", () => {
fluidRenderer.reset();
});
}
// ============================================
// Particles Controls
// ============================================
const particleCount = document.getElementById("particle-count");
const particleCountValue = document.getElementById("particle-count-value");
const particleMode = document.getElementById("particle-mode");
const particlePalette = document.getElementById("particle-palette");
const particleSize = document.getElementById("particle-size");
const particleSizeValue = document.getElementById("particle-size-value");
const particleSpeed = document.getElementById("particle-speed");
const particleSpeedValue = document.getElementById("particle-speed-value");
const particleTrail = document.getElementById("particle-trail");
const particleTrailValue = document.getElementById("particle-trail-value");
const resetParticles = document.getElementById("reset-particles");
particleCount.addEventListener("input", () => {
const value = parseInt(particleCount.value);
particleCountValue.textContent = value;
particlesRenderer.setCount(value);
});
particleMode.addEventListener("change", () => {
particlesRenderer.setMode(particleMode.value);
});
if (particlePalette) {
particlePalette.addEventListener("change", () => {
particlesRenderer.setPalette(particlePalette.value);
});
}
particleSize.addEventListener("input", () => {
const value = parseFloat(particleSize.value);
particleSizeValue.textContent = value;
particlesRenderer.setSize(value);
});
particleSpeed.addEventListener("input", () => {
const value = parseFloat(particleSpeed.value);
particleSpeedValue.textContent = value;
particlesRenderer.setSpeed(value);
});
if (particleTrail) {
particleTrail.addEventListener("input", () => {
const value = parseFloat(particleTrail.value);
particleTrailValue.textContent = value;
particlesRenderer.setTrail(value);
});
}
resetParticles.addEventListener("click", () => {
particlesRenderer.reset();
});
// ============================================
// Patterns Controls
// ============================================
const patternType = document.getElementById("pattern-type");
const patternPalette = document.getElementById("pattern-palette");
const patternScale = document.getElementById("pattern-scale");
const patternScaleValue = document.getElementById("pattern-scale-value");
const patternSpeed = document.getElementById("pattern-speed");
const patternSpeedValue = document.getElementById("pattern-speed-value");
const patternComplexity = document.getElementById("pattern-complexity");
const patternComplexityValue = document.getElementById("pattern-complexity-value");
const patternIntensity = document.getElementById("pattern-intensity");
const patternIntensityValue = document.getElementById("pattern-intensity-value");
const patternAnimate = document.getElementById("pattern-animate");
const patternMouseReact = document.getElementById("pattern-mouse-react");
const resetPatterns = document.getElementById("reset-patterns");
patternType.addEventListener("change", () => {
patternsRenderer.setType(patternType.value);
});
if (patternPalette) {
patternPalette.addEventListener("change", () => {
patternsRenderer.setPalette(patternPalette.value);
});
}
patternScale.addEventListener("input", () => {
const value = parseFloat(patternScale.value);
patternScaleValue.textContent = value;
patternsRenderer.setScale(value);
});
patternSpeed.addEventListener("input", () => {
const value = parseFloat(patternSpeed.value);
patternSpeedValue.textContent = value;
patternsRenderer.setSpeed(value);
});
patternComplexity.addEventListener("input", () => {
const value = parseInt(patternComplexity.value);
patternComplexityValue.textContent = value;
patternsRenderer.setComplexity(value);
});
if (patternIntensity) {
patternIntensity.addEventListener("input", () => {
const value = parseFloat(patternIntensity.value);
patternIntensityValue.textContent = value;
patternsRenderer.setIntensity(value);
});
}
if (patternAnimate) {
patternAnimate.addEventListener("change", () => {
patternsRenderer.setAnimate(patternAnimate.checked);
});
}
if (patternMouseReact) {
patternMouseReact.addEventListener("change", () => {
patternsRenderer.setMouseReact(patternMouseReact.checked);
});
}
if (resetPatterns) {
resetPatterns.addEventListener("click", () => {
patternsRenderer.reset();
});
}
// ============================================
// Audio Controls
// ============================================
const audioSource = document.getElementById("audio-source");
const audioFile = document.getElementById("audio-file");
const audioStyle = document.getElementById("audio-style");
const audioPalette = document.getElementById("audio-palette");
const audioSensitivity = document.getElementById("audio-sensitivity");
const audioSensitivityValue = document.getElementById("audio-sensitivity-value");
const audioSmoothing = document.getElementById("audio-smoothing");
const audioSmoothingValue = document.getElementById("audio-smoothing-value");
const audioBassBoost = document.getElementById("audio-bass-boost");
const audioBassBoostValue = document.getElementById("audio-bass-boost-value");
const audioGlow = document.getElementById("audio-glow");
const audioMirror = document.getElementById("audio-mirror");
const startAudioBtn = document.getElementById("start-audio");
const audioHint = document.getElementById("audio-hint");
audioSource.addEventListener("change", () => {
audioRenderer.setSource(audioSource.value);
if (audioSource.value === "file") {
audioFile.click();
}
});
audioFile.addEventListener("change", async () => {
if (audioFile.files.length > 0) {
const success = await audioRenderer.loadAudioFile(audioFile.files[0]);
if (success) {
startAudioBtn.textContent = "⏹️ Stop";
audioHint.textContent = "🎵 Playing...";
}
}
});
audioStyle.addEventListener("change", () => {
audioRenderer.setStyle(audioStyle.value);
});
if (audioPalette) {
audioPalette.addEventListener("change", () => {
audioRenderer.setPalette(audioPalette.value);
});
}
audioSensitivity.addEventListener("input", () => {
const value = parseFloat(audioSensitivity.value);
audioSensitivityValue.textContent = value;
audioRenderer.setSensitivity(value);
});
audioSmoothing.addEventListener("input", () => {
const value = parseFloat(audioSmoothing.value);
audioSmoothingValue.textContent = value;
audioRenderer.setSmoothing(value);
});
if (audioBassBoost) {
audioBassBoost.addEventListener("input", () => {
const value = parseFloat(audioBassBoost.value);
audioBassBoostValue.textContent = value;
audioRenderer.setBassBoost(value);
});
}
if (audioGlow) {
audioGlow.addEventListener("change", () => {
audioRenderer.setGlow(audioGlow.checked);
});
}
if (audioMirror) {
audioMirror.addEventListener("change", () => {
audioRenderer.setMirror(audioMirror.checked);
});
}
startAudioBtn.addEventListener("click", async () => {
if (audioRenderer.isAudioStarted) {
audioRenderer.stopAudio();
startAudioBtn.textContent = "▶️ Start";
audioHint.textContent = "🎧 Allow microphone access to begin";
} else {
const success = await audioRenderer.startAudio();
if (success) {
startAudioBtn.textContent = "⏹️ Stop";
audioHint.textContent = "🎤 Mic active! I'm singing! 🌿";
} else {
audioHint.textContent = "❌ Error: Microphone access denied";
}
}
});
// ============================================
// Three.js Controls
// ============================================
const threeScene = document.getElementById("three-scene");
const threeMaterial = document.getElementById("three-material");
const threePalette = document.getElementById("three-palette");
const threeObjects = document.getElementById("three-objects");
const threeObjectsValue = document.getElementById("three-objects-value");
const threeSpeed = document.getElementById("three-speed");
const threeSpeedValue = document.getElementById("three-speed-value");
const threeScale = document.getElementById("three-scale");
const threeScaleValue = document.getElementById("three-scale-value");
const threeWireframe = document.getElementById("three-wireframe");
const threeAutorotate = document.getElementById("three-autorotate");
const threeShadows = document.getElementById("three-shadows");
const threeBloom = document.getElementById("three-bloom");
const resetThreejs = document.getElementById("reset-threejs");
if (threeScene) {
threeScene.addEventListener("change", () => {
threejsRenderer.setSceneType(threeScene.value);
});
}
if (threeMaterial) {
threeMaterial.addEventListener("change", () => {
threejsRenderer.setMaterialType(threeMaterial.value);
});
}
if (threePalette) {
threePalette.addEventListener("change", () => {
threejsRenderer.setPalette(threePalette.value);
});
}
if (threeObjects) {
threeObjects.addEventListener("input", () => {
const value = parseInt(threeObjects.value);
threeObjectsValue.textContent = value;
threejsRenderer.setObjectCount(value);
});
}
if (threeSpeed) {
threeSpeed.addEventListener("input", () => {
const value = parseFloat(threeSpeed.value);
threeSpeedValue.textContent = value;
threejsRenderer.setSpeed(value);
});
}
if (threeScale) {
threeScale.addEventListener("input", () => {
const value = parseFloat(threeScale.value);
threeScaleValue.textContent = value;
threejsRenderer.setScale(value);
});
}
if (threeWireframe) {
threeWireframe.addEventListener("change", () => {
threejsRenderer.setWireframe(threeWireframe.checked);
});
}
if (threeAutorotate) {
threeAutorotate.addEventListener("change", () => {
threejsRenderer.setAutoRotate(threeAutorotate.checked);
});
}
if (threeShadows) {
threeShadows.addEventListener("change", () => {
threejsRenderer.setShadows(threeShadows.checked);
});
}
if (threeBloom) {
threeBloom.addEventListener("change", () => {
threejsRenderer.setBloom(threeBloom.checked);
});
}
if (resetThreejs) {
resetThreejs.addEventListener("click", () => {
threejsRenderer.reset();
});
}
// ============================================
// p5.js Controls
// ============================================
const p5Mode = document.getElementById("p5-mode");
const p5Density = document.getElementById("p5-density");
const p5DensityValue = document.getElementById("p5-density-value");
const p5Speed = document.getElementById("p5-speed");
const p5SpeedValue = document.getElementById("p5-speed-value");
const p5Palette = document.getElementById("p5-palette");
const p5Brush = document.getElementById("p5-brush");
const p5BrushValue = document.getElementById("p5-brush-value");
const p5Trails = document.getElementById("p5-trails");
const p5Glow = document.getElementById("p5-glow");
const p5Symmetry = document.getElementById("p5-symmetry");
const p5AudioBtn = document.getElementById("p5-audio-btn");
const resetP5js = document.getElementById("reset-p5js");
if (p5Mode) {
p5Mode.addEventListener("change", () => {
p5jsRenderer.setMode(p5Mode.value);
});
}
if (p5Density) {
p5Density.addEventListener("input", () => {
const value = parseInt(p5Density.value);
p5DensityValue.textContent = value;
p5jsRenderer.setDensity(value);
});
}
if (p5Speed) {
p5Speed.addEventListener("input", () => {
const value = parseFloat(p5Speed.value);
p5SpeedValue.textContent = value;
p5jsRenderer.setSpeed(value);
});
}
if (p5Palette) {
p5Palette.addEventListener("change", () => {
p5jsRenderer.setPalette(p5Palette.value);
});
}
if (p5Brush) {
p5Brush.addEventListener("input", () => {
const value = parseInt(p5Brush.value);
p5BrushValue.textContent = value;
p5jsRenderer.setBrushSize(value);
});
}
if (p5Trails) {
p5Trails.addEventListener("change", () => {
p5jsRenderer.setTrails(p5Trails.checked);
});
}
if (p5Glow) {
p5Glow.addEventListener("change", () => {
p5jsRenderer.setGlow(p5Glow.checked);
});
}
if (p5Symmetry) {
p5Symmetry.addEventListener("change", () => {
p5jsRenderer.setSymmetry(p5Symmetry.checked);
});
}
if (p5AudioBtn) {
p5AudioBtn.addEventListener("click", async () => {
await p5jsRenderer.enableAudio();
p5AudioBtn.textContent = "🎤 Audio Enabled!";
p5AudioBtn.disabled = true;
});
}
if (resetP5js) {
resetP5js.addEventListener("click", () => {
p5jsRenderer.reset();
});
}
// ============================================
// p5.js Audio Controls
// ============================================
const p5audioStyle = document.getElementById("p5audio-style");
const p5audioSensitivity = document.getElementById("p5audio-sensitivity");
const p5audioSensitivityValue = document.getElementById("p5audio-sensitivity-value");
const p5audioSmoothing = document.getElementById("p5audio-smoothing");
const p5audioSmoothingValue = document.getElementById("p5audio-smoothing-value");
const p5audioPalette = document.getElementById("p5audio-palette");
const p5audioBass = document.getElementById("p5audio-bass");
const p5audioBassValue = document.getElementById("p5audio-bass-value");
const p5audioMirror = document.getElementById("p5audio-mirror");
const p5audioGlow = document.getElementById("p5audio-glow");
const p5audioParticles = document.getElementById("p5audio-particles");
const startP5audio = document.getElementById("start-p5audio");
const p5audioHint = document.getElementById("p5audio-hint");
if (p5audioStyle) {
p5audioStyle.addEventListener("change", () => {
p5audioRenderer.setStyle(p5audioStyle.value);
p5audioRenderer.reset();
});
}
if (p5audioSensitivity) {
p5audioSensitivity.addEventListener("input", () => {
const value = parseFloat(p5audioSensitivity.value);
p5audioSensitivityValue.textContent = value;
p5audioRenderer.setSensitivity(value);
});
}
if (p5audioSmoothing) {
p5audioSmoothing.addEventListener("input", () => {
const value = parseFloat(p5audioSmoothing.value);
p5audioSmoothingValue.textContent = value;
p5audioRenderer.setSmoothing(value);
});
}
if (p5audioPalette) {
p5audioPalette.addEventListener("change", () => {
p5audioRenderer.setPalette(p5audioPalette.value);
});
}
if (p5audioBass) {
p5audioBass.addEventListener("input", () => {
const value = parseFloat(p5audioBass.value);
p5audioBassValue.textContent = value;
p5audioRenderer.setBassBoost(value);
});
}
if (p5audioMirror) {
p5audioMirror.addEventListener("change", () => {
p5audioRenderer.setMirror(p5audioMirror.checked);
});
}
if (p5audioGlow) {
p5audioGlow.addEventListener("change", () => {
p5audioRenderer.setGlow(p5audioGlow.checked);
});
}
if (p5audioParticles) {
p5audioParticles.addEventListener("change", () => {
p5audioRenderer.setParticles(p5audioParticles.checked);
});
}
if (startP5audio) {
startP5audio.addEventListener("click", async () => {
const success = await p5audioRenderer.startAudio();
if (success) {
startP5audio.textContent = "🎵 Audio Active!";
startP5audio.disabled = true;
p5audioHint.textContent = "🎤 Mic is capturing sound! Ivy sings! 🌿";
} else {
p5audioHint.textContent = "❌ Error: Microphone access denied";
}
});
}
// ============================================
// Window resize handling
// ============================================
window.addEventListener("resize", () => {
WebGPUUtils.resizeCanvasToDisplaySize(canvas, window.devicePixelRatio);
});
// Initial resize
WebGPUUtils.resizeCanvasToDisplaySize(canvas, window.devicePixelRatio);
// ============================================
// About Modal
// ============================================
const aboutLink = document.getElementById("about-link");
const aboutModal = document.getElementById("about-modal");
const modalClose = document.getElementById("modal-close");
const modalOverlay = aboutModal?.querySelector(".modal-overlay");
if (aboutLink && aboutModal) {
// Open modal
aboutLink.addEventListener("click", e => {
e.preventDefault();
aboutModal.classList.remove("hidden");
document.body.style.overflow = "hidden";
});
// Close modal - X button
modalClose?.addEventListener("click", () => {
aboutModal.classList.add("hidden");
document.body.style.overflow = "";
});
// Close modal - overlay click
modalOverlay?.addEventListener("click", () => {
aboutModal.classList.add("hidden");
document.body.style.overflow = "";
});
// Close modal - Escape key
document.addEventListener("keydown", e => {
if (e.key === "Escape" && !aboutModal.classList.contains("hidden")) {
aboutModal.classList.add("hidden");
document.body.style.overflow = "";
}
});
}
console.log("🌿 Ivy's Creative Studio loaded!");
console.log("💚 WebGPU + Three.js + p5.js");
console.log('🌿 "Le lierre pousse où il veut. Moi aussi."');
})();