From 85659ce928b97648b053400952f46ffeff10d2d0 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 27 Apr 2026 09:00:39 +0900 Subject: [PATCH] =?UTF-8?q?fix(study):=20perfect-freehand=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=8B=A8=EC=88=9C=20ctx?= =?UTF-8?q?.stroke()=20=EC=A0=84=ED=99=98=20+=20=EC=A2=8C=ED=91=9C=20scale?= =?UTF-8?q?=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: stroke count 는 올라가는데 화면에 그려지지 않음 + 위치 어긋남. 원인 격리 시도: - perfect-freehand 의 polygon fill 이 일부 환경에서 제대로 그려지지 않는 것으로 보여 단순 ctx.beginPath/moveTo/lineTo/stroke() 로 갈아치움. lineCap/lineJoin 'round' + lineWidth=baseSize 로 자연스러운 라인. 압력 효과는 일시 제거. - getLocalXY 에 scale 보정 추가: canvas.style.width(cssWidth) 와 rect.width 가 다른 ResizeObserver 지연 케이스에서 좌표가 어긋나지 않도록 비율 보정. 이번 변경으로도 stroke 가 안 보이면 디버그 오버레이의 좌표/크기를 보고 다른 경로 (캔버스 자체 비활성, layer 가림 등) 추적. --- .../src/lib/components/HandwriteCanvas.svelte | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) 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 {