diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index bb615bf..265fc2f 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -214,9 +214,10 @@ if (!path) { const outline = getStroke(pts, { size, - // thinning 0.15 — pressure 변동에 stroke 폭 ±15% 반응. EMA 로 부드럽게 - // smooth 된 pressure 와 조합 → Notability 같은 자연스러운 굵기 흐름. - thinning: 0.15, + // thinning 0.22 — pressure 변동에 stroke 폭 ±22% 반응. moving-window 평균 + // pressure 와 조합 → 큰 흐름의 굵기 변화는 명확하지만 점 간 micro 변동 없음 + // (마디 안 발생). Notability 비슷한 자연스러운 변화. + thinning: 0.22, // smoothing 0.99 — 점 간 보간 사실상 최대. smoothing: 0.99, // streamline 0.86 — input lazy 강하게. 손떨림 보정 + 부드러움. 0.9 이상은 lag. @@ -337,18 +338,25 @@ // 점 사이 거리가 너무 멀면 중간 점 보간 — 빠른 stroke 의 sparse point 점선 방지. // iPad 60Hz pointermove + 빠른 펜 이동 시 점 간격이 16~30px 가 될 수 있음. const MAX_GAP_PX = 8; - // pressure EMA — 점 간 pressure 변동을 *지수 이동 평균* 으로 부드럽게. - // alpha 0.15 = 새 값 15% + 이전 값 85% 가중. 잡음/덜컥 변동 제거 + Notability - // 같은 자연스러운 흐름. thinning=0.15 와 조합 시 stroke 굵기가 부드럽게 변함. - const PRESSURE_EMA_ALPHA = 0.15; - function smoothPressure(prev: number, current: number): number { - return prev + (current - prev) * PRESSURE_EMA_ALPHA; + // pressure moving-average window — 마지막 N 점의 pressure 평균. EMA 가 매 점마다 + // 살짝 변동시켜 thinning 적용 시 *점 간 micro 폭 변동 = 마디* 일으키던 회귀를 + // 차단. window 평균은 큰 흐름만 반영, 매 점 변동은 1/N 수준. 사용자 보고 "선 + // 사이사이 애매한 끊어짐" 의 원인이었음. + const PRESSURE_WINDOW = 16; + function smoothPressureWindow(pts: Point[], current: number): number { + let sum = current; + let count = 1; + const start = Math.max(0, pts.length - PRESSURE_WINDOW + 1); + for (let i = start; i < pts.length; i++) { + sum += pts[i][2]; + count++; + } + return sum / count; } function pushPointWithInterp(target: Stroke, x: number, y: number, p: number) { const last = target.points[target.points.length - 1]; if (last) { - const lp = last[2]; - const sp = smoothPressure(lp, p); + const sp = smoothPressureWindow(target.points, p); const dx = x - last[0]; const dy = y - last[1]; const dist = Math.hypot(dx, dy); @@ -356,10 +364,12 @@ const steps = Math.ceil(dist / MAX_GAP_PX); for (let i = 1; i < steps; i++) { const t = i / steps; + // 보간된 점들도 모두 sp 동일. 보간 점에 점진 변동 두면 perfect-freehand + // 알고리즘이 outline polygon 에 micro 변동 → 마디. 전부 sp 로 통일. target.points.push([ last[0] + dx * t, last[1] + dy * t, - lp + (sp - lp) * t, + sp, ]); } }