Skip to content

๐Ÿชต 4. ์บ”๋ฒ„์Šค ํ™œ์šฉํ•˜์—ฌ ๋ถˆ๊ทœ์น™ ํŒจํ„ด & ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ปค์„œ ์ œ์ž‘

D.Joung edited this page Dec 5, 2024 · 2 revisions

๊ฐœ์š”

  • Canvas ์— ๋ฐฐ๊ฒฝ์— ๋ถˆ๊ทœ์น™ํ•œ ์ง€๊ทธ์žฌ๊ทธ ํŒจํ„ด์„ ๊ทธ๋ฆฌ๊ณ , ๊ผฌ๋ฆฌ๊ฐ€ ๊ธธ๊ฒŒ ์ด์–ด์ง€๋Š” ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ปค์„œ๋ฅผ ์ œ์ž‘ํ•ด๋ด…๋‹ˆ๋‹ค.
  • ์ฐธ๊ณ  : ๊ฐˆํ‹ฑํฐ

ํŒจํ„ด ๋งŒ๋“ค๊ธฐ

  • pattenrs ํด๋”์— ์‚ฌ์šฉํ•  ์ด๋ฏธ์ง€ ์†Œ์Šค๋“ค์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

    • pattern-n : ๋žœ๋ค์œผ๋กœ ์ฐ์–ด๋‚ผ ํŒจํ„ด ํŒŒ์ธ 

    • particle-n : ํŒŒ์ธ  ์‚ฌ์ด์— ์ฐ์–ด๋‚ผ ํŒŒํ‹ฐํด ํŒŒ์ธ 

  • ์ƒ์ˆ˜

    export const SIZE = 55; // ์ด๋ฏธ์ง€ ํฌ๊ธฐ
    export const GAP = 40; // ์ด๋ฏธ์ง€์™€ ์ด๋ฏธ์ง€ ์‚ฌ์ด ๊ฐ„๊ฒฉ
    export const OFFSET = SIZE; // ํ™€์ˆ˜ ์ค„ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋‹น๊ธฐ๋Š” ๊ฐ„๊ฒฉ (์ฒด์ŠคํŒ ๋ชจ์–‘์œผ๋กœ ๋ฐฐ์น˜)
    export const PARTICLE_SIZE = SIZE / 3; // ํŒŒํ‹ฐํด ์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ
    
    export const RANDOM_POINT_RANGE_WIDTH = 20; // ์ด๋ฏธ์ง€ ๊ฐ€๋กœ ์ด๋™์— ๋ถ™์ผ ๋žœ๋ค ๋ณด์ •์น˜ ๋ฒ”์œ„ 
    export const RANDOM_POINT_RANGE_HEIGHT = 30; // ์ด๋ฏธ์ง€ ์„ธ๋กœ ์ด๋™์— ๋ถ™์ผ ๋žœ๋ค ๋ณด์ •์น˜ ๋ฒ”์œ„
  • ์ด๋ฏธ์ง€ ๋กœ๋“œ

    • ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๋“ค์„ ๋ฐฐ์—ด์— ๋‹ด์•„, promise ๊ฐ์ฒด๋กœ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ์ด ์™„๋ฃŒ๋˜๋ฉด drawํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

          const patterns = [
            pattern_0,
            pattern_1,
            pattern_2,
            pattern_3,
            pattern_4,
            pattern_5,
            pattern_6,
            pattern_7,
            particle_1,
          ]; // ํŒจํ„ด ๊ฒฝ๋กœ ๋ฐฐ์—ด๋กœ ์ €์žฅ
      
          const { pattern, particle } = getImageLists(patterns); // ์ด๋ฏธ์ง€๋“ค์„ pattern๊ณผ particle๋กœ ๋ถ„๋ฅ˜ํ•ด ๊ฐ ๋ฐฐ์—ด๋กœ ์ €์žฅํ•œ๋‹ค.
      
          Promise.all([
            Promise.all(pattern.map((imgData) => new Promise((res) => (imgData.img.onload = res)))),
            Promise.all(particle.map((imgData) => new Promise((res) => (imgData.img.onload = res)))),
          ])
            .then(() => {
              redraw(canvas, cursorCanvas, ctx, pattern, particle); 
              // ์ด๋ฏธ์ง€ ๋กœ๋”ฉ์ด ๋์•„๋ฉด redrawํ•จ์ˆ˜๋กœ ํŒจํ„ด์„ ๋‹ค์‹œ ๊ทธ๋ ค์ค€๋‹ค.
            })
            .catch((err) => {
              console.error(err);
            });
  • resize ์ด๋ฒคํŠธ ๋ถ€์ฐฉ

    • resize ๋ฅผ ๋ถ™์—ฌ ํ™”๋ฉด ํฌ๊ธฐ๊ฐ€ ๋ณ€ํ•  ๋•Œ๋งˆ๋‹ค ํŒจํ„ด์„ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
        window.addEventListener('resize', handleResize);
        return () => {
          window.removeEventListener('resize', handleResize);
        };
  • redraw ํ•จ์ˆ˜

    • pattern๊ณผ particle ์ด๋ฏธ์ง€๋ฅผ ๋žœ๋คํ•˜๊ฒŒ ํ™”๋ฉด์— ๊ทธ๋ ค์ค๋‹ˆ๋‹ค.
      1. ์ฒด์ŠคํŒ ๋ชจ์–‘์œผ๋กœ ๊ฐ ํŒจํ„ด ์ด๋ฏธ์ง€๋ฅผ ์ฐ์„ ์ขŒํ‘œ๋ฅผ ๊ตฌํ•ฉ๋‹ˆ๋‹ค.
      2. ํ•ด๋‹น ์ขŒํ‘œ์— ์ด๋ฏธ์ง€๋ฅผ ์ฐ์Šต๋‹ˆ๋‹ค.
      3. ๋žœ๋ค ๋ณด์ •์น˜ ๋งŒํผ ์ƒํ•˜์ขŒ์šฐ๋กœ ์ด๋™์‹œํ‚ต๋‹ˆ๋‹ค.
    const redraw = (
      canvas: HTMLCanvasElement,
      ctx: CanvasRenderingContext2D,
      pattern: { img: HTMLImageElement; type: string }[],
      particle: { img: HTMLImageElement; type: string }[],
    ) => {
      canvas.width = canvas.offsetWidth; // ์บ”๋ฒ„์Šค ํฌ๊ธฐ๋ฅผ ํ™”๋ฉด ํฌ๊ธฐ๋กœ ์ง€์ •
      canvas.height = canvas.offsetHeight; 
    
      ctx.clearRect(0, 0, canvas.width, canvas.height); // ์บ”๋ฒ„์Šค ์ดˆ๊ธฐํ™”
      ctx.globalAlpha = 0.3; // ํŒจํ„ด ํˆฌ๋ช…๋„ ์ง€์ •
    
      const rows = Math.ceil(canvas.height / (SIZE + GAP)); // ์„ธ๋กœ์ค„ ํŒจํ„ด ์ˆ˜
      const cols = Math.ceil(canvas.width / (SIZE + GAP)); // ๊ฐ€๋กœ์ค„ ํŒจํ„ด ์ˆ˜
    
      for (let row = 0; row < rows; row++) {
        for (let col = -1; col < cols; col++) {
          const patternX = col * (SIZE + GAP) + (row % 2 === 0 ? 0 : OFFSET);
          const patternY = row * (SIZE + GAP);
    
          const random1 = Math.random() * 10 - 5; //์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ ๋ณด์ •์น˜
    
          ctx.beginPath();
          ctx.save();
          ctx.translate(patternX + SIZE / 2, patternY + SIZE / 2); // ์›์  ์ด๋™
          ctx.rotate(Math.random() * 2 * Math.PI); // ๋žœ๋คํ•œ ๊ฐ๋„๋กœ ํšŒ์ „
          ctx.drawImage( // ์ด๋ฏธ์ง€ ๋“œ๋กœ์ž‰
            pattern[Math.floor(Math.random() * pattern.length)].img, 
            -SIZE / 2 + randomizeWidth(),
            -SIZE / 2 + randomizeHeight(),
            SIZE + random1,
            SIZE + random1,
          );
          ctx.restore();
    
          const random2 = Math.random() * 10 - 5; // ํŒŒํ‹ฐํด ์‚ฌ์ด์ฆˆ ๋ณด์ •์น˜
          const particleX = patternX + SIZE;
          const particleY = patternY + SIZE + (GAP - PARTICLE_SIZE) / 2 + randomizeWidth();
          ctx.save();
          ctx.translate(particleX + PARTICLE_SIZE / 2, particleY + PARTICLE_SIZE / 2);
          ctx.rotate(Math.random() * 2 * Math.PI);
          ctx.drawImage(
            particle[Math.floor(Math.random() * particle.length)].img,
            -PARTICLE_SIZE / 2 + randomizeWidth(),
            -PARTICLE_SIZE / 2,
            PARTICLE_SIZE + random2,
            PARTICLE_SIZE + random2,
          );
          ctx.restore();
          ctx.fill();
        }
      }
    };

