@awflow/avatar-element 1.0.0 → 1.0.1
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 +3 -1
- package/dist/index.js +360 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@ Methods: `react(name)`, `pulse(strength?)`, `setVoice(fn | null)`,
|
|
|
43
43
|
Properties `ring`, `gaze`, `reaction` and `reactKey` accept objects directly.
|
|
44
44
|
|
|
45
45
|
`defineAvatarElements(prefix = 'awf')` registers the tags under another prefix.
|
|
46
|
-
Importing in Node is safe and defines nothing
|
|
46
|
+
Importing in Node (SSR, tests) is safe from 1.0.1 and defines nothing; the
|
|
47
|
+
1.0.0 build was broken (its `dist/index.js` lacked the definitions), so use
|
|
48
|
+
1.0.1 or later.
|
|
47
49
|
|
|
48
50
|
## Docs
|
|
49
51
|
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,363 @@
|
|
|
1
|
+
// src/element.ts
|
|
2
|
+
import { LATEST } from "@awflow/avatar";
|
|
3
|
+
import {
|
|
4
|
+
mount
|
|
5
|
+
} from "@awflow/avatar/adapter";
|
|
6
|
+
|
|
7
|
+
// src/group.ts
|
|
8
|
+
function createGroup() {
|
|
9
|
+
const members = [];
|
|
10
|
+
let speaker;
|
|
11
|
+
const aim = () => {
|
|
12
|
+
const at = members.find((m) => m.name === speaker)?.handle ?? null;
|
|
13
|
+
for (const m of members)
|
|
14
|
+
m.handle.lookAt(m.handle === at ? null : at);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
registerMember(name, handle) {
|
|
18
|
+
const m = { name, handle };
|
|
19
|
+
members.push(m);
|
|
20
|
+
aim();
|
|
21
|
+
return () => {
|
|
22
|
+
const i = members.indexOf(m);
|
|
23
|
+
if (i < 0)
|
|
24
|
+
return;
|
|
25
|
+
members.splice(i, 1);
|
|
26
|
+
if (m.name === speaker)
|
|
27
|
+
aim();
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
setSpeaker(name) {
|
|
31
|
+
speaker = name;
|
|
32
|
+
aim();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/parse.ts
|
|
38
|
+
var BOOLEANS = new Set(["animate", "dress", "riso", "square"]);
|
|
39
|
+
var NUMBERS = new Set(["size", "version"]);
|
|
40
|
+
var STRINGS = [
|
|
41
|
+
"name",
|
|
42
|
+
"theme",
|
|
43
|
+
"framing",
|
|
44
|
+
"frame",
|
|
45
|
+
"expression",
|
|
46
|
+
"activity",
|
|
47
|
+
"shape",
|
|
48
|
+
"color",
|
|
49
|
+
"eyes",
|
|
50
|
+
"brows",
|
|
51
|
+
"mouth",
|
|
52
|
+
"cheeks",
|
|
53
|
+
"trait",
|
|
54
|
+
"motif",
|
|
55
|
+
"headwear",
|
|
56
|
+
"eyewear",
|
|
57
|
+
"neckwear",
|
|
58
|
+
"face",
|
|
59
|
+
"prop",
|
|
60
|
+
"reaction"
|
|
61
|
+
];
|
|
62
|
+
var ATTRIBUTES = {
|
|
63
|
+
...Object.fromEntries([...NUMBERS, ...BOOLEANS, ...STRINGS, "gaze", "fx", "ring"].map((k) => [
|
|
64
|
+
k,
|
|
65
|
+
k
|
|
66
|
+
])),
|
|
67
|
+
"react-key": "reactKey",
|
|
68
|
+
label: "title"
|
|
69
|
+
};
|
|
70
|
+
var clamp = (n) => Math.max(-1, Math.min(1, n));
|
|
71
|
+
function parseGaze(text) {
|
|
72
|
+
const t = text.trim();
|
|
73
|
+
if (t === "pointer")
|
|
74
|
+
return "pointer";
|
|
75
|
+
const parts = t.split(",");
|
|
76
|
+
if (parts.length !== 2 || parts.some((p) => p.trim() === ""))
|
|
77
|
+
return;
|
|
78
|
+
const [x, y] = parts.map((p) => Number(p));
|
|
79
|
+
if (Number.isNaN(x) || Number.isNaN(y))
|
|
80
|
+
return;
|
|
81
|
+
return [clamp(x), clamp(y)];
|
|
82
|
+
}
|
|
83
|
+
function parseAttribute(attr, text) {
|
|
84
|
+
const key = ATTRIBUTES[attr];
|
|
85
|
+
if (!key)
|
|
86
|
+
return;
|
|
87
|
+
if (text === null) {
|
|
88
|
+
return { key, value: undefined };
|
|
89
|
+
}
|
|
90
|
+
if (BOOLEANS.has(attr))
|
|
91
|
+
return { key, value: text !== "false" };
|
|
92
|
+
if (NUMBERS.has(attr)) {
|
|
93
|
+
if (text.trim() === "")
|
|
94
|
+
return;
|
|
95
|
+
const n = Number(text);
|
|
96
|
+
return Number.isNaN(n) ? undefined : { key, value: n };
|
|
97
|
+
}
|
|
98
|
+
if (attr === "gaze") {
|
|
99
|
+
const g = parseGaze(text);
|
|
100
|
+
return g === undefined ? undefined : { key, value: g };
|
|
101
|
+
}
|
|
102
|
+
if (attr === "ring") {
|
|
103
|
+
try {
|
|
104
|
+
const v = JSON.parse(text);
|
|
105
|
+
return v && typeof v === "object" && !Array.isArray(v) ? { key, value: v } : undefined;
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (attr === "fx") {
|
|
111
|
+
return {
|
|
112
|
+
key,
|
|
113
|
+
value: text === "false" ? false : text === "" || text === "true" ? true : text
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (attr === "label")
|
|
117
|
+
return { key, value: text === "false" ? false : text };
|
|
118
|
+
return { key, value: text };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/element.ts
|
|
122
|
+
var HANDLE = Symbol.for("awflow.avatar.element.handle");
|
|
123
|
+
var JOIN = Symbol.for("awflow.avatar.element.join");
|
|
124
|
+
var LEAVE = Symbol.for("awflow.avatar.element.leave");
|
|
125
|
+
var GROUP = Symbol.for("awflow.avatar.element.group");
|
|
126
|
+
var NOTIFY = Symbol.for("awflow.avatar.element.notify");
|
|
127
|
+
var AIM = Symbol.for("awflow.avatar.element.aim");
|
|
128
|
+
var serial = 0;
|
|
129
|
+
function domId(n, version) {
|
|
130
|
+
const v = typeof version === "number" ? version : LATEST;
|
|
131
|
+
return `${v >= 2 ? "awf" : "peek"}-e${n}`;
|
|
132
|
+
}
|
|
133
|
+
function upgradeProperties(el, keys) {
|
|
134
|
+
for (const k of keys) {
|
|
135
|
+
if (!Object.hasOwn(el, k))
|
|
136
|
+
continue;
|
|
137
|
+
const v = el[k];
|
|
138
|
+
delete el[k];
|
|
139
|
+
el[k] = v;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
var isAvatar = (x) => typeof x === "object" && x !== null && (HANDLE in x);
|
|
143
|
+
var isGroup = (x) => typeof x === "object" && x !== null && (GROUP in x);
|
|
144
|
+
function createElements() {
|
|
145
|
+
|
|
146
|
+
class Avatar extends HTMLElement {
|
|
147
|
+
static get observedAttributes() {
|
|
148
|
+
return Object.keys(ATTRIBUTES);
|
|
149
|
+
}
|
|
150
|
+
opts = {};
|
|
151
|
+
mounted = null;
|
|
152
|
+
leaveGroup = null;
|
|
153
|
+
lookTarget = null;
|
|
154
|
+
rendered = false;
|
|
155
|
+
serial = ++serial;
|
|
156
|
+
[NOTIFY] = new Set;
|
|
157
|
+
constructor() {
|
|
158
|
+
super();
|
|
159
|
+
upgradeProperties(this, ["ring", "gaze", "reaction", "reactKey"]);
|
|
160
|
+
}
|
|
161
|
+
[AIM]() {
|
|
162
|
+
const t = this.lookTarget;
|
|
163
|
+
this.mounted?.lookAt(isAvatar(t) ? t[HANDLE] : t);
|
|
164
|
+
}
|
|
165
|
+
changed() {
|
|
166
|
+
for (const w of this[NOTIFY])
|
|
167
|
+
w[AIM]();
|
|
168
|
+
}
|
|
169
|
+
get handle() {
|
|
170
|
+
return this.mounted;
|
|
171
|
+
}
|
|
172
|
+
get [HANDLE]() {
|
|
173
|
+
return this.mounted;
|
|
174
|
+
}
|
|
175
|
+
connectedCallback() {
|
|
176
|
+
for (const attr of Object.keys(ATTRIBUTES)) {
|
|
177
|
+
const p = this.hasAttribute(attr) ? parseAttribute(attr, this.getAttribute(attr)) : undefined;
|
|
178
|
+
if (p && !(p.key in this.opts))
|
|
179
|
+
this.opts[p.key] = p.value;
|
|
180
|
+
}
|
|
181
|
+
if (isAvatar(this.lookTarget))
|
|
182
|
+
this.lookTarget[NOTIFY].add(this);
|
|
183
|
+
this.mountNow();
|
|
184
|
+
}
|
|
185
|
+
disconnectedCallback() {
|
|
186
|
+
if (isAvatar(this.lookTarget))
|
|
187
|
+
this.lookTarget[NOTIFY].delete(this);
|
|
188
|
+
this[LEAVE]();
|
|
189
|
+
this.mounted?.destroy();
|
|
190
|
+
this.mounted = null;
|
|
191
|
+
this.changed();
|
|
192
|
+
}
|
|
193
|
+
attributeChangedCallback(attr, _old, value) {
|
|
194
|
+
const p = parseAttribute(attr, value);
|
|
195
|
+
if (!p)
|
|
196
|
+
return;
|
|
197
|
+
this.apply({ [p.key]: p.value });
|
|
198
|
+
}
|
|
199
|
+
apply(input) {
|
|
200
|
+
Object.assign(this.opts, input);
|
|
201
|
+
if (!this.isConnected)
|
|
202
|
+
return;
|
|
203
|
+
const patch = "version" in input ? { ...input, id: domId(this.serial, this.opts.version) } : input;
|
|
204
|
+
if (patch.name !== undefined || this.opts.name === undefined) {
|
|
205
|
+
if (patch.name === undefined) {
|
|
206
|
+
this.unmount();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (this.mounted) {
|
|
210
|
+
this.mounted.set(patch);
|
|
211
|
+
this[LEAVE]();
|
|
212
|
+
this[JOIN]();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.mountNow();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.mounted?.set(patch);
|
|
219
|
+
}
|
|
220
|
+
mountNow() {
|
|
221
|
+
this.mounted?.destroy();
|
|
222
|
+
this.mounted = null;
|
|
223
|
+
if (typeof this.opts.name !== "string") {
|
|
224
|
+
this.unmount();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.mounted = mount(this, {
|
|
228
|
+
...this.opts,
|
|
229
|
+
id: domId(this.serial, this.opts.version)
|
|
230
|
+
});
|
|
231
|
+
this.rendered = true;
|
|
232
|
+
this[AIM]();
|
|
233
|
+
this[JOIN]();
|
|
234
|
+
this.changed();
|
|
235
|
+
}
|
|
236
|
+
unmount() {
|
|
237
|
+
this[LEAVE]();
|
|
238
|
+
this.mounted?.destroy();
|
|
239
|
+
this.mounted = null;
|
|
240
|
+
if (this.rendered)
|
|
241
|
+
this.replaceChildren();
|
|
242
|
+
this.rendered = false;
|
|
243
|
+
this.changed();
|
|
244
|
+
}
|
|
245
|
+
[JOIN]() {
|
|
246
|
+
this[LEAVE]();
|
|
247
|
+
const handle = this.mounted;
|
|
248
|
+
if (!handle)
|
|
249
|
+
return;
|
|
250
|
+
for (let p = this.parentElement;p; p = p.parentElement) {
|
|
251
|
+
if (isGroup(p)) {
|
|
252
|
+
const off = p[GROUP].registerMember(String(this.opts.name ?? ""), handle);
|
|
253
|
+
this.leaveGroup = off;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
[LEAVE]() {
|
|
259
|
+
if (!this.leaveGroup)
|
|
260
|
+
return;
|
|
261
|
+
this.leaveGroup();
|
|
262
|
+
this.leaveGroup = null;
|
|
263
|
+
this[AIM]();
|
|
264
|
+
}
|
|
265
|
+
react(name) {
|
|
266
|
+
this.mounted?.react(name);
|
|
267
|
+
}
|
|
268
|
+
pulse(strength) {
|
|
269
|
+
this.mounted?.pulse(strength);
|
|
270
|
+
}
|
|
271
|
+
setVoice(source) {
|
|
272
|
+
this.mounted?.setVoice(source);
|
|
273
|
+
}
|
|
274
|
+
lookAt(target) {
|
|
275
|
+
if (isAvatar(this.lookTarget))
|
|
276
|
+
this.lookTarget[NOTIFY].delete(this);
|
|
277
|
+
this.lookTarget = target;
|
|
278
|
+
if (isAvatar(target) && this.isConnected)
|
|
279
|
+
target[NOTIFY].add(this);
|
|
280
|
+
this[AIM]();
|
|
281
|
+
}
|
|
282
|
+
get ring() {
|
|
283
|
+
return this.opts.ring;
|
|
284
|
+
}
|
|
285
|
+
set ring(v) {
|
|
286
|
+
this.apply({ ring: v ?? undefined });
|
|
287
|
+
}
|
|
288
|
+
get gaze() {
|
|
289
|
+
return this.opts.gaze;
|
|
290
|
+
}
|
|
291
|
+
set gaze(v) {
|
|
292
|
+
this.apply({ gaze: v ?? undefined });
|
|
293
|
+
}
|
|
294
|
+
get reaction() {
|
|
295
|
+
return this.opts.reaction;
|
|
296
|
+
}
|
|
297
|
+
set reaction(v) {
|
|
298
|
+
this.apply({ reaction: v ?? undefined });
|
|
299
|
+
}
|
|
300
|
+
get reactKey() {
|
|
301
|
+
return this.opts.reactKey;
|
|
302
|
+
}
|
|
303
|
+
set reactKey(v) {
|
|
304
|
+
this.apply({ reactKey: v });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class AvatarGroup extends HTMLElement {
|
|
309
|
+
static get observedAttributes() {
|
|
310
|
+
return ["speaker"];
|
|
311
|
+
}
|
|
312
|
+
[GROUP] = createGroup();
|
|
313
|
+
constructor() {
|
|
314
|
+
super();
|
|
315
|
+
upgradeProperties(this, ["speaker"]);
|
|
316
|
+
}
|
|
317
|
+
get speaker() {
|
|
318
|
+
return this.getAttribute("speaker");
|
|
319
|
+
}
|
|
320
|
+
set speaker(v) {
|
|
321
|
+
if (v === null)
|
|
322
|
+
this.removeAttribute("speaker");
|
|
323
|
+
else
|
|
324
|
+
this.setAttribute("speaker", v);
|
|
325
|
+
}
|
|
326
|
+
attributeChangedCallback(_attr, _old, value) {
|
|
327
|
+
this[GROUP].setSpeaker(value ?? undefined);
|
|
328
|
+
}
|
|
329
|
+
connectedCallback() {
|
|
330
|
+
for (const el of Array.from(this.querySelectorAll("*")))
|
|
331
|
+
if (isAvatar(el))
|
|
332
|
+
el[JOIN]();
|
|
333
|
+
}
|
|
334
|
+
disconnectedCallback() {
|
|
335
|
+
for (const el of Array.from(this.querySelectorAll("*")))
|
|
336
|
+
if (isAvatar(el))
|
|
337
|
+
el[LEAVE]();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
Avatar,
|
|
342
|
+
Group: AvatarGroup
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/define.ts
|
|
347
|
+
function defineAvatarElements(prefix = "awf") {
|
|
348
|
+
if (typeof customElements === "undefined" || typeof HTMLElement === "undefined")
|
|
349
|
+
return;
|
|
350
|
+
const avatar = `${prefix}-avatar`;
|
|
351
|
+
const group = `${prefix}-avatar-group`;
|
|
352
|
+
if (customElements.get(avatar) && customElements.get(group))
|
|
353
|
+
return;
|
|
354
|
+
const { Avatar, Group } = createElements();
|
|
355
|
+
if (!customElements.get(avatar))
|
|
356
|
+
customElements.define(avatar, Avatar);
|
|
357
|
+
if (!customElements.get(group))
|
|
358
|
+
customElements.define(group, Group);
|
|
359
|
+
}
|
|
360
|
+
|
|
1
361
|
// src/index.ts
|
|
2
362
|
defineAvatarElements();
|
|
3
363
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awflow/avatar-element",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"sideEffects": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@awflow/avatar": "^1.0.
|
|
22
|
+
"@awflow/avatar": "^1.0.1"
|
|
23
23
|
},
|
|
24
24
|
"description": "Deterministic SVG avatars from a name, as <awf-avatar> custom elements.",
|
|
25
25
|
"license": "MIT",
|