2026 03 18 Vitto Avatar Improvements Design
Documentation for Plans
Vitto Avatar Improvements — Design Spec
Session: S237 Date: 2026-03-18 Approach: Vertical Slices (testable end-to-end after each slice)
Overview
Comprehensive enhancement of the Vitto avatar system across 6 vertical slices:
- CharacterConfig + Palette System — decouple appearance from animation
- Disney Animation Core — anticipation, follow-through, overlapping action, secondary action, exaggeration, staging
- 8 New Emotions — thinking, surprised, frustrated, grateful, mischievous, determined, listening, excited (total: 22 moods)
- Accessory System — wearables + held props with anchor points and spring physics
- Environmental Effects — weather, stage/spotlight, floating thought symbols, ground effects
- Backend + Refinement — API for character configs, accessory CRUD, enhanced inference
All changes must be testable on:
- Demo page:
http://localhost:6300/demos/vitto-avatar - Production: vitto-chat components
Slice 1: CharacterConfig + Palette System
Goal
Extract all hardcoded colors and geometry constants into a configurable CharacterConfig object so Vitto's appearance can be swapped without touching animation code.
CharacterConfig Interface
interface CharacterConfig {
// ─── Palette ─────────────────────────────────────
bodyColor: string; // '#1a1a1a' (default black cat)
bodyHighlight: string; // '#2d2d2d'
eyeScleraColor: string; // '#ffffff'
pupilColor: string; // '#111111'
noseColor: string; // '#d4a060'
noseHighlight: string; // 'rgba(255,220,180,.35)'
tongueColor: string; // 'rgba(230,100,120,1)'
earInnerColor: string; // 'rgba(200,140,160,.35)'
blushColor: string; // 'rgba(255,130,170,.35)'
whiskerColor: string; // 'rgba(90,80,70,.65)'
muzzleColor: string; // 'rgba(50,42,35,.65)'
browColor: string; // 'rgba(60,55,50,.9)'
teethColor: string; // '#ffffff'
mouthInterior: string; // '#3a1520'
// ─── Geometry Ratios (1.0 = current default) ─────
headScale: number; // multiplier on HEAD.radiusX/Y
bodyScale: number; // multiplier on BODY.radiusX/Y
earSize: number; // multiplier on ear triangle dimensions
earPointiness: number; // controls ear tip height (1.0 = current, 1.5 = pointed)
eyeSpacing: number; // multiplier on EYE.spacingX
eyeSize: number; // multiplier on EYE.baseWidth/baseHeight
tailLength: number; // multiplier on tail bezier control points
tailThickness: number; // multiplier on TAIL.strokeMain/Secondary
bodyRoundness: number; // interpolates body radiusX vs radiusY ratio
whiskerLength: number; // multiplier on WHISKER.length
feetSize: number; // multiplier on FEET.radiusX/Y
noseSize: number; // multiplier on NOSE.width/height
}
Default + Presets
const DEFAULT_CHARACTER: CharacterConfig = {
// All current hardcoded values → 1.0 ratios
};
const CHARACTER_PRESETS: Record<string, CharacterConfig> = {
classic: DEFAULT_CHARACTER, // Current black cat
mochi: { ...DEFAULT, bodyColor: '#f0ede8', earInnerColor: 'rgba(255,180,200,.4)', ... },
marmalade: { ...DEFAULT, bodyColor: '#c86a28', noseColor: '#8b4513', ... },
smokey: { ...DEFAULT, bodyColor: '#5a5a6a', ... },
midnight: { ...DEFAULT, bodyColor: '#1a1a3a', bodyHighlight: '#2a2a4a', ... },
};
Integration Points
| Layer | Change |
|---|---|
engine/config/character.ts |
NEW — CharacterConfig type, DEFAULT, presets |
engine/types.ts |
Add character: CharacterConfig to DrawContext |
engine/renderer.ts |
Add setCharacterConfig() method, pass to DrawContext |
engine/drawing/body.ts |
Replace #1a1a1a → dc.character.bodyColor, apply geometry ratios |
engine/drawing/head.ts |
Replace hardcoded colors, apply headScale, earSize |
engine/drawing/face.ts |
Replace all hardcoded colors with config reads |
engine/drawing/effects.ts |
No color changes (particles are mood-colored, not character-colored) |
vitto-avatar.component.ts |
New input: characterConfig: Partial<CharacterConfig> |
vitto-mood.service.ts |
New signal: characterConfig() |
services/character-config-api.service.ts |
NEW — CharacterConfigApiService for CRUD to /character-configs |
| Demo page | Preset dropdown + color pickers + geometry sliders |
Backend
New schema CharacterConfigSchema in backend-vitto-api/src/app/mind/schemas/:
name: string(unique)config: CharacterConfig(embedded object)isBuiltIn: booleanspaceId?: stringcreatedBy?: string
New controller CharacterConfigController at /character-configs:
GET /— list all (built-in + space)GET /:name— single configPOST /— create customPUT /:name— updateDELETE /:name— delete (non-built-in only)
Seed 5 built-in presets on startup.
Slice 2: Disney Animation Core
Goal
Implement 6 Disney animation principles as behavior modules that enhance ALL moods — existing and new.
2a: Anticipation System
What: Before transitioning to a high-energy mood, Vitto briefly winds up in the opposite direction.
// engine/behaviors/anticipation.ts
interface AnticipationConfig {
/** Moods that trigger anticipation on entry */
triggerMoods: Set<string>; // celebratory, excited, surprised, inspired, urgent
/** Duration of wind-up phase (seconds) */
windUpDuration: number; // 0.15
/** How much to overshoot properties in opposite direction */
overshootFactor: number; // 0.3
}
How it works:
- When
setMood()is called and the target is intriggerMoods, inject a brief wind-up phase - During wind-up: body squashes slightly (crouch), ears pull back, head dips
- After wind-up: transition proceeds normally but with
easeOutBackfor overshoot feel - Total time: ~150ms anticipation + normal 350ms transition = 500ms
New property added to MoodProperties:
anticipation: number— 0-1, intensity of the wind-up. High-energy moods get 0.7-1.0, calm moods get 0.
Default values for ALL 14 existing moods (added as part of Slice 2):
- All existing moods get
anticipation: 0except:celebratory:anticipation: 0.7inspired:anticipation: 0.5urgent:anticipation: 0.6
2b: Follow-Through System
What: After a mood transition completes, body parts overshoot their target and settle back.
// engine/behaviors/follow-through.ts
interface FollowThroughState {
earOvershoot: number; // decaying oscillation
headOvershoot: number; // head position overshoot
tailOvershoot: number; // tail speed overshoot
bodyOvershoot: number; // body position overshoot
browOvershoot: number; // brow position overshoot
}
How it works:
- On transition complete (
tTreaches 1), capture velocity of change for each body part - Apply decaying spring oscillation (reuse
spring.tspattern) per body part - Ears: overshoot earAngle by 15%, settle in ~300ms
- Head: overshoot headTilt by 10%, settle in ~250ms
- Body: overshoot bodyBounce by 20%, settle in ~400ms
- Tail: spike tailSpeed by 30%, decay back in ~500ms
2c: Overlapping Action
What: Different body parts move at different rates during transitions, not all in lockstep.
// New property groups with per-group transition speeds
const OVERLAP_GROUPS = {
eyes: { speed: 3.5, delay: 0 }, // Eyes react first (fastest)
mouth: { speed: 3.0, delay: 0.03 }, // Mouth follows eyes closely
brows: { speed: 2.5, delay: 0.05 }, // Brows slightly behind
ears: { speed: 2.0, delay: 0.08 }, // Ears are slower (heavier)
body: { speed: 1.8, delay: 0.1 }, // Body is heaviest, moves last
tail: { speed: 2.2, delay: 0.12 }, // Tail reacts last with spring
};
How it works (single-tT with per-group speed multipliers — preserves existing API):
- Keep the single global
tT(preservesisTransitioning(),getTransitionProgress(), and the 700msTRANSITION_DURATION_MSin VittoMoodService) - Each property group gets its own eased progress:
groupT = easeInOutCubic(clamp(tT * groupSpeed - groupDelay, 0, 1)) - Eyes snap to new expression almost instantly (speed 3.5), ears lag behind (speed 2.0), body is last (speed 1.8)
- Creates wave-like motion through the character during transitions
- Map each
MoodPropertyKeyto its group in a lookup table (e.g.,eyeH → eyes,earAngle → ears) - Modify
updateTransition()in renderer.ts to use per-groupgroupTinstead of globaltwhen interpolating
2d: Secondary Action
What: Small supporting gestures that happen alongside the primary mood expression.
// engine/behaviors/secondary-action.ts
interface SecondaryAction {
trigger: string; // mood name or transition event
getOverrides: (t: number) => Partial<IdleOffsets>; // returns offsets the renderer blends additively
duration: number; // how long the action lasts (seconds)
probability: number; // 0-1, chance of firing per trigger
cooldown: number; // minimum seconds between fires
}
// NOTE: Actions return Partial<IdleOffsets> instead of mutating state via closures.
// The renderer blends active secondary action offsets additively into the idle offsets.
// This keeps the engine functional and debuggable.
const SECONDARY_ACTIONS: SecondaryAction[] = [
// Ear flick when transitioning to cheerful
{ trigger: 'cheerful', probability: 0.8, cooldown: 2, action: earFlick },
// Tail curl when thinking
{ trigger: 'thinking', probability: 0.6, cooldown: 3, action: tailCurl },
// Quick nose scrunch when confused
{ trigger: 'confused', probability: 0.7, cooldown: 1.5, action: noseScrunch },
// Chest puff when proud
{ trigger: 'proud', probability: 0.9, cooldown: 3, action: chestPuff },
// Double-blink when surprised
{ trigger: 'surprised', probability: 1.0, cooldown: 0, action: doubleBlink },
// Whisker bristle when determined
{ trigger: 'determined', probability: 0.8, cooldown: 2, action: whiskerBristle },
// Yawn cycle when sleepy (periodic)
{ trigger: 'sleepy', probability: 0.3, cooldown: 8, action: yawnCycle },
];
How it works:
- Secondary actions are triggered on mood entry or periodically while in a mood
- Each action temporarily overrides specific idle parameters for a brief duration
earFlick: spike one ear's idle twitch for 200ms then decaytailCurl: tighten tail curl for 500msnoseScrunch: briefly compress nose geometrychestPuff: briefly increase bodyScale then relaxdoubleBlink: force two quick blinks in successionyawnCycle: animate mouth open → wide → close over 1.5s, periodically while sleepy
2e: Exaggeration
What: Stronger expressions for dramatic moods. The 27 properties already support this — we enhance the ranges.
Implementation:
- Add
expressiveness: number(0-2, default 1) toRendererConfig - Applied SELECTIVELY to expressive properties only (not base geometry like
eyeH,eyeW):- Scaled properties:
bodyBounce,tailSpeed,blush,sparkle,hearts,sweat,exclaim,question,zzz,bodySquash,headTilt,browY,browAngle,smile,mouthOpen,tongue,teeth,earAngle - NOT scaled:
eyeH,eyeW,pupilR,eyeSquish,happyEye,starEye,mouthW,whiskerAngle(these control shape, not expression intensity)
- Scaled properties:
- Formula for scaled properties:
scaledTarget = target * expressivenessclamped to[property_min, property_max] expressiveness: 1.5makes celebratory even bouncier, apologetic even droopierexpressiveness: 0.5creates subtle, understated expressions- Exposed on demo page as a slider
- VittoMoodService gets
expressivenesssignal
2f: Staging
What: Vitto subtly shifts position and rotation to "present" the emotion to the viewer.
// New MoodProperties additions:
stageX: number; // horizontal shift (-1 to 1, normalized)
stageY: number; // vertical shift (-1 to 1, normalized)
stageRotate: number; // subtle body rotation (-0.1 to 0.1 radians)
Default values for ALL 14 existing moods (added as part of Slice 2):
- All existing moods get
stageX: 0, stageY: 0, stageRotate: 0except:proud:stageX: 0, stageY: -0.06, stageRotate: -0.02(chin up, slight backward lean)apologetic:stageX: 0, stageY: 0.05, stageRotate: 0.03(bowing forward)inspired:stageX: 0, stageY: -0.08, stageRotate: -0.01(reaching for the sky)sleepy:stageX: 0.04, stageY: 0.06, stageRotate: 0.04(nodding off to one side)celebratory:stageX: 0, stageY: -0.05, stageRotate: 0(jumping for joy)
Backend sync: The backend MoodProperties interface in mood-definition.schema.ts and BUILTIN_MOODS in mood-definition.service.ts must BOTH be updated with stageX, stageY, stageRotate, and anticipation fields (default 0 in schema, non-zero values for moods listed above).
Slice 3: 8 New Emotions
New MoodProperties Additions
Before defining moods, add new animatable properties:
// Added to MoodProperties in types.ts
stageX: number; // staging horizontal shift
stageY: number; // staging vertical shift
stageRotate: number; // staging body rotation
anticipation: number; // wind-up intensity (0-1)
New Mood Definitions (complete — ALL 31 properties specified)
CRITICAL: Every mood MUST define ALL properties including whiskerAngle, stageX, stageY, stageRotate, anticipation. Missing properties cause NaN in interpolation.
// engine/moods.ts — 8 new entries (indices 14-21)
thinking: {
label: 'Thinking', sub: 'processing · working', accent: '#fbbf24',
bg: [235, 175, 55],
eyeH: .85, eyeW: .95, pupilR: .7, eyeSquish: .15, happyEye: 0, starEye: 0,
smile: 0, mouthOpen: 0, mouthW: .7, teeth: 0, tongue: 0,
browY: .15, browAngle: .2, browAsym: .3,
earAngle: .1, headTilt: -.08, whiskerAngle: 0,
blush: 0,
sparkle: 0, sweat: 0, exclaim: 0, hearts: 0, question: 0, zzz: 0,
bodyBounce: 0, tailSpeed: .4, bodySquash: .02,
stageX: 0, stageY: -.05, stageRotate: .02, anticipation: 0,
},
surprised: {
label: 'Surprised', sub: 'wow · startled', accent: '#facc15',
bg: [255, 210, 75],
eyeH: 1.4, eyeW: 1.2, pupilR: .4, eyeSquish: 0, happyEye: 0, starEye: 0,
smile: 0, mouthOpen: .4, mouthW: .7, teeth: 0, tongue: 0,
browY: -.5, browAngle: 0, browAsym: .1,
earAngle: .5, headTilt: 0, whiskerAngle: .3,
blush: .15,
sparkle: 0, sweat: 0, exclaim: .6, hearts: 0, question: 0, zzz: 0,
bodyBounce: .12, tailSpeed: 2.0, bodySquash: 0,
stageX: 0, stageY: .1, stageRotate: 0, anticipation: .8,
},
frustrated: {
label: 'Frustrated', sub: 'struggling · stuck', accent: '#f97316',
bg: [240, 145, 40],
eyeH: .6, eyeW: .95, pupilR: .5, eyeSquish: .4, happyEye: 0, starEye: 0,
smile: -.7, mouthOpen: .05, mouthW: .6, teeth: .15, tongue: 0,
browY: .6, browAngle: .6, browAsym: .15,
earAngle: -.3, headTilt: 0, whiskerAngle: -.15,
blush: .2,
sparkle: 0, sweat: .5, exclaim: .3, hearts: 0, question: 0, zzz: 0,
bodyBounce: .02, tailSpeed: 2.5, bodySquash: .08,
stageX: 0, stageY: .03, stageRotate: 0, anticipation: .3,
},
grateful: {
label: 'Grateful', sub: 'thankful · warm', accent: '#a78bfa',
bg: [248, 195, 65],
eyeH: 1.0, eyeW: 1.05, pupilR: 1.2, eyeSquish: 0, happyEye: .6, starEye: 0,
smile: .6, mouthOpen: .1, mouthW: .9, teeth: 0, tongue: 0,
browY: -.1, browAngle: -.2, browAsym: 0,
earAngle: .15, headTilt: .06, whiskerAngle: .08,
blush: .6,
sparkle: .2, sweat: 0, exclaim: 0, hearts: .4, question: 0, zzz: 0,
bodyBounce: 0, tailSpeed: .8, bodySquash: 0,
stageX: 0, stageY: -.08, stageRotate: -.03, anticipation: 0,
},
mischievous: {
label: 'Mischievous', sub: 'sly · sneaky', accent: '#34d399',
bg: [245, 200, 60],
eyeH: .8, eyeW: 1.0, pupilR: .85, eyeSquish: .2, happyEye: 0, starEye: 0,
smile: .65, mouthOpen: .08, mouthW: 1.0, teeth: .1, tongue: .3,
browY: -.05, browAngle: .3, browAsym: .8,
earAngle: .2, headTilt: .1, whiskerAngle: .2,
blush: .15,
sparkle: .15, sweat: 0, exclaim: 0, hearts: 0, question: 0, zzz: 0,
bodyBounce: .04, tailSpeed: 1.8, bodySquash: .02,
stageX: .03, stageY: 0, stageRotate: .02, anticipation: 0,
},
determined: {
label: 'Determined', sub: 'resolute · locked', accent: '#f59e0b',
bg: [242, 165, 35],
eyeH: .75, eyeW: 1.0, pupilR: .6, eyeSquish: .35, happyEye: 0, starEye: 0,
smile: -.1, mouthOpen: 0, mouthW: .7, teeth: 0, tongue: 0,
browY: .3, browAngle: .5, browAsym: 0,
earAngle: .4, headTilt: 0, whiskerAngle: .12,
blush: 0,
sparkle: 0, sweat: 0, exclaim: 0, hearts: 0, question: 0, zzz: 0,
bodyBounce: .01, tailSpeed: .5, bodySquash: .03,
stageX: 0, stageY: -.03, stageRotate: -.02, anticipation: .2,
},
listening: {
label: 'Listening', sub: 'attentive · present', accent: '#60a5fa',
bg: [242, 185, 48],
eyeH: 1.1, eyeW: 1.05, pupilR: 1.15, eyeSquish: 0, happyEye: 0, starEye: 0,
smile: .08, mouthOpen: 0, mouthW: .75, teeth: 0, tongue: 0,
browY: -.05, browAngle: 0, browAsym: .1,
earAngle: .45, headTilt: .05, whiskerAngle: .05,
blush: 0,
sparkle: 0, sweat: 0, exclaim: 0, hearts: 0, question: 0, zzz: 0,
bodyBounce: 0, tailSpeed: .6, bodySquash: 0,
stageX: 0, stageY: 0, stageRotate: 0, anticipation: 0,
},
excited: {
label: 'Excited', sub: 'hyped · electric', accent: '#f472b6',
bg: [255, 210, 65],
eyeH: 1.1, eyeW: 1.1, pupilR: .3, eyeSquish: 0, happyEye: .7, starEye: 0,
smile: 1, mouthOpen: .35, mouthW: 1.3, teeth: .2, tongue: .4,
browY: -.25, browAngle: 0, browAsym: 0,
earAngle: .35, headTilt: .06, whiskerAngle: .25,
blush: .7,
sparkle: .7, sweat: 0, exclaim: 0, hearts: .3, question: 0, zzz: 0,
bodyBounce: .18, tailSpeed: 3.2, bodySquash: 0,
stageX: 0, stageY: .05, stageRotate: 0, anticipation: .9,
},
New Particle Types
| Particle | Used By | Description |
|---|---|---|
| gears | thinking | 2-3 interlocking gear outlines rotating slowly near head-top |
| steam | frustrated | small puffs rising from top of head, expanding and fading |
| lightbulb | inspired (enhanced) | brief lightbulb flash on eureka transitions (one-shot, not continuous) |
Backend Seeding
Add 8 new entries to BUILTIN_MOODS in mood-definition.service.ts. Update seedBuiltInMoods() to handle incremental seeding (existing moods untouched, new ones added).
Frontend Registrations
- Update
VittoMoodtype union invitto-avatar.types.ts: add 8 new mood literals - Update
VITTO_MOOD_INDEX: assign indices 14-21 - Update
VITTO_MOOD_REGISTRY: add full metadata + palettes for each- Palette derivation: Use
deriveAuraFromBg()fromengine/config/palette.tsforauraandglowfields. For the remaining 5 palette fields (face,earInner,iris,scarf,pulseSpeed), derive algorithmically frombgRGB:face:[lighten(bg, 40%), bg, darken(bg, 60%)]— highlight, base, shadowearInner:[lighten(bg, 50%), darken(bg, 20%)]with pink shiftiris:[shiftHue(bg, +30°), darken(shiftHue(bg, +30°), 40%)]scarf:[desaturate(bg, 20%), darken(desaturate(bg, 20%), 30%)]pulseSpeed: map from mood energy (high energy → fast pulse ~1.5s, low energy → slow ~4s)
- Palette derivation: Use
- Update
VittoEmotionServicein vitto-chat:- Add heuristic rules for new moods
- Update
PHASE_MOOD_MAP(e.g.,ANALYZE → thinking,VERIFY → determined) - Update
CONTEXT_MOOD_MAPif needed
- Update
MoodInferenceServicesystem prompt to include new moods - Demo page: automatically shows new moods (reads from MOOD_DEFINITIONS)
Slice 4: Accessory System
Architecture
engine/
accessories/
types.ts — AccessoryDef, AnchorPoint, AccessoryState
registry.ts — ACCESSORY_DEFINITIONS map
manager.ts — AccessoryManager (spring physics, lifecycle)
drawing/
wearables.ts — drawGlasses, drawHat, drawScarf, drawCape, drawBowTie, drawCrown, drawBadge
props.ts — drawCoffee, drawBook, drawWand, drawPhone, drawToolbox, drawUmbrella
Anchor Points
// NOTE: Vitto has no arms/hands. "body-side-left" and "body-side-right" are positioned
// at the sides of the body ellipse (CX ± BODY.radiusX, CY + BODY.offsetY).
// Props "hover" beside the body — no visible limb needed. This matches the
// cartoon aesthetic where items appear held without explicit arms.
type AnchorPoint = 'head-top' | 'head-left' | 'head-right' | 'nose-bridge' | 'body-front' | 'body-side-left' | 'body-side-right' | 'body-back';
interface AccessoryAnchor {
point: AnchorPoint;
offsetX: number;
offsetY: number;
followRotation: boolean; // rotate with head tilt
lagFactor: number; // 0=locked, 1=full follow-through lag
}
Accessory Definition
interface AccessoryDef {
id: string;
name: string;
category: 'wearable' | 'prop';
anchor: AccessoryAnchor;
triggerMoods?: string[]; // auto-show on these moods
manualOnly?: boolean; // never auto-triggered
draw: (ctx: CanvasRenderingContext2D, state: AccessoryState) => void;
springConfig?: { stiffness: number; damping: number };
}
Priority Accessories (Phase 1)
| Priority | Accessory | Category | Anchor | Trigger Moods |
|---|---|---|---|---|
| 1 | Reading glasses | wearable | nose-bridge | analytical, thinking |
| 2 | Party hat | wearable | head-top | celebratory, excited |
| 3 | Coffee cup | prop | body-side-right | thinking |
| 4 | Crown | wearable | head-top | proud |
| 5 | Magic wand | prop | body-side-right | inspired |
| 6 | Nightcap | wearable | head-top | sleepy |
| 7 | Toolbox/wrench | prop | body-side-left | determined, focused |
| 8 | Sunglasses | wearable | nose-bridge | proud, mischievous |
| 9 | Bow tie | wearable | body-front | focused |
| 10 | Umbrella | prop | body-side-right | apologetic |
Physics
Each accessory has its own SpringState for follow-through:
- Wearables: stiffness 150, damping 0.75 (tight, small overshoot)
- Props: stiffness 80, damping 0.6 (looser, more bounce)
- Hat pompom: stiffness 60, damping 0.5 (very bouncy — secondary action on the secondary action)
Renderer Integration
// In renderer.ts render() method, after drawHead():
if (this.accessoryManager) {
this.accessoryManager.update(dt, this.state, headPosition, headRotation);
this.accessoryManager.draw(this.ctx);
}
Public API
class VittoRenderer {
setAccessory(id: string, visible: boolean): void;
getActiveAccessories(): string[];
setAccessoryAutoTrigger(enabled: boolean): void;
}
Backend
New schema AccessoryConfigSchema:
accessoryId: stringenabled: booleanspaceId?: stringcustomConfig?: Partial<AccessoryDef>
Endpoint: GET /character-configs/:name/accessories — returns enabled accessories for a character config.
Slice 5: Environmental Effects
Architecture
engine/
environment/
types.ts — EnvironmentLayer, WeatherType, etc.
manager.ts — EnvironmentManager (particle pools, layer ordering)
weather.ts — sun rays, rain, snow, lightning, clouds
stage.ts — spotlight cone, dim ambiance, dramatic lighting
floating.ts — thought symbols: gears, lightbulbs, music notes, etc.
ground.ts — flowers, cracks, sparkle trails, grass
Layer Ordering (draw order)
- Background (existing)
- Weather background layer (rain, snow, clouds — behind Vitto)
- Ground effects (flowers, cracks — at feet level)
- Shadow (existing)
- Tail (existing)
- Body (existing)
- Accessories — body layer (bow tie, badges)
- Head + face (existing)
- Accessories — head layer (glasses, hats)
- Particles (existing, enhanced)
- Floating thought symbols (above head)
- Weather foreground layer (rain drops in front, lightning flash)
- Stage effects (spotlight cone overlay)
Mood-to-Environment Mapping
| Mood | Weather | Stage | Floating | Ground |
|---|---|---|---|---|
| cheerful | sunshine rays | warm glow | — | flowers bloom |
| celebratory | confetti | spotlight | — | sparkle burst |
| apologetic | light rain | dim | — | — |
| sleepy | gentle snow | dim, warm | — | — |
| urgent | lightning flash | red pulse | — | cracks |
| inspired | — | spotlight pulse | lightbulbs | sparkle trail |
| thinking | — | — | gears | — |
| analytical | — | — | gears (slow) | — |
| frustrated | storm clouds | red tint | — | cracks |
| playful | — | — | music notes | flowers |
| excited | sunshine + confetti | bright spotlight | — | sparkle burst |
| determined | — | focused beam | — | — |
| grateful | warm rays | golden glow | hearts (soft) | flowers (gentle) |
Particle Pool
Environmental effects use a shared particle pool (max 100 particles) to avoid GC pressure:
class ParticlePool {
private pool: Particle[] = [];
acquire(): Particle;
release(p: Particle): void;
}
Each particle: { x, y, vx, vy, life, maxLife, size, rotation, type, color }.
Coexistence with existing effects.ts: The existing particle effects (sparkles, hearts, question marks, sweat, exclamation, zzz) are drawn procedurally each frame with no lifecycle state — they stay as-is. The new ParticlePool is ONLY for environmental effects (weather, ground). Two rendering paradigms coexist intentionally: existing effects are mood-property-driven (immediate), environmental particles are lifecycle-driven (spawn, live, die).
Renderer Integration
// In renderer.ts render():
this.environmentManager.updateWeatherBg(dt, state); // before drawShadow
this.environmentManager.drawWeatherBg(ctx);
// ... existing draw calls ...
this.environmentManager.updateWeatherFg(dt, state); // after drawHead
this.environmentManager.drawFloating(ctx);
this.environmentManager.drawWeatherFg(ctx);
this.environmentManager.drawStage(ctx);
Slice 6: Backend + Refinement
Enhanced Mood Inference
Update ALL locations in MoodInferenceService (APIs/backend-vitto-api/src/app/mind/mood-inference.service.ts):
VALID_MOODSarray (line ~17) — add 8 new mood stringsMOOD_LABELSmap (line ~26) — add 8 new label entries- Local
VittoMoodtype (line ~23) — add 8 new literals - System prompt string — add 8 new moods with descriptions
heuristicMood()regex rules — add patterns for new moods
Add weighted scoring for new moods:
thinkingtriggered by: processing indicators, "let me check", "analyzing"surprisedtriggered by: unexpected results, "wow", "I didn't expect"frustratedtriggered by: repeated failures, "still not working", error patternsgratefultriggered by: thank you, appreciation, help completionmischievoustriggered by: witty remarks, playful teasing, clever solutionsdeterminedtriggered by: "I'll fix this", tackling hard problemslisteningtriggered by: long user messages, questions needing attentionexcitedtriggered by: major breakthroughs, great news, enthusiasm
Character Config API
Full CRUD for CharacterConfig:
GET /character-configs— list allGET /character-configs/:name— singlePOST /character-configs— create custom (per space)PUT /character-configs/:name— updateDELETE /character-configs/:name— delete (non-built-in)POST /character-configs/:name/refine— AI agent self-improvement (like mood refine)
Accessory API
GET /accessories— list all definitionsGET /accessories/for-mood/:mood— get accessories triggered by moodPUT /accessories/:id/config— per-space accessory overrides
MoodDefinition Schema Updates
Add to existing schema:
@Prop({ type: Object, default: {} })
environmentConfig?: {
weather?: string;
stage?: string;
floating?: string[];
ground?: string;
};
@Prop({ type: [String], default: [] })
accessories?: string[]; // accessory IDs triggered by this mood
@Prop({ type: Number, default: 0 })
anticipation?: number; // 0-1
@Prop({ type: Object, default: {} })
staging?: {
x?: number;
y?: number;
rotate?: number;
};
Demo Page Updates
Demo page component: apps/gograb-dev/src/app/pages/vitto-avatar-dev/vitto-avatar-dev.component.ts
Route: http://localhost:6300/demos/vitto-avatar
Each slice adds its controls to the demo page:
| Slice | Demo Addition |
|---|---|
| 1 | Preset dropdown, color pickers (9 colors), geometry sliders (12 values) |
| 2 | "Expressiveness" slider, checkboxes per Disney principle, transition speed control |
| 3 | 8 new mood cards in the grid (auto from MOOD_DEFINITIONS) |
| 4 | Accessory toggle panel (checkboxes per accessory, auto-trigger on/off toggle) |
| 5 | Environment toggles per layer (weather, stage, floating, ground) |
| 6 | N/A (backend-only) |
vitto-chat Integration
| Slice | Chat Change |
|---|---|
| 1 | VittoAvatarComponent accepts [characterConfig] input. Load from backend at init. |
| 2 | Automatic — Disney behaviors enhance all existing mood transitions |
| 3 | VittoEmotionService updated with new heuristics + PHASE_MOOD_MAP additions |
| 4 | Accessories auto-triggered by mood. No chat code change needed beyond renderer. |
| 5 | Environment auto-triggered by mood. Same as above. |
| 6 | Inference returns new moods. MoodInferenceService updated. |
Testing Strategy
Each slice is self-validating on the demo page:
- Slice 1: Switch presets → character colors/proportions change, animations unchanged
- Slice 2: Trigger mood transitions → observe anticipation wind-up, ear/head overshoot, staggered movement
- Slice 3: Click new mood cards → see 8 new expressions with correct particles
- Slice 4: Toggle accessories → see glasses/hat/coffee appear, bounce with head movement
- Slice 5: Trigger moods → see rain, sunshine, floating gears, ground flowers
- Slice 6: Send chat messages → backend infers new moods correctly
Risk Mitigations
| Risk | Mitigation |
|---|---|
| Overlapping action breaks transition smoothness | Per-group tT still uses same easing; if jerky, fall back to single tT with per-group speed multiplier |
| Accessories clip with head/body | Each accessory has z-index (draw order) and clip-safe bounds; demo page shows bounds overlay for debugging |
| Too many particles = frame drops | Particle pool capped at 100; profile on demo page; environment effects have per-frame budget |
| New moods confuse LLM inference | Inference prompt has explicit examples per mood; heuristic fallback always works |
| CharacterConfig bloat | Config is flat object (~26 fields), not nested. Serializes to <1KB JSON |