/** * 🌿 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."'); })();