| name | chat-pet-sprite-creation |
| description | Use when creating or changing VS Code chat pet sprite art, sprite sheets, state animations, eye treatments, Stable/Insiders variants, or pet transitions under src/vs/workbench/contrib/chat/browser/widget/media/chatPet. |
Chat Pet Sprite Creation
Create pet sprites that feel like one continuous character rather than separate drawings. Start from the current pet body, preserve its visual anchor, and make every difference intentional.
Source of truth
- Sprite art:
src/vs/workbench/contrib/chat/browser/widget/media/chatPet/
- Runtime states, dimensions, timing, and transitions:
src/vs/workbench/contrib/chat/browser/widget/chatPetWidget.ts
- Eye geometry, facing, clipping, and motion CSS:
src/vs/workbench/contrib/chat/browser/widget/media/chatPet.css
- Unit tests:
src/vs/workbench/contrib/chat/test/browser/widget/chatPetWidget.test.ts
Read the current implementation before drawing. Existing dimensions and timings may have changed since this skill was written.
PR precedence and lineage
This skill distills the complete pet PR history. When rules conflict, use this precedence:
- Current code and tests
- The newest relevant open PR
- Newer merged PRs
- Older historical behavior
In particular, animation cleanup PR #330399 supersedes the old revive-sign flow and several earlier sprite/timing assumptions.
| PR | Durable learning |
|---|
| #327063 | The pet is one semantic button; sprite canvases, images, eyes, and effects are visual-only and aria-hidden. Resolve asset URLs through FileAccess. |
| #327412 | Model reactions as explicit states. Do not immediately repeat ordinary random reactions; sample rare transformations separately. |
| #327588 | Double-buffer sprite images and swap only after the pending image loads. Never blank the active sprite while a new source is loading. |
| #327696 | Stable/Insiders is a typed, persisted appearance choice. Keep variant naming and geometry systematic. |
| #327714 | Animated art is a horizontal sprite sheet plus a deliberate static reduced-motion PNG. Timing is runtime data, not image metadata. |
| #328334 | Persistent behavior such as “on the run” belongs in service state. Multi-phase animations advance from completion signals, not guessed delays. |
| #328480 | A drag release must not also trigger a click reaction. Gestures that share pointer events need explicit suppression/ownership. |
| #328530 | Waking consumes the interaction that woke the pet; it must not also trigger a random reaction. Reduced motion may skip the wake transition. |
| #329121 | Coalesce gaze updates, pause sprite timers while the document is hidden, and resume from elapsed time rather than replaying missed frames. |
| #329347 | Keep pixel-art pose changes in sprite frames and spatial motion in layout/CSS/physics. Complete falls from transition signals. |
| #329729 | Face the interaction before starting a reaction. Give each transient state an explicit lifetime and use real completion events for chained phases. |
| #329852 | Pointer and keyboard gestures need equivalent outcomes. Reduced motion preserves the state change while removing the travel animation. |
| #329867 | Attention animations are bounded; clapping stops after the confirmation-attention window even if the confirmation remains pending. |
| #330160 | Eyes are a composited runtime layer with tracking/blinking modes. Rapid facing changes can trigger a separate dizzy state. Wide art must respect live bounds. |
| #330275 | Physics uses the body footprint, bounded frame steps, and explicit impact moments. Recompute geometry when layout changes during flight. |
| #330399 | Latest animation contract: wide sleep/wake art, frame-aware blink composition, fixed-orientation decorations, and reverse despawn → forward respawn using one effect sheet. |
The logical pixel
The canonical art unit is one logical pixel:
8×8 source pixels
4×4 CSS pixels at the standard 96×96 source → 48×48 display scale
Core silhouettes, facial features, props, and repeated details must use the same logical-pixel scale. Do not make a new object from smaller or larger blocks simply to fit more detail.
Effects may deliberately subdivide the logical pixel—for example, small stars, bubbles, or curved motion—but the subdivision must:
- Serve a specific effect that cannot read on the core grid.
- Stay consistent throughout that effect.
- Never come from antialiasing or resampling.
Use nearest-neighbor rendering only. Keep transparent backgrounds and hard pixel edges. Do not introduce blur, feathered alpha, color interpolation, or accidental off-grid scaling.
Start from a baseline
Never redraw the pet body from memory.
- Use the current idle sprite as the baseline for resting, pointer-driven, or playful states.
- Use the current rendering sprite as the baseline for request/processing states.
- Copy the complete body first, then alter only the parts required by the new state.
This preserves:
- Body width and height
- Bottom baseline
- Antenna roots and spacing
- Highlight and shadow ramps
- Eye sockets
- Collision/interaction footprint
The first and last key poses must transition cleanly to idle or rendering without a body-position, silhouette, baseline, or scale pop.
Canvas and anchoring
The default source canvas is 96×96, or 12×12 logical pixels.
Use a larger canvas only when content intentionally extends outside the canonical body box:
- A terminal or button may need extra width.
- A note, star, or bubble may need extra height.
- The body must remain anchored exactly where it sits in the baseline sprite.
Do not center the body inside a wider frame. Keep the body in the canonical 96×96 region and let the prop/effect overhang. Declare every nonstandard source dimension in chatPetWidget.ts and update wide-sprite boundary handling.
The runtime collision and movement box belongs to the pet body, not to a decorative overhang.
Eyes
The standard open eye is 1×2 logical pixels:
8×16 source pixels
4×8 CSS pixels
Use a different eye shape only when the state communicates a special expression, such as sleep, dizzy, love, worry, or impact.
DOM eye layer
The DOM eye layer has two independent modes:
- Tracking supplies gaze, facing, blink, and drag expressions.
- Blinking can temporarily show DOM eyes for a non-tracking state and can vary by sprite frame.
States that use runtime gaze must use eye-less *-tracking-96 assets. States that use DOM blinking, such as typing, button press, or love in PR #330399, must also leave the corresponding pupil area available for the overlay.
When adding or changing a DOM-eye state:
- Keep the sprite eye sockets empty.
- Add cursor behavior to
doesChatPetStateTrackCursor when appropriate.
- Add blink behavior to
doesChatPetStateBlink(state, frameIndex) when appropriate.
- Verify the tall-eye blink, state-specific eye height, and pointer offsets in both facing directions.
- Verify frames that intentionally stop blinking, such as a held terminal pose.
Never combine baked pupils with the runtime eye overlay.
Baked-eye states
States with a fixed expression should bake the eyes into the art and opt out of cursor tracking. Preserve the standard eye position unless the expression intentionally changes it.
Stable and Insiders parity
Stable and Insiders are palette variants of the same animation.
They must have identical:
- Canvas dimensions
- Frame count and ordering
- Alpha mask and geometry
- Body anchor and overhang
- Static fallback pose
- Timing and iteration behavior
Create one geometry master and apply the two established palettes. Do not redraw variants independently.
Review the two variants side by side. Any shape difference is a bug unless the product explicitly requires it.
Facing and mirroring
Author one canonical facing direction and let the runtime mirror it for the other direction.
Test both directions because mirroring also changes where wide sprites overhang. Keep enough room for boundary correction near the left and right edges.
Decorations that must retain their screen orientation—such as text, musical notation, or directional symbols—must not be baked into a layer that the runtime mirrors. Model them as fixed-orientation decorations or render them separately.
Static asset and sprite sheet
Each animated state needs both variants of:
- A static PNG for reduced motion:
buddy-<state>-<variant>-<height>.png
- A horizontal sprite sheet:
buddy-<state>-<variant>-<height>.spritesheet.png
The sprite sheet contract is:
sheet width = frame width × frame count
sheet height = frame height
Every frame uses the same rectangle. Never shift frame boundaries or add per-frame padding.
Choose a meaningful static pose that communicates the state without motion. Do not assume the first animation frame is automatically the best reduced-motion fallback.
Layered accessories
Wearable accessories use a body-owned attachment rig and one palette-independent atlas per appearance under media/chatPet/accessories/. Do not export copies for individual runtime states or frames. Body animation metadata owns attachment movement; an accessory atlas only contains the few canonical shapes needed when the body geometry changes.
The default atlas is 256×192, divided into 64×64 cells. A hat whose supplied silhouette genuinely needs the full 12-logical-pixel body width may declare atlasCellSize: 96 and use a 384×288 atlas with 96×96 cells. Do not choose the wider tier merely to add detail; both tiers still use the same whole 8×8 logical pixels.
columns: upright | sleeping | impact | splat
row 0: head back layers
row 1: head front layers
row 2: right-eye front layers
An appearance may leave any cell transparent. For example, a plain hat leaves the eye row empty, while a combined Top Hat & Monocle appearance uses both head rows and the eye row. The airborne rig pose reuses the upright column. Add another canonical column only when an existing pose plus body-owned translation cannot preserve the intended silhouette.
Attachment tracks live in chatPetAccessoryRig.ts. Each track uses compressed frame spans with a canonical pose and independent head/right-eye anchors. A slot is omitted while a body-authored expression, prop, or complete silhouette replacement owns the same geometry, such as dizzy eyes, sunglasses, or the rare icon transformation. When frames inside one sheet bake different facing directions, the track marks those frames so the head accessory mirrors with the body before the whole pet's outer facing transform is applied. Update the body-owned track only when body geometry changes. Adding an ordinary accessory must not require editing tracks or renderer code.
Head-slot anchors include a one-logical-pixel wear offset so hats overlap the top of the head rather than resting on its silhouette. The love animation suppresses head accessories because its transformed head/antenna silhouette is the reaction. Treat slot visibility and fit as body-owned behavior, not per-hat exceptions.
Appearances that tuck the antennae under a larger hat opt into coversAntennae. The compositor uses body-owned occlusion bounds before drawing the hat front, so the behavior follows animation tracks and mirroring without painting body-colored cover pixels into each atlas.
Directional head accessories are authored for the canonical right-facing body and mirror with the complete pet canvas. A hat with directional structure should stay visually balanced over the head while expressing facing through a restrained one-logical-pixel cue in its brim, visor, lean, nose guard, or other asymmetric detail; do not shift the whole silhouette far off-center. Make asymmetric parts read correctly after mirroring. Identity-bound eye accessories may opt out of mirroring and use a direction-specific eye anchor so they remain on the same eye; the Top Hat & Monocle is the reference. Fixed-eye anchors must include the body origin shift for wide frames (frameWidth - 96). Review every appearance in both directions with the facing fixture.
Static reduced-motion art may represent a later frame of its animated sheet. Keep getChatPetReducedMotionRigFrame() aligned with the representative body frame; do not assume rig frame zero merely because the static PNG has one image frame.
Author every wearable part on the same 8×8 logical-pixel grid as the body. Brims, crowns, bands, rims, chains, highlights, and shadows must all be composed from whole aligned logical pixels; do not use diagonal polygons or source-pixel stair steps to imply curves. The small-effect exception does not apply to wearable accessories. Use the current Stable body sheets as geometry guides and validate that every alpha value is fully transparent or fully opaque. The runtime mirrors layers with the body; fixed-orientation body decorations are restored without erasing accessory pixels.
When changing the accessory source at runtime, keep the current composite visible until the replacement image loads and passes exact dimension validation. Redraw the current frame without restarting the body animation. A failed or malformed accessory source falls back to the body alone and must not repeatedly retry.
Animation design
Animate key poses, not noise.
- The trigger should be readable.
- The action should have a clear apex or strongest pose.
- The result should be readable before returning to the baseline.
- Holds are useful; duplicate motion is not.
Frame duration is part of the art. Define it in the matching *_FRAME_DURATIONS array in chatPetWidget.ts; do not encode or infer timing from the PNG.
Keep the transient-state duration consistent with the intended loops or one-shot playback. Explicitly choose whether the sheet:
- Plays once and holds its final pose
- Loops until the transient state ends
- Plays in reverse as part of another transition
Reversible effects
If one sheet is reused in reverse, as with respawn → despawn:
- Design both endpoints to work as starts and finishes.
- Check the effect in both directions.
- Avoid one-way visual cues that become nonsensical backward.
- Verify the reverse effect at its actual destination, not only on a neutral canvas.
PR #330399 is the reference pattern: the respawn burst plays backward to despawn at the bottom, then forward to respawn at the top. The old revive-sign interstitial is superseded.
Separate pose animation from spatial motion
Use sprite frames for changes to the pet or prop silhouette. Use CSS transforms or the physics loop for:
- Translation
- Rotation
- Enter/exit motion
- Drag resistance
- Search reveal/hide
- Falls, throws, rebounds, and impacts
Do not redraw every translated/rotated pose into the sheet. Conversely, do not use a CSS transform to fake a body deformation that should be readable in the pixel art.
Advance multi-phase behavior from deterministic completion signals:
- Sprite completion for one-shot sheets
animationend for CSS keyframes
transitionend/transitioncancel for falls
- Physics settlement for throws
Do not guess a completion time with an unrelated timeout.
Frame scheduling and restart behavior
Sprite timing uses elapsed-time scheduling at frame boundaries, not a display-refresh polling loop. A late callback recalculates the correct frame and shortens the next delay.
- Pause frame timers while the document is hidden.
- On visibility return, recompute from elapsed time.
- Finite animations hold their terminal frame.
- Reverse playback must use the reversed frame durations, not only reversed frame indices.
- Restart a loaded state without forcing a redundant image fetch.
- Force a style flush before re-adding a CSS animation class when a blink or motion must restart.
Environment and layering
Review the sprite in the same environment where it runs:
- Platform/input edge
- Transparent background
- Left and right workbench boundaries
- Platform clipping and occlusion
- Drag, fall, throw, and respawn motion
- Any speech bubble or fixed decoration
Pixels intended to pass behind the platform must be clipped or layered behind it. Do not leave body/effect pixels visible below an occluding platform.
Movement should preserve the body anchor until physics intentionally moves the whole pet. Decorative frame size must not change the physics target.
State and gesture ownership
The rendered state has a deliberate priority. Busy/task states must not be accidentally hidden by decorative reactions. Preserve the current order in getChatPetBaseState and the transient-state guards in getChatPetRenderedState.
Interaction rules:
- Dragging owns the pointer until release and suppresses the following click.
- A wake interaction wakes only.
- Yapping or other idle-only reactions yield to non-idle base states.
- Keyboard interaction is disabled while the pet is dead, pointer-controlled, airborne, or on the run.
- Dizzy consumes the direction-change gesture that triggered it.
- Confirmation clapping is an attention cue, not a permanent pending-state animation.
Rare interactions should be sampled independently from the ordinary non-repeating pool so adding a common reaction does not silently change an easter egg's probability.
Rendering and performance
Keep the current rendering architecture:
- Two sprite elements provide load-time double buffering.
- Compare an image's original source with
getAttribute('src'); .src may normalize VS Code resource URLs.
- Size the canvas at source-native frame dimensions and the CSS box at display dimensions.
- Keep
imageSmoothingEnabled = false and image-rendering: pixelated.
- Cache resolved sprite source descriptors per variant.
- Coalesce pointer gaze updates to one animation frame.
- Use
requestAnimationFrame for physics, but cap integration steps to prevent tunneling after a slow frame.
- Re-read throw geometry when layout changes during flight.
Runtime wiring
When adding a state, update every applicable surface:
- Add the state to
ChatPetState.
- Map the filename in
getChatPetSpriteName.
- Add frame durations in
getChatPetFrameDurations.
- Declare nonstandard source width/height constants.
- Register animated and reduced-motion sources in
getSpriteSources.
- Choose cursor-tracking and frame-aware blinking behavior.
- Choose one-shot, loop, reverse, or static iteration behavior.
- Add the transient-state duration and trigger.
- Add deterministic completion transitions for multi-phase states.
- Add facing, overhang, fixed-decoration, clipping, or CSS treatment.
- Add a localized ARIA status announcement for user-triggered interactions.
- Update accessible help when the gesture is discoverable/useful.
- Update focused unit tests.
Do not add art without completing the runtime contract.
Keep the visuals silent: images, canvases, eyes, and effects use empty alt text and aria-hidden. The button owns the accessible label, focusability, and localized status() announcements. Hidden, dead, or non-interactive states must leave the tab order.
Validation checklist
Art
Animation
Integration
Use the chat/chatPetAccessoryRig/chatPetAccessoryRig/CriticalPoses component fixture to review critical animated and reduced-motion poses, and AllAccessoriesFacing to compare every appearance in both directions.
Run the focused unit tests using the repository's unit-tests skill. At minimum, run the ChatPetWidget test suite.
Prefer exporting pure helpers for timing, geometry, random selection, and state precedence, then test them directly. Use fake timers for scheduler behavior. Assert exact frame-duration arrays so art and runtime timing cannot drift independently.
Optional public-preview update
The public showcase lives in the sibling vscode-chat-pet repository. Use its update-pet-previews skill after production sprites are final.
When replacing an existing APNG, prefer a new versioned filename in the README so GitHub and browser caches cannot keep showing the previous animation.