Animal SVG¶
The <animal-svg> web component renders the map view's animal companion
(cat/dog/raccoon/parrot/snake by default; any registered animal). It is a
standalone web component β no React, no build step, no dependencies β with
animals that self-register via a small JS API. The element re-renders on
attribute change.
Source lives at
custom_components/eufy_vacuum/frontend/animal-svg/;
this document is the contract reference for two audiences:
- Integration authors who use
<animal-svg>to express device state. - Creature-pack authors who add new animals.
Sharing an animal in the gallery? Start with the descriptor guide β
Authoring an animal β submissions are
declarative data, not JavaScript. This document is the runtime register()
contract those descriptors compile down to (and the maintainer raw-JS path).
If you are using or extending the eufy_vacuum panel's map view, jump to
Integration contract β it documents exactly what the
panel passes to <animal-svg> (and what it does not pass).
Files¶
animal-svg/
βββ animal-svg.js custom element + registry + shared keyframes
βββ manifest.js loads animal-svg.js then each animal file
βββ animals/
β βββ cat.js
β βββ dog.js
β βββ fox.js community animal (generated from gallery/animals/fox.json)
β βββ raccoon.js
β βββ parrot.js
β βββ snake.js
β βββ mittens.js memorial tribute (see "Memorial animals")
β βββ index.json auto-generated at HA startup; enumerates the *.js above
βββ src/ declarative descriptors (cat.json, dog.json, β¦) that compile to animals/*.js
βββ demo.html open in a browser to verify everything works
The set above is representative, not fixed β any community animal self-registers
from its own animals/*.js file and is enumerated in the auto-generated
animals/index.json (see Adding / removing / editing an animal).
fox.js is one such community-generated quadruped.
Install¶
When this folder is part of the eufy_vacuum integration, the integration
serves it at /eufy_vacuum/frontend/animal-svg/ automatically β nothing else
to do. The panel card loads manifest.js from that path during its first
render.
To use the component standalone (outside the eufy_vacuum panel), drop the
folder into <config>/www/animal-svg/, then in Settings β Dashboards β
Resources add the resource:
- URL:
/local/animal-svg/manifest.js - Type: JavaScript Module
Then anywhere in a card:
Attributes¶
| Attribute | Values | Default | Behaviour |
|---|---|---|---|
animal |
cat, dog, raccoon, parrot, snake, or any registered animal |
cat |
Triggers full re-render |
pose |
animating, standing, curled, alert, walking, warning |
standing |
Triggers full re-render |
width |
any CSS length | 360px |
Applied to host (:host { width: ... }); observed, triggers full re-render |
height |
any CSS length | 240px |
Applied to host; observed, triggers full re-render |
battery-state |
good, mid, warn, low, charging |
absent β‘ good |
Pure CSS β re-targets --animal-eye to the matching framework default (good=green, mid=yellow, warn=orange, low=red, charging=blue). Themes can override via --evcc-animal-eye-* tokens. charging additionally drives a gentle brightness pulse on elements tagged .animal-eyes. |
animal, pose, width, and height are all observed and trigger a full
re-render via attributeChangedCallback. Only battery-state works without
re-rendering β it is implemented as five :host([battery-state="X"])
CSS rules plus a keyframe animation, so flipping through values is essentially
free.
From JS you can also use el.setAnimal(name) / el.setPose(pose).
Integration contract¶
This is the specific contract the eufy_vacuum panel's map view honors β
the only place <animal-svg> is invoked from this integration. Creature-pack
authors targeting this panel can rely on every point below.
What the panel passes¶
The panel renders exactly one <animal-svg> per map view, mounted in
.evcc-map-animal and positioned absolutely on the map by % coordinates
(driven by the same per-room anchor system that the legacy presence dot used).
It sets these attributes and nothing else:
| Attribute | Source on the panel side |
|---|---|
animal |
state.mapAnimalSelection() β user-configurable, default "cat" |
pose |
Derived from the HA vacuum entity's state (see mapping below) |
width |
64 * state.mapAnimalScale() (user-configurable, default 1.0Γ) |
height |
44 * state.mapAnimalScale() |
battery-state |
state.batteryState() β resolved from battery % + charging into one of good / mid / warn / low / charging |
Things the panel deliberately does NOT pass directly to the element:
- No color attributes or inline color styles. Color customisation happens
through the theme token system (see Theme integration
below) β the card sets
--evcc-animal-*tokens on its host, the animal-svg shadow root picks them up via CSS custom-property inheritance, and the animal's per-definition defaults serve as fallbacks. - No event handlers. The wrapper div carries the click/drag handlers for
repositioning the anchor; the
<animal-svg>itself receives no click listeners from the panel. The component's built-in click-to-cycle-pose feature (if your custom animal implements one) is unused β pose is fully driven by vacuum state.
State β pose mapping¶
The stateβpose mapping lives in the card's map renderer (the shipped frontend bundle) and uses the canonical HA vacuum-platform state vocabulary (brand-agnostic):
| HA vacuum state | pose requested |
|---|---|
cleaning |
alert |
returning |
walking |
paused |
standing |
error |
warning |
docked / idle |
curled (+ a --pulse CSS modifier on the wrapper) |
| anything else / null | curled |
The panel only ever requests one of the six framework poses. It never makes up new pose names. Creature packs only need to implement the six poses the framework defines.
battery-state semantics¶
The panel sets battery-state to one of five values resolved by
state.batteryState():
| State | Trigger | Framework default eye color |
|---|---|---|
charging |
binary_sensor.<vacuum>_charging is on (overrides level-based bands) |
blue, pulsing |
low |
battery β€ 15% | red |
warn |
15% < battery β€ 25% | orange |
mid |
25% < battery β€ 50% | yellow |
good |
battery > 50% (or unavailable) | green |
It's orthogonal to pose β a docked vacuum gets pose="curled"
regardless of which battery-state band applies. A docked vacuum at full
battery gets pose="curled" + battery-state="good". A docked vacuum
actively charging gets pose="curled" + battery-state="charging".
Implementation: five :host([battery-state="X"]) rules swap --animal-eye
to a state-specific color. The charging value additionally drives a
brightness pulse via the evcc-animal-eye-pulse keyframe on any element
tagged with the .animal-eyes class. The framework auto-tags the eye
group in quadruped/parrot types; custom animals add the class
themselves.
Themes override any of the five colors via the corresponding theme token:
| State | Theme token |
|---|---|
good |
--evcc-animal-eye-good |
mid |
--evcc-animal-eye-mid |
warn |
--evcc-animal-eye-warn |
low |
--evcc-animal-eye-low |
charging |
--evcc-animal-eye-charging |
Animals may also override these per-animal by including the matching
--animal-eye-* keys in their colors block (theme token still wins
over per-animal default).
How creature-pack authors should think about this¶
When you write a new animal for the eufy_vacuum panel, the only state inputs you can rely on are:
- The six framework poses (the panel will only request these)
- The five
battery-statevalues (the panel will only set one of these)
If your animal needs more state than that β fan speed, dock activity, error code, anything β you cannot get it. The contract is intentionally narrow. This protects the panel from being coupled to creature internals, and protects creatures from breaking when the panel's state model changes.
If you want to ship an animal that does more, it must be expressed entirely through pose Γ battery-state combinations.
Theme integration¶
Every color the animal-svg framework consumes is theme-overridable. The shadow root wraps each per-animal default as a two-level fallback:
Override priority, highest first:
- Per-animal theme token β
--evcc-animal-cat-fur,--evcc-animal-dog-eye-warn, etc. - Global animal token β
--evcc-animal-fur,--evcc-animal-eye-warn, etc. - Animal's own default β the value baked into the animal's
colorsblock.
A theme that just wants "all forest dark" sets the global tokens once. A theme that wants per-animal character (cat black, dog brown, parrot bright) sets per-animal tokens. Both layers can coexist β per-animal wins where set, global fills in everywhere else.
The theme editor surfaces these in grouped sections:
- Animal Companion β global tokens (5 eye-state colors + 9 palette fallbacks). Bulk-override every animal at once.
- Animal Companion β Cat / Dog / Raccoon / Parrot / Snake β per-animal
overrides. The full catalog is 14 tokens (5 eye-state + 9 palette), but each
sub-group lists only the tokens that animal actually themes, derived from
its
colorsblock. A full-palette animal shows all 14; a baked-down one (only--animal-eyedeclared) shows just its 6 live tokens instead of 8 inert no-ops. Each sub-group also has its own preview showing that animal across all five battery states, so it's clear what each token controls before you save. - Rainbow Bridge β
β memorial animals (registered memorial: true) group here instead, a tribute section apart from the everyday companions. See Memorial animals.
Wiring to HA state (standalone usage)¶
When using the component outside the eufy_vacuum panel, drive it from any HA template-capable card:
type: custom:html-template-card
content: |
<animal-svg
animal="cat"
pose="{{ 'walking' if is_state('vacuum.alfred','cleaning')
else 'curled' if is_state('vacuum.alfred','docked')
else 'warning' if is_state('vacuum.alfred','error')
else 'standing' }}"
battery-state="{% set b = states('sensor.alfred_battery') | int(0) -%}
{% if is_state('binary_sensor.alfred_charging','on') -%}charging
{%- elif b <= 15 -%}low
{%- elif b <= 25 -%}warn
{%- elif b <= 50 -%}mid
{%- else -%}good
{%- endif %}">
</animal-svg>
(The exact card you use to inject HTML is up to you β html-template-card,
config-template-card, or any custom card that handles templating.)
Adding / removing / editing an animal¶
This is the runtime mechanic β how the integration loads animals. To share a companion in the gallery, author a declarative descriptor instead (no JS) β see Authoring an animal; the raw-JS route below is for maintainers and your own install.
Each animal is its own self-registering file in animals/. The integration
generates animals/index.json at every HA startup by scanning that directory,
so manifest.js never needs to be edited.
To add one:
- Create
animals/myanimal.jsthat callsAnimalSVG.register('myanimal', { ... }). - Restart Home Assistant.
That's it. When the file's register() call fires, an
animal-svg-registered document event tells the integration's theme
system to rebuild its dynamic registry. Your animal appears automatically
in the map view's animal selector, in the theme editor with its own
sub-group (listing the tokens it actually themes, derived from its colors
block), and in the editor's per-animal preview pane. There are no src/
edits to make.
To remove one, delete its .js file and restart HA.
To edit colors only, change the colors block in the relevant file β no other
files need to change.
Definition shape¶
The first argument to AnimalSVG.register() is the animal's ID β the value used in the animal attribute and stored in theme configs. It must be unique across all loaded animal files. If two files register the same ID, the second call silently overwrites the first and only one animal appears. Use the filename (minus .js) as the ID to make collisions obvious.
AnimalSVG.register('myanimal', {
label: 'My Animal',
type: 'quadruped', // or 'parrot' or 'custom'
// memorial: true, // optional β groups under "Rainbow Bridge" (see "Memorial animals")
// CSS variables consumed by the SVG paths (HSL components, no `hsl()`).
colors: {
'--animal-fur': '0 0% 7%',
'--animal-fur-shadow': '0 0% 5%',
'--animal-fur-highlight': '0 0% 10%',
'--animal-eye': '142 71% 45%',
'--animal-eye-good': '142 71% 45%', // optional; per-animal default for battery>50
'--animal-eye-mid': '50 100% 55%', // optional; battery 25-50
'--animal-eye-warn': '30 100% 50%', // optional; battery 15-25
'--animal-eye-low': '0 80% 50%', // optional; battery β€15
'--animal-eye-charging': '210 100% 55%', // optional; pulses while charging
'--animal-pupil': '0 0% 7%',
'--animal-nose': '0 0% 33%',
'--animal-whisker': '0 0% 33%',
'--animal-ear-inner': '0 0% 10%',
'--animal-white-tip': '0 0% 100%',
},
// For `quadruped` and `parrot` types, supply SVG-fragment strings.
// The host element wraps each in a transform group so pose animations apply.
parts: {
body: '<path .../>',
frontLeftLeg: '<g>...</g>',
frontRightLeg: '<g>...</g>',
backLeftLeg: '<g>...</g>',
backRightLeg: '<g>...</g>',
tail: '<g>...</g>',
head: '<g>...</g>',
eyes: '<g>...</g>',
face: '<g>...</g>',
warning: '<g>...</g>', // additive overlay rendered only in warning pose
extra: '<g>...</g>', // optional, drawn behind everything (e.g. perch)
},
// Parrot only: shown only in walking (flight) pose.
wingLeft: '<g class="f-wing-l">...</g>',
wingRight: '<g class="f-wing-r">...</g>',
});
Memorial animals (Rainbow Bridge)¶
Set memorial: true on a definition to mark it a tribute (e.g. a baked-fur
memorial like mittens.js). The flag is orthogonal to type β a memorial
keeps its body plan (Mittens is still a quadruped) β and changes only how the
animal is presented:
- Theme editor β it groups under a Rainbow Bridge section
(
Rainbow Bridge β <Name>) instead of Animal Companion, a tribute area set apart from the everyday companions. - Animal picker (map view) β it appears in a
π Rainbow Bridge<optgroup>, below the regular animals. - Token list β like any animal, only the tokens it actually themes are
listed (derived from its
colorsblock). A memorial typically bakes its fur and markings as literalhsl()directly in the parts and leaves just--animal-eyedynamic (the battery-state system re-targets it), so the editor shows only the eye base + the 5 battery-state bands.
To add one: register with memorial: true, bake the colors you don't want
themed into the parts, and keep only the keys you do want themeable in the
colors block. No framework or src/ edits β the editor and picker derive the
grouping + token list from the flag and the colors block automatically.
Coordinate space¶
ViewBox is -10 -10 500 340. Quadruped anatomy is anchored around:
| Part | Approx. centre |
|---|---|
| Head | (140, 140) |
| Eyes | (145, 117) |
| Body | (250, 200) |
| Tail base | (340, 180) |
| Front-left leg | (166, 198) |
| Front-right leg | (194, 198) |
| Back-left leg | (303, 198) |
| Back-right leg | (332, 195) |
Parrot anatomy:
| Part | Approx. centre |
|---|---|
| Head | (220, 140) |
| Eyes | (218, 108) |
| Body | (258, 244) (perched) / (258, 190) (warning) |
| Tail base | (320, 225) |
| Wings | left (210, 155), right (310, 155) |
| Perch | y = 278 |
These are the transform-origin anchors used by the shared keyframes.
Following them keeps animations consistent. You can deviate, but pose
transforms (rotate/translate) will pivot around those points regardless.
Warning-pose responsibility¶
For quadruped and parrot animals, the framework handles the warning pose
through the standard pipeline: it applies head/body/tail transforms and
toggles the parts.warning overlay group (if you supply one). You don't have
to do anything special β just author the warning paths into parts.warning
and they'll appear in warning pose, hidden in every other pose.
For custom animals (see next section), warning is the renderer's
responsibility entirely. The framework hands you the pose string and walks
away. The reference implementation is animals/snake.js, which maps the
framework's warning pose to its own internal 'warning' mode and renders
a coiled-strike posture procedurally.
The rule: declarative animals get the warning pipeline for free.
Procedural (type: 'custom') animals own the warning rendering themselves.
Lower-leg knee folding¶
For curling and walking knee-flex animations to fire on a quadruped, the lower half of each leg must be wrapped in a group with one of these classes (pick the namespace that does not collide with another animal):
- Front-left lower:
xxx-fl-lower - Front-right lower:
xxx-fr-lower - Back-left lower:
xxx-bl-lower - Back-right lower:
xxx-br-lower
Built-in namespaces are cat-, dog-, rac-. To add a new namespace, edit
the selector lists in animal-svg.js (.pose-animating .xxx-fl-lower, ...
and .pose-walking .xxx-fl-lower, ...).
Custom (procedural) animals¶
For animals like the snake that need procedural geometry β anything the
quadruped or parrot parts pipeline can't express β use type: 'custom' and
supply a render(svg, pose) function.
The host gives you the live <svg> element. Build DOM into it however you
want. Return a cleanup function that the host will call on pose change or
disconnect β at minimum, cancel any requestAnimationFrame and remove your
nodes.
AnimalSVG.register('myproc', {
label: 'Procedural', type: 'custom',
colors: { ... },
render(svg, pose) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
svg.appendChild(g);
let raf = requestAnimationFrame(function tick(t) {
// ...mutate g based on t and pose...
raf = requestAnimationFrame(tick);
});
return () => { cancelAnimationFrame(raf); g.remove(); };
},
});
The framework still honors battery-state for custom animals β the five
:host([battery-state="X"]) rules that swap --animal-eye apply
regardless of type, so any element your render callback creates with
fill="hsl(var(--animal-eye))" responds automatically. To opt elements
into the charging pulse, tag them class="animal-eyes" (the snake does
this on its eye circles). For anything more elaborate, add your own CSS
scoped to :host([battery-state="charging"]) .your-class or read the
attribute inside your render callback.
Contributing or making your own¶
The bundled animals are placeholder quality β built to validate the framework, not to look polished. Two guides cover making your own:
- Authoring an animal β the public path: a declarative descriptor (sanitised SVG + colour tokens) that the intake validates and generates the runtime module from. This is how shared gallery companions are submitted β no hand-written JavaScript.
- Mascot authoring β the maintainer /
runtime path: adding
animals/<id>.jsdirectly (register(),type: 'custom'), reviewed as code. It also carries the craft standards (pose silhouette, palette intent, stroke hierarchy, warning expression) that apply to both paths.
Read the relevant one before opening a PR or shipping a custom animal for your own install β this doc tells you what works; those tell you what looks finished and which path is safe to share.
Debugging¶
Open demo.html in a browser (use the HA file editor or any static-file
viewer that can serve /config/www/ or /eufy_vacuum/frontend/). The page
builds a grid of every registered animal and lets you scrub through every
pose.
If you see "animal-svg: unknown animal" in the host element, the registration
file did not run β check the browser console for a load error in manifest.js.