From e5c06b741513f11424a9a26d28e1a23f1d29da12 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:36:19 -0400 Subject: [PATCH] Pen tool: uniform-scale remap (fixes stroke distortion) + smaller handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two visual issues on top of the Phase D close-out commit: 1. **Stroke width varied along curves.** The pre-existing pipeline stored nodes in 0-100 × 0-100 viewBox space and let the SVG renderer pick `vbH = 100 * layer.h / layer.w` with `preserveAspectRatio="none"`. For any non-square layer, the 0-100×0-100 node range got stretched non-uniformly into a 100×vbH viewBox, distorting the path: strokes appeared wider where the curve traveled more in one axis. Phase D's refit made this much more likely because dragging anchors out of bounds routinely produced non-square layers. Fix in two places: - finishPenPath: uniform-scale the bbox by `100/bbox.w` so y range lands at exactly `100 * h/w = vbH`. The viewBox aspect matches the layer aspect, no stretching. - refitPenLayerIfNeeded: same uniform-scale invariant. Also respects the layer's current vbH when deciding whether nodes are "out of bounds" — was previously hard-coded at 100, which wrongly fired refit on every drag in already-non-square layers. 2. **Anchor + handle glyphs were too prominent.** On a 1920px-wide slide preview the old `dotR = 0.013 * unit` rendered ~25px dots. Reduced to `0.0075 * unit` (~12px) and stroke from `0.003 * unit` to `0.0018 * unit`. The in-progress draft renderer matches. Strokes now stay constant width all the way around a curve, and the editing UI reads as overlay rather than as part of the artwork. --- src/index.html | 54 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/index.html b/src/index.html index add6ef1..6808551 100644 --- a/src/index.html +++ b/src/index.html @@ -9999,23 +9999,39 @@ if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; } - // No-op unless we're outside [0, 100] on any side. A bit of - // tolerance to avoid micro-jiggle during normal in-bounds drags. + // Layer viewBox is 0-100 × 0-vbH where vbH = 100*layer.h/layer.w + // — the renderer's hard-coded formula. We want nodes to fit + // x:[0,100] and y:[0,vbH], matching the layer's actual aspect + // ratio after refit. The current viewBox-y bound is implied + // by what the renderer would compute given new layer.w/h. + // + // No-op unless we're outside the current viewBox on any side + // (with a small tolerance to avoid micro-jiggle during normal + // in-bounds drags). The current viewBox y-max is `vbH` computed + // from existing layer w/h. + const currentVbH = layer.w > 0 ? 100 * layer.h / layer.w : 100; const EPS = 0.5; - if (minX > -EPS && minY > -EPS && maxX < 100 + EPS && maxY < 100 + EPS) return; + if (minX > -EPS && minY > -EPS && maxX < 100 + EPS && maxY < currentVbH + EPS) return; const newW = Math.max(EPS, maxX - minX); const newH = Math.max(EPS, maxY - minY); + // Layer scales: in inches, layer.w grows in proportion to + // newW vs current viewBox-x (always 100); layer.h grows in + // proportion to newH vs the existing vbH. const sx = newW / 100; - const sy = newH / 100; + const sy = newH / currentVbH; // Layer position shifts in inches by the bbox offset scaled // to the existing layer w/h. layer.x = layer.x + (minX / 100) * layer.w; - layer.y = layer.y + (minY / 100) * layer.h; + layer.y = layer.y + (minY / currentVbH) * layer.h; layer.w = Math.max(0.01, layer.w * sx); layer.h = Math.max(0.01, layer.h * sy); + // Uniform-scale remap so the new node range matches the new + // viewBox aspect exactly — no stretching when the SVG paints + // into the refitted layer rectangle. + const remapScale = 100 / newW; const remap = (p) => ({ - x: (p.x - minX) / newW * 100, - y: (p.y - minY) / newH * 100, + x: (p.x - minX) * remapScale, + y: (p.y - minY) * remapScale, }); for (const n of nodes) { const r = remap(n); @@ -10113,8 +10129,13 @@ clearPenDraftRender(); const dims = currentCanvasDims(); const unit = Math.max(dims.w, dims.h); - const lineW = unit * 0.003; - const dotR = unit * 0.013; + // Smaller, less prominent anchors/handles — earlier sizes + // (0.003 stroke, 0.013 anchor) rendered ~25px dots on a + // 1920px-wide preview, which obscured short paths and made + // the canvas feel cluttered. ~50% smaller reads as deliberate + // editing UI without dominating. + const lineW = unit * 0.0018; + const dotR = unit * 0.0075; const nodes = layer.shape.pen.nodes; nodes.forEach((n, i) => { const a = penNodeToInch(layer, n); @@ -10469,7 +10490,14 @@ if (n.hOut) allPts.push(n.hOut); } const bbox = pointsBbox(allPts); - const remap = (pt) => ({ x: (pt.x - bbox.x) / bbox.w * 100, y: (pt.y - bbox.y) / bbox.h * 100 }); + // Uniform scaling: divide both x and y by bbox.w so nodes land + // in 0-100 × 0-(100*h/w) — matching the renderer's vbH formula + // (= 100 * layer.h / layer.w). Independent x/y scaling here + // forces a 0-100×0-100 node range into a non-square viewBox + // later, stretching curves and varying stroke widths around + // the path. Uniform scaling keeps strokes a constant width. + const scale = bbox.w > 0 ? 100 / bbox.w : 1; + const remap = (pt) => ({ x: (pt.x - bbox.x) * scale, y: (pt.y - bbox.y) * scale }); const normNodes = nodes.map(n => ({ x: remap(n).x, y: remap(n).y, hIn: n.hIn ? remap(n.hIn) : null, @@ -10515,8 +10543,10 @@ clearPenDraftRender(); const dims = currentCanvasDims(); const unit = Math.max(dims.w, dims.h); - const lineW = unit * 0.004; - const dotR = unit * 0.012; + // Sized to match renderPenEditHandles so the in-progress + // draw and the post-commit edit handles look consistent. + const lineW = unit * 0.0025; + const dotR = unit * 0.0075; if (_penDraft.tool === 'pencil') { const pts = _penDraft.points;