Curve Decorations (2D)
Renderer-agnostic model for attaching glyphs (arrowheads, markers, ticks) to 2D paths using the path's local frame.
| feature id | status | description | PRs |
|---|---|---|---|
curve-decoration | partial | Endpoint markers (built-in presets) implemented; joins/mid-path in spec only | #538 |
Abstract
This working group defines curve decorations: how arbitrary glyphs (arrowheads, dots, diamonds, dimension ticks) are attached to a 2D path and oriented by the path's local tangent/normal. Raster backends such as Skia only provide three native stroke caps (butt/round/square); custom endpoint styles are implemented as explicit marker geometry. The model unifies endpoint markers, mid-path markers, and repeated directional symbols under a single, arc-length-based placement and local-frame semantics, and is intended to be compatible with SVG markers, Skia, PDF, and CAD-style pipelines.
Proposed direction (SVG-aligned, 2026 profile)
This proposal sets the normative direction for Grida markers:
- We adopt the SVG marker mental model as the compatibility baseline (
marker,marker-start,marker-mid,marker-end;orient=auto|auto-start-reverse). - We extend the baseline where modern authoring requirements are not met by SVG 1.1 era behavior.
The Working Group therefore adopts the following profile.
1) Endpoint cutback is first-class (not an authoring workaround)
SVG implementations traditionally rely on marker-space translation (refX-style tuning) to reduce overlap with the stroked path. That approach is useful but insufficient for professional editing workflows where marker attachment must remain terminal-accurate while stroke overlap is removed deterministically.
Grida therefore defines endpoint cutback as a core rendering semantic:
- start and end may each trim the stroked path independently
- marker placement remains anchored to the logical endpoint on the untrimmed path
- visual overlap behind the marker is eliminated by construction, not by manual offset guessing
2) Arc-length placement is normative (animation-grade placement)
Placement by segment parameter alone is not distance-stable and is not suitable as a primary model for motion or distribution semantics. This is especially visible when trying to emulate "slide along curve" behavior using marker translation controls.
Grida therefore makes arc-length-based placement normative for curve decorations:
- endpoint, explicit-position, and repeated placement semantics are defined on contour length
- animation and interpolation use distance-consistent placement
- curve-parameter placement may exist only as an explicit alternate mode
Reference demonstration of why refX translation does not provide arc-length-aware behavior:
3) Orientation remains SVG-compatible
To preserve interoperability and author expectations, orientation semantics remain aligned with SVG:
autoauto-start-reverse
Any additional orientation policy in Grida is additive and must not redefine the behavior of these two modes.
4) Two-level semantics: renderer-level parity, editor-level ergonomics
At the renderer/spec layer, Grida recognizes the SVG 2 annotation semantics:
marker(all applicable points)marker-startmarker-midmarker-end
At the Grida document layer (editor-first model), we expose:
markermarker_startmarker_end
marker_mid is intentionally omitted from the high-level document schema. If authors want mid-only behavior, they set marker and override start/end to none. This preserves expressiveness while keeping the authoring surface compact and predictable.
Motivation
Design tools commonly expose "line endpoints" like arrowheads, dots, diamonds, and dimension ticks. Those must be rendered as explicit marker geometry attached to a path, not as backend stroke caps.
This spec defines a renderer-agnostic model for attaching such glyphs to a 2D path using the path's local frame. It is intended to unify:
- endpoint markers (arrowheads, dots, squares, etc.)
- mid-path markers (at joins / along length)
- repeated directional symbols
- measurement / diagram markers
Path model and parameterization
Although we use "curve" informally, the engine deals with paths: piecewise contours comprised of line/quad/cubic segments.
We distinguish two parameterizations:
Curve parameter (segment parameter)
Each segment has its own parameter (e.g. Bézier ). This is useful for geometry math but not stable for placement by distance.
Arc-length parameter (recommended for placement)
For placement, we use arc-length measured along a contour, or its normalized fraction :
- where is the contour length
Unless explicitly stated otherwise, placement in this spec uses arc-length (absolute or normalized ).
Local frame on a path
For a placement position on a contour, we define:
- position:
- unit tangent:
- unit normal:
Tangent
At arc-length , tangent is the direction of travel along the contour:
Normal (2D convention)
To make "normal offset" unambiguous, we define the left normal:
This means normal offsets are relative to the path direction. If orientation reverses , also flips.
Degenerate tangent fallback
Real paths can contain degenerate segments (zero length, repeated points) where the tangent is undefined. Implementations should:
- skip orientation for that decoration, or
- search for the nearest non-zero tangent along the contour (preferred for endpoints/joins)
This document treats the fallback as an implementation policy, but the behavior must be deterministic.
Marker glyph
A MarkerGlyph is geometry defined in its own local "marker space".
Required properties:
- geometry: an arbitrary 2D path/primitive set (filled and/or stroked)
- anchor: a point in marker space that will be placed at
- forward axis: a unit vector in marker space that represents the glyph's "forward" direction
- cutback depth: a scalar in marker-space units (see § Cutback)
Optional properties:
- style override (fill/stroke/paint)
- intrinsic rotation offset (if is not the +X axis)
This is conceptually similar to SVG <marker> (refX/refY as anchor, orient as tangent alignment), but kept renderer-agnostic.
Anchor modes
Two anchor modes are defined (implementation also supports a parametric Offset along the forward axis):
- Terminal: the marker's forward edge or tip is placed at the evaluation point. The body extends in the direction. Suitable for endpoint decorations where the marker should sit flush with the logical path boundary.
- Centroid (also: center / node): the marker's geometric center or centroid is placed at the evaluation point. The body extends symmetrically in both directions. Suitable for mid-path or junction markers.
The anchor mode determines both the geometric placement of the marker and how the cutback depth is computed.
Placement
A Placement determines where decorations appear.
Attachment domains
For a given contour:
- start: contour start ()
- end: contour end ()
- joins: interior vertices (segment boundaries) on a piecewise path
- at: explicit arc-length positions (absolute ) or fractions (normalized )
- every: repeated placement at regular arc-length intervals
Notes:
- Endpoints are meaningful only for open contours. For closed contours,
startandendcoincide; endpoint placement is typically ignored or treated asat(u=0). joinsimplies a piecewise path model; joins do not exist on a single analytic curve without segmentation.
Arc-length vs parameter (important)
If a placement is specified by "parameter value" on a Bézier segment, it is not proportional to distance and is rarely what users expect. For design-tool semantics, arc-length placement should be the default.
If we ever expose curve-parameter placement, it should be a separate explicit mode (e.g. at_param).
Multi-contour paths
Paths may have multiple contours (subpaths). Placement resolution must specify whether it applies:
- per contour (SVG-style start/mid/end), or
- to a flattened "entire path" ordering
For initial 2D editor semantics, per contour is recommended.
Orientation policy
Orientation controls how marker space is rotated relative to the local frame.
Policies
- none: no tangent alignment (fixed world rotation)
- auto: align glyph forward axis to
- auto-start-reverse: like SVG
orient="auto-start-reverse"- end marker uses
- start marker uses (so it points outward)
Join tangent selection (for joins)
At a join there are two natural tangents:
- incoming: tangent approaching the vertex
- outgoing: tangent leaving the vertex
For orientation at joins, define one of:
incomingoutgoingbisector(angle bisector between incoming/outgoing; may require miter-limit style clamping)
Scale policy
Scale controls how marker glyphs size in world space.
- absolute: fixed world-unit size
- stroke-relative: proportional to effective stroke width at placement
If stroke width varies along the path (width profile), stroke-relative should use the local effective width at the placement position.
Offset
Offset is an optional translation relative to the local frame:
- tangent offset: along
- normal offset: along
World translation contribution:
Offsets are essential for:
- pulling an arrowhead "back" so the tip sits on the endpoint
- drawing dimension ticks slightly off the stroke centerline
Transform composition (conceptual)
For an arc-length position , with position , tangent , and marker anchor , the marker transform is conceptually:
Where:
- for
autopolicies - for
none, is a fixed world rotation - comes from the scale policy
- comes from offset (see Offset section)
The exact multiplication order depends on engine conventions, but the intent is: anchor → scale → orient → place.
Rendering semantics (policy-level)
Marker glyphs are separate geometry. Two practical semantics matter:
Draw order
Default: stroke path first, then draw markers on top. This matches most design tools.
Cutback / stroke trimming (endpoint-only)
When a marker with cutback depth is placed at an endpoint of an open contour, the renderer must shorten the stroked path by at that endpoint before computing the stroke outline.
Formally, for an open contour with arc-length :
- End marker with cutback : stroke the sub-path instead of .
- Start marker with cutback : stroke the sub-path instead of .
- Both: stroke the sub-path .
The marker itself is still placed at the original endpoint position (arc-length or ), evaluated on the untrimmed path. This ensures the marker's tip/center aligns with the logical endpoint while the stroke terminates cleanly at the marker's base/edge.
For closed contours and mid-path placements, cutback is not applied (the stroke continues on both sides of the placement point).
Cutback depth computation
The cutback depth is not a fixed constant per shape — it depends on both the marker geometry and the stroke width. The goal is to trim the stroke just enough so it does not visually bleed under the filled marker.
For a terminal-aligned filled marker, is the distance from the anchor (forward edge) to the point where the marker's boundary first clears the stroke's half-width (where is the stroke width).
Triangular / pointed shapes
For a shape with straight edges from the tip to a base at where is the depth and is the half-height:
clamped to . When , the stroke is wider than the marker and .
This applies to arrows, triangles, diamonds, and any convex pointed shape with linear edges.
Circular shapes
For a circle with radius and forward edge at the anchor:
When , the stroke is wider than the circle and (full diameter).
Rectangular shapes
For an axis-aligned rectangle (e.g. square, vertical bar), the edges are parallel to the stroke. The stroke cannot "peek around" the sides, so equals the full backward extent of the rectangle.
Open (stroked) markers
For markers that are stroked rather than filled (e.g. an open chevron), there is no filled silhouette to intersect with . Cutback is typically a fixed amount (e.g. proportional to stroke width) so the stroke path ends cleanly at the marker's visible base; the exact value is shape-dependent.
General case
For arbitrary marker geometry, the cutback can be computed by finding the -coordinate (in marker space, along ) of the intersection between the marker's silhouette boundary and the line . Implementations may approximate this via path bounds or analytic solutions for known shape classes.
Relationship to existing models
SVG markers
SVG is a specific instance of this model:
- placements: start/mid/end
- orientation:
auto/auto-start-reverse - marker anchor:
refX/refY - scaling:
markerUnits(oftenstrokeWidth)
Curve Decorations are intended to be at least as expressive, while remaining backend-agnostic.
Grida's explicit extensions over the historical SVG marker baseline are:
- endpoint cutback as a normative rendering behavior
- arc-length-first placement semantics for stable animation and distribution
Stroke caps (backend caps vs decorations)
Classic stroke caps (butt/round/square) are natively supported by many renderers (including Skia) and should usually remain a paint/stroke style property for performance and fidelity.
Custom "caps" (arrowheads, diamonds, circles, etc.) are best represented as endpoint placements of curve decorations.
In other words:
- backend cap styles are still used when no decoration is present at that endpoint
- curve decorations cover the generalized marker cases
- cap override: when a decoration is placed at an endpoint, the renderer should use butt cap at that endpoint so the native cap geometry does not show under the marker. The stroke is trimmed by the marker's cutback depth and the marker is drawn on top.
Minimal conceptual schema
Conceptually:
MarkerGlyph
├─ geometry: Path/Primitive
├─ anchor: Point
├─ forward_axis: Vec2
└─ cutback_depth: f32 (computed from geometry + stroke width)
CurveDecoration
├─ glyph: MarkerGlyph
├─ placement: Placement
├─ orient: OrientationPolicy
├─ scale: ScalePolicy
└─ offset: Offset
This decomposition covers common 2D editor needs without introducing overlapping primitives.
Implementation scope (document model)
The current document model exposes built-in presets only (no arbitrary glyph geometry in the wire format):
- LineNode:
marker_start_shape,marker_end_shape(enum per endpoint). - VectorNode: same endpoint model (
marker_start_shape,marker_end_shape) on the logical start/end of the vector path.
All built-in presets are terminal-aligned and stroke-relative in scale. The schema enum includes: None, RightTriangleOpen (open stroked chevron), EquilateralTriangle, Circle, Square, Diamond, VerticalBar. The engine supports the full anchor and cutback model internally (including Centroid and parametric Offset) for tests and future extensibility; see shape/marker.rs and golden examples.
When exposed as generalized marker fields in the Grida document format, the intended high-level API remains:
markermarker_startmarker_end
No marker_mid key is required at the editor-facing schema level.
Implementation note (Skia viability)
Skia provides arc-length traversal utilities that return contour length and position plus tangent at distance . Endpoint and along-path placement are therefore straightforward: evaluate the local frame at and for endpoint markers, and negate the tangent for start markers when using auto-start-reverse.
Design goals and non-goals
Design goals
The model aims to be:
- renderer-agnostic: Skia, SVG, PDF, CAD-style pipelines
- precise: explicit arc-length placement and local frame definitions
- extensible: repeated markers, join semantics, variable stroke widths
- collaboration-friendly: stable parameterization options for CRDT usage
Non-goals (initial scope)
Out of scope for this spec version:
- continuous extrusion / procedural brushes
- full along-curve ornament fields (texture-like decoration)
- 3D curve decorations
These can be layered later on top of the same "attach glyphs to a path" primitive.
Terminology
| Term | Meaning |
|---|---|
| Path / contour | Piecewise curve, possibly multiple subpaths |
| Curve Decoration | Glyph attached to a path via local frame evaluation |
| MarkerGlyph | Geometry in marker space with anchor + forward axis |
| Placement | Where to attach (endpoints, joins, arc-length positions, repeated) |
| Orientation policy | How to align glyph relative to tangent |
| Anchor mode | Terminal (forward-edge aligned) or Centroid (center-aligned); implementation also supports parametric Offset |
| Cutback depth | Distance by which the stroke is trimmed at a decorated endpoint; function of marker geometry and stroke width |