390. A turned shape is a path, and the frame can compose
Date: 2026-09-18
Status
Accepted. Closes docs/gaps.md G46. Promotes example.motion.Rotated
(ADR-0354)
into the toolkit and adds the composing transform
ADR-0068 left out, without touching
the native boundary.
Context
An application’s landing page turns its tiles as they settle: each starts a few
degrees over and lands flat. It is drawn on a canvas, and inside a canvas a
painter has no way to turn anything.
Frame.transform(a, b, c, d, e, f) replaces the matrix, and the matrix
already holds the translation that put the canvas on screen. A painter cannot
read that translation back — bl_context_get_transform is not on the export
list — so setting a rotation draws the tile at the window’s corner instead of at
its own. ADR-0197 fixed the same bug once already, for the charts, by handing
the ambient matrix down to paintCanvas; an application’s painter is handed no
such thing.
That left rewriting the path’s own coordinates, which is what the showcase does:
example.motion.Rotated, forty lines of switch over every Path.Segment
kind. It is path geometry, it is not the showcase’s to own, and a second copy of
it in every application that animates a canvas is a second toolkit growing
beside this one. It is also wrong in the interesting case. Rotated adds
the turn to an arc’s rotation and leaves the radii and the sweep flag alone,
which is right for a rotation and right for nothing else: a scale stretches the
ellipse, and a mirror makes the arc run the other way round.
Decision
The transform is a path’s, in paint.geom, over the matrix type the toolkit
already has — and Frame gains a composing concat that costs no native
change.
paint.geom.Transformer, besideFlattenerandDasher, maps aPaththrough an affine.Path.transformed(Affine)is the entry point, withrotated(radians, cx, cy),translated(dx, dy)andscaled(sx, sy)for the three cases that would otherwise be a matrix spelled out at every call site. The identity gives back the same path rather than a copy, so a tile at rest costs nothing.css.value.Affineis that matrix, unmoved. It already hasrotate,translate,scale,thenandabout, itsaboutistransform-origin, and its arithmetic is the arithmetic hit testing inverts. A second matrix type inpaint.geomwould be two implementations that must agree exactly, which is the failure its own javadoc was written to prevent. It stays incss.valuebecause moving it would churn the cascade for a package name:Pathalready importscss.Cornersfor the same reason.- The arc is decomposed rather than adjusted. An arc carries the shape of
its ellipse — two radii and a rotation — and the transformed ellipse is the
unit circle under the caller’s linear part times the arc’s own basis.
Recovering radii and an angle from that product is the singular value
decomposition of a 2×2 matrix, which has a closed form: the singular values
are the semi-axes, the left rotation is the angle they sit at, and the right
rotation is discarded because it spins the unit circle onto itself. The
large-arc flag is unchanged. The sweep flag flips when the determinant is
negative, because a mirror reverses the direction the arc is travelled and
the endpoints do not say which side of the chord the ink is on. The rotation
comes back in
[0, π), since an ellipse is unchanged by a half turn and a shape turned a degree at a time for an hour should not accumulate an angle. Frame.concat(a, b, c, d, e, f)multiplies rather than replaces: the caller’s matrix applies first, in the coordinates it draws in, and whatever was in force applies to the result. Same display-scale semantics asFrame.transform— a concatenatedtranslate(10, 0)moves ten logical pixels at any scale — and the same absence of a push, sincesave()andrestore()are already the state stack.- The composition is Java’s, and no native symbol or constant was added.
This is the part G46 asked to be checked.
bl_context_apply_transform_opis exported — it is how the display scale reaches the rasterizer — but the compose operation is an enumerator,BL_TRANSFORM_OP_TRANSFORM, and the enumerators the bindings hard-code are checked against the compiled library byLayoutVerifier. OnlyRESET,ASSIGN,TRANSLATEandSCALEare ingoldberry_shim.c, so naming a fifth means a newGB_CONSTANTrow and a rebuilt native library — a native change for six multiplies, and six multiplies that must agree with what hit testing inverts. SoFramemirrors its own logical matrix in anAffinefield, composes there, and assigns the answer through theASSIGNop that was already bound. The mirror is exact because every change to the matrix goes throughtransform,concatorresetTransform, andsave/restorepush and pop it alongside the rasterizer’s own stack. example.motion.Rotatedis deleted.TileFloorcomposes the turn and the drop into oneAffineand callsPath.transformed. No golden image moved, which is the evidence that the geometry it had was the geometry it kept.
Consequences
- A canvas painter has two ways to turn what it draws, and they are for
different things. A transformed path is a value: it can be measured,
hit-tested and drawn many times in the coordinates it will appear in, and it
needs nothing on the frame to be balanced.
concatis for a painter drawing many shapes under one matrix — text included, which a path transform cannot reach. Transformerkeeps curves as curves and arcs as arcs. Nothing is flattened, so a transformed path costs one pass over the segments and the rasterizer still sees the shape the author wrote.Frameholds state it did not hold before — six doubles and a stack of them. It is the frame’s own matrix, mirrored, and it is what makes a composing transform possible without either a native change or a second answer to “where am I”.- ADR-0068 is unchanged where it matters: the painter still assigns an absolute
matrix per box, hit testing still inverts that same matrix, and a run of
untransformed boxes still costs no native call.
concatis a convenience over the assignment, not a second mechanism. - The arc arithmetic has no other caller today. It is tested on the four cases that break it separately — a turn, a stretch, a mirror and a large arc — and on one that cannot be faked: the transformed arc is flattened a thousand times finer than a frame ever is and asserted to pass through the transformed points of the original.