Reactjs Example#1
Conversation
|
noted |
|
This pull request introduces a new React component called Key changes include: Documentation:
Component Implementation:
These changes provide a detailed guide and a robust implementation for using the new 'use client';
import React, { useEffect, useRef, useCallback } from 'react';
interface PixelProps {
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
x: number;
y: number;
color: string;
speed: number;
delay: number;
}
class Pixel {
width: number;
height: number;
ctx: CanvasRenderingContext2D;
x: number;
y: number;
color: string;
speed: number;
size: number;
sizeStep: number;
minSize: number;
maxSizeInteger: number;
maxSize: number;
delay: number;
counter: number;
counterStep: number;
isIdle: boolean;
isReverse: boolean;
isShimmer: boolean;
constructor({ canvas, context, x, y, color, speed, delay }: PixelProps) {
this.width = canvas.width;
this.height = canvas.height;
this.ctx = context;
this.x = x;
this.y = y;
this.color = color;
this.speed = this.getRandomValue(0.1, 0.9) * speed;
this.size = 0;
this.sizeStep = Math.random() * 0.4;
this.minSize = 0.5;
this.maxSizeInteger = 2;
this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);
this.delay = delay;
this.counter = 0;
this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;
this.isIdle = false;
this.isReverse = false;
this.isShimmer = false;
}
getRandomValue(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
draw(): void {
const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;
this.ctx.fillStyle = this.color;
this.ctx.fillRect(
this.x + centerOffset,
this.y + centerOffset,
this.size,
this.size
);
}
appear(): void {
this.isIdle = false;
if (this.counter <= this.delay) {
this.counter += this.counterStep;
return;
}
if (this.size >= this.maxSize) {
this.isShimmer = true;
}
if (this.isShimmer) {
this.shimmer();
} else {
this.size += this.sizeStep;
}
this.draw();
}
disappear(): void {
this.isShimmer = false;
this.counter = 0;
if (this.size <= 0) {
this.isIdle = true;
return;
} else {
this.size -= 0.1;
}
this.draw();
}
shimmer(): void {
if (this.size >= this.maxSize) {
this.isReverse = true;
} else if (this.size <= this.minSize) {
this.isReverse = false;
}
if (this.isReverse) {
this.size -= this.speed;
} else {
this.size += this.speed;
}
}
}
interface PixelCanvasProps {
colors?: string[];
gap?: number;
speed?: number;
noFocus?: boolean;
}
const PixelCanvas: React.FC<PixelCanvasProps> = ({
colors = ['#f8fafc', '#f1f5f9', '#cbd5e1'],
gap = 5,
speed = 35,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const pixelsRef = useRef<Pixel[]>([]);
const animationFrameRef = useRef<number | null>(null);
const timePreviousRef = useRef<number>(0);
const timeInterval = 1000 / 60;
const init = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const rect = canvas.getBoundingClientRect();
const width = Math.floor(rect.width);
const height = Math.floor(rect.height);
canvas.width = width;
canvas.height = height;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const newPixels: Pixel[] = [];
for (let x = 0; x < width; x += gap) {
for (let y = 0; y < height; y += gap) {
const color = colors[Math.floor(Math.random() * colors.length)];
const delay = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : getDistanceToCanvasCenter(x, y, width, height);
newPixels.push(new Pixel({ canvas, context: ctx, x, y, color, speed: speed * 0.001, delay }));
}
}
pixelsRef.current = newPixels;
}, [colors, gap, speed]);
const getDistanceToCanvasCenter = (x: number, y: number, width: number, height: number): number => {
const dx = x - width / 2;
const dy = y - height / 2;
return Math.sqrt(dx * dx + dy * dy);
};
const animate = useCallback(() => {
const timeNow = performance.now();
const timePassed = timeNow - timePreviousRef.current;
if (timePassed < timeInterval) {
animationFrameRef.current = requestAnimationFrame(animate);
return;
}
timePreviousRef.current = timeNow - (timePassed % timeInterval);
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < pixelsRef.current.length; i++) {
pixelsRef.current[i].appear();
}
animationFrameRef.current = requestAnimationFrame(animate);
}, [timeInterval]);
useEffect(() => {
if (typeof window !== 'undefined') {
init();
animationFrameRef.current = requestAnimationFrame(animate);
}
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [init, animate]);
return <canvas ref={canvasRef} className="w-full h-full"></canvas>;
};
export default PixelCanvas; |
SaadBazaz
left a comment
There was a problem hiding this comment.
I've tested this, it doesn't work.
Instead, I made this one and it works absolutely fine:
'use client';
import { cn } from '@/lib/utils';
import React, { useEffect, useRef, useCallback } from 'react';
import { useState } from 'react';
interface PixelProps {
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
x: number;
y: number;
color: string;
speed: number;
delay: number;
}
class Pixel {
width: number;
height: number;
ctx: CanvasRenderingContext2D;
x: number;
y: number;
color: string;
speed: number;
size: number;
sizeStep: number;
minSize: number;
maxSizeInteger: number;
maxSize: number;
delay: number;
counter: number;
counterStep: number;
isIdle: boolean;
isReverse: boolean;
isShimmer: boolean;
constructor({ canvas, context, x, y, color, speed, delay }: PixelProps) {
this.width = canvas.width;
this.height = canvas.height;
this.ctx = context;
this.x = x;
this.y = y;
this.color = color;
this.speed = this.getRandomValue(0.1, 0.9) * speed;
this.size = 0;
this.sizeStep = Math.random() * 0.4;
this.minSize = 0.5;
this.maxSizeInteger = 2;
this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);
this.delay = delay;
this.counter = 0;
this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;
this.isIdle = false;
this.isReverse = false;
this.isShimmer = false;
}
getRandomValue(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
draw(): void {
const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;
this.ctx.fillStyle = this.color;
this.ctx.fillRect(
this.x + centerOffset,
this.y + centerOffset,
this.size,
this.size
);
}
appear(): void {
this.isIdle = false;
if (this.counter <= this.delay) {
this.counter += this.counterStep;
return;
}
if (this.size >= this.maxSize) {
this.isShimmer = true;
}
if (this.isShimmer) {
this.shimmer();
} else {
this.size += this.sizeStep;
}
this.draw();
}
disappear(): void {
this.isShimmer = false;
this.counter = 0;
if (this.size <= 0) {
this.isIdle = true;
return;
} else {
this.size -= 0.1;
}
this.draw();
}
shimmer(): void {
if (this.size >= this.maxSize) {
this.isReverse = true;
} else if (this.size <= this.minSize) {
this.isReverse = false;
}
if (this.isReverse) {
this.size -= this.speed;
} else {
this.size += this.speed;
}
}
}
interface PixelCanvasProps {
colors?: string[];
gap?: number;
speed?: number;
noFocus?: boolean;
className?: string;
}
const PixelCanvas: React.FC<PixelCanvasProps> = ({
colors = ['#f8fafc', '#f1f5f9', '#cbd5e1'],
gap = 5,
speed = 35,
className = ""
}) => {
const [isHovered, setIsHovered] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const pixelsRef = useRef<Pixel[]>([]);
const animationFrameRef = useRef<number | null>(null);
const timePreviousRef = useRef<number>(0);
const timeInterval = 1000 / 60;
const init = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const rect = canvas.getBoundingClientRect();
const width = Math.floor(rect.width);
const height = Math.floor(rect.height);
canvas.width = width;
canvas.height = height;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const newPixels: Pixel[] = [];
for (let x = 0; x < width; x += gap) {
for (let y = 0; y < height; y += gap) {
const color = colors[Math.floor(Math.random() * colors.length)];
const delay = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : getDistanceToCanvasCenter(x, y, width, height);
newPixels.push(new Pixel({ canvas, context: ctx, x, y, color, speed: speed * 0.001, delay }));
}
}
pixelsRef.current = newPixels;
}, [colors, gap, speed]);
const getDistanceToCanvasCenter = (x: number, y: number, width: number, height: number): number => {
const dx = x - width / 2;
const dy = y - height / 2;
return Math.sqrt(dx * dx + dy * dy);
};
const animate = useCallback(() => {
const timeNow = performance.now();
const timePassed = timeNow - timePreviousRef.current;
if (timePassed < timeInterval) {
animationFrameRef.current = requestAnimationFrame(animate);
return;
}
timePreviousRef.current = timeNow - (timePassed % timeInterval);
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < pixelsRef.current.length; i++) {
pixelsRef.current[i].appear();
}
animationFrameRef.current = requestAnimationFrame(animate);
}, [timeInterval]);
useEffect(() => {
if (typeof window !== 'undefined') {
if (isHovered) {
init();
}
animationFrameRef.current = requestAnimationFrame(animate);
}
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [init, animate, isHovered]);
return <canvas ref={canvasRef}
className={cn("w-full h-full opacity-0 hover:opacity-30 transition-opacity hover:duration-75 duration-500", className)}
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
></canvas>;
};
export default PixelCanvas;Usage example:
import PixelCanvas from "./PixelCanvas";
import { Card } from "../ui/card";
const PixelDemo = () =>
<Card className="relative h-60 w-50 flex flex-col justify-center items-center py-4 px-2 border-opacity-50 hover:border-opacity-100 overflow-hidden bg-opacity-50 hover:bg-opacity-100">
<PixelCanvas className="absolute top-0 left-0"/>
<h3 className="text-lg font-semibold">hello</h3>
<p className="text-sm text-gray-500">world</p>
</Card>|
My approach may have a memory leak on re-render though. Lol. good luck fixing that. |
|
I don't know which part doesn't work, works fine on my projects, I just removed the hover effect take care |
|
Yup also not sure. I build it with React Vite, but I'm using Tauri, but hopefully this gives a headstart to anyone looking to implement it. I'll take a closer look on the weekend. |
|
I updated the code, I was seeing a bug with the active property that prevented the trigger of the effect the first time. |
Added an example for React using typescript