shapes¶
goo ships three shape primitives - Sector, Arc, Polygon - and a small set of compositor helpers under Goo.Shapes. all three primitives render as a panel whose background image is a baked alpha mask of the shape's silhouette.
the color quirk¶
the engine renders panel backgrounds in this order: BackgroundColor is a solid rectangle fill behind the panel, BackgroundImage composites on top with its per-pixel alpha, and BackgroundTint multiplies into the image's RGB channels.
shape primitives use BackgroundImage for the alpha mask. if BackgroundColor were left alone, every shape would render as a solid rectangle in that color with the shape composited inside - not the silhouette you wanted.
goo redirects BackgroundColor on a shape blob through to the engine's BackgroundTint so the user-facing property always means "the color of the shape." the redirect lives in Applier.cs and only fires for Sector, Arc, and Polygon.
new Sector
{
StartAngle = 0f, EndAngle = 60f,
InnerRadius = 0.4f, OuterRadius = 1f,
BackgroundColor = Color.Red, // paints the wedge red, not the surrounding rect
};
explicit BackgroundTint wins¶
if both BackgroundColor and BackgroundTint are declared on a shape blob, the explicit BackgroundTint takes precedence. the applier reads BackgroundTint ?? BackgroundColor, so your intent - "i'm tinting the mask" - beats the shorthand fall-through.
for author code that doesn't mix the two, treat the names as interchangeable on shapes. the doc convention going forward is: prefer BackgroundColor on shape blobs (matches Container; reads as "this shape is red") and reserve BackgroundTint for the rare case where you want to be explicit about multiplying into a pre-tinted mask.
composites¶
Goo.Shapes factories return composite subtrees. every helper that internally applies a rotation defaults PointerEvents = PointerEvents.None on every node it produces. mouse events bubble past the rotated descendants and reach the parent in its unrotated frame.
| helper | what it builds |
|---|---|
Shapes.Ring(segments, innerRadius, colors, outerRadius = 1.0f, key = null) |
N rotation-wrapped Sector wedges arranged clockwise from up (12 o'clock). |
Shapes.Disc(key?) |
circular Container using the BorderRadius shorthand at 50%. width/height = 100% by default. |
the Ring overload that takes a ReadOnlySpan<Color> is preferred from animated Build() bodies - stackalloc Color[SlotCount] avoids the per-frame heap allocation that the Color[] overload incurs.
Shapes.Ring is designed to pair with a hit-shape resolver for radial slot dispatch. the SlotDispatcher and HitShape.Radial types in the HitShapes project connect the ring to per-slot hover and click events.
// stackalloc span avoids a heap allocation on each Build() call
Span<Color> colors = stackalloc Color[SlotCount];
for (int i = 0; i < SlotCount; i++)
colors[i] = i == _hovered ? HoverColor : BaseColor;
var ring = Shapes.Ring(SlotCount, innerRadius: 0.4f, colors);
stacking shapes must be absolute¶
sibling shapes default to flex flow, not overlap. to stack two or more shapes concentrically - a gauge track ring under a fill arc, layered Sector wedges - each sibling must set Position = PositionMode.Absolute, Top = 0, Left = 0. without it the engine lays the siblings out side by side and shrinks each to its share of the row, so two full-size shapes render as squashed half-width ovals that read as a single ring "tilted in 3D."
it is not 3D. a shape is a flat baked alpha mask with no transform in its render path, so a squashed look is always a layout problem, never an orientation one - do not chase PanelTransform.Rotate or camera framing.
// a gauge: track ring under a fill arc. both children MUST be absolute or they lay out side by side.
new Container
{
Position = PositionMode.Relative, // positioned ancestor for the absolute children (primer rule 13)
Children =
{
new Sector { Position = PositionMode.Absolute, Top = 0, Left = 0, /* track */ },
new Sector { Position = PositionMode.Absolute, Top = 0, Left = 0, /* fill */ },
},
};
this is the inverse of primer rule 13: rule 13 says absolute children need a positioned ancestor; this rule says shapes you intend to overlap have to be made absolute in the first place. Shapes.Ring already wraps each wedge in an absolutely-positioned full-size container, so reach for the helper before hand-stacking.
animate shapes with transforms and tints, never geometry¶
the baked mask is keyed by the shape's geometry: StartAngle, EndAngle, InnerRadius, OuterRadius, CornerRadius, and polygon points. change any of them and the mask re-bakes. a one-off change (a gauge fill updating on damage) is fine; driving a geometry field from a per-frame clock re-bakes a texture every frame and will visibly drop the frame rate.
for continuous motion, keep the geometry fixed and move the panel instead:
// a radar sweep: fixed 40-degree wedge, rotated by a transform. rotation is GPU-side and free;
// animating StartAngle/EndAngle instead re-bakes the mask texture every frame.
new Container
{
Position = PositionMode.Absolute, Top = 0, Left = 0,
Width = Length.Percent(100), Height = Length.Percent(100),
Transform = PanelTransform.Rotate(_t * 90f % 360f),
Children = { new Sector { StartAngle = 0, EndAngle = 40, InnerRadius = 0.1f, OuterRadius = 0.68f } },
};
color is also cheap to animate: BackgroundColor on a shape is applied as a tint over the mask, a plain style write with no re-bake, so hue-shifting a ring's segments per frame costs nothing extra.
see also¶
- container - the full property surface that shape facades draw a subset from.
- panel transforms - rotate, scale, and skew a shape after you build it.