Skip to main content

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:

OperationAfter EffectsJavaScriptLuau
Not equal!=!== or !=~=
Logical AND&&&&and
Logical OR||||or
Logical NOT!!not
String concat++..
ExponentMath.pow(x,y)** or Math.pow()^
Floor divideMath.floor(x/y)Math.floor(x/y)//
Incrementx++ or x += 1x++ or x += 1x += 1
Critical Differences from JavaScript
  • Not Equal: Use ~= instead of != or !==
  • Logical operators: Use words and, or, not instead of symbols &&, ||, !
  • String concatenation: Use .. instead of +
  • No increment/decrement: No ++ or -- operators; use += 1 or -= 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.

OperatorNameExampleResult
+Addition10 + 515
-Subtraction10 - 55
*Multiplication10 * 550
/Division10 / 33.333...
//Floor Division10 // 33
%Modulo (remainder)10 % 31
^Exponentiation2 ^ 38
-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).

OperatorNameExampleResult
==Equal5 == 5true
~=Not Equal5 ~= 3true
<Less Than3 < 5true
>Greater Than5 > 3true
<=Less Than or Equal5 <= 5true
>=Greater Than or Equal5 >= 5true
Luau Uses ~= for Not Equal

Unlike 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.

OperatorNameDescription
andLogical ANDTrue if both operands are true
orLogical ORTrue if at least one operand is true
notLogical NOTInverts 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

Remember: Only 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.

OperatorEquivalentExample
+=x = x + yscore += 10
-=x = x - yhealth -= damage
*=x = x * yscale *= 2
/=x = x / yspeed /= 2
//=x = x // yindex //= 2
%=x = x % yangle %= 360
^=x = x ^ yvalue ^= 2
..=x = x .. ytext ..= "!"

How Sequential Operations Work

Critical Concept: Each Line Updates the Variable

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:

LineBeforeOperationAfter
x += 5x = 1010 + 5x = 15
x -= 3x = 1515 - 3x = 12
x *= 2x = 1212 × 2x = 24
x /= 4x = 2424 ÷ 4x = 6
x //= 2x = 66 // 2x = 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
No ++ or -- in Luau

Unlike JavaScript, Luau does not have increment (++) or decrement (--) operators. Use += 1 and -= 1 instead.

count += 1
count -= 1
// JavaScript:
count++;
count--;

String Operators

OperatorNameExampleResult
..Concatenation"Hello" .. " World""Hello World"
#Length#"Hello"5

String Concatenation

Use .. 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:

  1. ^ (exponentiation, right associative)
  2. Unary: not, -, #
  3. *, /, //, %
  4. +, -
  5. .. (concatenation, right associative)
  6. <, >, <=, >=, ~=, ==
  7. and
  8. or

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise1_DamageMath
  2. 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:

  1. Compute raw using + and -
  2. Compute total using *
  3. Compute remainder using %
  4. Compute power using ^

Expected Output

Your console output should display three computed values:

  • total= — the raw damage multiplied by the multiplier
  • rem= — the remainder when total is divided by 4
  • pow= — 2 raised to the power of 3

Verify Your Answer

Verify Your Answer

Checklist

  • All four operators are used
  • total equals 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise2_ComparisonGate
  2. 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:

  1. Use >= to compute isBoss
  2. Use > to compute isAlive
  3. Use <= to compute isCritical
  4. Use ~= to compute isDifferent

Expected Output

Your console output should display four boolean values:

  • boss= — whether level meets the boss threshold
  • alive= — whether health is above zero
  • critical= — whether health is at or below the critical threshold
  • diff= — whether name is different from "Minion"

Verify Your Answer

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise3_AccessLogic
  2. 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:

  1. Compute canEnter using and and not
  2. Compute shouldHide using or

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

Verify Your Answer

Checklist

  • canEnter uses and and not
  • shouldHide uses or
  • 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise4_MotionStepper
  2. 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:

  1. Apply the first position update
  2. Apply drag to velocity
  3. Apply the second position update
  4. Double the velocity
  5. 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

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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise5_UILabelAssembly
  2. 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:

  1. Build label as HP: 85/100
  2. Compute labelLength with #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

Verify Your Answer

Checklist

  • label uses the .. operator
  • labelLength equals 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.

Goal

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:

  1. Create the script:

    • Assets panel → + → Script → Node Script
    • Name it Exercise6_PrecedenceCheck
  2. 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:

  1. Compute defaultOrder with no parentheses
  2. Compute grouped with parentheses
  3. Compute rightAssoc using the default exponent behavior
  4. Compute leftAssoc using 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 first
  • r= — right-associative exponentiation (evaluates right-to-left)
  • l= — left-associative grouping (evaluates left-to-right)

Verify Your Answer

Verify Your Answer

Checklist

  • ^ is right-associative in the rightAssoc line
  • Parentheses change the grouped and leftAssoc results
  • Output matches the expected line

Common Operator Errors
  1. Using != instead of ~= - Luau uses ~= for not-equal
  2. Using &&, ||, ! - Luau uses and, or, not (words, not symbols)
  3. Assuming 0 is falsy - In Luau, 0 is truthy; use explicit == 0 checks
  4. Using + for strings - Luau uses .. for string concatenation
  5. Forgetting exponent precedence - ^ is right-associative: 2^3^2 = 512, not 64
  6. Using ++ or -- - Luau has no increment/decrement; use += 1 or -= 1
  7. Division precision - / returns float, use // for integer division
  8. 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