ํŒจํ„ด ์ด๋ฏธ์ง€ ์ถ”์ถœํ•˜๊ธฐ

  • resize() ํ•จ์ˆ˜ ํ˜ธ์ถœ ํ›„ ์•„๋ž˜ ๋งํฌ๋ฅผ ๋ง๋ถ™์ž…๋‹ˆ๋‹ค.
  • a ํƒœ๊ทธ๋ฅผ ์ž„์‹œ๋กœ ๋งŒ๋“ค์–ด ๋‹ค์šด๋กœ๋“œ URL์„ ์„ค์ •ํ•œ ํ›„, click์„ ํŠธ๋ฆฌ๊ฑฐ ํ•ด ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์šด๋ฐ›์Šต๋‹ˆ๋‹ค.
const dataURL = canvas.toDataURL("image/png");

const link = document.createElement('a');
link.href = dataURL;
link.download = 'canvas-image.png'; // ํŒŒ์ผ๋ช… ์„ค์ •
link.click();
  • ํŒจํ„ด ์กฐ๊ฐ๋“ค๊ณผ ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€ :

์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ปค์„œ ๋งŒ๋“ค๊ธฐ

  • ์ƒ์ˆ˜
export const CURSOR_WIDTH = 20; // ์ปค์„œ ๋“œ๋กœ์ž‰ ๊ตต๊ธฐ
export const CURSOR_LENGTH = 7; // ์ปค์„œ ๊ผฌ๋ฆฌ ๊ธธ์ด
export const DELETE_INTERVAL = 30; // ๊ผฌ๋ฆฌ ์‚ญ์ œ ์ฃผ๊ธฐ (ms). 30ms๋งˆ๋‹ค ๊ผฌ๋ฆฌ ๋ถ€๋ถ„์˜ ์ขŒํ‘œ ํ•˜๋‚˜๋ฅผ ์ง€์šด๋‹ค.
  • ๋งˆ์šฐ์Šค ํ•ธ๋“ค๋Ÿฌ
    • mousemove ์ด๋ฒคํŠธ์—์„œ ์ƒˆ ์ขŒํ‘œ๊ฐ€ ์ƒ˜ํ”Œ๋ง ๋  ๋•Œ๋งˆ๋‹ค pointsRef ์ขŒํ‘œ ๋ฐฐ์—ด์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  const handleMouseMove = (e: MouseEvent<HTMLCanvasElement>) => {
    const { canvas } = getCanvasContext(cursorCanvasRef);
    const point = getDrawPoint(e, canvas);
    pointsRef.current.push(point);
  };

  const handleMouseLeave = () => {
    const { canvas, ctx } = getCanvasContext(cursorCanvasRef);
    pointsRef.current.length = 0;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  };
  • Cursor ์• ๋‹ˆ๋ฉ”์ด์…˜ (useEffect + requsetAnimationFrame ์‚ฌ์šฉ)
    • pointsRef ์ขŒํ‘œ ๋ฐฐ์—ด์˜ ์ขŒํ‘œ๋“ค์ด ํ˜„์žฌ ๊ทธ๋ ค์งˆ ์„ ์ด ๋ฉ๋‹ˆ๋‹ค.
    • performance.now() ๋ฉ”์†Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด 16ms๋งˆ๋‹ค ๋“œ๋กœ์ž‰ํ•˜๊ณ , DELETE_INTERVAL ms๋งˆ๋‹ค ๊ผฌ๋ฆฌ ๋ ์ขŒํ‘œ๋ฅผ ์‚ญ์ œํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
useEffect(() => {
    const { canvas, ctx } = getCanvasContext(cursorCanvasRef);

    const drawAni = () => {
      const now = performance.now();
			
			// ๊ผฌ๋ฆฌ ๋“œ๋กœ์ž‰ ๋ถ€
      if (now - drawTimeRef.current > 16 && pointsRef.current.length > 1) {
        if (pointsRef.current.length > CURSOR_LENGTH) pointsRef.current = pointsRef.current.slice(-CURSOR_LENGTH);
        drawTimeRef.current = now;

        ctx.clearRect(0, 0, canvas.width, canvas.height);

        ctx.beginPath();
        ctx.globalAlpha = 0.3;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.lineWidth = CURSOR_WIDTH;
        ctx.strokeStyle = 'white';

        const points = pointsRef.current;
        points.forEach((point, idx) => {
          if (idx === 0) ctx.moveTo(point.x, point.y);
          else if (idx < points.length - 1) {
            const midX = (points[idx + 1].x + point.x) / 2;
            const midY = (points[idx + 1].y + point.y) / 2;
            ctx.quadraticCurveTo(point.x, point.y, midX, midY); //2์ฐจ ์Šคํ”Œ๋ผ์ธ์œผ๋กœ ๋“œ๋กœ์ž‰
          } else {
            ctx.lineTo(point.x, point.y);
            ctx.stroke();
          }
        });
      }

			// ๊ผฌ๋ฆฌ ์ขŒํ‘œ ์‚ญ์ œ ๋ถ€
      if (now - deleteTimeRef.current > DELETE_INTERVAL && pointsRef.current.length > 1) {
        pointsRef.current.shift();
        deleteTimeRef.current = now;
      }

      requestAnimationFrame(drawAni);
    };

    cursorAnimation.current = requestAnimationFrame(drawAni);

    return () => {
      if (cursorAnimation.current) cancelAnimationFrame(cursorAnimation.current);
    };
  }, []);

