Drawing and Styling

A few drawing decisions can give a small interface a recognizable character: a warm surface, a fine outline, a generous corner, a line that guides the eye. Pax's vector elements let you make those decisions in the same template that positions your text and controls.

This chapter builds on Layout and PAXEL. The examples can go inside a component's template, with use pax_kit::*; in its Rust file. Start with ordinary shapes and paint; the later sections introduce custom paths, SVG, and lighting.

Compose a surface

Here is a small surface for the Field notes content from the previous chapter:

<Group x=24px y=24px width=280px height=156px>
    <Text x=24px y=24px width=232px height=36px text="Field notes"
        style={font: "Arial", font_size: 24px, fill: rgb(36, 54, 47)} />
    <Line x=24px y=80px width=232px height=1px
        x1=0px y1=0px x2=100% y2=0px
        stroke={paint: rgb(152, 168, 146), width: 1px} />
    <Ellipse x=24px y=108px width=12px height=12px fill=rgb(67, 112, 86) />
    <Rectangle width=100% height=100% corner_radius=18
        fill=rgb(237, 241, 226)
        stroke={paint: rgb(178, 192, 168), width: 1px} />
</Group>

The rectangle is last because it sits behind the other elements. Keep this foreground-first order when adding a highlight or a background; see Element ordering.

Rectangle fills its bounds; Ellipse fits an oval inside them. Equal width and height give Ellipse a circle. Line connects two local endpoints, while Path describes a longer sequence of segments and curves. They share layout properties with other elements, so percentages, transforms, and reactive bindings apply to drawings too.

A Group organizes these elements but does not paint a background of its own. Likewise, rounding a Rectangle changes that rectangle's geometry; it does not clip its siblings. Use a Frame when content needs to stay inside a rounded boundary.

Color and transparency

Use rgb for an opaque color and rgba when its paint should be translucent. RGB channels and an integer alpha use the range 0–255. Percent channels are also accepted. These two rectangles use equivalent half-opacity paint:

<Rectangle x=24px y=24px width=120px height=72px
    fill=rgba(56, 120, 91, 50%) />
<Rectangle x=160px y=24px width=120px height=72px
    fill=rgb(56, 120, 91) opacity=50% />

For an integer alpha, 255 is fully opaque and 128 is approximately half. Use 50% when you mean an exact half: a decimal 0.5 in rgba is not a normalized alpha value. Element opacity, by contrast, accepts 0.5 or 50%. Paint alpha and element opacity multiply, so applying both halves leaves one-quarter opacity for that paint.

hsl(150deg, 30%, 45%) describes hue, saturation, and lightness; hsla(150deg, 30%, 45%, 50%) adds alpha. Named colors such as WHITE, BLACK, BLUE, and TEAL are convenient palette values. The colored names use Pax's palette—RED, for example, is not a synonym for rgb(255, 0, 0). Use explicit channels when matching a design precisely. TRANSPARENT provides clear paint.

For a vector, fill paints the interior and stroke paints its outline. An omitted fill uses one SLATE layer; an omitted stroke is an empty stack. Set fill=[] when you want only an outline. fill=TRANSPARENT also retains its existing meaning: one transparent fill layer.

Opacity on a container also affects its descendants. The behavior of overlapping children and native elements belongs to Compositing; do not assume a group fades as a single flattened image. Text's style.fill has its own native-rendering limits, covered in Text and fonts.

Paint layers

Paint supplies color: a solid, linear gradient, radial gradient, mesh gradient, or a mixture produced by a transition. A Fill supplies an interior layer; a Stroke supplies an outline layer. Both own paint, material, and opacity. Rectangle, Ellipse, and Path accept ordered lists for fill and stroke. Line and Handwriter accept a stroke list. The property names stay singular.

<Rectangle width=240px height=120px fill=[
    Fill {paint: @gradient {0%: rgba(255, 255, 255, 0), 100%: WHITE}, opacity: 40%},
    TEAL
] stroke=[
    {paint: WHITE, width: 2px},
    {paint: @gradient {0%: CYAN, 100%: FUCHSIA}, width: 10px}
]/>

Index zero is on top within each list, matching element ordering. Every stroke is above every fill. The thin white outline here overlays the wider gradient. fill=RED, fill={paint: RED}, fill=Fill {paint: RED}, and fill=[RED] are equivalent. Mix paints and typed or untyped layer objects in one flat list. Use [] for no layers; nested lists, None, and mixing Fill with Stroke objects are errors. Bindings and settings use the same conversions; see appearance values.

