-
Notifications
You must be signed in to change notification settings - Fork 7
๐ชต 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 ์ด๋ฏธ์ง๋ฅผ ๋๋คํ๊ฒ ํ๋ฉด์ ๊ทธ๋ ค์ค๋๋ค.
- ์ฒด์คํ ๋ชจ์์ผ๋ก ๊ฐ ํจํด ์ด๋ฏธ์ง๋ฅผ ์ฐ์ ์ขํ๋ฅผ ๊ตฌํฉ๋๋ค.
- ํด๋น ์ขํ์ ์ด๋ฏธ์ง๋ฅผ ์ฐ์ต๋๋ค.
- ๋๋ค ๋ณด์ ์น ๋งํผ ์ํ์ข์ฐ๋ก ์ด๋์ํต๋๋ค.
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(); } } };
- pattern๊ณผ particle ์ด๋ฏธ์ง๋ฅผ ๋๋คํ๊ฒ ํ๋ฉด์ ๊ทธ๋ ค์ค๋๋ค.
- 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
์ขํ ๋ฐฐ์ด์ ์ ์ฅํฉ๋๋ค.
- mousemove ์ด๋ฒคํธ์์ ์ ์ขํ๊ฐ ์ํ๋ง ๋ ๋๋ง๋ค
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;
- ์ปค์คํฐ๋ง์ด์ง ์ปค์๋ง ๊ตฌํ๋ ์ต์ข ์ฝ๋๋ก ๊ตฌํ ์๋ฃํ์์ต๋๋ค.
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;
- 1. ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ๋ฐ ํ๋ก์ ํธ ๋ฌธ์ํ
- 2. ์ค์๊ฐ ํต์
- 3. ์ธํ๋ผ ๋ฐ CI/CD
- 4. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด Canvas ๊ตฌํํ๊ธฐ
- 5. ์บ๋ฒ์ค ๋๊ธฐํ๋ฅผ ์ํ ์์ CRDT ๊ตฌํ๊ธฐ
-
6. ์ปดํฌ๋ํธ ํจํด๋ถํฐ ์น์์ผ๊น์ง, ํจ์จ์ ์ธ FE ์ค๊ณ
- ์ข์ ์ปดํฌ๋ํธ๋ ๋ฌด์์ธ๊ฐ? + Headless Pattern
- ํจ์จ์ ์ธ UI ์ปดํฌ๋ํธ ์คํ์ผ๋ง: Tailwind CSS + cn.ts
- Tailwind CSS๋ก ๋์์ธ ์์คํ ๋ฐ UI ์ปดํฌ๋ํธ ์ธํ
- ์น์์ผ ํด๋ผ์ด์ธํธ ๊ตฌํ๊ธฐ: React ํ๊ฒฝ์์ ํจ์จ์ ์ธ ์น์์ผ ์ํคํ ์ฒ
- ์น์์ผ ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ถ์ ๋ฐ ๊ณต์
- 7. ํธ๋ฌ๋ธ ์ํ ๋ฐ ์ฑ๋ฅ/UX ๊ฐ์
- 1์ฃผ์ฐจ ๊ธฐ์ ๊ณต์
- 2์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 3์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 4์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 5์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- WEEK 06 ์ฃผ๊ฐ ๊ณํ
- WEEK 06 ๋ฐ์ผ๋ฆฌ ์คํฌ๋ผ
- WEEK 06 ์ฃผ๊ฐ ํ๊ณ