fix(study): perfect-freehand 미사용으로 단순 ctx.stroke() 전환 + 좌표 scale 보정
증상: 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 가림 등) 추적.
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user