bloby-bot 0.50.3 → 0.51.2
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-DX6-xoTY.js} +4 -4
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-z8OpDrVr.js → highlighted-body-OFNGDK62-Bace_Nul.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-CYB8h2y4.js +1 -0
- package/dist-bloby/bloby.html +1 -1
- package/package.json +4 -4
- package/supervisor/chat/src/components/Chat/HeadphonesAnimation.tsx +74 -36
- package/supervisor/index.ts +8 -2
- package/supervisor/public/morphy/headphones.json +40 -0
- package/supervisor/public/morphy/headphones.png +0 -0
- package/supervisor/public/morphy/teleporting.json +38 -0
- package/supervisor/public/morphy/teleporting.png +0 -0
- package/supervisor/widget.js +108 -51
- package/workspace/client/public/morphy/headphones.json +40 -0
- package/workspace/client/public/morphy/headphones.png +0 -0
- package/workspace/client/public/morphy/teleporting.json +38 -0
- package/workspace/client/public/morphy/teleporting.png +0 -0
- package/workspace/client/public/sw.js +1 -1
- package/workspace/skills/plaud/SKILL.md +139 -37
- package/dist-bloby/assets/mermaid-GHXKKRXX-DAQ7dBvr.js +0 -1
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bloby-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.2",
|
|
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.",
|
|
@@ -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,24 @@ 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 (state === 'idle' || !sprite) {
|
|
105
|
+
if (state === 'idle' || !sprite || !cfg) {
|
|
78
106
|
ctx.clearRect(0, 0, displayW, displayH);
|
|
79
107
|
rafRef.current = requestAnimationFrame(tick);
|
|
80
108
|
return;
|
|
81
109
|
}
|
|
82
110
|
|
|
83
|
-
const
|
|
111
|
+
const baseFps = cfg.fps;
|
|
112
|
+
const fps =
|
|
113
|
+
state === 'activating' || state === 'activating_then_deactivate'
|
|
114
|
+
? (cfg.clips.enter.fps ?? baseFps)
|
|
115
|
+
: state === 'deactivating'
|
|
116
|
+
? (cfg.clips.exit.fps ?? baseFps)
|
|
117
|
+
: (cfg.clips.active.fps ?? baseFps);
|
|
84
118
|
const frameMs = 1000 / fps;
|
|
85
119
|
const delta = now - lastRef.current;
|
|
86
120
|
if (delta >= frameMs) {
|
|
@@ -88,24 +122,25 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
88
122
|
|
|
89
123
|
if (state === 'activating' || state === 'activating_then_deactivate') {
|
|
90
124
|
frameRef.current++;
|
|
91
|
-
if (frameRef.current >
|
|
125
|
+
if (frameRef.current > cfg.clips.enter.to) {
|
|
92
126
|
if (state === 'activating_then_deactivate') {
|
|
93
127
|
stateRef.current = 'deactivating';
|
|
94
|
-
frameRef.current =
|
|
128
|
+
frameRef.current = cfg.clips.exit.from;
|
|
95
129
|
} else {
|
|
96
130
|
stateRef.current = 'recording';
|
|
97
|
-
frameRef.current =
|
|
131
|
+
frameRef.current = cfg.clips.active.from;
|
|
98
132
|
pingPongRef.current = 1;
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
} else if (state === 'recording') {
|
|
102
136
|
frameRef.current += pingPongRef.current;
|
|
103
|
-
if (frameRef.current >=
|
|
104
|
-
else if (frameRef.current <=
|
|
137
|
+
if (frameRef.current >= cfg.clips.active.to) { frameRef.current = cfg.clips.active.to; pingPongRef.current = -1; }
|
|
138
|
+
else if (frameRef.current <= cfg.clips.active.from) { frameRef.current = cfg.clips.active.from; pingPongRef.current = 1; }
|
|
105
139
|
} else if (state === 'deactivating') {
|
|
106
|
-
frameRef.current
|
|
107
|
-
if (frameRef.current
|
|
140
|
+
frameRef.current++;
|
|
141
|
+
if (frameRef.current > cfg.clips.exit.to) {
|
|
108
142
|
stateRef.current = 'idle';
|
|
143
|
+
frameRef.current = cfg.clips.idle.from;
|
|
109
144
|
ctx.clearRect(0, 0, displayW, displayH);
|
|
110
145
|
onDoneRef.current?.();
|
|
111
146
|
rafRef.current = requestAnimationFrame(tick);
|
|
@@ -114,12 +149,15 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
|
|
|
114
149
|
}
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
152
|
+
const cols = cfg.grid.cols;
|
|
153
|
+
const fw = cfg.frame.w;
|
|
154
|
+
const fh = cfg.frame.h;
|
|
155
|
+
const col = frameRef.current % cols;
|
|
156
|
+
const row = Math.floor(frameRef.current / cols);
|
|
119
157
|
ctx.clearRect(0, 0, displayW, displayH);
|
|
120
158
|
ctx.imageSmoothingEnabled = true;
|
|
121
159
|
ctx.imageSmoothingQuality = 'high';
|
|
122
|
-
ctx.drawImage(sprite, col *
|
|
160
|
+
ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);
|
|
123
161
|
|
|
124
162
|
rafRef.current = requestAnimationFrame(tick);
|
|
125
163
|
}, [displayW, displayH]);
|
package/supervisor/index.ts
CHANGED
|
@@ -46,6 +46,11 @@ const PLATFORM_ASSETS = new Set([
|
|
|
46
46
|
'/manifest.json',
|
|
47
47
|
]);
|
|
48
48
|
|
|
49
|
+
// Directory-prefix platform assets — anything under these is served from supervisor/public/.
|
|
50
|
+
// Used for the Morphy animation set: drop new {clip}.png + {clip}.json into public/morphy/
|
|
51
|
+
// and they're automatically served without touching the allowlist.
|
|
52
|
+
const PLATFORM_ASSET_DIRS = ['/morphy/'];
|
|
53
|
+
|
|
49
54
|
// Ensure dist-bloby exists (postinstall may have failed silently)
|
|
50
55
|
if (!fs.existsSync(DIST_BLOBY)) {
|
|
51
56
|
log.info('Building bloby chat UI (first run)...');
|
|
@@ -85,7 +90,7 @@ const SW_JS = `// Service worker — app-shell caching + push notifications
|
|
|
85
90
|
// JS/CSS modules → stale-while-revalidate
|
|
86
91
|
// API, WebSocket, Vite internals → network-only (no cache)
|
|
87
92
|
|
|
88
|
-
var CACHE = 'bloby-
|
|
93
|
+
var CACHE = 'bloby-v16';
|
|
89
94
|
var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
|
|
90
95
|
|
|
91
96
|
// Precache the HTML shell on install so the cache is never empty.
|
|
@@ -1631,7 +1636,8 @@ mint();
|
|
|
1631
1636
|
|
|
1632
1637
|
// Platform assets — served from supervisor/public/ so they survive workspace swaps
|
|
1633
1638
|
const cleanUrl = (req.url || '').split('?')[0];
|
|
1634
|
-
|
|
1639
|
+
const inAssetDir = PLATFORM_ASSET_DIRS.some((d) => cleanUrl.startsWith(d) && !cleanUrl.includes('..'));
|
|
1640
|
+
if (PLATFORM_ASSETS.has(cleanUrl) || inAssetDir) {
|
|
1635
1641
|
const assetPath = path.join(SUPERVISOR_PUBLIC, cleanUrl);
|
|
1636
1642
|
try {
|
|
1637
1643
|
const stat = fs.statSync(assetPath);
|
|
@@ -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
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teleporting",
|
|
3
|
+
"spritesheet": "teleporting.png",
|
|
4
|
+
"frame": {
|
|
5
|
+
"w": 218,
|
|
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": 19,
|
|
17
|
+
"to": 19,
|
|
18
|
+
"mode": "hold"
|
|
19
|
+
},
|
|
20
|
+
"enter": {
|
|
21
|
+
"from": 20,
|
|
22
|
+
"to": 45,
|
|
23
|
+
"mode": "forward",
|
|
24
|
+
"next": "active"
|
|
25
|
+
},
|
|
26
|
+
"active": {
|
|
27
|
+
"from": 46,
|
|
28
|
+
"to": 68,
|
|
29
|
+
"mode": "pingpong"
|
|
30
|
+
},
|
|
31
|
+
"exit": {
|
|
32
|
+
"from": 69,
|
|
33
|
+
"to": 121,
|
|
34
|
+
"mode": "forward",
|
|
35
|
+
"next": "idle"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
Binary file
|
package/supervisor/widget.js
CHANGED
|
@@ -33,24 +33,34 @@
|
|
|
33
33
|
|
|
34
34
|
// ══════════════════════════════════════════════════════════════════
|
|
35
35
|
// ── Blob Sprite Sheet Config (splash only) ───────────────────────
|
|
36
|
+
// Loaded from /morphy/teleporting.json. Clips map to animStates:
|
|
37
|
+
// idle (hold) → animState 'idle'
|
|
38
|
+
// enter (forward) → animState 'melting'
|
|
39
|
+
// active (pingpong)→ animState 'traveling'
|
|
40
|
+
// exit (forward) → animState 'reforming'
|
|
36
41
|
// ══════════════════════════════════════════════════════════════════
|
|
37
42
|
|
|
43
|
+
var BLOB_CONFIG_URL = '/morphy/teleporting.json';
|
|
44
|
+
var BLOB_ASSETS_DIR = '/morphy/';
|
|
45
|
+
|
|
46
|
+
// Defaults mirror the teleporting.json shipped at the URL above so
|
|
47
|
+
// animation works even if the fetch is cached at the wrong revision.
|
|
38
48
|
var COLS = 16;
|
|
39
|
-
var FRAME_W =
|
|
40
|
-
var FRAME_H =
|
|
49
|
+
var FRAME_W = 218;
|
|
50
|
+
var FRAME_H = 180;
|
|
41
51
|
var DISPLAY_H = 58;
|
|
42
52
|
var DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
43
53
|
|
|
44
|
-
var IDLE_START =
|
|
45
|
-
var MELT_START =
|
|
46
|
-
var TRAVEL_START =
|
|
47
|
-
var REFORM_START =
|
|
54
|
+
var IDLE_START = 19, IDLE_END = 19;
|
|
55
|
+
var MELT_START = 20, MELT_END = 45;
|
|
56
|
+
var TRAVEL_START = 46, TRAVEL_END = 68;
|
|
57
|
+
var REFORM_START = 69, REFORM_END = 121;
|
|
48
58
|
|
|
49
|
-
var FPS =
|
|
59
|
+
var FPS = 24;
|
|
50
60
|
var FRAME_MS = 1000 / FPS;
|
|
51
|
-
var IDLE_FPS =
|
|
61
|
+
var IDLE_FPS = 24;
|
|
52
62
|
var IDLE_FRAME_MS = 1000 / IDLE_FPS;
|
|
53
|
-
var REFORM_FPS =
|
|
63
|
+
var REFORM_FPS = 24;
|
|
54
64
|
var REFORM_FRAME_MS = 1000 / REFORM_FPS;
|
|
55
65
|
|
|
56
66
|
var TRAVEL_PX_PER_MS = 0.65;
|
|
@@ -67,24 +77,31 @@
|
|
|
67
77
|
document.body.appendChild(badgeEl);
|
|
68
78
|
|
|
69
79
|
// ══════════════════════════════════════════════════════════════════
|
|
70
|
-
// ── 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'
|
|
71
86
|
// ══════════════════════════════════════════════════════════════════
|
|
72
87
|
|
|
88
|
+
var HP_CONFIG_URL = '/morphy/headphones.json';
|
|
89
|
+
|
|
90
|
+
// Defaults mirror the shipped JSON so animation works even if fetch is cached.
|
|
73
91
|
var HP_COLS = 16;
|
|
74
|
-
var HP_FRAME_W =
|
|
75
|
-
var HP_FRAME_H =
|
|
76
|
-
// Bubble display: fill 60px width exactly
|
|
92
|
+
var HP_FRAME_W = 180;
|
|
93
|
+
var HP_FRAME_H = 180;
|
|
77
94
|
var HP_BUBBLE_H = 40;
|
|
78
|
-
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);
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
var
|
|
82
|
-
var
|
|
83
|
-
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;
|
|
84
101
|
|
|
85
|
-
|
|
86
|
-
var
|
|
87
|
-
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
|
|
88
105
|
|
|
89
106
|
// Long-press threshold
|
|
90
107
|
var HP_HOLD_MS = 500;
|
|
@@ -147,6 +164,7 @@
|
|
|
147
164
|
var animState = 'loading';
|
|
148
165
|
var currentFrame = 0;
|
|
149
166
|
var idleDirection = 1;
|
|
167
|
+
var travelFrameDir = 1;
|
|
150
168
|
var lastFrameTime = 0;
|
|
151
169
|
var travelDuration = 0;
|
|
152
170
|
var travelStartTime = 0;
|
|
@@ -181,30 +199,70 @@
|
|
|
181
199
|
var hpSpeechInstance = null;
|
|
182
200
|
var hpSpeechTranscript = '';
|
|
183
201
|
|
|
184
|
-
// ── Load blob sprite sheet ──
|
|
202
|
+
// ── Load blob sprite sheet (config + image) ──
|
|
203
|
+
function applyBlobConfig(cfg) {
|
|
204
|
+
COLS = cfg.grid.cols;
|
|
205
|
+
FRAME_W = cfg.frame.w;
|
|
206
|
+
FRAME_H = cfg.frame.h;
|
|
207
|
+
DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
208
|
+
IDLE_START = cfg.clips.idle.from; IDLE_END = cfg.clips.idle.to;
|
|
209
|
+
MELT_START = cfg.clips.enter.from; MELT_END = cfg.clips.enter.to;
|
|
210
|
+
TRAVEL_START = cfg.clips.active.from; TRAVEL_END = cfg.clips.active.to;
|
|
211
|
+
REFORM_START = cfg.clips.exit.from; REFORM_END = cfg.clips.exit.to;
|
|
212
|
+
FPS = cfg.fps; FRAME_MS = 1000 / FPS;
|
|
213
|
+
IDLE_FPS = cfg.fps; IDLE_FRAME_MS = 1000 / IDLE_FPS;
|
|
214
|
+
REFORM_FPS = cfg.fps; REFORM_FRAME_MS = 1000 / REFORM_FPS;
|
|
215
|
+
}
|
|
216
|
+
|
|
185
217
|
function loadSprite(onDone, onFail) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
fetch(BLOB_CONFIG_URL)
|
|
219
|
+
.then(function (r) { if (!r.ok) throw new Error('blob config ' + r.status); return r.json(); })
|
|
220
|
+
.then(function (cfg) {
|
|
221
|
+
applyBlobConfig(cfg);
|
|
222
|
+
var img = new Image();
|
|
223
|
+
img.onload = function () {
|
|
224
|
+
spriteSheet = img;
|
|
225
|
+
center.x = Math.round(W / 2);
|
|
226
|
+
center.y = Math.round(H / 2);
|
|
227
|
+
currentFrame = IDLE_START;
|
|
228
|
+
animState = 'idle';
|
|
229
|
+
var splash = document.getElementById('splash');
|
|
230
|
+
if (splash) splash.style.display = 'none';
|
|
231
|
+
onDone();
|
|
232
|
+
};
|
|
233
|
+
img.onerror = function () { if (onFail) onFail(); };
|
|
234
|
+
img.src = BLOB_ASSETS_DIR + cfg.spritesheet;
|
|
235
|
+
})
|
|
236
|
+
.catch(function () { if (onFail) onFail(); });
|
|
237
|
+
}
|
|
238
|
+
|
|
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);
|
|
199
252
|
}
|
|
200
253
|
|
|
201
|
-
// ── Load headphones sprite sheet ──
|
|
202
254
|
function loadHpSprite(cb) {
|
|
203
255
|
if (hpSpriteSheet) { if (cb) cb(); return; }
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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); });
|
|
208
266
|
}
|
|
209
267
|
|
|
210
268
|
// ── Trigger blob move (splash) ──
|
|
@@ -230,7 +288,8 @@
|
|
|
230
288
|
// ══════════════════════════════════════════════════════════════════
|
|
231
289
|
|
|
232
290
|
function hpFps() {
|
|
233
|
-
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;
|
|
234
293
|
return HP_BASE_FPS;
|
|
235
294
|
}
|
|
236
295
|
|
|
@@ -246,9 +305,8 @@
|
|
|
246
305
|
hpFrame++;
|
|
247
306
|
if (hpFrame > HP_ACTIVATE_END) {
|
|
248
307
|
if (hpState === 'activating_then_deactivate') {
|
|
249
|
-
// Reverse back to idle
|
|
250
308
|
hpState = 'deactivating';
|
|
251
|
-
hpFrame =
|
|
309
|
+
hpFrame = HP_EXIT_START;
|
|
252
310
|
} else {
|
|
253
311
|
hpState = 'recording';
|
|
254
312
|
hpFrame = HP_RECORD_START;
|
|
@@ -260,9 +318,8 @@
|
|
|
260
318
|
if (hpFrame >= HP_RECORD_END) { hpFrame = HP_RECORD_END; hpPingPong = -1; }
|
|
261
319
|
else if (hpFrame <= HP_RECORD_START) { hpFrame = HP_RECORD_START; hpPingPong = 1; }
|
|
262
320
|
} else if (hpState === 'deactivating') {
|
|
263
|
-
|
|
264
|
-
hpFrame
|
|
265
|
-
if (hpFrame < HP_ACTIVATE_START) {
|
|
321
|
+
hpFrame++;
|
|
322
|
+
if (hpFrame > HP_EXIT_END) {
|
|
266
323
|
hpState = 'idle';
|
|
267
324
|
hpFrame = HP_IDLE_FRAME;
|
|
268
325
|
}
|
|
@@ -300,10 +357,11 @@
|
|
|
300
357
|
else if (currentFrame <= IDLE_START) { currentFrame = IDLE_START; idleDirection = 1; }
|
|
301
358
|
} else if (animState === 'melting') {
|
|
302
359
|
currentFrame++;
|
|
303
|
-
if (currentFrame > MELT_END) { animState = 'traveling'; currentFrame = TRAVEL_START; travelStartTime = now; }
|
|
360
|
+
if (currentFrame > MELT_END) { animState = 'traveling'; currentFrame = TRAVEL_START; travelStartTime = now; travelFrameDir = 1; }
|
|
304
361
|
} else if (animState === 'traveling') {
|
|
305
|
-
currentFrame
|
|
306
|
-
if (currentFrame
|
|
362
|
+
currentFrame += travelFrameDir;
|
|
363
|
+
if (currentFrame >= TRAVEL_END) { currentFrame = TRAVEL_END; travelFrameDir = -1; }
|
|
364
|
+
else if (currentFrame <= TRAVEL_START) { currentFrame = TRAVEL_START; travelFrameDir = 1; }
|
|
307
365
|
} else if (animState === 'reforming') {
|
|
308
366
|
currentFrame++;
|
|
309
367
|
if (currentFrame > REFORM_END) {
|
|
@@ -540,12 +598,11 @@
|
|
|
540
598
|
canvas.style.transition = 'transform 0.2s ease';
|
|
541
599
|
canvas.style.transform = '';
|
|
542
600
|
|
|
543
|
-
// v2 deactivation: reverse 40→11 (activate played backwards)
|
|
544
601
|
if (hpState === 'activating') {
|
|
545
602
|
hpState = 'activating_then_deactivate';
|
|
546
603
|
} else if (hpState === 'recording') {
|
|
547
604
|
hpState = 'deactivating';
|
|
548
|
-
hpFrame =
|
|
605
|
+
hpFrame = HP_EXIT_START;
|
|
549
606
|
hpLastFrameTime = performance.now();
|
|
550
607
|
}
|
|
551
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
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teleporting",
|
|
3
|
+
"spritesheet": "teleporting.png",
|
|
4
|
+
"frame": {
|
|
5
|
+
"w": 218,
|
|
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": 19,
|
|
17
|
+
"to": 19,
|
|
18
|
+
"mode": "hold"
|
|
19
|
+
},
|
|
20
|
+
"enter": {
|
|
21
|
+
"from": 20,
|
|
22
|
+
"to": 45,
|
|
23
|
+
"mode": "forward",
|
|
24
|
+
"next": "active"
|
|
25
|
+
},
|
|
26
|
+
"active": {
|
|
27
|
+
"from": 46,
|
|
28
|
+
"to": 68,
|
|
29
|
+
"mode": "pingpong"
|
|
30
|
+
},
|
|
31
|
+
"exit": {
|
|
32
|
+
"from": 69,
|
|
33
|
+
"to": 121,
|
|
34
|
+
"mode": "forward",
|
|
35
|
+
"next": "idle"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
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
|