ํŒจํ„ด & ์ปค์„œ ์ œ์ž‘ ์ „์ฒด ์ฝ”๋“œ

  • ํŒจํ„ด๊ณผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ปค์„œ๋ฅผ ๋งŒ๋“  ์ „์ฒด ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.
  • ํŒจํ„ด์˜ ๊ฒฝ์šฐ, ์บ”๋ฒ„์Šค์—์„œ ์ถ”์ถœํ•œ ์ด๋ฏธ์ง€๋ฅผ css background์— ์‚ฝ์ž…ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด ๋กœ์ง์—์„œ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
import { useEffect, useRef, MouseEvent } from 'react';
import { Point } from '@troublepainter/core';
import particle_1 from '@/assets/patterns/particle-1.png';
import pattern_0 from '@/assets/patterns/pattern-0.png';
import pattern_1 from '@/assets/patterns/pattern-1.png';
import pattern_2 from '@/assets/patterns/pattern-2.png';
import pattern_3 from '@/assets/patterns/pattern-3.png';
import pattern_4 from '@/assets/patterns/pattern-4.png';
import pattern_5 from '@/assets/patterns/pattern-5.png';
import pattern_6 from '@/assets/patterns/pattern-6.png';
import pattern_7 from '@/assets/patterns/pattern-7.png';
import {
  CURSOR_LENGTH,
  CURSOR_WIDTH,
  DELETE_INTERVAL,
  GAP,
  OFFSET,
  PARTICLE_SIZE,
  RANDOM_POINT_RANGE_HEIGHT,
  RANDOM_POINT_RANGE_WIDTH,
  SIZE,
} from '@/constants/backgroundConstants';
import { getCanvasContext } from '@/utils/getCanvasContext';
import { getDrawPoint } from '@/utils/getDrawPoint';

