CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 9s
CI / Tests (Python 3.12) (push) Successful in 38s
CI / Tests (Python 3.13) (push) Successful in 22s
CI / Tests (Python 3.14) (push) Successful in 18s
CI / Type Checking (push) Successful in 10s
CI / Spelling (push) Successful in 5s
158 lines
4.9 KiB
JavaScript
158 lines
4.9 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const easings = ["ease", "ease-in", "ease-out", "ease-in-out", "linear"];
|
|
const activeEmojis = [];
|
|
const DEG_TO_RAD = Math.PI / 180;
|
|
|
|
function randomBetween(min, max) {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
function computeTrajectory(angleDeg) {
|
|
// 0 = up, 90 = right, 180 = down, 270 = left (clockwise).
|
|
const rad = angleDeg * DEG_TO_RAD;
|
|
const dx = Math.sin(rad);
|
|
const dy = -Math.cos(rad);
|
|
|
|
// Starting position: opposite edge, randomized along that edge.
|
|
// Ending position: off-screen in travel direction.
|
|
// Units are in vw/vh for viewport-relative positioning.
|
|
const travel = 120;
|
|
let startX, startY;
|
|
const absDx = Math.abs(dx);
|
|
const absDy = Math.abs(dy);
|
|
|
|
if (absDy >= absDx) {
|
|
// Primarily vertical movement.
|
|
startX = Math.random() * 100;
|
|
startY = dy < 0 ? 100 : -10;
|
|
} else {
|
|
// Primarily horizontal movement.
|
|
startX = dx > 0 ? -10 : 100;
|
|
startY = Math.random() * 100;
|
|
}
|
|
|
|
// Drift perpendicular to travel direction.
|
|
const drift = (Math.random() - 0.5) * 15;
|
|
const perpDx = -dy;
|
|
const perpDy = dx;
|
|
|
|
return {
|
|
startX,
|
|
startY,
|
|
endDx: dx * travel + perpDx * drift,
|
|
endDy: dy * travel + perpDy * drift
|
|
};
|
|
}
|
|
|
|
function spawnEmoji(element) {
|
|
const size = randomBetween(SETTINGS.min_size, SETTINGS.max_size);
|
|
const duration = randomBetween(SETTINGS.min_duration, SETTINGS.max_duration);
|
|
const delay = Math.random() * 1.5;
|
|
const easing = easings[Math.floor(Math.random() * easings.length)];
|
|
|
|
const angle = SETTINGS.direction === "random"
|
|
? Math.floor(Math.random() * 360)
|
|
: SETTINGS.direction;
|
|
const traj = computeTrajectory(angle);
|
|
|
|
let rotStart = 0;
|
|
let rotEnd = 0;
|
|
if (SETTINGS.max_rotation > 0) {
|
|
rotStart = randomBetween(-SETTINGS.max_rotation, SETTINGS.max_rotation);
|
|
rotEnd = randomBetween(-SETTINGS.max_rotation, SETTINGS.max_rotation);
|
|
}
|
|
|
|
element.classList.add("emoji_item");
|
|
element.style.left = `${traj.startX}vw`;
|
|
element.style.top = `${traj.startY}vh`;
|
|
|
|
if (element.tagName === "IMG") {
|
|
element.style.width = `${size}px`;
|
|
element.style.height = `${size}px`;
|
|
} else {
|
|
element.style.fontSize = `${size}px`;
|
|
}
|
|
|
|
// Enforce max count by removing oldest.
|
|
while (activeEmojis.length >= SETTINGS.max_count) {
|
|
const oldest = activeEmojis.shift();
|
|
if (oldest) {
|
|
oldest.remove();
|
|
}
|
|
}
|
|
|
|
activeEmojis.push(element);
|
|
document.body.append(element);
|
|
|
|
const keyframes = [
|
|
{
|
|
transform: `translate(0, 0) rotate(${rotStart}deg)`,
|
|
opacity: 1
|
|
},
|
|
{
|
|
transform: `translate(${traj.endDx}vw, ${traj.endDy}vh) rotate(${rotEnd}deg)`,
|
|
opacity: 0
|
|
}
|
|
];
|
|
const timing = {
|
|
duration: duration * 1000,
|
|
delay: delay * 1000,
|
|
easing,
|
|
fill: "forwards"
|
|
};
|
|
|
|
const animation = element.animate(keyframes, timing);
|
|
animation.onfinish = () => {
|
|
element.remove();
|
|
const index = activeEmojis.indexOf(element);
|
|
if (index !== -1) {
|
|
activeEmojis.splice(index, 1);
|
|
}
|
|
};
|
|
}
|
|
|
|
function handleEvent(data) {
|
|
let element;
|
|
if (data.type === "unicode") {
|
|
element = document.createElement("span");
|
|
element.textContent = data.emoji;
|
|
} else if (data.type === "emote") {
|
|
element = document.createElement("img");
|
|
element.src = data.url;
|
|
element.alt = "emote";
|
|
} else {
|
|
return;
|
|
}
|
|
spawnEmoji(element);
|
|
}
|
|
|
|
const RECONNECT_DELAY = 5000;
|
|
|
|
function connect() {
|
|
const source = new EventSource(EVENTS_URL);
|
|
source.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
handleEvent(data);
|
|
} catch (e) {
|
|
// Ignore malformed events.
|
|
}
|
|
};
|
|
source.onerror = () => {
|
|
// EventSource auto-reconnects on transient network errors
|
|
// (readyState stays CONNECTING). However, HTTP-level errors
|
|
// like 502/503 from a reverse proxy set readyState to CLOSED
|
|
// and the browser gives up. Reconnect manually so the overlay
|
|
// survives bot restarts behind a proxy.
|
|
if (source.readyState === EventSource.CLOSED) {
|
|
source.close();
|
|
setTimeout(connect, RECONNECT_DELAY);
|
|
}
|
|
};
|
|
}
|
|
|
|
connect();
|
|
})();
|