Skip to main content

Converter Script Protocol

Learning Objectives​

  • Understand when to use Converter Scripts
  • Implement convert() for source-to-target transformation
  • Implement reverseConvert() for two-way bindings
  • Work with DataValue types for type-safe conversions

AE/JS Syntax Comparison​

ConceptJavaScriptLuau (Rive Converter Scripts)
Data transformationCustom getter/setterconvert() / reverseConvert()
Type checkingtypeof value === 'number'input:isNumber()
Type castingNumber(value)(input :: DataValueNumber).value
Creating typed values{ type: 'number', value: 42 }DataValue.number() with .value = 42
Key Insight

Converter Scripts are bidirectional transformers. Unlike JavaScript where you might write separate functions for each direction, Rive requires both convert() AND reverseConvert() to handle two-way data bindings properly.


Overview​

Converter Scripts enable custom data transformations within Rive's data binding system. They act as a bridge between source and target data with bidirectional conversion.

When to use Converter Scripts:

  • Transform data between incompatible types
  • Format/parse values (e.g., number to formatted string)
  • Apply mathematical transformations to bound values
  • Create reusable data transformations across your project

The Converter Script Template​

export type MyConverter = {
-- Converter state (rarely needed)
}

-- Types of inputs this converter accepts
type DataInputs = DataValueNumber

-- Data type this converter outputs
type DataOutput = DataValueNumber

function init(self: MyConverter, context: Context): boolean
-- Initialize any converter state
return true
end

-- Converts value when binding from SOURCE to TARGET
function convert(self: MyConverter, input: DataInputs): DataOutput
local dv: DataValueNumber = DataValue.number()
if input:isNumber() then
dv.value = (input :: DataValueNumber).value * 2 -- Example: double
end
return dv
end

-- Converts value when binding from TARGET to SOURCE (for two-way binding)
function reverseConvert(self: MyConverter, input: DataOutput): DataInputs
local dv: DataValueNumber = DataValue.number()
if input:isNumber() then
dv.value = (input :: DataValueNumber).value / 2 -- Inverse: halve
end
return dv
end

return function(): Converter<MyConverter, DataInputs, DataOutput>
return {
init = init,
convert = convert,
reverseConvert = reverseConvert,
}
end

Type Signature Reference​

Converter Protocol Functions​

FunctionSignatureRequiredDescription
init(self: T, context: Context): booleanNoInitialize converter state
convert(self: T, input: DataInputs): DataOutputYesTransform source → target
reverseConvert(self: T, input: DataOutput): DataInputsYesTransform target → source
advance(self: T, seconds: number): booleanNoPer-frame updates for time-based converters

Factory Return Type​

-- Define your input and output types
type DataInputs = DataValueNumber
type DataOutput = DataValueString

return function(): Converter<YourConverterType, DataInputs, DataOutput>
return {
init = init, -- Optional
convert = convert, -- Required
reverseConvert = reverseConvert, -- Required
advance = advance, -- Optional
}
end

DataValue Types​

TypeCheck MethodCast Pattern
DataValueNumberinput:isNumber()(input :: DataValueNumber).value
DataValueStringinput:isString()(input :: DataValueString).value
DataValueBooleaninput:isBoolean()(input :: DataValueBoolean).value
DataValueColorinput:isColor()(input :: DataValueColor).value

Key Functions​

convert(self, input): DataOutput — REQUIRED​

Transforms data from source to target. Called whenever the source value changes.

function convert(self: MyConverter, input: DataInputs): DataOutput
local dv = DataValue.number()
if input:isNumber() then
local value = (input :: DataValueNumber).value
dv.value = value * 100 -- Convert to percentage
end
return dv
end

reverseConvert(self, input): DataInputs — REQUIRED​

Transforms data from target back to source. Called during two-way binding when the target changes.

function reverseConvert(self: MyConverter, input: DataOutput): DataInputs
local dv = DataValue.number()
if input:isNumber() then
local value = (input :: DataValueNumber).value
dv.value = value / 100 -- Convert from percentage
end
return dv
end
Important

reverseConvert must be the mathematical inverse of convert. If convert multiplies by 2, reverseConvert must divide by 2.


init(self, context): boolean — Optional​

One-time initialization when the converter is first used.

function init(self: MyConverter, context: Context): boolean
-- Set up any initial state
return true
end

advance(self, seconds): boolean — Optional​

Frame callback for converters that depend on elapsed time. Most converters won't need this.

function advance(self: MyConverter, seconds: number): boolean
self.elapsed += seconds
return true
end

