omnidash/web/src/lib/components/site/particles.svelte

248 lines
6.1 KiB
Svelte

<script>
import { mode } from 'mode-watcher';
import { onMount, beforeUpdate, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
const mousePositionStore = writable({ x: 0, y: 0 });
let x = 0;
let y = 0;
const handleMouseMove = (/** @type {{ clientX: number; clientY: number; }} */ event) => {
x = event.clientX;
y = event.clientY;
mousePositionStore.set({ x, y });
};
export let className = 'h-full';
export let quantity = 30;
export let staticity = 50;
export let ease = 50;
export let vx = 0;
export let vy = 0;
let color = '#ffffff';
let rgb = hexToRgb(color);
/**
* @type {HTMLCanvasElement}
*/
let canvasRef;
/**
* @type {HTMLDivElement}
*/
let canvasContainerRef;
/**
* @type {CanvasRenderingContext2D | null}
*/
let context;
/**
* @type {any[]}
*/
let circles = [];
let mousePosition = mousePositionStore;
let mouse = { x: 0, y: 0 };
let canvasSize = { w: 0, h: 0 };
let dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
/**
* @param {string} hex
*/
function hexToRgb(hex) {
hex = hex.replace('#', '');
const hexInt = parseInt(hex, 16);
const red = (hexInt >> 16) & 255;
const green = (hexInt >> 8) & 255;
const blue = hexInt & 255;
return [red, green, blue];
}
mode.subscribe((value) => {
color = value === 'dark' ? '#ffffff' : '#000000';
rgb = hexToRgb(color);
});
/**
* @param {{ x: any; y: any; translateX: any; translateY: any; size: any; alpha: any; targetAlpha?: number; dx?: number; dy?: number; magnetism?: number; }} circle
*/
function drawCircle(circle, update = false) {
if (context) {
const { x, y, translateX, translateY, size, alpha } = circle;
context.translate(translateX, translateY);
context.beginPath();
context.arc(x, y, size, 0, 2 * Math.PI);
context.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`;
context.fill();
context.setTransform(dpr, 0, 0, dpr, 0, 0);
if (!update) {
circles.push(circle);
}
}
}
function initCanvas() {
resizeCanvas();
drawParticles();
}
function onMouseMove() {
if (canvasRef) {
const rect = canvasRef.getBoundingClientRect();
const { w, h } = canvasSize;
const x = $mousePosition.x - rect.left - w / 2;
const y = $mousePosition.y - rect.top - h / 2;
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
if (inside) {
mouse.x = x;
mouse.y = y;
}
}
}
function resizeCanvas() {
if (canvasContainerRef && canvasRef && context) {
circles = [];
canvasSize.w = canvasContainerRef.offsetWidth;
canvasSize.h = canvasContainerRef.offsetHeight;
canvasRef.width = canvasSize.w * dpr;
canvasRef.height = canvasSize.h * dpr;
canvasRef.style.width = `${canvasSize.w}px`;
canvasRef.style.height = `${canvasSize.h}px`;
context.scale(dpr, dpr);
}
}
function circleParams() {
const x = Math.floor(Math.random() * canvasSize.w);
const y = Math.floor(Math.random() * canvasSize.h);
const translateX = 0;
const translateY = 0;
const size = Math.floor(Math.random() * 2) + 1;
const alpha = 0;
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
const dx = (Math.random() - 0.5) * 0.2;
const dy = (Math.random() - 0.5) * 0.2;
const magnetism = 0.1 + Math.random() * 4;
return {
x,
y,
translateX,
translateY,
size,
alpha,
targetAlpha,
dx,
dy,
magnetism
};
}
function clearContext() {
if (context) {
context.clearRect(0, 0, canvasSize.w, canvasSize.h);
}
}
function drawParticles() {
clearContext();
const particleCount = quantity;
for (let i = 0; i < particleCount; i++) {
const circle = circleParams();
drawCircle(circle);
}
}
/**
* @param {number} value
* @param {number} start1
* @param {number} end1
* @param {number} start2
* @param {number} end2
*/
function remapValue(value, start1, end1, start2, end2) {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
}
function animate() {
clearContext();
circles.forEach((circle, i) => {
const edge = [
circle.x + circle.translateX - circle.size,
canvasSize.w - circle.x - circle.translateX - circle.size,
circle.y + circle.translateY - circle.size,
canvasSize.h - circle.y - circle.translateY - circle.size
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
if (remapClosestEdge > 1) {
circle.alpha += 0.02;
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha;
}
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
}
circle.x += circle.dx + vx;
circle.y += circle.dy + vy;
circle.translateX += (mouse.x / (staticity / circle.magnetism) - circle.translateX) / ease;
circle.translateY += (mouse.y / (staticity / circle.magnetism) - circle.translateY) / ease;
if (
circle.x < -circle.size ||
circle.x > canvasSize.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.h + circle.size
) {
circles.splice(i, 1);
const newCircle = circleParams();
drawCircle(newCircle);
} else {
drawCircle(
{
...circle,
x: circle.x,
y: circle.y,
translateX: circle.translateX,
translateY: circle.translateY,
alpha: circle.alpha
},
true
);
}
});
setTimeout(() => {
requestAnimationFrame(animate);
}, 1000 / 60); // Limit the frame rate to 60 FPS
}
onMount(() => {
window.addEventListener('mousemove', handleMouseMove);
if (canvasRef) {
context = canvasRef?.getContext('2d');
}
initCanvas();
animate();
window.addEventListener('resize', initCanvas);
return () => {
window.removeEventListener('resize', initCanvas);
window.removeEventListener('mousemove', handleMouseMove);
};
});
beforeUpdate(() => {
onMouseMove();
});
onDestroy(() => {
if (canvasRef) {
window.removeEventListener('resize', initCanvas);
}
});
</script>
<div class={className} bind:this={canvasContainerRef} aria-hidden="true">
<canvas bind:this={canvasRef}></canvas>
</div>