peepface 0.0.0-stage → 0.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.
@@ -0,0 +1,329 @@
1
+ /* peepface v0.1.0 — 由 build.mjs 从 core.js 生成,不要手改 */
2
+ /* peepface core — 工厂版
3
+ * createFace(container, options) → { setState, getState, destroy }
4
+ * 无状态判定、无全局单例,可在一页里同时跑多个实例。
5
+ */
6
+ (function (root) {
7
+ 'use strict';
8
+
9
+ const DEG = Math.PI / 180;
10
+ const SAMPLES = 64;
11
+ const R = 56;
12
+
13
+ /* ---------------- 几何 ---------------- */
14
+
15
+ const cross = (a, b) => [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]];
16
+ const norm = v => { const l = Math.hypot(...v) || 1; return [v[0]/l, v[1]/l, v[2]/l]; };
17
+
18
+ const sphere = (lon, lat) => {
19
+ const lo = lon * DEG, la = lat * DEG;
20
+ return [Math.sin(lo)*Math.cos(la), Math.sin(la), Math.cos(lo)*Math.cos(la)];
21
+ };
22
+
23
+ const rotate = (n, yaw, pitch) => {
24
+ const a = yaw * DEG, b = pitch * DEG;
25
+ const x1 = n[0]*Math.cos(a) + n[2]*Math.sin(a);
26
+ const z1 = -n[0]*Math.sin(a) + n[2]*Math.cos(a);
27
+ const y2 = n[1]*Math.cos(b) - z1*Math.sin(b);
28
+ const z2 = n[1]*Math.sin(b) + z1*Math.cos(b);
29
+ return [x1, y2, z2];
30
+ };
31
+
32
+ const tangentFrame = n => {
33
+ let u = cross([0, 1, 0], n);
34
+ if (Math.hypot(...u) < 1e-6) u = [1, 0, 0];
35
+ u = norm(u);
36
+ return { u, v: norm(cross(n, u)) };
37
+ };
38
+
39
+ /* 椭圆眼形:用两段 A 命令,带旋转角。
40
+ 之前的 capsule 实现在 hw > hh(横条)时两端圆心重合,会退化成圆。 */
41
+ function eyePath(cx, cy, ux, uy, hw, hh) {
42
+ const theta = Math.atan2(uy, ux) * 180 / Math.PI;
43
+ const p0x = cx + ux * hw, p0y = cy + uy * hw;
44
+ const p1x = cx - ux * hw, p1y = cy - uy * hw;
45
+ return 'M' + p0x.toFixed(2) + ' ' + p0y.toFixed(2) +
46
+ 'A' + hw.toFixed(2) + ' ' + hh.toFixed(2) + ' ' + theta.toFixed(1) +
47
+ ' 0 1 ' + p1x.toFixed(2) + ' ' + p1y.toFixed(2) +
48
+ 'A' + hw.toFixed(2) + ' ' + hh.toFixed(2) + ' ' + theta.toFixed(1) +
49
+ ' 0 1 ' + p0x.toFixed(2) + ' ' + p0y.toFixed(2) + 'Z';
50
+ }
51
+
52
+ /* ---------------- 形体 ---------------- */
53
+
54
+ const SHAPES = {
55
+ circle: () => 1,
56
+ blob: th => 1 + 0.05*Math.cos(3*th + 0.6) + 0.02*Math.cos(5*th - 1.1),
57
+ tall: th => { const a=0.90, b=1.12, c=Math.cos(th), s=Math.sin(th); return a*b/Math.hypot(b*c, a*s); },
58
+ wide: th => { const a=1.14, b=0.88, c=Math.cos(th), s=Math.sin(th); return a*b/Math.hypot(b*c, a*s); },
59
+ };
60
+
61
+ const TABLE = {};
62
+ for (const k in SHAPES) {
63
+ TABLE[k] = Array.from({ length: SAMPLES }, (_, i) => SHAPES[k](i / SAMPLES * Math.PI * 2));
64
+ }
65
+
66
+ function radiusPath(radii, scale) {
67
+ let d = '';
68
+ for (let i = 0; i < SAMPLES; i++) {
69
+ const th = i / SAMPLES * Math.PI * 2;
70
+ const r = radii[i] * R * scale;
71
+ d += (i ? 'L' : 'M') + (Math.cos(th) * r).toFixed(1) + ' ' + (Math.sin(th) * r).toFixed(1);
72
+ }
73
+ return d + 'Z';
74
+ }
75
+
76
+ /* ---------------- 表情 ---------------- */
77
+
78
+ const EXPR = {
79
+ neutral: { w: 11.5, h: 24, tilt: -13 },
80
+ sad: { w: 11.5, h: 21, tilt: -28 },
81
+ focused: { w: 19, h: 8.5, tilt: 0 },
82
+ happy: { w: 16, h: 7, tilt: -18 },
83
+ surprised: { w: 13, h: 29, tilt: -8 },
84
+ sleepy: { w: 20, h: 3.2, tilt: -2 },
85
+ confused: { w: 10.5, h: 19, tilt: 30 },
86
+ droop: { w: 11, h: 16, tilt: -44 },
87
+ };
88
+
89
+ /* ---------------- 状态(来自 states.json) ---------------- */
90
+
91
+ const STATES = {
92
+ idle: { expr:'neutral', shape:'circle', gaze:[0, 0.05], breath:0.008, blink:[2.6, 6.0] },
93
+ listening: { expr:'neutral', shape:'circle', gaze:[0, 0.60], breath:0.010, blink:[2.4, 5.0] },
94
+ thinking: { expr:'sad', shape:'blob', gaze:[0, 0.60], breath:0.014, blink:[2.0, 4.0], ring:1 },
95
+ searching: { expr:'focused', shape:'circle', gaze:[0, 0.10], breath:0.012, blink:[3.0, 6.0], scan:1 },
96
+ streaming: { expr:'neutral', shape:'circle', gaze:[0, 0.15], breath:0.012, blink:[3.0, 6.0] },
97
+ working: { expr:'focused', shape:'circle', gaze:[0, 0.15], breath:0.020, blink:[3.0, 7.0] },
98
+ waiting: { expr:'neutral', shape:'blob', gaze:[0.55, 0.05], breath:0.006, blink:[3.0, 7.0], ring:0.5 },
99
+ blocked: { expr:'confused', shape:'tall', gaze:[-0.40, 0.10], breath:0.005, blink:[4.0, 8.0] },
100
+ finished: { expr:'happy', shape:'wide', gaze:[0, 0.20], breath:0.006, blink:[3.0, 6.0] },
101
+ failed: { expr:'droop', shape:'circle', gaze:[0, 0.42], breath:0.004, blink:[5.0, 9.0] },
102
+ sleeping: { expr:'sleepy', shape:'circle', gaze:[0, -0.20], breath:0.004, blink:[9, 16], asleep:1 },
103
+ waking: { expr:'surprised', shape:'circle', gaze:[0, 0.35], breath:0.016, blink:[1.4, 2.6] },
104
+ };
105
+
106
+ const ease = (cur, tgt, tau, dt) => cur + (tgt - cur) * (1 - Math.exp(-dt / tau));
107
+
108
+ /* ---------------- 工厂 ---------------- */
109
+
110
+ let uid = 0;
111
+
112
+ function createFace(container, options) {
113
+ if (!container) throw new Error('peepface: container required');
114
+ const opts = options || {};
115
+ const size = opts.size || 84;
116
+ const dark = !!opts.dark;
117
+ const follow = opts.follow !== false;
118
+ const id = 'pf' + (++uid);
119
+
120
+ const host = document.createElement('div');
121
+ host.className = 'peepface';
122
+ const shadow = host.attachShadow({ mode: 'open' });
123
+
124
+ shadow.innerHTML = `
125
+ <style>
126
+ :host { all: initial; }
127
+ .wrap {
128
+ position: relative; width: ${size}px; height: ${size}px;
129
+ display: block; user-select: none;
130
+ }
131
+ svg { width: 100%; height: 100%; display: block; overflow: visible; }
132
+ .face { fill: ${dark ? '#e8e8e8' : '#0a0a0c'}; }
133
+ .ring { fill: none; stroke: ${dark ? '#e8e8e8' : '#0a0a0c'}; stroke-width: 1.4; opacity: 0; }
134
+ </style>
135
+ <div class="wrap">
136
+ <svg viewBox="-70 -70 140 140">
137
+ <defs>
138
+ <mask id="${id}-holes" maskUnits="userSpaceOnUse" x="-70" y="-70" width="140" height="140">
139
+ <path id="${id}-body-mask" fill="#fff" d=""/>
140
+ <path id="${id}-eye-l" fill="#000" d=""/>
141
+ <path id="${id}-eye-r" fill="#000" d=""/>
142
+ </mask>
143
+ </defs>
144
+ <path id="${id}-ring" class="ring" d=""/>
145
+ <path id="${id}-body" class="face" mask="url(#${id}-holes)" d=""/>
146
+ </svg>
147
+ </div>`;
148
+
149
+ const el = {
150
+ body: shadow.getElementById(id + '-body'),
151
+ bodyMask: shadow.getElementById(id + '-body-mask'),
152
+ eyeL: shadow.getElementById(id + '-eye-l'),
153
+ eyeR: shadow.getElementById(id + '-eye-r'),
154
+ ring: shadow.getElementById(id + '-ring'),
155
+ wrap: shadow.querySelector('.wrap'),
156
+ };
157
+
158
+ const S = {
159
+ state: opts.state && STATES[opts.state] ? opts.state : 'idle',
160
+ yaw: 0, pitch: 0,
161
+ expr: 'neutral', shape: 'circle',
162
+ radii: TABLE.circle.slice(),
163
+ blink: 0, blinkT: -1, blinkDur: 0.26, nextBlink: 2 + Math.random() * 3,
164
+ breath: 0.008, ring: 0, tRing: 0,
165
+ pointer: null, time: Math.random() * 5, morph: null, asleep: 0,
166
+ };
167
+
168
+ function applyState(name) {
169
+ const st = STATES[name] || STATES.idle;
170
+ S.state = name;
171
+ S.expr = st.expr;
172
+ S.shape = st.shape;
173
+ S.tRing = st.ring || 0;
174
+ S.breath = st.breath;
175
+ S.asleep = st.asleep ? 1 : 0;
176
+ if (st.blink) S.nextBlink = st.blink[0] + Math.random() * (st.blink[1] - st.blink[0]);
177
+ }
178
+
179
+ function blink(dur) {
180
+ if (S.blinkT >= 0 && S.blinkT < 0.5) return;
181
+ S.blinkT = 0; S.blinkDur = dur || 0.26;
182
+ }
183
+
184
+ function onPointer(e) {
185
+ if (!follow) return;
186
+ const r = el.wrap.getBoundingClientRect();
187
+ if (!r.width) return;
188
+ const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
189
+ const half = Math.max(r.width, 1);
190
+ let gx = (e.clientX - cx) / half, gy = -(e.clientY - cy) / half;
191
+ const len = Math.hypot(gx, gy);
192
+ if (len > 1) { gx /= len; gy /= len; }
193
+ S.pointer = [gx, gy];
194
+ }
195
+ const onLeave = () => { S.pointer = null; };
196
+
197
+ if (follow) {
198
+ window.addEventListener('pointermove', onPointer, { passive: true });
199
+ document.addEventListener('mouseleave', onLeave);
200
+ window.addEventListener('blur', onLeave);
201
+ }
202
+
203
+ let last = performance.now();
204
+ let alive = true;
205
+
206
+ function tick(now) {
207
+ const dt = Math.min(0.05, (now - last) / 1000);
208
+ last = now; S.time += dt;
209
+
210
+ const cur = STATES[S.state];
211
+ const bias = cur.gaze || [0, 0];
212
+
213
+ let ty, tp;
214
+ if (S.pointer) {
215
+ ty = S.pointer[0] * 40 + bias[0] * 18;
216
+ tp = -S.pointer[1] * 28 - bias[1] * 16;
217
+ } else {
218
+ ty = Math.sin(S.time * 0.42) * 8 + Math.sin(S.time * 0.17 + 1.3) * 4 + bias[0] * 18;
219
+ tp = Math.cos(S.time * 0.31) * 5 - bias[1] * 16;
220
+ }
221
+ if (cur.scan) ty += Math.sin(S.time * 3.6) * 30;
222
+
223
+ S.yaw = ease(S.yaw, ty, 0.08, dt);
224
+ S.pitch = ease(S.pitch, tp, 0.08, dt);
225
+
226
+ if (S.asleep) {
227
+ S.blink = 0; // 闭眼交给 sleepy 眼形本身表达
228
+ S.blinkT = -1;
229
+ S.nextBlink = 1e9; // 睡着不眨
230
+ } else if (S.blinkT >= 0) {
231
+ S.blinkT += dt / S.blinkDur;
232
+ if (S.blinkT >= 1) { S.blinkT = -1; S.blink = 0; }
233
+ else {
234
+ const t = S.blinkT, U1 = 0.42, U2 = 0.56;
235
+ const ss = x => x * x * (3 - 2 * x);
236
+ S.blink = t < U1 ? ss(t / U1) : t <= U2 ? 1 : 1 - ss((t - U2) / (1 - U2));
237
+ }
238
+ } else {
239
+ S.nextBlink -= dt;
240
+ if (S.nextBlink <= 0) {
241
+ blink(0.26);
242
+ const b = cur.blink || [2.6, 6];
243
+ S.nextBlink = b[0] + Math.random() * (b[1] - b[0]);
244
+ }
245
+ }
246
+
247
+ const target = TABLE[S.shape] || TABLE.circle;
248
+ for (let i = 0; i < SAMPLES; i++) S.radii[i] = ease(S.radii[i], target[i], 0.12, dt);
249
+
250
+ const scale = 1 + Math.sin(S.time * 1.4) * S.breath;
251
+ const d = radiusPath(S.radii, scale);
252
+ el.body.setAttribute('d', d);
253
+ el.bodyMask.setAttribute('d', d);
254
+
255
+ const ex = EXPR[S.expr] || EXPR.neutral;
256
+ const eyes = [{ id: 'eyeL', lon: -27 }, { id: 'eyeR', lon: 27 }];
257
+ for (let k = 0; k < eyes.length; k++) {
258
+ const e = eyes[k];
259
+ const n = rotate(sphere(e.lon, 10), S.yaw, S.pitch);
260
+ const px = n[0] * R, py = -n[1] * R;
261
+ const tf = tangentFrame(n);
262
+ const uS = [tf.u[0], -tf.u[1]], vS = [tf.v[0], -tf.v[1]];
263
+ const uz = Math.hypot(uS[0], uS[1]) || 1e-4;
264
+ const vz = Math.hypot(vS[0], vS[1]) || 1e-4;
265
+ const ux = uS[0] / uz, uy = uS[1] / uz;
266
+ const roll = -S.yaw * 0.28 + ex.tilt;
267
+ const cR = Math.cos(roll * DEG), sR = Math.sin(roll * DEG);
268
+ const wx = ux * cR - uy * sR, wy = ux * sR + uy * cR;
269
+ const hw = (ex.w / 2) * Math.max(0.22, uz);
270
+ const hh = Math.max(0.3, (ex.h / 2) * vz * (1 - S.blink * 0.995));
271
+ el[e.id].setAttribute('d', eyePath(px, py, wx, wy, hw, hh));
272
+ const vis = Math.max(0, Math.min(1, (n[2] - 0.02) / 0.22));
273
+ el[e.id].setAttribute('opacity', vis.toFixed(2));
274
+ }
275
+
276
+ S.ring = ease(S.ring, S.tRing, 0.25, dt);
277
+ if (S.ring > 0.02) {
278
+ const rx = 74, ry = 26, rot = -18 * DEG, a0 = S.time * 0.9;
279
+ let p = '';
280
+ for (let i = 0; i <= 60; i++) {
281
+ const a = i / 60 * Math.PI * 2 + a0;
282
+ const x0 = Math.cos(a) * rx, y0 = Math.sin(a) * ry;
283
+ p += (i ? 'L' : 'M') + (x0 * Math.cos(rot) - y0 * Math.sin(rot)).toFixed(1) + ' '
284
+ + (x0 * Math.sin(rot) + y0 * Math.cos(rot)).toFixed(1);
285
+ }
286
+ el.ring.setAttribute('d', p + 'Z');
287
+ el.ring.style.opacity = (S.ring * 0.45).toFixed(2);
288
+ } else {
289
+ el.ring.style.opacity = 0;
290
+ }
291
+ }
292
+
293
+ function frame(now) {
294
+ if (!alive) return;
295
+ try { tick(now); } catch (e) { /* 一帧出错不杀循环 */ }
296
+ requestAnimationFrame(frame);
297
+ }
298
+
299
+ container.appendChild(host);
300
+ applyState(S.state);
301
+ requestAnimationFrame(frame);
302
+
303
+ return {
304
+ setState(name) {
305
+ applyState(name);
306
+ // 换状态的瞬间眨一次眼盖住形变
307
+ blink(0.26);
308
+ },
309
+ getState() { return S.state; },
310
+ blink() { blink(0.26); },
311
+ destroy() {
312
+ alive = false;
313
+ if (follow) {
314
+ window.removeEventListener('pointermove', onPointer);
315
+ document.removeEventListener('mouseleave', onLeave);
316
+ window.removeEventListener('blur', onLeave);
317
+ }
318
+ host.remove();
319
+ },
320
+ el: host,
321
+ };
322
+ }
323
+
324
+ root.peepface = {
325
+ createFace,
326
+ STATES: Object.keys(STATES),
327
+ VERSION: '0.1.0',
328
+ };
329
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -0,0 +1,10 @@
1
+ /* peepface v0.1.0 — 由 build.mjs 从 core.js 生成,不要手改 */
2
+ import './peepface.js';
3
+
4
+ const api = globalThis.peepface;
5
+ if (!api) throw new Error('peepface: UMD 入口没有正确挂载');
6
+
7
+ export const createFace = api.createFace;
8
+ export const STATES = api.STATES;
9
+ export const VERSION = api.VERSION;
10
+ export default api;
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "peepface",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "agent 状态的视觉词汇表:12 个生命周期状态、12 KB、零依赖纯 SVG",
5
+ "type": "module",
6
+ "main": "./dist/peepface.mjs",
7
+ "module": "./dist/peepface.mjs",
8
+ "browser": "./dist/peepface.js",
9
+ "types": "./peepface.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./peepface.d.ts",
13
+ "import": "./dist/peepface.mjs",
14
+ "default": "./dist/peepface.js"
15
+ },
16
+ "./states.json": "./states.json",
17
+ "./plugin-dsh": "./plugin-dsh/lib/client.js",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist/",
22
+ "core.js",
23
+ "states.json",
24
+ "README.md",
25
+ "LICENSE",
26
+ "peepface.d.ts"
27
+ ],
28
+ "sideEffects": [
29
+ "./dist/peepface.js"
30
+ ],
31
+ "keywords": [
32
+ "agent",
33
+ "avatar",
34
+ "status",
35
+ "svg",
36
+ "ui",
37
+ "ai",
38
+ "llm",
39
+ "status-indicator",
40
+ "peepface"
41
+ ],
42
+ "license": "Apache-2.0",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/SilasSolivagus/peepface.git"
46
+ },
47
+ "homepage": "https://silassolivagus.github.io/peepface/",
48
+ "bugs": {
49
+ "url": "https://github.com/SilasSolivagus/peepface/issues"
50
+ },
51
+ "scripts": {
52
+ "build": "node build.mjs",
53
+ "prepublishOnly": "node build.mjs"
54
+ }
6
55
  }