type ImgType = 'particle' | 'pattern';

interface PatternData {
  img: HTMLImageElement;
  type: ImgType;
}

interface patterns {
  pattern: PatternData[];
  particle: PatternData[];
}

const randomizeWidth = () => Math.random() * RANDOM_POINT_RANGE_WIDTH - RANDOM_POINT_RANGE_WIDTH / 2;
const randomizeHeight = () => Math.random() * RANDOM_POINT_RANGE_HEIGHT - RANDOM_POINT_RANGE_HEIGHT / 2;

const redraw = (
  canvas: HTMLCanvasElement,
  cursorCanvas: HTMLCanvasElement,
  ctx: CanvasRenderingContext2D,
  pattern: { img: HTMLImageElement; type: string }[],
  particle: { img: HTMLImageElement; type: string }[],
) => {
  canvas.width = canvas.offsetWidth;
  canvas.height = canvas.offsetHeight;

  cursorCanvas.width = canvas.offsetWidth;
  cursorCanvas.height = canvas.offsetHeight;

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.globalAlpha = 0.3;

  const rows = Math.ceil(canvas.height / (SIZE + GAP));
  const cols = Math.ceil(canvas.width / (SIZE + GAP));

  for (let row = 0; row < rows; row++) {
    for (let col = -1; col < cols; col++) {
      const patternX = col * (SIZE + GAP) + (row % 2 === 0 ? 0 : OFFSET);
      const patternY = row * (SIZE + GAP);

      const random1 = Math.random() * 10 - 5;

      ctx.beginPath();
      ctx.save();
      ctx.translate(patternX + SIZE / 2, patternY + SIZE / 2);
      ctx.rotate(Math.random() * 2 * Math.PI);
      ctx.drawImage(
        pattern[Math.floor(Math.random() * pattern.length)].img,
        -SIZE / 2 + randomizeWidth(),
        -SIZE / 2 + randomizeHeight(),
        SIZE + random1,
        SIZE + random1,
      );
      ctx.restore();

      const random2 = Math.random() * 10 - 5;
      const particleX = patternX + SIZE;
      const particleY = patternY + SIZE + (GAP - PARTICLE_SIZE) / 2 + randomizeWidth();
      ctx.save();
      ctx.translate(particleX + PARTICLE_SIZE / 2, particleY + PARTICLE_SIZE / 2);
      ctx.rotate(Math.random() * 2 * Math.PI);
      ctx.drawImage(
        particle[Math.floor(Math.random() * particle.length)].img,
        -PARTICLE_SIZE / 2 + randomizeWidth(),
        -PARTICLE_SIZE / 2,
        PARTICLE_SIZE + random2,
        PARTICLE_SIZE + random2,
      );
      ctx.restore();
      ctx.fill();
    }
  }
};

/*
const getPatternType = (src: string): ImgType => {
  const paths = src.split('/');
  const type = 'pattern';
  if (!(type === 'pattern' || type === 'particle')) throw new Error('ํŒŒ์ธ  ํŒŒ์ผ๋ช…์ด ์ž˜๋ชป๋˜์—ˆ์Œ.');
  debugger;
  return type;
};
*/

const getImageLists = (patterns: string[]): patterns => {
  const lists: patterns = {
    pattern: [],
    particle: [],
  };

  patterns.forEach((src, idx) => {
    const img = new Image();
    img.src = src;
    const type = idx === patterns.length - 1 ? 'particle' : 'pattern';

    //const type = getPatternType(/*src*/);

    lists[type as keyof patterns].push({ img, type });
  });

  return lists;
};

