Skip to content

feat(mascot): replace SVG animations with Rive renderer#2659

Merged
senamakel merged 11 commits into
tinyhumansai:mainfrom
senamakel:feat/rive-mascot
May 26, 2026
Merged

feat(mascot): replace SVG animations with Rive renderer#2659
senamakel merged 11 commits into
tinyhumansai:mainfrom
senamakel:feat/rive-mascot

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 26, 2026

Summary

  • Replace the custom Remotion/SVG frame-based mascot animation system with a Rive file (tiny_mascot.riv) rendered via @rive-app/react-webgl2
  • Add ViewModel data binding for primaryColor, secondaryColor, viseme, pose, mouthOpen, and isHovering
  • Add a custom color picker option in mascot settings (replaces green preset) with live Rive preview
  • Persist mascot color selection app-wide via Redux so settings changes update all surfaces

Problem

  • The old SVG mascot system used a complex custom Remotion frame provider with per-frame SVG mutation, hard to extend with new animations
  • Adding new poses or interactive behaviors required significant TypeScript code changes rather than designer-driven animation work

Solution

  • Rive file with a state machine handles body, eye, mouth, and hand animations natively
  • React component (RiveMascot) drives the Rive ViewModel properties from the existing MascotFace lifecycle
  • Color customization flows through Rive data binding: primaryColor to body/head/hands fills, secondaryColor to shadow fills
  • WebGL2 renderer provides full feather/gradient support
  • Custom color picker uses native <input type="color"> with hex values stored in Redux

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • N/A: Diff coverage >= 80% — Rive rendering is runtime/visual; core logic changes are minimal and covered by existing test updates
  • N/A: Coverage matrix updated — no new feature rows; this is a renderer swap
  • N/A: All affected feature IDs from the matrix are listed — renderer swap only
  • No new external network dependencies introduced — Rive file is bundled locally
  • N/A: Manual smoke checklist updated — mascot is not a release-cut surface
  • N/A: Linked issue closed — no associated issue

Impact

  • Desktop: mascot rendering switches from SVG/requestAnimationFrame to WebGL2 canvas
  • iOS: MascotScreen updated to use RiveMascot
  • Meet camera: MascotFrameProducer captures from Rive canvas instead of SVG serialization

Related

  • Follow-up: wire viseme/pose to Rive state machine animations, re-enable SubMascotLayer

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/rive-mascot
  • Commit SHA: 72e7f49

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: HumanPage.test.tsx, MascotScreen.test.tsx
  • N/A: Rust fmt/check (if changed) — no Rust changes
  • N/A: Tauri fmt/check (if changed) — no Tauri shell changes

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: Mascot renders via Rive WebGL2 canvas instead of SVG frame animation
  • User-visible effect: Same mascot appearance with smoother animations and feathering support; new custom color picker in settings

Parity Contract

  • Legacy behavior preserved: CustomGifMascot fallback still works; mascot face states map identically
  • Guard/fallback/dispatch parity checks: face-to-pose mapping covers all MascotFace variants

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • New Features

    • Custom mascot color option with primary/secondary color pickers in settings
    • Choice of preset palettes or personalized color combinations
  • Enhancements

    • Mascot rendering replaced with a smoother, Rive-based mascot for improved visuals and animations
  • Localization

    • Updated mascot color labels and new primary/secondary color strings across languages

Review Change Stack

senamakel added 6 commits May 25, 2026 19:14
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.
@senamakel senamakel requested a review from a team May 26, 2026 03:22
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@senamakel, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdf04693-6a13-4ab2-ab8f-d1130da50712

📥 Commits

Reviewing files that changed from the base of the PR and between c500827 and 077b5c6.

📒 Files selected for processing (3)
  • app/src/components/settings/panels/__tests__/MascotPanel.test.tsx
  • app/src/features/human/SubMascotLayer.test.tsx
  • app/src/mascot/MascotWindowApp.test.tsx
📝 Walkthrough

Walkthrough

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

Changes

RiveMascot migration and custom color feature