DataValue Types​

Converter Scripts work with typed DataValue wrappers:

TypeConstructorType Check.value Type
DataValueNumberDataValue.number()isNumber()number
DataValueStringDataValue.string()isString()string
DataValueBooleanDataValue.boolean()isBoolean()boolean
DataValueColorDataValue.color()isColor()Color

DataValue API​

-- Create new DataValue instances
local numVal = DataValue.number() -- Creates DataValueNumber
local strVal = DataValue.string() -- Creates DataValueString
local boolVal = DataValue.boolean() -- Creates DataValueBoolean
local colorVal = DataValue.color() -- Creates DataValueColor

-- Type checking (call on any DataValue)
if input:isNumber() then
-- Safe to cast and access .value
end

-- Type casting and value access
local num = (input :: DataValueNumber).value -- Get number
local str = (input :: DataValueString).value -- Get string
local bool = (input :: DataValueBoolean).value -- Get boolean
local color = (input :: DataValueColor).value -- Get Color

How to Apply Converters​

  1. Create a new script, select Converter as the type
  2. In the Data panel, click the "+" button
  3. Choose Converters → Script → [YourScript]
  4. Apply to bindings in the editor

Practice Exercises​

Exercise 1: Percentage Converter ⭐​

Premise​

Converters transform data between formats. Understanding the relationship between convert() and reverseConvert() is essential for two-way bindings.

Goal

By the end of this exercise, you will implement a converter that transforms a decimal (0.75) to a percentage (75).

Use Case​

You have a progress value stored as a decimal (0.0 to 1.0), but you want to display it as a percentage (0 to 100) in the UI.


Setup​

In Rive Editor:​

  1. Create the script:

    • Assets panel → + → Script → Converter Script
    • Name it Exercise1_PercentConverter
  2. Test setup:

    • Create a ViewModel with a progress number property
    • Bind it to a text element using your converter

Starter Code​

export type Exercise1 = {}
type DataInputs = DataValueNumber
type DataOutput = DataValueNumber

function convert(self: Exercise1, input: DataInputs): DataOutput
local dv = DataValue.number()
if input:isNumber() then
local decimal = (input :: DataValueNumber).value

-- TODO: Convert decimal to percentage
-- Formula: percentage = decimal * 100
local percentage = 0

dv.value = percentage
print(`ANSWER: convert={percentage}`)
end
return dv
end

function reverseConvert(self: Exercise1, input: DataOutput): DataInputs
local dv = DataValue.number()
if input:isNumber() then
local percentage = (input :: DataValueNumber).value

-- TODO: Convert percentage back to decimal
-- Formula: decimal = percentage / 100
local decimal = 0

dv.value = decimal
end
return dv
end

return function(): Converter<Exercise1, DataInputs, DataOutput>
return {
convert = convert,
reverseConvert = reverseConvert,
}
end

Assignment​

  1. Calculate percentage as decimal * 100
  2. Calculate decimal as percentage / 100
  3. Test with Input decimal of 0.75

Expected Output​

Your console output should display the converted percentage value.


Verify Your Answer​

Verify Your Answer

Checklist​

  • convert() multiplies by 100
  • reverseConvert() divides by 100
  • Functions are mathematical inverses

Exercise 2: Temperature Converter ⭐⭐​

Premise​

Real-world converters often involve non-trivial formulas. Temperature conversion is a classic example of bidirectional transformation with offsets.

Goal

By the end of this exercise, you will implement Celsius to Fahrenheit conversion with proper inverse operations.

Use Case​

Your app stores temperatures in Celsius internally, but displays Fahrenheit for US users.


Setup​

In Rive Editor:​

  1. Create the script:
    • Assets panel → + → Script → Converter Script
    • Name it Exercise2_TempConverter

Starter Code​

export type Exercise2 = {}
type DataInputs = DataValueNumber
type DataOutput = DataValueNumber

function convert(self: Exercise2, input: DataInputs): DataOutput
local dv = DataValue.number()
if input:isNumber() then
local celsius = (input :: DataValueNumber).value

-- TODO: Convert Celsius to Fahrenheit
-- Formula: F = C * 9/5 + 32
local fahrenheit = 0

dv.value = fahrenheit
print(`ANSWER: celsius={celsius},fahrenheit={fahrenheit}`)
end
return dv
end

function reverseConvert(self: Exercise2, input: DataOutput): DataInputs
local dv = DataValue.number()
if input:isNumber() then
local fahrenheit = (input :: DataValueNumber).value

