Skip to main content

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()
Mutation rule

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()
Winding direction matters for fills

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.cppm_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.cpppath->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

AttributeTypeDescription
style"fill" or "stroke"Rendering style
colorColorFill or stroke color
thicknessnumberStroke width (stroke only)
cap"round", "butt", "square"Line endings (stroke only)
join"round", "bevel", "miter"Corner style (stroke only)
blendModeBlendModeCompositing mode
feathernumberFeathering amount
gradientGradient?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.

See Also: Mat2D, Path, Paint

Next Steps