Each layer defaults to SLATE paint, matte material, and opacity 1. Stroke also defaults to width 1px, butt caps, and miter joins. Handwriter's omitted stroke remains one black 3px round stroke; an explicit stroke replaces that default. Nonpositive widths draw nothing; nonfinite widths or opacity draw nothing and emit a development diagnostic. Finite opacity is clamped to [0, 1].

Layer opacity multiplies paint alpha before source-over composition. Element opacity then fades the completed appearance once. Two opaque fills on an element at 50% opacity give 50% alpha; two fills each at 50% layer opacity give 75% alpha. Material is per layer and uses the element's lighting context. Blend modes and interleaving fills with strokes are not exposed yet.

Paint coordinates use the complete local shape after smoothing, before stroke expansion or reveal trimming. Percentage and pixel endpoints include that bounds' origin. Stroke width, caps, joins, and reveal progress do not move the paint. A zero-width or zero-height domain expands that axis symmetrically to 1px solely for sampling; empty geometry still draws nothing. Transforms carry paint and geometry together. This applies to all layers of a Path, including its subpaths, and to Handwriter's complete generated path.

Native Text and controls retain their single-paint interfaces and existing solid-color limitations. They do not acquire appearance stacks or materials. Piet supports the vector layers and linear/radial gradient paints but does not apply lighting materials or alpha masks. WGPU web is the end-to-end validation target.

Migrating existing drawing code

Change stroke={color: RED, width: 2px} to stroke={paint: RED, width: 2px}. Rename the old paint enum's constructors from Fill::... to Paint::.... Move primitive material onto each fill/stroke layer that should use it. In Rust, vector properties now hold Vec<Fill> and Vec<Stroke>; Stroke::default() means a 1px stroke, so use an empty vector for absence. Element opacity now applies once after the layers are assembled. Applications must rebuild rich and baked cartridges with the matching compiler/runtime.

Outlines and corners

Strokes

A stroke combines a paint, pixel width, cap, join, material, and opacity. It is centered on the path, so a 4px outline extends about 2px to either side of its centerline. Leave room for that extension near a clipping edge.

<Line x=24px y=24px width=240px height=40px
    x1=8px y1=20px x2={100% - 8px} y2=20px
    stroke={
        paint: rgb(56, 120, 91)
        width: 8px
        cap: StrokeCap::Round
    }
/>

StrokeCap::Butt ends at the endpoint, Round adds a semicircle, and Square extends a squared end by half the stroke width. Butt is the default. Closed contours have no exposed endpoints, so cap selection is useful for Line and open Path contours.

For joined segments, choose StrokeJoin::Miter, Round, or Bevel. Miter is the default and extends edges toward a point; Round softens the turn; Bevel cuts the corner across. Strokes support the same solid, linear, radial, and interpolated paints as fills. A bare stroke=RED creates a 1px stroke. Use pixel widths such as 2px; percentage stroke widths are not supported by the current vector renderers.

Corner radii

Rectangle accepts one to four corner values. They are unitless numbers representing local pixel radii. A single number rounds every corner:

<Rectangle x=24px y=24px width=240px height=100px
    corner_radius=[24, 8, 24, 8] fill=rgb(220, 232, 211) />

The list expands clockwise from the top-left:

InputTop-leftTop-rightBottom-rightBottom-left
12 or [12]12121212
[12, 6]126126
[12, 6, 3]12636
[12, 6, 3, 1]12631

Use a named object when the corner names make an asymmetric shape clearer: corner_radius={top_left: 12, top_right: 6, bottom_right: 3, bottom_left: 1}. The explicit CornerRadii { ... } form is also available. Size, anchoring, and transforms remain the responsibilities described in Layout.

Gradients

A gradient varies paint across a shape or its outline. @gradient lists the colors at positions along it; the renderer blends between those stops:

<Rectangle x=24px y=24px width=280px height=140px corner_radius=18
    fill=@gradient {
        linear: {
            start: [0%, 0%]
            end: [100%, 100%]
        }
        0%: rgb(237, 241, 226)
        55%: rgb(173, 205, 173)
        100%: rgb(67, 112, 86)
    }
/>

