diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index d4c9c83..c99b4dc 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -18,7 +18,6 @@ * (전체 지우기 버튼은 별도) */ import { onMount, onDestroy } from 'svelte'; - import { getStroke } from 'perfect-freehand'; import { Eraser, Pencil, Undo2, Redo2, Trash2 } from 'lucide-svelte'; import IconButton from '$lib/components/ui/IconButton.svelte'; @@ -118,24 +117,30 @@ redraw(); } - // ── render ── - function strokeToPath(_stroke: Stroke): Path2D { - const outlinePoints = getStroke(_stroke.points, { - size: baseSize, - thinning: 0.3, // 시작 부분이 너무 얇아지지 않게 (0.5 → 0.3) - smoothing: 0.5, - streamline: 0.4, - simulatePressure: true, // pressure 0 으로 들어오는 케이스 방어 - last: true, // 진행 중에도 양쪽 outline + cap 완성 (polygon 닫힘 보장) - }); - const path = new Path2D(); - if (outlinePoints.length === 0) return path; - path.moveTo(outlinePoints[0][0], outlinePoints[0][1]); - for (let i = 1; i < outlinePoints.length; i++) { - path.lineTo(outlinePoints[i][0], outlinePoints[i][1]); + // ── render — 단순 ctx.stroke() 로 갈아치움 (perfect-freehand 미사용). + // 이유: perfect-freehand 의 polygon fill 이 그려지지 않는 보고. 단순 line 으로 + // 안정성 확보 후 문제 격리. 압력 효과는 lineWidth 변경으로 흉내. + function drawStroke(ctx: CanvasRenderingContext2D, s: Stroke) { + if (s.points.length === 0) return; + ctx.beginPath(); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.lineWidth = baseSize; + ctx.strokeStyle = strokeColor; + if (s.points.length === 1) { + // 점 하나만 있으면 작은 원 + const [x, y] = s.points[0]; + ctx.beginPath(); + ctx.fillStyle = strokeColor; + ctx.arc(x, y, baseSize / 2, 0, Math.PI * 2); + ctx.fill(); + return; } - path.closePath(); - return path; + ctx.moveTo(s.points[0][0], s.points[0][1]); + for (let i = 1; i < s.points.length; i++) { + ctx.lineTo(s.points[i][0], s.points[i][1]); + } + ctx.stroke(); } function drawTraceBackground(ctx: CanvasRenderingContext2D) { @@ -156,12 +161,11 @@ if (!ctx) return; ctx.clearRect(0, 0, cssWidth, cssHeight); drawTraceBackground(ctx); - ctx.fillStyle = strokeColor; for (const s of strokes) { - ctx.fill(strokeToPath(s)); + drawStroke(ctx, s); } if (inflight) { - ctx.fill(strokeToPath(inflight)); + drawStroke(ctx, inflight); } } @@ -169,7 +173,14 @@ function getLocalXY(e: PointerEvent): [number, number] { if (!canvas) return [0, 0]; const rect = canvas.getBoundingClientRect(); - return [e.clientX - rect.left, e.clientY - rect.top]; + // canvas 의 실제 표시 크기와 attribute 크기가 다른 경우 (ResizeObserver 가 아직 못 따라잡은 케이스) + // 비율로 보정. rect.width 가 canvas 의 실제 표시 width. + const scaleX = rect.width === 0 ? 1 : cssWidth / rect.width; + const scaleY = rect.height === 0 ? 1 : cssHeight / rect.height; + return [ + (e.clientX - rect.left) * scaleX, + (e.clientY - rect.top) * scaleY, + ]; } function isPenLike(e: PointerEvent): boolean {