- 68 exercises from UBA FCE chapters 1-3 - Step-by-step solutions with KaTeX rendering - Theory panels (26 topics) expandable per exercise - Matrix builder (2x2/3x3/4x4) with 7 operations - System solver (Gauss, Gauss-Jordan, Cramer, Rouché-Frobenius) - Glassmorphism UI with dark mode - Canvas particle background - ARIA accessibility (keyboard nav, screen reader) - Zero build step - open index.html directly
139 lines
3.7 KiB
JavaScript
139 lines
3.7 KiB
JavaScript
/**
|
||
* Particles — Canvas floating math symbols background
|
||
* Full-viewport canvas with slow-drifting math symbols (Σ, π, ∫, ∞, Δ, √, α, β)
|
||
* Respects prefers-reduced-motion, disables on viewports < 768px, caps at 30 FPS.
|
||
*/
|
||
const Particles = (() => {
|
||
let canvas = null;
|
||
let ctx = null;
|
||
let particles = [];
|
||
let animId = null;
|
||
let lastTime = 0;
|
||
const FPS = 30;
|
||
const FRAME_TIME = 1000 / FPS;
|
||
|
||
const SYMBOLS = ['Σ', 'π', '∫', '∞', 'Δ', '√', 'α', 'β', 'λ', 'μ', '∂', '∑'];
|
||
|
||
function prefersReducedMotion() {
|
||
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
}
|
||
|
||
function randomBetween(a, b) {
|
||
return a + Math.random() * (b - a);
|
||
}
|
||
|
||
class Particle {
|
||
constructor() {
|
||
this.reset();
|
||
}
|
||
|
||
reset() {
|
||
this.x = Math.random() * (canvas ? canvas.width : window.innerWidth);
|
||
this.y = Math.random() * (canvas ? canvas.height : window.innerHeight);
|
||
this.size = randomBetween(16, 32);
|
||
this.symbol = SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)];
|
||
this.vx = randomBetween(-0.2, 0.2);
|
||
this.vy = randomBetween(-0.15, 0.15);
|
||
this.rotation = randomBetween(0, Math.PI * 2);
|
||
this.rotationSpeed = randomBetween(-0.005, 0.005);
|
||
this.opacity = randomBetween(0.4, 0.8);
|
||
}
|
||
|
||
update() {
|
||
this.x += this.vx;
|
||
this.y += this.vy;
|
||
this.rotation += this.rotationSpeed;
|
||
|
||
const W = canvas ? canvas.width : window.innerWidth;
|
||
const H = canvas ? canvas.height : window.innerHeight;
|
||
|
||
if (this.x < -50) this.x = W + 50;
|
||
if (this.x > W + 50) this.x = -50;
|
||
if (this.y < -50) this.y = H + 50;
|
||
if (this.y > H + 50) this.y = -50;
|
||
}
|
||
|
||
draw(ctx) {
|
||
ctx.save();
|
||
ctx.translate(this.x, this.y);
|
||
ctx.rotate(this.rotation);
|
||
ctx.font = `${this.size}px "Times New Roman", serif`;
|
||
ctx.fillStyle = `rgba(108, 92, 231, ${this.opacity})`;
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(this.symbol, 0, 0);
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
function initCanvas() {
|
||
if (window.innerWidth < 768) return;
|
||
if (prefersReducedMotion()) return;
|
||
|
||
canvas = document.getElementById('particles-bg');
|
||
if (!canvas) {
|
||
canvas = document.createElement('canvas');
|
||
canvas.id = 'particles-bg';
|
||
document.body.appendChild(canvas);
|
||
}
|
||
|
||
ctx = canvas.getContext('2d');
|
||
resize();
|
||
window.addEventListener('resize', resize);
|
||
|
||
// Create particles
|
||
const count = Math.min(40, Math.floor((window.innerWidth * window.innerHeight) / 25000));
|
||
particles = [];
|
||
for (let i = 0; i < count; i++) {
|
||
particles.push(new Particle());
|
||
}
|
||
|
||
lastTime = performance.now();
|
||
loop(performance.now());
|
||
}
|
||
|
||
function resize() {
|
||
if (!canvas) return;
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
}
|
||
|
||
function loop(timestamp) {
|
||
animId = requestAnimationFrame(loop);
|
||
|
||
if (timestamp - lastTime < FRAME_TIME) return;
|
||
lastTime = timestamp;
|
||
|
||
if (!ctx || !canvas) return;
|
||
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
for (const p of particles) {
|
||
p.update();
|
||
p.draw(ctx);
|
||
}
|
||
}
|
||
|
||
function destroy() {
|
||
if (animId) {
|
||
cancelAnimationFrame(animId);
|
||
animId = null;
|
||
}
|
||
if (canvas && canvas.parentElement) {
|
||
canvas.parentElement.removeChild(canvas);
|
||
}
|
||
canvas = null;
|
||
ctx = null;
|
||
particles = [];
|
||
window.removeEventListener('resize', resize);
|
||
}
|
||
|
||
// Auto-init on load
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initCanvas);
|
||
} else {
|
||
initCanvas();
|
||
}
|
||
|
||
return { init: initCanvas, destroy };
|
||
})(); |