bloby-bot 0.51.0 → 0.51.3
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/dist-bloby/assets/{bloby-B2Y96IF2.js → bloby-vi0Xitb-.js} +13 -13
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-z8OpDrVr.js → highlighted-body-OFNGDK62-DMeCY5Rc.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-BOqNyL14.js +1 -0
- package/dist-bloby/bloby.html +1 -1
- package/package.json +4 -4
- package/supervisor/chat/bloby-main.tsx +2 -14
- package/supervisor/chat/src/components/Chat/HeadphonesAnimation.tsx +90 -36
- package/supervisor/index.ts +1 -1
- package/supervisor/public/morphy/headphones.json +40 -0
- package/supervisor/public/morphy/headphones.png +0 -0
- package/supervisor/widget.js +50 -25
- package/workspace/client/public/morphy/headphones.json +40 -0
- package/workspace/client/public/morphy/headphones.png +0 -0
- package/workspace/client/public/sw.js +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-DAQ7dBvr.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-
|
|
1
|
+
import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-vi0Xitb-.js";var o=e(t(),1),s=n(),c=({code:e,language:t,raw:n,className:c,startLine:l,lineNumbers:u,...d})=>{let{shikiTheme:f}=(0,o.useContext)(i),p=r(),[m,h]=(0,o.useState)(n);return(0,o.useEffect)(()=>{if(!p){h(n);return}let r=p.highlight({code:e,language:t,themes:f},e=>{h(e)});r&&h(r)},[e,t,f,p,n]),(0,s.jsx)(a,{className:c,language:t,lineNumbers:u,result:m,startLine:l,...d})};export{c as HighlightedCodeBlockBody};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e}from"./bloby-vi0Xitb-.js";export{e as Mermaid};
|
package/dist-bloby/bloby.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Bloby Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/bloby/assets/bloby-
|
|
7
|
+
<script type="module" crossorigin src="/bloby/assets/bloby-vi0Xitb-.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/bloby/assets/jsx-runtime-C0W9Wf2W.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/bloby/assets/globals-DNO3ilRx.js">
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/bloby/assets/globals-D60b-8LY.css">
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bloby-bot",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.3",
|
|
4
4
|
"releaseNotes": [
|
|
5
|
-
"1.
|
|
6
|
-
"2. ",
|
|
7
|
-
"3. ",
|
|
5
|
+
"1. New Morphy animation system: config-driven sprites loaded from /morphy/*.json",
|
|
6
|
+
"2. Swapped teleporting (splash) and headphones (bubble + chat) to the new format",
|
|
7
|
+
"3. SW cache bumped to v16 to clear old assets",
|
|
8
8
|
"4. "
|
|
9
9
|
],
|
|
10
10
|
"description": "Self-hosted, self-evolving AI agent with its own dashboard.",
|
|
@@ -32,7 +32,6 @@ function BlobyApp() {
|
|
|
32
32
|
|
|
33
33
|
// Recording state (for header animation)
|
|
34
34
|
const [chatRecording, setChatRecording] = useState(false);
|
|
35
|
-
const [showHpAnim, setShowHpAnim] = useState(false);
|
|
36
35
|
|
|
37
36
|
// Push notifications
|
|
38
37
|
const [pushState, setPushState] = useState<'loading' | 'unsupported' | 'denied' | 'subscribed' | 'unsubscribed'>('loading');
|
|
@@ -361,15 +360,7 @@ function BlobyApp() {
|
|
|
361
360
|
|
|
362
361
|
{/* Mascot / headphones animation */}
|
|
363
362
|
<div className="relative ml-1.5 shrink-0">
|
|
364
|
-
{
|
|
365
|
-
<HeadphonesAnimation
|
|
366
|
-
recording={chatRecording}
|
|
367
|
-
height={36}
|
|
368
|
-
onDone={() => setShowHpAnim(false)}
|
|
369
|
-
/>
|
|
370
|
-
) : (
|
|
371
|
-
<img src="/bloby.png" alt={botName} className="h-9 w-auto" />
|
|
372
|
-
)}
|
|
363
|
+
<HeadphonesAnimation recording={chatRecording} height={36} />
|
|
373
364
|
</div>
|
|
374
365
|
|
|
375
366
|
{/* Name + status line */}
|
|
@@ -529,10 +520,7 @@ function BlobyApp() {
|
|
|
529
520
|
});
|
|
530
521
|
}}
|
|
531
522
|
onAudioReady={addPendingAudio}
|
|
532
|
-
onRecordingChange={
|
|
533
|
-
setChatRecording(r);
|
|
534
|
-
if (r) setShowHpAnim(true);
|
|
535
|
-
}}
|
|
523
|
+
onRecordingChange={setChatRecording}
|
|
536
524
|
/>
|
|
537
525
|
</div>
|
|
538
526
|
|
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
// Config-driven. Loaded from /morphy/headphones.json.
|
|
4
|
+
// Clips map to states:
|
|
5
|
+
// idle (hold) → 'idle'
|
|
6
|
+
// enter (forward) → 'activating'
|
|
7
|
+
// active (pingpong)→ 'recording'
|
|
8
|
+
// exit (forward) → 'deactivating'
|
|
9
|
+
const HP_CONFIG_URL = '/morphy/headphones.json';
|
|
10
|
+
const HP_ASSETS_DIR = '/morphy/';
|
|
11
|
+
|
|
12
|
+
interface HpConfig {
|
|
13
|
+
spritesheet: string;
|
|
14
|
+
frame: { w: number; h: number };
|
|
15
|
+
grid: { cols: number; rows: number };
|
|
16
|
+
fps: number;
|
|
17
|
+
clips: {
|
|
18
|
+
idle: { from: number; to: number; mode: string; fps?: number };
|
|
19
|
+
enter: { from: number; to: number; mode: string; fps?: number };
|
|
20
|
+
active: { from: number; to: number; mode: string; fps?: number };
|
|
21
|
+
exit: { from: number; to: number; mode: string; fps?: number };
|
|
22
|
+
};
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
let cachedSprite: HTMLImageElement | null = null;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
let cachedConfig: HpConfig | null = null;
|
|
27
|
+
|
|
28
|
+
function loadConfigAndSprite(): Promise<{ img: HTMLImageElement; cfg: HpConfig }> {
|
|
29
|
+
if (cachedSprite && cachedConfig) return Promise.resolve({ img: cachedSprite, cfg: cachedConfig });
|
|
30
|
+
return fetch(HP_CONFIG_URL)
|
|
31
|
+
.then((r) => { if (!r.ok) throw new Error('hp config ' + r.status); return r.json() as Promise<HpConfig>; })
|
|
32
|
+
.then((cfg) => new Promise<{ img: HTMLImageElement; cfg: HpConfig }>((resolve, reject) => {
|
|
33
|
+
const img = new Image();
|
|
34
|
+
img.onload = () => { cachedSprite = img; cachedConfig = cfg; resolve({ img, cfg }); };
|
|
35
|
+
img.onerror = reject;
|
|
36
|
+
img.src = HP_ASSETS_DIR + cfg.spritesheet;
|
|
37
|
+
}));
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
interface Props {
|
|
@@ -34,8 +48,9 @@ type HpState = 'idle' | 'activating' | 'activating_then_deactivate' | 'recording
|
|
|
34
48
|
export default function HeadphonesAnimation({ recording, height = 36, onDone }: Props) {
|
|
35
49
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
36
50
|
const spriteRef = useRef<HTMLImageElement | null>(cachedSprite);
|
|
51
|
+
const configRef = useRef<HpConfig | null>(cachedConfig);
|
|
37
52
|
const stateRef = useRef<HpState>('idle');
|
|
38
|
-
const frameRef = useRef(
|
|
53
|
+
const frameRef = useRef(0);
|
|
39
54
|
const pingPongRef = useRef(1);
|
|
40
55
|
const lastRef = useRef(0);
|
|
41
56
|
const rafRef = useRef(0);
|
|
@@ -43,16 +58,28 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
43
58
|
const onDoneRef = useRef(onDone);
|
|
44
59
|
onDoneRef.current = onDone;
|
|
45
60
|
|
|
61
|
+
// Aspect ratio from config (defaults to 1:1 — matches new sprite).
|
|
62
|
+
const cfg = configRef.current;
|
|
63
|
+
const aspectW = cfg ? cfg.frame.w : 180;
|
|
64
|
+
const aspectH = cfg ? cfg.frame.h : 180;
|
|
46
65
|
const displayH = height;
|
|
47
|
-
const displayW = Math.round(displayH * (
|
|
66
|
+
const displayW = Math.round(displayH * (aspectW / aspectH));
|
|
48
67
|
|
|
49
|
-
useEffect(() => {
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
loadConfigAndSprite().then(({ img, cfg }) => {
|
|
70
|
+
spriteRef.current = img;
|
|
71
|
+
configRef.current = cfg;
|
|
72
|
+
if (stateRef.current === 'idle') frameRef.current = cfg.clips.idle.from;
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
50
75
|
|
|
51
76
|
// React to recording prop
|
|
52
77
|
useEffect(() => {
|
|
78
|
+
const cfg = configRef.current;
|
|
79
|
+
if (!cfg) return;
|
|
53
80
|
if (recording && !prevRecording.current) {
|
|
54
81
|
stateRef.current = 'activating';
|
|
55
|
-
frameRef.current =
|
|
82
|
+
frameRef.current = cfg.clips.enter.from;
|
|
56
83
|
pingPongRef.current = 1;
|
|
57
84
|
lastRef.current = performance.now();
|
|
58
85
|
} else if (!recording && prevRecording.current) {
|
|
@@ -60,7 +87,7 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
60
87
|
stateRef.current = 'activating_then_deactivate';
|
|
61
88
|
} else if (stateRef.current === 'recording') {
|
|
62
89
|
stateRef.current = 'deactivating';
|
|
63
|
-
frameRef.current =
|
|
90
|
+
frameRef.current = cfg.clips.exit.from;
|
|
64
91
|
lastRef.current = performance.now();
|
|
65
92
|
}
|
|
66
93
|
}
|
|
@@ -70,17 +97,40 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
70
97
|
const tick = useCallback((now: number) => {
|
|
71
98
|
const canvas = canvasRef.current;
|
|
72
99
|
const sprite = spriteRef.current;
|
|
100
|
+
const cfg = configRef.current;
|
|
73
101
|
if (!canvas) { rafRef.current = requestAnimationFrame(tick); return; }
|
|
74
102
|
const ctx = canvas.getContext('2d')!;
|
|
75
103
|
const state = stateRef.current;
|
|
76
104
|
|
|
77
|
-
if (
|
|
105
|
+
if (!sprite || !cfg) {
|
|
106
|
+
ctx.clearRect(0, 0, displayW, displayH);
|
|
107
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (state === 'idle') {
|
|
112
|
+
// Hold the configured idle frame so the chat header shows a real visual at rest.
|
|
113
|
+
const cols = cfg.grid.cols;
|
|
114
|
+
const fw = cfg.frame.w;
|
|
115
|
+
const fh = cfg.frame.h;
|
|
116
|
+
const idleFrame = cfg.clips.idle.from;
|
|
117
|
+
const col = idleFrame % cols;
|
|
118
|
+
const row = Math.floor(idleFrame / cols);
|
|
78
119
|
ctx.clearRect(0, 0, displayW, displayH);
|
|
120
|
+
ctx.imageSmoothingEnabled = true;
|
|
121
|
+
ctx.imageSmoothingQuality = 'high';
|
|
122
|
+
ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);
|
|
79
123
|
rafRef.current = requestAnimationFrame(tick);
|
|
80
124
|
return;
|
|
81
125
|
}
|
|
82
126
|
|
|
83
|
-
const
|
|
127
|
+
const baseFps = cfg.fps;
|
|
128
|
+
const fps =
|
|
129
|
+
state === 'activating' || state === 'activating_then_deactivate'
|
|
130
|
+
? (cfg.clips.enter.fps ?? baseFps)
|
|
131
|
+
: state === 'deactivating'
|
|
132
|
+
? (cfg.clips.exit.fps ?? baseFps)
|
|
133
|
+
: (cfg.clips.active.fps ?? baseFps);
|
|
84
134
|
const frameMs = 1000 / fps;
|
|
85
135
|
const delta = now - lastRef.current;
|
|
86
136
|
if (delta >= frameMs) {
|
|
@@ -88,24 +138,25 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
88
138
|
|
|
89
139
|
if (state === 'activating' || state === 'activating_then_deactivate') {
|
|
90
140
|
frameRef.current++;
|
|
91
|
-
if (frameRef.current >
|
|
141
|
+
if (frameRef.current > cfg.clips.enter.to) {
|
|
92
142
|
if (state === 'activating_then_deactivate') {
|
|
93
143
|
stateRef.current = 'deactivating';
|
|
94
|
-
frameRef.current =
|
|
144
|
+
frameRef.current = cfg.clips.exit.from;
|
|
95
145
|
} else {
|
|
96
146
|
stateRef.current = 'recording';
|
|
97
|
-
frameRef.current =
|
|
147
|
+
frameRef.current = cfg.clips.active.from;
|
|
98
148
|
pingPongRef.current = 1;
|
|
99
149
|
}
|
|
100
150
|
}
|
|
101
151
|
} else if (state === 'recording') {
|
|
102
152
|
frameRef.current += pingPongRef.current;
|
|
103
|
-
if (frameRef.current >=
|
|
104
|
-
else if (frameRef.current <=
|
|
153
|
+
if (frameRef.current >= cfg.clips.active.to) { frameRef.current = cfg.clips.active.to; pingPongRef.current = -1; }
|
|
154
|
+
else if (frameRef.current <= cfg.clips.active.from) { frameRef.current = cfg.clips.active.from; pingPongRef.current = 1; }
|
|
105
155
|
} else if (state === 'deactivating') {
|
|
106
|
-
frameRef.current
|
|
107
|
-
if (frameRef.current
|
|
156
|
+
frameRef.current++;
|
|
157
|
+
if (frameRef.current > cfg.clips.exit.to) {
|
|
108
158
|
stateRef.current = 'idle';
|
|
159
|
+
frameRef.current = cfg.clips.idle.from;
|
|
109
160
|
ctx.clearRect(0, 0, displayW, displayH);
|
|
110
161
|
onDoneRef.current?.();
|
|
111
162
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -114,12 +165,15 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
114
165
|
}
|
|
115
166
|
}
|
|
116
167
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
168
|
+
const cols = cfg.grid.cols;
|
|
169
|
+
const fw = cfg.frame.w;
|
|
170
|
+
const fh = cfg.frame.h;
|
|
171
|
+
const col = frameRef.current % cols;
|
|
172
|
+
const row = Math.floor(frameRef.current / cols);
|
|
119
173
|
ctx.clearRect(0, 0, displayW, displayH);
|
|
120
174
|
ctx.imageSmoothingEnabled = true;
|
|
121
175
|
ctx.imageSmoothingQuality = 'high';
|
|
122
|
-
ctx.drawImage(sprite, col *
|
|
176
|
+
ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);
|
|
123
177
|
|
|
124
178
|
rafRef.current = requestAnimationFrame(tick);
|
|
125
179
|
}, [displayW, displayH]);
|
package/supervisor/index.ts
CHANGED
|
@@ -90,7 +90,7 @@ const SW_JS = `// Service worker — app-shell caching + push notifications
|
|
|
90
90
|
// JS/CSS modules → stale-while-revalidate
|
|
91
91
|
// API, WebSocket, Vite internals → network-only (no cache)
|
|
92
92
|
|
|
93
|
-
var CACHE = 'bloby-
|
|
93
|
+
var CACHE = 'bloby-v16';
|
|
94
94
|
var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
|
|
95
95
|
|
|
96
96
|
// Precache the HTML shell on install so the cache is never empty.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "headphones",
|
|
3
|
+
"spritesheet": "headphones.png",
|
|
4
|
+
"frame": {
|
|
5
|
+
"w": 174,
|
|
6
|
+
"h": 180
|
|
7
|
+
},
|
|
8
|
+
"grid": {
|
|
9
|
+
"cols": 16,
|
|
10
|
+
"rows": 8
|
|
11
|
+
},
|
|
12
|
+
"totalFrames": 121,
|
|
13
|
+
"fps": 24,
|
|
14
|
+
"clips": {
|
|
15
|
+
"idle": {
|
|
16
|
+
"from": 15,
|
|
17
|
+
"to": 15,
|
|
18
|
+
"mode": "hold",
|
|
19
|
+
"fps": 38
|
|
20
|
+
},
|
|
21
|
+
"enter": {
|
|
22
|
+
"from": 16,
|
|
23
|
+
"to": 25,
|
|
24
|
+
"mode": "forward",
|
|
25
|
+
"fps": 48,
|
|
26
|
+
"next": "active"
|
|
27
|
+
},
|
|
28
|
+
"active": {
|
|
29
|
+
"from": 26,
|
|
30
|
+
"to": 94,
|
|
31
|
+
"mode": "pingpong"
|
|
32
|
+
},
|
|
33
|
+
"exit": {
|
|
34
|
+
"from": 96,
|
|
35
|
+
"to": 105,
|
|
36
|
+
"mode": "forward",
|
|
37
|
+
"next": "idle"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
Binary file
|
package/supervisor/widget.js
CHANGED
|
@@ -77,24 +77,31 @@
|
|
|
77
77
|
document.body.appendChild(badgeEl);
|
|
78
78
|
|
|
79
79
|
// ══════════════════════════════════════════════════════════════════
|
|
80
|
-
// ── Headphones Sprite Sheet Config (
|
|
80
|
+
// ── Headphones Sprite Sheet Config (loaded from JSON) ────────────
|
|
81
|
+
// Clips map to hpStates:
|
|
82
|
+
// idle (hold) → hpState 'idle'
|
|
83
|
+
// enter (forward) → hpState 'activating'
|
|
84
|
+
// active (pingpong)→ hpState 'recording'
|
|
85
|
+
// exit (forward) → hpState 'deactivating'
|
|
81
86
|
// ══════════════════════════════════════════════════════════════════
|
|
82
87
|
|
|
88
|
+
var HP_CONFIG_URL = '/morphy/headphones.json';
|
|
89
|
+
|
|
90
|
+
// Defaults mirror the shipped JSON so animation works even if fetch is cached.
|
|
83
91
|
var HP_COLS = 16;
|
|
84
|
-
var HP_FRAME_W =
|
|
85
|
-
var HP_FRAME_H =
|
|
86
|
-
// Bubble display: fill 60px width exactly
|
|
92
|
+
var HP_FRAME_W = 180;
|
|
93
|
+
var HP_FRAME_H = 180;
|
|
87
94
|
var HP_BUBBLE_H = 40;
|
|
88
|
-
var HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H);
|
|
95
|
+
var HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H);
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
var
|
|
92
|
-
var
|
|
93
|
-
var
|
|
97
|
+
var HP_IDLE_FRAME = 15;
|
|
98
|
+
var HP_ACTIVATE_START = 16, HP_ACTIVATE_END = 25;
|
|
99
|
+
var HP_RECORD_START = 26, HP_RECORD_END = 94;
|
|
100
|
+
var HP_EXIT_START = 96, HP_EXIT_END = 105;
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
var
|
|
97
|
-
var
|
|
102
|
+
var HP_BASE_FPS = 24; // active / record default
|
|
103
|
+
var HP_ACTIVATE_FPS = 48; // enter clip fps
|
|
104
|
+
var HP_EXIT_FPS = 24; // exit clip fps
|
|
98
105
|
|
|
99
106
|
// Long-press threshold
|
|
100
107
|
var HP_HOLD_MS = 500;
|
|
@@ -229,13 +236,33 @@
|
|
|
229
236
|
.catch(function () { if (onFail) onFail(); });
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
// ── Load headphones sprite sheet ──
|
|
239
|
+
// ── Load headphones sprite sheet (config + image) ──
|
|
240
|
+
function applyHpConfig(cfg) {
|
|
241
|
+
HP_COLS = cfg.grid.cols;
|
|
242
|
+
HP_FRAME_W = cfg.frame.w;
|
|
243
|
+
HP_FRAME_H = cfg.frame.h;
|
|
244
|
+
HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H);
|
|
245
|
+
HP_IDLE_FRAME = cfg.clips.idle.from;
|
|
246
|
+
HP_ACTIVATE_START = cfg.clips.enter.from; HP_ACTIVATE_END = cfg.clips.enter.to;
|
|
247
|
+
HP_RECORD_START = cfg.clips.active.from; HP_RECORD_END = cfg.clips.active.to;
|
|
248
|
+
HP_EXIT_START = cfg.clips.exit.from; HP_EXIT_END = cfg.clips.exit.to;
|
|
249
|
+
HP_BASE_FPS = (cfg.clips.active.fps != null ? cfg.clips.active.fps : cfg.fps);
|
|
250
|
+
HP_ACTIVATE_FPS = (cfg.clips.enter.fps != null ? cfg.clips.enter.fps : cfg.fps);
|
|
251
|
+
HP_EXIT_FPS = (cfg.clips.exit.fps != null ? cfg.clips.exit.fps : cfg.fps);
|
|
252
|
+
}
|
|
253
|
+
|
|
233
254
|
function loadHpSprite(cb) {
|
|
234
255
|
if (hpSpriteSheet) { if (cb) cb(); return; }
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
fetch(HP_CONFIG_URL)
|
|
257
|
+
.then(function (r) { if (!r.ok) throw new Error('hp config ' + r.status); return r.json(); })
|
|
258
|
+
.then(function (cfg) {
|
|
259
|
+
applyHpConfig(cfg);
|
|
260
|
+
var img = new Image();
|
|
261
|
+
img.onload = function () { hpSpriteSheet = img; hpFrame = HP_IDLE_FRAME; if (cb) cb(); };
|
|
262
|
+
img.onerror = function () { console.error('[widget] headphones sprite failed'); };
|
|
263
|
+
img.src = BLOB_ASSETS_DIR + cfg.spritesheet;
|
|
264
|
+
})
|
|
265
|
+
.catch(function (err) { console.error('[widget] headphones config failed', err); });
|
|
239
266
|
}
|
|
240
267
|
|
|
241
268
|
// ── Trigger blob move (splash) ──
|
|
@@ -261,7 +288,8 @@
|
|
|
261
288
|
// ══════════════════════════════════════════════════════════════════
|
|
262
289
|
|
|
263
290
|
function hpFps() {
|
|
264
|
-
if (hpState === 'activating' || hpState === 'activating_then_deactivate'
|
|
291
|
+
if (hpState === 'activating' || hpState === 'activating_then_deactivate') return HP_ACTIVATE_FPS;
|
|
292
|
+
if (hpState === 'deactivating') return HP_EXIT_FPS;
|
|
265
293
|
return HP_BASE_FPS;
|
|
266
294
|
}
|
|
267
295
|
|
|
@@ -277,9 +305,8 @@
|
|
|
277
305
|
hpFrame++;
|
|
278
306
|
if (hpFrame > HP_ACTIVATE_END) {
|
|
279
307
|
if (hpState === 'activating_then_deactivate') {
|
|
280
|
-
// Reverse back to idle
|
|
281
308
|
hpState = 'deactivating';
|
|
282
|
-
hpFrame =
|
|
309
|
+
hpFrame = HP_EXIT_START;
|
|
283
310
|
} else {
|
|
284
311
|
hpState = 'recording';
|
|
285
312
|
hpFrame = HP_RECORD_START;
|
|
@@ -291,9 +318,8 @@
|
|
|
291
318
|
if (hpFrame >= HP_RECORD_END) { hpFrame = HP_RECORD_END; hpPingPong = -1; }
|
|
292
319
|
else if (hpFrame <= HP_RECORD_START) { hpFrame = HP_RECORD_START; hpPingPong = 1; }
|
|
293
320
|
} else if (hpState === 'deactivating') {
|
|
294
|
-
|
|
295
|
-
hpFrame
|
|
296
|
-
if (hpFrame < HP_ACTIVATE_START) {
|
|
321
|
+
hpFrame++;
|
|
322
|
+
if (hpFrame > HP_EXIT_END) {
|
|
297
323
|
hpState = 'idle';
|
|
298
324
|
hpFrame = HP_IDLE_FRAME;
|
|
299
325
|
}
|
|
@@ -572,12 +598,11 @@
|
|
|
572
598
|
canvas.style.transition = 'transform 0.2s ease';
|
|
573
599
|
canvas.style.transform = '';
|
|
574
600
|
|
|
575
|
-
// v2 deactivation: reverse 40→11 (activate played backwards)
|
|
576
601
|
if (hpState === 'activating') {
|
|
577
602
|
hpState = 'activating_then_deactivate';
|
|
578
603
|
} else if (hpState === 'recording') {
|
|
579
604
|
hpState = 'deactivating';
|
|
580
|
-
hpFrame =
|
|
605
|
+
hpFrame = HP_EXIT_START;
|
|
581
606
|
hpLastFrameTime = performance.now();
|
|
582
607
|
}
|
|
583
608
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "headphones",
|
|
3
|
+
"spritesheet": "headphones.png",
|
|
4
|
+
"frame": {
|
|
5
|
+
"w": 174,
|
|
6
|
+
"h": 180
|
|
7
|
+
},
|
|
8
|
+
"grid": {
|
|
9
|
+
"cols": 16,
|
|
10
|
+
"rows": 8
|
|
11
|
+
},
|
|
12
|
+
"totalFrames": 121,
|
|
13
|
+
"fps": 24,
|
|
14
|
+
"clips": {
|
|
15
|
+
"idle": {
|
|
16
|
+
"from": 15,
|
|
17
|
+
"to": 15,
|
|
18
|
+
"mode": "hold",
|
|
19
|
+
"fps": 38
|
|
20
|
+
},
|
|
21
|
+
"enter": {
|
|
22
|
+
"from": 16,
|
|
23
|
+
"to": 25,
|
|
24
|
+
"mode": "forward",
|
|
25
|
+
"fps": 48,
|
|
26
|
+
"next": "active"
|
|
27
|
+
},
|
|
28
|
+
"active": {
|
|
29
|
+
"from": 26,
|
|
30
|
+
"to": 94,
|
|
31
|
+
"mode": "pingpong"
|
|
32
|
+
},
|
|
33
|
+
"exit": {
|
|
34
|
+
"from": 96,
|
|
35
|
+
"to": 105,
|
|
36
|
+
"mode": "forward",
|
|
37
|
+
"next": "idle"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
Binary file
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// JS/CSS modules → stale-while-revalidate
|
|
7
7
|
// API, WebSocket, Vite internals → network-only (no cache)
|
|
8
8
|
|
|
9
|
-
const CACHE = 'bloby-
|
|
9
|
+
const CACHE = 'bloby-v7';
|
|
10
10
|
|
|
11
11
|
// Precache the HTML shell on install so the cache is never empty.
|
|
12
12
|
// Without this, the first navigation isn't intercepted (SW wasn't
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e}from"./bloby-B2Y96IF2.js";export{e as Mermaid};
|