-
Notifications
You must be signed in to change notification settings - Fork 7
๐ชต 4. Canvas Undo, Redo ๊ตฌํ โ path ๋จ์ ๊ด๋ฆฌ
D.Joung edited this page Dec 5, 2024
·
1 revision
- ์์ ๋ CRDT ์์ ์ ํฉ์ณ์ ธ ์ ์ฉ๋์์ต๋๋ค. (์ต์ข : stroke ๋จ์๋ก ๊ด๋ฆฌํ๊ธฐ๋ก ํจ)
- ์๋ ์ฝ๋๋ undo / redo ํ๋กํ ํ์ ์ผ๋ก ์ค์ ์ฌ์ฉ๋์ง ์์์ต๋๋ค.
- ์๋๋ก ์ด์ด์ง๋ ์ฝ๋๋ ์์ง CRDT๊ฐ ๊ตฌํ ์ค์ธ ๋จ๊ณ์์, Undo & Redo์ ํ๋กํ ํ์ ์ ์ ๊ตฌํ ํด๋ณธ ๋ด์ฉ์ ๋๋ค.
- stroke id๋ฅผ key๊ฐ์ผ๋ก, stroke์ ํด๋นํ๋
peerId
,timestamp
,{x,y}[]
,width
,color
๊ฐ ์ ์ฅ ๋์ด์์ต๋๋ค.
Stroke = {
points: {x,y}[];
style: {color, width};
};
LWWRegister = {
peerId,
state: {
peerId,
timeStamp,
value: Stroke
}
}
LWWMap = {
strokeId: LWWRegister
}[]
-
๋์ ๊ทธ๋ฆฌ๊ธฐ
๋ง undo, redo ๋์ด์ผ ํฉ๋๋ค.. (์๋ ๊ฒ์ ๊ทธ๋๋ก ๋จ์์์ด์ผ ํจ) - mousemove ํธ๋ฆฌ๊ฑฐ ์๋ง๋ค ์๋ก์ด stroke, ์ฆ ์๋ก์ด LWWRegister ๊ฐ์ฒด๊ฐ ์์ฑ ๋ฉ๋๋ค.
- onMouseMove(๋ง์ฐ์ค ์์ง์ผ ๋)
- ๋ฐ์ดํฐ ๋ณด๋ด๊ธฐ (key, x,y,color,width)
- undo ๋ฐฐ์ด์ ๋ค์ด๊ฐ key ๊ฐ.
- ๋ฐฐ์ด์ {x,y}[] push
- ๋ฐ์ดํฐ ๋ณด๋ด๊ธฐ (key, x,y,color,width)
- onMouseLeave?(๋ง์ฐ์ค ๋์ ๋)
- undo ๋ฐฐ์ด์ {key, {x,y}[], color, width} push
- undo ํ์๋
- undo ๋ฐฐ์ด์์ popํด์, key์ ํด๋นํ๋ {x,y}๋ฅผ null๋ก ๋ฐ๊พธ๊ธฐ
- redo ๋ฐฐ์ด์ {key, {x,y}[], color, width} push
- redo ํ์ ๋
- redo ๋ฐฐ์ด์์ popํด์, key์ ํด๋นํ๋ {x, y}[]๋ฅผ color, width๋ก ๋ฐ๊พธ๊ธฐ
- ๋ชจ๋ Path๋ pathStack(Map)์ ์ ์ฅ๋์ด ๊ด๋ฆฌ๋ฉ๋๋ค.
- ๋ชจ๋ Path๋ pathID์ strokeID๋ฅผ ๊ฐ์ต๋๋ค.
- UNDO, REDO๊ฐ ๋ฐ์ํ ๋๋ง๋ค, ์บ๋ฒ์ค ์ด๊ธฐํ ํ pathStack์ 0๋ฒ์งธ ๋ฐ์ดํฐ๋ถํฐ ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
- UNDO : ํ์ฌ strokeID๋ฅผ ๊ฐ์ง path๋ฅผ pathStack์์ ์ ๋ถ ์ญ์ ํฉ๋๋ค.
- REDO : ๋ค์ strokeID๋ฅผ ๊ฐ์ง path๋ฅผ pathStack์ ์ ๋ถ ๋ณต๊ตฌ์ํต๋๋ค.
- ๊ด๋ฆฌ ๋จ์
- path : move ์ด๋ฒคํธ๋ก ์ ์ขํ๊ฐ ์ถ๊ฐ๋ ๋ ๋ง๋ค pathStack์ ์ ์ฅ๋ฉ๋๋ค.
- stroke : undo / redo์ ๊ธฐ๋ณธ ๋จ์. up ๋๋ leave ์ด๋ฒคํธ ๋ฐ์ ์ strokeID๊ฐ myStrokeIDStack์ ์ ์ฅ๋ฉ๋๋ค.
- pathStack : map ๊ฐ์ฒด. <pathID, PathData> ์์ผ๋ก path ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํฉ๋๋ค.
- myStrokeIDStack : ์์ ์ง์ผ์ array ์ ์ฅํฉ๋๋ค.
- myStrokeIDIndex : ํ์ฌ stroke id๋ฅผ ๊ฐ๋ฆฌํต๋๋ค.
- deletedPath : map ๊ฐ์ฒด. pop๋ pathData๋ฅผ ์ ์ฅํฉ๋๋ค. <pathID, value> ์์ผ๋ก ์ ์ฅํฉ๋๋ค.
- PathData ๊ฐ์ฒด
- userID : ์ฌ์ฉ์๋ฅผ ๊ตฌ๋ถํ๋ ๊ณ ์ ํ ID ์ ๋๋ค.
- strokeID : Stroke๋ฅผ ๊ตฌ๋ถํ๋ ๊ณ ์ ํ ID ์ ๋๋ค.
- path : ๋๋ก์ ๊ฒฝ๋ก๋ฅผ ์ ์ฅํ๊ณ ์๋ path2D ๊ฐ์ฒด์ ๋๋ค.
- style : ๊ฒฝ๋ก์ ์์, ๋๊ป, ํ์ ์ ๋ณด๋ฅผ ์ ์ฅํฉ๋๋ค.
- pushPath : ์ path ๋ฐ์ดํฐ๋ฅผ ์ธ์๋ก ๋ฐ์ pathStack์ ์ถ๊ฐํฉ๋๋ค.
- popPath : pathID๋ฅผ ์ธ์๋ก ๋ฐ์ ํด๋นํ๋ path์ pathData๋ฅผ pathStack์์ ์ญ์ ํฉ๋๋ค. (null ๋ฐฐ์ )
- restorePath : ๋ณต๊ตฌํ pathID์ PathData๋ฅผ ์ธ์๋ก ๋ฐ์ pathStack์์ ๋ณต๊ตฌํฉ๋๋ค. (null โ PathData)
- pushMyStroke : strokeID ๋ฅผ ์ธ์๋ก ๋ฐ์ myStrokeIDStack์ ์ ์ฅํฉ๋๋ค.
- popStroke : ํ์ฌ strokeID๋ฅผ ๊ฐ์ง path์ pathData๋ฅผ pathStack์์ ๋ชจ๋ ์ญ์ ํฉ๋๋ค.
- restoreStroke : ๋ค์ strokeID๋ฅผ ๊ฐ์ง path์ pathData๋ฅผ pathStack์์ ๋ชจ๋ ๋ณต๊ตฌํฉ๋๋ค.
- undo : undo ๊ธฐ๋ฅ ์คํ ํ ์บ๋ฒ์ค ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
- redo : redo ๊ธฐ๋ฅ ์คํ ํ ์บ๋ฒ์ค ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
- redo, undo ์์๋ ๋๋ก์ ์ฐ์ ์์๊ฐ ์ง์ผ์ง๋ ๊ฒ์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
- PathData์ ํฌํจ๋ path์ ์คํ์ผ์
๋๋ค.
- color : ์์ (16์ง์ ํ๊ธฐ๋ฒ)
- lineWidth : ์ ๋๊ป
- type
- path : ์ ์ ๊ทธ๋ฆด ๊ฒฝ์ฐ
- shape : ๋ชจ์์ ๊ทธ๋ฆด ๊ฒฝ์ฐ (rect, arc ๋ฑ์ผ๋ก ๋ํ์ ๊ทธ๋ ธ์ ๊ฒฝ์ฐ)
export interface PathStyle {
color: string;
lineWidth: number;
type: 'path' | 'shape';
}
- Path๋ฅผ ๊ทธ๋ฆฌ๋๋ฐ ํ์ํ ๋ฐ์ดํฐ๋ค์ ๊ฐ์ต๋๋ค.
- path : ๊ฒฝ๋ก๊ฐ ์ ์ฅ๋ path2D ๊ฐ์ฒด
- userID : ๊ทธ๋ฆฐ ํ๋ ์ด์ด์ ID
- strokeID : path๊ฐ ์์๋ ์ ์ ID.
- style : ์ ์ style ๊ฐ์ฒด๋ฅผ ์ ์ฅ
class PathData {
path: Path2D;
userID: number;
strokeID: string;
style: PathStyle;
constructor(userId: number, strokeID: string, path: Path2D, pathStyle: PathStyle) {
this.userID = userId;
this.strokeID = strokeID;
this.path = path;
this.style = pathStyle;
}
}
- undo, redo์ ๊ด๋ จ๋ ์ ์ฒด path ๋ฐ stroke ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํฉ๋๋ค.
- undo, redo ํจ์๋ ์คํ๋ ๋ ๋ง๋ค ์บ๋ฒ์ค๋ฅผ ์ด๊ธฐํํ๊ณ ์ ์ฒด path๋ฅผ ๋ค์ ๊ทธ๋ฆฌ๋ ์์ ์ ํฌํจํฉ๋๋ค.
- pathStack
- ๋ชจ๋ pathData๋ฅผ <pathID, pathData> ์์ผ๋ก ๊ฐ์ต๋๋ค.
- ๋ฆฌ๋๋ก์ฐ ์ ํด๋น pathStack์ ์ฒซ ๋ฒ์งธ path๋ถํฐ ๋ง์ง๋ง path๊น์ง ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
- myStrokeIDStack
- ๋ด๊ฐ ๊ทธ๋ฆฐ Stroke๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ๋ฐฐ์ด์ ๋๋ค.
- ์ ํ๋๊ฐ ์์ฑ๋๋ฉด (์ผ๋ฐ์ ์ผ๋ก up ํน์ leave ์ด๋ฒคํธ) myStrokeIDStack์ ํ์ฌ์ strokeID๋ฅผ ์ถ๊ฐํฉ๋๋ค.
- myStrokeIDIter๋ undo, redo ๋ฐ์ ์ ๋ง๋ค ์ฆ๊ฐํ๋ฉฐ, myStrokeIDStack์์ ํ์ฌ ์ด๋ ์์น๋ฅผ ๊ฐ๋ฆฌ์ผ์ผ ํ๋ ์ง๋ฅผ ์๋ ค์ค๋๋ค.
- undo ๋ฐ์ ์ ์ stack์์ myStrokeIDIter๋ฅผ ๋ค๋ก ์ฎ๊ฒจ ํ์ฌ ํด๋น strokeID์ ์ํ path๋ฅผ ์ ๋ถ popPath ํฉ๋๋ค.
- redo ๋ฐ์ ์ ์ stack์์ myStrokeIDIter๋ฅผ ์์ผ๋ก ๋น๊ฒจ ํด๋น strokeID์ ์ํ path๋ฅผ ์ ๋ถ restorePath ํฉ๋๋ค.
export class StrokeManager {
pathStack: Map<string, PathData | null>; //pathID, PathData ์์ผ๋ก ์ ์ฅ
myStrokeIDStack: string[];
myStrokeIDIter: number;
deletedPathStack: Map<string, PathData | null>;
constructor() {
this.pathStack = new Map<string, PathData>();
this.deletedPathStack = new Map<string, PathData>();
this.myStrokeIDStack = [];
this.myStrokeIDIter = 0;
}
void pushPath(userID: number, strokeID: string, path: Path2D, pathStyle: PathStyle)
void popPath(pathID: string)
void restorePath(pathID: string, pathData: PathData)
void pushMyStroke(strokeID: string)
void popStroke()
void restoreStroke()
void undo(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D)
void redo(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D)
}
pushPath(userID: number, strokeID: string, path: Path2D, pathStyle: PathStyle) {
if (!this.pathStack) return;
// ์ pathData ๊ฐ์ฒด ์์ฑํ๋ค.
const newPath = new PathData(userID, strokeID, path, pathStyle);
// ์์ฑํ pathData์ ์์ ์ด๋ฃฐ pathID๋ฅผ ์์ฑํ๋ค.
const newPathID = [userID, '-', Date.now()].join('');
// pathStack Map์ ์ ์ฅํ๋ค.
this.pathStack.set(newPathID, newPath);
}
popPath(pathID: string) {
// popํ pathID์ pathData๋ฅผ ๊ฐ์ ธ์จ๋ค.
const pathData = this.pathStack.get(pathID);
if (!pathData) return;
// pathData๋ฅผ ์ญ์ ํด์ค๋ค.
this.pathStack.set(pathID, null);
// ํ์ ๋ณต๊ตฌ ํ ์ํฉ์ ์ํ์ฌ deletedPathStack์ ์ญ์ ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐฑ์
ํด์ค๋ค.
this.deletedPathStack.set(pathID, pathData);
}
restorePath(pathID: string, pathData: PathData) {
// ์ญ์ ๋์๋ pathData๋ฅผ ๋ณต๊ตฌํด์ค๋ค.
this.pathStack.set(pathID, pathData);
}
pushMyStroke(strokeID: string) {
// ์ stroke์ด ๋ค์ด์์ผ๋ฏ๋ก, ํ์ฌ myStrokeIDIter ์ดํ์ ID๋ค์ ์ญ์ ํ๋ค. (๋ณต๊ตฌ ๋ถ๊ฐ)
this.myStrokeIDStack.length = this.myStrokeIDIter + 1;
// ์๋ก์ด srokeID๋ฅผ ์ ์ฅํด์ค๋ค.
this.myStrokeIDStack.push(strokeID);
// ์ strokeID๊ฐ ๋ค์ด์์ผ๋, myStrokeIDIter๋ฅผ +1 ํด์ค๋ค.
this.myStrokeIDIter++;
}
popStroke() {
//popํ strokeID๋ฅผ myStrokeIDStack์์ ๊บผ๋ด์จ๋ค.
const currentStrokeID = this.myStrokeIDStack[this.myStrokeIDIter--];
//์์์ ๊บผ๋ธ strokeID๋ฅผ ๊ฐ์ง path๋ฅผ ์ ๋ถ popPush ํด์ค๋ค.
//myStrokeIDStack์ '๋ด stroke' ๋ง๋ค ๊ด๋ฆฌํ๋ฏ๋ก, ํ์ธ์ ์ ์ด ์ญ์ ๋ ์ผ์ ์๋ค.
for (const [pathID, pathData] of this.pathStack) {
if (!pathData) continue;
if (pathData.strokeID === currentStrokeID) this.popPath(pathID);
}
}
restoreStroke() {
//๋ณต๊ตฌํ StrokeID๋ฅผ ๊ฐ์ ธ์จ๋ค.
const nextStrokeID = this.myStrokeIDStack[++this.myStrokeIDIter];
//pathData๋ฅผ ๋ฐฑ์
์คํ์ ์ํํ์ฌ ์์์ ๊ฐ์ ธ์จ strokeID์ ํด๋นํ๋ pathData๋ฅผ ๋ณต๊ตฌํ๋ค.
for (const [pathID, pathData] of this.deletedPathStack) {
if (!pathData) return;
if (pathData.strokeID === nextStrokeID) this.restorePath(pathID, pathData);
}
}
undo(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
if (this.myStrokeIDIter <= 0) {
console.log('๋ undoํ ์ ์ด ์์');
return;
}
//popStroke ์๋
this.popStroke();
//์บ๋ฒ์ค ์ด๊ธฐํ
ctx.clearRect(0, 0, canvas.width, canvas.height);
// pathStack์ ์ฒซ pathData๋ถํฐ ์ฐจ๋ก๋๋ก ๊ทธ๋ฆฐ๋ค.
for (const pathData of this.pathStack.values()) {
if (!pathData) continue;
ctx.beginPath();
ctx.fillStyle = pathData.style.color;
ctx.strokeStyle = pathData.style.color;
ctx.lineWidth = pathData.style.lineWidth;
if (pathData.style.type === 'path') ctx.stroke(pathData.path);
if (pathData.style.type === 'shape') ctx.fill(pathData.path);
}
}
redo(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {
if (this.myStrokeIDIter >= this.myStrokeIDStack.length - 1) {
console.log('๋ redoํ ์ ์ด ์์');
return;
}
//restoreStroke ์๋
this.restoreStroke();
//์บ๋ฒ์ค ์ด๊ธฐํ
ctx.clearRect(0, 0, canvas.width, canvas.height);
// pathStack์ ์ฒซ pathData๋ถํฐ ์ฐจ๋ก๋๋ก ๊ทธ๋ฆฐ๋ค.
for (const pathData of this.pathStack.values()) {
if (!pathData) continue;
ctx.beginPath();
ctx.fillStyle = pathData.style.color;
ctx.strokeStyle = pathData.style.color;
ctx.lineWidth = pathData.style.lineWidth;
if (pathData.style.type === 'path') ctx.stroke(pathData.path);
if (pathData.style.type === 'shape') ctx.fill(pathData.path);
}
}
- strokeManager๋ ์ฑ ๋ด์์ ๋ฑ ํ๋๋ง ๋ง๋ค์ด์ง๋ฉด ๋ฉ๋๋ค.
- ๊ทธ๋ฆฌ๊ธฐ๊ฐ ์์๋๋ startDrawing๋ถํฐ ๋๋๋ stopDrawing๊น์ง ๊ฐ์ path2D ๊ฐ์ฒด์ ๊ฒฝ๋ก๋ฅผ ์ ์ฅํด์ผํ๋ฏ๋ก ref๋ก ์ ์ธํฉ๋๋ค.
- currentID ๋ํ startDrawing์ draw ํจ์์์ ๋ง๋ค์ด์ง๋ ๋ชจ๋ path๊ฐ ๊ฐ์ strokeID๋ฅผ ๋ฐฐ์ ๋ฐ์์ผ ํ๋ฏ๋ก ref๋ก ์ ์ธํฉ๋๋ค.
- path2D๋ฅผ ์ฌ์ฉํ๋ ์ด์
- path2D๋ ์ ์ฅํ ๊ฒฝ๋ก๋ฅผ canvas์ ์บ์ํ์ฌ, ๋ฆฌ๋๋ก์ฐ ์ ๊ฒฝ๋ก ์ฌ๊ณ์ฐ์ ์ค์ฌ ์ฑ๋ฅ ํฅ์์ ๋ ธ๋ฆด ์ ์์ต๋๋ค.
- ๋์์ ๋ ๊ฐ ์ด์์ ์ ์ ๊ทธ๋ฆฌ๊ธฐ ์ํด์๋ path2D๋ก ๊ฒฝ๋ก๋ฅผ ๋ถ๋ฆฌํด์ค์ผ ํฉ๋๋ค.
- ctx๋ canvas์์ 1๊ฐ๋ง ์๋ ๊ฐ์ฒด์ด๊ธฐ ๋๋ฌธ์, ๋ ์ฌ์ฉ์๊ฐ ๋์์ ์ ๊ทผํ์ฌ ๋๋ก์ ์ ๋ ์ฌ์ฉ์์ ์ ์ด ์ด์ด์ง๋ ํ์ ๋ฑ ์ค๋ฅ๊ฐ ์์ ์ ์์ต๋๋ค.
const strokeManager = useRef(new StrokeManager());
const path2D = useRef<Path2D | null>(null);
const currentStrokeID = useRef<string | null>(null);
const startDrawing = useCallback(
(point: Point) => {
if (!strokeManager.current) return;
...
...
const { ctx } = getCanvasContext(canvasRef);
//์ค์๊ฐ ๊ทธ๋ฆฌ๊ธฐ๋ฅผ ์ํด ctx์ ๋๋ก์ style์ ์ง์
ctx.strokeStyle = currentColor;
ctx.lineWidth = brushSize;
ctx.fillStyle = currentColor;
//ํด๋ฆญ ์ ์ ์ฐ๋ ๊ฒฝ๋ก๋ฅผ Path2D ๊ฐ์ฒด์ ์ ์ฅํ๋ค.
path2D.current = new Path2D();
path2D.current.arc(point.x, point.y, brushSize / 2, 0, Math.PI * 2);
//์์ path2D ๊ฐ์ฒด๋ฅผ ๋๋ก์ํ๋ค.
ctx.fill(path2D.current);
//strokeManager์ ๋ณด๋ผ PathStyle ๊ฐ์ฒด๋ฅผ ์ค๋นํ๋ค.
const pathStyle: PathStyle = {
color: currentColor,
lineWidth: brushSize,
type: 'shape',
};
// strokeID๋ฅผ ์๋ก ๋ง๋ ๋ค.
currentStrokeID.current = [0, '-', Date.now(), '-stroke'].join('');
// pathStack์ ์ ์ฅํด์ค๋ค.
strokeManager.current.pushPath(0, currentStrokeID.current, path2D.current, pathStyle);
// draw์์ ์๋ก ๋ง๋ค์ด์ง ๊ฒฝ๋ก๋ค์ ์ํด path2D๋ฅผ ์ด๊ธฐํํ๊ณ , ์์ ์ง์ ์ ๋ฐฐ์ ํ๋ค.
path2D.current = new Path2D();
path2D.current.moveTo(point.x, point.y);
...
...
},
[.....],
);
const draw = useCallback(
(point: Point) => {
if (!path2D.current) return;
if (!currentStrokeID.current) return;
...
...
const { ctx } = getCanvasContext(canvasRef);
// 'startDrawing'๋ฉ์๋์์ ์ด๊ธฐํํ path2D์ ๊ฒฝ๋ก๋ฅผ ์ด์ด ์ ์ฅํ๋ค.
path2D.current.lineTo(point.x, point.y);
// path2D ๊ฐ์ฒด์ ์ ์ฅ๋ ๊ฒฝ๋ก๋ฅผ ๊ทธ๋ฆฐ๋ค.
ctx.stroke(path2D.current);
//strokeManager์ ๋ณด๋ผ PathStyle ๊ฐ์ฒด๋ฅผ ์ค๋นํ๋ค.
const pathStyle: PathStyle = {
color: currentColor,
lineWidth: brushSize,
type: 'path',
};
// pathStack์ ์ ์ฅํด์ค๋ค.
strokeManager.current.pushPath(0, currentStrokeID.current, path2D.current, pathStyle);
...
...
},
[.....],
);
const stopDrawing = useCallback(() => {
if (!currentStrokeID.current) return;
...
...
// ๋๋ก์์ด ๋๋ฌ์ผ๋ strokeID๋ฅผ myStrokeIDStack ์ ์ ์ฅํด์ค๋ค.
// ๋ด stroke์ผ ๊ฒฝ์ฐ์๋ง ์ ์ฅํด์ผ ํ๋ค.
strokeManager.current.pushMyStroke(currentStrokeID.current);
path2D.current = null;
...
...
}, [.....]);
const undo = () => {
const { canvas, ctx } = getCanvasContext(canvasRef);
strokeManager.current.undo(canvas, ctx);
};
const redo = () => {
const { canvas, ctx } = getCanvasContext(canvasRef);
strokeManager.current.redo(canvas, ctx);
};
- ์๋์ ํด๋น ๋ผ์ธ ์ด ๋๋ฝ๋์ด ๋ง์ฐ์ค๊ฐ ํด๋ฆญํ ์ํ๊ฐ ์๋๋ฐ๋ ์บ๋ฒ์ค๋ฅผ ๋ฒ์ด๋๋ฉด pushMyStroke์ด ์๋ํ๋ ์ด์๊ฐ ์์์ต๋๋ค.
- ํด๋น ๋ผ์ธ ์ผ๋ก ๊ทธ๋ฆฌ๊ธฐ ์ํ์์ ๋์๋ง ํธ๋ค๋ฌ๊ฐ ์๋ํ๋๋ก ์์ ํ์ฌ ํด๊ฒฐํ์์ต๋๋ค.
const stopDrawing = useCallback(() => {
if (!drawingState.isDrawing) return; // ํด๋น ๋ผ์ธ
if (!currentStrokeID.current) return;
strokeManager.current.pushMyStroke(currentStrokeID.current);
path2D.current = null;
setDrawingState({
isDrawing: false,
startPoint: null,
});
}, [drawingState.isDrawing]);
- GameCanvasExample.tsx ์์ Canvas ์ปดํฌ๋ํธ์ ํ๋กญ์ค๋ฅผ ์ ๋ฌํ ๋, ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ค๋ณต ์ ๋ฌํ๊ณ ์์์ต๋๋ค.
- ์๋ return ๋ถ์ onMouseUp ๋ถํฐ onTouchCancel ๊น์ง์ ์์ฑ์ canvas ํ๊ทธ๊ฐ ์๋๋ผ ๊ทธ ๋ถ๋ชจ์ ๋ถ๋ชจ์ธ div ํ๊ทธ์ ์ ์ฉ๋๋ ๋ก์ง์ผ๋ก ๊ตฌ์ฑ๋์ด์์ต๋๋ค.
- ๋ฐ๋ผ์, ํด๋น ๋ถ๋ถ๋ค์ ์ญ์ ํ์ฌ ์ด๋ฒคํธ๊ฐ ์ค๋ณต ๋ฐ์ํ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์์ต๋๋ค.
- (๋ธ๋์น์ ๋ณํฉ๋ ์ฝ๋์ด๋ฏ๋ก, ์ดํ ์์ ์ ๊ผญ ์ญ์ ํด์ค์ผ ํ๋ ์ฝ๋๋ค.)
return (
<Canvas
canvasRef={canvasRef}
className="min-w-[280px]"
isDrawable={isDrawable}
colors={isDrawable ? COLORS : []}
// toolbarPosition="floating"
brushSize={brushSize}
setBrushSize={setBrushSize}
drawingMode={drawingMode}
onDrawingModeChange={setDrawingMode}
inkRemaining={inkRemaining}
maxPixels={maxPixels}
canUndo={canUndo}
canRedo={canRedo}
onUndo={undo}
onRedo={redo}
canvasEvents={canvasEventHandlers}
// canvas ํ๊ทธ ๊ธฐ๋ณธ ์์ฑ (ํด๋น ์๋ ๋ถ๋ถ์ด ์๋ชป๋จ)
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchEnd={stopDrawing}
onTouchCancel={stopDrawing}
/>
);
- ํ์ฌ๋ move ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋ ๋ง๋ค (์ ์ขํ๊ฐ ์์ฑ๋ ๋ ๋ง๋ค) path2D๋ฅผ ์์ฑํด ์ ์ฅํ๊ณ ์๊ณ , ๊ทธ๋ ๋ค๋ณด๋ path ๊ด๋ฆฌ ๋จ์๊ฐ ๊ต์ฅํ ์์ต๋๋ค.
- ์ผ๋ง๋ ์๋ํ๋ฉด, ์กฐ๊ธ๋ง ๊ทธ๋ ค๋ pathStack์ ์์๊ฐ 700~900๊ฐ ๋๊ฒ ์์
๋๋ค.
- ๋ฉํฐ ์ ์ ๊น์ง ์ฐธ์ฌํ๋ค๋ฉด 2000๊ฐ๋ ์กฑํ ๋์ ๊ฒ์ผ๋ก ์์๋ฉ๋๋ค.
- path2D ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ธด ํ์ง๋ง, 2000๊ฐ ๊ฐ๋์ path๋ฅผ undo & redo ํ ๋๋ง๋ค ๋ฆฌ๋๋ก์ฐ ํด์ผํ๋ค๋ ์ ์ ์ฑ๋ฅ๋ฉด์์ ๋๋ฌด ์น๋ช
์ ์ด์ง ์๋ ์๊ฐ๋ฉ๋๋ค.
- ์ค์ ๋ก ์ ์ ์์ฃผ ๊ธฐ์ด์ด์ผ๊ฒ ๊ทธ๋ฆฌ๋ฉด ์ข ๋๋ ค์ง๋ ๋ฏํ ์ฒด๊ฐ์ด ๋ญ๋๋ค.
- path ๋จ์๋ก ์ ์ ์ ๊ด๋ฆฌํ๋ ํ์ฌ ๋ฐฉ์์ ์ฅ์ ์, path ๊ฐ์ ์ฐ์ ์์๊ฐ ๋์ ์ ํ๋๋ก ์ง์ผ์ง๋ค๋ ๊ฒ์
๋๋ค.
- ๋ ์ ์ ๋ง ๊ฒน์ณ ๊ทธ๋ฆฐ ํ undo/redo ํ์ฌ๋ ์ฐ์ ์์๊ฐ ์ ์ง๋ฉ๋๋ค.
- ํ์ง๋ง ์์์ ์ธ๊ธํ ๋ฌธ์ ์ ์ด ์์ผ๋, ์กฐ์จํ๋ค๋ฉด ์๋์ ๋ฐฉํฅ์ ์์ํด๋ด์ผ ํฉ๋๋ค.
- ์ ๊ด๋ฆฌ ๋จ์ ์กฐ์
- path ๋จ์ or stroke ๋จ์ or ๊ทธ ์ฌ์ด์ ์ด๋๊ฐ
- stroke ๋จ์๋ก ๊ด๋ฆฌํ๊ฒ ๋๋ฉด, undo & redo ์ path๊ฐ ์๋ ์ ๋จ์๋ก ์ฐ์ ์์๊ฐ ์ ํด์ง๋ค.
- ๋ค๋ฅธ ๋ฐฉ๋ฒ ๋ชจ์
- ์ ๊ด๋ฆฌ ๋จ์ ์กฐ์
- ์ฌ๊ธฐ์ ์ฐ์ ์์๋, ์บ๋ฒ์ค์์ ์๊ฐ์ ์ผ๋ก ์ด๋ค ์ ์ด ๋ ์๋ก ์ฌ๋ผ๊ฐ ๊ฒ์ธ๊ฐ ์ ๋๋ค.
- redo / undo ์ ๋จ์ ํฝ์ ๋ ๋ฐ์
- ํ์ธํธ ํด ๋ณํฉ
- 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 ์ฃผ๊ฐ ํ๊ณ