Skip to content

Reactjs Example#1

Open
MiguelArmendariz wants to merge 2 commits into
hexagoncircle:mainfrom
MiguelArmendariz:React-Example
Open

Reactjs Example#1
MiguelArmendariz wants to merge 2 commits into
hexagoncircle:mainfrom
MiguelArmendariz:React-Example

Conversation

@MiguelArmendariz
Copy link
Copy Markdown

@MiguelArmendariz MiguelArmendariz commented Feb 16, 2025

Added an example for React using typescript

@amadevss
Copy link
Copy Markdown

noted

@amadevss
Copy link
Copy Markdown

This pull request introduces a new React component called PixelCanvas, which is a TypeScript-based, TailwindCSS-compatible version of the original Pixel Canvas Component. The component includes several customizable properties and supports continuous animation via an active prop.

Key changes include:

Documentation:

  • Added a new README.MD file for the PixelCanvas component, detailing its properties, usage, and an example implementation.

Component Implementation:

  • Created the PixelCanvas component in pixel-canvas.tsx, which includes properties such as colors, gap, speed, noFocus, active, style, and className.
  • Implemented the Pixel class within pixel-canvas.tsx to handle the drawing and animation of individual pixels on the canvas.
  • Added animation logic to support both appear and disappear effects, with handling for active and noFocus props.

These changes provide a detailed guide and a robust implementation for using the new PixelCanvas component in React applications.

'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;

Copy link
Copy Markdown

@SaadBazaz SaadBazaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@SaadBazaz
Copy link
Copy Markdown

My approach may have a memory leak on re-render though. Lol. good luck fixing that.

@amadevss
Copy link
Copy Markdown

I don't know which part doesn't work, works fine on my projects, I just removed the hover effect take care

@MiguelArmendariz
Copy link
Copy Markdown
Author

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.

@MiguelArmendariz
Copy link
Copy Markdown
Author

MiguelArmendariz commented Feb 26, 2025

I updated the code, I was seeing a bug with the active property that prevented the trigger of the effect the first time.
Unsure if this was causing the issue discussed above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants