Skip to content

feat(render): solid arrowhead style + start-head render coverage#64

Merged
dancj merged 8 commits into
stagingfrom
feat/solid-arrowhead
Jul 7, 2026
Merged

feat(render): solid arrowhead style + start-head render coverage#64
dancj merged 8 commits into
stagingfrom
feat/solid-arrowhead

Conversation

@dancj

@dancj dancj commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Arrowheads can now render as solid filled triangles. headStyle: 'solid' closes the head path and fills it with the stroke color through rough.js's solid fill, so the hand-drawn look carries into the fill. The default open-V style is untouched — omitting headStyle (or passing 'line') renders byte-identical output, guarded by seed-deterministic tests.

A filled shape can't reveal through stroke-dashoffset, so solid heads fade in instead: each head's fill opacity rides its own outline's draw fraction, meaning the fill materializes while the outline sketches once the line reaches that head. Reduced-motion and static renders show it fully.

This branch also closes the render-level coverage gap where a start-only arrowhead was never exercised — the start-inset, socket-anchoring, and elbow leg-trim paths were previously only reached via head: 'both'.

Design notes

  • Fill reveals are group-tied, zero-length. rough.js emits a head's fill path before its outline strokes, so raw segment order can't drive the reveal. Each drawable gets a reveal group; a fill segment contributes no length to the draw budget and mirrors its own head group's stroke fraction (segmentFractions in src/draw.ts).
  • dashOffsets retired from production. applyProgress consumes per-segment fractions directly (dash offset for strokes, opacity for fills). The old numeric suite still guards the offset math via a test-local helper.
  • Hot path preserved. segmentFractions short-circuits to the previous two-pass, zero-allocation shape when no fill segment exists, so default line-style arrows pay nothing new per animation frame.
  • Redundant fill clobber removed. rough.js already stamps fill="none" on stroke paths, so appendDrawable no longer overwrites fills — which is also what lets a solid head's fill path survive.

Validation

Full suite green: 186 vitest tests across 15 files, including byte-identity guards for the default style (rendered d strings and fill attributes with headStyle omitted vs 'line'), animation-offset equivalence for stroke-only segments, per-head independent fade for head: 'both', and start-head anchoring at true socket coordinates. Typecheck, ESLint, Prettier, and tsup build all clean. Browser spot-check of the demo page was not run in this session (no browser tooling available); the solid style is visually verifiable there.

Related


Compound Engineering
Claude Code

dancj added 8 commits July 6, 2026 23:14
Closes the gap where hasStartHead was only ever true via 'both' (#61):
curve start inset, start-head socket anchoring, and elbow startTrim.
arrowHeadPath(closed=true) appends Z so the head can be filled (#60).
'solid' closes the head triangle and fills it with the stroke color via
rough.js fillStyle 'solid', keeping the hand-drawn look (#60). Default
'line' output is unchanged. Drops the redundant fill='none' clobber in
appendDrawable (rough.js already sets it on stroke paths) and tags each
segment with its drawable's reveal group for the fade-in that follows.
rough.js emits a head's fill path before its outline strokes, so raw
segment order can't drive sequential reveal. segmentFractions gives every
segment its revealed fraction: fill paths carry no length in the draw
budget and mirror their own head group's stroke fraction, fading in while
the outline dash-draws (#60). dashOffsets delegates to it, so stroke
timing is unchanged.
README gains headStyle next to head/headSize; plan for #60/#61 included.
…ions

Simplify pass: shared anchors()/paths() test helpers (pathDs derives from
paths); dashOffsets deleted from src (applyProgress uses segmentFractions
directly; its numeric suite now guards segmentFractions via a local offsets
helper); segments field typed via DrawSegment; segmentFractions skips group
bookkeeping when no fill segment exists (per-frame hot path).
Add integration test proving each solid head fades independently for
head:'both' via appendDrawable's real group-id wiring (review finding #1).
New section between obstacle avoidance and the tree reveal: head 'both'
with headStyle 'solid' so both filled heads fade in with their outlines.
Hardcoded section indexes for the tree and reduced-motion cards shift by
one.
@dancj
dancj merged commit 86ce82f into staging Jul 7, 2026
2 checks passed
@dancj
dancj deleted the feat/solid-arrowhead branch July 7, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant