🎮 Runtime Controls Guide¶
Comprehensive guide to controlling Rive animations at runtime
This guide covers the runtime control system that allows you to dynamically control Rive animations programmatically without requiring UI controls.
📋 Table of Contents¶
🎯 Overview¶
The runtime control system provides a streamlined way to control Rive animations programmatically, allowing you to:
- Toggle state machine inputs: Boolean, number, and trigger inputs
- Update text content: Dynamic string properties in ViewModels
- Change colors: Real-time color property updates
- Select enum values: Dropdown-style enum property control
- Swap images at runtime: Dynamic image asset replacement
- Listen for animation events: React to Rive events and state changes
🚀 Basic Setup¶
Initialize Rive Instance¶
const rive = require("@rive-app/webgl2");
// Initialize Rive with event handling
const riveInstance = new rive.Rive({
src: "your-animation.riv",
artboard: "YourArtboard",
stateMachines: "Your State Machine",
canvas: document.getElementById("rive-canvas"),
autoplay: true,
autoBind: true,
onStateChange: (stateMachine, state) => {
console.log(`State changed: ${stateMachine} -> ${state}`);
},
onLoad() {
riveInstance.resizeDrawingSurfaceToCanvas();
// Initialize your controls after load
initializeControls();
},
});
// Handle window resizing
window.addEventListener("resize", () =>
riveInstance.resizeDrawingSurfaceToCanvas(),
);
Control Object Structure¶
Create a control object that represents all modifiable properties:
const riveControls = {
stateMachines: {
"State Machine 1": {
inputs: {
"Diagram Enter": false,
Speed: 1.0,
Reset: null, // trigger
},
},
},
viewModels: {
pill_1: {
"Button Label": "Click Me",
"Label Color": 0xffffffff,
},
popup_1: {
"Image Picker": "image1",
Title: "Hello World",
Content: "This is some content",
},
},
imageAssets: {
background: "https://example.com/new-background.png",
},
};
🎛️ Control System¶
Initialize Controls¶
After the Rive instance loads, initialize your control system:
function initializeControls() {
// Get the diagram view model
const diagramVM = riveInstance.viewModelInstance;
// Map all available properties for programmatic control
const controlMap = buildControlMap(riveInstance, diagramVM);
// Apply initial values if needed
applyControls(controlMap, riveControls);
// Now you can update controls programmatically
// Example: updateControl(controlMap, "pill_1.Button Label", "New Label");
}
Building the Control Map¶
Create a map of all controllable properties:
function buildControlMap(riveInst, diagramVM) {
const controlMap = {
stateMachineInputs: {},
viewModels: {},
imageAssets: {},
};
// Map state machine inputs
riveInst.stateMachineNames.forEach((smName) => {
controlMap.stateMachineInputs[smName] = {};
riveInst.stateMachineInputs(smName).forEach((input) => {
controlMap.stateMachineInputs[smName][input.name] = input;
});
});
// Map view model properties
diagramVM.properties
.filter((p) => p.type === "viewModel")
.forEach((p) => {
const vm = diagramVM.viewModel(p.name);
controlMap.viewModels[p.name] = {};
// Try to map string properties
try {
const stringInputs = vm.strings();
stringInputs.forEach((name) => {
controlMap.viewModels[p.name][name] = vm.string(name);
});
} catch (_e) {}
// Try to map color properties
try {
const colorInputs = vm.colors();
colorInputs.forEach((name) => {
controlMap.viewModels[p.name][name] = vm.color(name);
});
} catch (_e) {}
// Try to map enum properties
try {
const enumInputs = vm.enums();
enumInputs.forEach((name) => {
controlMap.viewModels[p.name][name] = vm.enum(name);
});
} catch (_e) {}
});
// Map image assets
riveInst.assets().forEach((asset) => {
if (asset.isImage) {
controlMap.imageAssets[asset.name] = asset;
}
});
return controlMap;
}
Updating Controls¶
Update any property at runtime:
function updateControl(controlMap, path, value) {
const parts = path.split(".");
// Handle state machine inputs
if (parts[0] === "stateMachines") {
const smName = parts[1];
const inputName = parts[2];
controlMap.stateMachineInputs[smName][inputName].value = value;
return true;
}
// Handle view model properties
if (parts[0] === "viewModels") {
const vmName = parts[1];
const propName = parts[2];
const prop = controlMap.viewModels[vmName][propName];
// Handle different property types
if (typeof value === "string" && prop.type === "string") {
prop.value = value.replace(/\n/g, "\\n");
} else if (typeof value === "string" && prop.type === "enum") {
prop.value = value;
} else if (typeof value === "number" && prop.type === "color") {
prop.value = value; // Expecting ARGB format (0xFFFFFFFF)
}
return true;
}
// Handle image assets
if (parts[0] === "imageAssets") {
const assetName = parts[1];
substituteImage(controlMap.imageAssets[assetName], value);
return true;
}
return false;
}
// Apply all controls at once
function applyControls(controlMap, controlValues) {
// Apply state machine inputs
Object.entries(controlValues.stateMachines || {}).forEach(
([smName, inputs]) => {
Object.entries(inputs.inputs || {}).forEach(
([inputName, value]) => {
updateControl(
controlMap,
`stateMachines.${smName}.${inputName}`,
value,
);
},
);
},
);
// Apply view model properties
Object.entries(controlValues.viewModels || {}).forEach(
([vmName, props]) => {
Object.entries(props).forEach(([propName, value]) => {
updateControl(
controlMap,
`viewModels.${vmName}.${propName}`,
value,
);
});
},
);
// Apply image assets
Object.entries(controlValues.imageAssets || {}).forEach(
([assetName, url]) => {
updateControl(controlMap, `imageAssets.${assetName}`, url);
},
);
}
🔧 API Reference¶
Rive Instance Methods¶
| Method | Description | Returns |
|---|---|---|
stateMachineNames | Get all state machine names | string[] |
stateMachineInputs(name) | Get inputs for a specific state machine | Input[] |
viewModelInstance | Get the root view model | ViewModelInstance |
assets() | Get all assets in the Rive file | Asset[] |
enums() | Get all enum definitions | Enum[] |
on(eventType, callback) | Listen for events | void |
Control Path Format¶
- State machine inputs:
stateMachines.{MACHINE_NAME}.{INPUT_NAME} - View model properties:
viewModels.{MODEL_NAME}.{PROPERTY_NAME} - Image assets:
imageAssets.{ASSET_NAME}
Input Types¶
| Type | Description | Value Format |
|---|---|---|
Boolean | True/false toggle | true or false |
Number | Numeric value | Any number |
Trigger | One-time event | Call .fire() method |
String | Text content | Any string |
Color | ARGB color value | 0xFFRRGGBB format |
Enum | Predefined options | String matching enum value |
💡 Examples¶
Basic State Machine Control¶
// Toggle a boolean input
updateControl(controlMap, "stateMachines.MainSM.isVisible", true);
// Set a number input
updateControl(controlMap, "stateMachines.MainSM.speed", 2.5);
// Fire a trigger
const triggerInput = controlMap.stateMachineInputs["MainSM"]["reset"];
triggerInput.fire();
ViewModel Property Updates¶
// Update text content
updateControl(controlMap, "viewModels.textBox.content", "New text content");
// Change color (ARGB format)
updateControl(controlMap, "viewModels.button.backgroundColor", 0xff00ff00); // Green
// Set enum value
updateControl(controlMap, "viewModels.dropdown.selectedOption", "option2");
Image Swapping¶
function substituteImage(asset, url) {
if (!asset || !url) return;
fetch(url)
.then((r) => r.arrayBuffer())
.then((buf) => rive.decodeImage(new Uint8Array(buf)))
.then((img) => {
asset.setRenderImage(img);
img.unref();
})
.catch((e) => console.error("Image decode error", e));
}
// Usage
updateControl(
controlMap,
"imageAssets.background",
"https://example.com/new-bg.png",
);
Event Listening¶
// Listen for Rive events
riveInstance.on(rive.EventType.RiveEvent, (e) => {
const data = e.data || {};
if (data.type === rive.RiveEventType.General) {
console.log(`Rive Event: ${data.name}`);
// Trigger your custom handlers here
} else if (data.type === rive.RiveEventType.OpenUrl) {
console.log(`OpenUrl Event: ${data.url}`);
// Handle URL opening
}
});
// Listen for state changes
function handleStateChange(stateMachine, state) {
console.log(`State Change: ${stateMachine} -> ${state}`);
// Add your custom state change handlers here
}
🎨 Advanced Techniques¶
Color Conversion Helpers¶
// Convert ARGB integer to hex string
function argbToHex(argb) {
return "#" + (argb & 0xffffff).toString(16).padStart(6, "0").toUpperCase();
}
// Convert hex string to ARGB integer
function hexToArgb(hex) {
return parseInt("FF" + hex.slice(1), 16);
}
// Usage
const redColor = hexToArgb("#FF0000");
updateControl(controlMap, "viewModels.button.color", redColor);
Batch Updates¶
// Update multiple properties efficiently
function batchUpdate(controlMap, updates) {
const startTime = performance.now();
updates.forEach(({ path, value }) => {
updateControl(controlMap, path, value);
});
const endTime = performance.now();
console.log(`Batch update completed in ${endTime - startTime}ms`);
}
// Usage
batchUpdate(controlMap, [
{ path: "viewModels.title.text", value: "New Title" },
{ path: "viewModels.title.color", value: 0xff0000ff },
{ path: "stateMachines.MainSM.isActive", value: true },
]);
Animation Sequences¶
// Create animated sequences
function animateProperty(controlMap, path, startValue, endValue, duration) {
const startTime = performance.now();
function animate() {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Linear interpolation
const currentValue = startValue + (endValue - startValue) * progress;
updateControl(controlMap, path, currentValue);
if (progress < 1) {
requestAnimationFrame(animate);
}
}
animate();
}
// Usage
animateProperty(controlMap, "stateMachines.MainSM.progress", 0, 100, 2000); // 2 second animation
Complete Usage Example¶
// Initialize Rive
const riveCanvas = document.getElementById("rive-canvas");
const riveInstance = new rive.Rive({
src: "diagram.riv",
artboard: "Diagram",
stateMachines: "State Machine 1",
canvas: riveCanvas,
autoplay: true,
autoBind: true,
onStateChange: handleStateChange,
onLoad() {
riveInstance.resizeDrawingSurfaceToCanvas();
// Initialize control system
const controlMap = buildControlMap(
riveInstance,
riveInstance.viewModelInstance,
);
// Update specific controls
updateControl(controlMap, "viewModels.pill_1.Button Label", "Start");
updateControl(controlMap, "viewModels.pill_1.Label Color", 0xff00ff00); // Green
updateControl(
controlMap,
"stateMachines.State Machine 1.Diagram Enter",
true,
);
// Or update all at once
applyControls(controlMap, {
viewModels: {
popup_1: {
Title: "Welcome!",
Content: "This is controlled programmatically",
},
},
imageAssets: {
background: "https://example.com/background.png",
},
});
},
});
🔧 Tips for Production Use¶
- Cache the control map after building it to avoid rebuilding on every update
- Implement data validation before updating properties
- Set up error handling for missing properties or invalid values
- Create convenience methods for your specific animation's common operations
- Use debouncing for rapid updates to avoid performance issues
- Monitor performance with the built-in debugging tools
- Test thoroughly across different browsers and devices
🐛 Debugging Runtime Controls¶
Use the global debugging features to troubleshoot control issues:
// Access global Rive instance
const rive = window.riveInstanceGlobal;
// Inspect available controls
console.log("Available controls:", getAllControllableProperties());
// Test control updates
function testControl(path, value) {
console.log(`Testing: ${path} = ${value}`);
const result = updateControl(controlMap, path, value);
console.log(`Result: ${result ? "Success" : "Failed"}`);
}
// Monitor control changes
function monitorControls() {
const snapshot1 = createStateSnapshot();
setTimeout(() => {
const snapshot2 = createStateSnapshot();
const differences = compareSnapshots(snapshot1, snapshot2);
console.log("Control changes:", differences);
}, 1000);
}
This runtime controls system provides powerful programmatic control over your Rive animations, enabling dynamic, interactive experiences without manual UI controls.