Files
algebra-webapp/js/particles.js
renato97 45582e5f59 feat: interactive linear algebra practice web app
- 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
2026-05-20 01:26:40 -03:00

139 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
})();