Core Types
Before this section, complete:
- How Rive Scripts Work — Script structure and lifecycle
- Node Protocol — Factory function and init/draw pattern
Rive Context: The Drawing Pipeline
Rive's drawing system follows a simple pipeline:
- Path = geometry (what shape to draw)
- Paint = styling (how it looks)
- Renderer = draw command (where it goes)
Understanding these core types is essential for procedural graphics in Rive. Create paths and paints in init, then use them in draw.
Path
A Path defines geometry using move, line, and curve commands. Think of it as a pen that you move around to trace shapes.
Path Methods
| Method | Signature | Description |
|---|---|---|
Path.new() | Path.new(): Path | Create an empty path |
path:moveTo(vec) | path:moveTo(vec: Vector) | Move pen to position without drawing |
path:lineTo(vec) | path:lineTo(vec: Vector) | Draw a line from current position to target |
path:close() | path:close() | Connect back to the starting point |
path:reset() | path:reset() | Clear all path data |
Exercise 1: Draw a Line ⭐
Premise
moveTo sets the pen position without drawing, while lineTo draws a line from the current position to the target. Together they form the most basic path operations.
By the end of this exercise, you will be able to Complete the init function to create a diagonal line path from (-120, -40) to (120, 40), and a stroke paint with thickness 4 and a pink color.
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_Exercise1DrawALine
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Draw a simple diagonal line
export type DrawLine = {
path: Path,
paint: Paint,
}
function init(self: DrawLine): boolean
print("Creating line path...")
-- TODO 1: Create a new Path using Path.new()
self.path = nil -- Replace with Path.new()
-- TODO 2: Move to the starting point (-120, -40)
-- Use self.path:moveTo(Vector.xy(x, y))
-- TODO 3: Draw a line to (120, 40)
-- Use self.path:lineTo(Vector.xy(x, y))
-- TODO 4: Create a stroke Paint with:
-- style = "stroke", thickness = 4, color = Color.rgb(255, 80, 120), cap = "round"
self.paint = nil -- Replace with Paint.with({...})
print("Line path created!")
print("ANSWER: line")
return true
end
function draw(self: DrawLine, renderer: Renderer)
renderer:drawPath(self.path, self.paint)
end
return function(): Node<DrawLine>
return {
init = init,
draw = draw,
path = late(),
paint = late(),
}
end
Assignment
Complete these tasks:
- Complete the init function to create a diagonal line path from (-120, -40) to (120, 40), and a stroke paint with thickness 4 and a pink color.
- 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:
moveTosets the starting position without drawinglineTodraws a line from the current position to the target- The path is built in
init, then rendered indraw(called on repaint)
Exercise 2: Filled Rectangle with Outline ⭐
Premise
close() connects the last point back to the starting point, creating a closed shape. You can draw the same path multiple times with different paints—fill first, then stroke on top.
By the end of this exercise, you will be able to Complete the init function to create a closed rectangle path and two paints (fill and stroke). In draw, render both.
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_Exercise2FilledRectangleWithOutline
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Draw a filled rectangle with a stroke outline
export type DrawRect = {
path: Path,
fill: Paint,
stroke: Paint,
}
function init(self: DrawRect): boolean
print("Creating rectangle...")
-- TODO 1: Create a new Path
self.path = nil -- Replace with Path.new()
-- TODO 2: Build a rectangle (-80,-50) to (80,50)
-- moveTo the first corner, lineTo the other 3 corners, then close()
-- TODO 3: Create a fill Paint (style = "fill", color = Color.rgb(80, 170, 255))
self.fill = nil -- Replace with Paint.with({...})
-- TODO 4: Create a stroke Paint (style = "stroke", thickness = 6, color = Color.rgb(20, 60, 120))
self.stroke = nil -- Replace with Paint.with({...})
print("Rectangle with outline created!")
print("ANSWER: rectangle")
return true
end
function draw(self: DrawRect, renderer: Renderer)
-- TODO 5: Draw the path with fill first, then stroke
-- renderer:drawPath(self.path, self.fill)
-- renderer:drawPath(self.path, self.stroke)
end
return function(): Node<DrawRect>
return {
init = init,
draw = draw,
path = late(),
fill = late(),
stroke = late(),
}
end
Assignment
Complete these tasks:
- Complete the init function to create a closed rectangle path and two paints (fill and stroke). In draw, render both.
- 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:
close()connects the last point back to the first- Draw fill first, then stroke on top
- One path can be drawn multiple times with different paints
Paint
Paint defines how a path renders: fill vs stroke, color, thickness, gradients, and more.
Paint Constructors
| Method | Signature | Description |
|---|---|---|
Paint.with(def) | Paint.with(def: PaintDefinition): Paint | Create a paint with initial properties |
paint:copy(overrides) | paint:copy(overrides: PaintDefinition): Paint | Clone a paint with overrides |
Paint Properties
| Property | Values | Description |
|---|---|---|
style | "fill", "stroke" | Fill interior or stroke outline |
color | Color | Solid color |
thickness | number | Stroke width (stroke only) |
cap | "butt", "round", "square" | Line ending style |
join | "miter", "round", "bevel" | Corner style |
gradient | Gradient | Linear or radial gradient |
Exercise 3: Fill vs Stroke ⭐
Premise
Fill and stroke are the two fundamental paint styles. Fill colors the interior, stroke draws the outline. You can combine both on the same path.
By the end of this exercise, you will be able to Create a rectangle path and two paints: a fill paint (orange) and a stroke paint (brown) with round cap and join settings.
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_Exercise3FillVsStroke
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Compare fill and stroke paint styles
export type PaintBasics = {
path: Path,
fill: Paint,
stroke: Paint,
}
function init(self: PaintBasics): boolean
print("Testing fill vs stroke...")
self.path = Path.new()
self.path:moveTo(Vector.xy(-70, -40))
self.path:lineTo(Vector.xy(70, -40))
self.path:lineTo(Vector.xy(70, 40))
self.path:lineTo(Vector.xy(-70, 40))
self.path:close()
-- TODO 1: Create a fill Paint with:
-- style = "fill", color = Color.rgb(255, 160, 80)
self.fill = nil -- Replace with Paint.with({...})
-- TODO 2: Create a stroke Paint with:
-- style = "stroke", thickness = 6, cap = "round", join = "round", color = Color.rgb(70, 40, 20)
self.stroke = nil -- Replace with Paint.with({...})
print("Both styles applied!")
print("ANSWER: styles")
return true
end
function draw(self: PaintBasics, renderer: Renderer)
-- TODO 3: Draw with fill first, then stroke
end
return function(): Node<PaintBasics>
return {
init = init,
draw = draw,
path = late(),
fill = late(),
stroke = late(),
}
end
Assignment
Complete these tasks:
- Create a rectangle path and two paints: a fill paint (orange) and a stroke paint (brown) with round cap and join settings.
- 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
Exercise 4: Gradient Fill ⭐⭐
Premise
Linear gradients interpolate colors between two points. Color stops define which colors appear at which positions along the gradient line.
By the end of this exercise, you will be able to Create a rectangle path and apply a linear gradient fill that transitions from red (left) to blue (right).
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_Exercise4GradientFill
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Apply a linear gradient to a filled rectangle
export type GradientDemo = {
path: Path,
paint: Paint,
}
function init(self: GradientDemo): boolean
print("Creating gradient...")
self.path = Path.new()
self.path:moveTo(Vector.xy(-90, -50))
self.path:lineTo(Vector.xy(90, -50))
self.path:lineTo(Vector.xy(90, 50))
self.path:lineTo(Vector.xy(-90, 50))
self.path:close()
-- TODO 1: Create a linear gradient from left (-90, 0) to right (90, 0)
-- Use Gradient.linear(startVec, endVec, {colorStops})
-- Color stops must be in a table:
-- {
-- { position = 0, color = Color.rgb(255, 80, 80) }, -- red
-- { position = 1, color = Color.rgb(80, 120, 255) } -- blue
-- }
local gradient = nil -- Replace with Gradient.linear(...)
-- TODO 2: Create a fill Paint with the gradient
-- Use Paint.with({ style = "fill", gradient = gradient })
self.paint = nil -- Replace with Paint.with({...})
print("Gradient applied!")
print("ANSWER: gradient")
return true
end
function draw(self: GradientDemo, renderer: Renderer)
renderer:drawPath(self.path, self.paint)
end
return function(): Node<GradientDemo>
return {
init = init,
draw = draw,
path = late(),
paint = late(),
}
end
Assignment
Complete these tasks:
- Create a rectangle path and apply a linear gradient fill that transitions from red (left) to blue (right).
- 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:
Gradient.lineartakes start point, end point, and color stops- Color stops use
position(0-1) andcolor - When using a gradient, the
colorproperty is ignored
Exercise 5: Paint Variations with copy() ⭐⭐
Premise
paint:copy() creates a new paint instance with inherited settings. This is more efficient than creating separate paints with redundant properties.
By the end of this exercise, you will be able to Create a base fill paint, then use copy() to create a highlight variation with a different color. Draw both with a slight offset.
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
Exercise5_Exercise5PaintVariationsWithCopy
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Create paint variations using copy()
export type PaintCopyDemo = {
path: Path,
base: Paint,
highlight: Paint,
}
function init(self: PaintCopyDemo): boolean
print("Creating paint variations...")
self.path = Path.new()
self.path:moveTo(Vector.xy(-60, -60))
self.path:lineTo(Vector.xy(60, -60))
self.path:lineTo(Vector.xy(60, 60))
self.path:lineTo(Vector.xy(-60, 60))
self.path:close()
-- TODO 1: Create a base fill Paint
-- Use Paint.with({ style = "fill", color = Color.rgb(40, 140, 200) })
self.base = nil -- Replace with Paint.with({...})
-- TODO 2: Create a highlight Paint using copy()
-- Use self.base:copy({ color = Color.rgb(120, 220, 255) })
self.highlight = nil -- Replace with self.base:copy({...})
print("Paint copied with variation!")
print("ANSWER: copied")
return true
end
function draw(self: PaintCopyDemo, renderer: Renderer)
-- TODO 3: Draw the base rectangle
renderer:drawPath(self.path, self.base)
-- TODO 4: Draw the highlight with an offset using transforms
renderer:save()
renderer:transform(Mat2D.withTranslation(12, -12))
renderer:drawPath(self.path, self.highlight)
renderer:restore()
end
return function(): Node<PaintCopyDemo>
return {
init = init,
draw = draw,
path = late(),
base = late(),
highlight = late(),
}
end
Assignment
Complete these tasks:
- Create a base fill paint, then use copy() to create a highlight variation with a different color. Draw both with a slight offset.
- 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:
paint:copy({ ... })creates a new paint with overridden properties- Useful for creating variations without duplicating all settings
- This example also demonstrates basic transform usage (covered in Drawing API)
Knowledge Check
Key Takeaway
Path and Paint are the foundation of Rive's drawing system. Create them once in init, then use them efficiently in draw. This separation keeps your code clean and performant.
Next Steps
- Continue to Drawing API
- Need a refresher? Review Quick Reference