This commit introduces a completely redesigned sponsor list page. The previous two-column layout is replaced with a modern, single-column auto-scrolling list that unifies all sponsors. A dynamic canvas animation has been added to the background for a more engaging visual experience. Previous versions of the page have been archived. This update also includes data corrections for sponsor names and minor fixes to the related PPT assets.
116 lines
3.3 KiB
JavaScript
116 lines
3.3 KiB
JavaScript
/**
|
||
* 格式化金额,默认 MYR (RM)
|
||
* @param {Number|String} amount 金额
|
||
* @param {String} currency 币种(ISO 4217),默认 MYR
|
||
* @param {String} locale 语言地区,默认 "ms-MY"
|
||
* @returns {String} 格式化后的金额
|
||
*/
|
||
function formatCurrency(amount, currency = "MYR", locale = "ms-MY") {
|
||
const num = Number(amount);
|
||
if (isNaN(num)) return String(amount); // 非数字直接返回原值
|
||
return new Intl.NumberFormat(locale, {
|
||
style: "currency",
|
||
currency,
|
||
}).format(num);
|
||
}
|
||
|
||
const sponsorListDiv = document.getElementById("sponsorList");
|
||
|
||
Promise.all([
|
||
fetch("../data/sponsors.json").then((res) => res.json()),
|
||
fetch("../data/seats.json").then((res) => res.json()),
|
||
]).then(([sponsors, seats]) => {
|
||
const sponsorList = sponsors.map((item) => ({
|
||
name: item.name,
|
||
amount: formatCurrency(item.amount), // 使用 Intl API
|
||
}));
|
||
|
||
const seatList = seats.map((item) => ({
|
||
name: item.name,
|
||
amount: `${item.seat} 席`,
|
||
}));
|
||
|
||
[...sponsorList, ...seatList].forEach((entry) => {
|
||
const card = document.createElement("div");
|
||
card.className = "sponsor-item card";
|
||
card.innerHTML = `<h2>${entry.name}</h2><p>${entry.amount}</p>`;
|
||
sponsorListDiv.appendChild(card);
|
||
});
|
||
});
|
||
|
||
// ================= Canvas 背景 =================
|
||
const canvas = document.getElementById("background");
|
||
const ctx = canvas.getContext("2d");
|
||
|
||
function resizeCanvas() {
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
}
|
||
resizeCanvas();
|
||
window.addEventListener("resize", resizeCanvas);
|
||
|
||
class Point {
|
||
constructor() {
|
||
this.radius = Math.random() * 4 + 2;
|
||
this.x = Math.random() * canvas.width;
|
||
this.y = Math.random() * canvas.height;
|
||
this.xSpeed = (Math.random() - 0.5) * 60;
|
||
this.ySpeed = (Math.random() - 0.5) * 60;
|
||
this.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
|
||
this.lastDrawTime = null;
|
||
}
|
||
|
||
draw() {
|
||
const now = Date.now();
|
||
if (this.lastDrawTime) {
|
||
const dt = (now - this.lastDrawTime) / 1000;
|
||
this.x += this.xSpeed * dt;
|
||
this.y += this.ySpeed * dt;
|
||
}
|
||
if (this.x < 0 || this.x > canvas.width) this.xSpeed *= -1;
|
||
if (this.y < 0 || this.y > canvas.height) this.ySpeed *= -1;
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||
ctx.fillStyle = this.color;
|
||
ctx.shadowBlur = 15;
|
||
ctx.shadowColor = this.color;
|
||
ctx.fill();
|
||
this.lastDrawTime = now;
|
||
}
|
||
}
|
||
|
||
class Graph {
|
||
constructor(pointCount = 70, maxDistance = 120) {
|
||
this.points = Array.from({ length: pointCount }, () => new Point());
|
||
this.maxDist = maxDistance;
|
||
}
|
||
|
||
draw() {
|
||
requestAnimationFrame(() => this.draw());
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
for (let i = 0; i < this.points.length; i++) {
|
||
const p1 = this.points[i];
|
||
p1.draw();
|
||
for (let j = i + 1; j < this.points.length; j++) {
|
||
const p2 = this.points[j];
|
||
const dx = p1.x - p2.x;
|
||
const dy = p1.y - p2.y;
|
||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||
if (dist < this.maxDist) {
|
||
const alpha = 1 - dist / this.maxDist;
|
||
ctx.beginPath();
|
||
ctx.moveTo(p1.x, p1.y);
|
||
ctx.lineTo(p2.x, p2.y);
|
||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
||
ctx.lineWidth = 2 * alpha;
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
new Graph().draw();
|