Rive Environment
Learning Objectives
- Understand how Rive scripts run inside the editor
- Use the Console panel for debugging
- Know the difference between Node and Util scripts
- Work within sandbox limitations
AE/JS Syntax Comparison
| Concept | After Effects / JavaScript | Luau (Rive Scripts) |
|---|---|---|
| Debug output | console.log() or alert() | print() to Console panel |
| Script execution | Runs in browser JS engine | Runs in sandboxed Luau VM |
| File access | File.read() (ExtendScript) | Not available (sandboxed) |
| Network requests | fetch() or XMLHttpRequest | Not available (sandboxed) |
| Module import | import or require() (Node.js) | require("ScriptName") |
| Type checking | TypeScript (optional) | --!strict (recommended) |
| Runtime | JavaScript (dynamic) | Luau (statically typed variant of Lua) |
After Effects: Expressions run in the AE JavaScript engine. ExtendScript runs as a separate process that can access files, network, etc.
JavaScript (Web): Code runs in the browser's V8/SpiderMonkey engine with full DOM access.
Rive Scripts: Code runs in a sandboxed Luau virtual machine inside the Rive editor. There is:
Rive Context: Scripts Run Inside the Editor
Rive scripts execute in a sandboxed Luau environment inside the Rive editor. This is not a standard Lua runtime - it has specific APIs, restrictions, and behaviors designed for animation development.
Key points to understand:
- Scripts run in the Rive editor, not in your browser's JavaScript environment
- Output goes to Rive's Console panel, not the browser developer console
- The runtime provides specific APIs (Path, Paint, Vector, etc.)
- Certain Lua capabilities are restricted for security and performance
The Console Panel
The Console panel is your primary debugging tool in Rive. All print() statements from your scripts appear here.
Opening the Console:
- View menu > Console (or use the keyboard shortcut)
- The panel appears at the bottom of the editor
Console features:
- Shows
print()output from all scripts - Displays error messages with line numbers
- Clears when you restart the animation
Use print() liberally during development. Unlike browser console logs, these don't affect your exported animation - they only appear in the editor.
-- Rive debugging
print("Debug message")
print(`Value is {myValue}`) -- String interpolation
// JavaScript equivalent:
console.log("Debug message");
console.log(`Value is ${myValue}`);
Script Types
Rive has two types of scripts with different purposes:
Node Scripts
Node scripts attach to Artboard nodes and have a lifecycle:
--!strict
export type MyNode = {}
function init(self: MyNode): boolean
print("I am a Node script")
return true
end
function draw(self: MyNode, renderer: Renderer)
end
return function(): Node<MyNode>
return {
init = init,
draw = draw,
}
end
// JavaScript/TypeScript mental model:
// A Node script is like a class with lifecycle methods
class MyNode {
constructor() {
console.log("I am a Node script"); // Like init()
}
draw(renderer: Renderer) {
// Called every frame
}
}
Characteristics:
- Must have
initanddrawfunctions - Can have
update,advance, and pointer handlers - Attach to nodes in the artboard hierarchy
- Have access to a
Rendererfor drawing
Util Scripts
Util scripts are reusable modules with no lifecycle. To create one, select Blank Script in the editor dropdown—there is no dedicated "Util Script" option. See the Rive docs for the full pattern.
--!strict
local MyUtil = {}
function MyUtil.helper(x: number): number
return x * 2
end
return MyUtil
// JavaScript/TypeScript equivalent:
// A Util script is like an ES6 module
export function helper(x: number): number {
return x * 2;
}
// Or as a module object:
const MyUtil = {
helper: (x: number) => x * 2
};
export default MyUtil;
Characteristics:
- No lifecycle functions
- Return a table of functions and types
- Imported with
require("ScriptName") - Shared across all scripts that import them
Artboard References (self.artboard)
In Rive scripts, self only contains fields you explicitly define in your exported type. There is no built-in self.artboard unless you add it.
If you see self.artboard in examples, it means the script defines an artboard Input:
--!strict
export type Spawner = {
artboard: Input<Artboard<Data.ComponentVM>>,
}
function init(self: Spawner): boolean
local instance = self.artboard:instance()
return true
end
Key takeaway: To access an artboard, declare it as an Input<Artboard<...>> (or late() if assigned in the editor) and then use self.artboard in your logic.
Practice Exercises
Exercise 1: Console Exploration ⭐
Premise
Understanding the lifecycle order (init → advance → draw) is fundamental to Rive scripting. The milestone detection shows how to track state across frames.
By the end of this exercise, you will be able to Complete the advance function to track frames and detect when frameCount reaches exactly 60. Print 'ANSWER: milestone' when you hit that 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
Exercise1_Exercise1ConsoleExploration
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- This script explores the Console panel and lifecycle callbacks
export type ConsoleExplorer = {
frameCount: number,
}
function init(self: ConsoleExplorer): boolean
print("init() called")
self.frameCount = 0
return true
end
function advance(self: ConsoleExplorer, seconds: number): boolean
-- TODO 1: Increment frameCount by 1
-- TODO 2: If this is one of the first 3 frames, print "Frame {frameCount}"
-- TODO 3: When frameCount reaches exactly 60, print "ANSWER: milestone"
return true
end
function draw(self: ConsoleExplorer, renderer: Renderer)
end
return function(): Node<ConsoleExplorer>
return {
init = init,
advance = advance,
draw = draw,
frameCount = 0,
}
end
Assignment
Complete these tasks:
- Complete the advance function to track frames and detect when frameCount reaches exactly 60. Print 'ANSWER: milestone' when you hit that 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
Exercise 2: Using Vector Constructors ⭐
Premise
Rive provides Vector as a core type with multiple constructors. Understanding these constructors is essential for positioning, scaling, and movement calculations in your animations.
By the end of this exercise, you will create vectors using different constructors and calculate a distance.
Use Case
Vectors are fundamental to all positioning and movement in Rive scripts.
Example scenarios:
- Positioning elements on the artboard
- Calculating distances between points
- Defining directions for movement
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_VectorConstructors
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Practice using Vector constructors
export type VectorPractice = {}
function init(self: VectorPractice): boolean
print("=== Vector Constructors ===")
-- TODO 1: Create a vector at position (100, 50) using Vector.xy()
local start = Vector.xy(0, 0) -- Replace with correct values
-- TODO 2: Create another vector at position (100, 150) using Vector.xy()
local finish = Vector.xy(0, 0) -- Replace with correct values
-- TODO 3: Calculate the difference (finish - start)
local diff = finish - start
-- TODO 4: Calculate the distance using diff:length()
local distance = diff:length()
print(`Start: ({start.x}, {start.y})`)
print(`Finish: ({finish.x}, {finish.y})`)
print(`Distance: {distance}`)
print(`ANSWER: {distance}`)
return true
end
function draw(self: VectorPractice, renderer: Renderer)
end
return function(): Node<VectorPractice>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Create
startat position (100, 50) - Create
finishat position (100, 150) - Calculate the distance between them
- Copy the
ANSWER:line into the validator
Expected Output
=== Vector Constructors ===
Start: (100, 50)
Finish: (100, 150)
Distance: 100
ANSWER: 100
Verify Your Answer
Checklist
-
--!strictis at the top - All TODOs are replaced with working code
- Console output includes the
ANSWER:line
Exercise 3: Type Safety with --!strict ⭐
Premise
Strict mode catches type mismatches at edit time rather than runtime. This is similar to TypeScript vs JavaScript—you get errors in the editor before running your code. Without --!strict, Node scripts won't be recognized by Rive.
By the end of this exercise, you will be able to Complete the type annotations for the Player type. Each field needs the correct type: number, string, or Vector.
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_Exercise3TypeSafetyWithStrict
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Type annotations help catch errors at edit time
export type Player = {
-- TODO 1: Add type annotation for name (should be a string)
name: TODO,
-- TODO 2: Add type annotation for health (should be a number)
health: TODO,
-- TODO 3: Add type annotation for position (should be a Vector)
position: TODO,
}
function init(self: Player): boolean
-- Initialize with specific values
self.name = "Hero"
self.health = 100
self.position = Vector.xy(50, 75)
-- Print the typed values
print("Player created:")
print(` name = {self.name}`)
print(` health = {self.health}`)
print(` position = ({self.position.x}, {self.position.y})`)
print("ANSWER: typed")
return true
end
function draw(self: Player, renderer: Renderer)
end
return function(): Node<Player>
return {
init = init,
draw = draw,
name = "",
health = 0,
position = Vector.xy(0, 0),
}
end
Assignment
Complete these tasks:
- Complete the type annotations for the Player type. Each field needs the correct type: number, string, or Vector.
- 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: Working Within the Sandbox ⭐⭐
Premise
The sandbox provides standard Luau math functions (sin, cos, rad) plus Rive graphics APIs. You can perform complex calculations and create procedural graphics—just no file I/O or network access.
By the end of this exercise, you will be able to Complete the trigonometry calculations: convert 45 degrees to radians, then calculate sin and cos. Store the cos value (rounded to 3 decimals) as the answer.
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_Exercise4WorkingWithinTheSandbox
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
-
Open the Console:
- View → Console
Starter Code
--!strict
-- Demonstrate math operations available in the sandbox
export type SandboxDemo = {
angle: number,
sinValue: number,
cosValue: number,
}
function init(self: SandboxDemo): boolean
print("Calculating for 45 degrees...")
-- TODO 1: Convert 45 degrees to radians using math.rad()
-- Store in self.angle
self.angle = 0
-- TODO 2: Calculate sin and cos of the angle
-- Store in self.sinValue and self.cosValue
self.sinValue = 0
self.cosValue = 0
-- Print results (these will use your calculated values)
print(`Radians: {string.format("%.3f", self.angle)}`)
print(`Sin: {string.format("%.3f", self.sinValue)}`)
print(`Cos: {string.format("%.3f", self.cosValue)}`)
-- TODO 3: Print the answer (cos value rounded to 3 decimals)
-- Format: print(`ANSWER: {string.format("%.3f", self.cosValue)}`)
return true
end
function draw(self: SandboxDemo, renderer: Renderer)
end
return function(): Node<SandboxDemo>
return {
init = init,
draw = draw,
angle = 0,
sinValue = 0,
cosValue = 0,
}
end
Assignment
Complete these tasks:
- Complete the trigonometry calculations: convert 45 degrees to radians, then calculate sin and cos. Store the cos value (rounded to 3 decimals) as the answer.
- 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
Sandbox Limitations
The Rive scripting environment has intentional restrictions:
Not Available
| Feature | Reason | Alternative |
|---|---|---|
| File I/O | Security - scripts cannot access filesystem | Use ViewModel to store data |
| Network requests | Security - no HTTP or socket access | Pre-load data into ViewModel |
os.execute | Security - cannot run system commands | N/A |
loadstring | Security - cannot dynamically load code | Use require() for Util scripts |
require for external files | Only Util scripts in your project | Create all code in Rive |
Available
| Feature | Notes | JavaScript Equivalent |
|---|---|---|
math library | Full math functions | Math.sin(), Math.cos(), etc. |
string library | String manipulation | String methods |
table library | Table utilities | Array/Object methods |
print | Output to Console | console.log() |
| Rive APIs | Vector, Path, Paint, Color, etc. | Canvas 2D API |
Rive uses Luau (Roblox's Lua variant) which includes features like type annotations, string interpolation ({variable}), and compound assignment (+=). These features don't exist in standard Lua.
Editor Integration
Script Creation Workflow
- Assets panel > + button > Script
- Choose Node or Util
- Name your script
- Write your code in the built-in editor
Attaching Node Scripts
- Select a node in the artboard
- In the properties panel, find the Script section
- Select your Node script from the dropdown
Script Errors
When your script has errors:
- Red underlines appear in the code editor
- Error messages show in the Console
- The script won't appear in the add-to-scene menu until fixed
Knowledge Check
Common Mistakes
1. Expecting Browser Console Output
-- WRONG expectation: Looking in browser DevTools
print("Where is this?")
-- CORRECT: Check Rive's Console panel (View > Console)
print("I appear in Rive's Console!")
2. Trying to Access External Resources
-- WRONG: These don't exist in Rive
-- local file = io.open("data.txt") -- NO file access
-- local response = http.get(url) -- NO network
-- CORRECT: Use ViewModel for external data
-- Data is passed into Rive through runtime API
3. Missing --!strict (Recommended)
-- WORKS but not recommended: Missing strict directive
export type MyNode = {}
-- Script runs, but type errors won't be caught
-- RECOMMENDED: Include --!strict for type checking
--!strict
export type MyNode = {}
-- Type checker catches errors before runtime
Script Compilation Settings
Rive provides compilation settings that affect debugging and performance. Find these in the Design/Animate panel → Scripting section.
Optimization Level
Controls how aggressively the Luau compiler optimizes your code:
| Level | Description | Use Case |
|---|---|---|
| None | No optimizations. Code runs as-written. | Development - easier to debug |
| Medium | Balanced tradeoff between performance and debuggability. | General development |
| Full | Maximum optimizations. Best runtime performance. | Production - final export |
Debug Level
Controls how much debug information is included in the compiled bytecode:
| Level | Description | Use Case |
|---|---|---|
| None | Smallest bytecode. No line numbers or stack traces. | Production - smallest file size |
| Medium | Basic debug info. Line numbers for errors and stack traces. | General development |
| Full | Full debug info. Line numbers, variable names, detailed error info. | Development - best error messages |
Recommended Settings
During development:
- Optimization:
NoneorMedium - Debug:
Full
For production export:
- Optimization:
Full - Debug:
NoneorMedium
Why It Matters
-- With Debug: None
-- Error: "error in script"
-- With Debug: Full
-- Error: "error at line 42: attempt to index nil value 'myVariable'"
Higher debug levels give you detailed error messages with line numbers and variable names, making it much easier to find and fix bugs.
Summary
- Rive scripts run in a sandboxed Luau environment inside the editor
- The Console panel shows all
print()output and errors - Node scripts have a lifecycle (init, update, advance, draw)
- Util scripts are reusable modules with no lifecycle
--!strictenables type safety and is strongly recommended- Script compilation settings affect debugging (Debug Level) and performance (Optimization Level)
- The sandbox restricts file I/O, network access, and dynamic code loading
Next Steps
- Continue to Script Types Overview
- Need a refresher? Review Quick Reference