Points are [x, y] pairs in the shape's local coordinate space. Here the gradient runs from the top-left to the bottom-right. Percent coordinates follow the shape's bounds as it resizes. For linear gradients, use at least two stops in ascending order. Percentage stops resolve along the gradient axis; pixel stops use its local logical-pixel length on WGPU and Piet. WGPU uses at most eight stops.

Omitting the linear block gives a left-to-right gradient, from [0%, 0%] to [100%, 0%]:

<Rectangle x=24px y=24px width=280px height=80px
    fill=@gradient {
        0%: rgb(237, 241, 226)
        100%: rgb(67, 112, 86)
    }
/>

Put transparency in each stop's color, such as rgba(255, 255, 255, 0). There is no separate stop-opacity field. A translucent gradient over a surface can supply a highlight while preserving the underlying color.

Fills also participate in automatic settings transitions. Gradient changes crossfade the two sampled paints, even with different stop counts or positions; transparent stops preserve their alpha. Solid colors interpolate their RGBA channels. This paint behavior does not require isolated group compositing or change how neighboring elements blend with each other.

Radial gradients

A radial block grows from a focal point (start) to an outer circle whose center is end. Its numeric radius is in local logical pixels. Equal start and end points produce concentric circles. Points resolve against the actual shape bounds, so percentages remain useful on non-square shapes:

<Rectangle width=280px height=180px
    fill=@gradient {
        radial: {
            start: [55%, 40%]
            end: [50%, 50%]
            radius: 90
        }
        0%: rgba(137, 179, 213, 180)
        60%: rgba(137, 179, 213, 60)
        100%: rgba(137, 179, 213, 0)
    }
/>

The 0% stop is at the focus; the 100% stop lies on the outer circle. Moving the focus creates directional highlights. A focus outside the outer circle creates a cone of paint, with transparent pixels outside that cone. Rotation, reflection, shear, and nonuniform scaling transform the whole gradient with its shape. GPU color fills and painted alpha masks share the same radial sampler; the Piet browser fallback resolves the same local geometry through a fixed radial brush.

Radial stops accept percentages or logical pixels measured against the radius: with radius: 90, a stop at 45px equals 50%. Use ascending stops within 0–100% (or the equivalent pixel range). Empty stops, nonpositive/nonfinite radii, nonfinite points, and singular transforms paint nothing. GPU gradients still use at most eight stops. Radial fills also participate in paint crossfades, including transitions to linear gradients and solids.

Migration: radial color fills previously differed between backends. GPU color fills projected along an axis whose length depended on start/end; Piet multiplied the radius by shape size and resolved points in a square. Painted GPU masks previously treated start as the center and used end only for orientation. Recheck existing radial artwork and masks: use equal points for a centered glow, set end to the intended outer center, and express the radius directly in local logical pixels. The serialized fields and Pax syntax are unchanged in both debug and release cartridges.

Mesh gradients

A mesh places colored anchors in a rectangular set of rows. Each anchor has independent x and y coordinates: rows determine which anchors connect, and do not require a shared y coordinate. Pax joins neighboring anchors with smooth bicubic patches and automatic Bézier controls.

<Rectangle width=100% height=240px
    fill={
        paint: @gradient {
            mesh: {
                rows: [
                    [{position: [0%, 0%], color: CYAN},
                     {position: [100%, 0%], color: FUCHSIA}],
                    [{position: [0%, 100%], color: YELLOW},
                     {position: [100%, 100%], color: CYAN}]
                ]
            }
        }
        material: {Material::unlit()}
    }/>

Use 2–8 rows with the same number of anchors in every row, and 2–8 anchors per row. A mesh has no percentage color-stop list. Positions accept pixels, percentages, or combined units such as {50% + 10px}. Alpha belongs in the anchor color: for example, rgba(255, 0, 255, 35%).

Bind an individual coordinate or color to animate the field: {position: [{self.center_x}, {self.center_y}], color: {self.accent}}. Alternatively, bind the entire grid with mesh: {rows: {self.rows}}, where Rust declares rows: Property<Vec<Vec<MeshPoint>>>. MeshPoint has ordinary position: (Size, Size) and color: Color fields. Rust can also construct Paint::MeshGradient(MeshGradient { rows }) directly. Changing points morphs the field; transitioning between complete Paint values crossfades the two sampled fields, even when their grids have matching dimensions.

