Drawing
APIs for vector paths, styling, and rendering commands.
Path
Vector path for custom geometry.
Constructors
Path.new()
Creates an empty path.
Path.new(): Path
Methods
path:moveTo(point)
Moves the pen to a new position, starting a new subpath.
path:moveTo(point: Vector)
path:lineTo(point)
Draws a straight line from the current position.
path:lineTo(point: Vector)
path:quadTo(control, end)
Draws a quadratic Bezier curve.
path:quadTo(control: Vector, endPoint: Vector)
path:cubicTo(c1, c2, end)
Draws a cubic Bezier curve.
path:cubicTo(controlOut: Vector, controlIn: Vector, endPoint: Vector)
path:close()
Closes the current subpath with a line to the start.
path:close()
path:reset()
Clears all path data. Warning: Don't call while rendering.
path:reset()
An unchanged path can be drawn multiple times per frame. But once a path has been drawn (passed to renderer:drawPath), any mutation — moveTo, lineTo, cubicTo, add, reset, etc. — must wait until the next frame before the path can be drawn again. The reset() warning above is one case of this general rule.
path:add(other, transform?)
Merges another path (or any PathData, such as the input collection a Path Effect receives) into this one.
path:add(other: PathData | Path, transform?: Mat2D)
path:measure()
Returns a PathMeasure for the entire path.
path:measure(): PathMeasure
path:contours()
Returns a ContourMeasure for the first contour.
path:contours(): ContourMeasure?
Operators
Length (#)
Returns the number of path commands.
local commandCount = #path
Example: Drawing a Triangle
local path = Path.new()
path:moveTo(Vector.xy(50, 0))
path:lineTo(Vector.xy(100, 100))
path:lineTo(Vector.xy(0, 100))
path:close()
Script-drawn paths fill clockwise-wound contours (in artboard coordinates, where y points down). A single counter-clockwise contour renders invisible with a fill paint — editor-validated 2026-06-12 with side-by-side CW/CCW triangles. Order your points clockwise, like the example above (top → bottom-right → bottom-left).
Fill rule is a property of authored Fill components, not of script objects: shapes drawn in the editor carry a Fill whose rule is selectable (nonZero / evenOdd / clockWise) in the inspector, but a script-drawn path has no Fill component and always uses the clockwise rule. The scripting API exposes no fill-rule control — Paint.with({ fillRule = ... }) and dynamic writes like paint.fillRule = "evenOdd" are accepted and silently ignored (validated visually with nested-contour rings: no even-odd hole appears).
Runtime-source confirmation: script render paths are created with the fill rule hard-coded to clockwise (src/lua/renderer/lua_path.cpp — m_renderPath->fillRule(FillRule::clockwise)), and the Paint binding has no fillRule key. The one exception is Path Effect scripts: the effect's output path inherits the host Fill component's rule (src/scripted/scripted_path_effect.cpp — path->rewind(source->isLocal(), source->fillRule())), so an authored fill set to evenOdd keeps even-odd behavior through the effect.
See Also: PathMeasure, Renderer.drawPath
PathMeasure
Measures and manipulates paths.
Attributes
measure.length
Total length of the path across all contours. Type: number (read-only)
measure.isClosed
True only if the path has exactly one closed contour. Type: boolean (read-only)
Methods
measure:positionAndTangent(distance)
Returns the position and tangent at a distance along the path. The distance is clamped to [0, measure.length], and the returned tangent is a normalized (unit) vector.
measure:positionAndTangent(distance: number): Vector, Vector
Returns: position, normalized tangent (both Vector)
Example:
local measure = path:measure()
local pos, tan = measure:positionAndTangent(measure.length / 2)
measure:warp(point)
Transforms a point from "path space" coordinates to world coordinates along the path.
measure:warp(point: Vector): Vector
How it works:
- X coordinate = Distance along the path (0 = start,
measure.length= end) - Y coordinate = Perpendicular offset from the path centerline
- Positive Y = Offset to the left of the path direction
- Negative Y = Offset to the right of the path direction
Example: Text Along a Curve
-- Place points along a wavy path
local measure = curvePath:measure()
-- Point at 50% along path, centered on path
local center = measure:warp(Vector.xy(measure.length * 0.5, 0))
-- Point at 50% along path, 10 units to the left
local leftOffset = measure:warp(Vector.xy(measure.length * 0.5, 10))
-- Point at 50% along path, 10 units to the right
local rightOffset = measure:warp(Vector.xy(measure.length * 0.5, -10))
Use Cases:
- Placing text or objects along a curved path
- Creating parallel offset paths
- Animating elements that follow a trajectory
measure:extract(start, end, dest, startWithMove?)
Extracts a segment into a destination path. Distances are clamped to [0, measure.length]. startWithMove defaults to true (the segment begins with a moveTo); pass false to continue from the destination path's previous point — useful when stitching a wrap-around extraction from two calls.
measure:extract(startDist: number, endDist: number, dest: Path, startWithMove?: boolean)
Example: Animated Line Drawing
local displayPath = Path.new()
measure:extract(0, measure.length * progress, displayPath, true)
See Also: ContourMeasure, Path.measure
ContourMeasure
Measurement and manipulation for individual contours within a path. ContourMeasure has all the capabilities of PathMeasure plus the ability to iterate through multi-contour paths via the next property.
Attributes
contour.length
Total length of this contour. Type: number (read-only)
contour.isClosed
True if this contour is closed. Type: boolean (read-only)
contour.next
Next contour, or nil if this is the last. Type: ContourMeasure? (read-only)
Methods
contour:positionAndTangent(distance)
Returns the position and tangent at a distance along this contour.
contour:positionAndTangent(distance: number): Vector, Vector
contour:warp(point)
Transforms a point from "path space" coordinates to world coordinates along this contour. See PathMeasure.warp for details.
contour:warp(point: Vector): Vector
contour:extract(start, end, dest, startWithMove?)
Extracts a segment of this contour into a destination path.
contour:extract(startDist: number, endDist: number, dest: Path, startWithMove?: boolean)
Example: Iterating Contours
local contour = path:contours()
local count = 0
while contour do
print("Contour length:", contour.length)
count += 1
contour = contour.next
end
print("Path has", count, "contours")
Example: Measuring Each Contour
local contour = path:contours()
while contour do
local pos, tan = contour:positionAndTangent(contour.length / 2)
print("Midpoint:", pos.x, pos.y)
contour = contour.next
end
See Also: PathMeasure
Paint
Defines how paths are rendered (fill/stroke, color, etc.).
Constructors
Paint.new()
Creates a paint with default settings.
Paint.new(): Paint
Paint.with(definition)
Creates a paint with multiple properties at once.
Paint.with(definition: PaintDefinition): Paint
PaintDefinition Type:
type PaintDefinition = {
style: ("fill" | "stroke")?, -- Rendering style (default: "fill")
color: Color?, -- Fill or stroke color
thickness: number?, -- Stroke width (stroke only)
cap: ("round" | "butt" | "square")?, -- Line endings (stroke only)
join: ("round" | "bevel" | "miter")?, -- Corner style (stroke only)
blendMode: BlendMode?, -- Compositing mode
feather: number?, -- Feathering amount
gradient: (Gradient | false)?, -- Applied gradient (false removes an existing gradient)
}
All fields are optional. Unspecified fields use Paint defaults.
Setting gradient = false removes an existing gradient — useful with paint:copy() to derive a solid-color paint from a gradient paint. Editor-validated: assigning false to the live paint.gradient property also works and the property reads back as nil afterwards.
Example:
local stroke = Paint.with({
style = "stroke",
thickness = 3,
color = Color.rgb(255, 0, 102),
cap = "round",
join = "round"
})
Attributes
| Attribute | Type | Description |
|---|---|---|
style | "fill" or "stroke" | Rendering style |
color | Color | Fill or stroke color |
thickness | number | Stroke width (stroke only) |
cap | "round", "butt", "square" | Line endings (stroke only) |
join | "round", "bevel", "miter" | Corner style (stroke only) |
blendMode | BlendMode | Compositing mode |
feather | number | Feathering amount |
gradient | Gradient? | Applied gradient (assign false to remove; reads back as nil) |
Methods
paint:copy(overrides?)
Creates a copy with optional property overrides.
paint:copy(overrides?: PaintDefinition): Paint
Example:
local highlight = basePaint:copy({ color = Color.rgb(255, 255, 0) })
See Also: Gradient, Renderer.drawPath
Gradient
Color gradients for fills.
Constructors
Gradient.linear(from, to, stops)
Creates a linear gradient.
Gradient.linear(from: Vector, to: Vector, stops: {GradientStop}): Gradient
Example:
local gradient = Gradient.linear(
Vector.xy(0, 0),
Vector.xy(100, 0),
{
{ position = 0, color = Color.rgb(255, 0, 0) },
{ position = 1, color = Color.rgb(0, 0, 255) }
}
)
Gradient.radial(center, radius, stops)
Creates a radial gradient.
Gradient.radial(center: Vector, radius: number, stops: {GradientStop}): Gradient
GradientStop Type
type GradientStop = {
position: number, -- 0.0 to 1.0
color: Color
}
See Also: GradientStop
Renderer
Drawing commands and state management. Passed to draw() functions.
Methods
renderer:drawPath(path, paint)
Draws a path with the specified paint.
renderer:drawPath(path: Path, paint: Paint)
renderer:drawImage(image, sampler, blendMode, opacity)
Draws an image.
renderer:drawImage(image: Image, sampler: ImageSampler, blend: BlendMode, opacity: number)
renderer:drawImageMesh(image, sampler, vertices, uvs, triangles, blendMode, opacity)
Draws an image mapped onto a triangle mesh.
renderer:drawImageMesh(image: Image, sampler: ImageSampler,
vertices: VertexBuffer, uvs: VertexBuffer, triangles: TriangleBuffer,
blendMode: BlendMode, opacity: number)
When to use: Advanced image warping onto custom geometry, cloth simulation, terrain rendering.
renderer:save()
Pushes the current state (transform, clip) onto the stack.
renderer:save()
renderer:restore()
Pops the last saved state. Always pair with save()!
renderer:restore()
renderer:transform(mat)
Applies a transformation to subsequent drawing.
renderer:transform(mat: Mat2D)
renderer:clipPath(path)
Restricts drawing to the path region until restore().
renderer:clipPath(path: Path)
Example: Drawing a Rotated Object
function draw(self, renderer)
renderer:save()
renderer:transform(Mat2D.withTranslation(self.x, self.y))
renderer:transform(Mat2D.withRotation(self.angle))
renderer:drawPath(self.path, self.paint)
renderer:restore()
end
Warning: Unbalanced save()/restore() calls cause rendering bugs in later frames.
Next Steps
- Continue to Scene
- Need a refresher? Review Quick Reference