public readonly record struct UiMaskThe per-pixel coverage a composited group's mask-image multiplies its surface by.
Remarks
⚠ Coverage and not colour, and the difference is not a simplification — it is what mask-image means. CSS masks an element by the alpha of the mask image (mask-mode: alpha, which is what match-source resolves to for every image that is not an SVG <mask>). So the three colours of linear-gradient(to right, black, transparent) never reach this type: only their alphas do, as Alphas.
⚠ Which is also why there is no GradientSpace here, and its absence is load-bearing rather than owed. A background gradient carries the space its stops are interpolated in because Oklab, sRGB and linear RGB disagree about what is halfway between two colours. They do not disagree about what is halfway between two alphas: every branch of the renderer's MixStops lerps the alpha channel plainly, in every space. A mask that stored a space would be storing a field nothing could read differently.
⚠ The box is carried, rather than being taken from Bounds, and a blur is the reason. A group's bounds are its ink, already outset by the blur's kernel radius and already narrowed by the entry clip. CSS resolves mask-image against the element's border box, which moves for neither. Reading the layer's bounds would make blur-sm mask-linear-to-r draw a different ramp from mask-linear-to-r alone — a gradient that slides when you soften it.
⚠ Document pixels, and not the group's own UVs, because both executors have to arrive at one number. Every layer surface is the size of the viewport (see UiLayer), so a composite quad's texture coordinate times the surface size needs no origin subtracted — on the device and in SoftwareUiRasterizer alike. That leaves neither path an origin to subtract and so neither can subtract it differently, which is the same argument the viewport-sized surface was chosen for.
⚠ That product is a target texel and this remark called it a document pixel until #1200. A layer surface is ceil(surface × scale) — see UiRenderer.Compose — so on a 2× display the number the fragment recovers is twice the document pixel, and a mask evaluated at it was drawn at half size in the top-left quadrant of its element. The box stays in document pixels, because UiRenderer.UploadGeometry writes these entries before the scale is known; ui-mask.frag divides its point down instead, from a scale the host pushes beside the list. SoftwareUiRasterizer has no scale at all — it rasterises geometry units straight into the buffer — so the two agree wherever that renderer can be asked, which is a scale of one, and only a device fixture composing at two can see the difference.
⚠ This does not commute with the Gaussian, and that is the one thing about it that is genuinely unlike UiColorMatrix. A colour matrix is the same affine map at every pixel, so it passes through a weighted sum — M(Σ wᵢsᵢ) = Σ wᵢM(sᵢ) — which is what lets the two executors apply it in two different places and still agree. A mask is a scalar that varies with position, so m(p)·Σ wᵢsᵢ ≠ Σ wᵢ·m(pᵢ)·sᵢ whenever m is not constant over the kernel, and it does not commute with the bilinear sampler for the same reason. The consequence is a rule and not a caveat: both executors apply the mask at the composite draw, after the blur and after the matrix, reading the same texture coordinate. Neither may fold it into the surface. See UiRenderer.SubmitDraw and SoftwareUiRasterizer.Frame.Execute, which is why the software renderer grew a mask lookup beside its surface lookup rather than a Masked pass beside its Filtered one.
⚠ Applied to premultiplied colour, which means all four channels and not just the alpha. A layer surface holds premultiplied colour, so scaling coverage by m is (rgb·m, a·m) — the whole vector. Masking a straight-alpha sample would be (rgb, a·m), and reaching for that form here is the premultiply mistake ui-image.frag's varying_shape.x exists to prevent, wearing a new hat.
Fields and properties (12)
public Vector2 CentreThe mask box's centre, in document pixels.
public Vector2 HalfHalf the mask box's size, in document pixels.
public Vector2 AxisThe gradient's direction. Its length is not read — Coverage normalises it — but its angle is, and for Conic it is the from <angle>.
public Vector3 AlphasThe three stops' coverages: X the from, Y the via, Z the to.
public GradientStops StopsWhere those three sit along the gradient line.
public GradientShape ShapeWhich gradient function draws the ramp.
public bool ViaWhether the middle stop is read at all.
public MaskComposite CompositeHow this entry combines with the entries below it in the same list.
public Vector2 AreaCentreWhere the first tile's centre sits, in document pixels. See AreaHalf.
public Vector2 AreaHalfHalf the tile, signed: mask-size, with mask-repeat in the sign.
public static UiMask HiddenA mask that hides everything, which is what an unresolvable mask-image is not.
public bool IsOpaqueWhether this mask leaves every pixel of the box exactly as it found it.
Methods (4)
public UiMask(Vector2 Centre, Vector2 Half, Vector2 Axis, Vector3 Alphas, GradientStops Stops, GradientShape Shape, bool Via)The per-pixel coverage a composited group's mask-image multiplies its surface by.
public float Coverage(Vector2 point)The coverage at a point, in document pixels.
public static float Coverage(ReadOnlySpan<UiMask> masks, Vector2 point)The coverage a whole mask list gives at a point.
public static float Compose(MaskComposite composite, float source, float backdrop)Combines one entry's coverage with the coverage of everything below it.
Used by (13)
- DrawListVixen.Ui
- DrawListBuilderVixen.Ui
- DrawListTestsVixen.Ui.Tests
- EditorShellBudgetTestsVixen.Ui.Controls.Advanced.Tests
- FrameVixen.Ui.Testing
- MaskGradientTestsVixen.Ui.Controls.Tests
- SoftwareUiRasterizerVixen.Ui.Testing
- UiCompositingTestsVixen.Graphics.Golden.Tests
- UiGeometryVixen.Ui
- UiGeometryBuilderVixen.Ui
- UiRendererVixen.Ui.Renderer
- UtilityConsumptionProbeVixen.Ui.Styling.Utilities.Tests
- EditorUiCompositingDeviceTestsVixen.Editor.App.Tests