Skip to main content

Core Types

Prerequisites

Before this section, complete:

Rive Context: The Drawing Pipeline

Rive's drawing system follows a simple pipeline:

  1. Path = geometry (what shape to draw)
  2. Paint = styling (how it looks)
  3. 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

MethodSignatureDescription
Path.new()Path.new(): PathCreate 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_Exercise1DrawALine
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. 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.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is at the top
  • All TODOs are replaced with working code
  • Console output includes the ANSWER: line

Key points:

  • moveTo sets the starting position without drawing
  • lineTo draws a line from the current position to the target
  • The path is built in init, then rendered in draw (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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_Exercise2FilledRectangleWithOutline
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Complete the init function to create a closed rectangle path and two paints (fill and stroke). In draw, render both.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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

MethodSignatureDescription
Paint.with(def)Paint.with(def: PaintDefinition): PaintCreate a paint with initial properties
paint:copy(overrides)paint:copy(overrides: PaintDefinition): PaintClone a paint with overrides

Paint Properties

PropertyValuesDescription
style"fill", "stroke"Fill interior or stroke outline
colorColorSolid color
thicknessnumberStroke width (stroke only)
cap"butt", "round", "square"Line ending style
join"miter", "round", "bevel"Corner style
gradientGradientLinear 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_Exercise3FillVsStroke
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Create a rectangle path and two paints: a fill paint (orange) and a stroke paint (brown) with round cap and join settings.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_Exercise4GradientFill
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Create a rectangle path and apply a linear gradient fill that transitions from red (left) to blue (right).
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is at the top
  • All TODOs are replaced with working code
  • Console output includes the ANSWER: line

Key points:

  • Gradient.linear takes start point, end point, and color stops
  • Color stops use position (0-1) and color
  • When using a gradient, the color property 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_Exercise5PaintVariationsWithCopy
  2. Attach and run:

    • Attach to any shape and press Play
  3. 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:

  1. Create a base fill paint, then use copy() to create a highlight variation with a different color. Draw both with a slight offset.
  2. Run the script and verify the console output
  3. Copy the ANSWER: line into the validator

Expected Output

Console prints the relevant debug lines for this exercise.
ANSWER: <your result>

Verify Your Answer

Verify Your Answer

Checklist

  • --!strict is 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

Q:What is the purpose of path:close()?
Q:What is the difference between 'fill' and 'stroke' paint styles?
Q:When should you call path:reset()?
Q:What happens when you set both 'color' and 'gradient' on a Paint?

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