@heyramzi/ui 6.0.1 → 7.0.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/COMPONENTS.md +7 -26
- package/README.md +12 -5
- package/package.json +14 -16
- package/scripts/sync-shadcn.mjs +12 -3
- package/src/agents/agent-avatar.svelte +639 -0
- package/src/agents/agent-cast.svelte +173 -0
- package/src/agents/demo.svelte +78 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/types.ts +72 -0
- package/{LICENSE → src/agents/vendor/LICENSE} +1 -1
- package/src/agents/vendor/color.ts +155 -0
- package/src/agents/vendor/draw.ts +840 -0
- package/src/agents/vendor/engine.ts +845 -0
- package/src/agents/vendor/mesh.ts +838 -0
- package/src/agents/vendor/plastic.ts +4958 -0
- package/src/agents/vendor/presets.ts +103 -0
- package/src/agents/vendor/shapes.ts +41 -0
- package/src/agents/vendor/ticker.ts +67 -0
- package/src/agents/vendor/types.ts +261 -0
- package/src/agents/vendor/wear.ts +1699 -0
- package/src/ai/index.ts +0 -18
- package/src/animations/README.md +3 -3
- package/src/lib/sliding-pill.ts +68 -0
- package/src/lib/theme-transition.ts +4 -2
- package/src/shadcn/code-block/code-block-actions.svelte +23 -0
- package/src/shadcn/code-block/code-block-content.svelte +55 -0
- package/src/shadcn/code-block/code-block-copy-button.svelte +59 -0
- package/src/shadcn/code-block/code-block-footer.svelte +20 -0
- package/src/shadcn/code-block/code-block-header.svelte +23 -0
- package/src/shadcn/code-block/code-block-line.svelte +71 -0
- package/src/shadcn/code-block/code-block-title.svelte +23 -0
- package/src/shadcn/code-block/code-block.svelte +47 -0
- package/src/shadcn/code-block/index.ts +42 -0
- package/src/shadcn/hooks/highlighter.ts +132 -0
- package/src/shadcn/hooks/use-highlighted-code.svelte.ts +98 -0
- package/src/shadcn/kbd/index.ts +10 -0
- package/src/shadcn/kbd/kbd-group.svelte +20 -0
- package/src/shadcn/kbd/kbd.svelte +23 -0
- package/src/shadcn/message-scroller/index.ts +28 -0
- package/src/shadcn/message-scroller/message-scroller-button.svelte +52 -0
- package/src/shadcn/message-scroller/message-scroller-content.svelte +17 -0
- package/src/shadcn/message-scroller/message-scroller-item.svelte +22 -0
- package/src/shadcn/message-scroller/message-scroller-provider.svelte +7 -0
- package/src/shadcn/message-scroller/message-scroller-viewport.svelte +20 -0
- package/src/shadcn/message-scroller/message-scroller.svelte +20 -0
- package/src/shadcn/tabs/tabs-list.svelte +13 -2
- package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
- package/src/shadcn/trace/index.ts +29 -0
- package/src/shadcn/trace/trace-collapsible-content.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible-trigger.svelte +29 -0
- package/src/shadcn/trace/trace-collapsible.svelte +20 -0
- package/src/shadcn/trace/trace-description.svelte +20 -0
- package/src/shadcn/trace/trace-icon.svelte +24 -0
- package/src/shadcn/trace/trace-line.svelte +31 -0
- package/src/shadcn/trace/trace-title.svelte +20 -0
- package/src/shadcn/trace/trace.svelte +23 -0
- package/src/showcase/AiSection.svelte +0 -7
- package/src/showcase/demos/shadcn/code-block.svelte +80 -0
- package/src/showcase/demos/shadcn/index.ts +24 -0
- package/src/showcase/demos/shadcn/message-scroller.svelte +44 -0
- package/src/showcase/demos/shadcn/trace.svelte +51 -0
- package/src/showcase/kit.ts +1 -2
- package/src/ai/conversation/Conversation.svelte +0 -28
- package/src/ai/conversation/ConversationContent.svelte +0 -35
- package/src/ai/conversation/ConversationEmptyState.svelte +0 -46
- package/src/ai/conversation/ConversationScrollButton.svelte +0 -51
- package/src/ai/conversation/index.ts +0 -33
- package/src/ai/conversation/stick-to-bottom-context.svelte.ts +0 -131
- package/src/charts/animated-value.css +0 -7
- package/src/charts/animated-value.svelte +0 -110
- package/src/charts/area-chart.css +0 -29
- package/src/charts/area-chart.svelte +0 -195
- package/src/charts/bar-chart.css +0 -119
- package/src/charts/bar-chart.svelte +0 -269
- package/src/charts/charts.css +0 -201
- package/src/charts/crosshair.svelte +0 -35
- package/src/charts/donut-chart.css +0 -56
- package/src/charts/donut-chart.svelte +0 -158
- package/src/charts/edge-fade.svelte +0 -43
- package/src/charts/edge-fade.ts +0 -16
- package/src/charts/figure.css +0 -100
- package/src/charts/figure.svelte +0 -122
- package/src/charts/gauge-chart.css +0 -94
- package/src/charts/gauge-chart.svelte +0 -303
- package/src/charts/hover-band.test.ts +0 -30
- package/src/charts/hover-band.ts +0 -21
- package/src/charts/hover-focus.svelte +0 -85
- package/src/charts/index.ts +0 -30
- package/src/charts/legend.css +0 -131
- package/src/charts/legend.svelte +0 -112
- package/src/charts/line-chart.css +0 -26
- package/src/charts/line-chart.svelte +0 -256
- package/src/charts/motion.ts +0 -19
- package/src/charts/number-chart.css +0 -168
- package/src/charts/number-chart.svelte +0 -177
- package/src/charts/primitives/chart-container.svelte +0 -57
- package/src/charts/primitives/chart-style.svelte +0 -36
- package/src/charts/primitives/chart-tooltip.svelte +0 -161
- package/src/charts/primitives/chart-utils.ts +0 -54
- package/src/charts/primitives/index.ts +0 -11
- package/src/charts/reveal.svelte +0 -61
- package/src/charts/ring-chart.css +0 -96
- package/src/charts/ring-chart.svelte +0 -158
- package/src/charts/sparkline.css +0 -45
- package/src/charts/sparkline.svelte +0 -140
- package/src/charts/stat-card.css +0 -103
- package/src/charts/stat-card.svelte +0 -267
- package/src/charts/time-series-chart.css +0 -28
- package/src/charts/time-series-chart.svelte +0 -179
- package/src/charts/trend-pill.css +0 -38
- package/src/charts/trend-pill.svelte +0 -48
- package/src/lib/hooks/is-mobile.svelte.ts +0 -11
- package/src/showcase/ChartsSection.svelte +0 -458
- package/src/showcase/demos/ai/conversation.svelte +0 -59
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
/* The batching shown-gate and the path caches are shared by every avatar
|
|
3
|
+
on the page, so a row of them comes in together. Engine: ./vendor. */
|
|
4
|
+
import { bakesPending as bakesPendingGlobal } from './vendor/plastic';
|
|
5
|
+
|
|
6
|
+
const SHOW_WITHIN = 4000;
|
|
7
|
+
const waiters = new Map<object, { ready: () => boolean; show: () => void }>();
|
|
8
|
+
|
|
9
|
+
function showReady() {
|
|
10
|
+
if (bakesPendingGlobal()) return;
|
|
11
|
+
for (const w of waiters.values()) if (!w.ready()) return;
|
|
12
|
+
const all = [...waiters.values()];
|
|
13
|
+
waiters.clear();
|
|
14
|
+
for (const w of all) w.show();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pathCache = new Map<string, Path2D>();
|
|
18
|
+
function bodyPath(d: string): Path2D {
|
|
19
|
+
let p = pathCache.get(d);
|
|
20
|
+
if (!p) {
|
|
21
|
+
p = new Path2D(d);
|
|
22
|
+
pathCache.set(d, p);
|
|
23
|
+
}
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const partsCache = new Map<string, Path2D[]>();
|
|
28
|
+
function partPaths(d: string): Path2D[] {
|
|
29
|
+
let p = partsCache.get(d);
|
|
30
|
+
if (!p) {
|
|
31
|
+
p = d
|
|
32
|
+
.split(/(?=M)/)
|
|
33
|
+
.filter((s) => s.trim())
|
|
34
|
+
.map((s) => new Path2D(s));
|
|
35
|
+
partsCache.set(d, p);
|
|
36
|
+
}
|
|
37
|
+
return p;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let uidSeq = 0;
|
|
41
|
+
function hashSeed(id: string): number {
|
|
42
|
+
let h = 2166136261;
|
|
43
|
+
for (let i = 0; i < id.length; i++) h = Math.imul(h ^ id.charCodeAt(i), 16777619);
|
|
44
|
+
return ((h >>> 0) % 1000) / 1000;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const reducedMotion = (): boolean =>
|
|
48
|
+
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<script lang="ts">
|
|
52
|
+
import { onMount } from 'svelte';
|
|
53
|
+
import type {
|
|
54
|
+
BotAvatarShading,
|
|
55
|
+
BotAvatarState,
|
|
56
|
+
BotAvatarType,
|
|
57
|
+
} from './vendor/types';
|
|
58
|
+
import type { AgentAvatarProps } from './types';
|
|
59
|
+
import { botAvatarPresets, stateLabels } from './vendor/presets';
|
|
60
|
+
import { SHAPE_PATHS, SHAPE_PARTS } from './vendor/shapes';
|
|
61
|
+
import { autoInk, richer, shade } from './vendor/color';
|
|
62
|
+
import { Sim, restPose } from './vendor/engine';
|
|
63
|
+
import { draw, LIGHT_DEFAULTS, OVERSCAN, RISE, type DrawConfig } from './vendor/draw';
|
|
64
|
+
import {
|
|
65
|
+
bakesPending,
|
|
66
|
+
hurry,
|
|
67
|
+
onLanded,
|
|
68
|
+
stockLights,
|
|
69
|
+
takeMissed,
|
|
70
|
+
warmPlastic,
|
|
71
|
+
type FurStyle,
|
|
72
|
+
} from './vendor/plastic';
|
|
73
|
+
import { subscribe, pointer } from './vendor/ticker';
|
|
74
|
+
|
|
75
|
+
type Props = AgentAvatarProps;
|
|
76
|
+
|
|
77
|
+
let {
|
|
78
|
+
name,
|
|
79
|
+
role,
|
|
80
|
+
shape = 'clover',
|
|
81
|
+
path,
|
|
82
|
+
face = 'eyes',
|
|
83
|
+
state: botState = 'default',
|
|
84
|
+
size,
|
|
85
|
+
color,
|
|
86
|
+
ink = '#1E1A33',
|
|
87
|
+
brightness: brightnessProp,
|
|
88
|
+
saturation: saturationProp,
|
|
89
|
+
speed = 1,
|
|
90
|
+
paused = false,
|
|
91
|
+
seed,
|
|
92
|
+
shading = 'fabric',
|
|
93
|
+
hat = 'none',
|
|
94
|
+
glasses = 'none',
|
|
95
|
+
headphones = false,
|
|
96
|
+
bowTie = false,
|
|
97
|
+
accessoryColor = '#27272b',
|
|
98
|
+
shadow,
|
|
99
|
+
highlight,
|
|
100
|
+
depth = 0.65,
|
|
101
|
+
roundness = 1,
|
|
102
|
+
furLength = 1,
|
|
103
|
+
furDensity = 1.6,
|
|
104
|
+
furFuzz = 0.9,
|
|
105
|
+
furClumps = 0.4,
|
|
106
|
+
furCurl = 0.7,
|
|
107
|
+
furGravity = 0.9,
|
|
108
|
+
light,
|
|
109
|
+
rim,
|
|
110
|
+
spread,
|
|
111
|
+
backLight,
|
|
112
|
+
lightFront,
|
|
113
|
+
shine,
|
|
114
|
+
sheen,
|
|
115
|
+
backSoftness,
|
|
116
|
+
pose: heldPose,
|
|
117
|
+
interactive = true,
|
|
118
|
+
turn = 1,
|
|
119
|
+
theme = 'auto',
|
|
120
|
+
whirl = 0,
|
|
121
|
+
whirlSize = 1,
|
|
122
|
+
whirlWidth = 1,
|
|
123
|
+
whirlLength = 1,
|
|
124
|
+
whirlTilt = 1,
|
|
125
|
+
jumpHeight = 26,
|
|
126
|
+
jumpTime = 0.68,
|
|
127
|
+
jumpStretch = 1,
|
|
128
|
+
jumpSpin = 1,
|
|
129
|
+
jumpLean = 6,
|
|
130
|
+
jumpEvery = 8,
|
|
131
|
+
jumpLand = 0,
|
|
132
|
+
jumpSquash = 1.15,
|
|
133
|
+
jumpSquashTime = 0.37,
|
|
134
|
+
jumpSquashEase = 'pulse',
|
|
135
|
+
jumpGroundTime = 0.11,
|
|
136
|
+
jumpGroundEase = 'pulse',
|
|
137
|
+
jumpRiseTime = 0.33,
|
|
138
|
+
jumpRiseEase = 'pulse',
|
|
139
|
+
jumpClickSquashTime = 0.24,
|
|
140
|
+
class: className,
|
|
141
|
+
style,
|
|
142
|
+
'aria-label': ariaLabel,
|
|
143
|
+
onclick: userClick,
|
|
144
|
+
...rest
|
|
145
|
+
}: Props = $props();
|
|
146
|
+
|
|
147
|
+
const clamp = (v: number, lo: number, hi: number) =>
|
|
148
|
+
Math.min(hi, Math.max(lo, Number.isFinite(v) ? v : 1));
|
|
149
|
+
const q = (v: number, lo: number, hi: number) => Math.round(clamp(v, lo, hi) * 20) / 20;
|
|
150
|
+
const clampOpt = (v: number | undefined, d: number, lo: number, hi: number) =>
|
|
151
|
+
clamp(v ?? d, lo, hi);
|
|
152
|
+
const shapePath = (t: BotAvatarType) => SHAPE_PATHS[t] ?? SHAPE_PATHS.clover;
|
|
153
|
+
const deviceDpr = () =>
|
|
154
|
+
Math.min(2, (typeof devicePixelRatio === 'number' && devicePixelRatio) || 1);
|
|
155
|
+
|
|
156
|
+
const uid = `ba-${++uidSeq}`;
|
|
157
|
+
|
|
158
|
+
/* The avatar draws at 75% of its slot so an idle jump has room in the box
|
|
159
|
+
and never clips. A numeric `size` overrides the measurement. */
|
|
160
|
+
const JUMP_CLEARANCE = 0.75;
|
|
161
|
+
let hostEl = $state<HTMLElement | null>(null);
|
|
162
|
+
let hostWidth = $state(0);
|
|
163
|
+
const drawnSize = $derived(
|
|
164
|
+
typeof size === 'number' ? size : hostWidth > 0 ? hostWidth * JUMP_CLEARANCE : 0,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const type = $derived(shape);
|
|
168
|
+
const preset = $derived(botAvatarPresets[type] ?? botAvatarPresets.clover);
|
|
169
|
+
const faceKind = $derived(face ?? preset.face);
|
|
170
|
+
const picked = $derived(color ?? preset.color);
|
|
171
|
+
const brightness = $derived(brightnessProp ?? (preset.brightness ?? 1));
|
|
172
|
+
const saturation = $derived(saturationProp ?? (preset.saturation ?? 1.5));
|
|
173
|
+
const vivid = $derived(clamp(saturation, 1.5, 2.5) - 1.5);
|
|
174
|
+
const body = $derived(
|
|
175
|
+
richer(
|
|
176
|
+
brightness === 1 && saturation === 1
|
|
177
|
+
? picked
|
|
178
|
+
: shade(
|
|
179
|
+
picked,
|
|
180
|
+
(Math.min(2, Math.max(0, brightness)) - 1) * 0.35,
|
|
181
|
+
(Math.min(1.5, Math.max(0, saturation)) - 1) * 0.5,
|
|
182
|
+
),
|
|
183
|
+
vivid,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
const inkColor = $derived(ink ?? autoInk(body));
|
|
187
|
+
const seedValue = $derived(Math.min(1, Math.max(0, seed ?? hashSeed(uid))));
|
|
188
|
+
const stateKey = $derived((botState in stateLabels ? botState : 'default') as BotAvatarState);
|
|
189
|
+
const frozen = $derived(paused || !(speed > 0));
|
|
190
|
+
const shadingMode = $derived(
|
|
191
|
+
(shading === true ? 'crisp' : shading === false ? 'flat' : shading) as BotAvatarShading,
|
|
192
|
+
);
|
|
193
|
+
const customPath = $derived(typeof path === 'string' && path.trim() ? path.trim() : null);
|
|
194
|
+
const outlineKey = $derived(
|
|
195
|
+
customPath ? `path:${hashSeed(customPath)}:${customPath.length}` : type,
|
|
196
|
+
);
|
|
197
|
+
const lit = $derived(LIGHT_DEFAULTS[shadingMode === 'fabric' ? 'fabric' : 'other']);
|
|
198
|
+
const fur = $derived({
|
|
199
|
+
length: q(furLength, 0.3, 2.5),
|
|
200
|
+
density: q(furDensity, 0.3, 2),
|
|
201
|
+
fuzz: q(furFuzz, 0, 1),
|
|
202
|
+
clumps: q(furClumps, 0, 1),
|
|
203
|
+
curl: q(furCurl, 0, 1),
|
|
204
|
+
gravity: q(furGravity, 0, 1),
|
|
205
|
+
} satisfies FurStyle);
|
|
206
|
+
const material = $derived(shadingMode === 'plastic' || shadingMode === 'fabric');
|
|
207
|
+
const lookKey = $derived(`${shadingMode}|${outlineKey}|${Math.round(drawnSize)}`);
|
|
208
|
+
const interactiveNow = $derived(interactive && !heldPose);
|
|
209
|
+
|
|
210
|
+
/* Captured once, on purpose: `shown` never returns to false, so a later
|
|
211
|
+
shading change must not reset it. */
|
|
212
|
+
function beginsMaterial() {
|
|
213
|
+
return shading === 'plastic' || shading === 'fabric';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function partsFor(t: BotAvatarType) {
|
|
217
|
+
return !customPath && SHAPE_PARTS[t] ? partPaths(SHAPE_PARTS[t] as string) : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function configure(): DrawConfig {
|
|
221
|
+
return {
|
|
222
|
+
path: bodyPath(customPath ?? shapePath(type)),
|
|
223
|
+
face: faceKind,
|
|
224
|
+
faceX: preset.faceX,
|
|
225
|
+
faceY: preset.faceY,
|
|
226
|
+
faceScale: preset.faceScale,
|
|
227
|
+
color: body,
|
|
228
|
+
ink: inkColor,
|
|
229
|
+
shading: shadingMode,
|
|
230
|
+
shadow: clampOpt(shadow, lit.shadow, 0, 2),
|
|
231
|
+
highlight: clampOpt(highlight, lit.highlight, 0, 2),
|
|
232
|
+
depth: clamp(depth, 0.2, 2),
|
|
233
|
+
roundness: clamp(roundness, 0, 1),
|
|
234
|
+
fur,
|
|
235
|
+
vivid,
|
|
236
|
+
backLight,
|
|
237
|
+
lightFront,
|
|
238
|
+
shine,
|
|
239
|
+
sheen,
|
|
240
|
+
backSoftness,
|
|
241
|
+
light: light ?? lit.light,
|
|
242
|
+
rim: clampOpt(rim, lit.rim, 0, 2),
|
|
243
|
+
spread: clampOpt(spread, lit.spread, 0.4, 2.5),
|
|
244
|
+
typeKey: outlineKey,
|
|
245
|
+
still: frozen || reducedMotion(),
|
|
246
|
+
furOnIdle: true,
|
|
247
|
+
whirl: {
|
|
248
|
+
strength: clamp(whirl, 0, 2),
|
|
249
|
+
size: clamp(whirlSize, 0.6, 1.6),
|
|
250
|
+
width: clamp(whirlWidth, 0.4, 2),
|
|
251
|
+
length: clamp(whirlLength, 0.4, 1.6),
|
|
252
|
+
tilt: clamp(whirlTilt, 0.5, 1.8),
|
|
253
|
+
},
|
|
254
|
+
parts: partsFor(type),
|
|
255
|
+
wear: { hat, glasses, headphones, bowTie, color: accessoryColor, fabric: shadingMode === 'fabric' },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const cfg = $derived(configure());
|
|
260
|
+
|
|
261
|
+
let canvasEl = $state<HTMLCanvasElement | null>(null);
|
|
262
|
+
let sim: Sim | null = null;
|
|
263
|
+
let cssSize = 0;
|
|
264
|
+
let lastLook: string | null = null;
|
|
265
|
+
|
|
266
|
+
let shown = $state(!beginsMaterial());
|
|
267
|
+
let shownRef = !beginsMaterial();
|
|
268
|
+
function markShown() {
|
|
269
|
+
shownRef = true;
|
|
270
|
+
shown = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let stillPaint: (() => void) | null = null;
|
|
274
|
+
let waiting: (() => void) | null = null;
|
|
275
|
+
let watch: (() => void) | null = null;
|
|
276
|
+
let showPaint: (() => void) | null = null;
|
|
277
|
+
let ready = false;
|
|
278
|
+
let holding: (() => void) | null = null;
|
|
279
|
+
const meRef = {};
|
|
280
|
+
|
|
281
|
+
function resolveTheme(el: HTMLElement | null): 'dark' | 'light' {
|
|
282
|
+
if (theme !== 'auto') return theme;
|
|
283
|
+
const host = el?.closest('[data-theme], .dark, .light') as HTMLElement | null;
|
|
284
|
+
if (host) {
|
|
285
|
+
const v = host.getAttribute('data-theme');
|
|
286
|
+
if (v === 'dark' || v === 'light') return v;
|
|
287
|
+
if (host.classList.contains('dark')) return 'dark';
|
|
288
|
+
if (host.classList.contains('light')) return 'light';
|
|
289
|
+
}
|
|
290
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: light)').matches
|
|
291
|
+
? 'light'
|
|
292
|
+
: 'dark';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function layoutPx(canvas: HTMLCanvasElement) {
|
|
296
|
+
return canvas.clientWidth / OVERSCAN || cssSize || drawnSize || 64;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function poseFor(rest0: ReturnType<typeof restPose>) {
|
|
300
|
+
const held = heldPose;
|
|
301
|
+
if (held)
|
|
302
|
+
return { ...rest0, yaw: held.yaw ?? 0, pitch: held.pitch ?? rest0.pitch, roll: held.roll ?? rest0.roll };
|
|
303
|
+
return sim ? sim.pose : rest0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function paint() {
|
|
307
|
+
const canvas = canvasEl;
|
|
308
|
+
const c = cfg;
|
|
309
|
+
if (!canvas || !c?.path) return;
|
|
310
|
+
const px = layoutPx(canvas);
|
|
311
|
+
if (!px) return;
|
|
312
|
+
const dpr = deviceDpr();
|
|
313
|
+
const want = Math.round(px * OVERSCAN * dpr);
|
|
314
|
+
if (canvas.width !== want || canvas.height !== want) {
|
|
315
|
+
canvas.width = want;
|
|
316
|
+
canvas.height = want;
|
|
317
|
+
}
|
|
318
|
+
cssSize = px;
|
|
319
|
+
const ctx = canvas.getContext('2d');
|
|
320
|
+
if (!ctx) return;
|
|
321
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
322
|
+
c.dpr = dpr;
|
|
323
|
+
draw(ctx, px, poseFor(restPose(stateKey)), c);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function paintStill() {
|
|
327
|
+
const canvas = canvasEl;
|
|
328
|
+
const c = cfg;
|
|
329
|
+
if (!canvas || !c?.path) return;
|
|
330
|
+
const px = layoutPx(canvas);
|
|
331
|
+
if (!px) return;
|
|
332
|
+
cssSize = px;
|
|
333
|
+
const dpr = deviceDpr();
|
|
334
|
+
canvas.width = canvas.height = Math.round(px * OVERSCAN * dpr);
|
|
335
|
+
const ctx = canvas.getContext('2d');
|
|
336
|
+
if (ctx) {
|
|
337
|
+
c.theme = resolveTheme(canvas);
|
|
338
|
+
c.dpr = dpr;
|
|
339
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
340
|
+
draw(ctx, px, restPose(stateKey), c);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function stopWaiting() {
|
|
345
|
+
if (waiting) waiting();
|
|
346
|
+
waiting = null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function settle(paintNow: () => void) {
|
|
350
|
+
takeMissed();
|
|
351
|
+
paintNow();
|
|
352
|
+
if (!takeMissed()) stopWaiting();
|
|
353
|
+
else if (!waiting)
|
|
354
|
+
waiting = onLanded(() => {
|
|
355
|
+
const p = stillPaint;
|
|
356
|
+
if (canvasEl && p) settle(p);
|
|
357
|
+
else stopWaiting();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function stopWatch() {
|
|
362
|
+
if (watch) watch();
|
|
363
|
+
watch = null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function showWhenBaked(paintNow: () => void) {
|
|
367
|
+
showPaint = paintNow;
|
|
368
|
+
takeMissed();
|
|
369
|
+
paintNow();
|
|
370
|
+
ready = !takeMissed();
|
|
371
|
+
if (ready && !bakesPending()) {
|
|
372
|
+
showReady();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!watch)
|
|
376
|
+
watch = onLanded(() => {
|
|
377
|
+
const p = showPaint;
|
|
378
|
+
if (canvasEl && p && !shownRef) showWhenBaked(p);
|
|
379
|
+
else stopWatch();
|
|
380
|
+
showReady();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function lightsFor(lightAt: number) {
|
|
385
|
+
const a = (lightAt * Math.PI) / 180;
|
|
386
|
+
const b = ((backLight ?? 0) * Math.PI) / 180;
|
|
387
|
+
const lights = stockLights(Math.sin(a), -Math.cos(a));
|
|
388
|
+
if (backLight !== undefined) [lights.bx, lights.by] = [Math.sin(b), -Math.cos(b)];
|
|
389
|
+
if (lightFront !== undefined) lights.front = (Math.min(85, Math.max(0, lightFront)) * Math.PI) / 180;
|
|
390
|
+
return lights;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function warm() {
|
|
394
|
+
if (!material || !cfg?.path) return;
|
|
395
|
+
const c = cfg;
|
|
396
|
+
const dev = (drawnSize || 64) * deviceDpr();
|
|
397
|
+
const lightAt = c.light ?? 295;
|
|
398
|
+
warmPlastic(outlineKey, c.path, dev, depth, shadingMode === 'fabric', fur, lightAt, lightsFor(lightAt));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function warmIdle(fn: () => void): () => void {
|
|
402
|
+
if (typeof requestIdleCallback === 'function') {
|
|
403
|
+
const id = requestIdleCallback(fn);
|
|
404
|
+
return () => cancelIdleCallback(id);
|
|
405
|
+
}
|
|
406
|
+
const id = setTimeout(fn, 1);
|
|
407
|
+
return () => clearTimeout(id);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function hold() {
|
|
411
|
+
warm();
|
|
412
|
+
if (!bakesPending()) return;
|
|
413
|
+
hurry(true);
|
|
414
|
+
const release = () => {
|
|
415
|
+
if (!holding) return;
|
|
416
|
+
holding();
|
|
417
|
+
holding = null;
|
|
418
|
+
paint();
|
|
419
|
+
};
|
|
420
|
+
const off = onLanded(() => {
|
|
421
|
+
if (!bakesPending()) release();
|
|
422
|
+
});
|
|
423
|
+
const t = setTimeout(release, SHOW_WITHIN);
|
|
424
|
+
holding = () => {
|
|
425
|
+
off();
|
|
426
|
+
clearTimeout(t);
|
|
427
|
+
hurry(false);
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function handleClick(e: MouseEvent) {
|
|
432
|
+
if (interactive && !frozen && !heldPose) sim?.poke();
|
|
433
|
+
userClick?.(e);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
onMount(() => () => {
|
|
437
|
+
stopWaiting();
|
|
438
|
+
holding?.();
|
|
439
|
+
holding = null;
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
function shouldHold() {
|
|
443
|
+
return shownRef && material && lastLook !== null && lastLook !== lookKey && !holding;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function applyPose() {
|
|
447
|
+
if (holding) return;
|
|
448
|
+
if (reducedMotion()) {
|
|
449
|
+
stillPaint = paintStill;
|
|
450
|
+
if (shownRef) settle(paintStill);
|
|
451
|
+
else showWhenBaked(paintStill);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (canvasEl) {
|
|
455
|
+
const c = cfg;
|
|
456
|
+
if (c) c.theme = resolveTheme(canvasEl);
|
|
457
|
+
}
|
|
458
|
+
if (!shownRef) {
|
|
459
|
+
stillPaint = frozen ? paint : null;
|
|
460
|
+
showWhenBaked(paint);
|
|
461
|
+
} else if (frozen) {
|
|
462
|
+
stillPaint = paint;
|
|
463
|
+
settle(paint);
|
|
464
|
+
} else {
|
|
465
|
+
stillPaint = null;
|
|
466
|
+
stopWaiting();
|
|
467
|
+
paint();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* Sync the rig with the props on every meaningful change: this is the
|
|
472
|
+
React `useLayoutEffect` (which ran each render) made reactive. */
|
|
473
|
+
$effect(() => {
|
|
474
|
+
if (!sim) sim = new Sim(seedValue, stateKey);
|
|
475
|
+
else sim.setState(stateKey);
|
|
476
|
+
sim.setTurn(clamp(turn, 0, 2));
|
|
477
|
+
if (shouldHold()) hold();
|
|
478
|
+
lastLook = lookKey;
|
|
479
|
+
sim.setJump({
|
|
480
|
+
height: jumpHeight,
|
|
481
|
+
time: Math.max(0.2, jumpTime),
|
|
482
|
+
stretch: jumpStretch,
|
|
483
|
+
spin: Math.max(0, Math.round(jumpSpin)),
|
|
484
|
+
lean: jumpLean,
|
|
485
|
+
every: jumpEvery,
|
|
486
|
+
land: jumpLand,
|
|
487
|
+
squash: jumpSquash,
|
|
488
|
+
squashTime: Math.max(0.05, jumpSquashTime),
|
|
489
|
+
squashEase: jumpSquashEase,
|
|
490
|
+
groundTime: Math.max(0, jumpGroundTime),
|
|
491
|
+
groundEase: jumpGroundEase,
|
|
492
|
+
riseTime: Math.max(0.05, jumpRiseTime),
|
|
493
|
+
riseEase: jumpRiseEase,
|
|
494
|
+
clickSquashTime: Math.max(0.05, jumpClickSquashTime),
|
|
495
|
+
});
|
|
496
|
+
applyPose();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
/* Register with the shared shown-gate so a row of baked avatars appears
|
|
500
|
+
together, or after SHOW_WITHIN at the latest. */
|
|
501
|
+
$effect(() => {
|
|
502
|
+
if (shown) return;
|
|
503
|
+
waiters.set(meRef, { ready: () => ready, show: markShown });
|
|
504
|
+
showReady();
|
|
505
|
+
hurry(true);
|
|
506
|
+
const t = setTimeout(markShown, SHOW_WITHIN);
|
|
507
|
+
return () => {
|
|
508
|
+
hurry(false);
|
|
509
|
+
clearTimeout(t);
|
|
510
|
+
stopWatch();
|
|
511
|
+
waiters.delete(meRef);
|
|
512
|
+
showReady();
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
/* Start the material's bakes on idle time as soon as the shape is known. */
|
|
517
|
+
$effect(() => {
|
|
518
|
+
void shadingMode;
|
|
519
|
+
void outlineKey;
|
|
520
|
+
void drawnSize;
|
|
521
|
+
void depth;
|
|
522
|
+
void light;
|
|
523
|
+
void backLight;
|
|
524
|
+
void lightFront;
|
|
525
|
+
if (!material || !cfg?.path) return;
|
|
526
|
+
return warmIdle(warm);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
function pointerStrength(d: number) {
|
|
530
|
+
if (d < 1) return 1;
|
|
531
|
+
if (d > 3) return 0;
|
|
532
|
+
return 1 - (d - 1) / 2;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function updatePointer(s: Sim, canvas: HTMLCanvasElement) {
|
|
536
|
+
if (!interactiveNow || Number.isNaN(pointer.x)) {
|
|
537
|
+
s.setPointer(0, 0, 0);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const r = canvas.getBoundingClientRect();
|
|
541
|
+
const box = r.width / OVERSCAN || 1;
|
|
542
|
+
const dx = (pointer.x - (r.left + r.width / 2)) / box;
|
|
543
|
+
const dy = (pointer.y - (r.top + r.height / 2 + RISE * box)) / box;
|
|
544
|
+
const d = Math.hypot(dx, dy);
|
|
545
|
+
s.setPointer(dx / Math.max(1, d), dy / Math.max(1, d), pointerStrength(d));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* The loop, only while visible, animated and not reduced. */
|
|
549
|
+
$effect(() => {
|
|
550
|
+
if (frozen || reducedMotion()) return;
|
|
551
|
+
const canvas = canvasEl;
|
|
552
|
+
if (!canvas) return;
|
|
553
|
+
let unsub: (() => void) | null = null;
|
|
554
|
+
const tick = (dt: number) => {
|
|
555
|
+
const s = sim;
|
|
556
|
+
if (!s) return;
|
|
557
|
+
updatePointer(s, canvas);
|
|
558
|
+
s.update(dt * speed);
|
|
559
|
+
if (shownRef && !holding) paint();
|
|
560
|
+
};
|
|
561
|
+
const run = () => {
|
|
562
|
+
if (!unsub) unsub = subscribe(tick);
|
|
563
|
+
};
|
|
564
|
+
const stop = () => {
|
|
565
|
+
if (unsub) unsub();
|
|
566
|
+
unsub = null;
|
|
567
|
+
};
|
|
568
|
+
let io: IntersectionObserver | null = null;
|
|
569
|
+
if (typeof IntersectionObserver === 'function') {
|
|
570
|
+
io = new IntersectionObserver((entries) => {
|
|
571
|
+
if (entries[0]?.isIntersecting ?? true) run();
|
|
572
|
+
else stop();
|
|
573
|
+
});
|
|
574
|
+
io.observe(canvas);
|
|
575
|
+
} else run();
|
|
576
|
+
return () => {
|
|
577
|
+
stop();
|
|
578
|
+
if (io) io.disconnect();
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const canvasStyle = $derived.by(() => {
|
|
583
|
+
const s = drawnSize || 64;
|
|
584
|
+
const dim = `${s * OVERSCAN}px`;
|
|
585
|
+
const side = (OVERSCAN - 1) / 2;
|
|
586
|
+
const pull = (k: number) => `${-s * k}px`;
|
|
587
|
+
return (
|
|
588
|
+
`display:inline-block;vertical-align:middle;width:${dim};height:${dim};` +
|
|
589
|
+
`margin-left:${pull(side)};margin-right:${pull(side)};` +
|
|
590
|
+
`margin-top:${pull(side + RISE)};margin-bottom:${pull(side - RISE)};` +
|
|
591
|
+
`flex:none;opacity:${shown ? 1 : 0};transition:opacity 0.3s ease-out`
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
/* No box: the slot carries no border, background or padding. Without an
|
|
596
|
+
explicit size it stays square, and the avatar fills three quarters of it. */
|
|
597
|
+
const hostStyle = $derived.by(() => {
|
|
598
|
+
const base =
|
|
599
|
+
size === undefined
|
|
600
|
+
? 'display:inline-grid;place-items:center;line-height:0;width:100%;aspect-ratio:1/1;'
|
|
601
|
+
: 'display:inline-block;line-height:0;';
|
|
602
|
+
return style ? `${base}${style}` : base;
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
onMount(() => {
|
|
606
|
+
if (!hostEl) return;
|
|
607
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
608
|
+
if (entry) hostWidth = entry.contentRect.width;
|
|
609
|
+
});
|
|
610
|
+
observer.observe(hostEl);
|
|
611
|
+
return () => observer.disconnect();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
export function getCanvas(): HTMLCanvasElement | null {
|
|
615
|
+
return canvasEl;
|
|
616
|
+
}
|
|
617
|
+
</script>
|
|
618
|
+
|
|
619
|
+
<span
|
|
620
|
+
bind:this={hostEl}
|
|
621
|
+
class={className ? `agent-avatar ${className}` : 'agent-avatar'}
|
|
622
|
+
style={hostStyle}
|
|
623
|
+
data-agent-shape={type}
|
|
624
|
+
>
|
|
625
|
+
{#if drawnSize > 0}
|
|
626
|
+
<canvas
|
|
627
|
+
bind:this={canvasEl}
|
|
628
|
+
data-bot-avatar={type}
|
|
629
|
+
data-face={faceKind}
|
|
630
|
+
data-state={stateKey}
|
|
631
|
+
role="img"
|
|
632
|
+
aria-label={ariaLabel ??
|
|
633
|
+
(name ? `${name}${role ? `, ${role}` : ''}` : `${preset.label} agent, ${stateLabels[stateKey]}`)}
|
|
634
|
+
style={canvasStyle}
|
|
635
|
+
onclick={handleClick}
|
|
636
|
+
{...rest}
|
|
637
|
+
></canvas>
|
|
638
|
+
{/if}
|
|
639
|
+
</span>
|