aaavatars 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ await avatar.current?.play("done");
40
40
  | `blink` / `look` / `motion` | turn blinking, pointer following or looping off |
41
41
  | `interactive` | click, double-click, drag-to-squish and press-and-hold reactions |
42
42
 
43
- Unset `shape`, `eyes` and colours come from the name. `mouth` and `accessory` stay off until you set them. Each name blinks on its own rhythm, in CSS only.
43
+ Unset `shape`, `eyes` and colours come from the name. `mouth` and `accessory` stay off until you set them. Each name blinks, drifts and loops at its own slightly different tempo and phase, in CSS only. With `look`, eyes follow a mouse or pen anywhere on the page through one shared listener, updated once per frame.
44
44
 
45
45
  `play(event)` accepts `acknowledge`, `handoff`, `done`, `success`, `failure`, `surprise`, `wake` and resolves when the animation ends. Under `prefers-reduced-motion`, or while the page is hidden, it resolves immediately.
46
46
 
package/dist/Avatar.js CHANGED
@@ -4,6 +4,7 @@ import { EVENT_ANIMATIONS, MOOD_SWAP_REST, MOOD_SWAP_SHOW, } from "./animations.
4
4
  import { SHAPE_GEOMETRY } from "./geometry.js";
5
5
  import { AccessoryShape, EyePair, MoodMouths, MouthShape, mouthSlot, } from "./parts.js";
6
6
  import { createSpec, shadeHex, } from "./spec.js";
7
+ import { watchPointer } from "./pointer.js";
7
8
  import { AVATAR_CSS } from "./styles.js";
