new pipeline and build improvements
Some checks failed
Homepage Build and Deploy (Test) / Build Homepage Image (push) Successful in 32s
Homepage Build and Deploy (Test) / Deploy Homepage (push) Failing after 3s

This commit is contained in:
2026-07-10 17:06:16 -03:00
parent 9410d3a7c6
commit 72c7d6c9e0
14 changed files with 1969 additions and 190 deletions

View File

@@ -1,13 +1,21 @@
class MatrixBackground {
constructor() {
this.canvas = document.getElementById('matrixCanvas');
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
if (!this.ctx) return;
this.dots = [];
this.connections = [];
this.mouse = { x: -9999, y: -9999, active: false };
this.rafId = null;
this.isRunning = true;
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
this.isRunning = false;
this.lastTimestamp = 0;
this.motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
this.prefersReducedMotion = this.motionQuery.matches;
this.dotSpriteCache = new Map();
this.onAnimationFrame = (timestamp) => this.animate(timestamp);
this.config = {
dotCount: this.getMobileDotCount(),
@@ -18,7 +26,6 @@ class MatrixBackground {
dotSize: 3,
mouseRadius: 110,
mouseForce: 0.6,
// Palette synced to the UI accent tokens
colors: {
dotDefault: 'rgba(230, 240, 255, 0.78)',
dotDefaultGlow: 'rgba(180, 220, 255, 0.40)',
@@ -51,15 +58,18 @@ class MatrixBackground {
init() {
this.handleResize();
this.bindEvents();
this.animate();
if (this.prefersReducedMotion) {
this.renderFrame(0);
} else {
this.start();
}
}
bindEvents() {
// Pointer reactivity — push dots away from cursor for a "matrix repel" feel.
// Touch devices are included; the dot pushes happen only while touched.
window.addEventListener('pointermove', (e) => {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
window.addEventListener('pointermove', (event) => {
this.mouse.x = event.clientX;
this.mouse.y = event.clientY;
this.mouse.active = true;
}, { passive: true });
@@ -69,35 +79,59 @@ class MatrixBackground {
this.mouse.y = -9999;
});
// Pause the rAF loop when the tab is hidden to save CPU / battery.
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.stop();
} else if (this.prefersReducedMotion) {
this.renderFrame(this.lastTimestamp);
} else {
this.start();
}
});
// Resize is debounced via rAF; disabled on mobile for perf parity with original.
const updateMotionPreference = (event) => {
this.prefersReducedMotion = event.matches;
if (this.prefersReducedMotion) {
this.stop();
this.renderFrame(this.lastTimestamp);
} else if (!document.hidden) {
this.start();
}
};
if (this.motionQuery.addEventListener) {
this.motionQuery.addEventListener('change', updateMotionPreference);
} else {
this.motionQuery.addListener(updateMotionPreference);
}
if (window.innerWidth > 768) {
let resizeRaf = null;
window.addEventListener('resize', () => {
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => this.handleResize());
if (resizeRaf !== null) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => {
resizeRaf = null;
this.handleResize();
});
});
}
}
start() {
if (this.rafId || !this.isRunning) return;
this.animate();
if (this.rafId !== null || this.prefersReducedMotion || document.hidden) return;
this.isRunning = true;
this.rafId = requestAnimationFrame(this.onAnimationFrame);
}
stop() {
if (this.rafId) {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
this.rafId = null;
this.isRunning = false;
}
handleResize() {
@@ -105,12 +139,17 @@ class MatrixBackground {
this.canvas.height = window.innerHeight;
this.config.dotCount = this.getMobileDotCount();
this.config.maxConnections = this.getMobileMaxConnections();
this.connections = [];
this.createDots();
if (this.prefersReducedMotion) {
this.renderFrame(this.lastTimestamp);
}
}
createDots() {
this.dots = [];
for (let i = 0; i < this.config.dotCount; i++) {
for (let index = 0; index < this.config.dotCount; index++) {
this.createDot();
}
}
@@ -118,7 +157,6 @@ class MatrixBackground {
createDot() {
const startX = Math.random() * (this.canvas.width - 20) + 10;
const startY = Math.random() * (this.canvas.height - 20) + 10;
const angle = Math.random() * Math.PI * 2;
const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4);
const baseVelocityX = Math.cos(angle) * speed;
@@ -141,36 +179,32 @@ class MatrixBackground {
updateDots() {
const radius = this.config.mouseRadius;
const radiusSq = radius * radius;
const radiusSquared = radius * radius;
const force = this.config.mouseForce;
this.dots.forEach(dot => {
// Mouse repulsion
for (const dot of this.dots) {
if (this.mouse.active) {
const dx = dot.x - this.mouse.x;
const dy = dot.y - this.mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < radiusSq && distSq > 0.5) {
const dist = Math.sqrt(distSq);
const strength = (1 - dist / radius) * force;
dot.vx += (dx / dist) * strength;
dot.vy += (dy / dist) * strength;
const distanceSquared = dx * dx + dy * dy;
if (distanceSquared < radiusSquared && distanceSquared > 0.5) {
const distance = Math.sqrt(distanceSquared);
const strength = (1 - distance / radius) * force;
dot.vx += (dx / distance) * strength;
dot.vy += (dy / distance) * strength;
}
}
dot.x += dot.vx;
dot.y += dot.vy;
// Ease back to a gentle autonomous drift after pointer interaction.
dot.wanderPhase += dot.wanderSpeed;
const wanderX = Math.cos(dot.wanderPhase) * 0.03;
const wanderY = Math.sin(dot.wanderPhase * 0.8) * 0.03;
dot.vx += (dot.baseVx + wanderX - dot.vx) * 0.02;
dot.vy += (dot.baseVy + wanderY - dot.vy) * 0.02;
// Reflect at the canvas edge. Keep the base velocity in sync with
// the bounce so the drift easing does not push the dot back into
// the same wall on the next frames.
const dotRadius = dot.size / 2;
const minX = dotRadius;
const minY = dotRadius;
@@ -197,69 +231,160 @@ class MatrixBackground {
dot.baseVy = -Math.abs(dot.baseVy);
}
// A large pointer impulse can overshoot both edges by more than
// one frame, so clamp after reflecting the position.
dot.x = Math.max(minX, Math.min(maxX, dot.x));
dot.y = Math.max(minY, Math.min(maxY, dot.y));
});
}
}
getDotSprite(size, isConnected) {
const roundedSize = Math.round(size * 2) / 2;
const state = isConnected ? 'connected' : 'default';
const cacheKey = state + ':' + roundedSize;
const cachedSprite = this.dotSpriteCache.get(cacheKey);
if (cachedSprite) {
return cachedSprite;
}
const padding = 10;
const dimension = Math.ceil(roundedSize + padding * 2);
const sprite = document.createElement('canvas');
sprite.width = dimension;
sprite.height = dimension;
const spriteContext = sprite.getContext('2d');
spriteContext.shadowBlur = 6;
spriteContext.shadowColor = isConnected
? this.config.colors.dotConnectedGlow
: this.config.colors.dotDefaultGlow;
spriteContext.fillStyle = isConnected
? this.config.colors.dotConnected
: this.config.colors.dotDefault;
spriteContext.beginPath();
spriteContext.arc(dimension / 2, dimension / 2, roundedSize / 2, 0, Math.PI * 2);
spriteContext.fill();
this.dotSpriteCache.set(cacheKey, sprite);
return sprite;
}
drawDots() {
this.dots.forEach(dot => {
const isConnected = dot.connectionCount > 0;
this.ctx.beginPath();
this.ctx.arc(dot.x, dot.y, dot.size / 2, 0, Math.PI * 2);
this.ctx.fillStyle = isConnected ? this.config.colors.dotConnected : this.config.colors.dotDefault;
for (const dot of this.dots) {
const sprite = this.getDotSprite(dot.size, dot.connectionCount > 0);
const halfDimension = sprite.width / 2;
this.ctx.globalAlpha = dot.opacity;
this.ctx.shadowBlur = 6;
this.ctx.shadowColor = isConnected ? this.config.colors.dotConnectedGlow : this.config.colors.dotDefaultGlow;
this.ctx.fill();
});
this.ctx.drawImage(sprite, dot.x - halfDimension, dot.y - halfDimension);
}
this.ctx.globalAlpha = 1;
this.ctx.shadowBlur = 0;
}
createConnection(dot1, dot2) {
createConnection(dot1, dot2, timestamp) {
if (this.connections.length >= this.config.maxConnections) return;
dot1.connectionCount++;
dot2.connectionCount++;
this.connections.push({
dot1: dot1,
dot2: dot2,
startTime: Date.now(),
duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min) + this.config.connectionDuration.min
dot1,
dot2,
startTime: timestamp,
duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min)
+ this.config.connectionDuration.min
});
}
updateConnections() {
this.connections = this.connections.filter(conn => {
const elapsed = Date.now() - conn.startTime;
if (elapsed > conn.duration) {
conn.dot1.connectionCount--;
conn.dot2.connectionCount--;
return false;
updateConnections(timestamp) {
let writeIndex = 0;
for (let readIndex = 0; readIndex < this.connections.length; readIndex++) {
const connection = this.connections[readIndex];
if (timestamp - connection.startTime > connection.duration) {
connection.dot1.connectionCount--;
connection.dot2.connectionCount--;
continue;
}
return true;
});
this.connections[writeIndex] = connection;
writeIndex++;
}
this.connections.length = writeIndex;
}
drawConnections() {
this.connections.forEach(conn => {
const { dot1, dot2, startTime, duration } = conn;
const elapsed = Date.now() - startTime;
const opacity = Math.min(1, elapsed / 500);
samplePoisson(lambda) {
if (lambda <= 0) return 0;
const threshold = Math.exp(-lambda);
let product = 1;
let count = 0;
do {
count++;
product *= Math.random();
} while (product > threshold);
return count - 1;
}
tryCreateConnections(timestamp) {
const dotCount = this.dots.length;
if (dotCount < 2 || this.connections.length >= this.config.maxConnections) return;
const pairCount = dotCount * (dotCount - 1) / 2;
const attempts = this.samplePoisson(pairCount * this.config.connectionChance);
for (let attempt = 0;
attempt < attempts && this.connections.length < this.config.maxConnections;
attempt++) {
const firstIndex = Math.floor(Math.random() * dotCount);
let secondIndex = Math.floor(Math.random() * (dotCount - 1));
if (secondIndex >= firstIndex) {
secondIndex++;
}
const dot1 = this.dots[firstIndex];
const dot2 = this.dots[secondIndex];
const dx = dot2.x - dot1.x;
const dy = dot2.y - dot1.y;
const distanceSquared = dx * dx + dy * dy;
if (distanceSquared >= 22500 || distanceSquared <= 900) {
continue;
}
const alreadyConnected = this.connections.some((connection) =>
(connection.dot1 === dot1 && connection.dot2 === dot2)
|| (connection.dot1 === dot2 && connection.dot2 === dot1)
);
if (!alreadyConnected) {
this.createConnection(dot1, dot2, timestamp);
}
}
}
drawConnections(timestamp) {
for (const connection of this.connections) {
const elapsed = timestamp - connection.startTime;
const opacity = Math.min(1, elapsed / 500);
const gradient = this.ctx.createLinearGradient(
connection.dot1.x,
connection.dot1.y,
connection.dot2.x,
connection.dot2.y
);
const gradient = this.ctx.createLinearGradient(dot1.x, dot1.y, dot2.x, dot2.y);
gradient.addColorStop(0, this.config.colors.connection.start);
gradient.addColorStop(0.5, this.config.colors.connection.middle);
gradient.addColorStop(1, this.config.colors.connection.end);
this.ctx.beginPath();
this.ctx.moveTo(dot1.x, dot1.y);
this.ctx.lineTo(dot2.x, dot2.y);
this.ctx.moveTo(connection.dot1.x, connection.dot1.y);
this.ctx.lineTo(connection.dot2.x, connection.dot2.y);
this.ctx.strokeStyle = gradient;
this.ctx.lineWidth = 2;
this.ctx.globalAlpha = opacity;
@@ -267,51 +392,34 @@ class MatrixBackground {
const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2;
this.ctx.shadowBlur = 4 + pulse * 6;
this.ctx.shadowColor = 'rgba(118, 75, 162, 0.5)';
this.ctx.stroke();
});
}
this.ctx.globalAlpha = 1;
this.ctx.shadowBlur = 0;
}
tryCreateConnections() {
for (let i = 0; i < this.dots.length; i++) {
for (let j = i + 1; j < this.dots.length; j++) {
if (Math.random() < this.config.connectionChance) {
const dot1 = this.dots[i];
const dot2 = this.dots[j];
const dx = dot2.x - dot1.x;
const dy = dot2.y - dot1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 150 && distance > 30) {
const alreadyConnected = this.connections.some(conn =>
(conn.dot1 === dot1 && conn.dot2 === dot2) ||
(conn.dot1 === dot2 && conn.dot2 === dot1)
);
if (!alreadyConnected) {
this.createConnection(dot1, dot2);
}
}
}
}
}
}
animate() {
renderFrame(timestamp) {
this.lastTimestamp = timestamp;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
if (!this.prefersReducedMotion) {
this.updateDots();
this.updateConnections();
this.tryCreateConnections();
this.updateConnections(timestamp);
this.tryCreateConnections(timestamp);
}
this.drawConnections();
this.drawConnections(timestamp);
this.drawDots();
}
this.rafId = requestAnimationFrame(() => this.animate());
animate(timestamp) {
this.rafId = null;
this.renderFrame(timestamp);
if (this.isRunning && !this.prefersReducedMotion && !document.hidden) {
this.rafId = requestAnimationFrame(this.onAnimationFrame);
}
}
}