@doan-labs/peek 0.1.0 → 1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/identity.d.ts +4 -1
- package/dist/identity.js +22 -2
- package/dist/index.d.ts +6 -4
- package/dist/index.js +282 -87
- package/dist/svg.d.ts +1 -1
- package/dist/svg.js +203 -10
- package/dist/tables.d.ts +14 -1
- package/package.json +3 -1
- package/src/draw.ts +221 -1
- package/src/identity.ts +11 -0
- package/src/index.ts +9 -2
- package/src/peek.tsx +1 -1
- package/src/svg.ts +16 -2
- package/src/tables.ts +27 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Doan Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -19,6 +19,21 @@ import { Peek } from '@doan-labs/peek'
|
|
|
19
19
|
Use `animate` for blinking and expression transitions, and
|
|
20
20
|
`gaze='pointer'` to follow the pointer. Animation respects reduced motion.
|
|
21
21
|
|
|
22
|
+
## Accessories
|
|
23
|
+
|
|
24
|
+
Dress a face with three independent slots:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<Peek name='Linh' eyewear='glasses' headwear='bow' />
|
|
28
|
+
<Peek name='Linh' eyewear='sunglasses' headwear='sprout' />
|
|
29
|
+
<Peek name='Linh' headwear='cap' neckwear='tie' animate />
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Eyewear: `none`, `glasses`, `sunglasses`. Headwear: `none`, `bow`, `cap`,
|
|
33
|
+
`sprout`. Neckwear: `none`, `tie`. All default to `none` at `peek@1`, so
|
|
34
|
+
existing avatars stay unchanged. Slots combine and work with every
|
|
35
|
+
expression, gaze and frame. The same options work with `toSvg`.
|
|
36
|
+
|
|
22
37
|
## Plain SVG, without React
|
|
23
38
|
|
|
24
39
|
The SVG and identity entry points have no runtime dependencies:
|
|
@@ -36,3 +51,7 @@ The same normalized name and options produce the same SVG every time.
|
|
|
36
51
|
|
|
37
52
|
See the [documentation](https://peek.doan-labs.com/docs) for options,
|
|
38
53
|
expressions, identity axes and versioned styles.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
[MIT](./LICENSE) © Doan Labs
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Axis, type Brows, type Cheeks, type Color, type Eyes, type Face, type Mouth, type Trait } from './tables.js';
|
|
1
|
+
import { type Axis, type Brows, type Cheeks, type Color, type Eyes, type Eyewear, type Face, type Headwear, type Mouth, type Neckwear, type Trait } from './tables.js';
|
|
2
2
|
export declare const STYLE = "peek";
|
|
3
3
|
export declare const tidy: (s: string) => string;
|
|
4
4
|
/** FNV-1a over the UTF-8 bytes. */
|
|
@@ -34,6 +34,9 @@ export type Axes = {
|
|
|
34
34
|
mouth: Mouth;
|
|
35
35
|
cheeks: Cheeks;
|
|
36
36
|
trait: Trait;
|
|
37
|
+
eyewear: Eyewear;
|
|
38
|
+
headwear: Headwear;
|
|
39
|
+
neckwear: Neckwear;
|
|
37
40
|
};
|
|
38
41
|
export type Identity = Axes & {
|
|
39
42
|
/** The tidied name. */
|
package/dist/identity.js
CHANGED
|
@@ -116,8 +116,24 @@ var PARTS = {
|
|
|
116
116
|
cheeks: ["oval", "dots", "lines"],
|
|
117
117
|
trait: ["square", "fin", "ring", "dot", "peak"]
|
|
118
118
|
};
|
|
119
|
+
var ACCESSORIES = {
|
|
120
|
+
eyewear: ["none", "glasses", "sunglasses"],
|
|
121
|
+
headwear: ["none", "bow", "cap", "sprout"],
|
|
122
|
+
neckwear: ["none", "tie"]
|
|
123
|
+
};
|
|
119
124
|
var VERSIONS = {
|
|
120
|
-
1: {
|
|
125
|
+
1: {
|
|
126
|
+
face: 4,
|
|
127
|
+
color: 7,
|
|
128
|
+
eyes: 3,
|
|
129
|
+
brows: 4,
|
|
130
|
+
mouth: 4,
|
|
131
|
+
cheeks: 3,
|
|
132
|
+
trait: 5,
|
|
133
|
+
eyewear: 1,
|
|
134
|
+
headwear: 1,
|
|
135
|
+
neckwear: 1
|
|
136
|
+
}
|
|
121
137
|
};
|
|
122
138
|
var LATEST = 1;
|
|
123
139
|
var BASE = {
|
|
@@ -339,7 +355,8 @@ function mulberry32(a) {
|
|
|
339
355
|
var LISTS = {
|
|
340
356
|
face: Object.keys(FACES),
|
|
341
357
|
color: Object.keys(COLORS),
|
|
342
|
-
...PARTS
|
|
358
|
+
...PARTS,
|
|
359
|
+
...ACCESSORIES
|
|
343
360
|
};
|
|
344
361
|
var AXES = Object.keys(LISTS);
|
|
345
362
|
function seed(name, axis, version = LATEST) {
|
|
@@ -383,6 +400,9 @@ function identify(name, { version = LATEST } = {}) {
|
|
|
383
400
|
mouth: pick("mouth"),
|
|
384
401
|
cheeks: pick("cheeks"),
|
|
385
402
|
trait: pick("trait"),
|
|
403
|
+
eyewear: pick("eyewear"),
|
|
404
|
+
headwear: pick("headwear"),
|
|
405
|
+
neckwear: pick("neckwear"),
|
|
386
406
|
persona
|
|
387
407
|
};
|
|
388
408
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export type { Frame, Gaze } from './draw.js';
|
|
1
|
+
export type { Frame, Gaze, Node } from './draw.js';
|
|
2
|
+
/** Low level: the drawing tree that toSvg and <Peek> both render. */
|
|
3
|
+
export { draw } from './draw.js';
|
|
2
4
|
export type { Axes, Identity, Persona } from './identity.js';
|
|
3
5
|
export { AXES, identify, LISTS, tidy } from './identity.js';
|
|
4
6
|
export type { PeekProps } from './peek.js';
|
|
5
7
|
export { Peek } from './peek.js';
|
|
6
8
|
export type { PeekOptions } from './svg.js';
|
|
7
|
-
export { toSvg } from './svg.js';
|
|
8
|
-
export type { Axis, Brows, Channel, Cheeks, Color, Expression, Eyes, Face, Mouth, Trait, } from './tables.js';
|
|
9
|
-
export { COLORS, EXPRESSIONS, FACES, GROUPS, LATEST, PARTS, VERSIONS, } from './tables.js';
|
|
9
|
+
export { settle, toSvg } from './svg.js';
|
|
10
|
+
export type { AccessorySlot, Axis, Brows, Channel, Cheeks, Color, Expression, Eyes, Eyewear, Face, Headwear, Mouth, Neckwear, Trait, } from './tables.js';
|
|
11
|
+
export { ACCESSORIES, COLORS, EXPRESSIONS, FACES, GROUPS, LATEST, PARTS, VERSIONS, } from './tables.js';
|
package/dist/index.js
CHANGED
|
@@ -121,8 +121,25 @@ var PARTS = {
|
|
|
121
121
|
cheeks: ["oval", "dots", "lines"],
|
|
122
122
|
trait: ["square", "fin", "ring", "dot", "peak"]
|
|
123
123
|
};
|
|
124
|
+
var ACCESSORY_INK = { knot: "#E5412D" };
|
|
125
|
+
var ACCESSORIES = {
|
|
126
|
+
eyewear: ["none", "glasses", "sunglasses"],
|
|
127
|
+
headwear: ["none", "bow", "cap", "sprout"],
|
|
128
|
+
neckwear: ["none", "tie"]
|
|
129
|
+
};
|
|
124
130
|
var VERSIONS = {
|
|
125
|
-
1: {
|
|
131
|
+
1: {
|
|
132
|
+
face: 4,
|
|
133
|
+
color: 7,
|
|
134
|
+
eyes: 3,
|
|
135
|
+
brows: 4,
|
|
136
|
+
mouth: 4,
|
|
137
|
+
cheeks: 3,
|
|
138
|
+
trait: 5,
|
|
139
|
+
eyewear: 1,
|
|
140
|
+
headwear: 1,
|
|
141
|
+
neckwear: 1
|
|
142
|
+
}
|
|
126
143
|
};
|
|
127
144
|
var LATEST = 1;
|
|
128
145
|
var GROUPS = {
|
|
@@ -330,84 +347,6 @@ var EXPRESSIONS = {
|
|
|
330
347
|
})
|
|
331
348
|
};
|
|
332
349
|
|
|
333
|
-
// src/identity.ts
|
|
334
|
-
var STYLE = "peek";
|
|
335
|
-
var tidy = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
336
|
-
function fnv1a(str) {
|
|
337
|
-
let h = 2166136261;
|
|
338
|
-
for (const b of new TextEncoder().encode(str)) {
|
|
339
|
-
h ^= b;
|
|
340
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
341
|
-
}
|
|
342
|
-
return h >>> 0;
|
|
343
|
-
}
|
|
344
|
-
function mulberry32(a) {
|
|
345
|
-
return () => {
|
|
346
|
-
a = a + 1831565813 | 0;
|
|
347
|
-
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
348
|
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
349
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
var LISTS = {
|
|
353
|
-
face: Object.keys(FACES),
|
|
354
|
-
color: Object.keys(COLORS),
|
|
355
|
-
...PARTS
|
|
356
|
-
};
|
|
357
|
-
var AXES = Object.keys(LISTS);
|
|
358
|
-
function seed(name, axis, version = LATEST) {
|
|
359
|
-
return fnv1a(`${STYLE}@${version}:${axis}:${tidy(name)}`);
|
|
360
|
-
}
|
|
361
|
-
function identify(name, { version = LATEST } = {}) {
|
|
362
|
-
const lengths = VERSIONS[version];
|
|
363
|
-
if (!lengths)
|
|
364
|
-
throw new Error(`peek@${version} does not exist`);
|
|
365
|
-
const pick = (axis) => LISTS[axis][seed(name, axis, version) % lengths[axis]];
|
|
366
|
-
const rnd = mulberry32(seed(name, "persona", version));
|
|
367
|
-
const r = (a, b) => Math.round((a + rnd() * (b - a)) * 100) / 100;
|
|
368
|
-
const persona = {
|
|
369
|
-
spread: r(-7, 7),
|
|
370
|
-
blink: r(0.75, 1.4),
|
|
371
|
-
add: {
|
|
372
|
-
browY: r(-4, 4),
|
|
373
|
-
browTilt: r(-5, 5),
|
|
374
|
-
browArch: r(-0.2, 0.3),
|
|
375
|
-
rot: r(-3, 3),
|
|
376
|
-
hair: r(-0.2, 0.35),
|
|
377
|
-
gx: r(-0.12, 0.12),
|
|
378
|
-
my: r(-2, 3)
|
|
379
|
-
},
|
|
380
|
-
mul: {
|
|
381
|
-
eyeS: r(0.92, 1.08),
|
|
382
|
-
pupil: r(0.9, 1.12),
|
|
383
|
-
browW: r(0.85, 1.2),
|
|
384
|
-
mw: r(0.85, 1.18)
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
const key = tidy(name);
|
|
388
|
-
return {
|
|
389
|
-
key,
|
|
390
|
-
hash: fnv1a(key),
|
|
391
|
-
version,
|
|
392
|
-
face: pick("face"),
|
|
393
|
-
color: pick("color"),
|
|
394
|
-
eyes: pick("eyes"),
|
|
395
|
-
brows: pick("brows"),
|
|
396
|
-
mouth: pick("mouth"),
|
|
397
|
-
cheeks: pick("cheeks"),
|
|
398
|
-
trait: pick("trait"),
|
|
399
|
-
persona
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
// src/peek.tsx
|
|
403
|
-
import {
|
|
404
|
-
createElement,
|
|
405
|
-
useEffect,
|
|
406
|
-
useId,
|
|
407
|
-
useRef,
|
|
408
|
-
useState
|
|
409
|
-
} from "react";
|
|
410
|
-
|
|
411
350
|
// src/draw.ts
|
|
412
351
|
var VB = [-30, -30, 400, 400];
|
|
413
352
|
var FLOOR = 370;
|
|
@@ -421,6 +360,9 @@ var f2 = (v) => String(Math.round(v * 100) / 100);
|
|
|
421
360
|
var f3 = (v) => String(Math.round(v * 1000) / 1000);
|
|
422
361
|
var strokeFor = (px) => px >= 96 ? 1 : px >= 56 ? 1.25 : px >= 40 ? 1.6 : px >= 30 ? 2 : 2.4;
|
|
423
362
|
var RAD = Math.PI / 180;
|
|
363
|
+
var STEM = "M0 0Q3 -19 0 -36";
|
|
364
|
+
var LEAF0 = "M0 -33C-24 -30 -38 -52 -25 -53C-12 -54 -2 -43 0 -33Z";
|
|
365
|
+
var LEAF1 = "M0 -33C5 -54 28 -61 27 -48C26 -36 11 -31 0 -33Z";
|
|
424
366
|
function frameOf(face) {
|
|
425
367
|
const baseY = FLOOR + face.sink - face.bottom;
|
|
426
368
|
const eyeY = face.eyes[0][1];
|
|
@@ -795,7 +737,7 @@ function draw(who, pose, o) {
|
|
|
795
737
|
const side = i === 0 ? -1 : 1;
|
|
796
738
|
const hw = sh.brow.hw;
|
|
797
739
|
const c = -sh.brow.arch * arch;
|
|
798
|
-
const [
|
|
740
|
+
const [w2, ca, k2] = who.brows === "bar" ? [hw * 0.9, c * 0.3, 1.75] : who.brows === "dash" ? [hw * 0.42, c * 0.25, 1.9] : [hw, c, 1];
|
|
799
741
|
const ti = bw * 1.7;
|
|
800
742
|
const to = bw * 0.45;
|
|
801
743
|
const [tl, tr] = side < 0 ? [to, ti] : [ti, to];
|
|
@@ -808,7 +750,7 @@ function draw(who, pose, o) {
|
|
|
808
750
|
const sin = Math.sin(side * btilt * RAD);
|
|
809
751
|
const inside = (dy) => {
|
|
810
752
|
for (let t = 0;t <= 1; t += 0.25) {
|
|
811
|
-
const lx =
|
|
753
|
+
const lx = w2 * (2 * t - 1);
|
|
812
754
|
const ly = 2 * t * (1 - t) * ca;
|
|
813
755
|
const px = bx + lx * cos - ly * sin;
|
|
814
756
|
const py = by + dy + lx * sin + ly * cos;
|
|
@@ -843,7 +785,7 @@ function draw(who, pose, o) {
|
|
|
843
785
|
});
|
|
844
786
|
}
|
|
845
787
|
return n("path", key, {
|
|
846
|
-
d: `M${f2(-
|
|
788
|
+
d: `M${f2(-w2)} 0Q0 ${f2(ca)} ${f2(w2)} 0`,
|
|
847
789
|
transform,
|
|
848
790
|
fill: "none",
|
|
849
791
|
stroke: INK.ink,
|
|
@@ -933,8 +875,8 @@ function draw(who, pose, o) {
|
|
|
933
875
|
});
|
|
934
876
|
} else if (who.trait === "ring") {
|
|
935
877
|
const b = 35 - hair * 14 - (hair < 0 ? hair * -21 : 0) + lag * 0.9;
|
|
936
|
-
const
|
|
937
|
-
const s = seat(sh.crown, 8, Math.max(
|
|
878
|
+
const w2 = ringWalk(sh);
|
|
879
|
+
const s = seat(sh.crown, 8, Math.max(w2.min, w2.rest + 158 * (b - 35) * RAD));
|
|
938
880
|
traitT = `translate(${f2(s.x)} ${f2(s.y)}) scale(${f2(1 + Math.max(0, hair) * 0.1)})`;
|
|
939
881
|
shape = n("circle", "trait.shape", {
|
|
940
882
|
cx: 0,
|
|
@@ -968,6 +910,169 @@ function draw(who, pose, o) {
|
|
|
968
910
|
});
|
|
969
911
|
}
|
|
970
912
|
const trait = n("g", "trait", { transform: traitT }, [shape]);
|
|
913
|
+
const wardrobe = [];
|
|
914
|
+
const w = (px) => f2(px * Math.min(st, 1.6));
|
|
915
|
+
const line = {
|
|
916
|
+
fill: "none",
|
|
917
|
+
stroke: INK.ink,
|
|
918
|
+
"stroke-width": w(5.25),
|
|
919
|
+
"stroke-linecap": "round",
|
|
920
|
+
"stroke-linejoin": "round"
|
|
921
|
+
};
|
|
922
|
+
if (who.eyewear !== "none") {
|
|
923
|
+
const lx = sh.eyes[0][0] - spread;
|
|
924
|
+
const rr = sh.eyes[1][0] + spread;
|
|
925
|
+
const ey = sh.eyes[0][1];
|
|
926
|
+
const gap = rr - lx;
|
|
927
|
+
const erx = Math.min(rx + 9, (gap - 15) / 2);
|
|
928
|
+
const ery = ry + 9;
|
|
929
|
+
const glasses = who.eyewear === "glasses";
|
|
930
|
+
const lensX = glasses ? erx : Math.min(rx + 11, (gap - 12) / 2);
|
|
931
|
+
const top = glasses ? ey - 4 : ey - ry - 2;
|
|
932
|
+
const frames = [
|
|
933
|
+
n("path", "eyewear.bridge", {
|
|
934
|
+
...line,
|
|
935
|
+
d: `M${f2(lx + lensX)} ${f2(top)}Q${f2((lx + rr) / 2)} ${f2(top - (glasses ? 13 : 0))} ${f2(rr - lensX)} ${f2(top)}`
|
|
936
|
+
})
|
|
937
|
+
];
|
|
938
|
+
for (const [i, cx] of [lx, rr].entries()) {
|
|
939
|
+
const key = `eyewear.lens${i}`;
|
|
940
|
+
frames.push(n("path", `${key}.arm`, {
|
|
941
|
+
...line,
|
|
942
|
+
d: i === 0 ? `M${f2(cx - lensX - 9)} ${f2(top + 2)}L${f2(cx - lensX)} ${f2(top)}` : `M${f2(cx + lensX)} ${f2(top)}L${f2(cx + lensX + 9)} ${f2(top + 2)}`
|
|
943
|
+
}));
|
|
944
|
+
if (glasses) {
|
|
945
|
+
frames.push(n("ellipse", key, {
|
|
946
|
+
...line,
|
|
947
|
+
cx: f2(cx),
|
|
948
|
+
cy: ey,
|
|
949
|
+
rx: f2(erx),
|
|
950
|
+
ry: f2(ery)
|
|
951
|
+
}));
|
|
952
|
+
frames.push(n("path", `${key}.glint`, {
|
|
953
|
+
...line,
|
|
954
|
+
stroke: INK.paper,
|
|
955
|
+
"stroke-width": w(3),
|
|
956
|
+
d: `M${f2(cx - erx * 0.65)} ${f2(ey - ery * 0.45)}Q${f2(cx - erx * 0.5)} ${f2(ey - ery * 0.8)} ${f2(cx - erx * 0.2)} ${f2(ey - ery * 0.86)}`
|
|
957
|
+
}));
|
|
958
|
+
} else {
|
|
959
|
+
const h = ry * 2 + 9;
|
|
960
|
+
frames.push(n("path", key, {
|
|
961
|
+
d: `M${f2(cx - lensX)} ${f2(top)}H${f2(cx + lensX)}V${f2(top + h * 0.55)}Q${f2(cx + lensX)} ${f2(top + h)} ${f2(cx)} ${f2(top + h)}Q${f2(cx - lensX)} ${f2(top + h)} ${f2(cx - lensX)} ${f2(top + h * 0.55)}Z`,
|
|
962
|
+
fill: INK.ink,
|
|
963
|
+
stroke: INK.ink,
|
|
964
|
+
"stroke-width": w(3),
|
|
965
|
+
"stroke-linejoin": "round"
|
|
966
|
+
}));
|
|
967
|
+
frames.push(n("path", `${key}.glint`, {
|
|
968
|
+
...line,
|
|
969
|
+
stroke: INK.paper,
|
|
970
|
+
"stroke-width": w(4),
|
|
971
|
+
d: `M${f2(cx - lensX * 0.58)} ${f2(top + 19)}l8 -8m-1 15 6 -6`
|
|
972
|
+
}), n("circle", `${key}.pin`, {
|
|
973
|
+
cx: f2(cx + lensX * 0.55),
|
|
974
|
+
cy: f2(top + h - 9),
|
|
975
|
+
r: 1.7,
|
|
976
|
+
fill: INK.paper
|
|
977
|
+
}));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
wardrobe.push(n("g", "eyewear", {}, frames));
|
|
981
|
+
}
|
|
982
|
+
if (who.headwear !== "none") {
|
|
983
|
+
const side = who.trait === "ring" || who.trait === "dot" ? -1 : 1;
|
|
984
|
+
const cap = who.headwear === "cap";
|
|
985
|
+
const sprout = who.headwear === "sprout";
|
|
986
|
+
const inset = sprout ? -6 : cap ? -8 : 0;
|
|
987
|
+
const offset = sprout ? 34 : 40;
|
|
988
|
+
let lo = 0;
|
|
989
|
+
let hi = 160;
|
|
990
|
+
for (let i = 0;i < 16; i++) {
|
|
991
|
+
const mid2 = (lo + hi) / 2;
|
|
992
|
+
const p = seat(sh.crown, inset, side * mid2);
|
|
993
|
+
if (side * (p.x - 170) < offset)
|
|
994
|
+
lo = mid2;
|
|
995
|
+
else
|
|
996
|
+
hi = mid2;
|
|
997
|
+
}
|
|
998
|
+
const anchor = seat(sh.crown, inset, side * (lo + hi) / 2);
|
|
999
|
+
const angle = clamp(anchor.a, -40, 40);
|
|
1000
|
+
const parts = [];
|
|
1001
|
+
if (cap) {
|
|
1002
|
+
parts.push(n("path", "headwear.crown", {
|
|
1003
|
+
...line,
|
|
1004
|
+
fill: INK.paper,
|
|
1005
|
+
d: "M-33 0C-35 -20 -20 -39 0 -39C20 -39 35 -20 33 0Z"
|
|
1006
|
+
}), n("path", "headwear.seam", {
|
|
1007
|
+
...line,
|
|
1008
|
+
"stroke-width": w(3.5),
|
|
1009
|
+
d: "M-4 -38Q9 -24 8 -1"
|
|
1010
|
+
}), n("path", "headwear.brim", {
|
|
1011
|
+
...line,
|
|
1012
|
+
"stroke-width": w(7),
|
|
1013
|
+
d: side > 0 ? "M-35 1Q8 4 52 3" : "M-52 3Q-8 4 35 1"
|
|
1014
|
+
}), n("circle", "headwear.button", {
|
|
1015
|
+
cx: 0,
|
|
1016
|
+
cy: -40,
|
|
1017
|
+
r: 3.5,
|
|
1018
|
+
fill: INK.ink
|
|
1019
|
+
}));
|
|
1020
|
+
} else if (sprout) {
|
|
1021
|
+
parts.push(n("path", "headwear.halo", {
|
|
1022
|
+
...line,
|
|
1023
|
+
stroke: fill,
|
|
1024
|
+
"stroke-width": w(11),
|
|
1025
|
+
d: `${STEM}${LEAF0}${LEAF1}`
|
|
1026
|
+
}), n("path", "headwear.stem", {
|
|
1027
|
+
...line,
|
|
1028
|
+
"stroke-width": w(4.5),
|
|
1029
|
+
d: STEM
|
|
1030
|
+
}), n("path", "headwear.leaf0", {
|
|
1031
|
+
...line,
|
|
1032
|
+
"stroke-width": w(4),
|
|
1033
|
+
fill: COLORS.mint.body,
|
|
1034
|
+
d: LEAF0
|
|
1035
|
+
}), n("path", "headwear.leaf1", {
|
|
1036
|
+
...line,
|
|
1037
|
+
"stroke-width": w(4),
|
|
1038
|
+
fill: COLORS.mint.deep,
|
|
1039
|
+
d: LEAF1
|
|
1040
|
+
}));
|
|
1041
|
+
} else {
|
|
1042
|
+
parts.push(n("path", "headwear.wings", {
|
|
1043
|
+
d: "M-3 0C-13 -16 -28 -23 -29 -10C-31 2 -26 19 -16 13L-3 4L3 4L16 13C26 19 31 2 29 -10C28 -23 13 -16 3 0Z",
|
|
1044
|
+
fill: INK.ink,
|
|
1045
|
+
stroke: fill,
|
|
1046
|
+
"stroke-width": w(6),
|
|
1047
|
+
"stroke-linejoin": "round",
|
|
1048
|
+
"paint-order": "stroke"
|
|
1049
|
+
}), n("ellipse", "headwear.knot", {
|
|
1050
|
+
cx: 0,
|
|
1051
|
+
cy: 1,
|
|
1052
|
+
rx: 6,
|
|
1053
|
+
ry: 8,
|
|
1054
|
+
fill: ACCESSORY_INK.knot
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
wardrobe.push(n("g", "headwear", {
|
|
1058
|
+
transform: `translate(${f2(anchor.x)} ${f2(anchor.y)}) rotate(${f2(sprout ? angle * 0.5 : angle)})`
|
|
1059
|
+
}, parts));
|
|
1060
|
+
}
|
|
1061
|
+
if (who.neckwear === "tie") {
|
|
1062
|
+
const skew = Math.abs(Math.sin(g("mk") * RAD)) * mw;
|
|
1063
|
+
const bottom = Math.max(0, mt * kb, mb * kb) + skew + m.sw * st;
|
|
1064
|
+
wardrobe.push(n("g", "neckwear", {
|
|
1065
|
+
transform: `translate(${m.x} ${f2(m.y + g("my") + bottom + 8)})`,
|
|
1066
|
+
fill: INK.ink
|
|
1067
|
+
}, [
|
|
1068
|
+
n("path", "neckwear.knot", {
|
|
1069
|
+
d: "M-9 0Q-11 0 -9 4L-5 11Q0 15 5 11L9 4Q11 0 9 0Z"
|
|
1070
|
+
}),
|
|
1071
|
+
n("path", "neckwear.blade", {
|
|
1072
|
+
d: "M-4 15L-13 63Q-14 66 -11 69L0 79L11 69Q14 66 13 63L4 15Z"
|
|
1073
|
+
})
|
|
1074
|
+
]));
|
|
1075
|
+
}
|
|
971
1076
|
const off = (dx, dy) => riso ? `translate(${f2(dx * k * upp)} ${f2(dy * k * upp)})` : undefined;
|
|
972
1077
|
const av = n("g", "av", {
|
|
973
1078
|
transform: `translate(${f2(x)} ${f2(ty)}) rotate(${f2(rot)} 170 ${B}) translate(170 ${B}) scale(${sc(sx)} ${sc(sy)}) translate(-170 ${-B})`,
|
|
@@ -980,7 +1085,7 @@ function draw(who, pose, o) {
|
|
|
980
1085
|
n("g", "plateB", {
|
|
981
1086
|
transform: off(0.6, -0.5),
|
|
982
1087
|
filter: riso ? `url(#${id}-pB)` : undefined
|
|
983
|
-
}, [...eyes, ...brows, mouth])
|
|
1088
|
+
}, [...eyes, ...brows, mouth, ...wardrobe])
|
|
984
1089
|
]);
|
|
985
1090
|
const scene = [];
|
|
986
1091
|
if (o.frame !== "none") {
|
|
@@ -1023,6 +1128,87 @@ function draw(who, pose, o) {
|
|
|
1023
1128
|
]);
|
|
1024
1129
|
return o.live ? root : prune(root) ?? root;
|
|
1025
1130
|
}
|
|
1131
|
+
// src/identity.ts
|
|
1132
|
+
var STYLE = "peek";
|
|
1133
|
+
var tidy = (s) => s.normalize("NFC").trim().replace(/\s+/g, " ").toLowerCase();
|
|
1134
|
+
function fnv1a(str) {
|
|
1135
|
+
let h = 2166136261;
|
|
1136
|
+
for (const b of new TextEncoder().encode(str)) {
|
|
1137
|
+
h ^= b;
|
|
1138
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
1139
|
+
}
|
|
1140
|
+
return h >>> 0;
|
|
1141
|
+
}
|
|
1142
|
+
function mulberry32(a) {
|
|
1143
|
+
return () => {
|
|
1144
|
+
a = a + 1831565813 | 0;
|
|
1145
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
1146
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1147
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
var LISTS = {
|
|
1151
|
+
face: Object.keys(FACES),
|
|
1152
|
+
color: Object.keys(COLORS),
|
|
1153
|
+
...PARTS,
|
|
1154
|
+
...ACCESSORIES
|
|
1155
|
+
};
|
|
1156
|
+
var AXES = Object.keys(LISTS);
|
|
1157
|
+
function seed(name, axis, version = LATEST) {
|
|
1158
|
+
return fnv1a(`${STYLE}@${version}:${axis}:${tidy(name)}`);
|
|
1159
|
+
}
|
|
1160
|
+
function identify(name, { version = LATEST } = {}) {
|
|
1161
|
+
const lengths = VERSIONS[version];
|
|
1162
|
+
if (!lengths)
|
|
1163
|
+
throw new Error(`peek@${version} does not exist`);
|
|
1164
|
+
const pick = (axis) => LISTS[axis][seed(name, axis, version) % lengths[axis]];
|
|
1165
|
+
const rnd = mulberry32(seed(name, "persona", version));
|
|
1166
|
+
const r = (a, b) => Math.round((a + rnd() * (b - a)) * 100) / 100;
|
|
1167
|
+
const persona = {
|
|
1168
|
+
spread: r(-7, 7),
|
|
1169
|
+
blink: r(0.75, 1.4),
|
|
1170
|
+
add: {
|
|
1171
|
+
browY: r(-4, 4),
|
|
1172
|
+
browTilt: r(-5, 5),
|
|
1173
|
+
browArch: r(-0.2, 0.3),
|
|
1174
|
+
rot: r(-3, 3),
|
|
1175
|
+
hair: r(-0.2, 0.35),
|
|
1176
|
+
gx: r(-0.12, 0.12),
|
|
1177
|
+
my: r(-2, 3)
|
|
1178
|
+
},
|
|
1179
|
+
mul: {
|
|
1180
|
+
eyeS: r(0.92, 1.08),
|
|
1181
|
+
pupil: r(0.9, 1.12),
|
|
1182
|
+
browW: r(0.85, 1.2),
|
|
1183
|
+
mw: r(0.85, 1.18)
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
const key = tidy(name);
|
|
1187
|
+
return {
|
|
1188
|
+
key,
|
|
1189
|
+
hash: fnv1a(key),
|
|
1190
|
+
version,
|
|
1191
|
+
face: pick("face"),
|
|
1192
|
+
color: pick("color"),
|
|
1193
|
+
eyes: pick("eyes"),
|
|
1194
|
+
brows: pick("brows"),
|
|
1195
|
+
mouth: pick("mouth"),
|
|
1196
|
+
cheeks: pick("cheeks"),
|
|
1197
|
+
trait: pick("trait"),
|
|
1198
|
+
eyewear: pick("eyewear"),
|
|
1199
|
+
headwear: pick("headwear"),
|
|
1200
|
+
neckwear: pick("neckwear"),
|
|
1201
|
+
persona
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
// src/peek.tsx
|
|
1205
|
+
import {
|
|
1206
|
+
createElement,
|
|
1207
|
+
useEffect,
|
|
1208
|
+
useId,
|
|
1209
|
+
useRef,
|
|
1210
|
+
useState
|
|
1211
|
+
} from "react";
|
|
1026
1212
|
|
|
1027
1213
|
// src/animate.ts
|
|
1028
1214
|
var GROUP_OF = {};
|
|
@@ -1601,6 +1787,7 @@ class Live {
|
|
|
1601
1787
|
}
|
|
1602
1788
|
|
|
1603
1789
|
// src/svg.ts
|
|
1790
|
+
var wear = (o, base, s) => ACCESSORIES[s].includes(o[s]) ? o[s] : base[s];
|
|
1604
1791
|
function settle(name, o, live = false) {
|
|
1605
1792
|
const base = identify(name, { version: o.version });
|
|
1606
1793
|
const who = {
|
|
@@ -1611,7 +1798,10 @@ function settle(name, o, live = false) {
|
|
|
1611
1798
|
brows: o.brows ?? base.brows,
|
|
1612
1799
|
mouth: o.mouth ?? base.mouth,
|
|
1613
1800
|
cheeks: o.cheeks ?? base.cheeks,
|
|
1614
|
-
trait: o.trait ?? base.trait
|
|
1801
|
+
trait: o.trait ?? base.trait,
|
|
1802
|
+
eyewear: wear(o, base, "eyewear"),
|
|
1803
|
+
headwear: wear(o, base, "headwear"),
|
|
1804
|
+
neckwear: wear(o, base, "neckwear")
|
|
1615
1805
|
};
|
|
1616
1806
|
const pose = restPose(who, o.expression ?? "normal", o.gaze);
|
|
1617
1807
|
const look = {
|
|
@@ -1621,7 +1811,9 @@ function settle(name, o, live = false) {
|
|
|
1621
1811
|
riso: o.riso ?? false,
|
|
1622
1812
|
live
|
|
1623
1813
|
};
|
|
1624
|
-
const
|
|
1814
|
+
const { eyewear, headwear, neckwear, ...legacy } = who;
|
|
1815
|
+
const wardrobe = eyewear !== "none" || headwear !== "none" || neckwear !== "none";
|
|
1816
|
+
const id = o.id ?? `peek-${fnv1a(JSON.stringify([wardrobe ? who : legacy, pose, look])).toString(36)}`;
|
|
1625
1817
|
return { who, pose, opts: { ...look, id, title: o.title ?? name } };
|
|
1626
1818
|
}
|
|
1627
1819
|
var ILLEGAL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
@@ -1684,6 +1876,7 @@ function Peek({
|
|
|
1684
1876
|
return toReact(tree, { ref: svg, className, style });
|
|
1685
1877
|
}
|
|
1686
1878
|
export {
|
|
1879
|
+
ACCESSORIES,
|
|
1687
1880
|
AXES,
|
|
1688
1881
|
COLORS,
|
|
1689
1882
|
EXPRESSIONS,
|
|
@@ -1694,7 +1887,9 @@ export {
|
|
|
1694
1887
|
PARTS,
|
|
1695
1888
|
Peek,
|
|
1696
1889
|
VERSIONS,
|
|
1890
|
+
draw,
|
|
1697
1891
|
identify,
|
|
1892
|
+
settle,
|
|
1698
1893
|
tidy,
|
|
1699
1894
|
toSvg
|
|
1700
1895
|
};
|
package/dist/svg.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DrawOptions, type Frame, type Gaze, type Node, type Pose } from './draw.js';
|
|
2
2
|
import { type Axes, type Identity } from './identity.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Expression } from './tables.js';
|
|
4
4
|
export type PeekOptions = Partial<Axes> & {
|
|
5
5
|
/** px. Drives the stroke ramp (thicker below 96) and the riso print. */
|
|
6
6
|
size?: number;
|