8
9
  export const AVATAR_STATUSES = [
9
10
  "idle",
@@ -13,7 +14,6 @@ export const AVATAR_STATUSES = [
13
14
  "error",
14
15
  "sleeping",
15
16
  ];
16
- const LOOK_RANGE = 4;
17
17
  const DRAG_THRESHOLD = 3;
18
18
  const HOLD_MS = 600;
19
19
  const SQUISH_LIMIT = 40;
@@ -85,17 +85,12 @@ export const Avatar = forwardRef(function Avatar({ accessory, background, badge,
85
85
  return Promise.all(running.current.map((animation) => animation.finished)).then(() => undefined, () => undefined);
86
86
  }, []);
87
87
  useImperativeHandle(ref, () => ({ play }), [play]);
88
- function lookAt(event) {
89
- const bounds = event.currentTarget.getBoundingClientRect();
90
- const x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2 * LOOK_RANGE;
91
- const y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2 * LOOK_RANGE;
92
- event.currentTarget.style.setProperty("--aaa-look-x", `${x}px`);
93
- event.currentTarget.style.setProperty("--aaa-look-y", `${y}px`);
94
- }
95
- function resetLook(event) {
96
- event.currentTarget.style.removeProperty("--aaa-look-x");
97
- event.currentTarget.style.removeProperty("--aaa-look-y");
98
- }
88
+ useEffect(() => {
89
+ const svg = svgRef.current;
90
+ if (!tracksPointer || !svg || prefersReducedMotion())
91
+ return;
92
+ return watchPointer(svg);
93
+ }, [tracksPointer]);
99
94
  function startDrag(event) {
100
95
  event.currentTarget.setPointerCapture?.(event.pointerId);
101
96
  const state = {
@@ -150,14 +145,7 @@ export const Avatar = forwardRef(function Avatar({ accessory, background, badge,
150
145
  }
151
146
  void play(event.detail > 1 ? "surprise" : "acknowledge");
152
147
  }
153
- const svg = (_jsxs("svg", { ref: svgRef, "aria-label": label ? `${name}, ${label}` : name, className: className && !badge ? `aaa ${className}` : "aaa", "data-blink": blink ? undefined : "off", "data-interactive": interactive ? "" : undefined, "data-motion": motion ? undefined : "off", "data-status": status, height: size, role: "img", viewBox: "0 0 100 100", width: size, onClick: interactive ? handleClick : undefined, onPointerCancel: interactive ? endDrag : undefined, onPointerDown: interactive ? startDrag : undefined, onPointerLeave: tracksPointer ? resetLook : undefined, onPointerMove: tracksPointer || interactive
154
- ? (event) => {
155
- if (tracksPointer)
156
- lookAt(event);
157
- if (interactive)
158
- moveDrag(event);
159
- }
160
- : undefined, onPointerUp: interactive ? endDrag : undefined, children: [label ? _jsx("title", { children: label }) : null, _jsx("style", { children: AVATAR_CSS }), _jsxs("defs", { children: [framed ? (_jsx("clipPath", { id: `${uid}-frame`, children: _jsx("rect", { width: 100, height: 100, rx: radius }) })) : null, fill === "gradient" ? (_jsxs("linearGradient", { id: `${uid}-fill`, x1: "0", y1: "0", x2: "1", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: shadeHex(spec.body, 0.18) }), _jsx("stop", { offset: "100%", stopColor: shadeHex(spec.body, -0.22) })] })) : null, shading > 0 ? (_jsxs("radialGradient", { id: `${uid}-shade`, cx: "0.35", cy: "0.3", r: "0.85", children: [_jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: 0.55 * shading }), _jsx("stop", { offset: "45%", stopColor: "#ffffff", stopOpacity: 0 }), _jsx("stop", { offset: "100%", stopColor: "#000000", stopOpacity: 0.35 * shading })] })) : null] }), framed ? (_jsx("rect", { "data-frame": "", width: 100, height: 100, rx: radius, fill: spec.background })) : null, _jsx("g", { clipPath: framed ? `url(#${uid}-frame)` : undefined, children: showImage ? (_jsx("g", { ref: bodyRef, className: "aaa-react", children: _jsx("image", { "data-image": "", height: 100, href: src, preserveAspectRatio: "xMidYMid slice", width: 100, onError: () => setImageFailed(true) }) })) : (_jsx("g", { transform: placement, children: _jsx("g", { ref: squishRef, className: "aaa-squish", children: _jsx("g", { className: "aaa-body-motion", children: _jsxs("g", { ref: bodyRef, className: "aaa-react", children: [accessoryBehind ? (_jsx(AccessoryShape, { accent: spec.accent, accessory: spec.accessory, body: spec.body, geometry: geometry })) : null, _jsx("path", { d: geometry.path, fill: bodyFill, stroke: bodyFill, strokeLinejoin: "round", strokeWidth: 8 }), shading > 0 ? (_jsx("path", { d: geometry.path, fill: `url(#${uid}-shade)`, stroke: `url(#${uid}-shade)`, strokeLinejoin: "round", strokeWidth: 8 })) : null, _jsx("g", { ref: eyesRef, className: "aaa-eyes-react", fill: spec.eyeColor, children: _jsx("g", { className: "aaa-look", children: content === "initial" ? (_jsx("text", { dominantBaseline: "central", fontFamily: "system-ui, sans-serif", fontSize: 38, fontWeight: 700, textAnchor: "middle", x: 50, y: (geometry.top + geometry.bottom) / 2 + 2, children: name.trim().charAt(0).toUpperCase() })) : (_jsx("g", { className: "aaa-eyes-motion", children: _jsx("g", { className: "aaa-blink", style: {
148
+ const svg = (_jsxs("svg", { ref: svgRef, "aria-label": label ? `${name}, ${label}` : name, className: className && !badge ? `aaa ${className}` : "aaa", "data-blink": blink ? undefined : "off", "data-interactive": interactive ? "" : undefined, "data-motion": motion ? undefined : "off", "data-status": status, height: size, overflow: framed ? undefined : "visible", role: "img", style: { "--aaa-phase": spec.phase, "--aaa-tempo": spec.tempo }, viewBox: "0 0 100 100", width: size, onClick: interactive ? handleClick : undefined, onPointerCancel: interactive ? endDrag : undefined, onPointerDown: interactive ? startDrag : undefined, onPointerMove: interactive ? moveDrag : undefined, onPointerUp: interactive ? endDrag : undefined, children: [label ? _jsx("title", { children: label }) : null, _jsx("style", { children: AVATAR_CSS }), _jsxs("defs", { children: [framed ? (_jsx("clipPath", { id: `${uid}-frame`, children: _jsx("rect", { width: 100, height: 100, rx: radius }) })) : null, fill === "gradient" ? (_jsxs("linearGradient", { id: `${uid}-fill`, x1: "0", y1: "0", x2: "1", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: shadeHex(spec.body, 0.18) }), _jsx("stop", { offset: "100%", stopColor: shadeHex(spec.body, -0.22) })] })) : null, shading > 0 ? (_jsxs("radialGradient", { id: `${uid}-shade`, cx: "0.35", cy: "0.3", r: "0.85", children: [_jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: 0.55 * shading }), _jsx("stop", { offset: "45%", stopColor: "#ffffff", stopOpacity: 0 }), _jsx("stop", { offset: "100%", stopColor: "#000000", stopOpacity: 0.35 * shading })] })) : null] }), framed ? (_jsx("rect", { "data-frame": "", width: 100, height: 100, rx: radius, fill: spec.background })) : null, _jsx("g", { clipPath: framed ? `url(#${uid}-frame)` : undefined, children: showImage ? (_jsx("g", { ref: bodyRef, className: "aaa-react", children: _jsx("image", { "data-image": "", height: 100, href: src, preserveAspectRatio: "xMidYMid slice", width: 100, onError: () => setImageFailed(true) }) })) : (_jsx("g", { transform: placement, children: _jsx("g", { ref: squishRef, className: "aaa-squish", children: _jsx("g", { className: "aaa-body-motion", children: _jsxs("g", { ref: bodyRef, className: "aaa-react", children: [accessoryBehind ? (_jsx(AccessoryShape, { accent: spec.accent, accessory: spec.accessory, body: spec.body, geometry: geometry })) : null, _jsx("path", { d: geometry.path, fill: bodyFill, stroke: bodyFill, strokeLinejoin: "round", strokeWidth: 8 }), shading > 0 ? (_jsx("path", { d: geometry.path, fill: `url(#${uid}-shade)`, stroke: `url(#${uid}-shade)`, strokeLinejoin: "round", strokeWidth: 8 })) : null, _jsx("g", { ref: eyesRef, className: "aaa-eyes-react", fill: spec.eyeColor, children: _jsx("g", { className: "aaa-look", children: content === "initial" ? (_jsx("text", { dominantBaseline: "central", fontFamily: "system-ui, sans-serif", fontSize: 38, fontWeight: 700, textAnchor: "middle", x: 50, y: (geometry.top + geometry.bottom) / 2 + 2, children: name.trim().charAt(0).toUpperCase() })) : (_jsx("g", { className: "aaa-eyes-motion", children: _jsx("g", { className: "aaa-blink", style: {
161
149
  animationDelay: `${spec.blinkDelay}s`,
162
150
  animationDuration: `${spec.blinkDuration}s`,
163
151
  }, children: _jsx(EyePair, { eyes: spec.eyes, gap: geometry.eyeGap, y: geometry.eyeY }) }) })) }) }), hasMouth ? (_jsxs("g", { className: "aaa-look", children: [_jsx("g", { className: "aaa-mouth-rest", children: renderMouth ? (renderMouth(slot)) : (_jsx(MouthShape, { mouth: spec.mouth, slot: slot })) }), _jsx(MoodMouths, { slot: slot })] })) : null, !accessoryBehind && (spec.accessory !== "glasses" || content === "face") ? (_jsx(AccessoryShape, { accent: spec.accent, accessory: spec.accessory, body: spec.body, geometry: geometry })) : null] }) }) }) })) })] }));
@@ -0,0 +1 @@
1
+ export declare function watchPointer(svg: SVGSVGElement): () => void;
@@ -0,0 +1,53 @@
1
+ const LOOK_RANGE = 7;
2
+ const FALLOFF = 240;
3
+ const watchers = new Set();
4
+ let pointer = null;
5
+ let frame = 0;
6
+ function aim() {
7
+ frame = 0;
8
+ const targets = [...watchers].map((svg) => ({ svg, bounds: svg.getBoundingClientRect() }));
9
+ for (const { svg, bounds } of targets) {
10
+ if (!pointer || bounds.bottom < 0 || bounds.top > window.innerHeight) {
11
+ svg.style.removeProperty("--aaa-look-x");
12
+ svg.style.removeProperty("--aaa-look-y");
13
+ continue;
14
+ }
15
+ const dx = pointer.x - (bounds.left + bounds.width / 2);
16
+ const dy = pointer.y - (bounds.top + bounds.height / 2);
17
+ const distance = Math.hypot(dx, dy) || 1;
18
+ const reach = (LOOK_RANGE * Math.min(distance, FALLOFF)) / FALLOFF;
19
+ svg.style.setProperty("--aaa-look-x", `${((dx / distance) * reach).toFixed(2)}px`);
20
+ svg.style.setProperty("--aaa-look-y", `${((dy / distance) * reach).toFixed(2)}px`);
21
+ }
22
+ }
23
+ function schedule() {
24
+ if (!frame)
25
+ frame = requestAnimationFrame(aim);
26
+ }
27
+ function handleMove(event) {
28
+ if (event.pointerType === "touch")
29
+ return;
30
+ pointer = { x: event.clientX, y: event.clientY };
31
+ schedule();
32
+ }
33
+ function handleLeave() {
34
+ pointer = null;
35
+ schedule();
36
+ }
37
+ export function watchPointer(svg) {
38
+ if (watchers.size === 0) {
39
+ window.addEventListener("pointermove", handleMove, { passive: true });
40
+ document.documentElement.addEventListener("pointerleave", handleLeave);
41
+ }
42
+ watchers.add(svg);
43
+ return () => {
44
+ watchers.delete(svg);
45
+ svg.style.removeProperty("--aaa-look-x");
46
+ svg.style.removeProperty("--aaa-look-y");
47
+ if (watchers.size === 0) {
48
+ window.removeEventListener("pointermove", handleMove);
49
+ document.documentElement.removeEventListener("pointerleave", handleLeave);
50
+ pointer = null;
51
+ }
52
+ };
53
+ }
package/dist/spec.d.ts CHANGED
@@ -21,8 +21,10 @@ export interface AvatarSpec {
21
21
  mouth: Mouth;
22
22
  offsetX: number;
23
23
  offsetY: number;
24
+ phase: number;
24
25
  rotate: number;
25
26
  shape: Shape;
27
+ tempo: number;
26
28
  }
27
29
  export interface SpecOptions {
28
30
  accessory?: Accessory;
package/dist/spec.js CHANGED
@@ -14,6 +14,8 @@ export const ACCESSORIES = ["none", "antenna", "sprout", "hat", "glasses", "bowt
14
14
  export const FILLS = ["solid", "gradient"];
15
15
  const BLINK_MIN_SECONDS = 5;
16
16
  const BLINK_SPREAD_SECONDS = 4;
17
+ const TEMPO_MIN = 0.85;
18
+ const TEMPO_SPREAD = 0.3;
17
19
  export const DEFAULT_COLORS = ["#264653", "#2A9D8F", "#E9C46A", "#F4A261", "#E76F51"];
18
20
  function hash(value) {
19
21
  let result = 2166136261;
@@ -37,6 +39,9 @@ function pick(items, random) {
37
39
  function roundTenth(value) {
38
40
  return Math.round(value * 10) / 10;
39
41
  }
42
+ function roundHundredth(value) {
43
+ return Math.round(value * 100) / 100;
44
+ }
40
45
  function signedRange(random, limit) {
41
46
  return Math.round((random() * 2 - 1) * limit);
42
47
  }
@@ -64,6 +69,8 @@ export function createSpec(name, options = {}) {
64
69
  const accent = accentCandidates.length ? pick(accentCandidates, random) : colors[0];
65
70
  const blinkDuration = roundTenth(BLINK_MIN_SECONDS + random() * BLINK_SPREAD_SECONDS);
66
71
  const blinkDelay = -roundTenth(random() * blinkDuration);
72
+ const tempo = roundHundredth(TEMPO_MIN + random() * TEMPO_SPREAD);
73
+ const phase = roundHundredth(random() * 0.99);
67
74
  const background = options.background ?? colors[backgroundIndex];
68
75
  const body = options.body ?? colors[bodyIndex];
69
76
  return {
@@ -78,8 +85,10 @@ export function createSpec(name, options = {}) {
78
85
  mouth: options.mouth ?? "none",
79
86
  offsetX,
80
87
  offsetY,
88
+ phase,
81
89
  rotate,
82
90
  shape: options.shape ?? shape,
91
+ tempo,
83
92
  };
84
93
  }
85
94
  export function shadeHex(hex, amount) {
package/dist/styles.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const AVATAR_CSS = "\n.aaa-body-motion,.aaa-react,.aaa-eyes-react,.aaa-eyes-motion,.aaa-blink,.aaa-squish{transform-box:fill-box;transform-origin:center}\n.aaa-mouth-rest,.aaa-mood{transform-box:fill-box;transform-origin:center;transition:opacity .2s,transform .2s}\n.aaa-mood{opacity:0}\n.aaa-accessory{transform-box:fill-box;transform-origin:50% 100%;transition:transform .3s ease-out}\n.aaa-accessory[data-accessory=glasses],.aaa-accessory[data-accessory=bowtie]{transform-origin:center}\n.aaa-squish{transition:transform .45s cubic-bezier(.34,1.56,.64,1)}\n.aaa-squish[data-dragging]{transition:none}\n.aaa[data-interactive]{cursor:grab;touch-action:none}\n.aaa[data-interactive]:active{cursor:grabbing}\n.aaa-look{transform:translate(var(--aaa-look-x,0px),var(--aaa-look-y,0px));transition:transform .25s ease-out}\n.aaa-blink{animation:aaa-blink 7s infinite}\n.aaa[data-status=idle] .aaa-body-motion{animation:aaa-bob 3.6s ease-in-out infinite}\n.aaa[data-status=thinking] .aaa-body-motion{animation:aaa-sway 3s ease-in-out infinite}\n.aaa[data-status=thinking] .aaa-eyes-motion{transform:translate(-5px,-6px);animation:aaa-ponder 2.4s ease-in-out infinite}\n.aaa[data-status=working] .aaa-body-motion{animation:aaa-bob .9s ease-in-out infinite}\n.aaa[data-status=working] .aaa-eyes-motion{animation:aaa-scan 1.1s ease-in-out infinite}\n.aaa[data-status=waiting] .aaa-body-motion{animation:aaa-hop 1.6s ease-out infinite}\n.aaa[data-status=waiting] .aaa-eyes-motion{transform:scale(1.18)}\n.aaa[data-status=error] .aaa-body-motion{transform:rotate(-10deg)}\n.aaa[data-status=error] .aaa-eyes-motion{transform:scaleY(.25)}\n.aaa[data-status=error] .aaa-blink{animation:none}\n.aaa[data-status=sleeping] .aaa-body-motion{animation:aaa-breathe 4.2s ease-in-out infinite}\n.aaa[data-status=sleeping] .aaa-eyes-motion{transform:translateY(2px) scaleY(.12)}\n.aaa[data-status=sleeping] .aaa-blink{animation:none}\n.aaa[data-status=working] .aaa-antenna-tip{animation:aaa-signal .9s ease-in-out infinite}\n.aaa[data-status=thinking] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=thinking] .aaa-mood-small{opacity:1}\n.aaa[data-status=thinking] .aaa-accessory:is([data-accessory=antenna],[data-accessory=sprout]){animation:aaa-acc-sway 2.4s ease-in-out infinite}\n.aaa[data-status=thinking] .aaa-accessory[data-accessory=glasses]{animation:aaa-glasses-think 2.4s ease-in-out infinite}\n.aaa[data-status=working] .aaa-mouth-rest{transform:scaleX(.7)}\n.aaa[data-status=working] .aaa-accessory[data-accessory=bowtie]{animation:aaa-wobble .9s ease-in-out infinite}\n.aaa[data-status=waiting] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=waiting] .aaa-mood-happy{opacity:1}\n.aaa[data-status=waiting] .aaa-accessory:not([data-accessory=glasses]){animation:aaa-wobble 1.6s ease-in-out infinite}\n.aaa[data-status=error] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=error] .aaa-mood-sad{opacity:1}\n.aaa[data-status=error] .aaa-accessory:is([data-accessory=antenna],[data-accessory=sprout]){transform:rotate(28deg)}\n.aaa[data-status=error] .aaa-accessory:is([data-accessory=hat],[data-accessory=glasses]){transform:translateY(3px) rotate(8deg)}\n.aaa[data-status=error] .aaa-accessory[data-accessory=bowtie]{transform:rotate(18deg)}\n.aaa[data-status=sleeping] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=sleeping] .aaa-mood-o{opacity:1;animation:aaa-snore 4.2s ease-in-out infinite}\n.aaa[data-status=sleeping] .aaa-accessory:is([data-accessory=antenna],[data-accessory=sprout]){transform:rotate(-35deg)}\n.aaa[data-status=sleeping] .aaa-accessory:is([data-accessory=hat],[data-accessory=glasses]){transform:translateY(5px)}\n.aaa[data-blink=off] .aaa-blink{animation:none}\n.aaa[data-motion=off] *{animation:none!important}\n@keyframes aaa-blink{0%,28%,32%,90%,94%,100%{transform:scaleY(1)}30%,92%{transform:scaleY(.1)}}\n@keyframes aaa-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}\n@keyframes aaa-sway{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(4deg)}}\n@keyframes aaa-ponder{0%,100%{transform:translate(-5px,-6px)}50%{transform:translate(5px,-6px)}}\n@keyframes aaa-scan{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}\n@keyframes aaa-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.035,.97)}}\n@keyframes aaa-signal{0%,100%{opacity:1}50%{opacity:.25}}\n@keyframes aaa-acc-sway{0%,100%{transform:rotate(-8deg)}50%{transform:rotate(8deg)}}\n@keyframes aaa-wobble{0%,100%{transform:rotate(-12deg)}50%{transform:rotate(12deg)}}\n@keyframes aaa-glasses-think{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}\n@keyframes aaa-snore{0%,100%{transform:scale(.7)}50%{transform:scale(1.1)}}\n@keyframes aaa-hop{0%,55%,100%{transform:translateY(0)}68%{transform:translateY(-7px)}80%{transform:translateY(0)}90%{transform:translateY(-3px)}}\n@media (prefers-reduced-motion:reduce){.aaa *{animation:none!important;transition:none!important}}\n";
1
+ export declare const AVATAR_CSS = "\n.aaa-body-motion,.aaa-react,.aaa-eyes-react,.aaa-eyes-motion,.aaa-blink,.aaa-squish{transform-box:fill-box;transform-origin:center}\n.aaa-mouth-rest,.aaa-mood{transform-box:fill-box;transform-origin:center;transition:opacity .2s,transform .2s}\n.aaa-mood{opacity:0}\n.aaa-accessory{transform-box:fill-box;transform-origin:50% 100%;transition:transform .3s ease-out}\n.aaa-accessory[data-accessory=glasses],.aaa-accessory[data-accessory=bowtie]{transform-origin:center}\n.aaa-squish{transition:transform .45s cubic-bezier(.34,1.56,.64,1)}\n.aaa-squish[data-dragging]{transition:none}\n.aaa[data-interactive]{cursor:grab;touch-action:none}\n.aaa[data-interactive]:active{cursor:grabbing}\n.aaa-look{transform:translate(var(--aaa-look-x,0px),var(--aaa-look-y,0px));transition:transform .25s ease-out}\n.aaa-blink{animation:aaa-blink 7s infinite}\n.aaa[data-status=idle] .aaa-body-motion{animation:aaa-drift calc(11s*var(--aaa-tempo,1)) ease-in-out calc(-11s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=thinking] .aaa-body-motion{animation:aaa-sway calc(3s*var(--aaa-tempo,1)) ease-in-out calc(-3s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=thinking] .aaa-eyes-motion{transform:translate(-5px,-6px);animation:aaa-ponder calc(2.4s*var(--aaa-tempo,1)) ease-in-out calc(-2.4s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=working] .aaa-body-motion{animation:aaa-bob calc(0.9s*var(--aaa-tempo,1)) ease-in-out calc(-0.9s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=working] .aaa-eyes-motion{animation:aaa-scan calc(1.1s*var(--aaa-tempo,1)) ease-in-out calc(-1.1s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=waiting] .aaa-body-motion{animation:aaa-hop calc(1.6s*var(--aaa-tempo,1)) ease-out calc(-1.6s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=waiting] .aaa-eyes-motion{transform:scale(1.18)}\n.aaa[data-status=error] .aaa-body-motion{transform:rotate(-10deg)}\n.aaa[data-status=error] .aaa-eyes-motion{transform:scaleY(.25)}\n.aaa[data-status=error] .aaa-blink{animation:none}\n.aaa[data-status=sleeping] .aaa-body-motion{animation:aaa-breathe calc(4.2s*var(--aaa-tempo,1)) ease-in-out calc(-4.2s*var(--aaa-phase,0)) infinite}\n.aaa[data-status=sleeping] .aaa-eyes-motion{transform:translateY(2px) scaleY(.12)}\n.aaa[data-status=sleeping] .aaa-blink{animation:none}\n.aaa[data-status=working] .aaa-antenna-tip{animation:aaa-signal .9s ease-in-out infinite}\n.aaa[data-status=thinking] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=thinking] .aaa-mood-small{opacity:1}\n.aaa[data-status=thinking] .aaa-accessory:is([data-accessory=antenna],[data-accessory=sprout]){animation:aaa-acc-sway 2.4s ease-in-out infinite}\n.aaa[data-status=thinking] .aaa-accessory[data-accessory=glasses]{animation:aaa-glasses-think 2.4s ease-in-out infinite}\n.aaa[data-status=working] .aaa-mouth-rest{transform:scaleX(.7)}\n.aaa[data-status=working] .aaa-accessory[data-accessory=bowtie]{animation:aaa-wobble .9s ease-in-out infinite}\n.aaa[data-status=waiting] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=waiting] .aaa-mood-happy{opacity:1}\n.aaa[data-status=waiting] .aaa-accessory:not([data-accessory=glasses]){animation:aaa-wobble 1.6s ease-in-out infinite}\n.aaa[data-status=error] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=error] .aaa-mood-sad{opacity:1}\n.aaa[data-status=error] .aaa-accessory:is([data-accessory=antenna],[data-accessory=sprout]){transform:rotate(28deg)}\n.aaa[data-status=error] .aaa-accessory:is([data-accessory=hat],[data-accessory=glasses]){transform:translateY(3px) rotate(8deg)}\n.aaa[data-status=error] .aaa-accessory[data-accessory=bowtie]{transform:rotate(18deg)}\n.aaa[data-status=sleeping] .aaa-mouth-rest{opacity:0}\n.aaa[data-status=sleeping] .aaa-mood-o{opacity:1;animation:aaa-snore 4.2s ease-in-out infinite}\n.aaa[data-status=sleeping] .aaa-accessory:is([data-accessory=antenna],[data-accessory=sprout]){transform:rotate(-35deg)}\n.aaa[data-status=sleeping] .aaa-accessory:is([data-accessory=hat],[data-accessory=glasses]){transform:translateY(5px)}\n.aaa[data-blink=off] .aaa-blink{animation:none}\n.aaa[data-motion=off] *{animation:none!important}\n@keyframes aaa-blink{0%,28%,32%,90%,94%,100%{transform:scaleY(1)}30%,92%{transform:scaleY(.1)}}\n@keyframes aaa-drift{0%,100%{transform:translate(0,0) rotate(0deg) scale(1)}20%{transform:translate(4px,-3px) rotate(5deg) scale(1.02,.98)}45%{transform:translate(-2px,-5px) rotate(-3deg) scale(.98,1.02)}70%{transform:translate(-5px,-1px) rotate(-6deg) scale(1.02,.98)}85%{transform:translate(-1px,1px) rotate(2deg) scale(1)}}\n@keyframes aaa-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}\n@keyframes aaa-sway{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(4deg)}}\n@keyframes aaa-ponder{0%,100%{transform:translate(-5px,-6px)}50%{transform:translate(5px,-6px)}}\n@keyframes aaa-scan{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}\n@keyframes aaa-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.035,.97)}}\n@keyframes aaa-signal{0%,100%{opacity:1}50%{opacity:.25}}\n@keyframes aaa-acc-sway{0%,100%{transform:rotate(-8deg)}50%{transform:rotate(8deg)}}\n@keyframes aaa-wobble{0%,100%{transform:rotate(-12deg)}50%{transform:rotate(12deg)}}\n@keyframes aaa-glasses-think{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}\n@keyframes aaa-snore{0%,100%{transform:scale(.7)}50%{transform:scale(1.1)}}\n@keyframes aaa-hop{0%,55%,100%{transform:translateY(0)}68%{transform:translateY(-7px)}80%{transform:translateY(0)}90%{transform:translateY(-3px)}}\n@media (prefers-reduced-motion:reduce){.aaa *{animation:none!important;transition:none!important}}\n";
package/dist/styles.js CHANGED
@@ -10,17 +10,17 @@ export const AVATAR_CSS = `
10
10
  .aaa[data-interactive]:active{cursor:grabbing}
11
11
  .aaa-look{transform:translate(var(--aaa-look-x,0px),var(--aaa-look-y,0px));transition:transform .25s ease-out}
12
12
  .aaa-blink{animation:aaa-blink 7s infinite}
13
- .aaa[data-status=idle] .aaa-body-motion{animation:aaa-bob 3.6s ease-in-out infinite}
14
- .aaa[data-status=thinking] .aaa-body-motion{animation:aaa-sway 3s ease-in-out infinite}
15
- .aaa[data-status=thinking] .aaa-eyes-motion{transform:translate(-5px,-6px);animation:aaa-ponder 2.4s ease-in-out infinite}
16
- .aaa[data-status=working] .aaa-body-motion{animation:aaa-bob .9s ease-in-out infinite}
17
- .aaa[data-status=working] .aaa-eyes-motion{animation:aaa-scan 1.1s ease-in-out infinite}
18
- .aaa[data-status=waiting] .aaa-body-motion{animation:aaa-hop 1.6s ease-out infinite}
13
+ .aaa[data-status=idle] .aaa-body-motion{animation:aaa-drift calc(11s*var(--aaa-tempo,1)) ease-in-out calc(-11s*var(--aaa-phase,0)) infinite}
14
+ .aaa[data-status=thinking] .aaa-body-motion{animation:aaa-sway calc(3s*var(--aaa-tempo,1)) ease-in-out calc(-3s*var(--aaa-phase,0)) infinite}
15
+ .aaa[data-status=thinking] .aaa-eyes-motion{transform:translate(-5px,-6px);animation:aaa-ponder calc(2.4s*var(--aaa-tempo,1)) ease-in-out calc(-2.4s*var(--aaa-phase,0)) infinite}
16
+ .aaa[data-status=working] .aaa-body-motion{animation:aaa-bob calc(0.9s*var(--aaa-tempo,1)) ease-in-out calc(-0.9s*var(--aaa-phase,0)) infinite}
17
+ .aaa[data-status=working] .aaa-eyes-motion{animation:aaa-scan calc(1.1s*var(--aaa-tempo,1)) ease-in-out calc(-1.1s*var(--aaa-phase,0)) infinite}
18
+ .aaa[data-status=waiting] .aaa-body-motion{animation:aaa-hop calc(1.6s*var(--aaa-tempo,1)) ease-out calc(-1.6s*var(--aaa-phase,0)) infinite}
19
19
  .aaa[data-status=waiting] .aaa-eyes-motion{transform:scale(1.18)}
20
20
  .aaa[data-status=error] .aaa-body-motion{transform:rotate(-10deg)}
21
21
  .aaa[data-status=error] .aaa-eyes-motion{transform:scaleY(.25)}
22
22
  .aaa[data-status=error] .aaa-blink{animation:none}
23
- .aaa[data-status=sleeping] .aaa-body-motion{animation:aaa-breathe 4.2s ease-in-out infinite}
23
+ .aaa[data-status=sleeping] .aaa-body-motion{animation:aaa-breathe calc(4.2s*var(--aaa-tempo,1)) ease-in-out calc(-4.2s*var(--aaa-phase,0)) infinite}
24
24
  .aaa[data-status=sleeping] .aaa-eyes-motion{transform:translateY(2px) scaleY(.12)}
25
25
  .aaa[data-status=sleeping] .aaa-blink{animation:none}
26
26
  .aaa[data-status=working] .aaa-antenna-tip{animation:aaa-signal .9s ease-in-out infinite}
@@ -45,6 +45,7 @@ export const AVATAR_CSS = `
45
45
  .aaa[data-blink=off] .aaa-blink{animation:none}
46
46
  .aaa[data-motion=off] *{animation:none!important}
47
47
  @keyframes aaa-blink{0%,28%,32%,90%,94%,100%{transform:scaleY(1)}30%,92%{transform:scaleY(.1)}}
48
+ @keyframes aaa-drift{0%,100%{transform:translate(0,0) rotate(0deg) scale(1)}20%{transform:translate(4px,-3px) rotate(5deg) scale(1.02,.98)}45%{transform:translate(-2px,-5px) rotate(-3deg) scale(.98,1.02)}70%{transform:translate(-5px,-1px) rotate(-6deg) scale(1.02,.98)}85%{transform:translate(-1px,1px) rotate(2deg) scale(1)}}
48
49
  @keyframes aaa-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
49
50
  @keyframes aaa-sway{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(4deg)}}
50
51
  @keyframes aaa-ponder{0%,100%{transform:translate(-5px,-6px)}50%{transform:translate(5px,-6px)}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaavatars",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Deterministic 2D character avatars with a play() animation API",
5
5
  "keywords": [
6
6
  "avatar",