cover logo: fit the traced outline to lines and curves, not a decimated polyline

traceToSvg emitted polylines. Marching squares produced a sub-pixel contour and
RDP threw points away at eps=0.2 until what was left could be joined with "L",
so a curve arrived as facets and a straight edge arrived as a ripple. The ripple
is the one that reads at hero size, and the reason is a measurement rather than a
matter of taste: on these masks a mathematically straight edge still wanders
through the alpha field -- print grain, JPEG ringing, and the shadow un-blend
being a two-colour model of a soft gradient. Measured over the acceptance set
that wander peaks around 0.15-0.3px across a 40px run on a clean source, so
eps=0.2 sits BELOW the noise floor and RDP was faithfully reproducing the grain.
Tightening it makes that worse; loosening it far enough to clear the noise starts
cutting real curvature.

Fitting cubics under a looser tolerance is the obvious answer and it does not
work. Schneider pins each segment's endpoints to actual sample points, so every
join inherits that one point's share of the noise; measured, the straight edges
came out no better than RDP left them (ripple 0.56 -> 0.64px). Raising the
tolerance does not help, because the endpoints are pinned regardless.

What removes the noise is fitting a LINE to the whole run and throwing the
samples away. A total-least-squares line over a few hundred points has an error
of noise/sqrt(n) -- hundredths of a pixel, not tenths. Vertices then become the
INTERSECTIONS of neighbouring primitives rather than sample points, which has a
second payoff: the trace blur rounds every real corner into a small arc, and
intersecting the two adjacent lines puts the sharp corner back. So the pass is
corners, cut at them, maximal straight runs, cubics for the gaps, then resolve
the joints. Prefix moments make a line fit over any index range O(1), which is
what keeps growing a run near-linear rather than quadratic on exactly the long
straight edges it exists for.

Three things a straightness test cannot decide on its own, each found by looking
at where the lines actually landed rather than at an aggregate.

- A residual test admits CHORDS OF A BOWL. On a circle of radius R a chord of arc
  length L departs from its own best-fit line by only about 0.3 L^2/(8R), so at
  any tolerance loose enough to absorb grain a big enough bowl accepts straight
  pieces -- which is how the counters of Hot Toddy's O's came out as polygons,
  73% of that outline drawn straight. The evidence lies OUTSIDE the run: fit a
  circle to a window extended past both ends, and if that window is a clean arc
  whose sagitta across the run exceeds arcSagitta, the run is a chord. On a loop
  with no corners the window has to WRAP, because the seam goes at the tightest
  bend and clipping there would exempt the one place that most needs the test.

- The ends OVERRUN into the bend. RMS is an average, so a run absorbs the first
  few px of a curve and still passes; the line then chords the start of the bend
  and the cubic is left too little room, which is the kink at a stem-to-shoulder
  junction. Each end is trimmed back to where a window-fitted direction still
  agrees with the run's line. It has to be a WINDOW fit -- a point tangent over a
  few px of grainy edge carries several degrees of noise by itself and trims at
  random, which put the ripple back up from 0.19 to 0.48px -- and the threshold
  has to rise with the run's own measured noise, or it means something different
  on a clean print than on a rough one. An end sitting on a detected corner is
  not trimmed: the contour runs straight into a corner by definition, and that
  direction is what the corner is reconstructed from.

- Extrapolating to an intersection is unbounded. As a wedge closes the legitimate
  apex offset grows without limit, and so does the error in it, because two
  nearly anti-parallel lines cross at a point a fraction of a degree can move a
  long way. The inside of Mildlife's M reconstructed 11px past its real apex and
  cut a white spike into the letterform. The shift is now bounded by what the
  rounding can explain -- r*(1/sin(theta/2) - 1) for a wedge of interior angle
  theta -- and CLAMPED rather than discarded, since a corner pulled back to the
  limit is still sharper than the blurred contour whereas falling back to the
  contour point throws the reconstruction away. Corner sharpness measures
  identical anywhere from a 2px to a 12px ceiling, so the ceiling costs nothing
  and only removes the failure.

Acceptance over the eight-artist set, against the polyline it replaces: ripple on
edges that are straight beyond argument 0.228 -> 0.015px, with 637 -> 8 interior
vertices spent on them; curvature reversals 6.7 -> 1.5 per 100px; corner turn-arc
8.03 -> 5.48px; 1557 -> 276 segments and 21.2 -> 9.5KB of path. Hot Toddy, the
mark whose terminals must not soften, goes 3.32 -> 0.25px with 0 of 34 corners
softened. Men I Trust improves least (9.7 -> 6.1 reversals) because its print
genuinely is crenellated and the path tracks it.

Cost is 18-33ms in the app's own engine, against contours() at 90-140ms. The old
polyline remains behind traceToSvg's `fit: false` for A/B.
This commit is contained in:
KeelrLeek committed 2026-07-27 06:37:08 -05:00
1 parent 4c0c4a1ea5
commit 0cc01328a4
1 file changed
+1047 -11
+1047 -11
View File
File diff suppressed because it is too large. Load diff