-- TODO: Convert Fahrenheit back to Celsius
-- Formula: C = (F - 32) * 5/9
local celsius = 0

dv.value = celsius
end
return dv
end

return function(): Converter<Exercise2, DataInputs, DataOutput>
return {
convert = convert,
reverseConvert = reverseConvert,
}
end

Assignment​

  1. Implement the Celsius to Fahrenheit formula: F = C * 9/5 + 32
  2. Implement the inverse: C = (F - 32) * 5/9
  3. Test with input of 25 Celsius

Expected Output​

Your console output should display both the input Celsius and converted Fahrenheit values.


Verify Your Answer​

Verify Your Answer

Checklist​

  • Fahrenheit formula correct (C * 9/5 + 32)
  • Celsius formula correct ((F - 32) * 5/9)
  • Formulas are mathematical inverses

Exercise 3: String Formatter ⭐⭐​

Premise​

Converters can transform between different types. Converting numbers to formatted strings is common for display purposes.

Goal

By the end of this exercise, you will implement a converter that formats a number with a currency prefix.

Use Case​

You have a price stored as a number (19.99), but need to display it as "$19.99" in the UI.


Setup​

In Rive Editor:​

  1. Create the script:
    • Assets panel → + → Script → Converter Script
    • Name it Exercise3_CurrencyFormatter

Starter Code​

export type Exercise3 = {}
type DataInputs = DataValueNumber
type DataOutput = DataValueString

function convert(self: Exercise3, input: DataInputs): DataOutput
local dv = DataValue.string()
if input:isNumber() then
local price = (input :: DataValueNumber).value

-- TODO: Format as currency string
-- Use string interpolation: `$amount`
local formatted = ""

dv.value = formatted
print(`ANSWER: formatted={formatted}`)
end
return dv
end

function reverseConvert(self: Exercise3, input: DataOutput): DataInputs
local dv = DataValue.number()
if input:isString() then
local str = (input :: DataValueString).value

-- TODO: Parse number from currency string
-- Remove "$" prefix and convert to number
-- Hint: Use string.sub(str, 2) to remove first character
local price = 0

dv.value = price
end
return dv
end

return function(): Converter<Exercise3, DataInputs, DataOutput>
return {
convert = convert,
reverseConvert = reverseConvert,
}
end

Assignment​

  1. Format the number as $ followed by the price
  2. Parse by removing the $ and converting with tonumber()
  3. Test with input price of 42

Expected Output​

Your console output should display the formatted currency string.


Verify Your Answer​

Verify Your Answer

Checklist​

  • convert() prepends "$" to number
  • reverseConvert() strips "$" and parses number
  • Note: DataInputs is Number, DataOutput is String

Knowledge Check​

Q:What must be true about convert() and reverseConvert()?
Q:How do you safely access a DataValue's underlying number?
Q:When is convert() called?
Q:Which functions are REQUIRED in a Converter Script?

Common Mistakes​

Avoid These Errors
  1. Forgetting reverseConvert is required

    -- WRONG: Missing reverseConvert
    return function(): Converter<MyConverter, DataInputs, DataOutput>
    return { convert = convert } -- Error!
    end

    -- CORRECT: Always include both
    return function(): Converter<MyConverter, DataInputs, DataOutput>
    return {
    convert = convert,
    reverseConvert = reverseConvert,
    }
    end
  2. Non-inverse operations

    -- WRONG: Operations don't reverse properly
    function convert(self, input)
    -- Adds 10, but reverseConvert subtracts 5 = BROKEN!
    end

    -- CORRECT: Mathematical inverses
    function convert(self, input)
    -- result = value * 2 + 10
    end
    function reverseConvert(self, input)
    -- value = (result - 10) / 2
    end
  3. Not checking input type

    -- WRONG: Accessing .value without type check
    local value = (input :: DataValueNumber).value -- Could crash!

    -- CORRECT: Always check first
    if input:isNumber() then
    local value = (input :: DataValueNumber).value -- Safe
    end

Current runtime correctness notes​

The source snapshot hardens two-way converters against alternating DataValue types (for example Number to String and back) and guards an unhydrated converter before dereferencing it. The protocol signatures do not change: keep both conversion directions type-safe and test a two-way Number-to-String round trip.

Self-Assessment Checklist​

  • I understand when to use Converter Scripts
  • I can implement convert() for source→target transformation
  • I can implement reverseConvert() as the mathematical inverse
  • I know how to use DataValue types and type checking
  • I understand the relationship between convert and reverseConvert

Next Steps​