From 3ba61014733843e1fd928a8f279613d998f116b6 Mon Sep 17 00:00:00 2001 From: "Ignacio Ceballos (Yayo)" Date: Mon, 6 Apr 2026 11:39:53 +0200 Subject: [PATCH 1/2] Update main.js --- static/web/main.js | 170 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 159 insertions(+), 11 deletions(-) diff --git a/static/web/main.js b/static/web/main.js index cf2e7c33..7afeb3d0 100644 --- a/static/web/main.js +++ b/static/web/main.js @@ -25,11 +25,9 @@ function filterLogos() { if (!searchInput) return; const query = searchInput.value.toLowerCase(); - const activeTypeChips = document.querySelectorAll( - ".chip.active[data-type]" - ); + const activeTypeChips = document.querySelectorAll(".chip.active[data-type]"); const activeColorChips = document.querySelectorAll( - ".chip.active[data-color]" + ".chip.active[data-color]", ); const cells = document.querySelectorAll("#logo-grid .logo-cell"); @@ -39,10 +37,10 @@ function filterLogos() { const color = cell.getAttribute("data-color"); const nameMatch = name.includes(query); const typeMatch = Array.from(activeTypeChips).some( - (chip) => chip.dataset.type === type + (chip) => chip.dataset.type === type, ); const colorMatch = Array.from(activeColorChips).some( - (chip) => chip.dataset.color.toLowerCase() === color.toLowerCase() + (chip) => chip.dataset.color.toLowerCase() === color.toLowerCase(), ); cell.style.display = nameMatch && typeMatch && colorMatch ? "" : "none"; }); @@ -68,7 +66,7 @@ function loadLogos() { cell.className = "logo-cell"; cell.setAttribute( "data-type", - logo.variant === "wordmark" ? "Wordmark" : "Glyph" + logo.variant === "wordmark" ? "Wordmark" : "Glyph", ); cell.setAttribute("data-color", logo.version); cell.setAttribute("data-name", logo.name); @@ -95,7 +93,7 @@ function loadLogos() { navigator.clipboard .writeText(apiUrl) .then(() => - showSnackbar(`${logo.name} logo copied to clipboard!`) + showSnackbar(`${logo.name} logo copied to clipboard!`), ) .catch((err) => console.error("Error copying URL:", err)); }); @@ -123,13 +121,163 @@ function loadFavicon() { document.getElementById("dynamic-favicon").href = randomFavicon; } }) - .catch((error) => - console.error("Error setting dynamic favicon:", error) - ); + .catch((error) => console.error("Error setting dynamic favicon:", error)); +} + +// --- Logo Particle Fountain --- +const PARTICLE_MIN_HEIGHT = 20; +const PARTICLE_MAX_HEIGHT = 20; + +function initParticleSystem() { + const canvas = document.createElement("canvas"); + canvas.id = "particle-canvas"; + canvas.style.cssText = + "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999"; + document.body.appendChild(canvas); + const ctx = canvas.getContext("2d"); + + function resize() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + resize(); + window.addEventListener("resize", resize); + + const particles = []; + let isActive = false; + let mouseX = 0; + let mouseY = 0; + let logoImages = []; + let spawnTimer = 0; + const SPAWN_INTERVAL = 40; // ms between particle spawns + + // Collect logo image sources from the grid + function collectLogoSources() { + const imgs = document.querySelectorAll("#logo-grid .grid-logos"); + const sources = []; + const seen = new Set(); + imgs.forEach((img) => { + if (img.complete && img.naturalWidth > 0 && !seen.has(img.src)) { + seen.add(img.src); + sources.push(img); + } + }); + return sources; + } + + function spawnParticle(x, y) { + if (logoImages.length === 0) return; + const img = logoImages[Math.floor(Math.random() * logoImages.length)]; + const height = + PARTICLE_MIN_HEIGHT + + Math.random() * (PARTICLE_MAX_HEIGHT - PARTICLE_MIN_HEIGHT); + const aspect = img.naturalWidth / img.naturalHeight; + const width = height * aspect; + const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.4; // upward spread + const speed = 2 + Math.random() * 4; + particles.push({ + img, + x, + y, + vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 1.5, + vy: Math.sin(angle) * speed, + width, + height, + opacity: 1, + rotation: Math.random() * Math.PI * 2, + rotationSpeed: (Math.random() - 0.5) * 0.15, + life: 1, + decay: 0.008 + Math.random() * 0.008, + gravity: 0.12, + }); + } + + function update(dt) { + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i]; + p.vy += p.gravity; + p.x += p.vx; + p.y += p.vy; + p.rotation += p.rotationSpeed; + p.life -= p.decay; + p.opacity = Math.max(0, p.life); + if (p.life <= 0) { + particles.splice(i, 1); + } + } + } + + function draw() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (const p of particles) { + ctx.save(); + ctx.globalAlpha = p.opacity; + ctx.translate(p.x, p.y); + ctx.rotate(p.rotation); + ctx.drawImage(p.img, -p.width / 2, -p.height / 2, p.width, p.height); + ctx.restore(); + } + } + + let lastTime = 0; + let lastSpawn = 0; + function loop(time) { + const dt = time - lastTime; + lastTime = time; + + if (isActive && time - lastSpawn > SPAWN_INTERVAL) { + spawnParticle(mouseX, mouseY); + lastSpawn = time; + } + + update(dt); + draw(); + + if (particles.length > 0 || isActive) { + requestAnimationFrame(loop); + } + } + + function startLoop() { + lastTime = performance.now(); + lastSpawn = 0; + requestAnimationFrame(loop); + } + + // Left-click hold + document.addEventListener("mousedown", (e) => { + if (e.button === 0) { + isActive = true; + document.body.style.userSelect = "none"; + mouseX = e.clientX; + mouseY = e.clientY; + logoImages = collectLogoSources(); + // Burst inicial para que se sienta inmediato + for (let i = 0; i < 5; i++) { + spawnParticle(mouseX, mouseY); + } + startLoop(); + } + }); + + document.addEventListener("mousemove", (e) => { + if (isActive) { + mouseX = e.clientX; + mouseY = e.clientY; + } + }); + + document.addEventListener("mouseup", (e) => { + if (e.button === 0) { + isActive = false; + document.body.style.userSelect = ""; + } + }); } document.addEventListener("DOMContentLoaded", () => { loadFavicon(); + initParticleSystem(); // Copy URL buttons (API examples) document.querySelectorAll(".icon-button").forEach((button) => { From 366b175b93f89e6bd76a20996100b98ba2ff444b Mon Sep 17 00:00:00 2001 From: "Ignacio Ceballos (Yayo)" Date: Mon, 6 Apr 2026 11:48:32 +0200 Subject: [PATCH 2/2] Update main.js --- static/web/main.js | 116 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/static/web/main.js b/static/web/main.js index 7afeb3d0..a69c3b2b 100644 --- a/static/web/main.js +++ b/static/web/main.js @@ -143,16 +143,20 @@ function initParticleSystem() { resize(); window.addEventListener("resize", resize); - const particles = []; + const MAX_PARTICLES = 200; + const SPAWN_INTERVAL = 40; + let particles = []; + let alive = 0; // track live count without array resize let isActive = false; + let loopRunning = false; let mouseX = 0; let mouseY = 0; let logoImages = []; - let spawnTimer = 0; - const SPAWN_INTERVAL = 40; // ms between particle spawns - // Collect logo image sources from the grid - function collectLogoSources() { + // Cache logo sources — rebuild only when grid changes + let logoCache = null; + function getLogoSources() { + if (logoCache) return logoCache; const imgs = document.querySelectorAll("#logo-grid .grid-logos"); const sources = []; const seen = new Set(); @@ -162,18 +166,42 @@ function initParticleSystem() { sources.push(img); } }); + logoCache = sources; return sources; } + // Invalidate cache when logos reload + const observer = new MutationObserver(() => { + logoCache = null; + }); + const grid = document.getElementById("logo-grid"); + if (grid) observer.observe(grid, { childList: true }); + + // Shuffle + cycle to avoid repeating the same logos + let shuffled = []; + let shuffleIndex = 0; + function nextLogo() { + if (shuffled.length === 0) return null; + if (shuffleIndex >= shuffled.length) { + // Re-shuffle when we've cycled through all + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + shuffleIndex = 0; + } + return shuffled[shuffleIndex++]; + } function spawnParticle(x, y) { - if (logoImages.length === 0) return; - const img = logoImages[Math.floor(Math.random() * logoImages.length)]; + if (logoImages.length === 0 || alive >= MAX_PARTICLES) return; + const img = nextLogo(); + if (!img) return; const height = PARTICLE_MIN_HEIGHT + Math.random() * (PARTICLE_MAX_HEIGHT - PARTICLE_MIN_HEIGHT); const aspect = img.naturalWidth / img.naturalHeight; const width = height * aspect; - const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.4; // upward spread + const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.4; const speed = 2 + Math.random() * 4; particles.push({ img, @@ -188,28 +216,34 @@ function initParticleSystem() { rotationSpeed: (Math.random() - 0.5) * 0.15, life: 1, decay: 0.008 + Math.random() * 0.008, - gravity: 0.12, + gravity: 0.3, }); + alive++; } - function update(dt) { - for (let i = particles.length - 1; i >= 0; i--) { + function update() { + // Compact: swap dead particles to end, then truncate once + let write = 0; + for (let i = 0; i < particles.length; i++) { const p = particles[i]; p.vy += p.gravity; p.x += p.vx; p.y += p.vy; p.rotation += p.rotationSpeed; p.life -= p.decay; - p.opacity = Math.max(0, p.life); - if (p.life <= 0) { - particles.splice(i, 1); + if (p.life > 0) { + p.opacity = p.life; + particles[write++] = p; } } + particles.length = write; + alive = write; } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); - for (const p of particles) { + for (let i = 0; i < particles.length; i++) { + const p = particles[i]; ctx.save(); ctx.globalAlpha = p.opacity; ctx.translate(p.x, p.y); @@ -219,44 +253,70 @@ function initParticleSystem() { } } - let lastTime = 0; let lastSpawn = 0; function loop(time) { - const dt = time - lastTime; - lastTime = time; - if (isActive && time - lastSpawn > SPAWN_INTERVAL) { spawnParticle(mouseX, mouseY); lastSpawn = time; } - update(dt); + update(); draw(); if (particles.length > 0 || isActive) { requestAnimationFrame(loop); + } else { + loopRunning = false; + } + } + + function ensureLoop() { + if (!loopRunning) { + loopRunning = true; + lastSpawn = 0; + requestAnimationFrame(loop); } } - function startLoop() { - lastTime = performance.now(); - lastSpawn = 0; - requestAnimationFrame(loop); + // Wait until all grid images are loaded + let ready = false; + function checkReady() { + const imgs = document.querySelectorAll("#logo-grid .grid-logos"); + if (imgs.length === 0) return false; + return Array.from(imgs).every( + (img) => img.complete && img.naturalWidth > 0, + ); } + const readyObserver = new MutationObserver(() => { + if (!ready && checkReady()) { + ready = true; + readyObserver.disconnect(); + } + }); + if (grid) readyObserver.observe(grid, { childList: true, subtree: true }); + // Also check on image load events + document.addEventListener( + "load", + () => { + if (!ready && checkReady()) ready = true; + }, + true, + ); // Left-click hold document.addEventListener("mousedown", (e) => { - if (e.button === 0) { + if (e.button === 0 && ready) { isActive = true; document.body.style.userSelect = "none"; mouseX = e.clientX; mouseY = e.clientY; - logoImages = collectLogoSources(); - // Burst inicial para que se sienta inmediato + logoImages = getLogoSources(); + shuffled = [...logoImages]; + shuffleIndex = shuffled.length; // force initial shuffle for (let i = 0; i < 5; i++) { spawnParticle(mouseX, mouseY); } - startLoop(); + ensureLoop(); } });