fix(study): widthMode 변경 시 기존 stroke 굵기 보존 + 부드러움 살짝 더
1. Stroke 별 size 저장 — 사용자 보고 "굵기 변경하면 기존에 입력했던거 전부 바뀜"
- 회귀 원인: drawStroke 가 매 redraw 시 effectiveSize ($derived) 사용 →
widthMode 변경 시 모든 stroke 재그려짐.
- Fix: Stroke type 에 size 추가. inflight 생성 시 size=effectiveSize 저장.
drawStroke 가 s.size 사용. legacy stroke (size 없음) 은 첫 draw 시점의
effectiveSize 로 fix (refW/refH 패턴 동일).
- cache 무효화 로직 정리: stroke.size 가 불변이므로 _path2d 캐시는 영원 유효.
기존 _size 비교 제거.
- serializableStrokes 에 size 포함 — 다음 load 시 굵기 보존.
2. Stroke 부드러움 살짝 더:
- smoothing 0.98 → 0.99 (사실상 max).
- streamline 0.82 → 0.86 (input lazy 강화, 손떨림 보정 큼).
- 0.9 이상은 lag 위험.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,19 +21,18 @@
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
|
||||
type Point = [number, number, number]; // [x, y, pressure]
|
||||
// refW / refH = stroke 가 그려진 시점의 cssWidth / cssHeight. drawStroke 시
|
||||
// ctx.scale(cssWidth/refW, cssHeight/refH) 로 비례 보정 → 창 크기 변화 / button
|
||||
// click 으로 인한 layout shift 후에도 stroke 가 *원래 시각적 위치/비율* 유지
|
||||
// (사용자 보고: "펜/지우개 누르면 해당 부분 확대"의 진짜 root cause = stroke
|
||||
// 좌표가 절대 px 라 cssWidth 변경 시 비례 깨짐).
|
||||
// _path2d / _size 는 런타임 캐시 — 직렬화 시 제외 (밑줄 prefix 가 marker).
|
||||
// size = 그렸을 시점의 effectiveSize. widthMode (가늘게/보통/굵게) 변경 시 *기존*
|
||||
// stroke 의 굵기는 변하지 않도록 (사용자 요청). 새 stroke 만 새 effectiveSize 사용.
|
||||
// refW / refH = 그렸을 시점의 cssWidth / cssHeight. canvas dimension 변화 후에도
|
||||
// stroke 의 시각적 위치/비율 유지 (style:cursor 회귀 fix 와 별개의 비례 보정).
|
||||
// _path2d 는 런타임 캐시 — 직렬화 시 제외 (밑줄 prefix 가 marker).
|
||||
type Stroke = {
|
||||
id: string;
|
||||
points: Point[];
|
||||
size?: number;
|
||||
refW?: number;
|
||||
refH?: number;
|
||||
_path2d?: Path2D;
|
||||
_size?: number;
|
||||
};
|
||||
export type StrokesJson = { version: 1; strokes: Stroke[] };
|
||||
type Tool = 'pen' | 'eraser';
|
||||
@@ -119,10 +118,11 @@
|
||||
// Path2D 등 런타임 캐시 (`_` prefix) 제외하고 직렬화. refW/refH 는 그렸을 시점의
|
||||
// cssWidth/cssHeight — 다른 환경 (창 크기 다른 데스크톱, 모바일 등) 에서 load 시
|
||||
// 비례 보정에 사용. 없으면 load 시점의 cssWidth/cssHeight 가 기준이 됨.
|
||||
function serializableStrokes(): Pick<Stroke, 'id' | 'points' | 'refW' | 'refH'>[] {
|
||||
function serializableStrokes(): Pick<Stroke, 'id' | 'points' | 'size' | 'refW' | 'refH'>[] {
|
||||
return strokes.map((s) => ({
|
||||
id: s.id,
|
||||
points: s.points,
|
||||
size: s.size,
|
||||
refW: s.refW,
|
||||
refH: s.refH,
|
||||
}));
|
||||
@@ -193,9 +193,14 @@
|
||||
const pts = s.points;
|
||||
if (pts.length === 0) return;
|
||||
|
||||
// stroke 별 size — widthMode 변경 시 기존 stroke 굵기 유지. legacy stroke (size
|
||||
// 없음) 은 첫 draw 시점의 effectiveSize 로 fix (refW/refH 패턴과 동일).
|
||||
if (s.size === undefined || s.size <= 0) s.size = effectiveSize;
|
||||
const size = s.size;
|
||||
|
||||
if (pts.length === 1) {
|
||||
const [x, y, p] = pts[0];
|
||||
const r = effectiveSize * (0.4 + p * 0.6) / 2;
|
||||
const r = size * (0.4 + p * 0.6) / 2;
|
||||
ctx.fillStyle = strokeColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, Math.max(1, r), 0, Math.PI * 2);
|
||||
@@ -203,32 +208,26 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 완료 stroke 는 캐시된 Path2D 재사용 (size 변경 시만 재계산). inflight 는
|
||||
// 점이 매 frame 추가되므로 매번 재계산.
|
||||
let path = !isInflight && s._size === effectiveSize ? s._path2d : undefined;
|
||||
// 완료 stroke 는 캐시된 Path2D 재사용. stroke.size 가 불변이므로 캐시 영원.
|
||||
// inflight 는 점이 매 frame 추가되므로 매번 재계산.
|
||||
let path = !isInflight ? s._path2d : undefined;
|
||||
if (!path) {
|
||||
const outline = getStroke(pts, {
|
||||
size: effectiveSize,
|
||||
// thinning 0 — pressure 변동에 stroke 폭 영향 안 받음. 일정한 굵기 = 흔들림
|
||||
// 최소화. 만년필 효과는 *부드러운 흐름* 으로 표현 (smoothing/streamline 강화).
|
||||
size,
|
||||
// thinning 0 — pressure 변동에 stroke 폭 영향 안 받음. 흔들림 1차 차단.
|
||||
thinning: 0,
|
||||
// smoothing 0.98 — 점 간 보간 거의 최대. Pencil 240Hz 미세 떨림 + 손떨림 흡수.
|
||||
smoothing: 0.98,
|
||||
// streamline 0.82 — input lazy 강하게. 손떨림 보정 큼. 너무 높으면 lag.
|
||||
streamline: 0.82,
|
||||
// smoothing 0.99 — 점 간 보간 사실상 최대.
|
||||
smoothing: 0.99,
|
||||
// streamline 0.86 — input lazy 강하게. 손떨림 보정 + 부드러움. 0.9 이상은 lag.
|
||||
streamline: 0.86,
|
||||
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));
|
||||
if (!isInflight) {
|
||||
s._path2d = path;
|
||||
s._size = effectiveSize;
|
||||
}
|
||||
if (!isInflight) s._path2d = path;
|
||||
}
|
||||
ctx.fillStyle = strokeColor;
|
||||
ctx.fill(path);
|
||||
@@ -462,6 +461,7 @@
|
||||
inflight = {
|
||||
id: `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
points: [[x, y, normalizePressure(e.pressure)]],
|
||||
size: effectiveSize,
|
||||
refW: cssWidth,
|
||||
refH: cssHeight,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user