diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index 1487760..3ebf6fb 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -94,7 +94,11 @@ let strokeColor = $state('#e4e4e7'); let tool = $state('pen'); let widthMode = $state('normal'); - const WIDTH_FACTOR: Record = { thin: 0.6, normal: 1, thick: 1.6 }; + // 굵기 단계 한 단계씩 가는 쪽으로 시프트 (사용자 요청): + // thin 0.4 — 새로 추가된 더 가는 단계 + // normal 0.6 — 이전 thin + // thick 1.0 — 이전 normal (이전 thick 1.6 은 너무 굵어 제거) + const WIDTH_FACTOR: Record = { thin: 0.4, normal: 0.6, thick: 1 }; let effectiveSize = $derived(baseSize * WIDTH_FACTOR[widthMode]); let eraserRadius = $derived(Math.max(16, effectiveSize * 4)); @@ -205,16 +209,19 @@ if (!path) { const outline = getStroke(pts, { size: effectiveSize, - // thinning 0 = pressure 변동에 stroke 폭 영향 받지 않음. Apple Pencil 의 - // pressure 변동 (0~1 빈번) 이 stroke 경계를 들쭉날쭉하게 만들던 회귀 차단. - // 마우스 stroke 와 동일한 일정 굵기 = 부드러움. + // thinning 0 — pressure 변동에 stroke 폭 영향 안 받음. 일정한 굵기 = 흔들림 + // 최소화. 만년필 효과는 *부드러운 흐름* 으로 표현 (smoothing/streamline 강화). thinning: 0, - // smoothing 0.95 — 점 간 보간 매우 강하게. Pencil 240Hz 미세 떨림 흡수. - smoothing: 0.95, - // streamline 0.7 — 손떨림 보정 강화 (이전 0.65 → 0.7). - streamline: 0.7, + // smoothing 0.98 — 점 간 보간 거의 최대. Pencil 240Hz 미세 떨림 + 손떨림 흡수. + smoothing: 0.98, + // streamline 0.82 — input lazy 강하게. 손떨림 보정 큼. 너무 높으면 lag. + streamline: 0.82, simulatePressure: false, last: !isInflight, + // cap round 만 유지 — taper 는 짧은 stroke 시작/끝에서 굵기 급변 일으켜 + // 흔들림 인식 강화. 만년필 nib 효과는 cap 으로 충분. + start: { cap: true, taper: 0 }, + end: { cap: true, taper: 0 }, }); if (outline.length < 2) return; path = new Path2D(getSvgPathFromStroke(outline)); @@ -330,24 +337,35 @@ // 점 사이 거리가 너무 멀면 중간 점 보간 — 빠른 stroke 의 sparse point 점선 방지. // iPad 60Hz pointermove + 빠른 펜 이동 시 점 간격이 16~30px 가 될 수 있음. const MAX_GAP_PX = 8; + // pressure 변동 limit — 만년필 효과: 약한 압력에도 부드럽게, 점 간 압력 차이가 + // 너무 크면 stroke 폭이 들쭉날쭉해짐. 한 점당 5% 이내로만 변동. + const PRESSURE_SMOOTH_RATE = 0.05; + function smoothPressure(prev: number, current: number): number { + const dp = current - prev; + if (Math.abs(dp) <= PRESSURE_SMOOTH_RATE) return current; + return prev + Math.sign(dp) * PRESSURE_SMOOTH_RATE; + } 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 dx = x - last[0]; const dy = y - last[1]; const dist = Math.hypot(dx, dy); if (dist > MAX_GAP_PX) { const steps = Math.ceil(dist / MAX_GAP_PX); - const lp = last[2]; for (let i = 1; i < steps; i++) { const t = i / steps; target.points.push([ last[0] + dx * t, last[1] + dy * t, - lp + (p - lp) * t, + lp + (sp - lp) * t, ]); } } + target.points.push([x, y, sp]); + return; } target.points.push([x, y, p]); }