Mesh paints work in WGPU fills, layered paints, strokes, and painted alpha masks, with normal clipping, material, and group-opacity behavior. For a Path, percentages resolve against its complete smoothed geometry, including its bounds' origin. Changing draw_start/draw_end or stroke width does not reposition the paint. A zero-width or zero-height paint domain uses one logical pixel on that axis. Direct Handwriter strokes use each generated path's domain; use a single painted rectangle behind a Handwriter alpha mask to keep a continuous field across an entire word.

The field is transparent outside the actual mesh coverage. Moving outer anchors inward can expose gaps; keep them around the area you want to cover, or add a lower fill. Curves can overshoot anchor positions and colors. Color interpolation uses premultiplied encoded-sRGB channels, with alpha clamped to 0–1 and premultiplied color clamped to alpha; it is not linear-light interpolation. At folds, later patches in row order replace earlier ones without accumulating alpha. Explicit Bézier handles and irregular row lengths are not supported yet.

Mesh paint currently requires WGPU. Piet emits a diagnostic and paints no mesh; native text controls do not support mesh text paint. Debug and baked release cartridges accept the same values and syntax. Invalid static grids are rejected during compilation; invalid dynamic grids paint nothing with a deduplicated diagnostic. GPU storage is bounded to 64 MiB per paint page and the device's texture dimensions/layer count. A field or paint mixture that exceeds those limits, or cannot meet the subdivision error budget within 128 steps per patch axis, paints no mesh field and reports the limit rather than silently dropping endpoints or reducing quality.

The runnable examples/fixtures/mesh-gradients study demonstrates nested bindings, whole-row bindings, a continuous Handwriter mask, a revealed stroke, and translucent anchors. Its Pause, Show/Hide anchors, and Narrow controls help inspect the field as it moves and resizes. The stroke extends its outer anchors beyond 0–100% so paint also covers the stroke's outer edges and caps.

Reusable visual settings

Choose a small vocabulary for your interface: perhaps a paper surface, a quiet outline, an accent color, and two corner sizes. Classes can name those roles so that repeated elements stay consistent:

<Rectangle x=24px y=24px width=120px height=80px class="paper" />
<Rectangle x=160px y=24px width=120px height=80px class="paper"
    fill=rgb(219, 233, 212) />

@settings {
    .paper {
        fill: rgb(237, 241, 226)
        stroke: {paint: rgb(178, 192, 168), width: 1px}
        corner_radius: 16
    }
}

Both surfaces share an outline and corner shape; the second supplies its own fill inline. Class names describe a role here, which makes them useful when the palette changes. Keep typography roles in the same visual system, using TextStyle on the Text elements that need them.

When several components need those choices, use a theme component through ImportSettings. Templates owns the provider setup, scope, and override order. A child component imports the settings needed by its own template; placing a theme above it does not automatically style its internals.

The canonical examples/fixtures/runtime-settings-themes example separates color, typography, and corners into providers and switches them with ordinary reactive state. It is a useful next step when one shared class has grown into a theme. Keep event handlers responsible for state changes and let bindings select the visual values, as described in PAXEL.

examples/paxflix shows a shared runtime dark/light choice across nested components. Each visual component imports CinemaTheme is_dark={!light_mode} and uses its semantic classes for text, surfaces, controls, and image fades. The profile menu changes the root's bound mode; the imported provider updates reactively while the catalog's Scrollers remain mounted.

Paths and SVG

Describe a path

Path's elements property holds commands and points. Start with a Point, then add a segment command followed by its endpoint:

<Path x=24px y=24px width=280px height=120px fill=TRANSPARENT
    stroke={paint: rgb(56, 120, 91), width: 4px, cap: StrokeCap::Round}
    elements=[
        PathElement::Point(0%, 75%),
        PathElement::Cubic(25%, 0%, 75%, 100%),
        PathElement::Point(100%, 25%)
    ]
/>

The Cubic supplies two control points, which pull the curve toward them; the following Point supplies its destination. Coordinates belong to the Path's own bounds. Percentages reshape the curve with those bounds; pixels give fixed local distances.

CommandMeaning
Point(x, y) by itselfMove to a point and begin a new contour
Line, then Point(x, y)Draw a straight segment
Quadratic(cx, cy), then Point(x, y)Draw a curve with one control point
Cubic(c1x, c1y, c2x, c2y), then Point(x, y)Draw a curve with two control points
CloseConnect back to the contour's starting point

