Operators
Learning Objectives
By the end of this lesson, you will be able to:
- Use all arithmetic operators including floor division
- Compare values using Luau's comparison operators (including
~=) - Combine conditions with logical operators (
and,or,not) - Simplify code with compound assignment operators (
+=,-=, etc.) - Concatenate strings and use interpolation
- Understand operator precedence to write predictable expressions
AE/JS Syntax Comparison
Here's a quick reference for how operators differ between languages:
| Operation | After Effects | JavaScript | Luau |
|---|---|---|---|
| Not equal | != | !== or != | ~= |
| Logical AND | && | && | and |
| Logical OR | || | || | or |
| Logical NOT | ! | ! | not |
| String concat | + | + | .. |
| Exponent | Math.pow(x,y) | ** or Math.pow() | ^ |
| Floor divide | Math.floor(x/y) | Math.floor(x/y) | // |
| Increment | x++ or x += 1 | x++ or x += 1 | x += 1 |
- Not Equal: Use
~=instead of!=or!== - Logical operators: Use words
and,or,notinstead of symbols&&,||,! - String concatenation: Use
..instead of+ - No increment/decrement: No
++or--operators; use+= 1or-= 1 - Exponentiation: Use
^instead of**
Rive Context
Operators are fundamental to every calculation in your Rive animations. Whether you're computing damage values, lerping positions, checking collision bounds, or animating properties over time, you'll use operators constantly. Understanding operator precedence and behavior is essential for writing predictable animation logic.
In Rive Node Scripts, operators work inside lifecycle functions (init, advance, draw). Results of calculations often get stored on self to persist across frames or drive visual properties.
Arithmetic Operators
These perform mathematical calculations on numbers.
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 10 + 5 | 15 |
- | Subtraction | 10 - 5 | 5 |
* | Multiplication | 10 * 5 | 50 |
/ | Division | 10 / 3 | 3.333... |
// | Floor Division | 10 // 3 | 3 |
% | Modulo (remainder) | 10 % 3 | 1 |
^ | Exponentiation | 2 ^ 3 | 8 |
- | Unary Negation | -5 | -5 |
Floor Division vs Regular Division
local regular = 10 / 3 -- 3.3333...
local floored = 10 // 3 -- 3 (rounds down)
JavaScript Equivalent:
// JavaScript requires Math.floor for floor division
const regular = 10 / 3; // 3.3333...
const floored = Math.floor(10 / 3); // 3
Floor division is useful for:
- Grid-based calculations (converting pixel position to tile index)
- Frame counting (every Nth frame)
- Integer-only operations
Exponentiation
-- Luau uses ^ for exponents
local squared = 5 ^ 2 -- 25
local cubed = 2 ^ 3 -- 8
local sqRoot = 9 ^ 0.5 -- 3 (square root)
JavaScript Equivalent:
// JavaScript uses ** or Math.pow()
const squared = 5 ** 2; // 25
const cubed = 2 ** 3; // 8
const sqRoot = 9 ** 0.5; // 3
// or
const squared = Math.pow(5, 2);
Comparison Operators
These compare two values and return a boolean (true or false).
| Operator | Name | Example | Result |
|---|---|---|---|
== | Equal | 5 == 5 | true |
~= | Not Equal | 5 ~= 3 | true |
< | Less Than | 3 < 5 | true |
> | Greater Than | 5 > 3 | true |
<= | Less Than or Equal | 5 <= 5 | true |
>= | Greater Than or Equal | 5 >= 5 | true |
~= for Not EqualUnlike JavaScript which uses != or !==, Luau uses ~= for inequality. This is the #1 source of syntax errors when coming from other languages.
-- CORRECT in Luau:
if score ~= 0 then print("Has score") end
-- WRONG (will cause error):
if score != 0 then print("Has score") end -- ERROR!
JavaScript Comparison:
// JavaScript
if (score !== 0) { console.log("Has score"); }
if (score != 0) { console.log("Has score"); }
// Luau
if score ~= 0 then print("Has score") end
Logical Operators
These work with boolean values and are essential for complex conditions.
| Operator | Name | Description |
|---|---|---|
and | Logical AND | True if both operands are true |
or | Logical OR | True if at least one operand is true |
not | Logical NOT | Inverts the boolean value |
Word Operators vs Symbol Operators
-- Luau uses WORDS for logic:
local canAttack = hasWeapon and notStunned
local isVulnerable = isLowHealth or isPoisoned
local isSafe = not isInDanger
// JavaScript uses SYMBOLS for logic:
const canAttack = hasWeapon && notStunned;
const isVulnerable = isLowHealth || isPoisoned;
const isSafe = !isInDanger;
Short-Circuit Evaluation
Logical operators use short-circuit evaluation - they stop as soon as the result is determined:
-- 'and' stops at the first false value
local result = false and expensiveFunction() -- expensiveFunction never called
-- 'or' stops at the first true value
local result = true or expensiveFunction() -- expensiveFunction never called
Practical Use - Default Values:
-- Common pattern: provide default value
local name = inputName or "Anonymous"
local health = inputHealth or 100
-- Works because 'or' returns first truthy value
Truthy and Falsy Values
false and nil are falsy!In Luau, 0, "", and {} are all truthy. This affects logical operators:
local x = 0
if x then
print("0 is truthy!") -- This prints!
end
-- Common mistake from JavaScript:
local count = 0
local display = count or "no count" -- display = 0, NOT "no count"!
-- Because 0 is truthy in Luau, 'or' returns 0
-- Correct approach:
local display = if count == 0 then "no count" else count
Compound Assignment Operators
These combine an operation with assignment for cleaner code. The key insight: they modify the variable in place and update it immediately.
| Operator | Equivalent | Example |
|---|---|---|
+= | x = x + y | score += 10 |
-= | x = x - y | health -= damage |
*= | x = x * y | scale *= 2 |
/= | x = x / y | speed /= 2 |
//= | x = x // y | index //= 2 |
%= | x = x % y | angle %= 360 |
^= | x = x ^ y | value ^= 2 |
..= | x = x .. y | text ..= "!" |
How Sequential Operations Work
When you chain compound assignments, each operation uses the NEW value from the previous line, not the original:
local x = 10
x += 5 -- x = 10 + 5 = 15 (x is NOW 15)
x -= 3 -- x = 15 - 3 = 12 (NOT 10 - 3! Using updated x)
x *= 2 -- x = 12 * 2 = 24 (using x = 12)
x /= 4 -- x = 24 / 4 = 6 (using x = 24)
x //= 2 -- x = 6 // 2 = 3 (floor division, using x = 6)
print(x) -- Output: 3
Step-by-step breakdown:
| Line | Before | Operation | After |
|---|---|---|---|
x += 5 | x = 10 | 10 + 5 | x = 15 |
x -= 3 | x = 15 | 15 - 3 | x = 12 |
x *= 2 | x = 12 | 12 × 2 | x = 24 |
x /= 4 | x = 24 | 24 ÷ 4 | x = 6 |
x //= 2 | x = 6 | 6 // 2 | x = 3 |
Common Animation Pattern
This sequential nature is perfect for animation where values change incrementally each frame:
function advance(self: Player, seconds: number): boolean
-- Each frame, rotation increases from its current value
self.rotation += 45 * seconds -- Add 45° per second
self.rotation %= 360 -- Keep in 0-360 range
-- Scale pulses: grows then shrinks
if self.growing then
self.scale *= 1.01 -- Grow 1% from current scale
else
self.scale /= 1.01 -- Shrink 1% from current scale
end
return true
end
++ or -- in LuauUnlike JavaScript, Luau does not have increment (++) or decrement (--) operators. Use += 1 and -= 1 instead.
count += 1
count -= 1
// JavaScript:
count++;
count--;
String Operators
| Operator | Name | Example | Result |
|---|---|---|---|
.. | Concatenation | "Hello" .. " World" | "Hello World" |
# | Length | #"Hello" | 5 |
String Concatenation
.. not +This is a common source of errors for JavaScript developers:
-- CORRECT in Luau:
local greeting = "Hello" .. " " .. "World"
-- WRONG (error or unexpected behavior):
local greeting = "Hello" + " " + "World" -- ERROR!
String Interpolation
Luau supports string interpolation with backticks - the cleanest approach:
local name = "Player"
local score = 100
local message = `{name} scored {score} points!` -- "Player scored 100 points!"
JavaScript Comparison:
// JavaScript uses ${} for interpolation
const message = `${name} scored ${score} points!`;
// Luau uses {} (no $ needed)
// const message = `{name} scored {score} points!`
Operator Precedence
From highest to lowest precedence:
^(exponentiation, right associative)- Unary:
not,-,# *,/,//,%+,-..(concatenation, right associative)<,>,<=,>=,~=,==andor
Why Precedence Matters
-- Without understanding precedence, this is confusing:
local result = 2 + 3 * 4 ^ 2 -- What is this?
-- With precedence knowledge:
-- Step 1: 4 ^ 2 = 16 (exponents first)
-- Step 2: 3 * 16 = 48 (multiplication next)
-- Step 3: 2 + 48 = 50 (addition last)
local result = 2 + 3 * 4 ^ 2 -- = 50
-- Always use parentheses for clarity:
local result = 2 + (3 * (4 ^ 2)) -- Clear: 50
Exponentiation is Right-Associative
-- This is a gotcha! Exponentiation chains right-to-left:
local a = 2 ^ 3 ^ 2 -- = 2 ^ (3 ^ 2) = 2 ^ 9 = 512
-- NOT (2 ^ 3) ^ 2 = 8 ^ 2 = 64
-- When in doubt, use parentheses:
local explicit = 2 ^ (3 ^ 2) -- 512, clearly right-to-left
local explicit2 = (2 ^ 3) ^ 2 -- 64, clearly left-to-right
Exercises
Exercise 1: Damage Math ⭐
Premise
Rive animations are driven by numbers: positions, sizes, and effects all come from arithmetic. Practicing the core operators ensures you can compute values that drive visuals correctly.
By the end of this exercise, you will use arithmetic operators to compute a deterministic combat result.
Use Case
You are building a combat HUD where damage numbers, shields, and effects are all computed in script. A single mistake in a multiplication or modulo can produce the wrong hit feedback. This exercise mirrors the exact sort of math that feeds a damage readout or a shake intensity.
Example scenarios:
- Damage numbers for a hit flash
- A screen shake scale based on damage
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise1_DamageMath
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise1 = {}
function init(self: Exercise1): boolean
local base = 12
local bonus = 5
local penalty = 3
local multiplier = 1.5
-- TODO 1: raw = base + bonus - penalty
local raw = 0
-- TODO 2: total = raw * multiplier
local total = 0
-- TODO 3: remainder = total % 4
local remainder = 0
-- TODO 4: power = 2 ^ 3
local power = 0
print(`ANSWER: total={total},rem={remainder},pow={power}`)
return true
end
function draw(self: Exercise1, renderer: Renderer)
end
return function(): Node<Exercise1>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Compute
rawusing+and- - Compute
totalusing* - Compute
remainderusing% - Compute
powerusing^
Expected Output
Your console output should display three computed values:
total=— the raw damage multiplied by the multiplierrem=— the remainder when total is divided by 4pow=— 2 raised to the power of 3
Verify Your Answer
Checklist
- All four operators are used
-
totalequals 21 - Output matches the expected line
Exercise 2: Comparison Gate ⭐
Premise
Comparisons decide when animations are allowed to play or when UI changes should occur. Incorrect comparisons lead to wrong states, like animations firing too early.
By the end of this exercise, you will use comparison operators to set boolean flags.
Use Case
You are gating a special animation that should only play when the player is high enough level and still alive. These checks are the backbone of all condition-driven animation logic.
Example scenarios:
- Unlocking premium visuals after a threshold
- Switching to critical animation states
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise2_ComparisonGate
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise2 = {}
function init(self: Exercise2): boolean
local health = 8
local level = 12
local name = "Boss"
-- TODO 1: isBoss should be true when level >= 10
local isBoss = false
-- TODO 2: isAlive should be true when health > 0
local isAlive = false
-- TODO 3: isCritical should be true when health <= 10
local isCritical = false
-- TODO 4: isDifferent should be true when name ~= "Minion"
local isDifferent = false
print(`ANSWER: boss={isBoss},alive={isAlive},critical={isCritical},diff={isDifferent}`)
return true
end
function draw(self: Exercise2, renderer: Renderer)
end
return function(): Node<Exercise2>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Use
>=to computeisBoss - Use
>to computeisAlive - Use
<=to computeisCritical - Use
~=to computeisDifferent
Expected Output
Your console output should display four boolean values:
boss=— whether level meets the boss thresholdalive=— whether health is above zerocritical=— whether health is at or below the critical thresholddiff=— whether name is different from "Minion"
Verify Your Answer
Checklist
- All four comparisons are used
-
~=is used for not equal - Output matches the expected line
Exercise 3: Access Logic ⭐
Premise
Most interaction logic is a combination of conditions. The and, or, and not operators let you describe those combinations clearly.
By the end of this exercise, you will combine multiple booleans into final decision flags.
Use Case
You are controlling access to a UI panel. It should open only if the user has a key, no guard is present, and the alarm is off. A separate hide flag triggers an alert animation if any danger is present.
Example scenarios:
- Gating a menu or dialog
- Conditional visibility of alerts
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise3_AccessLogic
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise3 = {}
function init(self: Exercise3): boolean
local hasKey = true
local alarmOn = false
local guardPresent = false
-- TODO 1: canEnter should be true only if hasKey and not alarmOn and not guardPresent
local canEnter = false
-- TODO 2: shouldHide should be true if alarmOn or guardPresent
local shouldHide = false
print(`ANSWER: enter={canEnter},hide={shouldHide}`)
return true
end
function draw(self: Exercise3, renderer: Renderer)
end
return function(): Node<Exercise3>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Compute
canEnterusingandandnot - Compute
shouldHideusingor
Expected Output
Your console output should display two boolean values:
enter=— whether all conditions for entry are met (key AND no alarm AND no guard)hide=— whether any danger condition is present (alarm OR guard)
Verify Your Answer
Checklist
-
canEnterusesandandnot -
shouldHideusesor - Output matches the expected line
Exercise 4: Motion Stepper ⭐⭐
Premise
Compound assignments are the most common way to update animation state each frame. If you are building motion, you will use them constantly.
By the end of this exercise, you will update position and velocity using compound operators.
Use Case
You are animating a UI element that accelerates and then speeds up. Each step updates position and velocity. Compact updates reduce mistakes and make the script easier to reason about.
Example scenarios:
- Moving indicators
- Animating slider handles
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise4_MotionStepper
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise4 = {}
function init(self: Exercise4): boolean
local position = 0
local velocity = 10
local drag = 2
-- TODO 1: position += velocity
-- TODO 2: velocity -= drag
-- TODO 3: position += velocity
-- TODO 4: velocity *= 2
-- TODO 5: position += velocity
print(`ANSWER: pos={position},vel={velocity}`)
return true
end
function draw(self: Exercise4, renderer: Renderer)
end
return function(): Node<Exercise4>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks in order:
- Apply the first position update
- Apply drag to velocity
- Apply the second position update
- Double the velocity
- Apply the final position update
Expected Output
Your console output should display the final position and velocity after all five operations are applied in sequence. Remember that each operation uses the updated value from the previous step.
Verify Your Answer
Checklist
- You used
+=,-=, and*=at least once - Final position is 34
- Output matches the expected line
Exercise 5: UI Label Assembly ⭐⭐
Premise
Operators also handle strings. Building UI labels often requires concatenation and length checks for layout.
By the end of this exercise, you will build a label and compute its length.
Use Case
You are creating a health label that shows current and max HP. You also need to know its length for alignment or truncation logic.
Example scenarios:
- Dynamic text labels
- Auto-resizing text containers
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise5_UILabelAssembly
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise5 = {}
function init(self: Exercise5): boolean
local prefix = "HP"
local current = 85
local maxHp = 100
-- TODO 1: Build label using concatenation
local label = ""
-- TODO 2: Compute labelLength using #label
local labelLength = 0
print(`ANSWER: {label}|len={labelLength}`)
return true
end
function draw(self: Exercise5, renderer: Renderer)
end
return function(): Node<Exercise5>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Build
labelasHP: 85/100 - Compute
labelLengthwith#label
Expected Output
Your console output should display:
- The formatted health label in the format
[prefix]: [current]/[max] - The length of that label string after the pipe character
Verify Your Answer
Checklist
-
labeluses the..operator -
labelLengthequals 10 - Output matches the expected line
Exercise 6: Precedence Check ⭐⭐
Premise
Operator precedence mistakes are subtle and lead to incorrect calculations that are hard to debug. Understanding order of operations keeps your scripts predictable.
By the end of this exercise, you will compute values using default precedence and explicit grouping.
Use Case
You are building a formula for a dynamic animation effect and need to verify whether multiplication and exponentiation are applied in the correct order. This is common when you write easing or intensity formulas.
Example scenarios:
- Easing curves and falloff calculations
- Priority rules in state computations
Setup
In Rive Editor:
-
Create the script:
- Assets panel →
+→ Script → Node Script - Name it
Exercise6_PrecedenceCheck
- Assets panel →
-
Attach and run:
- Attach to any shape and press Play
Starter Code
--!strict
export type Exercise6 = {}
function init(self: Exercise6): boolean
local defaultOrder = 0
local grouped = 0
local rightAssoc = 0
local leftAssoc = 0
-- TODO 1: defaultOrder should be 2 + 3 * 4
-- TODO 2: grouped should be (2 + 3) * 4
-- TODO 3: rightAssoc should be 2 ^ 3 ^ 2
-- TODO 4: leftAssoc should be (2 ^ 3) ^ 2
print(`ANSWER: d={defaultOrder},g={grouped},r={rightAssoc},l={leftAssoc}`)
return true
end
function draw(self: Exercise6, renderer: Renderer)
end
return function(): Node<Exercise6>
return {
init = init,
draw = draw,
}
end
Assignment
Complete these tasks:
- Compute
defaultOrderwith no parentheses - Compute
groupedwith parentheses - Compute
rightAssocusing the default exponent behavior - Compute
leftAssocusing explicit grouping
Expected Output
Your console output should display four values demonstrating precedence:
d=— default precedence with no parentheses (multiplication before addition)g=— grouped with parentheses to force addition firstr=— right-associative exponentiation (evaluates right-to-left)l=— left-associative grouping (evaluates left-to-right)
Verify Your Answer
Checklist
-
^is right-associative in the rightAssoc line - Parentheses change the grouped and leftAssoc results
- Output matches the expected line
- Using
!=instead of~=- Luau uses~=for not-equal - Using
&&,||,!- Luau usesand,or,not(words, not symbols) - Assuming 0 is falsy - In Luau, 0 is truthy; use explicit
== 0checks - Using
+for strings - Luau uses..for string concatenation - Forgetting exponent precedence -
^is right-associative:2^3^2= 512, not 64 - Using
++or--- Luau has no increment/decrement; use+= 1or-= 1 - Division precision -
/returns float, use//for integer division - String concat in interpolation - Inside backticks, don't use
..; use{a}{b}
Quick Reference Card
-- Arithmetic
+ - * / // % ^ -x
-- Comparison (note: ~= not !=)
== ~= < > <= >=
-- Logical (note: words not symbols)
and or not
-- String
.. #str `interpolation {var}`
-- Compound Assignment
+= -= *= /= //= %= ^= ..=
-- NO ++ or -- in Luau!
-- Use: x += 1 or x -= 1
Next Steps
- Continue to 1.2 Control Flow & Loops
- Need a refresher? Review Quick Reference