bloub-to-vue-and-react 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,111 +1,249 @@
1
- # bloub
2
-
3
- An SVG recreation of the x.ai bot avatar: **one filled black shape** that morphs
4
- between 14 states, **two white shapes** for the eyes that morph independently, on
5
- a plain background. No animation library.
6
-
7
- ![The avatar going through idle, wink, orbit and burst](docs/demo.gif)
8
-
9
- ## Running it
10
-
11
- ```bash
12
- pnpm install
13
- pnpm dev
14
- ```
15
-
16
- Then open http://localhost:5190.
17
-
18
- ```bash
19
- pnpm test # vitest
20
- pnpm build # vue-tsc --noEmit && vite build
21
- ```
22
-
23
- Vue 3, Vite, TypeScript, Tailwind 4. No ESLint and no Prettier: `vue-tsc` is the
24
- only gate, so run `pnpm build` before you call something done.
25
-
26
- ## What's in it
27
-
28
- The rail on the left switches between three views. **Customise** offers 8 body
29
- shapes, 12 colours and 16 rest expressions, kept between visits. **Animations** is
30
- a small editor: arrange states into a timeline, set how long each is held, save the
31
- result. **Settings** holds the language (French, English or Chinese) and the
32
- credits.
33
-
34
- Anything on screen can be exported: the avatar as SVG, PNG or an animated GIF, and
35
- a whole timeline as GIF or MP4. The still formats need no library at all, and the
36
- video encoder is only fetched the first time you ask for one.
37
-
38
- Two URLs are worth knowing:
39
-
40
- - `#planche`: the 14 states side by side, frozen. Quick visual check.
41
- - `#etat=orbit&stop`: opens one state directly, playback paused.
42
-
43
- ![The 14 states, frozen side by side](docs/states.png)
44
-
45
- ## Why the numbers look arbitrary
46
-
47
- They're measured, not chosen. The reference video was cut at 10 fps and each
48
- state measured off the frames: silhouettes by sub-pixel ray casting, eyes by
49
- capsule fitting, colours and stroke widths by direct sampling.
50
-
51
- So the constants in the code are **measurements**, and rounding them to friendlier
52
- values breaks the resemblance, which is the only thing this project is trying to
53
- get right. A few are counter-intuitive enough to be worth knowing before you
54
- correct anything:
55
-
56
- | What you'd assume | What the video shows |
57
- |---|---|
58
- | The eyes lean `//` | They lean `\\`, around 26° off vertical |
59
- | The body is a squircle | It's a perfect circle, radial deviation under 0.7% |
60
- | Transitions are springs | Exponential ease-outs; the body never overshoots |
61
- | The comet crosses the screen | The dot stays put, the trail orbits it |
62
- | The avatar floats at rest | It doesn't. The life is gaze drift and blinking |
63
-
64
- [docs/measurements.md](docs/measurements.md) has the rest, including how to
65
- regenerate the extracted profiles.
66
-
67
- ## How it's put together
68
-
69
- `src/bot/` is framework-free and clock-free: `engine.sample(t)` is a pure
70
- function of time. Pausing, resuming, jumping to an arbitrary date and running
71
- tests all produce the same image, which is what makes the frozen state board and
72
- the DOM-less test suite possible.
73
-
74
- | | |
75
- |---|---|
76
- | [docs/architecture.md](docs/architecture.md) | The engine, radial-profile morphing, eyes as mask holes |
77
- | [docs/measurements.md](docs/measurements.md) | What was measured, and regenerating `profiles.ts` |
78
- | [docs/intro.md](docs/intro.md) | The arrival sequence, and why it only plays one state |
79
- | [docs/interface.md](docs/interface.md) | Layout, the three-column scene, CSS traps |
80
- | [docs/export.md](docs/export.md) | Exporting to SVG, PNG, GIF and MP4 |
81
- | [docs/i18n.md](docs/i18n.md) | The hand-rolled translation layer |
82
-
83
- ## Using the component
84
-
85
- ```vue
86
- <BloubBot v-model:block="block" v-model:state="state" v-model:playing="playing" />
87
- <BloubBot state="orbit" :size="120" :frozen-at="1.2" />
88
- ```
89
-
90
- `block` is the playback cursor: a montage can play the same state twice, so the
91
- index is what identifies where you are; `state` follows it as an output. Pass
92
- `frozenAt` and the component renders one exact frame with no animation loop, which
93
- is how the thumbnails and the state board are drawn.
94
-
95
- Props: `size`, `shape`, `color`, `expression`, `paper`, `frozenAt`, `cycle`,
96
- `follow`, `gaze`. Models: `block`, `state`, `playing`, `elapsed`. See
97
- [BloubBot.vue](src/components/BloubBot.vue) for the details.
98
-
99
- ## Changes
100
-
101
- [CHANGELOG.md](CHANGELOG.md), one entry per release — which is how you tell whether the
102
- copy you have carries a given fix.
103
-
104
- ## License
105
-
106
- MIT. See [LICENSE](LICENSE).
107
-
108
- Not affiliated with, endorsed by or connected to x.ai. It recreates the visual
109
- behaviour of their bot avatar as an exercise; "Grok" and "x.ai" belong to their
110
- owners. The MIT licence covers the code in this repository, not the design it
111
- imitates.
1
+ # bloub
2
+
3
+ 一个用 SVG 还原 x.ai 机器人头像的项目:**一个实心黑色形状**在 14 个状态之间形变,**两个白色形状**作为眼睛、各自独立形变,背景为纯色。不依赖任何动画库。
4
+
5
+ ![头像经历 idle、wink、orbit burst](docs/demo.gif)
6
+
7
+ ## 介绍
8
+
9
+ bloub 是一个框架无关的头像引擎,为 **Vue 3** 和 **React 18+** 提供了一等公民的适配器。整套东西由一个共享内核(`src/bot/` + `src/core/`)驱动,因此两个框架渲染出的画面像素级一致,也永远不会互相漂移。
10
+
11
+ | | Vue 3 | React 18+ |
12
+ |---|---|---|
13
+ | 入口 | `bloub-to-vue-and-react` | `bloub-to-vue-and-react/react` |
14
+ | 组件 | `BloubBot` | `BloubBot` |
15
+ | Props | `size shape color expression paper frozenAt cycle follow gaze ariaLabel` | 相同 |
16
+ | Models | `block state playing elapsed`(通过 `v-model`) | `block state playing`(受控)+ `elapsed`(输出) |
17
+ | 命令式 | `seek` `rendAt` | `seek` `rendAt`(通过 `ref`) |
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ pnpm add bloub-to-vue-and-react
23
+ ```
24
+
25
+ `vue` 是 peer 依赖。`react` 和 `react-dom` 是**可选** peer 依赖:只有使用 React 适配器时才需要安装。纯 Vue 项目永远不会把 React 拉进来,反之亦然。
26
+
27
+ ```bash
28
+ # React 用户
29
+ pnpm add bloub-to-vue-and-react react react-dom
30
+ ```
31
+
32
+ ## 使用
33
+
34
+ ### Vue 3
35
+
36
+ ```vue
37
+ <script setup lang="ts">
38
+ import { ref } from 'vue'
39
+ import { BloubBot } from 'bloub-to-vue-and-react'
40
+
41
+ const block = ref(0)
42
+ const state = ref('idle')
43
+ const playing = ref(false)
44
+ </script>
45
+
46
+ <template>
47
+ <BloubBot v-model:block="block" v-model:state="state" v-model:playing="playing" />
48
+ </template>
49
+ ```
50
+
51
+ `block` 是播放游标——一段蒙太奇可能两次播放同一个状态,所以用索引来标识你所在的位置;`state` 作为输出跟随它。`playing` 用于开始和停止循环播放。
52
+
53
+ 要渲染一帧精确画面而不启动动画循环(缩略图、状态看板),传入 `frozenAt`:
54
+
55
+ ```vue
56
+ <BloubBot state="orbit" :size="120" :frozen-at="1.2" />
57
+ ```
58
+
59
+ 通过模板 ref 进行命令式播放:
60
+
61
+ ```vue
62
+ <script setup lang="ts">
63
+ import { ref } from 'vue'
64
+ import { BloubBot } from 'bloub-to-vue-and-react'
65
+
66
+ const bot = ref<InstanceType<typeof BloubBot> | null>(null)
67
+ bot.value?.seek(2, 0.5) // 跳到第 2 块、0.5 秒处
68
+ bot.value?.rendAt(3.1) // 在绝对时间 3.1 秒处渲染蒙太奇
69
+ </script>
70
+ ```
71
+
72
+ ### React 18+
73
+
74
+ ```tsx
75
+ import { useRef, useState } from 'react'
76
+ import { BloubBot, type BloubModel, type BloubBotHandle } from 'bloub-to-vue-and-react/react'
77
+
78
+ function Demo() {
79
+ const bot = useRef<BloubBotHandle>(null)
80
+ const [model, setModel] = useState<BloubModel>({ block: 0, state: 'idle', playing: false, elapsed: 0 })
81
+
82
+ return (
83
+ <BloubBot
84
+ ref={bot}
85
+ block={model.block}
86
+ state={model.state}
87
+ playing={model.playing}
88
+ onModelChange={setModel}
89
+ />
90
+ )
91
+ }
92
+ ```
93
+
94
+ `block` / `state` / `playing` 是受控的;`onModelChange` 会把内核的状态回报回来,让父组件在蒙太奇推进时保持同步。`elapsed` 是只读输出。
95
+
96
+ 冻结单帧渲染:
97
+
98
+ ```tsx
99
+ <BloubBot state="orbit" size={120} frozenAt={1.2} />
100
+ ```
101
+
102
+ 命令式播放:
103
+
104
+ ```tsx
105
+ bot.current?.seek(2, 0.5)
106
+ bot.current?.rendAt(3.1)
107
+ ```
108
+
109
+ ## 包含内容
110
+
111
+ ### 形状(8 种)
112
+
113
+ `cercle`、`galet`、`squircle`、`capsule`、`triangle`、`hexagone`、`nuage`、`goutte`。
114
+
115
+ ### 颜色(12 种)
116
+
117
+ `encre`、`creme`、`brun`、`rouge`、`orange`、`ambre`、`vert`、`turquoise`、`bleu`、`violet`、`rose`、`gris`。
118
+
119
+ ### 表情(31 个)
120
+
121
+ 引擎内置 19 个面部表情和 12 个视线方向。其中带嘴巴的两个(`stupefait`、`sourire`)也会绘制出嘴巴。
122
+
123
+ | id | 含义 |
124
+ |---|---|
125
+ | `neutre` | 中性(默认) |
126
+ | `attentif` | 专注 |
127
+ | `surpris` | 惊讶 |
128
+ | `excite` | 兴奋 |
129
+ | `heureux` | 开心 |
130
+ | `hilare` | 大笑 |
131
+ | `colere` | 愤怒 |
132
+ | `triste` | 悲伤 |
133
+ | `effraye` | 害怕 |
134
+ | `mefiant` | 狐疑 |
135
+ | `confus` | 困惑 |
136
+ | `curieux` | 好奇 |
137
+ | `fier` | 得意 |
138
+ | `timide` | 害羞 |
139
+ | `blase` | 冷漠 |
140
+ | `somnolent` | 困倦 |
141
+ | `admiratif` | 钦佩 |
142
+ | `stupefait` | 目瞪口呆(带嘴巴) |
143
+ | `sourire` | 微笑(带嘴巴) |
144
+ | `regardHaut` / `regardBas` | 向上看 / 向下看 |
145
+ | `regardGauche` / `regardDroite` | 向左看 / 向右看 |
146
+ | `regardHautGauche` / `regardHautDroite` | 左上 / 右上 |
147
+ | `regardBasGauche` / `regardBasDroite` | 左下 / 右下 |
148
+ | `regardArriereHaut` / `regardArriereBas` | 向后上 / 向后下 |
149
+ | `regardArriereGauche` / `regardArriereDroite` | 向后左 / 向后右 |
150
+
151
+ ### 状态(14 个)
152
+
153
+ `idle`、`thinking`、`wink`、`wide`、`alert`、`notify`、`exclaim`、`sleep`、`egg`、`hexagon`、`play`、`orbit`、`burst`、`comet`——另加 `swirl`,保留用于界面过渡。
154
+
155
+ ## 使用场景
156
+
157
+ - **聊天 / 助手头像**——空闲时目光游移、眨眼,模型流式输出时进入 `thinking`,来新消息时 `notify`。
158
+ - **响应式存在感**——用 `follow` 跟随指针,用 `gaze` 脚本编排入场序列,事件触发时 `alert` / `exclaim`。
159
+ - **动画导出**——冻结渲染器(`frozenAt`、`rendAt`)支撑逐帧精确导出为 SVG、PNG、GIF 和 MP4。
160
+ - **产品自定义器**——8 种形状 × 12 种颜色 × 31 个表情,持久化并作为 `cycle` 蒙太奇回放。
161
+
162
+ ## 体积与兼容负担
163
+
164
+ 内核与框架无关,因此消费方为框架特定代码付出的成本只有一层薄薄的适配器。生产构建体积:
165
+
166
+ | 分块 | 原始 | gzip | 谁加载 |
167
+ |---|---|---|---|
168
+ | 共享内核(`src/bot/` + `src/core/`) | 46.96 kB | 14.60 kB | 所有人 |
169
+ | Vue 适配器 | 2.80 kB | 1.21 kB | 仅 Vue |
170
+ | React 适配器 | 2.39 kB | 1.14 kB | 仅 React |
171
+
172
+ 两个适配器是**相互独立的入口**,共享同一份内核分块:
173
+
174
+ - 一个 Vue 应用引入 `bloub-to-vue-and-react`,加载内核 + Vue 适配器,携带 **0 字节 React 代码**——产物中不含任何 `react`、`react/jsx-runtime` 或 `react-dom` 的引入。
175
+ - 一个 React 应用引入 `bloub-to-vue-and-react/react`,加载内核 + React 适配器,携带 **0 字节 Vue 代码**。
176
+ - `react` / `react-dom` 是可选 peer 依赖,因此纯 Vue 项目永远不会安装 React。
177
+
178
+ 简而言之:约 47 kB 的内核才是真正的产品本体(形变、表情、动画);框架税是每个适配器约 1–1.2 kB(gzip),且没有任何跨框架冗余。
179
+
180
+ ## 运行
181
+
182
+ ```bash
183
+ pnpm install
184
+ pnpm dev
185
+ ```
186
+
187
+ 然后打开 http://localhost:5190。
188
+
189
+ ```bash
190
+ pnpm test # vitest
191
+ pnpm build # vue-tsc --noEmit && vite build
192
+ pnpm build:lib # 为两个入口打包并生成类型声明
193
+ ```
194
+
195
+ Vue 3、Vite、TypeScript、Tailwind 4。没有 ESLint 和 Prettier:`vue-tsc` 是唯一的关卡,所以完工前请先运行 `pnpm build`。
196
+
197
+ ## 里面有什么
198
+
199
+ 左侧的导航栏在三个视图之间切换。**Customise** 提供 8 种身体形状、12 种颜色和 31 个表情,跨访问保留。**Animations** 是一个小编辑器:把状态编排进时间轴、设置每个状态停留的时长、保存结果。**Settings** 存放语言(法语、英语或中文)和致谢。
200
+
201
+ 屏幕上的任何东西都可以导出:头像可导出为 SVG、PNG 或动画 GIF,整条时间轴可导出为 GIF 或 MP4。静态格式完全不依赖任何库,视频编码器只在首次请求时才会按需加载。
202
+
203
+ 有两个值得了解的 URL:
204
+
205
+ - `#planche`:14 个状态并排冻结展示,用于快速目视检查。
206
+ - `#etat=orbit&stop`:直接打开某个状态,播放暂停。
207
+
208
+ ![14 个状态并排冻结展示](docs/states.png)
209
+
210
+ ## 为什么这些数字看起来很随意
211
+
212
+ 它们是测量出来的,而不是拍脑袋定的。参考视频按 10 fps 切帧,每个状态都从帧上测量出来:轮廓用亚像素射线投射,眼睛用胶囊拟合,颜色和描边宽度用直接采样。
213
+
214
+ 所以代码里的常量都是**测量值**,把它们四舍五入成更"顺眼"的数值会破坏相似度——而这正是本项目唯一追求的东西。有几个反直觉的点值得在改动之前了解:
215
+
216
+ | 你以为的样子 | 视频里实际的样子 |
217
+ |---|---|
218
+ | 眼睛是 `//` 方向倾斜 | 它们是 `\\` 方向,相对竖直约倾斜 26° |
219
+ | 身体是方圆形 | 它是完美的圆形,径向偏差低于 0.7% |
220
+ | 过渡是弹簧动效 | 指数缓出;身体从不回弹过冲 |
221
+ | 彗星划过屏幕 | 圆点停在原地,是拖尾绕着它转 |
222
+ | 头像在静止时漂浮 | 并非如此,生命力来自目光游移和眨眼 |
223
+
224
+ [docs/measurements.md](docs/measurements.md) 里有其余内容,包括如何重新生成提取出的轮廓。
225
+
226
+ ## 架构说明
227
+
228
+ `src/bot/` 和 `src/core/` 与框架无关、与时钟无关:`engine.sample(t)` 是时间的纯函数。暂停、恢复、跳转到任意时间点、运行测试,都会产出同一帧画面——这正是冻结状态看板和无需 DOM 的测试套件能够实现的原因。Vue 和 React 适配器([BloubBot.vue](src/components/Core/BloubBot.vue) 和 [BloubBot.tsx](src/components/Core/react/BloubBot.tsx))只是包住这个内核,做的唯一事情就是转发 props 并把帧推送到 DOM。
229
+
230
+ | | |
231
+ |---|---|
232
+ | [docs/architecture.md](docs/architecture.md) | 引擎、径向轮廓形变、作为遮罩孔洞的眼睛 |
233
+ | [docs/measurements.md](docs/measurements.md) | 测量了什么,以及如何重新生成 `profiles.ts` |
234
+ | [docs/intro.md](docs/intro.md) | 入场序列,以及为什么它只播放一个状态 |
235
+ | [docs/interface.md](docs/interface.md) | 布局、三栏场景、CSS 的坑 |
236
+ | [docs/export.md](docs/export.md) | 导出为 SVG、PNG、GIF 和 MP4 |
237
+ | [docs/i18n.md](docs/i18n.md) | 手写的翻译层 |
238
+
239
+ ## 变更记录
240
+
241
+ [CHANGELOG.md](CHANGELOG.md),每个版本一条记录——这也是判断你手上的副本是否包含某个修复的方法。
242
+
243
+ ## 致谢
244
+
245
+ bloub — 动画引擎与素材([jeremy-prt/bloub](https://github.com/jeremy-prt/bloub),MIT)。
246
+
247
+ ## 许可证
248
+
249
+ MIT。见 [LICENSE](LICENSE)。
@@ -0,0 +1,80 @@
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c } from "./dom-DPqMRRT2.js";
2
+ import { forwardRef as l, useCallback as u, useEffect as d, useImperativeHandle as f, useRef as p, useSyncExternalStore as m } from "react";
3
+ import { jsx as h } from "react/jsx-runtime";
4
+ //#region src/components/Core/react/BloubBot.tsx
5
+ var g = "#f9f9f9", _ = "Animated bloub avatar", v = e().blocks;
6
+ function y(e) {
7
+ return a.get(e)?.hex ?? "#0a0a0c";
8
+ }
9
+ var b = l(function(e, a) {
10
+ let { size: l = 320, shape: b = t, color: x = s, expression: S = r, paper: C = g, frozenAt: w, cycle: T = v, follow: E = !1, gaze: D = null, ariaLabel: O = _, block: k, state: A, playing: j, onModelChange: M } = e, N = p(null);
11
+ N.current === null && (N.current = new n({
12
+ shape: b,
13
+ expression: S,
14
+ cycle: T,
15
+ follow: E,
16
+ gaze: D,
17
+ frozenAt: w,
18
+ block: k ?? 0,
19
+ state: A ?? "idle",
20
+ playing: j ?? !1,
21
+ elapsed: 0
22
+ }));
23
+ let P = N.current, F = p(C), I = p(y(x)), L = p(null), R = p(M), z = u(() => {
24
+ let e = L.current;
25
+ e && c(e, o(P.frame, {
26
+ paper: F.current,
27
+ ink: I.current,
28
+ uid: P.uid
29
+ }));
30
+ }, [P]), B = u((e) => P.subscribe(e), [P]), V = m(B, () => P.modelSnapshot);
31
+ d(() => {
32
+ let e = P.subscribeFrame(z);
33
+ return z(), e;
34
+ }, [P, z]), d(() => (P.setRectProvider(() => L.current?.getBoundingClientRect() ?? null), P.start(), () => P.stop()), [P]), d(() => {
35
+ F.current = C, z();
36
+ }, [C, z]), d(() => {
37
+ I.current = y(x), z();
38
+ }, [x, z]), d(() => {
39
+ k !== void 0 && k !== P.block && P.setBlock(k);
40
+ }, [k, P]), d(() => {
41
+ A !== void 0 && A !== P.state && P.setState(A);
42
+ }, [A, P]), d(() => {
43
+ j !== void 0 && j !== P.playing && P.setPlaying(j);
44
+ }, [j, P]), d(() => {
45
+ P.setShape(b);
46
+ }, [b, P]), d(() => {
47
+ P.setExpression(S);
48
+ }, [S, P]), d(() => {
49
+ P.setCycle(T);
50
+ }, [T, P]), d(() => {
51
+ P.setFrozenAt(w);
52
+ }, [w, P]), d(() => {
53
+ P.setFollow(E);
54
+ }, [E, P]), d(() => {
55
+ P.setGaze(D);
56
+ }, [D, P]), d(() => {
57
+ R.current = M;
58
+ }, [M]);
59
+ let H = p(0);
60
+ return d(() => {
61
+ H.current !== V && (H.current = V, R.current?.({
62
+ block: P.block,
63
+ state: P.state,
64
+ playing: P.playing,
65
+ elapsed: P.elapsed
66
+ }));
67
+ }, [V, P]), f(a, () => ({
68
+ seek: (e, t = 0) => P.seek(e, t),
69
+ rendAt: (e) => P.rendAt(e)
70
+ }), [P]), /* @__PURE__ */ h("svg", {
71
+ ref: L,
72
+ width: l,
73
+ height: l,
74
+ viewBox: i,
75
+ role: "img",
76
+ "aria-label": O
77
+ });
78
+ });
79
+ //#endregion
80
+ export { b as BloubBot };
@@ -0,0 +1,96 @@
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c } from "./dom-DPqMRRT2.js";
2
+ import { computed as l, createElementBlock as u, defineComponent as d, mergeModels as f, onBeforeUnmount as p, onMounted as m, openBlock as h, ref as g, unref as _, useModel as v, watch as y } from "vue";
3
+ //#region src/components/Core/BloubBot.vue?vue&type=script&setup=true&lang.ts
4
+ var b = [
5
+ "width",
6
+ "height",
7
+ "viewBox",
8
+ "aria-label"
9
+ ], x = /* @__PURE__ */ d({
10
+ __name: "BloubBot",
11
+ props: /*@__PURE__*/ f({
12
+ size: { default: 320 },
13
+ shape: { default: t },
14
+ color: { default: s },
15
+ expression: { default: r },
16
+ paper: { default: "#f9f9f9" },
17
+ frozenAt: { default: void 0 },
18
+ cycle: { default: () => e().blocks },
19
+ follow: {
20
+ type: Boolean,
21
+ default: !1
22
+ },
23
+ gaze: {
24
+ type: [Function, null],
25
+ default: null
26
+ },
27
+ ariaLabel: { default: "Animated bloub avatar" }
28
+ }, {
29
+ block: { default: 0 },
30
+ blockModifiers: {},
31
+ state: { default: "idle" },
32
+ stateModifiers: {},
33
+ playing: {
34
+ type: Boolean,
35
+ default: !1
36
+ },
37
+ playingModifiers: {},
38
+ elapsed: { default: 0 },
39
+ elapsedModifiers: {}
40
+ }),
41
+ emits: [
42
+ "update:block",
43
+ "update:state",
44
+ "update:playing",
45
+ "update:elapsed"
46
+ ],
47
+ setup(e, { expose: t }) {
48
+ let r = e, s = v(e, "block"), d = v(e, "state"), f = v(e, "playing"), x = v(e, "elapsed"), S = l(() => a.get(r.color)?.hex ?? "#0a0a0c"), C = new n({
49
+ shape: r.shape,
50
+ expression: r.expression,
51
+ cycle: r.cycle,
52
+ follow: r.follow,
53
+ gaze: r.gaze,
54
+ frozenAt: r.frozenAt,
55
+ block: s.value,
56
+ state: d.value,
57
+ playing: f.value,
58
+ elapsed: x.value
59
+ }), w = g(null);
60
+ function T() {
61
+ let e = w.value;
62
+ e && c(e, o(C.frame, {
63
+ paper: r.paper,
64
+ ink: S.value,
65
+ uid: C.uid
66
+ }));
67
+ }
68
+ let E = !1;
69
+ return C.subscribe(() => {
70
+ E = !0, s.value = C.block, d.value = C.state, f.value = C.playing, x.value = C.elapsed, E = !1, T();
71
+ }), y(s, (e) => {
72
+ E || C.setBlock(e);
73
+ }, { flush: "sync" }), y(d, (e) => {
74
+ E || C.setState(e);
75
+ }, { flush: "sync" }), y(f, (e) => {
76
+ E || C.setPlaying(e);
77
+ }, { flush: "sync" }), y(() => r.shape, (e) => C.setShape(e)), y(() => r.expression, (e) => C.setExpression(e)), y(() => r.cycle, (e) => C.setCycle(e)), y(() => r.frozenAt, (e) => C.setFrozenAt(e)), y(() => r.follow, (e) => C.setFollow(e)), y(() => r.gaze, (e) => C.setGaze(e)), y([() => r.paper, S], () => T()), t({
78
+ seek: (e, t = 0) => C.seek(e, t),
79
+ rendAt: (e) => C.rendAt(e)
80
+ }), m(() => {
81
+ C.setRectProvider(() => w.value?.getBoundingClientRect() ?? null), T(), C.start();
82
+ }), p(() => {
83
+ C.stop();
84
+ }), (e, t) => (h(), u("svg", {
85
+ ref_key: "svg",
86
+ ref: w,
87
+ width: r.size,
88
+ height: r.size,
89
+ viewBox: _(i),
90
+ role: "img",
91
+ "aria-label": r.ariaLabel
92
+ }, null, 8, b));
93
+ }
94
+ });
95
+ //#endregion
96
+ export { x as BloubBot };