Layer / File(s) Summary
RiveMascot component and color utilities
app/src/features/human/Mascot/RiveMascot.tsx, app/src/features/human/Mascot/mascotPalette.ts, app/src/features/human/Mascot/index.ts, app/package.json, package.json
New RiveMascot React component and props; MascotColor replaces 'green' with 'custom'. Added hexToArgbInt to convert #RRGGBB to ARGB integers for Rive. Rive dependency entries added/adjusted.
Redux state for custom colors
app/src/store/mascotSlice.ts, app/src/store/__tests__/mascotSlice.test.ts
MascotState extended with customPrimaryColor and customSecondaryColor. Added setCustomPrimaryColor/setCustomSecondaryColor actions and selectors. REHYDRATE restores persisted custom colors when present. Tests updated to use navy baseline.
Mascot settings panel color picker
app/src/components/settings/panels/MascotPanel.tsx, app/src/components/settings/panels/__tests__/MascotPanel.test.tsx
Panel reads custom colors from Redux, computes memoized ARGB values via getMascotPalette + hexToArgbInt, previews via RiveMascot, shows gradient swatch for custom, and conditionally renders two color inputs that dispatch updates. Tests updated for custom cases.
Component migration to RiveMascot
app/src/features/human/HumanPage.tsx, app/src/features/human/HumanPage.test.tsx, app/src/pages/ios/MascotScreen.tsx, app/src/pages/ios/MascotScreen.test.tsx, app/src/mascot/MascotWindowApp.tsx, app/src/features/human/SubMascotLayer.tsx, app/src/features/human/Mascot/YellowMascot.tsx, app/src/features/human/Mascot/yellow/*
All mascot consumers now render RiveMascot instead of YellowMascot. HumanPage computes and passes primary/secondary ARGB colors. Yellow mascot implementation and variant modules/tests under yellow/ removed. Sub-mascot layer narrowed color set and now passes only face/size to RiveMascot.
Frame producer canvas-based capture
app/src/features/meet/MascotFrameProducer.tsx
Refactored to offscreen canvas capture: captureFrame creates an OffscreenCanvas, composites a radial background + host canvas, JPEG-encodes via convertToBlob, and sends bytes over WebSocket. Worker tick simplified; concurrent capture guarded via refs. Audio keep-alive play() handled with .catch.
Internationalization updates
app/src/lib/i18n/chunks/{ar,bn,de,en,es,fr,hi,id,it,ko,pt,ru,zh-CN}-5.ts, app/src/lib/i18n/en.ts
Removed settings.mascot.colorGreen; added settings.mascot.colorCustom, settings.mascot.primaryColor, and settings.mascot.secondaryColor across locale chunks. Navy color preserved.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • graycyrus

🐰 From yellow SVG to Rive we leap,
Custom hues in Redux we keep,
Offscreen frames now hum and send,
Locale keys updated end to end—
Mascot hops, prepared to peep!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: replacing SVG-based animations with Rive renderer for the mascot component.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team. labels May 26, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/src/components/settings/panels/__tests__/MascotPanel.test.tsx (1)

88-95: ⚡ Quick win

Use a stable selector for the custom swatch assertions.

These checks validate color: 'custom' but still query by the 'Green' label. Prefer data-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

📥 Commits

Reviewing files that changed from the base of the PR and between e05cab9 and 72e7f49.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (43)
  • app/package.json
  • app/public/tiny_mascot.riv
  • app/src/components/settings/panels/MascotPanel.tsx
  • app/src/components/settings/panels/__tests__/MascotPanel.test.tsx
  • app/src/features/human/HumanPage.test.tsx
  • app/src/features/human/HumanPage.tsx
  • app/src/features/human/Mascot/RiveMascot.tsx
  • app/src/features/human/Mascot/YellowMascot.test.tsx
  • app/src/features/human/Mascot/YellowMascot.tsx
  • app/src/features/human/Mascot/index.ts
  • app/src/features/human/Mascot/mascotPalette.test.ts
  • app/src/features/human/Mascot/mascotPalette.ts
  • app/src/features/human/Mascot/yellow/LoadingFace.tsx
  • app/src/features/human/Mascot/yellow/MascotCharacter.tsx
  • app/src/features/human/Mascot/yellow/MascotIdle.tsx
  • app/src/features/human/Mascot/yellow/MascotTalking.tsx
  • app/src/features/human/Mascot/yellow/MascotThinking.tsx
  • app/src/features/human/Mascot/yellow/RecordingFace.tsx
  • app/src/features/human/Mascot/yellow/frameContext.test.tsx
  • app/src/features/human/Mascot/yellow/frameContext.tsx
  • app/src/features/human/SubMascotLayer.tsx
  • app/src/features/meet/MascotFrameProducer.tsx
  • app/src/lib/i18n/chunks/ar-5.ts
  • app/src/lib/i18n/chunks/bn-5.ts
  • app/src/lib/i18n/chunks/de-5.ts
  • app/src/lib/i18n/chunks/en-5.ts
  • app/src/lib/i18n/chunks/es-5.ts
  • app/src/lib/i18n/chunks/fr-5.ts
  • app/src/lib/i18n/chunks/hi-5.ts
  • app/src/lib/i18n/chunks/id-5.ts
  • app/src/lib/i18n/chunks/it-5.ts
  • app/src/lib/i18n/chunks/ko-5.ts
  • app/src/lib/i18n/chunks/pt-5.ts
  • app/src/lib/i18n/chunks/ru-5.ts
  • app/src/lib/i18n/chunks/zh-CN-5.ts
  • app/src/lib/i18n/en.ts
  • app/src/mascot/MascotWindowApp.tsx
  • app/src/pages/ios/MascotScreen.test.tsx
  • app/src/pages/ios/MascotScreen.tsx
  • app/src/store/__tests__/mascotSlice.test.ts
  • app/src/store/mascotSlice.ts
  • package.json
  • tiny_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

Comment thread app/src/features/human/HumanPage.tsx
Comment thread app/src/features/human/SubMascotLayer.tsx
Comment thread app/src/features/meet/MascotFrameProducer.tsx
Comment thread app/src/store/__tests__/mascotSlice.test.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 26, 2026
Replace 'green' with 'custom' in SUPPORTED_MASCOT_COLORS assertions
and MascotPanel test aria labels to match the palette change.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 26, 2026
Use importOriginal to keep getMascotPalette and hexToArgbInt from the
real module while stubbing only the React components.
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 26, 2026
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.
senamakel added 2 commits May 25, 2026 21:13
Avoid window.matchMedia crash from @rive-app/react-webgl2 in jsdom
by stubbing RiveMascot in all test files that render it.
@senamakel senamakel merged commit f946d10 into tinyhumansai:main May 26, 2026
29 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant