17 Geometry
struct
(struct vec2 (x y) #:transparent) x : finite-real? y : finite-real?
The constructor checks both fields. Infinite values and NaN values are rejected. The structure is immutable and transparent. The public bindings created for this structure include vec2, vec2?, vec2-x, and vec2-y.
procedure
(finite-real? value) → boolean?
value : any/c
procedure
(finite-complex? value) → boolean?
value : any/c
procedure
(real-lerp from to progress) → finite-real?
from : finite-real? to : finite-real? progress : finite-real?
procedure
(interpolable? value) → boolean?
value : any/c
procedure
(interpolate-value from to progress) → any/c
from : any/c to : any/c progress : (and/c finite-real? (>=/c 0) (<=/c 1))
17.1 Scene Parameters
procedure
(parameter id initial-value) → scene-parameter?
id : symbol? initial-value : any/c
procedure
(scene-parameter? value) → boolean?
value : any/c
procedure
(parameter-id value) → symbol?
value : scene-parameter?
procedure
(parameter-initial-value value) → any/c
value : scene-parameter?
procedure
(vec2-scale scalar value) → vec2?
scalar : finite-real? value : vec2?
procedure
from : vec2? to : vec2? progress : finite-real?
17.2 Path Geometry
Path geometry describes outlines and filled regions without using Pict. It supports straight line segments and cubic Bézier segments. Both kinds are semantic model values. They do not contain drawing-context commands or a rendered approximation.
A path may contain several subpaths. Subpath order is significant. Within each subpath, the start point comes first and segments follow in traversal order. Each segment begins where the preceding segment ended. A closed subpath reconnects its final endpoint to its start when it is rendered and measured. That implicit closing edge is straight.
Length, partial extraction, and morphing use local coordinates before a Visual’s scale, rotation, or translation is applied. Compound paths traverse one subpath fully before continuing with the next one. Morphing pairs subpaths and segments by their stored order; it does not reorder them automatically.
struct
(struct line-path-segment (end) #:transparent) end : vec2?
The structure is immutable and transparent. Its public bindings include line-path-segment, line-path-segment?, line-path-segment-end, and struct:line-path-segment.
struct
(struct cubic-bezier-path-segment (control1 control2 end) #:transparent) control1 : vec2? control2 : vec2? end : vec2?
The curve starts in the direction from its start toward control1. It approaches end from the direction of control2. Control points usually do not lie on the visible curve. They may lie outside the curve’s actual bounds.
The structure is immutable and transparent. Its public bindings include cubic-bezier-path-segment, cubic-bezier-path-segment?, the three field accessors, and struct:cubic-bezier-path-segment.
procedure
(path-segment? value) → boolean?
value : any/c
struct
(struct path-subpath (start segments closed?) #:transparent) start : vec2? segments : (listof path-segment?) closed? : boolean?
The start field is the first local point. The segments list contains segment values in significant traversal order. Each segment begins where the preceding segment ended. An empty segment list represents one point.
When closed? is true, rendering reconnects the last endpoint to start with an implicit straight edge. The edge is not stored as an extra segment. It participates in bounds, length, partial extraction, and rendering. A closed subpath can be filled. An open subpath is only stroked by the built-in Pict renderer.
The structure is immutable and transparent. Its public bindings include its constructor, predicate, three field accessors, and structure descriptor.
struct
(struct path-geometry (subpaths) #:transparent) subpaths : (listof path-subpath?)
An empty list is valid and represents geometry that draws nothing. The structure is immutable and transparent. Its public bindings include its constructor, predicate, field accessor, and structure descriptor.
procedure
(path-geometry-empty? geometry) → boolean?
geometry : path-geometry?
procedure
(path-subpath-points subpath) → (listof vec2?)
subpath : path-subpath?
procedure
(path-geometry-subpath-points geometry)
→ (listof (listof vec2?)) geometry : path-geometry?
procedure
(path-geometry-map-points geometry transform-point) → path-geometry? geometry : path-geometry? transform-point : (procedure-arity-includes/c 1)
This operation transforms local geometry only. It does not change a Visual’s reference position, rotation, or scale.
procedure
(path-geometry-translate geometry displacement) → path-geometry? geometry : path-geometry? displacement : vec2?
procedure
(path-geometry-reverse geometry) → path-geometry?
geometry : path-geometry?
For an open subpath, the former final endpoint becomes the new start. For a closed subpath, the original stored start is preserved so the loop changes direction without also changing phase. When the original implicit closing line has positive length, that line is materialized as the first explicit reversed segment; the resulting synthetic closing edge is then zero length. This representation detail can change segment count, but it does not change the visible or measured loop.
For a positive finite closed loop, point lookup on the reversed result follows the original loop at fraction (- 1 fraction) (subject to the same deterministic cubic arc-length approximation as path-geometry-point-at). Tangent direction is reversed correspondingly away from corner one-sided boundaries.
The operation is purely semantic and does not apply a containing Visual transform. Empty geometry and zero-length subpaths are valid.
procedure
(path-geometry->cubic geometry) → path-geometry?
geometry : path-geometry?
(vec2-lerp start end 1/3) (vec2-lerp start end 2/3)
The resulting cubic traces the same straight line in the same direction. An existing cubic segment is kept unchanged. Subpath starts, subpath order, segment order, endpoints, and closure values are preserved. Point-only subpaths and empty geometry are valid.
Only stored line segments are converted. The implicit straight closing edge of a closed subpath remains implicit and is not added to the segment list.
When geometry already contains no stored line segments, the procedure returns geometry itself. Otherwise it returns new immutable geometry.
procedure
(path-geometry-morph-normalizable? from to) → boolean?
from : path-geometry? to : path-geometry?
The paths are normalizable when all of the following hold:
They contain the same number of subpaths.
Corresponding subpaths have the same closure value.
Each pair of corresponding subpaths is either point-only on both sides or contains at least one stored segment on both sides.
Stored segment counts and line-versus-cubic kinds may differ. Coordinates and path lengths do not affect this predicate. The predicate does not change either path.
procedure
(path-geometry-align-for-morph source destination [ #:allow-reverse? allow-reverse? #:sample-count sample-count]) → path-geometry? source : path-geometry? destination : path-geometry? allow-reverse? : boolean? = #t sample-count : (and/c exact-integer? (>=/c 8)) = 64
Correspondence is measured in total-arc-length coordinates. The procedure samples source at sample-count evenly spaced fractions, scores candidate destination phases by mean Euclidean point distance, and chooses the lowest score deterministically. Candidate phases include a uniform full-loop grid and every stored positive-edge boundary of the destination. A fixed number of local refinement rounds then improves the best phase. The algorithm does not depend on frame rate, camera scale, rendering, wall-clock time, or randomness.
When reverse traversal is allowed, the same search is performed on (path-geometry-reverse destination). An exact score tie prefers the original forward traversal. Exact phase ties prefer the smaller phase. An already aligned destination therefore remains unchanged rather than being reversed or split gratuitously.
The returned value is ordinary semantic path geometry. Use it before path-geometry-normalize-for-morph when a caller wants explicit access to the chosen correspondence, or use morph-to-aligned to perform both steps during timeline compilation.
This operation deliberately handles one closed loop on each side. It does not pair multiple subpaths, add or remove subpaths, change closure, solve arbitrary global shape correspondence, or guarantee a globally optimal continuous phase for every pathological curve. Increasing sample-count makes the fixed deterministic geometric score finer.
procedure
(path-geometry-align-open-for-morph source destination [ #:allow-reverse? allow-reverse? #:sample-count sample-count]) → path-geometry? source : path-geometry? destination : path-geometry? allow-reverse? : boolean? = #t sample-count : (and/c exact-integer? (>=/c 8)) = 64
The procedure samples source and destination at sample-count evenly spaced total-arc-length fractions including both endpoints and scores mean Euclidean point distance. When allow-reverse? is true, the same score is computed for (path-geometry-reverse destination). Reverse traversal is selected only when its score is strictly lower; an exact score tie keeps the caller’s stored forward destination. There is no cyclic phase search for an open path.
When forward traversal wins, the exact destination object is returned. When reverse traversal wins, the returned geometry traces the same destination from its former endpoint toward its former start. The operation is pure semantic geometry and does not inspect Visual transforms, renderers, camera state, frame rate, pixels, or output files.
Use the result before path-geometry-normalize-for-morph for explicit preparation, or use morph-to-open-aligned for timeline compilation. This operation deliberately handles one open subpath on each side; it does not select cyclic phases, pair compound subpaths, change closure, or add/remove subpaths.
procedure
(path-geometry-align-open-compound-for-morph source destination [ #:allow-reverse? allow-reverse? #:sample-count sample-count]) → path-geometry? source : path-geometry? destination : path-geometry? allow-reverse? : boolean? = #t sample-count : (and/c exact-integer? (>=/c 8)) = 64
For every source/destination subpath pair, the procedure uses the SCENE-AE endpoint-direction score from path-geometry-align-open-for-morph: both paths are sampled at sample-count inclusive total-arc-length fractions, and destination reversal is selected only when its score is strictly lower. One source subpath’s samples are cached while every destination candidate for that assignment row is evaluated.
After all pair costs are known, the same deterministic minimum-total-cost assignment policy as SCENE-AD chooses one distinct destination subpath for every source subpath. Pairing is global rather than greedy. Exact assignment ties use the deterministic destination-index policy of path-geometry-align-compound-for-morph, while exact per-pair direction ties preserve the caller’s stored forward traversal.
The returned value is ordinary immutable path-geometry whose subpath order matches source correspondence. A destination subpath whose stored direction wins is reused exactly. When pairing and direction alignment change nothing, the exact destination object is returned.
Use the result with path-geometry-normalize-for-morph for explicit preparation, or use morph-to-open-compound-aligned for timeline compilation. This specialized operation deliberately requires all subpaths to be open and counts to match. Use path-geometry-align-mixed-compound-for-morph when matching-count open and closed topology is combined in one compound. Use path-geometry-prepare-topology-changing-morph or morph-to-topology-changing when topology-class counts differ.
procedure
(path-geometry-align-mixed-compound-for-morph source destination [ #:allow-reverse? allow-reverse? #:sample-count sample-count]) → path-geometry? source : path-geometry? destination : path-geometry? allow-reverse? : boolean? = #t sample-count : (and/c exact-integer? (>=/c 8)) = 64
The procedure partitions both paths by path-subpath-closed?. Open candidates are evaluated with the SCENE-AE endpoint-direction rule used by path-geometry-align-open-for-morph. Closed candidates are evaluated with the SCENE-AC phase/direction rule used by path-geometry-align-for-morph. The deterministic global assignment policy is then solved independently inside the open and closed classes. An open subpath is therefore never paired with a closed loop merely because it is spatially nearby.
After both assignments, selected destination subpaths are placed in the exact subpath order of source. Each open pair may reverse only for a strictly lower score; each closed pair may choose cyclic phase and optional reversal under SCENE-AC’s deterministic tie rules. Unchanged destination subpath objects are reused, and when the entire correspondence is already a no-op the exact destination object is returned.
When one topology class is absent, this operation reduces to the corresponding SCENE-AF all-open or SCENE-AD all-closed behavior. Use path-geometry-prepare-topology-changing-morph when either topology-class count differs.
Use the result with path-geometry-normalize-for-morph for explicit preparation, or use morph-to-mixed-compound-aligned for timeline compilation.
procedure
(path-geometry-prepare-topology-changing-morph source destination [ #:allow-reverse? allow-reverse? #:sample-count sample-count #:birth-anchor birth-anchor #:death-anchor death-anchor #:birth-anchor-map birth-anchor-map #:death-anchor-map death-anchor-map #:birth-penalty birth-penalty #:death-penalty death-penalty #:birth-penalty-map birth-penalty-map #:death-penalty-map death-penalty-map #:match-penalty-map match-penalty-map])
→
path-geometry? path-geometry? source : path-geometry? destination : path-geometry? allow-reverse? : boolean? = #t sample-count : (and/c exact-integer? (>=/c 8)) = 64 birth-anchor : (or/c symbol? vec2?) = 'bounds-center death-anchor : (or/c symbol? vec2?) = 'bounds-center birth-anchor-map : hash? = #hash() death-anchor-map : hash? = #hash()
birth-penalty : (or/c symbol? (and/c finite-real? (>=/c 0))) = 'forced
death-penalty : (or/c symbol? (and/c finite-real? (>=/c 0))) = 'forced birth-penalty-map : hash? = #hash() death-penalty-map : hash? = #hash() match-penalty-map : hash? = #hash()
Every real subpath on either side must have positive finite arc length. The whole source or destination may nevertheless be empty, allowing pure birth from empty geometry and pure death to empty geometry. Open and closed topology classes are handled independently and are never paired directly with one another.
Within each topology class, real open pairs use the SCENE-AE forward/reverse endpoint score and real closed pairs use the SCENE-AC phase/direction score. With the default 'forced value for both penalty keywords, the rectangular assignment pads only the forced class-count difference with zero-cost dummy slots; matching topology counts therefore reduce exactly to SCENE-AG as before.
SCENE-AJ adds an optional numeric policy. When both birth-penalty and death-penalty are finite nonnegative reals, the procedure solves an augmented global assignment. A real source/destination edge costs its geometric correspondence score, a source-to-dummy edge costs death-penalty, a dummy-to-destination edge costs birth-penalty, and unused dummy pairs cost zero. A poor real pair may therefore be replaced by death plus birth even when topology counts match. Exact primary-cost ties minimize the number of birth/death edges as a secondary objective, so equal-cost replacement does not occur. The two penalty keywords must either both be 'forced or both be finite nonnegative real numbers. SCENE-AL additionally accepts sparse birth-penalty-map and death-penalty-map hashes in numeric mode. Birth-map keys are exact nonnegative original destination subpath indexes; death-map keys are exact nonnegative original source subpath indexes. Map values are finite nonnegative real costs and missing keys fall back to the corresponding shared numeric penalty. Nonempty penalty maps are rejected in 'forced mode. The sparse costs replace dummy-edge costs only. SCENE-AM additionally accepts match-penalty-map in both forced and numeric modes. Each key is (cons source-index destination-index) using original caller subpath indexes, and each finite nonnegative value is added to that real edge’s existing geometric correspondence score. Missing pairs add zero. Pair penalties do not change topology classes or direction/phase alignment; they bias the global assignment after those per-edge geometric choices are scored. In numeric mode a penalized real edge also competes against death plus birth, while AJ’s exact-tie preference for fewer topology changes remains unchanged. Direct preparation rejects out-of-range pair indexes and keys that name impossible open/closed correspondence edges. allow-reverse? and sample-count retain their existing correspondence meanings.
A destination subpath assigned to a dummy source is a birth. By default its prepared source is a one-line-segment degenerate subpath at the exact axis-aligned bounds center of that destination subpath. A source subpath assigned to a dummy destination is a death and receives the analogous prepared destination seed at its own bounds center. Each seed preserves the real subpath’s path-subpath-closed? value. The controlled seeds have zero arc length; pre-existing zero-length real subpaths are rejected before assignment.
SCENE-AI extends this placement with birth-anchor and death-anchor. Each accepts exactly 'bounds-center or a finite vec2. An explicit point is local path geometry and is shared by every unmatched subpath on that side. SCENE-AK additionally accepts sparse birth-anchor-map and death-anchor-map hashes. Birth-map keys are exact nonnegative original destination subpath indexes; death-map keys are exact nonnegative original source subpath indexes. Map values use the same anchor syntax. A missing key falls back to the corresponding shared anchor, while an explicit 'bounds-center map value may override a shared vec2. Direct preparation rejects out-of-range keys. Anchor selection affects seed placement only; real-pair scores, direction/phase correspondence, penalties, and slot ordering are unchanged. In numeric SCENE-AJ penalty mode the selected assignment may contain additional voluntary unmatched slots, and those slots use the same original-index map lookup.
All slots corresponding to real source subpaths remain first in exact source order. Birth-only slots are appended in exact caller destination order. When open and closed class counts already match under the default forced-only policy, the operation reduces exactly to path-geometry-align-mixed-compound-for-morph, returning the original source as the first value. Numeric penalties may intentionally produce additional interior slots even when endpoint counts match.
Use the two results with path-geometry-normalize-for-morph for explicit preparation, or use morph-to-topology-changing for timeline compilation. This stage does not infer semantic holes, pair an open subpath directly with a closed loop, use appearance-aware scores, or accept arbitrary per-pair scoring callbacks beyond SCENE-AM sparse numeric additions.
procedure
(path-geometry-align-compound-for-morph source destination [ #:allow-reverse? allow-reverse? #:sample-count sample-count]) → path-geometry? source : path-geometry? destination : path-geometry? allow-reverse? : boolean? = #t sample-count : (and/c exact-integer? (>=/c 8)) = 64
The procedure computes every source/destination pair cost with the SCENE-AC closed-loop algorithm used by path-geometry-align-for-morph. Thus each candidate pair may choose a cyclic phase and, when allow-reverse? is true, reverse traversal. One source loop’s score samples are cached while all destination candidates for that source are evaluated.
After all pair costs are known, a deterministic minimum-total-cost assignment selects one distinct destination subpath for every source subpath. Pairing is global rather than greedy. Exact assignment ties preserve earlier source-row matches when an equally good free destination exists and then prefer the lower destination index. Per-pair direction and phase ties retain the SCENE-AC rules.
The returned value is ordinary immutable path-geometry whose subpath order matches source correspondence. When pairing and per-loop alignment change nothing, the exact destination object is returned. Otherwise untouched destination subpath objects are reused whenever possible.
Use the result with path-geometry-normalize-for-morph for explicit preparation, or use morph-to-compound-aligned for timeline compilation. This stage does not add/remove subpaths, pair open subpaths, infer semantic hole nesting, or support topology changes.
procedure
(path-geometry-normalize-for-morph from to)
→
path-geometry? path-geometry? from : path-geometry? to : path-geometry?
Normalization works independently on corresponding subpaths:
Every stored line segment is converted to an equivalent cubic Bézier segment using path-geometry->cubic.
The target segment count is the larger of the two stored segment counts.
The side with fewer segments repeatedly splits its longest current cubic at parameter 1/2 until the counts match.
Segment length is the same deterministic approximate cubic arc length used by path-subpath-length. When lengths tie, the earliest segment in traversal order is split.
De Casteljau subdivision preserves the exact cubic curve. Splitting therefore changes representation and correspondence, not the traced source or destination figure. Both returned paths contain only cubic stored segments, and path-geometry-morph-compatible? returns #t for them. Point-only corresponding subpaths remain point-only.
The implicit closing edge of a closed subpath is not stored, converted, or split. Closure remains represented by path-subpath-closed?. Subpath order and stored starting points are preserved.
This is deliberately limited normalization. It does not add or remove subpaths, change closure, turn a point-only subpath into a drawn path, reverse traversal, rotate the starting point of a closed path, reorder subpaths, or choose a geometric best match. A difference requiring one of those operations raises an exception that identifies the first unsupported subpath difference. Reported indexes start at zero.
Example:
(define triangle (polygon-path (list (vec2 -3 -1) (vec2 3 -1) (vec2 0 2)))) (define rectangle (polygon-path (list (vec2 -3 -2) (vec2 3 -2) (vec2 3 2) (vec2 -3 2)))) (define-values (normalized-triangle normalized-rectangle) (path-geometry-normalize-for-morph triangle rectangle)) (path-geometry-morph-compatible? normalized-triangle normalized-rectangle)
procedure
(path-geometry-morph-compatible? from to) → boolean?
from : path-geometry? to : path-geometry?
Compatibility requires all of the following:
The paths contain the same number of subpaths.
Each corresponding subpath has the same closed? value.
Each corresponding subpath contains the same number of segments.
Each corresponding segment has the same kind: line with line, or cubic Bézier with cubic Bézier.
Subpaths and segments are paired by their stored order. Only stored segments are counted. A closed subpath’s implicit closing edge is represented by its closed? value, not by another stored line segment. Coordinates, path lengths, and style do not affect compatibility. Empty geometry is compatible with empty geometry. Point-only subpaths are compatible when their closure values correspond.
The procedure remains strict. It does not insert segments, convert lines to cubics, reverse a path, rotate a closed path’s starting point, or reorder subpaths. Use path-geometry-morph-normalizable? and path-geometry-normalize-for-morph for the limited explicit normalization provided by this version.
procedure
(path-geometry-lerp from to progress) → path-geometry?
from : path-geometry? to : path-geometry? progress : (real-in 0 1)
progress must be a finite real in the closed unit interval. A value of 0 returns from itself. A value of 1 returns to itself. An interior value returns new path geometry.
The interpolation uses local mathematical coordinates. It does not change a Visual’s identity, reference position, rotation, scale, fill, stroke, stroke width, or drawing order. Use morph-to to place this interpolation on a timeline.
When the paths are incompatible, the procedure raises an exception describing the first mismatch in deterministic traversal order. The diagnostic identifies a subpath-count, closure, segment-count, or segment-kind mismatch. Reported subpath and segment indexes start at zero. The procedure remains strict and does not normalize incompatible paths automatically. Normalize explicitly or use morph-to-normalized for the limited supported cases.
Example:
(define rectangle-path (polygon-path (list (vec2 -2 -1) (vec2 2 -1) (vec2 2 1) (vec2 -2 1)))) (define diamond-path (polygon-path (list (vec2 0 -2) (vec2 3 0) (vec2 0 2) (vec2 -3 0)))) (path-geometry-lerp rectangle-path diamond-path 1/2)
procedure
(path-geometry-bounds geometry) →
finite-real? finite-real? finite-real? finite-real? geometry : path-geometry?
Line bounds come from their endpoints. Cubic bounds include their endpoints and every interior parameter where the x or y derivative is zero. The result therefore follows the curve itself instead of using the usually larger box of its control points. The result is mathematically tight subject to the ordinary numeric behavior of the supplied coordinates.
The implicit straight closing edge of a closed subpath is included. Empty geometry has no bounds and raises an exception.
procedure
(path-geometry-center geometry) → vec2?
geometry : path-geometry?
procedure
(path-subpath-length subpath) → (and/c real? (>=/c 0))
subpath : path-subpath?
Cubic length is computed by repeatedly splitting the curve in half. For each piece, the implementation compares the control-polygon length with the chord length. Subdivision stops when their difference is at most the larger of 1e-10 world units and 1e-8 times the original cubic’s control-polygon length, or after 20 subdivisions. The piece estimate is the average of its chord and control-polygon lengths. These constants are part of the current numeric behavior, but they are not a formal error guarantee.
A point-only subpath has length zero. Repeated adjacent points and completely degenerate cubics contribute zero. The affine transform of a containing Visual is not applied. The line distance calculation avoids unnecessary overflow, but a true distance beyond the inexact number range can produce +inf.0.
procedure
(path-geometry-length geometry) → (and/c real? (>=/c 0))
geometry : path-geometry?
This is a geometric model operation. A non-uniform scale on a path Visual can change the displayed world-space length, but it does not change the value returned here. The result can be +inf.0 when an inexact distance is too large to represent. create, uncreate, and non-full partial extraction require a finite result.
procedure
(path-geometry-point-at geometry fraction) → vec2?
geometry : path-geometry? fraction : (real-in 0 1)
Traversal uses the same significant edge order and length model as path-geometry-partial. Line portions use exact Euclidean distance. A point inside a cubic uses the same deterministic adaptive arc-length table described for path-subpath-length to approximate the corresponding curve parameter. The returned value is a semantic local point; no containing Visual transform is applied.
Compound path geometry is traversed subpath by subpath in stored order. Zero-length edges consume no positive fraction. The implicit closing edge of a closed subpath is included. Exact 0 returns the start of the first positive-length edge, and exact 1 returns the endpoint of the final positive-length traversal edge. At an exact boundary between positive subpaths, the endpoint of the preceding subpath is selected.
Empty geometry, zero-total-length geometry, or a non-finite computed total length raises an exception. move-along-path adds a stricter continuity rule and accepts only one positive-length subpath as a motion route.
procedure
(path-geometry-tangent-at geometry fraction) → vec2? geometry : path-geometry? fraction : (real-in 0 1)
Line tangents are normalized directed edge vectors. Cubic tangents are computed from the cubic derivative at the parameter selected by the same adaptive arc-length table used for point lookup. When that derivative is zero at a stationary endpoint or cusp, deterministic one-sided curve probes recover the forward traversal direction when one exists. The result is semantic local geometry; no containing Visual transform is applied.
Empty geometry, zero-total-length geometry, a non-finite computed total length, or a selected positive-length cubic point with no recoverable traversal direction raises an exception.
procedure
(path-geometry-normal-at geometry fraction) → vec2?
geometry : path-geometry? fraction : (real-in 0 1)
procedure
(path-geometry-offset geometry distance [ #:join join #:miter-limit miter-limit]) → path-geometry? geometry : path-geometry? distance : finite-real? join : (or/c 'miter 'bevel 'round) = 'miter miter-limit : (and/c finite-real? (>=/c 1)) = 4
For an outside corner, join selects how adjacent shifted edge lines are connected. 'miter extends them to their intersection. The distance from the original vertex to that intersection is divided by the absolute offset distance; when the ratio exceeds miter-limit, the outside corner falls back to bevel. 'bevel connects the shifted edge endpoints with one straight segment. 'round uses one or more cubic Bézier circular-arc approximations centered at the original vertex, with no cubic spanning more than a quarter turn.
Inside corners always use the natural intersection of the two shifted lines, independent of join. This keeps the offset path traversing forward along both adjacent lines; the short centered arc on the inside would have the wrong endpoint tangents. Collinear same-direction edges share their shifted point.
Open subpath endpoints are shifted by their endpoint edge normals. Closed subpaths are joined cyclically, including the stored start vertex. Every edge participating in a nonzero offset must be a positive-length line-path-segment. A zero-length edge, an exact 180-degree reversal, or a cubic source segment raises an exception in this stage. Cubic segments may still appear in the result as round-join pieces.
The construction is geometric rather than renderer-dependent: camera scale, stroke width, and output resolution do not affect the generated path. The result has its own arc length, so move-along-path with linear easing moves uniformly along the joined offset route itself.
procedure
(path-geometry-partial geometry start end) → path-geometry?
geometry : path-geometry? start : (real-in 0 1) end : (real-in 0 1)
Fractions apply to the whole compound path. Traversal completes each subpath before beginning the next. A cut inside a line is inserted by linear interpolation. A cut inside a cubic uses the same deterministic adaptive length table as path-subpath-length to approximate the corresponding curve parameter. The selected cubic interval is then extracted with de Casteljau subdivision. The result remains a cubic segment; it is not replaced by a polyline or a Pict command.
A partially selected closed subpath becomes open. Its implicit closing edge is part of the traversal and may itself be returned as an open line segment. A closed subpath keeps its closure only when its complete traversal is selected. This rule prevents a partial prefix from being filled as if it were complete.
When start and end are equal, the result is empty-path-geometry. The exact interval from 0 through 1 returns geometry unchanged. This preserves segment types, control points, point-only subpaths, and other zero-length structure. For a zero-length path, every other interval returns empty geometry.
Zero-length edges and subpaths do not consume a positive portion of the arc length. They can therefore disappear from a non-full partial result. Any non-full partial extraction requires the computed total path length to be finite. Extremely large inexact coordinates whose distance overflows raise an exception here.
Examples:
(define mixed-path (path-geometry (list (path-subpath origin (list (line-path-segment (vec2 2 0)) (cubic-bezier-path-segment (vec2 3 2) (vec2 4 2) (vec2 5 0))) #f)))) (path-geometry-partial mixed-path 0 1/2)
procedure
(path-geometry-cycle-start geometry fraction) → path-geometry? geometry : path-geometry? fraction : (real-in 0 1)
geometry must contain exactly one closed subpath with positive finite length. Exact 0 and 1 return geometry unchanged. For any other fraction, the selected point becomes both fraction zero and fraction one of the returned closed loop.
When the selected point lies inside a line, the line is split by interpolation. When it lies inside a cubic, the arc-length table chooses the deterministic curve parameter and de Casteljau subdivision splits the curve. The tail from the selected point through the original closing edge is followed by the old prefix back to the selected point. The returned subpath remains closed; its final synthetic closing edge has zero length.
The operation deliberately handles one closed subpath rather than assigning one phase to every subpath of compound geometry. Automatic per-subpath phase selection is a separate correspondence problem. The returned value is ordinary path geometry and can be used directly by make-path-visual, move-along-path, orient-along-path, camera following, partial extraction, and normalized morph preparation.
procedure
(polyline-path points) → path-geometry?
points : (listof vec2?)
The points are local coordinates. Use make-path-visual to choose the reference position in the Visual’s containing coordinate system. That system is world space at the top level and group-local space for a child.
procedure
(polygon-path points) → path-geometry?
points : (listof vec2?)
Point order determines traversal direction and can affect later path operations. The built-in renderer uses the odd-even fill rule, so clockwise and counter-clockwise order produce the same simple fill in this version.
procedure
(cubic-bezier-path start segments [ #:closed? closed?]) → path-geometry? start : vec2?
segments :
(and/c pair? (listof cubic-bezier-path-segment?)) closed? : boolean? = #f
When closed? is true, the subpath also has an implicit straight edge from its final endpoint back to start. A smooth closed cubic shape normally includes a final cubic whose endpoint is start and also sets closed? to true so that the built-in renderer fills it. In that case, the implicit closing edge has length zero.
Example:
(define arch (cubic-bezier-path (vec2 -2 0) (list (cubic-bezier-path-segment (vec2 -2 2) (vec2 2 2) (vec2 2 0)))))
17.2.1 General Boolean Path Geometry and Clipping
SCENE-DY extends the immutable Boolean path operations to simple concave and compound closed paths. Each cubic contour is uniformly sampled into #:curve-samples line pieces before clipping, so curve results are deterministic polygonal approximations rather than exact Bézier intersections. Each input contour must still be simple and closed.
The default 'odd-even fill rule matches the path renderers: nested contours alternate between filled regions and holes regardless of their orientation. 'nonzero additionally supports ordinary oriented, nonintersecting contour nests, where reversing an inner loop makes it a hole. The implementation triangulates internally but reconstructs exterior and hole loops before returning a path-geometry?, so applying a cosmetic stroke does not reveal triangulation seams.
procedure
(path-union first second [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? first : path-geometry? second : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
procedure
(path-intersection first second [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? first : path-geometry? second : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
procedure
(path-difference first second [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? first : path-geometry? second : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
procedure
(path-xor first second [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? first : path-geometry? second : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
procedure
(cutout outer inner [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? outer : path-geometry? inner : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
procedure
(clip-to subject clip [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? subject : path-geometry? clip : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
clip-to also accepts an affine Visual as subject and a local path-geometry? as clip. In that form, supply #:id to receive a clipped-visual?: a normal affine/opacity Visual that clips the complete vector content at render time. Its content and clip path move, rotate, scale, and fade together. The optional Boolean quality and fill rule keywords apply only to the two-path form.
procedure
(mask-with subject mask [ #:curve-samples curve-samples #:fill-rule fill-rule]) → path-geometry? subject : path-geometry? mask : path-geometry? curve-samples : exact-positive-integer? = 16 fill-rule : (or/c 'odd-even 'nonzero) = 'odd-even
procedure
(clip-visual content path #:id id [ #:center center #:rotation rotation #:scale scale #:opacity opacity]) → clipped-visual? content : affine-visual? path : path-geometry? id : symbol? center : vec2? = origin rotation : finite-real? = 0 scale : scale-factor? = 1 opacity : opacity? = 1
procedure
(clipped-visual? value) → boolean?
value : any/c
Current limits: 'odd-even repairs contours with proper self-crossings, but rejects touching or overlapping segments; 'nonzero rejects crossing contour boundaries; and cubic input is flattened rather than preserved as cubic output.
17.3 Affine Transforms
Affine-transform values are created through make-affine-transform. The raw structure constructor is not part of the public API.
procedure
(affine-transform? value) → boolean?
value : any/c
procedure
(affine-transform-translation transform) → vec2?
transform : affine-transform?
procedure
(affine-transform-rotation transform) → finite-real?
transform : affine-transform?
procedure
(affine-transform-scale transform) → vec2?
transform : affine-transform?
procedure
(scale-factor? value) → boolean?
value : any/c
procedure
(scale-factor->vec2 value) → vec2?
value : scale-factor?
procedure
(make-affine-transform [ #:translation translation #:rotation rotation #:scale scale]) → affine-transform? translation : vec2? = origin rotation : finite-real? = 0 scale : scale-factor? = 1
procedure
(affine-transform-with-translation transform translation) → affine-transform? transform : affine-transform? translation : vec2?
procedure
(affine-transform-with-rotation transform rotation) → affine-transform? transform : affine-transform? rotation : finite-real?
procedure
(affine-transform-with-scale transform scale) → affine-transform? transform : affine-transform? scale : scale-factor?
procedure
(affine-transform-lerp from to progress) → affine-transform?
from : affine-transform? to : affine-transform? progress : (and/c finite-real? (>=/c 0) (<=/c 1))
Rotation is interpolated as an ordinary number. The procedure does not choose the shortest path around a circle. For example, interpolation from 0 to (* 2 pi) describes one full turn.
procedure
(affine-transform-apply-vector transform vector) → vec2? transform : affine-transform? vector : vec2?
procedure
(affine-transform-apply-point transform point) → vec2? transform : affine-transform? point : vec2?
17.3.1 General Affine Maps
SCENE-CY-A keeps affine-transform as the established decomposed placement protocol and adds linear2 and affine2 for general mathematical maps. A linear2 value represents the matrix
| a b | | c d |
acting on column vectors. Matrix entries may be negative or form a singular matrix. In particular, entry-wise interpolation toward a reflection normally passes through a singular map.
The public constructor follows the displayed row order, which keeps matrix literals readable when split across lines:
(linear2 a b c d)
An affine2 adds the translation column in the same order:
(affine2 a b h c d k)
This represents (values (+ (* a x) (* b y) h) (+ (* c x) (* d y) k)).
procedure
a : finite-real? b : finite-real? c : finite-real? d : finite-real?
procedure
(make-linear2 a b c d) → linear2?
a : finite-real? b : finite-real? c : finite-real? d : finite-real?
value
procedure
(linear2-a map) → finite-real?
map : linear2?
procedure
(linear2-b map) → finite-real?
map : linear2?
procedure
(linear2-c map) → finite-real?
map : linear2?
procedure
(linear2-d map) → finite-real?
map : linear2?
procedure
(linear2-determinant map) → finite-real?
map : linear2?
procedure
(linear2-invert map) → (or/c linear2? #f)
map : linear2?
procedure
(linear2-compose outer inner) → linear2?
outer : linear2? inner : linear2?
procedure
(linear2-apply-vector map vector) → vec2?
map : linear2? vector : vec2?
procedure
a : finite-real? b : finite-real? h : finite-real? c : finite-real? d : finite-real? k : finite-real?
procedure
(make-affine2 [ #:linear linear #:translation translation]) → affine2? linear : linear2? = identity-linear2 translation : vec2? = origin
value
procedure
(affine2-linear map) → linear2?
map : affine2?
procedure
(affine2-translation map) → vec2?
map : affine2?
procedure
(affine2-a map) → finite-real?
map : affine2?
procedure
(affine2-b map) → finite-real?
map : affine2?
procedure
(affine2-h map) → finite-real?
map : affine2?
procedure
(affine2-c map) → finite-real?
map : affine2?
procedure
(affine2-d map) → finite-real?
map : affine2?
procedure
(affine2-k map) → finite-real?
map : affine2?
procedure
(affine2-with-linear map linear) → affine2?
map : affine2? linear : linear2?
procedure
(affine2-with-translation map translation) → affine2?
map : affine2? translation : vec2?
procedure
(affine2-invert map) → (or/c affine2? #f)
map : affine2?
procedure
(affine2-compose outer inner) → affine2?
outer : affine2? inner : affine2?
procedure
(affine2-lerp from to progress) → affine2?
from : affine2? to : affine2? progress : (and/c finite-real? (>=/c 0) (<=/c 1))
procedure
(affine2-apply-vector map vector) → vec2?
map : affine2? vector : vec2?
procedure
(affine2-apply-point map point) → vec2?
map : affine2? point : vec2?
procedure
(affine-transform->affine2 transform) → affine2?
transform : affine-transform?
17.4 Cameras
A camera is an immutable orthographic view. Its pixel dimensions, visible world width, center, and background are explicit values. The raw camera constructor is not public; use make-camera. A scene can store and animate camera values without mutating them.
procedure
(camera-width camera) → exact-positive-integer?
camera : camera?
procedure
(camera-height camera) → exact-positive-integer?
camera : camera?
procedure
(camera-world-width camera) → (and/c finite-real? positive?)
camera : camera?
procedure
(camera-center camera) → vec2?
camera : camera?
procedure
(camera-background camera) → any/c
camera : camera?
procedure
(make-camera [ #:width width #:height height #:world-width world-width #:center center #:background background]) → camera? width : exact-positive-integer? = 1280 height : exact-positive-integer? = 720 world-width : (and/c finite-real? positive?) = 14 center : vec2? = origin background : any/c = "white"
value
procedure
(camera-scale camera) → (and/c real? positive?)
camera : camera?
procedure
(camera-world-height camera) → (and/c real? positive?)
camera : camera?
procedure
(camera-length->pixels camera length) → real?
camera : camera? length : finite-real?
procedure
(camera-world->pixel camera point) →
real? real? camera : camera? point : vec2?
procedure
(camera-pan-to center) → camera-pan-to-request?
center : vec2?
procedure
(camera-pan-to-request? value) → boolean?
value : any/c
procedure
(camera-pan-by delta) → camera-pan-by-request?
delta : vec2?
procedure
(camera-pan-by-request? value) → boolean?
value : any/c
procedure
(camera-zoom-to world-width) → camera-zoom-to-request?
world-width : (and/c finite-real? positive?)
procedure
(camera-zoom-to-request? value) → boolean?
value : any/c
procedure
(camera-zoom-by factor) → camera-zoom-by-request?
factor : (and/c finite-real? positive?)
Compilation raises an exception if the division produces a nonpositive or non-finite visible width.
procedure
(camera-zoom-by-request? value) → boolean?
value : any/c
procedure
(camera-follow target) → camera-follow-request?
target : (or/c visual? symbol?)
Frame-space Visuals are not valid follow targets; camera following is defined only for world-space top-level Visuals. A SCENE-AW derived target is resolved against the same sampled scalar state before its world-space position is read.
At each scene sample, following reads the target’s actual sampled visual-position at the same eased progress as its Visual animations. The pre-removal motion state is retained for camera completion, so the request can follow a Visual introduced by fade-in or create, and it can follow a Visual until a same-clip fade-out or uncreate removes it at the structural endpoint. The sampled visual-position result must be a vec2.
Following preserves normalized horizontal and vertical frame position. When a zoom request runs in the same clip, the camera adjusts its world-space offset as the visible width changes, so the target remains at the same pixel coordinates. Because the sampled Visual state supplies the position, the camera also follows nonlinear motion such as move-along-path through a bend or curve rather than interpolating only between clip endpoints. The request changes the camera-center component and may run with one zoom request. It conflicts with pan, fitting, or another follow request.
Following tracks only visual-position, not the target’s rendered box, rotation, scale, or shape. It lasts for one play clip. It does not install a persistent observer. Camera-only clips without following do not require scene state sampling.
procedure
(camera-follow-request? value) → boolean?
value : any/c
procedure
(camera-fit-request? value) → boolean?
value : any/c
procedure
(camera-fit-request-center request) → vec2?
request : camera-fit-request?
procedure
(camera-fit-request-world-width request)
→ (and/c finite-real? positive?) request : camera-fit-request?
procedure
(camera-fit-layout-box box [ #:camera camera #:padding padding]) → camera-fit-request? box : layout-box? camera : camera? = default-camera padding : (and/c finite-real? (>=/c 0)) = 1/2
the box width plus padding on both horizontal sides; and
the width needed to contain the padded box height while preserving the pixel aspect ratio of camera.
Padding is measured in world units and is applied equally on all four sides before aspect-ratio correction. Pixel width, pixel height, and background are preserved. A zero-size box with zero padding is rejected because it would produce a nonpositive visible width.
The result is a snapshot request. It stores only the concrete target center and visible width; it does not retain box or recompute it during animation. Use a camera with the same pixel aspect ratio as the scene camera that will run the request. The stored fit is not recomputed for a different aspect ratio.
procedure
(camera-fit-visuals visuals [ #:camera camera #:renderers renderers #:padding padding]) → camera-fit-request? visuals : (and/c (listof visual?) pair?) camera : camera? = default-camera renderers : pict-renderer-list? = default-pict-renderers padding : (and/c finite-real? (>=/c 0)) = 1/2
Measurement uses camera and the first-supporting renderer rule from renderers. Custom renderer padding therefore participates in fitting. A standalone derived-visual? cannot be measured here because this function has no scene-state scalar context; use camera-fit-scene for a derived top-level target. Measuring a nonempty formula through the built-in formula renderer can invoke LaTeX and Poppler. Opacity does not change the measured Pict dimensions.
The values are measured when this function is called. Geometry or renderer changes later in the same clip do not cause automatic remeasurement. The fit is also not recomputed at its target zoom. A custom renderer whose Pict has a fixed pixel size can therefore occupy a different world-space size at the endpoint. To fit a planned endpoint in the same clip, supply Visual values and a measurement camera that describe the intended view as closely as possible.
procedure
(camera-fit-scene scene [ #:targets targets #:renderers renderers #:padding padding]) → camera-fit-request? scene : scene?
targets :
(or/c false/c (and/c pair? (listof (or/c visual? symbol? visual-path?)))) = #f renderers : pict-renderer-list? = default-pict-renderers padding : (and/c finite-real? (>=/c 0)) = 1/2
A scene with no world-space Visuals, an empty target list, a missing target, or an explicitly selected frame-space target raises an exception. The result is a snapshot of the current endpoint state and does not follow later scene changes.
procedure
(camera-focus scene focus [ #:context context #:renderers renderers #:padding padding]) → camera-fit-request? scene : scene? focus : (or/c visual? symbol? visual-path?) context : (listof (or/c visual? symbol? visual-path?)) = '() renderers : pict-renderer-list? = default-pict-renderers padding : (and/c finite-real? (>=/c 0)) = 1/2
All selected targets are measured in fully composed world coordinates. This makes an imported SVG element or formula/group child a useful focus subject without rebuilding its parent. Frame-space targets are rejected. The request is a current-scene snapshot: it does not choose context automatically, remeasure during the clip, or live-follow later subject/context motion.
17.5 Frame-Space Overlays and Callouts
Frame-space Visuals are semantic wrappers, not cached Picts. They remain ordinary top-level Visuals with stable identity and can participate in move-to, rotation, scale, opacity, structural fade, and ordinary scene ordering. Their reference positions are interpreted in frame coordinates by the adapter rather than world coordinates.
procedure
(frame-space-visual? value) → boolean?
value : any/c
procedure
(frame-space-visual-frame-width visual)
→ (and/c finite-real? positive?) visual : frame-space-visual?
procedure
(frame-space-camera camera frame-width) → camera?
camera : camera? frame-width : (and/c finite-real? positive?)
procedure
(fixed-in-frame content [ #:camera camera #:at position #:rotation rotation #:scale scale #:opacity opacity]) → fixed-in-frame-visual? content : visual? camera : camera? = default-camera position : (or/c vec2? false/c) = #f rotation : finite-real? = 0 scale : scale-factor? = 1 opacity : opacity? = 1
The wrapper snapshots (camera-world-width camera). If position is #f, its initial frame position is (vec2- (visual-position content) (camera-center camera)). Therefore a Visual wrapped with the camera through which it is currently being viewed keeps the same pixel position and size at the moment it becomes fixed. An explicit position is interpreted directly in the captured origin-centered frame coordinate system.
Later world-camera pan and zoom do not change the overlay’s frame position or local rendering scale. Animated output pixel dimensions are not supported by the camera model, but a static rendering override with different pixel dimensions scales frame content with the output frame. Frame-space wrappers cannot be nested inside one another and must remain top-level scene Visuals. For a compound overlay, construct an ordinary group first and wrap the complete group.
procedure
(fixed-in-frame-visual? value) → boolean?
value : any/c
procedure
(fixed-in-frame-visual-content visual) → visual?
visual : fixed-in-frame-visual?
procedure
(camera-view [ target] #:id id [ #:targets targets] #:camera camera [ #:frame-camera frame-camera #:at position #:width width #:clip clip #:opacity opacity]) → camera-view-visual? target : (or/c false/c visual? symbol? visual-path?) = #f id : symbol?
targets :
(or/c false/c (and/c pair? (listof (or/c visual? symbol? visual-path?)))) = #f camera : camera? frame-camera : camera? = default-camera position : vec2? = origin width : (and/c finite-real? positive?) = 3 clip : (or/c 'rectangle 'rounded 'rounded-frame) = 'rectangle opacity : opacity? = 1
Supply either positional target or a nonempty #:targets list, not both. A target may be a Visual, top-level symbol, or built-in group/formula visual-path?. If both target arguments are omitted, the view uses every top-level world-space layer in ordinary drawing order. Frame-space overlays, including other views, are deliberately excluded from this all-layer form so an inset cannot recursively render itself. Each explicit target must resolve to world space.
At scene rendering time, selected Visuals are resolved against the same sampled state as ordinary Visuals and painted in order onto the complete inset camera canvas. Their complete world transforms and opacity are therefore synchronized between main view and inset. #:clip chooses a rectangle or rounded rectangle viewport border; 'rounded-frame is a compatibility alias for 'rounded. The viewport itself is an affine/opacity frame-space Visual and can be moved, rotated, scaled, faded, or structurally removed.
Unlike a fixed overlay, the viewport cannot be converted by visual->pict by itself because resolving its live targets needs a sampled scene state. Use complete scene rendering such as scene-state->pict or render-frames! instead.
procedure
(camera-view-visual? value) → boolean?
value : any/c
procedure
(camera-view-visual-target visual)
→ (or/c false/c symbol? visual-path?) visual : camera-view-visual?
procedure
(camera-view-visual-targets visual)
→ (or/c false/c (listof (or/c symbol? visual-path?))) visual : camera-view-visual?
procedure
(camera-view-visual-camera visual) → camera?
visual : camera-view-visual?
procedure
(camera-view-visual-with-camera visual camera) → camera-view-visual? visual : camera-view-visual? camera : camera?
procedure
(camera-view-visual-width visual)
→ (and/c finite-real? positive?) visual : camera-view-visual?
procedure
(camera-view-visual-clip visual) → (or/c 'rectangle 'rounded)
visual : camera-view-visual?
procedure
(camera-view-pan-to view center) → camera-view-pan-to-request?
view : (or/c symbol? camera-view-visual?) center : vec2?
procedure
(camera-view-pan-to-request? value) → boolean?
value : any/c
procedure
(camera-view-pan-by view delta) → camera-view-pan-by-request?
view : (or/c symbol? camera-view-visual?) delta : vec2?
procedure
(camera-view-pan-by-request? value) → boolean?
value : any/c
procedure
(camera-view-zoom-to view world-width)
→ camera-view-zoom-to-request? view : (or/c symbol? camera-view-visual?) world-width : (and/c finite-real? positive?)
procedure
(camera-view-zoom-to-request? value) → boolean?
value : any/c
procedure
(camera-view-zoom-by view factor) → camera-view-zoom-by-request?
view : (or/c symbol? camera-view-visual?) factor : (and/c finite-real? positive?)
procedure
(camera-view-zoom-by-request? value) → boolean?
value : any/c
procedure
(camera-view-follow view target) → camera-view-follow-request?
view : (or/c symbol? camera-view-visual?) target : (or/c visual? symbol? visual-path?)
procedure
(camera-view-follow-request? value) → boolean?
value : any/c
procedure
(camera-view-fit view fit) → camera-view-fit-request?
view : (or/c symbol? camera-view-visual?) fit : camera-fit-request?
procedure
(camera-view-fit-request? value) → boolean?
value : any/c
procedure
(callout content target [ #:camera camera #:at position #:rotation rotation #:scale scale #:opacity opacity #:target-anchor target-anchor #:connector-stroke connector-stroke #:connector-width connector-width]) → callout-visual? content : visual? target : (or/c visual? symbol? visual-path? vec2?) camera : camera? = default-camera position : (or/c vec2? false/c) = #f rotation : finite-real? = 0 scale : scale-factor? = 1 opacity : opacity? = 1
target-anchor :
(or/c 'bottom-left 'bottom 'bottom-right 'left 'center 'right 'top-left 'top 'top-right) = 'center connector-stroke : any/c = "black" connector-width : (and/c finite-real? (>=/c 0)) = 2
Visual-path targets are resolved against each sampled scene state when a complete scene is converted to a Pict. A nested result has every enclosing group/formula transform and opacity composed before its world position is read. This makes the connector follow ordinary movement of a target or its parent without adding observer state to the timeline. SCENE-AW derived targets are resolved from the same sampled scalar state. The resolved Visual must belong to world space. A missing target or a frame-space target raises an exception at scene rendering.
The connector is drawn beneath the annotation from the target’s current world pixel position to the edge of the complete annotation Pict box. For a Visual target, target-anchor selects the target’s live renderer-box center, edge, or corner; it is measured as each scene sample is rendered, so it follows target size changes as well as movement. A literal vec2 target accepts only the default 'center anchor. Its width is a cosmetic pixel width. A false connector-stroke or a zero connector-width suppresses the line. The callout’s outer opacity also applies to the connector.
visual->pict on a callout returns only its local annotation Pict, because resolving a symbolic connector target requires a complete sampled scene state. scene-state->pict and higher-level frame rendering include both the leader and the annotation.
procedure
(callout-visual? value) → boolean?
value : any/c
procedure
(callout-visual-content visual) → visual?
visual : callout-visual?
procedure
(callout-visual-target visual)
→ (or/c symbol? visual-path? vec2?) visual : callout-visual?
procedure
(callout-visual-target-anchor visual) → symbol?
visual : callout-visual?
procedure
(callout-visual-connector-stroke visual) → any/c
visual : callout-visual?
procedure
(callout-visual-connector-width visual)
→ (and/c finite-real? (>=/c 0)) visual : callout-visual?
Renderer-aware relative layout recognizes frame-space coordinates. Layout of a single frame-space Visual uses its captured frame scale, so the result is independent of later world-camera pan or zoom. Pair and list layout operations may combine frame-space Visuals only when they have the same captured frame width. Mixing world and frame coordinate domains in one relative-layout calculation raises an exception. A callout layout box measures its fixed annotation Pict only; the cross-space connector is deliberately not part of frame-space layout.
World-camera operations deliberately exclude frame-space content: camera-fit-visuals rejects it, camera-fit-scene ignores it for implicit all-scene fitting and rejects it when explicitly selected, and camera-follow cannot follow a frame-space target.
17.6 Semantic Colors and Paints
SCENE-AT introduces a small renderer-independent color representation for style interpolation. Existing Visual constructors still accept their historical color strings; semantic RGBA values are needed only when an animation is sampled in its interior or when callers choose to construct one explicitly.
struct
(struct rgba-color (red green blue alpha) #:transparent) red : (and/c finite-real? (>=/c 0) (<=/c 255)) green : (and/c finite-real? (>=/c 0) (<=/c 255)) blue : (and/c finite-real? (>=/c 0) (<=/c 255)) alpha : opacity?
procedure
(rgb-color red green blue) → rgba-color?
red : (and/c finite-real? (>=/c 0) (<=/c 255)) green : (and/c finite-real? (>=/c 0) (<=/c 255)) blue : (and/c finite-real? (>=/c 0) (<=/c 255))
procedure
(color-spec? value) → boolean?
value : any/c
procedure
(color-spec->rgba-color value [who]) → rgba-color?
value : any/c who : symbol? = 'color-spec->rgba-color
procedure
(rgba-color-lerp from to progress) → rgba-color?
from : rgba-color? to : rgba-color? progress : (and/c finite-real? (>=/c 0) (<=/c 1))
17.6.1 Semantic fill paints
SCENE-EC extends a fill from a solid color-spec? to an immutable semantic paint?. These values contain neither a drawing-context brush nor a bitmap. The Pict/racket-draw adapter creates a native vector gradient brush only when it renders a supported Visual, so a paint remains ordinary scene data through sampling, affine transforms, and clipping.
struct
(struct paint-stop (offset color) #:transparent) offset : (and/c finite-real? (>=/c 0) (<=/c 1)) color : color-spec?
struct
(struct linear-gradient-paint (start end stops) #:transparent) start : vec2? end : vec2? stops : (listof paint-stop?)
procedure
(linear-gradient start end stops) → linear-gradient-paint?
start : vec2? end : vec2? stops : (and/c list? pair?)
struct
(struct radial-gradient-paint ( focal-center focal-radius center radius stops) #:transparent) focal-center : vec2? focal-radius : nonnegative-real? center : vec2? radius : nonnegative-real? stops : (listof paint-stop?)
procedure
(radial-gradient center radius stops [ #:focal-center focal-center #:focal-radius focal-radius]) → radial-gradient-paint? center : vec2? radius : nonnegative-real? stops : (and/c list? pair?) focal-center : vec2? = center focal-radius : nonnegative-real? = 0
struct
(struct checker-pattern-paint (first second cell-size) #:transparent) first : color-spec? second : color-spec? cell-size : positive-real?
procedure
(checker-pattern first second [ #:cell-size cell-size]) → checker-pattern-paint? first : color-spec? second : color-spec? cell-size : positive-real? = 1
procedure
(paint-lerp from to progress) → paint?
from : paint? to : paint? progress : (and/c finite-real? (>=/c 0) (<=/c 1))
Paint kinds must agree. In particular, this function does not invent a meaningless halfway value between a solid fill and a gradient; use a deliberate cross-fade of two Visuals for that visual change.
The built-in Pict renderer supports paints on the fills of paths, circles, and rectangles; a structured paint on a circle or rectangle is sent through its vector path equivalent. Strokes remain solid colours. Gradients are native vector brushes. The checker implementation is a deterministic device-aligned stipple and does not yet follow an enclosing affine transform. Formula, SVG, image, and custom renderer paint support remains renderer-specific.