Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 218 additions & 10 deletions static/web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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";
});
Expand All @@ -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);
Expand All @@ -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));
});
Expand Down Expand Up @@ -123,13 +121,223 @@ 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 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 = [];

// 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();
imgs.forEach((img) => {
if (img.complete && img.naturalWidth > 0 && !seen.has(img.src)) {
seen.add(img.src);
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 || 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;
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.3,
});
alive++;
}

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;
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 (let i = 0; i < particles.length; i++) {
const p = particles[i];
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 lastSpawn = 0;
function loop(time) {
if (isActive && time - lastSpawn > SPAWN_INTERVAL) {
spawnParticle(mouseX, mouseY);
lastSpawn = time;
}

update();
draw();

if (particles.length > 0 || isActive) {
requestAnimationFrame(loop);
} else {
loopRunning = false;
}
}

function ensureLoop() {
if (!loopRunning) {
loopRunning = true;
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 && ready) {
isActive = true;
document.body.style.userSelect = "none";
mouseX = e.clientX;
mouseY = e.clientY;
logoImages = getLogoSources();
shuffled = [...logoImages];
shuffleIndex = shuffled.length; // force initial shuffle
for (let i = 0; i < 5; i++) {
spawnParticle(mouseX, mouseY);
}
ensureLoop();
}
});

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) => {
Expand Down