package/peepface.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ export type FaceState =
2
+ | 'idle'
3
+ | 'listening'
4
+ | 'thinking'
5
+ | 'searching'
6
+ | 'streaming'
7
+ | 'working'
8
+ | 'waiting'
9
+ | 'blocked'
10
+ | 'finished'
11
+ | 'failed'
12
+ | 'sleeping'
13
+ | 'waking';
14
+
15
+ export interface FaceOptions {
16
+ /** 初始状态,默认 'idle' */
17
+ state?: FaceState;
18
+ /** 显示尺寸(px),默认 84 */
19
+ size?: number;
20
+ /** 深色主题:白脸黑眼。默认 false */
21
+ dark?: boolean;
22
+ /** 眼睛跟随鼠标,默认 true */
23
+ follow?: boolean;
24
+ }
25
+
26
+ export interface Face {
27
+ setState(state: FaceState): void;
28
+ getState(): FaceState;
29
+ blink(): void;
30
+ destroy(): void;
31
+ readonly el: HTMLElement;
32
+ }
33
+
34
+ export function createFace(container: Element, options?: FaceOptions): Face;
35
+ export const STATES: FaceState[];
36
+ export const VERSION: string;
37
+
38
+ declare const peepface: {
39
+ createFace: typeof createFace;
40
+ STATES: typeof STATES;
41
+ VERSION: typeof VERSION;
42
+ };
43
+ export default peepface;