These entries all use the PathElement:: prefix in a template. Close a contour deliberately when drawing a filled shape:

<Path x=24px y=24px width=160px height=100px
    fill=rgb(220, 232, 211)
    stroke={paint: rgb(56, 120, 91), width: 3px, join: StrokeJoin::Round}
    elements=[
        PathElement::Point(50%, 8%),
        PathElement::Line, PathElement::Point(92%, 92%),
        PathElement::Line, PathElement::Point(8%, 92%),
        PathElement::Close
    ]
/>

Fills use non-zero winding. Overlapping contours that run in the same direction remain filled; an inner contour running in the opposite direction cuts a hole. This also preserves overlaps within imported font outlines. Earlier GPU builds used even-odd winding for ordinary fills, which could cut out overlapping regions. If your drawing relied on that cancellation, reverse the intended hole's contour so it also works with non-zero winding.

For data-driven drawings, keep a Property<Vec<PathElement>> in Rust and bind it to elements. Updating the property changes the drawing through the usual reactive loop. Rust helpers Path::start, Path::line_to, and Path::curve_to can assemble a command list; the last makes a quadratic curve. See Properties for collection updates.

Path geometry may extend outside its layout bounds. Use Frame or Mask when you want clipping, rather than relying on width and height to crop it. smoothing=PathSmoothing::Light or Strong can soften polyline runs; the default None preserves the authored geometry. Smoothing changes the shape, so inspect corners and lettering after enabling it.

Reveal a stroke

draw_start and draw_end select a range along the total path length. This line shows its first half:

<Path x=24px y=24px width=280px height=40px fill=TRANSPARENT
    stroke={paint: rgb(56, 120, 91), width: 6px, cap: StrokeCap::Round}
    draw_start=0% draw_end=50%
    elements=[
        PathElement::Point(0%, 50%),
        PathElement::Line, PathElement::Point(100%, 50%)
    ]
/>

The default range is 0 to 1, showing the full stroke. 0.5 and 50% mean the same progress; values are clamped to this range. An empty or reversed range shows no stroke. The fill remains intact: these properties reveal the outline, so use a transparent fill for a pen-like drawing effect.

On the GPU renderer, the revealed range cuts across the existing stroke; round caps at the original endpoints do not add a rounded tip at that cut. Original endpoint caps appear as a whole when the reveal reaches their path position; they are not progressively swept like a mask. This can be visible when a wide, round-capped stroke reaches draw_end=100%.

Bind the range to application state or animate it with a timeline. The path-drawing example combines authored curves and imported lettering; Animation and Motion owns the timing and playback model. Handwriter is a higher-level component that turns text into paths using bundled stroke fonts, with the same draw-range controls. See its API reference for font and text options.

Bring in an SVG

SVG import converts supported vector artwork into Pax Paths at build time. First validate the source from your project directory:

pax-cli svg-import assets/signature.svg

For a concrete source to try, copy examples/fixtures/path-drawing/assets/svg/signature-strokes.svg to that location. The command reports its viewBox, generated path count, and warnings. Review those warnings and compare the result visually before adopting the artwork.

The importer requires a valid viewBox and supports paths and polygons, line and Bézier commands, transforms, solid fills, and strokes. It is a bounded subset of SVG: convert shapes and text to paths before importing; convert arc commands to curves. Gradient paints are rejected. Filters, masks, and other unsupported elements are not reproduced. Complex SVG styling and compositing need a separate fidelity check.

To keep the SVG as your source, declare a component in Rust. This example can live in src/signature.rs:

#![allow(unused)]
fn main() {
use pax_kit::*;

#[pax]
#[custom(Default)]
#[svg("assets/signature.svg")]
pub struct Signature {
    pub draw_start: Property<UnitValue>,
    pub draw_end: Property<UnitValue>,
}

impl Default for Signature {
    fn default() -> Self {
        Self {
            draw_start: Property::new(UnitValue::Unitless(0.0.into())),
            draw_end: Property::new(UnitValue::Unitless(1.0.into())),
        }
    }
}
}

Declare pub mod signature; and use signature::Signature; in src/lib.rs, then place the component in the template:

<Signature x=24px y=24px width=320px height=80px />

