aaavatars 1.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`, `mouth`, `accessory` and colours come from the name.
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,10 @@ 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", 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] }) }) }) })) })] }));
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: {
149
+ animationDelay: `${spec.blinkDelay}s`,
150
+ animationDuration: `${spec.blinkDuration}s`,
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] }) }) }) })) })] }));
161
152
  if (!badge)
162
153
  return svg;
163
154
  return (_jsxs("span", { className: className, style: {
@@ -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
@@ -13,14 +13,18 @@ export interface AvatarSpec {
13
13
  accent: string;
14
14
  accessory: Accessory;
15
15
  background: string;
16
+ blinkDelay: number;
17
+ blinkDuration: number;
16
18
  body: string;
17
19
  eyeColor: string;
18
20
  eyes: Eyes;
19
21
  mouth: Mouth;
20
22
  offsetX: number;
21
23
  offsetY: number;
24
+ phase: number;
22
25
  rotate: number;
23
26
  shape: Shape;
27
+ tempo: number;
24
28
  }
25
29
  export interface SpecOptions {
26
30
  accessory?: Accessory;
package/dist/spec.js CHANGED
@@ -12,18 +12,10 @@ export const EYES = ["round", "dot", "tall", "wide"];
12
12
  export const MOUTHS = ["none", "smile", "flat", "open"];
13
13
  export const ACCESSORIES = ["none", "antenna", "sprout", "hat", "glasses", "bowtie"];
14
14
  export const FILLS = ["solid", "gradient"];
15
- const HASHED_MOUTHS = ["none", "none", "none", "smile", "flat", "open"];
16
- const HASHED_ACCESSORIES = [
17
- "none",
18
- "none",
19
- "none",
20
- "none",
21
- "antenna",
22
- "sprout",
23
- "hat",
24
- "glasses",
25
- "bowtie",
26
- ];
15
+ const BLINK_MIN_SECONDS = 5;
16
+ const BLINK_SPREAD_SECONDS = 4;
17
+ const TEMPO_MIN = 0.85;
18
+ const TEMPO_SPREAD = 0.3;
27
19
  export const DEFAULT_COLORS = ["#264653", "#2A9D8F", "#E9C46A", "#F4A261", "#E76F51"];
28
20
  function hash(value) {
29
21
  let result = 2166136261;
@@ -44,6 +36,12 @@ function seededRandom(seed) {
44
36
  function pick(items, random) {
45
37
  return items[Math.floor(random() * items.length)];
46
38
  }
39
+ function roundTenth(value) {
40
+ return Math.round(value * 10) / 10;
41
+ }
42
+ function roundHundredth(value) {
43
+ return Math.round(value * 100) / 100;
44
+ }
47
45
  function signedRange(random, limit) {
48
46
  return Math.round((random() * 2 - 1) * limit);
49
47
  }
@@ -67,24 +65,30 @@ export function createSpec(name, options = {}) {
67
65
  const offsetX = signedRange(random, 6);
68
66
  const offsetY = signedRange(random, 5);
69
67
  const rotate = signedRange(random, 12);
70
- const mouth = pick(HASHED_MOUTHS, random);
71
- const accessory = pick(HASHED_ACCESSORIES, random);
72
68
  const accentCandidates = colors.filter((_, index) => index !== bodyIndex && index !== backgroundIndex);
73
69
  const accent = accentCandidates.length ? pick(accentCandidates, random) : colors[0];
70
+ const blinkDuration = roundTenth(BLINK_MIN_SECONDS + random() * BLINK_SPREAD_SECONDS);
71
+ const blinkDelay = -roundTenth(random() * blinkDuration);
72
+ const tempo = roundHundredth(TEMPO_MIN + random() * TEMPO_SPREAD);
73
+ const phase = roundHundredth(random() * 0.99);
74
74
  const background = options.background ?? colors[backgroundIndex];
75
75
  const body = options.body ?? colors[bodyIndex];
76
76
  return {
77
77
  accent,
78
- accessory: options.accessory ?? accessory,
78
+ accessory: options.accessory ?? "none",
79
79
  background,
80
+ blinkDelay,
81
+ blinkDuration,
80
82
  body,
81
83
  eyeColor: relativeLuminance(body) > 0.4 ? "#141414" : "#FFFFFF",
82
84
  eyes: options.eyes ?? eyes,
83
- mouth: options.mouth ?? mouth,
85
+ mouth: options.mouth ?? "none",
84
86
  offsetX,
85
87
  offsetY,
88
+ phase,
86
89
  rotate,
87
90
  shape: options.shape ?? shape,
91
+ tempo,
88
92
  };
89
93
  }
90
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 4.6s 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%,93%,100%{transform:scaleY(1)}96%{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
@@ -9,18 +9,18 @@ export const AVATAR_CSS = `
9
9
  .aaa[data-interactive]{cursor:grab;touch-action:none}
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
- .aaa-blink{animation:aaa-blink 4.6s 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}
12
+ .aaa-blink{animation:aaa-blink 7s 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}
@@ -44,7 +44,8 @@ export const AVATAR_CSS = `
44
44
  .aaa[data-status=sleeping] .aaa-accessory:is([data-accessory=hat],[data-accessory=glasses]){transform:translateY(5px)}
45
45
  .aaa[data-blink=off] .aaa-blink{animation:none}
46
46
  .aaa[data-motion=off] *{animation:none!important}
47
- @keyframes aaa-blink{0%,93%,100%{transform:scaleY(1)}96%{transform:scaleY(.1)}}
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": "1.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",