const Background = ({ className }: { className: string }) => {
  const bgCanvasRef = useRef<HTMLCanvasElement>(null);
  const cursorCanvasRef = useRef<HTMLCanvasElement>(null);

  const cursorAnimation = useRef<number>();

  const pointsRef = useRef<Point[]>([]);

  const drawTimeRef = useRef(performance.now());
  const deleteTimeRef = useRef(performance.now());

  // ํŒจํ„ด ์ฐ๊ธฐ
  useEffect(() => {
    const { canvas, ctx } = getCanvasContext(bgCanvasRef);
    const { canvas: cursorCanvas } = getCanvasContext(cursorCanvasRef);

    const patterns = [
      pattern_0,
      pattern_1,
      pattern_2,
      pattern_3,
      pattern_4,
      pattern_5,
      pattern_6,
      pattern_7,
      particle_1,
    ];
    /* 
    const patterns_vite: Record<string, { default: string }> = import.meta.glob('@/assets/patterns/*.png', {
      eager: true,
    });
    const patterns = Object.values(patterns_vite).map((module) => module.default);
*/
    const { pattern, particle } = getImageLists(patterns);

    Promise.all([
      Promise.all(pattern.map((imgData) => new Promise((res) => (imgData.img.onload = res)))),
      Promise.all(particle.map((imgData) => new Promise((res) => (imgData.img.onload = res)))),
    ])
      .then(() => {
        redraw(canvas, cursorCanvas, ctx, pattern, particle);
      })
      .catch((err) => {
        console.error(err);
      });

    const handleResize = () => {
      redraw(canvas, cursorCanvas, ctx, pattern, particle);
    };

    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  // ์ปค์„œ ๊ทธ๋ฆฌ๊ธฐ
  useEffect(() => {
    const { canvas, ctx } = getCanvasContext(cursorCanvasRef);

    const drawAni = () => {
      const now = performance.now();

      if (now - drawTimeRef.current > 16 && pointsRef.current.length > 1) {
        if (pointsRef.current.length > CURSOR_LENGTH) pointsRef.current = pointsRef.current.slice(-CURSOR_LENGTH);
        drawTimeRef.current = now;

        ctx.clearRect(0, 0, canvas.width, canvas.height);

        ctx.beginPath();
        ctx.globalAlpha = 0.3;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.lineWidth = CURSOR_WIDTH;
        ctx.strokeStyle = 'white';

        const points = pointsRef.current;
        points.forEach((point, idx) => {
          if (idx === 0) ctx.moveTo(point.x, point.y);
          else if (idx < points.length - 1) {
            const midX = (points[idx + 1].x + point.x) / 2;
            const midY = (points[idx + 1].y + point.y) / 2;
            ctx.quadraticCurveTo(point.x, point.y, midX, midY);
          } else {
            ctx.lineTo(point.x, point.y);
            ctx.stroke();
          }
        });
      }

      if (now - deleteTimeRef.current > DELETE_INTERVAL && pointsRef.current.length > 1) {
        pointsRef.current.shift();
        deleteTimeRef.current = now;
      }

      requestAnimationFrame(drawAni);
    };

    cursorAnimation.current = requestAnimationFrame(drawAni);

    return () => {
      if (cursorAnimation.current) cancelAnimationFrame(cursorAnimation.current);
    };
  }, []);

  const handleMouseMove = (e: MouseEvent<HTMLCanvasElement>) => {
    const { canvas } = getCanvasContext(cursorCanvasRef);
    const point = getDrawPoint(e, canvas);
    pointsRef.current.push(point);
  };

  const handleMouseLeave = () => {
    const { canvas, ctx } = getCanvasContext(cursorCanvasRef);
    pointsRef.current.length = 0;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  };

  return (
    <div className={className}>
      <canvas ref={bgCanvasRef} className="absolute h-full w-full" />
      <canvas
        ref={cursorCanvasRef}
        className="absolute h-full w-full cursor-none"
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
      />
    </div>
  );
};

export default Background;

background.tsx ์ตœ์ข… ์ฝ”๋“œ

  • ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ปค์„œ๋งŒ ๊ตฌํ˜„๋œ ์ตœ์ข… ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ ์™„๋ฃŒํ•˜์˜€์Šต๋‹ˆ๋‹ค.
import { useEffect, useRef, MouseEvent } from 'react';
import { Point } from '@troublepainter/core';
import { CURSOR_LENGTH, CURSOR_WIDTH, DELETE_INTERVAL } from '@/constants/backgroundConstants';
import { getCanvasContext } from '@/utils/getCanvasContext';
import { getDrawPoint } from '@/utils/getDrawPoint';

const Background = ({ className }: { className: string }) => {
  const cursorCanvasRef = useRef<HTMLCanvasElement>(null);
  const cursorAnimation = useRef<number>();

  const pointsRef = useRef<Point[]>([]);

  const drawTimeRef = useRef(performance.now());
  const deleteTimeRef = useRef(performance.now());

  const currentTimestamp = useRef(performance.now());
  const lastTimestamp = useRef(performance.now());

  // ์ปค์„œ ๊ทธ๋ฆฌ๊ธฐ
  useEffect(() => {
    const { canvas, ctx } = getCanvasContext(cursorCanvasRef);

    const handleResize = () => {
      canvas.width = canvas.offsetWidth;
      canvas.height = canvas.offsetHeight;
    };

    handleResize();
    window.addEventListener('resize', handleResize);

    const drawAni = () => {
      const now = performance.now();

      if (now - drawTimeRef.current > 16 && pointsRef.current.length > 1) {
        if (pointsRef.current.length > CURSOR_LENGTH) pointsRef.current = pointsRef.current.slice(-CURSOR_LENGTH);
        drawTimeRef.current = now;

        ctx.clearRect(0, 0, canvas.width, canvas.height);

        ctx.beginPath();
        ctx.globalAlpha = 0.3;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.lineWidth = CURSOR_WIDTH;
        ctx.strokeStyle = 'white';

        const points = pointsRef.current;
        points.forEach((point, idx) => {
          if (idx === 0) ctx.moveTo(point.x, point.y);
          else if (idx < points.length - 1) {
            const midX = (points[idx + 1].x + point.x) / 2;
            const midY = (points[idx + 1].y + point.y) / 2;
            ctx.quadraticCurveTo(point.x, point.y, midX, midY);
          } else {
            ctx.lineTo(point.x, point.y);
            ctx.stroke();
          }
        });
      }

      if (now - deleteTimeRef.current > DELETE_INTERVAL && pointsRef.current.length > 1) {
        pointsRef.current.shift();
        deleteTimeRef.current = now;
      }

      requestAnimationFrame(drawAni);
    };

    cursorAnimation.current = requestAnimationFrame(drawAni);

    return () => {
      if (cursorAnimation.current) cancelAnimationFrame(cursorAnimation.current);
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  const handleMouseMove = (e: MouseEvent<HTMLCanvasElement>) => {
    currentTimestamp.current = performance.now();
    if (currentTimestamp.current - lastTimestamp.current < 16) return;
    lastTimestamp.current = currentTimestamp.current;

    const { canvas } = getCanvasContext(cursorCanvasRef);
    const point = getDrawPoint(e, canvas);
    pointsRef.current.push(point);
  };

  const handleMouseLeave = () => {
    const { canvas, ctx } = getCanvasContext(cursorCanvasRef);
    pointsRef.current.length = 0;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  };

  return (
    <div className={className}>
      <canvas
        ref={cursorCanvasRef}
        className="absolute h-full w-full cursor-none"
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
      />
    </div>
  );
};

export default Background;

๐Ÿ˜Ž ์›จ๋ฒ ๋ฒ ๋ฒ ๋ฒฑ

๐Ÿ‘ฎ๐Ÿป ํŒ€ ๊ทœ์น™

๐Ÿ’ป ํ”„๋กœ์ ํŠธ

๐Ÿชต ์›จ๋ฒ ๋ฒฑ ๊ธฐ์ˆ ๋กœ๊ทธ

๐Ÿช„ ๋ฐ๋ชจ ๊ณต์œ 

๐Ÿ”„ ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๋ก

๐Ÿ“— ํšŒ์˜๋ก

Clone this wiki locally