The example preserves the source's 4:1 aspect ratio. Imported coordinates scale into the component's allocated bounds; use a matching ratio when you want the original shape proportions. Emitted stroke widths remain pixel values, so inspect their weight at the size you intend to display. The draw-range properties let the generated strokes participate in the same reveal workflow as a hand-authored Path.

To edit the generated Pax instead, eject a component:

pax-cli svg-import assets/signature.svg --component Signature --out src/signature

This writes src/signature.rs and src/signature.pax; use that pair instead of the #[svg] declaration above. Existing files are protected unless you explicitly pass --force. After ejection, the Pax file is editable source: changes to the original SVG do not automatically update it. To inspect the generated template without writing files, use --stdout.

Lighting and materials

Vector materials can respond to authored lights on the GPU renderer. A LightSource describes illumination; a Material describes how a surface responds. LightFrame keeps a local light from spilling into neighboring component instances:

<LightFrame x=24px y=24px width=280px height=160px>
    <Rectangle width=100% height=100% corner_radius=20
        fill={paint: rgb(67, 112, 86), material: {Material::glossy(0.6)}}  />
    <LightSource x=64px y=40px width=1px height=1px
        z=90px radius=240px intensity=1.5 color=rgb(255, 231, 192) />
</LightFrame>

LightSource is a non-rendering resource; it does not draw a visible lamp. Its point-light position uses its local center plus the authored depth. Use pixel values for radius and z. Depth here affects lighting, not the element order established by the template. Directional lights are also available through shape=LightShape::Directional and a direction vector.

Each fill and stroke layer has its own material; the default is matte. Material::glossy(...) and Material::metallic(...) adjust the response; Material::unlit() preserves the authored paint independently of lights. Material::emissive(...) adds color to its own surface; it does not turn the shape into a light source for its neighbors. The material reference describes explicit response coefficients.

LightFrame containment is one-way: outside lights can enter, while lights inside cannot affect parents or sibling frames. Canvas-layer reachability still applies, and AmbientLight is layer-wide rather than confined by LightFrame. A primitive without eligible direct lights or authored ambient keeps its unlit appearance.

Light contributions stay live as their properties animate or their containers scroll. Offscreen lights can still affect visible surfaces; viewport culling does not disable them. The runtime tracks light contributors and reuses direct light membership until light selection or scene structure changes. Scenes without authored lights do not scan their elements for lighting on redraw.

Unchanged surface-local lighting also reuses its GPU uniform without marking retained drawing dirty.

On the GPU renderer, animating light values repaints only retained surface tiles containing vector draws eligible for those lights. Unlit materials and bitmap images do not make a tile depend on a light. Cached opacity groups and mask-source captures likewise retain their pixels when only unrelated lights change. A tile that does need repainting redraws its composition using retained geometry; moving a light alone does not require tessellating its paths again. This applies on web and Apple GPU targets, in debug and release builds.

Eligibility is conservative: it follows light slots, canvas-layer reachability and LightFrame scope, without testing the light radius against each shape. Authored ambient light can affect all lit materials in its reachable layers. Scene structure, light selection, scrolling, and tile reassignment can require broader updates. LightFrame does not allocate an isolated render surface.

If you maintain a custom Rust lighting primitive, implement InstanceNode::has_scene_lighting() as true alongside resolve_scene_light or resolve_scene_ambient_light. This capability hook must stay true while the primitive is disabled or returns None; it describes its role, not its current contribution. Existing .pax lighting templates need no changes. For value changes, call RuntimeContext::mark_scene_lighting_dirty() to schedule lighting resolution without requesting a full canvas redraw. Structural changes still use the runtime's normal scene invalidation.

This feature is backend-dependent. GPU/WGPU rendering supports the material response, with a current limit of eight active direct lights in each canvas layer's lighting set, shared across its LightFrames. The Piet fallback draws the ordinary fills and strokes without that response. Native Text, native controls, and bitmap Images are not lit vector materials. Texture maps, cast shadows, and a 3D camera are not part of this API. Keep information legible when lighting is absent.

Try Materials in Compositing and Effects to compare surface responses as scrolling moves a light through the scene. The source tabs show the material parameters and scroll-driven light position.

Read more

Continue with Native Controls to add familiar inputs, Animation and Motion to change these values over time, and Compositing to combine clipping, masking, and native content.

The API references cover Rectangle and corners, Ellipse, Line, Path, colors, and light resources.