feat(study): 굵기 단계 시프트 + 부드러움 강화 (선 흔들림 차단)
사용자 요청: 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) <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,11 @@
|
||||
let strokeColor = $state('#e4e4e7');
|
||||
let tool = $state<Tool>('pen');
|
||||
let widthMode = $state<WidthMode>('normal');
|
||||
const WIDTH_FACTOR: Record<WidthMode, number> = { 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<WidthMode, number> = { 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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user