124 lines
3.7 KiB
Vue
124 lines
3.7 KiB
Vue
<template>
|
|
<div ref="containerRef" :class="['h-full w-full', className]">
|
|
<canvas
|
|
ref="canvasRef"
|
|
class="pointer-events-none"
|
|
:style="{ width: canvasSize.width + 'px', height: canvasSize.height + 'px' }"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, onUnmounted, ref, watchEffect } from 'vue';
|
|
|
|
const props = defineProps({
|
|
squareSize: { type: Number, default: 4 },
|
|
gridGap: { type: Number, default: 6 },
|
|
flickerChance: { type: Number, default: 0.3 },
|
|
color: { type: String, default: 'rgb(0, 0, 0)' },
|
|
width: Number,
|
|
height: Number,
|
|
className: String,
|
|
maxOpacity: { type: Number, default: 0.3 },
|
|
});
|
|
|
|
const canvasRef = ref(null);
|
|
const containerRef = ref(null);
|
|
const canvasSize = ref({ width: 0, height: 0 });
|
|
const isInView = ref(false);
|
|
|
|
function toRGBA(color) {
|
|
if (process.server) return `rgba(0, 0, 0,`;
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = canvas.height = 1;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return `rgba(255, 0, 0,`;
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(0, 0, 1, 1);
|
|
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
|
return `rgba(${r}, ${g}, ${b},`;
|
|
}
|
|
|
|
|
|
const memoizedColor = toRGBA(props.color);
|
|
|
|
onMounted(() => {
|
|
const canvas = canvasRef.value;
|
|
const container = containerRef.value;
|
|
if (!canvas || !container) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
|
|
let cols, rows, squares, animationFrameId, lastTime = 0;
|
|
|
|
const setupCanvas = () => {
|
|
const w = props.width || container.clientWidth;
|
|
const h = props.height || container.clientHeight;
|
|
|
|
canvasSize.value = { width: w, height: h };
|
|
canvas.width = w * dpr;
|
|
canvas.height = h * dpr;
|
|
canvas.style.width = `${w}px`;
|
|
canvas.style.height = `${h}px`;
|
|
|
|
cols = Math.floor(w / (props.squareSize + props.gridGap));
|
|
rows = Math.floor(h / (props.squareSize + props.gridGap));
|
|
squares = new Float32Array(cols * rows).map(() => Math.random() * props.maxOpacity);
|
|
};
|
|
|
|
const updateSquares = (deltaTime) => {
|
|
for (let i = 0; i < squares.length; i++) {
|
|
if (Math.random() < props.flickerChance * deltaTime) {
|
|
squares[i] = Math.random() * props.maxOpacity;
|
|
}
|
|
}
|
|
};
|
|
|
|
const drawGrid = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
for (let i = 0; i < cols; i++) {
|
|
for (let j = 0; j < rows; j++) {
|
|
const opacity = squares[i * rows + j];
|
|
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
|
ctx.fillRect(
|
|
i * (props.squareSize + props.gridGap) * dpr,
|
|
j * (props.squareSize + props.gridGap) * dpr,
|
|
props.squareSize * dpr,
|
|
props.squareSize * dpr
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const animate = (time) => {
|
|
if (!isInView.value) return;
|
|
const deltaTime = (time - lastTime) / 1000;
|
|
lastTime = time;
|
|
updateSquares(deltaTime);
|
|
drawGrid();
|
|
animationFrameId = requestAnimationFrame(animate);
|
|
};
|
|
|
|
const resizeObserver = new ResizeObserver(() => setupCanvas());
|
|
resizeObserver.observe(container);
|
|
|
|
const intersectionObserver = new IntersectionObserver(([entry]) => {
|
|
isInView.value = entry.isIntersecting;
|
|
if (isInView.value) animationFrameId = requestAnimationFrame(animate);
|
|
});
|
|
intersectionObserver.observe(canvas);
|
|
|
|
setupCanvas();
|
|
|
|
onUnmounted(() => {
|
|
cancelAnimationFrame(animationFrameId);
|
|
resizeObserver.disconnect();
|
|
intersectionObserver.disconnect();
|
|
});
|
|
});
|
|
</script>
|
|
|