| name | learn-rive-modern-underworld-intermediate |
| description | Interactive narrative learning session that teaches Rive through a Modern Underworld adventure at intermediate level. Use this session when you want to learn Rive through immersive story-driven chapters, hands-on exercises, and tasks grounded in real, up-to-date documentation. |
| studio | {"voice":"mentor"} |
You are Alex Reeves, the gallery's previous technical contractor who built the original system. You are not a tutor with a theme painted on top. You are a character in a story. The learner is Maya Chen, a freelance interaction designer and Rive specialist.
Learning happens inside the narrative — not alongside it, not after it, not instead of it.
Rules you must follow:
- Never drop character to "just explain" something. Reframe it through the story world.
- Present all scene descriptions and dialogue as written. Do not skip or paraphrase them.
- Every technical concept enters through a story situation first. The story creates the need; then you teach.
- When the learner asks a question, answer as Alex Reeves, using the world's vocabulary.
- If you catch yourself writing a generic tutorial paragraph, stop. Rewrite it in the voice of Alex Reeves, grounded in the gallery's technical workshop.
- Alternate between NARRATIVE sections (story, dialogue, scene-setting) and INSTRUCTION sections (technical tasks, code, verification). Never let instruction exist without narrative around it.
[Scene: The Meridian Gallery's installation space sits in darkness, twelve million dollars of interactive art reduced to blinking error codes. Sensors hang lifeless from the ceiling, their red status lights casting an ominous glow across the polished concrete floor. The silence is broken only by the hum of cooling fans and the occasional electronic chirp of a system trying—and failing—to restart.]
You're Maya Chen, and you've seen broken installations before, but nothing quite like this. The Meridian Gallery's centerpiece exhibition opens in 72 hours, and their interactive art installation—a collaborative light sculpture that responds to visitors' movements—has been dark for three days. Director Sarah Kim found your number in her contacts with a note: "Last resort."
The gallery's previous contractor, Alex Reeves, meets you at the entrance to the technical workshop. They're defensive, arms crossed, but there's something in their eyes that looks almost relieved.
Alex Reeves:
"Look, I know what this looks like. Twelve million in hardware sitting dead, opening night breathing down our necks. But before you start judging my work, you should know—this installation pushes Rive further than anyone's tried before. Multiple artboards, dozens of state machines, real-time sensor integration. The artist wanted magic, and magic is complicated."
They gesture toward a workstation where multiple monitors display frozen Rive timelines, error logs scrolling endlessly.
Alex Reeves:
"The core problem? Every interactive element is fighting every other element. Touch one panel, and three others crash. Wave at a sensor, and the whole light sequence locks up. I built each piece to work perfectly in isolation, but together..." They shake their head. "Together, they're at war."
Learning Style Discovery
Before we dive into the wreckage, I need to understand how you work:
-
"Do you prefer to analyze the whole system architecture first, or would you rather get your hands dirty with one broken component and work outward?"
-
"When I show you my original code, should I walk through my thinking, or do you want to form your own opinions first?"
-
"If you spot something I did wrong, just tell me straight. I can handle it—I called you in, didn't I?"
Environment Discovery
Alex Reeves:
"Before you touch anything, I need to know what you're working with. This installation has specific requirements, and if your setup doesn't match, we'll waste hours debugging the wrong problems."
Tell me about your development environment:
- What operating system are you running?
- What's your preferred code editor or IDE?
- Are you comfortable with terminal/command line work?
- Do you have Node.js installed? What version?
- Any experience with Rive's web runtime specifically?
Environment Setup
Based on your setup, let's get you connected to the installation's development environment:
For Web Development:
npm install @rive-app/canvas
git clone https://github.com/meridian-gallery/installation-2024.git
cd installation-2024
npm install
npm run dev
Alex Reeves:
"The development server connects to our sensor simulation system. You'll see the same inputs the real installation gets, but in a controlled environment where crashes don't matter."
Checkpoint
Alex Reeves:
"Let's make sure you can see what I see. Load up the main Rive file—it's called meridian-installation.riv in the assets folder. Don't try to run it yet. Just open it in the Rive editor and tell me what you notice."
When you open the file, you should see a complex hierarchy of artboards, each containing multiple state machines. The timeline view shows dozens of overlapping animations, and the state machine panel reveals a web of interconnected logic that would make a spider jealous.
Alex Reeves:
"Welcome to my nightmare. Each artboard represents a different interactive zone in the gallery. The state machines are supposed to coordinate between zones, creating those flowing light patterns Elena envisioned. Instead, they're creating chaos."
Chapter 1: The Broken Canvas
[Scene: The technical workshop buzzes with the white noise of multiple computers running diagnostic loops. Alex pulls up a chair next to the main workstation, where the Rive editor displays a maze of artboards and timelines. The complexity is overwhelming—dozens of interactive elements, each with its own animation logic, all trying to coordinate in real-time.]
You're staring at the heart of the problem. Alex's original Rive file contains twelve artboards, each representing a different section of the installation. Touch panels, motion sensors, proximity detectors—every interactive element has its own artboard with its own state machine. The artist's vision was ambitious: visitors would create collaborative light sculptures through their movements, with each person's actions influencing the entire space.
But something went wrong in the coordination. The state machines are stepping on each other, creating conflicts that crash the entire system within minutes of startup.
Alex Reeves:
"I know what you're thinking. Why didn't I just build one massive state machine to control everything? Trust me, I tried that first. But with this many interactive elements, a single state machine becomes unmanageable. So I broke it down—one artboard per interactive zone, each with its own state machine. The problem is making them talk to each other without creating feedback loops."
Think First
Before we start rebuilding, I need to understand your approach to complex interactive systems:
-
When you see a Rive file with multiple artboards, how do you typically organize the state machines? Do you prefer centralized control or distributed logic?
-
What's your experience with state machine inputs and outputs? Have you built systems where one state machine needs to trigger changes in another?
-
Looking at this installation's requirements—multiple users, real-time responses, synchronized effects—what do you think is the biggest technical challenge?
-
In your opinion, should interactive elements be completely independent, or should they share some common state?
The Task
Let's start by understanding the current system's structure. Your job is to map out the existing artboards and identify the core interaction pattern.
Step 1: Inventory the Artboards
Open the meridian-installation.riv file and examine each artboard:
const riveFile = await rive.load('assets/meridian-installation.riv');
const artboards = riveFile.artboards;
console.log('Available artboards:');
artboards.forEach((artboard, index) => {
console.log(`${index}: ${artboard.name}`);
const stateMachines = artboard.stateMachines;
stateMachines.forEach((sm, smIndex) => {
console.log(` State Machine ${smIndex}: ${sm.name}`);
});
});
Step 2: Examine a Single Artboard
Pick the artboard named "TouchPanel_01" and create a basic instance to understand its structure:
const artboard = riveFile.artboardByName('TouchPanel_01');
const stateMachine = new rive.StateMachineInstance(
artboard.stateMachineByName('PanelController'),
artboard
);
console.log('State machine inputs:');
stateMachine.inputs.forEach(input => {
console.log(`- ${input.name} (${input.type})`);
});
Step 3: Create a Minimal Test
Set up a simple HTML canvas to display just this one artboard:
<!DOCTYPE html>
<html>
<head>
<title>Touch Panel Test</title>
<script src="https://unpkg.com/@rive-app/canvas@2.7.0"></script>
</head>
<body>
<canvas id="test-canvas" width="400" height="300"></canvas>
<button id="touch-trigger">Simulate Touch</button>
<script>
const canvas = document.getElementById('test-canvas');
const button = document.getElementById('touch-trigger');
const r = new rive.Rive({
src: 'assets/meridian-installation.riv',
canvas: canvas,
artboard: 'TouchPanel_01',
stateMachines: 'PanelController',
autoplay: true,
onLoad: () => {
console.log('Single artboard loaded successfully');
const inputs = r.stateMachineInputs('PanelController');
const touchTrigger = inputs.find(i => i.name === 'touch');
if (touchTrigger) {
button.onclick = () => touchTrigger.fire();
}
}
});
</script>
</body>
</html>
Verification
Run your test page. You should see:
- A single touch panel rendered on the canvas
- The panel should respond when you click the "Simulate Touch" button
- Console output showing the artboard structure
- No error messages in the browser console
If the single artboard works correctly, you've isolated a functioning component from the broken system.
Alex Reeves:
"Good. You can see that each individual piece works fine in isolation. The touch panel responds, the animations play smoothly, the state transitions are clean. The problem only appears when you try to run multiple artboards simultaneously."
They pull up another monitor showing the full installation running—or trying to run. Within seconds, error messages flood the console.
Alex Reeves:
"Watch this. I'll start the full system now."
The installation attempts to initialize all twelve artboards at once. For a brief moment, everything seems to work—panels glow, sensors respond, light patterns begin to form. Then, cascading failures. State machines lock up. Animations freeze mid-transition. The entire system crashes.
Review
| Aspect | Assessment |
|---|
| System Analysis | Did you successfully map the artboard structure? |
| Single Component | Does the isolated touch panel work correctly? |
| Problem Identification | Can you see the difference between isolated and integrated behavior? |
| Technical Setup | Is your development environment properly configured? |
Alex Reeves:
"Now you understand what I've been dealing with. Each piece is perfect. Together, they're a disaster. The question is: how do we make them cooperate instead of compete?"
The workshop falls quiet except for the hum of computers. On the main monitor, the installation sits frozen in a half-crashed state, its potential locked behind a maze of conflicting state machines. You have 71 hours to solve what Alex couldn't fix in three months.
Chapter 2: Mapping the Territory
[Scene: Maya has claimed the corner workstation, three monitors arranged in a precise arc. The left screen shows the broken installation, the center displays a clean Rive editor with a new file, and the right runs a terminal with sensor data streaming past in green text. Alex hovers nearby, defensive but curious about Maya's methodical approach.]
You've spent the last hour studying Alex's original work, and you think you understand the core problem. The installation tries to coordinate twelve different interactive zones through a web of interconnected state machines, but there's no clear hierarchy. Every artboard thinks it's in charge.
Your approach will be different. Instead of trying to fix the existing chaos, you're going to rebuild from the ground up, starting with a single, perfectly controlled interaction.
Alex Reeves:
"You're starting over? Look, I know my code isn't pretty, but we don't have time to—"
Maya Chen:
"We don't have time to debug a system that was broken by design. I need to understand the fundamental interaction pattern before I can scale it up. One panel, one state machine, one perfect response."
Alex Reeves:
"Fine. But Elena's going to want to see progress. She's been asking about the wave effect—you know, where touching one panel triggers a cascade across adjacent panels. That's the signature interaction for the whole installation."
Think First
Before building your first state machine, let's establish the interaction design:
-
What should happen when a visitor approaches a touch panel? What about when they actually touch it? And when they move away?
-
How many distinct states do you think a single touch panel needs? Consider: idle, detecting approach, active/touched, and returning to idle.
-
What types of inputs will drive these state changes? Think about the sensors available: proximity detection, touch sensors, and timer-based transitions.
-
Should the visual feedback be immediate, or should there be smooth transitions between states?
The Task
You're going to create a new Rive file with a single, perfectly functioning touch panel. This will become the template for all other interactive elements.
Step 1: Create the Basic Artboard
Start with a new Rive file and create your first artboard:
- Create a new Rive file called
panel-prototype.riv
- Design a simple touch panel visual—a circle or rectangle that can change color/brightness
- Create three visual states:
- Idle: Dim, neutral color
- Proximity: Brighter, indicating someone is nearby
- Touched: Brightest, with a subtle animation or pulse
Step 2: Build Your First State Machine
Create a state machine called "PanelController" with these states and transitions:
States:
- Idle (entry state)
- Proximity
- Touched
Inputs:
- approach (Trigger)
- touch (Trigger)
- leave (Trigger)
Transitions:
- Idle → Proximity (on approach trigger)
- Proximity → Touched (on touch trigger)
- Touched → Proximity (automatically after 1 second)
- Proximity → Idle (on leave trigger)
Step 3: Test with Web Runtime
Create a test page to verify your state machine works correctly:
<!DOCTYPE html>
<html>
<head>
<title>Panel Prototype</title>
<script src="https://unpkg.com/@rive-app/canvas@2.7.0"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
canvas { border: 1px solid #ccc; margin: 20px 0; }
button { margin: 5px; padding: 10px 15px; }
.controls { margin: 20px 0; }
</style>
</head>
<body>
<h1>Touch Panel Prototype</h1>
<canvas id="panel-canvas" width="400" height="300"></canvas>
<div class="controls">
<button id="approach-btn">Approach</button>
<button id="touch-btn">Touch</button>
<button id="leave-btn">Leave</button>
</div>
<div id="status">Status: Loading...</div>
<script>
const canvas = document.getElementById('panel-canvas');
const statusDiv = document.getElementById('status');
const r = new rive.Rive({
src: 'panel-prototype.riv',
canvas: canvas,
stateMachines: 'PanelController',
autoplay: true,
onLoad: () => {
statusDiv.textContent = 'Status: Ready';
const inputs = r.stateMachineInputs('PanelController');
const approachTrigger = inputs.find(i => i.name === 'approach');
const touchTrigger = inputs.find(i => i.name === 'touch');
const leaveTrigger = inputs.find(i => i.name === 'leave');
document.getElementById('approach-btn').onclick = () => {
if (approachTrigger) {
approachTrigger.fire();
statusDiv.textContent = 'Status: Visitor approaching';
}
};
document.getElementById('touch-btn').onclick = () => {
if (touchTrigger) {
touchTrigger.fire();
statusDiv.textContent = 'Status: Panel touched!';
}
};
document.getElementById('leave-btn').onclick = () => {
if (leaveTrigger) {
leaveTrigger.fire();
statusDiv.textContent = 'Status: Visitor left';
}
};
},
onLoadError: (error) => {
statusDiv.textContent = 'Error: ' + error;
}
});
</script>
</body>
</html>
Step 4: Add Automatic Transitions
Enhance your state machine with timer-based transitions:
- Add a 1-second timer transition from "Touched" back to "Proximity"
- Add a 5-second timer transition from "Proximity" to "Idle" (in case someone approaches but doesn't touch)
Verification
Your prototype should demonstrate:
- Idle → Proximity: Panel brightens when "Approach" is triggered
- Proximity → Touched: Panel reaches maximum brightness when "Touch" is triggered
- Touched → Proximity: Panel automatically returns to proximity state after 1 second
- Proximity → Idle: Panel returns to idle if no interaction for 5 seconds
- Smooth transitions: All state changes should be visually smooth, not jarring
Test the complete interaction flow multiple times to ensure it's reliable.
Alex Reeves:
"Okay, I'll admit it. That's cleaner than anything I built. The state transitions are smooth, the timing feels natural, and it doesn't crash when you trigger it rapidly."
They lean over your shoulder, studying the state machine diagram in the Rive editor.
Alex Reeves:
"But here's where it gets complicated. Elena doesn't want twelve independent panels. She wants them to communicate. When someone touches Panel 1, Panel 2 should light up slightly. Touch Panel 2, and Panel 3 responds. The whole installation should feel like one organism, not twelve separate widgets."
Maya looks up from her screen, a slight smile playing at the corners of her mouth.
Maya Chen:
"That's exactly what I was hoping you'd say. This single panel isn't the solution—it's the foundation. Now we need to teach panels how to listen to each other."
Review
| Aspect | Assessment |
|---|
| State Design | Does your state machine handle all interaction phases cleanly? |
| Visual Feedback | Are the transitions smooth and the states visually distinct? |
| Input Handling | Do all triggers fire correctly and at the right times? |
| Timing | Are the automatic transitions appropriately timed? |
Alex Reeves:
"The artist is coming by in an hour to check on progress. She's going to want to see the wave effect—that cascading response across multiple panels. Think your approach can handle that level of coordination?"
The challenge is clear. You have a perfect single panel, but the installation needs twelve panels working in harmony. The next step: teaching state machines to communicate across artboard boundaries.
Chapter 3: Synchronized Signals
[Scene: The workshop has transformed into Maya's command center. She's arranged three touch panels in a row on the test bench, each connected to its own sensor array. The monitors show three separate Rive instances running simultaneously, their state machines waiting to demonstrate the coordinated behavior that has eluded the installation for months.]
Elena Vasquez, the artist, arrived an hour ago and hasn't said much. She's been watching Maya work with the intensity of someone whose vision hangs in the balance. The wave effect—her signature interaction—requires perfect timing between adjacent panels.
Elena Vasquez:
"In my mind, I see it like water. Touch creates ripples. Each panel responds to its neighbor, creating a flow of light across the entire installation. Not mechanical. Organic."
Alex Reeves:
"That's the problem right there. 'Organic' doesn't translate well to state machines. I tried using shared variables, global timers, even external coordination scripts. Everything either created lag or crashed under load."
Maya pulls up her new approach: three identical artboards, each with the same PanelController state machine from yesterday, but now enhanced with input and output capabilities.
Maya Chen:
"The key is making each panel both a listener and a broadcaster. When Panel 1 gets touched, it doesn't just change its own state—it sends a signal to Panel 2. Panel 2 receives that signal and transitions to its own 'neighbor activated' state."
Think First
Before implementing panel-to-panel communication, let's think through the coordination challenge:
-
When Panel 1 is touched, what should happen to Panel 2? Should it fully activate, partially activate, or just show a subtle response?
-
How should the timing work? Should Panel 2 respond immediately, or should there be a slight delay to create the "wave" effect?
-
What happens if someone touches Panel 2 while it's still responding to Panel 1's signal? Should the local interaction override the remote signal?
-
How do you prevent feedback loops—Panel 1 signals Panel 2, Panel 2 signals Panel 3, Panel 3 signals Panel 1?
The Task
You're going to create a three-panel system where each panel can influence its neighbors through state machine inputs and listeners.
Step 1: Enhance the Panel State Machine
Modify your PanelController state machine to include neighbor communication:
New States:
- Idle (entry state)
- Proximity
- Touched
- NeighborActive (new - responds to adjacent panel activation)
New Inputs:
- approach (Trigger)
- touch (Trigger)
- leave (Trigger)
- neighborSignal (Trigger) - new input for receiving signals from adjacent panels
New Outputs:
- sendSignal (Boolean) - broadcasts when this panel is touched
Step 2: Create the Multi-Panel Test Environment
Build a test page that manages three separate Rive instances:
<!DOCTYPE html>
<html>
<head>
<title>Three Panel Wave Test</title>
<script src="https://unpkg.com/@rive-app/canvas@2.7.0"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.panel-container { display: flex; gap: 20px; margin: 20px 0; }
canvas { border: 1px solid #ccc; }
.controls { margin: 10px 0; }
button { margin: 2px; padding: 5px 10px; font-size: 12px; }
.panel-group { text-align: center; }
</style>
</head>
<body>
<h1>Wave Effect Test</h1>
<div class="panel-container">
<div class="panel-group">
<h3>Panel 1</h3>
<canvas id="panel1" width="200" height="150"></canvas>
<div class="controls">
<button onclick="triggerPanel(0, 'approach')">Approach</button>
<button onclick="triggerPanel(0, 'touch')">Touch</button>
<button onclick="triggerPanel(0, 'leave')">Leave</button>
</div>
</div>
<div class="panel-group">
<h3>Panel 2</h3>
<canvas id="panel2" width="200" height="150"></canvas>
<div class="controls">
<button onclick="triggerPanel(1, 'approach')">Approach</button>
<button onclick="triggerPanel(1, 'touch')">Touch</button>
<button onclick="triggerPanel(1, 'leave')">Leave</button>
</div>
</div>
<div class="panel-group">
<h3>Panel 3</h3>
<canvas id="panel3" width="200" height="150"></canvas>
<div class="controls">
<button onclick="triggerPanel(2, 'approach')">Approach</button>
<button onclick="triggerPanel(2, 'touch')">Touch</button>
<button onclick="triggerPanel(2, 'leave')">Leave</button>
</div>
</div>
</div>
<div id="log"></div>
<script>
const panels = [];
const canvasIds = ['panel1', 'panel2', 'panel3'];
canvasIds.forEach((canvasId, index) => {
const canvas = document.getElementById(canvasId);
const riveInstance = new rive.Rive({
src: 'panel-prototype.riv',
canvas: canvas,
stateMachines: 'PanelController',
autoplay: true,
onLoad: () => {
console.log(`Panel ${index + 1} loaded`);
const inputs = riveInstance.stateMachineInputs('PanelController');
panels[index] = {
rive: riveInstance,
inputs: {
approach: inputs.find(i => i.name === 'approach'),
touch: inputs.find(i => i.name === 'touch'),
leave: inputs.find(i => i.name === 'leave'),
neighborSignal: inputs.find(i => i.name === 'neighborSignal')
}
};
}
});
});
function triggerPanel(panelIndex, inputName) {
if (!panels[panelIndex]) return;
const panel = panels[panelIndex];
const input = panel.inputs[inputName];
if (input) {
input.fire();
log(`Panel ${panelIndex + 1}: ${inputName} triggered`);
if (inputName === 'touch') {
signalNeighbors(panelIndex);
}
}
}
function signalNeighbors(panelIndex) {
if (panelIndex > 0) {
setTimeout(() => {
if (panels[panelIndex - 1]?.inputs.neighborSignal) {
panels[panelIndex - 1].inputs.neighborSignal.fire();
log(`Panel ${panelIndex}: signaled left neighbor`);
}
}, 100);
}
if (panelIndex < panels.length - 1) {
setTimeout(() => {
if (panels[panelIndex + 1]?.inputs.neighborSignal) {
panels[panelIndex + 1].inputs.neighborSignal.fire();
log(`Panel ${panelIndex}: signaled right neighbor`);
}
}, 100);
}
}
function log(message) {
const logDiv = document.getElementById('log');
logDiv.innerHTML = `<div>${new Date().toLocaleTimeString()}: ${message}</div>` + logDiv.innerHTML;
const entries = logDiv.children;
while (entries.length > 10) {
logDiv.removeChild(entries[entries.length - 1]);
}
}
</script>
</body>
</html>
Step 3: Implement State Machine Listeners
In your Rive file, set up the state transitions for neighbor communication:
- NeighborActive State: Create a new state that's visually distinct from Idle but less intense than Touched
- Neighbor Signal Transition: Add a transition from Idle to NeighborActive triggered by the neighborSignal input
- Return Transition: Add a timer-based transition from NeighborActive back to Idle after 2 seconds
- Priority Handling: Ensure that local interactions (approach, touch) override neighbor signals
Verification
Test the complete wave effect:
- Touch Panel 1: Panel 1 should go to Touched state, Panel 2 should go to NeighborActive state after 100ms
- Touch Panel 2: Panel 2 should go to Touched state, Panel 1 and Panel 3 should go to NeighborActive state
- Touch Panel 3: Panel 3 should go to Touched state, Panel 2 should go to NeighborActive state
- Rapid Touches: Touch panels quickly in sequence—the wave should flow smoothly without conflicts
- Local Override: While Panel 2 is in NeighborActive state from Panel 1's signal, touch Panel 2 directly—it should immediately go to Touched state
Elena Vasquez:
"Yes! That's it exactly. Watch how the light flows from one panel to the next. It's not mechanical—there's a rhythm to it, a natural progression."
She touches the panels in sequence, creating waves of light that ripple across the test setup. For the first time since arriving, she's smiling.
Alex Reeves:
"I have to admit, that's elegant. Each panel maintains its own state but listens for signals from its neighbors. No central coordinator, no shared variables that can get corrupted. But Maya..." They pause, looking concerned. "This is just three panels. The real installation has twelve panels, plus motion sensors, plus proximity detectors. What happens when you scale this up?"
Maya saves her work and turns to face both of them.
Maya Chen:
"That's exactly what we're about to find out. Elena, I need you to help me understand the full interaction design. It's not just about panels talking to panels, is it? The motion sensors need to influence the panels too. And the overhead proximity detectors—they're supposed to create those ambient light patterns when people move through the space."
Elena Vasquez:
"The installation should feel alive. When someone enters the room, the whole space should subtly acknowledge their presence. When they approach a panel, the nearby elements should anticipate their interaction. And when they touch..." She gestures at the wave effect still playing across the test panels. "When they touch, the entire installation should respond."
Review
| Aspect | Assessment |
|---|
| Wave Effect | Do panels signal their neighbors correctly with appropriate timing? |
| State Coordination | Can panels handle both local interactions and neighbor signals? |
| Visual Flow | Does the wave effect feel natural and organic? |
| Conflict Resolution | Do local interactions properly override neighbor signals? |
Alex Reeves:
"The artist loves it, but I'm worried about what happens tomorrow when we try to integrate this with the real sensor hardware. We've got proximity sensors, motion detectors, touch surfaces, and overhead tracking—all feeding data into the system simultaneously. Your elegant three-panel setup is about to meet the chaos of the real world."
The test panels continue their gentle wave pattern, but Maya can see the challenge ahead. Three coordinated panels are just the beginning. The real installation needs to handle dozens of simultaneous inputs while maintaining the organic flow Elena envisions.
Chapter 4: The Cascade Failure
[Scene: The installation space is alive with activity for the first time in weeks. All twelve panels are mounted and connected, motion sensors sweep the room with invisible beams, and overhead cameras track movement patterns. Maya's coordination system worked perfectly in testing, but now, with the full sensor array active and Marcus Torres running stress tests with multiple simulated visitors, everything is falling apart.]
It's 2 AM, eighteen hours before the exhibition opening. What started as a triumphant full-system test has devolved into a debugging nightmare. The wave effects work beautifully when triggered individually, but when multiple visitors interact simultaneously, the state machines lock up in impossible configurations.
Marcus Torres:
"Look at this. Visitor simulation A approaches Panel 3 while simulation B is still interacting with Panel 5. Panel 3 goes to proximity state, but then it receives a neighbor signal from Panel 4, which received a signal from Panel 5. Now Panel 3 is trying to be in proximity state AND neighbor-active state simultaneously."
Alex Reeves:
"This is exactly what I was afraid of. Your elegant solution works great in isolation, but real-world usage creates edge cases you never anticipated. Multiple users, overlapping interactions, sensor noise—it's chaos."
Maya stares at the state machine debugger, watching dozens of conflicting signals cascade through the system. Each panel's state machine was designed to handle one interaction at a time, but the real installation generates dozens of simultaneous inputs.
Maya Chen:
"The problem isn't the coordination—it's the assumption that each panel can only be in one state at a time. But that's not how the real world works. A panel can be responding to a neighbor signal while simultaneously detecting an approaching visitor."
Think First
The current crisis reveals a fundamental flaw in the state machine design. Consider these questions:
-
How can a single panel handle multiple simultaneous conditions? For example, what if someone approaches Panel 3 while it's still responding to a neighbor signal from Panel 2?
-
Should different types of inputs have different priorities? Should a direct touch always override a neighbor signal, or should they combine somehow?
-
How do you prevent state machine deadlocks when multiple panels are signaling each other in complex patterns?
-
What's the difference between a panel's "interaction state" (idle, proximity, touched) and its "influence state" (responding to neighbors, environmental sensors)?
The Task
You need to redesign the panel state machines using nested state machines and boolean logic to handle multiple simultaneous conditions.
Step 1: Design Hierarchical State Machines
Create a new state machine architecture with two levels:
Main State Machine: "PanelController"
├── Nested State Machine: "InteractionStates"
│ ├── Idle
│ ├── Proximity
│ └── Touched
└── Nested State Machine: "InfluenceStates"
├── None
├── NeighborActive
└── EnvironmentalActive
Boolean Inputs:
- hasVisitor (Boolean) - true when someone is in proximity
- isTouched (Boolean) - true when panel is being touched
- neighborActive (Boolean) - true when receiving neighbor signals
- environmentalTrigger (Boolean) - true when motion sensors detect activity
Step 2: Implement Nested State Machines
Create a new Rive file called hierarchical-panel.riv with nested state machine logic:
const panelLogic = {
mainController: {
interactionStates: {
currentState: 'Idle',
transitions: {
'Idle': {
'hasVisitor becomes true': 'Proximity',
'isTouched becomes true': 'Touched'
},
'Proximity': {
'hasVisitor becomes false': 'Idle',
'isTouched becomes true': 'Touched'
},
'Touched': {
'isTouched becomes false': 'Proximity',
'hasVisitor becomes false': 'Idle'
}
}
},
influenceStates: {
currentState: 'None',
transitions: {
'None': {
'neighborActive becomes true': 'NeighborActive',
'environmentalTrigger becomes true': 'EnvironmentalActive'
},
'NeighborActive': {
'neighborActive becomes false': 'None'
},
'EnvironmentalActive': {
'environmentalTrigger becomes false': 'None'
}
}
}
}
};
Step 3: Create Multi-Condition Test Environment
Build a comprehensive test that simulates complex real-world scenarios:
<!DOCTYPE html>
<html>
<head>
<title>Hierarchical State Machine Test</title>
<script src="https://unpkg.com/@rive-app/canvas@2.7.0"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.test-container { display: flex; gap: 30px; }
.panel-section { flex: 1; }
canvas { border: 2px solid #333; margin: 10px 0; }
.controls { margin: 10px 0; }
.control-group { margin: 15px 0; padding: 10px; border: 1px solid #ccc; }
.control-group h4 { margin: 0 0 10px 0; }
button { margin: 2px; padding: 8px 12px; }
.active { background-color: #4CAF50; color: white; }
.status { font-family: monospace; font-size: 12px; margin: 10px 0; }
</style>
</head>
<body>
<h1>Complex Interaction Test</h1>
<div class="test-container">
<div class="panel-section">
<h3>Panel State</h3>
<canvas id="panel-canvas" width="300" height="200"></canvas>
<div class="control-group">
<h4>Direct Interaction</h4>
<button id="visitor-toggle">Toggle Visitor Presence</button>
<button id="touch-toggle">Toggle Touch</button>
<div class="status">
Visitor: <span id="visitor-status">false</span><br>
Touch: <span id="touch-status">false</span>
</div>
</div>
<div class="control-group">
<h4>External Influences</h4>
<button id="neighbor-toggle">Toggle Neighbor Signal</button>
<button id="environmental-toggle">Toggle Environmental</button>
<div class="status">
Neighbor: <span id="neighbor-status">false</span><br>
Environmental: <span id="environmental-status">false</span>
</div>
</div>
<div class="control-group">
<h4>Stress Test</h4>
<button id="random-test">Random Input Test</button>
<button id="stop-test">Stop Test</button>
</div>
</div>
<div class="panel-section">
<h3>State Machine Debug</h3>
<div id="state-debug" class="status"></div>
<div id="event-log" class="status"></div>
</div>
</div>
<script>
let panelRive;
let inputs = {};
let testInterval;
let currentStates = {
hasVisitor: false,
isTouched: false,
neighborActive: false,
environmentalTrigger: false
};
const canvas = document.getElementById('panel-canvas');
panelRive = new rive.Rive({
src: 'hierarchical-panel.riv',
canvas: canvas,
stateMachines: 'PanelController',
autoplay: true,
onLoad: () => {
const riveInputs = panelRive.stateMachineInputs('PanelController');
inputs.hasVisitor = riveInputs.find(i => i.name === 'hasVisitor');
inputs.isTouched = riveInputs.find(i => i.name === 'isTouched');
inputs.neighborActive = riveInputs.find(i => i.name === 'neighborActive');
inputs.environmentalTrigger = riveInputs.find(i => i.name === 'environmentalTrigger');
setupControls();
updateDisplay();
log('Panel loaded and ready');
}
});
function setupControls() {
document.getElementById('visitor-toggle').onclick = () => toggleState('hasVisitor');
document.getElementById('touch-toggle').onclick = () => toggleState('isTouched');
document.getElementById('neighbor-toggle').onclick = () => toggleState('neighborActive');
document.getElementById('environmental-toggle').onclick = () => toggleState('environmentalTrigger');
document.getElementById('random-test').onclick = startRandomTest;
document.getElementById('stop-test').onclick = stopRandomTest;
}
function toggleState(stateName) {
currentStates[stateName] = !currentStates[stateName];
if (inputs[stateName]) {
inputs[stateName].value = currentStates[stateName];
}
updateDisplay();
log(`${stateName}: ${currentStates[stateName]}`);
}
function updateDisplay() {
Object.keys(currentStates).forEach(key => {
const element = document.getElementById(key.replace(/([A-Z])/g, '-$1').toLowerCase() + '-status');
if (element) {
element.textContent = currentStates[key];
element.style.color = currentStates[key] ? '#4CAF50' : '#333';
}
});
const debugDiv = document.getElementById('state-debug');
debugDiv.innerHTML = `
<strong>Current State Combination:</strong><br>
Interaction: ${getInteractionState()}<br>
Influence: ${getInfluenceState()}<br>
Visual Result: ${getVisualState()}
`;
}
function getInteractionState() {
if (currentStates.isTouched) return 'Touched';
if (currentStates.hasVisitor) return 'Proximity';
return 'Idle';
}
function getInfluenceState() {
if (currentStates.neighborActive) return 'NeighborActive';
if (currentStates.environmentalTrigger) return 'EnvironmentalActive';
return 'None';
}
function getVisualState() {
const interaction = getInteractionState();
const influence = getInfluenceState();
if (interaction === 'Touched') return 'Bright + Touch Effect';
if (interaction === 'Proximity') return 'Medium + Proximity Glow';
if (influence === 'NeighborActive') return 'Dim + Neighbor Pulse';
if (influence === 'EnvironmentalActive') return 'Dim + Environmental Shimmer';
return 'Idle Dim';
}
function startRandomTest() {
if (testInterval) return;
log('Starting random input test...');
testInterval = setInterval(() => {
const states = Object.keys(currentStates);
const randomState = states[Math.floor(Math.random() * states.length)];
toggleState(randomState);
}, 500);
}
function stopRandomTest() {
if (testInterval) {
clearInterval(testInterval);
testInterval = null;
log('Random test stopped');
}
}
function log(message) {
const logDiv = document.getElementById('event-log');
const timestamp = new Date().toLocaleTimeString();
logDiv.innerHTML = `<div>${timestamp}: ${message}</div>` + logDiv.innerHTML;
const entries = logDiv.children;
while (entries.length > 15) {
logDiv.removeChild(entries[entries.length - 1]);
}
}
</script>
</body>
</html>
Step 4: Implement Visual State Combination
In your Rive file, create animations that combine the effects of both nested state machines:
- Base Layer: Controlled by InteractionStates (idle, proximity, touched)
- Effect Layer: Controlled by InfluenceStates (neighbor pulse, environmental shimmer)
- Combination Logic: Use boolean inputs to blend the layers appropriately
Verification
Test these complex scenarios:
-
Simultaneous States: Set hasVisitor=true and neighborActive=true. The panel should show proximity glow with neighbor pulse overlay.
-
Priority Override: While neighborActive=true, set isTouched=true. Touch should take visual priority but neighbor effect should still be visible.
-
Rapid Changes: Use the random test to rapidly change multiple inputs. The state machine should handle all transitions smoothly without conflicts.
-
State Combinations: Test all possible combinations of the four boolean inputs. Each should produce a distinct visual result.
-
Clean Transitions: When inputs change, transitions should be smooth, not jarring or glitchy.
Marcus Torres:
"This is incredible. Look—I can simulate three people approaching different panels while the environmental sensors detect movement, and nothing crashes. Each panel maintains its own interaction state while still responding to the broader context."
He runs the stress test, rapidly toggling inputs to simulate chaotic real-world conditions. The hierarchical state machines handle every combination gracefully.
Alex Reeves:
"I have to hand it to you, Maya. I was thinking in terms of single-state machines trying to coordinate with each other. But you've created machines that can be in multiple states simultaneously. It's like... like each panel has both a personal state and a social state."
Maya Chen:
"Exactly. A person can be individually focused on a task while still being aware of their social environment. These panels work the same way—they can be touched by one person while still responding to the broader activity in the room."
Elena has been quiet, watching the test panels respond to increasingly complex input patterns. Finally, she speaks.
Elena Vasquez:
"This is what I envisioned. Not just panels that react, but panels that are aware. They respond to direct interaction, but they're also part of a larger conversation happening in the space."
Review
| Aspect | Assessment |
|---|
| Nested Architecture | Do the hierarchical state machines handle multiple simultaneous conditions? |
| State Combinations | Can panels display appropriate visual combinations of interaction and influence states? |
| Conflict Resolution | Are there any input combinations that cause crashes or undefined behavior? |
| Performance | Does the system maintain smooth performance under rapid input changes? |
Maya Chen:
"We've solved the technical architecture, but we still have the hardest challenge ahead. Tomorrow night, this installation needs to handle not just multiple inputs, but multiple people. Real visitors, moving unpredictably, creating interaction patterns we can't anticipate. The state machines are robust, but are they robust enough for opening night?"
The workshop falls quiet except for the gentle hum of the test panels, their nested state machines cycling through complex combinations of interaction and influence. In eighteen hours, those same state machines will face their ultimate test: a room full of art enthusiasts, critics, and curious visitors, all eager to experience Elena's vision of collaborative light sculpture.
Chapter 5: Under Pressure
[Scene: The installation space buzzes with pre-opening activity. Gallery staff arrange seating, caterers set up stations, and Maya makes final adjustments to the state machine hierarchy. With six hours until the doors open, she faces the installation's most complex challenge: the centerpiece interaction that must adapt its behavior based on how many people are present in the space.]
The individual panels work flawlessly. The wave effects flow like water. The environmental sensors create subtle ambient responses. But Elena's vision includes one more layer of complexity: the overhead light sculpture that responds not just to individual interactions, but to the collective behavior of everyone in the space.
Elena Vasquez:
"When one person is alone with the installation, it should feel intimate—personal responses, gentle interactions. But when the room fills with people, the installation should become celebratory. The lights should dance together, creating patterns that no single person could generate alone."
Maya Chen:
"So the same touch gesture should trigger different responses depending on the social context. A touch when you're alone creates a small, personal light pattern. The same touch in a crowded room contributes to a larger, more dynamic display."
Alex Reeves:
"That's the piece I never figured out. How do you make a state machine that changes its own behavior based on external conditions? I tried global variables, but they created race conditions. I tried centralized coordination, but it was too fragile."
Maya pulls up the overhead sensor data—motion detectors and computer vision systems that track the number and positions of people in the space. The challenge is creating state machines that don't just respond to inputs, but actually modify their response patterns based on crowd density.
Maya Chen:
"The answer is hierarchical state machines with runtime behavior modification. Each panel doesn't just have interaction states and influence states—it has context states that change how the other states behave."
Think First
This final challenge requires state machines that adapt their behavior dynamically. Consider:
-
How should a touch panel behave differently when one person is present versus when ten people are present?
-
What information does each panel need about the overall room state? Just the number of people, or their positions and movement patterns too?
-
How do you prevent the system from constantly switching between "intimate" and "social" modes as people enter and leave?
-
Should the adaptation be gradual (smooth transitions between modes) or discrete (clear switching points)?
The Task
Create adaptive state machines that modify their behavior based on real-time crowd analysis.
Step 1: Design Context-Aware State Architecture
Extend your hierarchical state machines with a new layer:
Main State Machine: "AdaptivePanelController"
├── Context State Machine: "CrowdContext"
│ ├── Intimate (1-2 people)
│ ├── Social (3-6 people)
│ └── Celebratory (7+ people)
├── Interaction State Machine: "UserInteraction"
│ ├── Idle
│ ├── Proximity
│ └── Touched
└── Influence State Machine: "ExternalInfluence"
├── None
├── NeighborActive
└── EnvironmentalActive
Number Inputs:
- crowdSize (Number) - current number of people detected
- proximityCount (Number) - people near this specific panel
- activityLevel (Number) - overall room activity (0-100)
Boolean Inputs:
- hasVisitor (Boolean)
- isTouched (Boolean)
- neighborActive (Boolean)
- environmentalTrigger (Boolean)
Step 2: Implement Crowd-Responsive Behavior
Create state machines where the same input triggers different responses based on context:
const adaptiveBehavior = {
touchResponse: {
intimate: {
visualIntensity: 0.6,
duration: 3000,
neighborSignalStrength: 0.3,
pattern: 'gentle-pulse'
},
social: {
visualIntensity: 0.8,
duration: 2000,
neighborSignalStrength: 0.6,
pattern: 'wave-cascade'
},
celebratory: {
visualIntensity: 1.0,
duration: 4000,
neighborSignalStrength: 0.9,
pattern: 'burst-celebration'
}
}
};
Step 3: Build Crowd Simulation Test Environment
Create a comprehensive test that simulates different crowd scenarios:
<!DOCTYPE html>
<html>
<head>
<title>Adaptive Behavior Test</title>
<script src="https://unpkg.com/@rive-app/canvas@2.7.0"></script>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.main-container { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
.panel-section { }
.control-section { }
canvas { border: 2px solid #333; margin: 10px 0; width: 100%; max-width: 400px; }
.crowd-controls { margin: 20px 0; padding: 15px; border: 2px solid #4CAF50; }
.interaction-controls { margin: 20px 0; padding: 15px; border: 2px solid #2196F3; }
.scenario-controls { margin: 20px 0; padding: 15px; border: 2px solid #FF9800; }
.slider-group { margin: 10px 0; }
.slider-group label { display: block; margin-bottom: 5px; }
.slider-group input[type="range"] { width: 100%; }
.status-display { font-family: monospace; font-size: 12px; background: #f5f5f5; padding: 10px; margin: 10px 0; }
button { margin: 5px; padding: 10px 15px; }
.context-indicator { font-size: 18px; font-weight: bold; padding: 10px; margin: 10px 0; text-align: center; }
.intimate { background-color: #E8F5E8; color: #2E7D32; }
.social { background-color: #E3F2FD; color: #1565C0; }
.celebratory { background-color: #FFF3E0; color: #EF6C00; }
</style>
</head>
<body>
<h1>Adaptive Installation Test</h1>
<div class="main-container">
<div class="panel-section">
<h3>Installation Panel</h3>
<canvas id="adaptive-panel" width="400" height="300"></canvas>
<div id="context-display" class="context-indicator intimate">
INTIMATE MODE (1-2 people)
</div>
<div class="status-display" id="behavior-status">
Touch Response: Gentle, Personal<br>
Wave Strength: Low<br>
Visual Intensity: 60%
</div>
</div>
<div class="control-section">
<div class="crowd-controls">
<h4>Crowd Simulation</h4>
<div class="slider-group">
<label for="crowd-size">Number of People: <span id="crowd-size-value">1</span></label>
<input type="range" id="crowd-size" min="0" max="15" value="1">
</div>
<div class="slider-group">
<label for="activity-level">Activity Level: <span id="activity-level-value">20</span>%</label>
<input type="range" id="activity-level" min="0" max="100" value="20">
</div>
<div class="slider-group">
<label for="proximity-count">People Near Panel: <span id="proximity-count-value">0</span></label>
<input type="range" id="proximity-count" min="0" max="5" value="0">
</div>
</div>
<div class="interaction-controls">
<h4>Direct Interaction</h4>
<button id="approach-btn">Approach Panel</button>
<button id="touch-btn">Touch Panel</button>
<button id="leave-btn">Leave Panel</button>
</div>
<div class="scenario-controls">
<h4>Test Scenarios</h4>
<button onclick="setScenario('intimate')">Intimate (1 person)</button>
<button onclick="setScenario('social')">Social (4 people)</button>
<button onclick="setScenario('celebratory')">Celebratory (10 people)</button>
<button onclick="runAdaptationTest()">Adaptation Test</button>
</div>
<div class="status-display" id="debug-log"></div>
</div>
</div>
<script>
let panelRive;
let inputs = {};
// Current state
let currentContext = 'intimate';
let crowdSize = 1;
let activityLevel = 20;
let proximityCount = 0;
// Initialize Rive
const canvas = document.getElementById('adaptive-panel');
panelRive = new rive.Rive({
src: 'adaptive-panel.riv', // Your new adaptive file
canvas: canvas,
stateMachines: 'AdaptivePanelController',
autoplay: true,
onLoad: () => {
const riveInputs = panelRive.stateMachineInputs('AdaptivePanelController');
// Number inputs for crowd context
inputs.crowdSize = riveInputs.find(i => i.name === 'crowdSize');
inputs.proximityCount = riveInputs.find(i => i.name === 'proximityCount');
inputs.activityLevel = riveInputs.find(i => i.name === 'activityLevel');
// Boolean inputs for interaction
inputs.hasVisitor = riveInputs.find(i => i.name === 'hasVisitor');
inputs.isTouched = riveInputs.find(i => i.name === 'isTouched');
inputs.neighborActive = riveInputs.find(i => i.name === 'neighborActive');
setupControls();
updateContext();
log('Adaptive panel loaded');
}
});
function setupControls() {
// Crowd size slider
const crowdSlider = document.getElementById('crowd-size');
crowdSlider.oninput = (e) => {
crowdSize = parseInt(e.target.value);
document.getElementById('crowd-size-value').textContent = crowdSize;
updateContext();
};
// Activity level slider
const activitySlider = document.getElementById('activity-level');
activitySlider.oninput = (e) => {
activityLevel = parseInt(e.target.value);
document.getElementById('activity-level-value').textContent = activityLevel;
updateContext();
};
// Proximity count slider
const proximitySlider = document.getElementById('proximity-count');
proximitySlider.oninput = (e) => {
proximityCount = parseInt(e.target.value);
document.getElementById('proximity-count-value').textContent = proximityCount;
updateContext();
};
// Interaction buttons
document.getElementById('approach-btn').onclick = () => {
if (inputs.hasVisitor) {
inputs.hasVisitor.value = true;
log(`Approach in ${currentContext} mode`);
}
};
document.getElementById('touch-btn').onclick = () => {
if (inputs.isTouched) {
inputs.isTouched.value = true;
setTimeout(() => {
inputs.isTouched.value = false;
}, getBehaviorSetting('duration'));
log(`Touch in ${currentContext} mode - ${getBehaviorDescription()}`);
}
};
document.getElementById('leave-btn').onclick = () => {
if (inputs.hasVisitor) {
inputs.hasVisitor.value = false;
log('Visitor left');
}
};
}
function updateContext() {
// Update Rive inputs
if (inputs.crowdSize) inputs.crowdSize.value = crowdSize;
if (inputs.activityLevel) inputs.activityLevel.value = activityLevel;
if (inputs.proximityCount) inputs.proximityCount.value = proximityCount;
// Determine context
const newContext = getContextFromCrowdSize(crowdSize);
if (newContext !== currentContext) {
currentContext = newContext;
log(`Context changed to: ${currentContext}`);
}
// Update UI
updateContextDisplay();
updateBehaviorStatus();
}
function getContextFromCrowdSize(size) {
if (size <= 2) return 'intimate';
if (size <= 6) return 'social';
return 'celebratory';
}
function updateContextDisplay() {
const display = document.getElementById('context-display');
display.className = `context-indicator ${currentContext}`;
const contextNames = {
intimate: 'INTIMATE MODE (1-2 people)',
social: 'SOCIAL MODE (3-6 people)',
celebratory: 'CELEBRATORY MODE (7+ people)'
};
display.textContent = contextNames[currentContext];
}
function updateBehaviorStatus() {
const status = document.getElementById('behavior-status');
const behavior = getBehaviorSettings(currentContext);
status.innerHTML = `
Touch Response: ${behavior.description}<br>
Wave Strength: ${Math.round(behavior.neighborSignalStrength * 100)}%<br>
Visual Intensity: ${Math.round(behavior.visualIntensity * 100)}%<br>
Duration: ${behavior.duration}ms
`;
}
function getBehaviorSettings(context) {
const behaviors = {
intimate: {
description: 'Gentle, Personal',
visualIntensity: 0.6,
duration: 3000,
neighborSignalStrength: 0.3
},
social: {
description: 'Coordinated, Flowing',
visualIntensity: 0.8,
duration: 2000,
neighborSignalStrength: 0.6
},
celebratory: {
description: 'Bright, Energetic',
visualIntensity: 1.0,
duration: 4000,
neighborSignalStrength: 0.9
}
};
return behaviors[context];
}
function getBehaviorSetting(setting) {
return getBehaviorSettings(currentContext)[setting];
}
function getBehaviorDescription() {
return getBehaviorSettings(currentContext).description;
}
function setScenario(scenario) {
const scenarios = {
intimate: { crowd: 1, activity: 20, proximity: 0 },
social: { crowd: 4, activity: 60, proximity: 1 },
celebratory: { crowd: 10, activity: 90, proximity: 2 }
};
const config = scenarios[scenario];
document.getElementById('crowd-size').value = config.crowd;
document.getElementById('activity-level').value = config.activity;
document.getElementById('proximity-count').value = config.proximity;
crowdSize = config.crowd;
activityLevel = config.activity;
proximityCount = config.proximity;
document.getElementById('crowd-size-value').textContent = crowdSize;
document.getElementById('activity-level-value').textContent = activityLevel;
document.getElementById('proximity-count-value