From 187fe2bb01b590579c83cdcbbd77ddc218fbe045 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 27 Apr 2026 15:00:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(study):=20=EA=B5=B5=EA=B8=B0=20=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=20=EC=8B=9C=ED=94=84=ED=8A=B8=20+=20=EB=B6=80?= =?UTF-8?q?=EB=93=9C=EB=9F=AC=EC=9B=80=20=EA=B0=95=ED=99=94=20(=EC=84=A0?= =?UTF-8?q?=20=ED=9D=94=EB=93=A4=EB=A6=BC=20=EC=B0=A8=EB=8B=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 요청: 1. 굵기 단계 한 단계씩 가는 쪽 시프트 — 새 thin (0.4) 추가, 새 normal (0.6) = 이전 thin, 새 thick (1.0) = 이전 normal. 이전 thick (1.6) 제거. 2. 만년필 같은 부드러움 + 약한 압력에도 안정. Stroke 옵션 튜닝 (선 흔들림 차단): - thinning 0.18 → 0. pressure 변동에 따른 stroke 폭 변화 제거 → 일정 굵기 → 흔들림 최소화. 사용자 보고 "선이 흔들림" 의 직접 원인이었음. - smoothing 0.95 → 0.98. 점 간 보간 거의 최대. Pencil 240Hz 미세 떨림 + 손떨림 흡수. - streamline 0.7 → 0.82. input lazy 강하게. 0.85 이상은 lag 발생 위험. - start/end taper effectiveSize*0.5 → 0. 짧은 stroke 시작/끝에서 굵기 급변이 흔들림 인식 강화. cap round 만 유지로 충분. Pressure smoothing 함수 추가 (선택적 만년필 효과 잔존): - pushPointWithInterp 에서 점 간 pressure 변동 5% 이내로 제한. - thinning 0 인 현재는 visible 영향 없지만, 향후 thinning 도입 시 재활용 가능. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/components/HandwriteCanvas.svelte | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) 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]); }