viewof wordcloud = {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 1920;
canvas.height = 1080;
canvas.style = "display:block;margin:0 auto;background:#00b140";
const emojiCache = {};
let emojiRegex;
try { emojiRegex = /\p{Emoji}/u; }
catch { emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; }
async function fetchData() {
const data = await loadData();
return data.map(d => d.text);
}
const gravity = 500;
const restitution = 0.2;
const dt = 1/60;
const cycleDuration = 30000;
const spawnDelay = 3000;
const sizeOptions = [40, 50, 60, 70];
let bodies = [];
let dataQueue = [];
let nextSpawnTime = 0;
function createBody(text) {
const size = sizeOptions[Math.floor(Math.random() * sizeOptions.length)];
ctx.font = `${size}px sans-serif`;
const raw = text;
const lines = raw.length > 10 ? raw.match(/.{1,10}/g) : [raw];
const widths = lines.map(line => ctx.measureText(line).width);
const width = Math.max(...widths);
const height = size * lines.length;
const x = Math.random() * (canvas.width - width);
const y = -height;
for (const ch of lines.join('')) {
if (emojiRegex.test(ch) && !emojiCache[ch]) {
const code = ch.codePointAt(0).toString(16);
const img = new Image();
img.src = `https://twemoji.maxcdn.com/v/latest/72x72/${code}.png`;
emojiCache[ch] = img;
}
}
return {
textLines: lines,
size, width, height,
x, y,
vx: 0, vy: 0,
settled: false,
prevColor: randomColor(),
targetColor: randomColor(),
colorStart: performance.now(),
colorDuration: cycleDuration
};
}
function reset() {
bodies = [];
fetchData().then(texts => { dataQueue = [...texts]; nextSpawnTime = 0; });
}
function maybeSpawn(now) {
if (dataQueue.length && now >= nextSpawnTime) {
bodies.push(createBody(dataQueue.shift()));
nextSpawnTime = now + spawnDelay;
}
}
function draw(now) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00b140";
ctx.fillRect(0, 0, canvas.width, canvas.height);
maybeSpawn(now);
for (const b of bodies) {
if (!b.settled) {
b.vy += gravity * dt;
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.y + b.height > canvas.height) {
b.y = canvas.height - b.height;
b.vy *= -restitution;
if (Math.abs(b.vy) < 10) { b.vy = 0; b.settled = true; }
}
for (const o of bodies) {
if (o === b || !o.settled) continue;
const overlapY = (b.y + b.height) - o.y;
if (overlapY > 0 && b.x + b.width > o.x && b.x < o.x + o.width) {
b.y -= overlapY;
b.vy *= -restitution;
if (Math.abs(b.vy) < 10) { b.vy = 0; b.settled = true; }
}
}
}
}
for (const b of bodies) {
const t = Math.min(1, (now - b.colorStart) / b.colorDuration);
const color = interpolateColor(b.prevColor, b.targetColor, t);
if (t >= 1) {
b.prevColor = b.targetColor;
b.targetColor = randomColor();
b.colorStart = now;
}
ctx.font = `${b.size}px sans-serif`;
ctx.fillStyle = color;
b.textLines.forEach((line, i) => {
ctx.fillText(line, b.x, b.y + i * b.size);
});
}
const settled = bodies.filter(b => b.settled);
if (settled.length) {
const minY = Math.min(...settled.map(b => b.y));
if (canvas.height - minY > canvas.height * 0.7) {
reset();
}
}
if (dataQueue.length === 0 && bodies.length > 0 && bodies.every(b => b.settled)) {
reset();
}
requestAnimationFrame(draw);
}
reset();
requestAnimationFrame(draw);
return canvas;
}