feat(mascot): replace SVG animations with Rive renderer#2659
Conversation
Switch from the custom Remotion/SVG frame-based mascot to a Rive file (`tiny_mascot.riv`) rendered via `@rive-app/react-webgl2`. The Rive state machine drives body, eye, and mouth animations natively; the `mouthOpen` ViewModel boolean is toggled from the `face` prop to animate speaking states. - Add RiveMascot component using Rive ViewModel data binding - Remove YellowMascot, yellow/ SVG compositions, and FrameProvider - Replace YellowMascot in HumanPage, MascotWindowApp, iOS MascotScreen, and MascotFrameProducer (now captures Rive canvas instead of SVG) - Temporarily disable SubMascotLayer rendering in HumanPage - Update test mocks to match new component names
- Switch from @rive-app/react-canvas to @rive-app/react-webgl2 for full feather/gradient support - Add ViewModel properties: primaryColor, secondaryColor (data-bound to mascot fills), viseme (string: REST/A/E/I/O/U/M/F), pose (string), isHovering (boolean for hover_hitbox interaction) - Wire primaryColor to body/head/hands fills, secondaryColor to shadow fills via Rive data binding - Add hands layer state machine transitions: idle↔party driven by isHovering boolean with 500ms cubic blends - Map MascotFace to pose strings (idle, thinking, sleeping) - Use Fit.Contain layout for proper artboard aspect ratio
Render a RiveMascot preview at the top of MascotPanel that updates in real-time when the user picks a color swatch. Maps each palette's bodyFill → primaryColor and neckShadowColor → secondaryColor as ARGB integers passed through ViewModel data binding.
Read mascotColor from Redux in HumanPage and pass the palette's primaryColor/secondaryColor as ARGB integers to RiveMascot. Extract hexToArgbInt into mascotPalette.ts so both HumanPage and MascotPanel share the same converter. Selecting a swatch in settings now updates the mascot everywhere instantly.
Replace the green preset with a custom color option that shows native color pickers for primary and secondary colors. Hex values are persisted in Redux (customPrimaryColor, customSecondaryColor) and flow to the Rive ViewModel on both the settings preview and HumanPage mascot. The custom swatch shows a split gradient of the two chosen colors. When selected, two color picker inputs appear below the swatches.
|
Warning Review limit reached
More reviews will be available in 7 minutes and 16 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR replaces the SVG-based YellowMascot with a Rive-based RiveMascot, adds Redux-persisted custom primary/secondary mascot colors and settings UI, refactors the frame producer to offscreen canvas JPEG capture, and updates i18n strings and tests to reflect the new custom color variant. ChangesRiveMascot migration and custom color feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
app/src/components/settings/panels/__tests__/MascotPanel.test.tsx (1)
88-95: ⚡ Quick winUse a stable selector for the
customswatch assertions.These checks validate
color: 'custom'but still query by the'Green'label. Preferdata-testid="mascot-color-custom"(or a translation-driven helper) so this stays stable across copy/i18n changes.Proposed fix
- fireEvent.click(screen.getByRole('radio', { name: 'Green' })); + fireEvent.click(screen.getByTestId('mascot-color-custom')); @@ - expect(screen.getByRole('radio', { name: 'Green' })).toHaveAttribute('aria-checked', 'true'); + expect(screen.getByTestId('mascot-color-custom')).toHaveAttribute('aria-checked', 'true');Also applies to: 140-149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/components/settings/panels/__tests__/MascotPanel.test.tsx` around lines 88 - 95, Tests in MascotPanel.test.tsx are asserting the mascot color by querying the "Green" radio label which is fragile to copy/i18n changes; update the assertions to use a stable test id (e.g. data-testid="mascot-color-custom") instead of label text. Locate the test that calls renderPanel(store) and fires click/select on the swatch, and change queries and expectations to use getByTestId('mascot-color-custom') (and any related assertions that check selection or dispatch behavior) so the check targets the custom swatch element directly; apply the same replacement to the analogous assertions around the 140-149 region.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/features/human/HumanPage.tsx`:
- Line 28: The RiveMascot is not receiving the current viseme so lip-sync stays
at the default; update the RiveMascot usage in HumanPage to pass the viseme from
the mascot hook (the value from useHumanMascot — e.g., face.viseme or
face?.viseme) as the viseme prop when rendering RiveMascot so runtime mouth
shapes follow the mascot state; ensure you reference the face object returned by
useHumanMascot and add the viseme prop to the RiveMascot JSX.
In `@app/src/features/human/SubMascotLayer.tsx`:
- Line 163: The RiveMascot render call drops per-agent colors by not passing
model.color; update the RiveMascot JSX in SubMascotLayer (the RiveMascot
component invocation) to include the color prop (e.g., face={model.face}
color={model.color}) so each sub-mascot receives its model.color and preserves
the per-agent palette when rendering.
In `@app/src/features/meet/MascotFrameProducer.tsx`:
- Line 204: MascotFrameProducer renders <RiveMascot face="idle" size={FRAME_H}
/> but does not pass the user-selected mascot palette, so meet video frames fall
back to Rive defaults; update MascotFrameProducer to accept or compute the ARGB
primaryColor and secondaryColor used by HumanPage/MascotPanel (the same computed
values those components pass) and thread them into the <RiveMascot ... /> call
(i.e., add primaryColor={primaryARGB} secondaryColor={secondaryARGB}) so the
view-model values are set when RiveMascot mounts; if this omission is
intentional, add a clear comment in MascotFrameProducer explaining why defaults
are used.
In `@app/src/store/__tests__/mascotSlice.test.ts`:
- Around line 53-56: The test 'exposes all five supported colors' currently
asserts a Set with a duplicated 'navy' and missing 'custom'; update the
expectation for SUPPORTED_MASCOT_COLORS in the test to use a Set containing
'yellow', 'burgundy', 'black', 'navy', and 'custom' (remove the duplicate 'navy'
and add 'custom') so it matches the slice contract exposed by
SUPPORTED_MASCOT_COLORS.
---
Nitpick comments:
In `@app/src/components/settings/panels/__tests__/MascotPanel.test.tsx`:
- Around line 88-95: Tests in MascotPanel.test.tsx are asserting the mascot
color by querying the "Green" radio label which is fragile to copy/i18n changes;
update the assertions to use a stable test id (e.g.
data-testid="mascot-color-custom") instead of label text. Locate the test that
calls renderPanel(store) and fires click/select on the swatch, and change
queries and expectations to use getByTestId('mascot-color-custom') (and any
related assertions that check selection or dispatch behavior) so the check
targets the custom swatch element directly; apply the same replacement to the
analogous assertions around the 140-149 region.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 11206c03-70eb-417a-af01-6994378da9a6
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (43)
app/package.jsonapp/public/tiny_mascot.rivapp/src/components/settings/panels/MascotPanel.tsxapp/src/components/settings/panels/__tests__/MascotPanel.test.tsxapp/src/features/human/HumanPage.test.tsxapp/src/features/human/HumanPage.tsxapp/src/features/human/Mascot/RiveMascot.tsxapp/src/features/human/Mascot/YellowMascot.test.tsxapp/src/features/human/Mascot/YellowMascot.tsxapp/src/features/human/Mascot/index.tsapp/src/features/human/Mascot/mascotPalette.test.tsapp/src/features/human/Mascot/mascotPalette.tsapp/src/features/human/Mascot/yellow/LoadingFace.tsxapp/src/features/human/Mascot/yellow/MascotCharacter.tsxapp/src/features/human/Mascot/yellow/MascotIdle.tsxapp/src/features/human/Mascot/yellow/MascotTalking.tsxapp/src/features/human/Mascot/yellow/MascotThinking.tsxapp/src/features/human/Mascot/yellow/RecordingFace.tsxapp/src/features/human/Mascot/yellow/frameContext.test.tsxapp/src/features/human/Mascot/yellow/frameContext.tsxapp/src/features/human/SubMascotLayer.tsxapp/src/features/meet/MascotFrameProducer.tsxapp/src/lib/i18n/chunks/ar-5.tsapp/src/lib/i18n/chunks/bn-5.tsapp/src/lib/i18n/chunks/de-5.tsapp/src/lib/i18n/chunks/en-5.tsapp/src/lib/i18n/chunks/es-5.tsapp/src/lib/i18n/chunks/fr-5.tsapp/src/lib/i18n/chunks/hi-5.tsapp/src/lib/i18n/chunks/id-5.tsapp/src/lib/i18n/chunks/it-5.tsapp/src/lib/i18n/chunks/ko-5.tsapp/src/lib/i18n/chunks/pt-5.tsapp/src/lib/i18n/chunks/ru-5.tsapp/src/lib/i18n/chunks/zh-CN-5.tsapp/src/lib/i18n/en.tsapp/src/mascot/MascotWindowApp.tsxapp/src/pages/ios/MascotScreen.test.tsxapp/src/pages/ios/MascotScreen.tsxapp/src/store/__tests__/mascotSlice.test.tsapp/src/store/mascotSlice.tspackage.jsontiny_mascot.riv
💤 Files with no reviewable changes (10)
- app/src/features/human/Mascot/yellow/frameContext.test.tsx
- app/src/features/human/Mascot/yellow/LoadingFace.tsx
- app/src/features/human/Mascot/yellow/frameContext.tsx
- app/src/features/human/Mascot/yellow/MascotCharacter.tsx
- app/src/features/human/Mascot/YellowMascot.tsx
- app/src/features/human/Mascot/yellow/MascotIdle.tsx
- app/src/features/human/Mascot/yellow/MascotThinking.tsx
- app/src/features/human/Mascot/yellow/MascotTalking.tsx
- app/src/features/human/Mascot/yellow/RecordingFace.tsx
- app/src/features/human/Mascot/YellowMascot.test.tsx
Replace 'green' with 'custom' in SUPPORTED_MASCOT_COLORS assertions and MascotPanel test aria labels to match the palette change.
Use importOriginal to keep getMascotPalette and hexToArgbInt from the real module while stubbing only the React components.
The @rive-app/react-webgl2 runtime calls window.matchMedia which does not exist in the jsdom test environment. Mock RiveMascot to a simple div stub while preserving real utility exports.
Avoid window.matchMedia crash from @rive-app/react-webgl2 in jsdom by stubbing RiveMascot in all test files that render it.
Summary
tiny_mascot.riv) rendered via@rive-app/react-webgl2primaryColor,secondaryColor,viseme,pose,mouthOpen, andisHoveringProblem
Solution
RiveMascot) drives the Rive ViewModel properties from the existingMascotFacelifecycleprimaryColorto body/head/hands fills,secondaryColorto shadow fills<input type="color">with hex values stored in ReduxSubmission Checklist
Impact
Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
Validation Run
pnpm --filter openhuman-app format:checkpnpm typecheckValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Parity Contract
Duplicate / Superseded PR Handling
Summary by CodeRabbit
New Features
Enhancements
Localization