@doan-labs/peek 1.0.0 → 2.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/LICENSE +21 -0
- package/README.md +64 -1
- package/dist/bloc.d.ts +2 -0
- package/dist/core.d.ts +9 -0
- package/dist/draw.d.ts +24 -2
- package/dist/identity.d.ts +4 -1
- package/dist/identity.js +22 -2
- package/dist/index.d.ts +1 -7
- package/dist/index.js +719 -142
- package/dist/peek.svelte +75 -0
- package/dist/peek.svelte.d.ts +5 -0
- package/dist/svelte-peek.d.ts +14 -0
- package/dist/svelte-peek.js +2205 -0
- package/dist/svelte.d.ts +3 -0
- package/dist/svelte.js +1650 -0
- package/dist/svg.d.ts +3 -1
- package/dist/svg.js +737 -166
- package/dist/tables.d.ts +67 -1
- package/dist/vue.d.ts +17 -0
- package/dist/vue.js +2302 -0
- package/package.json +30 -5
- package/src/bloc.ts +326 -0
- package/src/core.ts +39 -0
- package/src/draw.ts +522 -109
- package/src/identity.ts +11 -0
- package/src/index.ts +1 -26
- package/src/peek.svelte +75 -0
- package/src/peek.svelte.d.ts +5 -0
- package/src/peek.tsx +1 -1
- package/src/svelte-peek.ts +18 -0
- package/src/svelte.ts +3 -0
- package/src/svg.ts +30 -2
- package/src/tables.ts +74 -2
- package/src/vue.ts +133 -0
package/src/identity.ts
CHANGED
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
ACCESSORIES,
|
|
12
13
|
type Axis,
|
|
13
14
|
type Brows,
|
|
14
15
|
type Cheeks,
|
|
15
16
|
COLORS,
|
|
16
17
|
type Color,
|
|
17
18
|
type Eyes,
|
|
19
|
+
type Eyewear,
|
|
18
20
|
FACES,
|
|
19
21
|
type Face,
|
|
22
|
+
type Headwear,
|
|
20
23
|
LATEST,
|
|
21
24
|
type Mouth,
|
|
25
|
+
type Neckwear,
|
|
22
26
|
PARTS,
|
|
23
27
|
type Trait,
|
|
24
28
|
VERSIONS,
|
|
@@ -74,6 +78,9 @@ export type Axes = {
|
|
|
74
78
|
mouth: Mouth
|
|
75
79
|
cheeks: Cheeks
|
|
76
80
|
trait: Trait
|
|
81
|
+
eyewear: Eyewear
|
|
82
|
+
headwear: Headwear
|
|
83
|
+
neckwear: Neckwear
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export type Identity = Axes & {
|
|
@@ -90,6 +97,7 @@ export const LISTS: { readonly [A in Axis]: readonly Axes[A][] } = {
|
|
|
90
97
|
face: Object.keys(FACES) as Face[],
|
|
91
98
|
color: Object.keys(COLORS) as Color[],
|
|
92
99
|
...PARTS,
|
|
100
|
+
...ACCESSORIES,
|
|
93
101
|
}
|
|
94
102
|
/** The axes in readout order. */
|
|
95
103
|
export const AXES = Object.keys(LISTS) as Axis[]
|
|
@@ -142,6 +150,9 @@ export function identify(
|
|
|
142
150
|
mouth: pick('mouth'),
|
|
143
151
|
cheeks: pick('cheeks'),
|
|
144
152
|
trait: pick('trait'),
|
|
153
|
+
eyewear: pick('eyewear'),
|
|
154
|
+
headwear: pick('headwear'),
|
|
155
|
+
neckwear: pick('neckwear'),
|
|
145
156
|
persona,
|
|
146
157
|
}
|
|
147
158
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export type { Axes, Identity, Persona } from './identity'
|
|
3
|
-
export { AXES, identify, LISTS, tidy } from './identity'
|
|
1
|
+
export * from './core'
|
|
4
2
|
export type { PeekProps } from './peek'
|
|
5
3
|
export { Peek } from './peek'
|
|
6
|
-
export type { PeekOptions } from './svg'
|
|
7
|
-
export { toSvg } from './svg'
|
|
8
|
-
export type {
|
|
9
|
-
Axis,
|
|
10
|
-
Brows,
|
|
11
|
-
Channel,
|
|
12
|
-
Cheeks,
|
|
13
|
-
Color,
|
|
14
|
-
Expression,
|
|
15
|
-
Eyes,
|
|
16
|
-
Face,
|
|
17
|
-
Mouth,
|
|
18
|
-
Trait,
|
|
19
|
-
} from './tables'
|
|
20
|
-
export {
|
|
21
|
-
COLORS,
|
|
22
|
-
EXPRESSIONS,
|
|
23
|
-
FACES,
|
|
24
|
-
GROUPS,
|
|
25
|
-
LATEST,
|
|
26
|
-
PARTS,
|
|
27
|
-
VERSIONS,
|
|
28
|
-
} from './tables'
|
package/src/peek.svelte
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
<Peek name="Ada" /> for Svelte 5: the same tree and the same rules as the
|
|
3
|
+
React component in peek.tsx. ids come from $props.id(), so the server and
|
|
4
|
+
the client agree. The inside of the svg is the exact markup toSvg writes.
|
|
5
|
+
|
|
6
|
+
With `animate`, the markup stays in the pose the component mounted with and
|
|
7
|
+
the shared loop writes every frame straight to the DOM, so a change of
|
|
8
|
+
expression or gaze never reaches Svelte.
|
|
9
|
+
-->
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { untrack } from 'svelte'
|
|
12
|
+
import {
|
|
13
|
+
draw,
|
|
14
|
+
Live,
|
|
15
|
+
type PeekProps,
|
|
16
|
+
restPose,
|
|
17
|
+
serialize,
|
|
18
|
+
settle,
|
|
19
|
+
} from './svelte-peek'
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
name,
|
|
23
|
+
gaze,
|
|
24
|
+
animate = false,
|
|
25
|
+
class: className,
|
|
26
|
+
style,
|
|
27
|
+
...rest
|
|
28
|
+
}: PeekProps = $props()
|
|
29
|
+
const uid = $props.id()
|
|
30
|
+
const id = `peek${uid.replace(/[^\w-]/g, '')}`
|
|
31
|
+
const fixedOf = (g: PeekProps['gaze']) =>
|
|
32
|
+
Array.isArray(g) ? (g as readonly [number, number]) : undefined
|
|
33
|
+
const first = untrack(() => ({
|
|
34
|
+
expression: rest.expression ?? 'normal',
|
|
35
|
+
gaze: fixedOf(gaze),
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
const s = $derived(settle(name, { ...rest, gaze: fixedOf(gaze), id }, animate))
|
|
39
|
+
const tree = $derived(
|
|
40
|
+
draw(
|
|
41
|
+
s.who,
|
|
42
|
+
animate ? restPose(s.who, first.expression, first.gaze) : s.pose,
|
|
43
|
+
s.opts,
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
const inner = $derived(tree.children.map(serialize).join(''))
|
|
47
|
+
const expression = $derived(rest.expression ?? 'normal')
|
|
48
|
+
// live: the face only restarts when what the face is changes
|
|
49
|
+
const sig = $derived(JSON.stringify([s.who, s.opts]))
|
|
50
|
+
const gazeKey = $derived(String(gaze ?? ''))
|
|
51
|
+
|
|
52
|
+
let svg: SVGSVGElement | undefined
|
|
53
|
+
let live: Live | null = null
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
void sig
|
|
57
|
+
if (!animate || !svg) return
|
|
58
|
+
const l = untrack(() => new Live(svg!, tree, s.who, s.opts, expression, gaze))
|
|
59
|
+
live = l
|
|
60
|
+
return () => {
|
|
61
|
+
l.destroy()
|
|
62
|
+
live = null
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
$effect(() => {
|
|
66
|
+
live?.setExpression(expression)
|
|
67
|
+
})
|
|
68
|
+
$effect(() => {
|
|
69
|
+
void gazeKey
|
|
70
|
+
untrack(() => live?.setGaze(gaze))
|
|
71
|
+
})
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<!-- spread, so an unset class is no attribute rather than class="" -->
|
|
75
|
+
<svg bind:this={svg} {...tree.attrs} {...{ class: className, style }}>{@html inner}</svg>
|
package/src/peek.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* come from useId, so the server and the client agree.
|
|
4
4
|
*
|
|
5
5
|
* A name decides identity unless the caller overrides an axis: pass `face`,
|
|
6
|
-
* `color`,
|
|
6
|
+
* `color`, a part, or a wardrobe slot and that axis wins
|
|
7
7
|
* over the hash while every other axis stays as the name made it.
|
|
8
8
|
*
|
|
9
9
|
* With `animate`, the first paint (server and hydration) is still the static
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* What peek.svelte imports. Built as its own file, since the component ships
|
|
2
|
+
* as source and the app's bundler compiles it. */
|
|
3
|
+
import type { Gaze } from './draw'
|
|
4
|
+
import type { PeekOptions } from './svg'
|
|
5
|
+
|
|
6
|
+
export { Live } from './animate'
|
|
7
|
+
export { draw, restPose } from './draw'
|
|
8
|
+
export { serialize, settle } from './svg'
|
|
9
|
+
|
|
10
|
+
export type PeekProps = Omit<PeekOptions, 'gaze' | 'id'> & {
|
|
11
|
+
name: string
|
|
12
|
+
/** x and y in -1..1, or 'pointer' to follow the pointer (animate only). */
|
|
13
|
+
gaze?: Gaze | 'pointer'
|
|
14
|
+
/** Bring it to life after mount. Off under prefers-reduced-motion. */
|
|
15
|
+
animate?: boolean
|
|
16
|
+
class?: string
|
|
17
|
+
style?: string
|
|
18
|
+
}
|
package/src/svelte.ts
ADDED
package/src/svg.ts
CHANGED
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
restPose,
|
|
14
14
|
} from './draw'
|
|
15
15
|
import { type Axes, fnv1a, type Identity, identify } from './identity'
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
ACCESSORIES,
|
|
18
|
+
type AccessorySlot,
|
|
19
|
+
type Expression,
|
|
20
|
+
VARIANTS,
|
|
21
|
+
type Variant,
|
|
22
|
+
} from './tables'
|
|
17
23
|
|
|
18
24
|
export type PeekOptions = Partial<Axes> & {
|
|
19
25
|
/** px. Drives the stroke ramp (thicker below 96) and the riso print. */
|
|
@@ -30,11 +36,19 @@ export type PeekOptions = Partial<Axes> & {
|
|
|
30
36
|
riso?: boolean
|
|
31
37
|
/** Pin a style version. Default the latest. */
|
|
32
38
|
version?: number
|
|
39
|
+
/** How the face is drawn: 'peek', 'haze' or 'bloc'. Never who it is. */
|
|
40
|
+
variant?: Variant
|
|
33
41
|
/** Prefix for ids inside the SVG. Default derived from everything drawn,
|
|
34
42
|
* so two different avatars inlined in one page never share defs. */
|
|
35
43
|
id?: string
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
// an unknown item from untyped callers keeps what the name picked
|
|
47
|
+
const wear = <S extends AccessorySlot>(o: PeekOptions, base: Identity, s: S) =>
|
|
48
|
+
(ACCESSORIES[s] as readonly string[]).includes(o[s] as string)
|
|
49
|
+
? (o[s] as Identity[S])
|
|
50
|
+
: base[s]
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* A name decides identity unless the caller overrides an axis: an explicit
|
|
40
54
|
* face, color or part wins over the hash, and every other axis stays put.
|
|
@@ -54,17 +68,31 @@ export function settle(
|
|
|
54
68
|
mouth: o.mouth ?? base.mouth,
|
|
55
69
|
cheeks: o.cheeks ?? base.cheeks,
|
|
56
70
|
trait: o.trait ?? base.trait,
|
|
71
|
+
eyewear: wear(o, base, 'eyewear'),
|
|
72
|
+
headwear: wear(o, base, 'headwear'),
|
|
73
|
+
neckwear: wear(o, base, 'neckwear'),
|
|
57
74
|
}
|
|
58
75
|
const pose = restPose(who, o.expression ?? 'normal', o.gaze)
|
|
76
|
+
// an unknown variant from untyped callers draws peek
|
|
77
|
+
const variant = VARIANTS.includes(o.variant as Variant)
|
|
78
|
+
? (o.variant as Variant)
|
|
79
|
+
: 'peek'
|
|
59
80
|
const look = {
|
|
60
81
|
size: o.size ?? 64,
|
|
61
82
|
frame: o.frame ?? 'ink',
|
|
62
83
|
square: o.square ?? true,
|
|
63
84
|
riso: o.riso ?? false,
|
|
64
85
|
live,
|
|
86
|
+
// left out for peek, so its ids and bytes stay as they were
|
|
87
|
+
...(variant !== 'peek' && { variant }),
|
|
65
88
|
}
|
|
89
|
+
// Keep the byte-level peek@1 contract for faces without accessories.
|
|
90
|
+
const { eyewear, headwear, neckwear, ...legacy } = who
|
|
91
|
+
const wardrobe =
|
|
92
|
+
eyewear !== 'none' || headwear !== 'none' || neckwear !== 'none'
|
|
66
93
|
const id =
|
|
67
|
-
o.id ??
|
|
94
|
+
o.id ??
|
|
95
|
+
`peek-${fnv1a(JSON.stringify([wardrobe ? who : legacy, pose, look])).toString(36)}`
|
|
68
96
|
return { who, pose, opts: { ...look, id, title: o.title ?? name } }
|
|
69
97
|
}
|
|
70
98
|
|
package/src/tables.ts
CHANGED
|
@@ -175,6 +175,53 @@ export const COLORS = {
|
|
|
175
175
|
aqua: { body: '#BDE1E5', deep: '#8FC7D1' },
|
|
176
176
|
} as const
|
|
177
177
|
|
|
178
|
+
/*
|
|
179
|
+
* Variants: how a face is drawn, never who it is. Every variant reads the
|
|
180
|
+
* same identity, so a name keeps its face, color and parts in all of them.
|
|
181
|
+
*/
|
|
182
|
+
export const VARIANTS = ['peek', 'haze', 'bloc'] as const
|
|
183
|
+
export type Variant = (typeof VARIANTS)[number]
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Haze, transcribed from the Haze sheet (01.3). Each ink has a glow and a
|
|
187
|
+
* tint; a body is hazed by the tint of one of its two neighbors on the hue
|
|
188
|
+
* wheel, so the tint is never picked by hand.
|
|
189
|
+
*/
|
|
190
|
+
export const HAZE = {
|
|
191
|
+
lavender: { tint: '#C8B1F5', glow: '#F2EDFD' },
|
|
192
|
+
fog: { tint: '#9EC3F2', glow: '#E6F1FF' },
|
|
193
|
+
clay: { tint: '#F4AE93', glow: '#FEECE5' },
|
|
194
|
+
mint: { tint: '#A3DAB5', glow: '#E5F5EA' },
|
|
195
|
+
butter: { tint: '#E1CC7A', glow: '#F4F0E0' },
|
|
196
|
+
rose: { tint: '#F0A9C4', glow: '#FDEAF1' },
|
|
197
|
+
aqua: { tint: '#85D3DC', glow: '#E0F5F7' },
|
|
198
|
+
} as const satisfies Record<keyof typeof COLORS, unknown>
|
|
199
|
+
/** The hue wheel, warm to cool: a body's warm side is the entry before it. */
|
|
200
|
+
export const WHEEL = [
|
|
201
|
+
'clay',
|
|
202
|
+
'butter',
|
|
203
|
+
'mint',
|
|
204
|
+
'aqua',
|
|
205
|
+
'fog',
|
|
206
|
+
'lavender',
|
|
207
|
+
'rose',
|
|
208
|
+
] as const satisfies readonly (keyof typeof COLORS)[]
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Bloc's strong inks, palette P2 of the Bloc sheet (02.4): the same hue at
|
|
212
|
+
* OKLCH L 0.72 and C 0.12, butter, mint and aqua held lighter so none goes
|
|
213
|
+
* olive. The strong ink fills the dark half and the trait.
|
|
214
|
+
*/
|
|
215
|
+
export const BLOC = {
|
|
216
|
+
lavender: '#B093E5',
|
|
217
|
+
fog: '#7BA8DF',
|
|
218
|
+
clay: '#E38A66',
|
|
219
|
+
mint: '#82CB9B',
|
|
220
|
+
butter: '#E2CA6A',
|
|
221
|
+
rose: '#DF84A8',
|
|
222
|
+
aqua: '#5EC3CD',
|
|
223
|
+
} as const satisfies Record<keyof typeof COLORS, string>
|
|
224
|
+
|
|
178
225
|
/** Discrete anatomy. Each part reads every channel its feature owns. */
|
|
179
226
|
export const PARTS = {
|
|
180
227
|
eyes: ['oval', 'bead', 'ring'],
|
|
@@ -184,6 +231,20 @@ export const PARTS = {
|
|
|
184
231
|
trait: ['square', 'fin', 'ring', 'dot', 'peak'],
|
|
185
232
|
} as const
|
|
186
233
|
|
|
234
|
+
// The bow's single warm accent, transcribed from the accessory sheet.
|
|
235
|
+
export const ACCESSORY_INK = { knot: '#E5412D' } as const
|
|
236
|
+
|
|
237
|
+
/** Independent wardrobe slots. peek@1 picks none; callers can dress it. */
|
|
238
|
+
export const ACCESSORIES = {
|
|
239
|
+
eyewear: ['none', 'glasses', 'sunglasses'],
|
|
240
|
+
headwear: ['none', 'bow', 'cap', 'sprout'],
|
|
241
|
+
neckwear: ['none', 'tie'],
|
|
242
|
+
} as const
|
|
243
|
+
export type Eyewear = (typeof ACCESSORIES.eyewear)[number]
|
|
244
|
+
export type Headwear = (typeof ACCESSORIES.headwear)[number]
|
|
245
|
+
export type Neckwear = (typeof ACCESSORIES.neckwear)[number]
|
|
246
|
+
export type AccessorySlot = keyof typeof ACCESSORIES
|
|
247
|
+
|
|
187
248
|
export type Face = keyof typeof FACES
|
|
188
249
|
export type Color = keyof typeof COLORS
|
|
189
250
|
export type Part = keyof typeof PARTS
|
|
@@ -192,11 +253,22 @@ export type Brows = (typeof PARTS.brows)[number]
|
|
|
192
253
|
export type Mouth = (typeof PARTS.mouth)[number]
|
|
193
254
|
export type Cheeks = (typeof PARTS.cheeks)[number]
|
|
194
255
|
export type Trait = (typeof PARTS.trait)[number]
|
|
195
|
-
export type Axis = 'face' | 'color' | Part
|
|
256
|
+
export type Axis = 'face' | 'color' | Part | AccessorySlot
|
|
196
257
|
|
|
197
258
|
/** How much of each list a version may pick from. Append-only too. */
|
|
198
259
|
export const VERSIONS: Record<number, Record<Axis, number>> = {
|
|
199
|
-
1: {
|
|
260
|
+
1: {
|
|
261
|
+
face: 4,
|
|
262
|
+
color: 7,
|
|
263
|
+
eyes: 3,
|
|
264
|
+
brows: 4,
|
|
265
|
+
mouth: 4,
|
|
266
|
+
cheeks: 3,
|
|
267
|
+
trait: 5,
|
|
268
|
+
eyewear: 1,
|
|
269
|
+
headwear: 1,
|
|
270
|
+
neckwear: 1,
|
|
271
|
+
},
|
|
200
272
|
}
|
|
201
273
|
export const LATEST = 1
|
|
202
274
|
|
package/src/vue.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* <Peek name="Ada" /> for Vue 3.5+: the same tree and the same rules as the
|
|
3
|
+
* React component in peek.tsx. ids come from Vue's useId, so the server and
|
|
4
|
+
* the client agree; `class` and `style` fall through to the <svg>.
|
|
5
|
+
*
|
|
6
|
+
* With `animate`, the tree Vue renders stays in the pose the component
|
|
7
|
+
* mounted with and the shared loop writes every frame straight to the DOM,
|
|
8
|
+
* so a change of expression or gaze never reaches Vue's patch.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
computed,
|
|
13
|
+
defineComponent,
|
|
14
|
+
h,
|
|
15
|
+
onBeforeUnmount,
|
|
16
|
+
onMounted,
|
|
17
|
+
type PropType,
|
|
18
|
+
ref,
|
|
19
|
+
useId,
|
|
20
|
+
type VNode,
|
|
21
|
+
type VNodeRef,
|
|
22
|
+
watch,
|
|
23
|
+
} from 'vue'
|
|
24
|
+
import { Live } from './animate'
|
|
25
|
+
import { draw, type Gaze, type Node, restPose } from './draw'
|
|
26
|
+
import { type PeekOptions, settle } from './svg'
|
|
27
|
+
|
|
28
|
+
export * from './core'
|
|
29
|
+
|
|
30
|
+
export type PeekProps = Omit<PeekOptions, 'gaze' | 'id'> & {
|
|
31
|
+
name: string
|
|
32
|
+
/** x and y in -1..1, or 'pointer' to follow the pointer (animate only). */
|
|
33
|
+
gaze?: Gaze | 'pointer'
|
|
34
|
+
/** Bring it to life after mount. Off under prefers-reduced-motion. */
|
|
35
|
+
animate?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toVue(node: Node, ref?: VNodeRef): VNode {
|
|
39
|
+
return h(
|
|
40
|
+
node.tag,
|
|
41
|
+
{ key: node.key, ...node.attrs, ref },
|
|
42
|
+
node.children.map((c) => toVue(c)),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Every prop unset stays undefined so the name decides it, as in React.
|
|
47
|
+
// Booleans need their type, or `<Peek animate />` would arrive as ''.
|
|
48
|
+
const any = { type: null, default: undefined }
|
|
49
|
+
const flag = { type: Boolean, default: undefined }
|
|
50
|
+
|
|
51
|
+
export const Peek = defineComponent(
|
|
52
|
+
(props: PeekProps) => {
|
|
53
|
+
const id = `peek${useId().replace(/[^\w-]/g, '')}`
|
|
54
|
+
const svg = ref<SVGSVGElement>()
|
|
55
|
+
let live: Live | null = null
|
|
56
|
+
const fixedOf = (g: PeekProps['gaze']) =>
|
|
57
|
+
Array.isArray(g) ? (g as Gaze) : undefined
|
|
58
|
+
const first = {
|
|
59
|
+
expression: props.expression ?? 'normal',
|
|
60
|
+
gaze: fixedOf(props.gaze),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const view = computed(() => {
|
|
64
|
+
const { name, gaze, animate, ...rest } = props
|
|
65
|
+
const { who, pose, opts } = settle(
|
|
66
|
+
name,
|
|
67
|
+
{ ...rest, gaze: fixedOf(gaze), id },
|
|
68
|
+
animate,
|
|
69
|
+
)
|
|
70
|
+
const tree = draw(
|
|
71
|
+
who,
|
|
72
|
+
animate ? restPose(who, first.expression, first.gaze) : pose,
|
|
73
|
+
opts,
|
|
74
|
+
)
|
|
75
|
+
// live: the tree only changes when what the face is changes
|
|
76
|
+
return { who, opts, tree, sig: JSON.stringify([who, opts]) }
|
|
77
|
+
})
|
|
78
|
+
const expression = () => props.expression ?? 'normal'
|
|
79
|
+
|
|
80
|
+
onMounted(() => {
|
|
81
|
+
watch(
|
|
82
|
+
[() => props.animate, () => view.value.sig],
|
|
83
|
+
() => {
|
|
84
|
+
live?.destroy()
|
|
85
|
+
live = null
|
|
86
|
+
if (!props.animate || !svg.value) return
|
|
87
|
+
const { tree, who, opts } = view.value
|
|
88
|
+
live = new Live(svg.value, tree, who, opts, expression(), props.gaze)
|
|
89
|
+
},
|
|
90
|
+
{ immediate: true, flush: 'post' },
|
|
91
|
+
)
|
|
92
|
+
watch(expression, (e) => live?.setExpression(e), { flush: 'post' })
|
|
93
|
+
watch(
|
|
94
|
+
() => String(props.gaze ?? ''),
|
|
95
|
+
() => live?.setGaze(props.gaze),
|
|
96
|
+
{ flush: 'post' },
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
onBeforeUnmount(() => live?.destroy())
|
|
100
|
+
|
|
101
|
+
return () => toVue(view.value.tree, svg)
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
// biome-ignore lint/style/useVueMultiWordComponentNames: <Peek> in every framework
|
|
105
|
+
name: 'Peek',
|
|
106
|
+
props: {
|
|
107
|
+
name: { type: String, required: true },
|
|
108
|
+
gaze: any,
|
|
109
|
+
animate: { type: Boolean, default: false },
|
|
110
|
+
size: { type: Number, default: undefined },
|
|
111
|
+
expression: any,
|
|
112
|
+
frame: any,
|
|
113
|
+
square: flag,
|
|
114
|
+
title: {
|
|
115
|
+
type: [String, Boolean] as PropType<string | false>,
|
|
116
|
+
default: undefined,
|
|
117
|
+
},
|
|
118
|
+
riso: flag,
|
|
119
|
+
version: { type: Number, default: undefined },
|
|
120
|
+
variant: any,
|
|
121
|
+
face: any,
|
|
122
|
+
color: any,
|
|
123
|
+
eyes: any,
|
|
124
|
+
brows: any,
|
|
125
|
+
mouth: any,
|
|
126
|
+
cheeks: any,
|
|
127
|
+
trait: any,
|
|
128
|
+
eyewear: any,
|
|
129
|
+
headwear: any,
|
|
130
|
+
neckwear: any,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
)
|