package updates, minor bug fixes, stack items, background animation fixes

This commit is contained in:
2026-06-06 12:15:01 +02:00
parent 65b9184a22
commit f5e8b87846
21 changed files with 775 additions and 359 deletions

View File

@@ -1,7 +1,8 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTheme } from "next-themes";
import type React from "react";
import { useCallback, useEffect, useRef } from "react";
const PALETTES = {
dark: {
@@ -37,6 +38,12 @@ interface Particle {
wobblePhase: number;
}
interface CanvasSize {
dpr: number;
height: number;
width: number;
}
interface AnimatedBackgroundContainerProps {
children: React.ReactNode;
className?: string;
@@ -48,6 +55,9 @@ interface AnimatedBackgroundContainerProps {
const DEFAULT_PARTICLE_COLORS: readonly string[] = PALETTES.dark.particles;
const PARTICLE_COLOR_COUNT = DEFAULT_PARTICLE_COLORS.length;
const EDGE_FADE_DISTANCE = 80;
const MAX_DEVICE_PIXEL_RATIO = 2;
const MOBILE_TARGET_DISTANCE = 30;
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
@@ -87,15 +97,18 @@ export default function AnimatedBackgroundContainer({
const frameRef = useRef(0);
const animationFrameRef = useRef<number | null>(null);
const isMobileRef = useRef(false);
const isVisibleRef = useRef(true);
const prefersReducedMotionRef = useRef(false);
const particlesRef = useRef<Particle[]>([]);
const mousePosRef = useRef({ x: 0, y: 0 });
const smoothMouseRef = useRef({ x: 0, y: 0 });
const mobileAnchorRef = useRef({ x: 0, y: 0 });
const mobileTargetRef = useRef({ x: 0, y: 0 });
const canvasSizeRef = useRef<CanvasSize>({ dpr: 1, height: 0, width: 0 });
const containerRectRef = useRef<DOMRect | null>(null);
const followSpeedRef = useRef(followSpeed);
const mobileSpeedRef = useRef(mobileSpeed);
const particleColorsRef = useRef<readonly string[]>(DEFAULT_PARTICLE_COLORS);
const [mounted, setMounted] = useState(false);
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === undefined || resolvedTheme === "dark";
@@ -120,13 +133,13 @@ export default function AnimatedBackgroundContainer({
}, [particleCount, orbitRadius]);
const seedPositions = useCallback(() => {
const container = containerRef.current;
if (!container) {
const { height, width } = canvasSizeRef.current;
if (width === 0 || height === 0) {
return;
}
const centerX = container.clientWidth / 2;
const centerY = container.clientHeight / 2;
const centerX = width / 2;
const centerY = height / 2;
mousePosRef.current = { x: centerX, y: centerY };
smoothMouseRef.current = { x: centerX, y: centerY };
@@ -137,6 +150,13 @@ export default function AnimatedBackgroundContainer({
};
}, []);
const updateContainerRect = useCallback(() => {
const container = containerRef.current;
if (container) {
containerRectRef.current = container.getBoundingClientRect();
}
}, []);
const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
@@ -146,10 +166,19 @@ export default function AnimatedBackgroundContainer({
const width = container.clientWidth;
const height = container.clientHeight;
const dpr = window.devicePixelRatio || 1;
const dpr = Math.min(window.devicePixelRatio || 1, MAX_DEVICE_PIXEL_RATIO);
const nextWidth = Math.round(width * dpr);
const nextHeight = Math.round(height * dpr);
canvas.width = width * dpr;
canvas.height = height * dpr;
canvasSizeRef.current = { dpr, height, width };
updateContainerRect();
if (canvas.width !== nextWidth) {
canvas.width = nextWidth;
}
if (canvas.height !== nextHeight) {
canvas.height = nextHeight;
}
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
@@ -160,14 +189,10 @@ export default function AnimatedBackgroundContainer({
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
if (!mounted) {
seedPositions();
}
}, [mounted, seedPositions]);
seedPositions();
}, [seedPositions, updateContainerRect]);
useEffect(() => {
setMounted(true);
isMobileRef.current = isMobileDevice();
resizeCanvas();
@@ -176,25 +201,35 @@ export default function AnimatedBackgroundContainer({
resizeCanvas();
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [resizeCanvas]);
const resizeObserver =
"ResizeObserver" in window
? new ResizeObserver(() => {
handleResize();
})
: null;
useEffect(() => {
if (!mounted) {
return;
if (containerRef.current && resizeObserver) {
resizeObserver.observe(containerRef.current);
}
seedPositions();
}, [mounted, seedPositions]);
window.addEventListener("resize", handleResize);
window.addEventListener("scroll", updateContainerRect, {
capture: true,
passive: true,
});
return () => {
resizeObserver?.disconnect();
window.removeEventListener("resize", handleResize);
window.removeEventListener("scroll", updateContainerRect, { capture: true });
};
}, [resizeCanvas, updateContainerRect]);
const handleMouseMove = useCallback((event: MouseEvent) => {
const container = containerRef.current;
if (!container || isMobileRef.current) {
const rect = containerRectRef.current;
if (!rect || isMobileRef.current) {
return;
}
const rect = container.getBoundingClientRect();
mousePosRef.current = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
@@ -212,13 +247,8 @@ export default function AnimatedBackgroundContainer({
}, [handleMouseMove]);
useEffect(() => {
if (!mounted) {
return;
}
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
if (!canvas) {
return;
}
@@ -227,9 +257,25 @@ export default function AnimatedBackgroundContainer({
return;
}
const stopAnimation = () => {
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
};
const draw = () => {
const width = container.clientWidth;
const height = container.clientHeight;
if (!isVisibleRef.current || prefersReducedMotionRef.current) {
animationFrameRef.current = null;
return;
}
const { height, width } = canvasSizeRef.current;
if (width === 0 || height === 0) {
animationFrameRef.current = window.requestAnimationFrame(draw);
return;
}
frameRef.current += 1;
if (isMobileRef.current) {
@@ -241,14 +287,15 @@ export default function AnimatedBackgroundContainer({
const dx = mobileTargetRef.current.x - mobileAnchorRef.current.x;
const dy = mobileTargetRef.current.y - mobileAnchorRef.current.y;
if (Math.hypot(dx, dy) < 30) {
if (Math.hypot(dx, dy) < MOBILE_TARGET_DISTANCE) {
mobileTargetRef.current = {
x: rand(width * 0.15, width * 0.85),
y: rand(height * 0.15, height * 0.85),
};
}
smoothMouseRef.current = { ...mobileAnchorRef.current };
smoothMouseRef.current.x = mobileAnchorRef.current.x;
smoothMouseRef.current.y = mobileAnchorRef.current.y;
} else {
const desktopLerp = followSpeedRef.current;
smoothMouseRef.current.x +=
@@ -275,7 +322,13 @@ export default function AnimatedBackgroundContainer({
const edgeFade = Math.max(
0,
Math.min(x / 80, (width - x) / 80, y / 80, (height - y) / 80, 1),
Math.min(
x / EDGE_FADE_DISTANCE,
(width - x) / EDGE_FADE_DISTANCE,
y / EDGE_FADE_DISTANCE,
(height - y) / EDGE_FADE_DISTANCE,
1,
),
);
if (edgeFade <= 0) {
@@ -293,15 +346,51 @@ export default function AnimatedBackgroundContainer({
animationFrameRef.current = window.requestAnimationFrame(draw);
};
animationFrameRef.current = window.requestAnimationFrame(draw);
return () => {
if (animationFrameRef.current !== null) {
window.cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
const startAnimation = () => {
if (
animationFrameRef.current === null &&
isVisibleRef.current &&
!prefersReducedMotionRef.current
) {
animationFrameRef.current = window.requestAnimationFrame(draw);
}
};
}, [mounted]);
const handleVisibilityChange = () => {
isVisibleRef.current = document.visibilityState === "visible";
if (isVisibleRef.current) {
startAnimation();
} else {
stopAnimation();
}
};
const motionMedia = window.matchMedia("(prefers-reduced-motion: reduce)");
const handleMotionChange = () => {
prefersReducedMotionRef.current = motionMedia.matches;
if (prefersReducedMotionRef.current) {
stopAnimation();
ctx.clearRect(0, 0, canvasSizeRef.current.width, canvasSizeRef.current.height);
} else {
startAnimation();
}
};
isVisibleRef.current = document.visibilityState === "visible";
prefersReducedMotionRef.current = motionMedia.matches;
startAnimation();
document.addEventListener("visibilitychange", handleVisibilityChange);
motionMedia.addEventListener("change", handleMotionChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
motionMedia.removeEventListener("change", handleMotionChange);
stopAnimation();
};
}, []);
return (
<div