Vixen
dd8b0a81
csharp
public readonly record struct UiTransform

The 2D affine a composited group's rotate and scale place it under.

Read the guide page for this →

Remarks

⚠ This used to be six floats that deliberately could not express a projective map, and the argument for that was sound rather than mistaken. It ran: the third row of a 2D affine is always 0 0 1, storing it costs bytes on a type both executors compare by value, and — the real point — a perspective divide is exactly what the composite quad's linearly interpolated texture coordinates cannot survive, so a type that cannot express one is the type that says so.

⚠ What that argument did not reach is that the element is planar. A plane under a 3D transform and a perspective projects to a plane: the four corners of the border box land at four points with four ws, and that map is exactly a 2D homography. So rotateX and perspective need a 3×3 and not a 4×4, and the third column — M13, M23, M33 — is the whole of the difference. The texture coordinates still cannot survive a linear interpolation, and the answer to that is a w on the vertex rather than a type that refuses the matrix: a rasteriser divides by w because that is what a rasteriser does.

⚠ The affine case is bit-for-bit what it was, and that is a requirement rather than a nicety. Every reference image in Vixen.Graphics.Golden.Tests.UiCompositingTests was rendered through the six-float arithmetic, so a rounding that moved would have to be accepted into every one of them — and "both executors changed together" is precisely what a bug in a shared specification looks like. Then, Apply, Determinant and About are written so that their extra terms are exactly + 0 and × 1 on an affine, which IEEE-754 leaves untouched; Invert cannot be written that way and branches instead, keeping the old expression verbatim. UiTransformProjectiveTests asserts the equality rather than leaving it as reasoning.

⚠ And default is still what it always was, which is why the third column is stored offset. A homography's identity has M33 = 1, so a naively added field would make every zeroed struct divide by zero — turning today's defined-but-wrong collapse-to-the-origin into a NaN that propagates into a vertex buffer. The field behind M33 holds M33 − 1, so a zeroed UiTransform is affine and behaves exactly as it did before this type could be projective.

⚠ Expressed in absolute document space, with the transform origin already folded in. CSS defines rotate about transform-origin, which is the border box's centre by default, so the matrix that reaches here is already T(origin) · R · S · T(-origin). Carrying the origin separately would put the same composition in the geometry builder and in the hit test, which is the two-copies-of-the- arithmetic failure UiDocument.Accumulate's own remark warns about for translate. One matrix, composed once, read by both.

⚠ Identity is the absence, and default is not it. A zeroed struct collapses every point to the origin, so a consumer that read a default field as "no transform" would draw nothing at all. Transform and Transform are therefore nullable, exactly as UiColorMatrix is and for the identical reason.

Fields and properties (13)

  • public float M11

    The x contribution to x.

  • public float M12

    The x contribution to y.

  • public float M21

    The y contribution to x.

  • public float M22

    The y contribution to y.

  • public float Dx

    The x translation, in document pixels.

  • public float Dy

    The y translation, in document pixels.

  • public static UiTransform Identity

    The transform that moves nothing.

  • public float M13

    How much a point's x contributes to its w.

  • public float M23

    And its y.

  • public float M33

    The homogeneous scale at the origin: one for an affine, anything else for a homography.

  • public bool IsAffine

    Whether this is a plain affine, so no w anywhere can be anything but one.

  • public bool IsIdentity

    Whether this is Identity, to the tolerance a pixel can tell.

  • public float Determinant

    This transform's determinant.

Methods (10)

  • public UiTransform(float M11, float M12, float M21, float M22, float Dx, float Dy)

    The 2D affine a composited group's rotate and scale place it under.

  • public static UiTransform Rotation(float degrees, Vector2 about)

    A rotation about a point, in degrees clockwise.

  • public static UiTransform Scale(float x, float y, Vector2 about)

    A scale about a point.

  • public UiTransform About(Vector2 origin)

    This transform performed about rather than about zero.

  • public UiTransform Then(in UiTransform then)

    This transform followed by .

  • public Vector2 Apply(Vector2 point)

    Where a point lands, projected.

  • public Vector3 Project(Vector2 point)

    Where a point lands, before the divide: x, y and w.

  • public UiTransform? Invert()

    The transform that undoes this one, or null where there is none.

  • public Rectangle Bounds(Rectangle rectangle)

    The axis-aligned box that contains once transformed.

  • public bool TryBounds(Rectangle rectangle, out Rectangle bounds)

    The axis-aligned box that contains once transformed, or false where it has none.

Used by (12)

  • DrawCommandVixen.Ui
  • DrawListBuilderVixen.Ui
  • OpeningVixen.Ui
  • TransformReaderVixen.Ui
  • TransformTestsVixen.Ui.Tests
  • UiCompositingTestsVixen.Graphics.Golden.Tests
  • UiDocumentVixen.Ui
  • UiElementVixen.Ui
  • UiGeometryBuilderVixen.Ui
  • UiLayerVixen.Ui
  • UiTransformProjectiveTestsVixen.Ui.Tests
  • UtilityFamilySupportTestsVixen.Editor.Ui.Tests