Drawing API
Before this section, complete:
- Core Types — Path, Paint fundamentals
- Node Protocol — Factory function and lifecycle
Rive Context: The Renderer Is a State Machine
The Renderer maintains a transform stack and clip state. Think of it as a canvas with layers of transformations. Always pair save() with restore() so transforms and clips don't leak into later draws.
The golden rule: Every save() needs a matching restore().
Renderer Methods
| Method | Description |
|---|---|
renderer:save() | Push current state onto the stack |
renderer:restore() | Pop state from the stack |
renderer:transform(mat) | Apply a transformation matrix |
renderer:clipPath(path) | Restrict drawing to inside the path |
renderer:drawPath(path, paint) | Draw a path with the given paint |
Mat2D Transforms
Mat2D provides 2D transformation matrices for translation, rotation, and scale.
| Method | Description |
|---|---|
Mat2D.withTranslation(x, y) | Move position |
Mat2D.withRotation(radians) | Rotate around origin |
Mat2D.withScale(sx, sy) | Scale horizontally and vertically |
Transforms are applied in order and accumulate. The order matters: translate-then-rotate produces different results than rotate-then-translate.
Exercise 1: Translate and Rotate a Shape ⭐⭐
Premise
The renderer maintains a transform stack. save() pushes state, transform() applies matrices, restore() pops state. Always pair save/restore.
By the end of this exercise, you will be able to Complete the advance and draw functions to create a rotating rectangle. Increment angle each frame and apply rotation in draw.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_Exercise1TranslateAndRotateAShape
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Apply translation and rotation transforms
export type TransformDemo = {
path: Path,
paint: Paint,
angle: number,
}
function init(self: TransformDemo): boolean
self.angle = 0
self.path = Path.new()
self.path:moveTo(Vector.xy(-40, -20))
self.path:lineTo(Vector.xy(40, -20))
self.path:lineTo(Vector.xy(40, 20))
self.path:lineTo(Vector.xy(-40, 20))
self.path:close()
self.paint = Paint.with({ style = "fill", color = Color.rgb(255, 140, 80) })
print("Transform demo initialized")
return true
end
function advance(self: TransformDemo, seconds: number): boolean
-- TODO 1: Add seconds to self.angle
-- TODO 2: Every second (when floor changes), print the angle
-- Use: print(`Rotating: angle = {self.angle:.2f}`)
-- TODO 3: After 2 seconds, print the answer
return true
end
function draw(self: TransformDemo, renderer: Renderer)
-- TODO 4: Save renderer state
-- TODO 5: Apply rotation using Mat2D.withRotation(self.angle)
-- renderer:transform(Mat2D.withRotation(self.angle))
-- TODO 6: Draw the path
-- TODO 7: Restore renderer state
end
return function(): Node<TransformDemo>
return {
init = init,
advance = advance,
draw = draw,
path = late(),
paint = late(),
angle = 0,
}
end
Assignment
Complete these tasks:
- Complete the advance and draw functions to create a rotating rectangle. Increment angle each frame and apply rotation in draw.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Key points:
save()preserves the current renderer state- Multiple
transform()calls accumulate restore()returns to the saved state- Animate values in
advance, apply them indraw
Exercise 2: Multiple Instances with save/restore ⭐⭐
Premise
save/restore in a loop lets you draw the same path multiple times with different transforms. Each iteration is isolated.
By the end of this exercise, you will be able to Complete the draw function to render 5 copies of the square at different positions and rotations.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_Exercise2MultipleInstancesWithSaveRestore
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Draw the same path multiple times with different transforms
export type CloneDemo = {
path: Path,
paint: Paint,
}
function init(self: CloneDemo): boolean
self.path = Path.new()
self.path:moveTo(Vector.xy(-20, -20))
self.path:lineTo(Vector.xy(20, -20))
self.path:lineTo(Vector.xy(20, 20))
self.path:lineTo(Vector.xy(-20, 20))
self.path:close()
self.paint = Paint.with({ style = "fill", color = Color.rgb(90, 200, 255) })
print("Clones initialized")
print("Drawing 5 clones...")
print("ANSWER: 5")
return true
end
function draw(self: CloneDemo, renderer: Renderer)
-- TODO 1: Create a for loop from i = 1 to 5
-- TODO 2: Inside the loop:
-- a) Call renderer:save()
-- b) Apply translation: Mat2D.withTranslation(-120 + i * 60, 0)
-- c) Apply rotation: Mat2D.withRotation(i * 0.4)
-- d) Draw the path
-- e) Call renderer:restore()
end
return function(): Node<CloneDemo>
return {
init = init,
draw = draw,
path = late(),
paint = late(),
}
end
Assignment
Complete these tasks:
- Complete the draw function to render 5 copies of the square at different positions and rotations.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Key points:
- Each loop iteration gets its own save/restore pair
- Without restore, transforms would accumulate incorrectly
- One path object can render multiple times with different transforms
Exercise 3: Animated Polyline ⭐⭐
Premise
Dynamic paths are rebuilt in advance() using reset(). Never call reset() in draw()—it can cause rendering artifacts.
By the end of this exercise, you will be able to Complete the advance function to rebuild the path as an animated wave each frame.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_Exercise3AnimatedPolyline
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Update path data dynamically in advance
export type WavyLine = {
path: Path,
paint: Paint,
time: number,
}
function init(self: WavyLine): boolean
self.time = 0
self.path = Path.new()
self.paint = Paint.with({ style = "stroke", thickness = 3, color = Color.rgb(255, 220, 80) })
print("Wavy line initialized")
print("Wave animating...")
print("ANSWER: wavy")
return true
end
function advance(self: WavyLine, seconds: number): boolean
-- TODO 1: Add seconds to self.time
self.time += seconds
-- TODO 2: Reset the path to clear previous frame's geometry
-- self.path:reset()
-- TODO 3: Loop from i = 0 to 12 to create 13 points
-- For each point:
-- Calculate x = -150 + i * 25
-- Calculate y = math.sin(self.time * 2 + i) * 20
-- If i == 0: use moveTo, otherwise use lineTo
return true
end
function draw(self: WavyLine, renderer: Renderer)
renderer:drawPath(self.path, self.paint)
end
return function(): Node<WavyLine>
return {
init = init,
advance = advance,
draw = draw,
time = 0,
path = late(),
paint = late(),
}
end
Assignment
Complete these tasks:
- Complete the advance function to rebuild the path as an animated wave each frame.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
path:reset() should only be called in advance, never in draw. Calling reset in draw can cause rendering artifacts because the path data changes during the render pass.
Key points:
- Rebuild path geometry in
advance, not indraw path:reset()clears all existing path commands- Use conditionals to handle the first point (moveTo) vs subsequent points (lineTo)
Exercise 4: Clipping a Region ⭐⭐
Premise
clipPath() restricts all subsequent drawing to inside the clip region. The clip is part of renderer state and cleared by restore().
By the end of this exercise, you will be able to Complete the draw function to clip content to the square region, then draw a stroke around the clip boundary.
Use Case
This pattern shows up whenever you build behavior in Rive scripts.
Example scenarios:
- Debugging script behavior
- Driving animation logic
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_Exercise4ClippingARegion
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Use clipPath to restrict drawing to a specific area
export type ClipDemo = {
clipPath: Path,
contentPath: Path,
fill: Paint,
stroke: Paint,
}
function init(self: ClipDemo): boolean
-- Clip region: 160x160 square
self.clipPath = Path.new()
self.clipPath:moveTo(Vector.xy(-80, -80))
self.clipPath:lineTo(Vector.xy(80, -80))
self.clipPath:lineTo(Vector.xy(80, 80))
self.clipPath:lineTo(Vector.xy(-80, 80))
self.clipPath:close()
-- Content: wider rectangle that extends beyond clip
self.contentPath = Path.new()
self.contentPath:moveTo(Vector.xy(-140, -20))
self.contentPath:lineTo(Vector.xy(140, -20))
self.contentPath:lineTo(Vector.xy(140, 20))
self.contentPath:lineTo(Vector.xy(-140, 20))
self.contentPath:close()
self.fill = Paint.with({ style = "fill", color = Color.rgb(255, 200, 60) })
self.stroke = Paint.with({ style = "stroke", thickness = 4, color = Color.rgb(60, 60, 60) })
print("Clip demo initialized")
print("Content clipped to region!")
print("ANSWER: clipped")
return true
end
function draw(self: ClipDemo, renderer: Renderer)
-- TODO 1: Save renderer state
-- TODO 2: Apply clip using renderer:clipPath(self.clipPath)
-- TODO 3: Draw the content path (it will be clipped)
-- TODO 4: Restore renderer state (clears the clip)
-- TODO 5: Draw the clip boundary stroke (outside save/restore)
end
return function(): Node<ClipDemo>
return {
init = init,
draw = draw,
clipPath = late(),
contentPath = late(),
fill = late(),
stroke = late(),
}
end
Assignment
Complete these tasks:
- Complete the draw function to clip content to the square region, then draw a stroke around the clip boundary.
- Run the script and verify the console output
- Copy the
ANSWER:line into the validator
Expected Output
Console prints the relevant debug lines for this exercise.
ANSWER: <your result>
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Key points:
clipPathrestricts all subsequent drawing to the clipped area- The clip is part of the renderer state and is cleared by
restore() - Draw the clip boundary stroke outside the save/restore to show the frame
Transform Order Matters
Transforms apply in the order you call them. Consider these two approaches:
-- Approach 1: Translate, then rotate
renderer:transform(Mat2D.withTranslation(100, 0))
renderer:transform(Mat2D.withRotation(math.pi / 4))
-- Shape moves right 100px, THEN rotates around its new position
-- Approach 2: Rotate, then translate
renderer:transform(Mat2D.withRotation(math.pi / 4))
renderer:transform(Mat2D.withTranslation(100, 0))
-- Shape rotates first, THEN moves in the rotated direction
These produce very different results. When in doubt, use save/restore to isolate transform groups.
Common Patterns
Center-Rotate Pattern
To rotate around a shape's center:
renderer:save()
renderer:transform(Mat2D.withTranslation(centerX, centerY))
renderer:transform(Mat2D.withRotation(angle))
renderer:transform(Mat2D.withTranslation(-centerX, -centerY))
renderer:drawPath(self.path, self.paint)
renderer:restore()
Scale from Center Pattern
To scale from a shape's center:
renderer:save()
renderer:transform(Mat2D.withTranslation(centerX, centerY))
renderer:transform(Mat2D.withScale(scaleX, scaleY))
renderer:transform(Mat2D.withTranslation(-centerX, -centerY))
renderer:drawPath(self.path, self.paint)
renderer:restore()
Knowledge Check
Key Takeaway
Use save() and restore() for every isolated transform or clip. It keeps your rendering predictable and clean. Build paths in init or advance, and keep draw focused solely on issuing renderer commands.
Next Steps
- Continue to ViewModels
- Need a refresher? Review Quick Reference