@mingxy/opencode-mascot 0.2.7 → 0.2.9
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
|
|
3
|
-
import { createSignal } from "solid-js";
|
|
3
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
4
4
|
import type { JSX } from "@opentui/solid";
|
|
5
5
|
import type { MascotPack, MascotState } from "../core/types";
|
|
6
6
|
import { createAnimatedRenderer } from "../core/ascii-renderer";
|
|
@@ -27,6 +27,11 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
|
|
|
27
27
|
sleeping: "baozi",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const MASCOT_WIDTH = 10;
|
|
31
|
+
const PEEK = 2;
|
|
32
|
+
const PEEK_INTERVAL = 1200;
|
|
33
|
+
const EDGE_THRESHOLD = 3;
|
|
34
|
+
|
|
30
35
|
export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
31
36
|
const names = Object.keys(props.mascots);
|
|
32
37
|
const initialName =
|
|
@@ -37,24 +42,96 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
37
42
|
const [currentName, setCurrentName] = createSignal(initialName);
|
|
38
43
|
const [posX, setPosX] = createSignal(20);
|
|
39
44
|
const [posY, setPosY] = createSignal(2);
|
|
45
|
+
const [containerWidth, setContainerWidth] = createSignal(0);
|
|
40
46
|
let dragStartX = 0;
|
|
41
47
|
let dragStartY = 0;
|
|
42
48
|
let dragAnchorX = 0;
|
|
43
49
|
let dragAnchorY = 0;
|
|
44
50
|
let lastClickTime = 0;
|
|
45
51
|
let isDragging = false;
|
|
52
|
+
let hideSide: "left" | "right" | null = null;
|
|
53
|
+
let peekTimer: ReturnType<typeof setInterval> | null = null;
|
|
54
|
+
let returnTimer: ReturnType<typeof setInterval> | null = null;
|
|
46
55
|
|
|
47
56
|
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
48
57
|
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
49
58
|
renderers[name] = createAnimatedRenderer(pack);
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
const stopPeek = () => {
|
|
62
|
+
if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
|
|
63
|
+
};
|
|
64
|
+
const stopReturn = () => {
|
|
65
|
+
if (returnTimer) { clearInterval(returnTimer); returnTimer = null; }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
onCleanup(() => { stopPeek(); stopReturn(); });
|
|
69
|
+
|
|
52
70
|
const switchToNext = () => {
|
|
53
71
|
const cur = currentName();
|
|
54
72
|
const idx = names.indexOf(cur);
|
|
55
73
|
setCurrentName(names[(idx + 1) % names.length]);
|
|
56
74
|
};
|
|
57
75
|
|
|
76
|
+
const getCw = () => containerWidth() || 30;
|
|
77
|
+
|
|
78
|
+
const clampX = (rawX: number): number => {
|
|
79
|
+
const cw = getCw();
|
|
80
|
+
return Math.max(-(MASCOT_WIDTH - PEEK), Math.min(rawX, cw - PEEK));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const clampY = (rawY: number): number => {
|
|
84
|
+
return Math.max(0, rawY);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const checkEdge = () => {
|
|
88
|
+
const cw = getCw();
|
|
89
|
+
const x = posX();
|
|
90
|
+
if (x <= -(MASCOT_WIDTH - PEEK) + EDGE_THRESHOLD) {
|
|
91
|
+
hideSide = "left";
|
|
92
|
+
startPeek();
|
|
93
|
+
} else if (x >= cw - PEEK - EDGE_THRESHOLD) {
|
|
94
|
+
hideSide = "right";
|
|
95
|
+
startPeek();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const startPeek = () => {
|
|
100
|
+
stopPeek();
|
|
101
|
+
let phase = false;
|
|
102
|
+
peekTimer = setInterval(() => {
|
|
103
|
+
phase = !phase;
|
|
104
|
+
const stretch = phase ? PEEK : 0;
|
|
105
|
+
const cw = getCw();
|
|
106
|
+
if (hideSide === "left") {
|
|
107
|
+
setPosX(-(MASCOT_WIDTH - PEEK) + stretch);
|
|
108
|
+
} else if (hideSide === "right") {
|
|
109
|
+
setPosX(cw - PEEK - stretch);
|
|
110
|
+
}
|
|
111
|
+
}, PEEK_INTERVAL);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const returnToView = () => {
|
|
115
|
+
if (!hideSide) return;
|
|
116
|
+
stopPeek();
|
|
117
|
+
const cw = getCw();
|
|
118
|
+
const cur = posX();
|
|
119
|
+
const targetX = hideSide === "left" ? 0 : Math.max(0, cw - MASCOT_WIDTH);
|
|
120
|
+
const step = targetX > cur ? 1 : -1;
|
|
121
|
+
|
|
122
|
+
returnTimer = setInterval(() => {
|
|
123
|
+
const now = posX();
|
|
124
|
+
if (Math.abs(now - targetX) <= 1) {
|
|
125
|
+
setPosX(targetX);
|
|
126
|
+
stopReturn();
|
|
127
|
+
hideSide = null;
|
|
128
|
+
renderers[currentName()].bounce();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
setPosX(now + step);
|
|
132
|
+
}, 16);
|
|
133
|
+
};
|
|
134
|
+
|
|
58
135
|
const setStateWithSwitch = (s: MascotState) => {
|
|
59
136
|
const cur = currentName();
|
|
60
137
|
renderers[cur].setState(s);
|
|
@@ -69,6 +146,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
69
146
|
const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
|
|
70
147
|
const statusType = payload?.properties?.status?.type;
|
|
71
148
|
if (statusType === "busy" || statusType === "retry") {
|
|
149
|
+
if (hideSide) returnToView();
|
|
72
150
|
renderers[currentName()].setState("busy");
|
|
73
151
|
} else {
|
|
74
152
|
setStateWithSwitch("idle");
|
|
@@ -110,7 +188,19 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
110
188
|
alignItems="center"
|
|
111
189
|
zIndex={100}
|
|
112
190
|
flexDirection="column"
|
|
191
|
+
ref={(node: any) => {
|
|
192
|
+
if (node) {
|
|
193
|
+
setContainerWidth(node.width || 0);
|
|
194
|
+
if (node.onSizeChange !== undefined) {
|
|
195
|
+
node.onSizeChange = () => {
|
|
196
|
+
setContainerWidth(node.width || 0);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
113
201
|
onMouseDown={(e: any) => {
|
|
202
|
+
if (hideSide) { returnToView(); return; }
|
|
203
|
+
|
|
114
204
|
const now = Date.now();
|
|
115
205
|
if (now - lastClickTime < 300) {
|
|
116
206
|
switchToNext();
|
|
@@ -133,8 +223,8 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
133
223
|
}}
|
|
134
224
|
onMouseDrag={(e: any) => {
|
|
135
225
|
if (e.modifiers?.alt && isDragging) {
|
|
136
|
-
setPosX(dragAnchorX + (e.x - dragStartX));
|
|
137
|
-
setPosY(dragAnchorY + (e.y - dragStartY));
|
|
226
|
+
setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
|
|
227
|
+
setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
|
|
138
228
|
e.preventDefault();
|
|
139
229
|
e.stopPropagation();
|
|
140
230
|
props.api.renderer.clearSelection();
|
|
@@ -144,12 +234,14 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
144
234
|
if (isDragging) {
|
|
145
235
|
isDragging = false;
|
|
146
236
|
renderers[currentName()].setDragging(false);
|
|
237
|
+
checkEdge();
|
|
147
238
|
}
|
|
148
239
|
}}
|
|
149
240
|
onMouseDragEnd={() => {
|
|
150
241
|
if (isDragging) {
|
|
151
242
|
isDragging = false;
|
|
152
243
|
renderers[currentName()].setDragging(false);
|
|
244
|
+
checkEdge();
|
|
153
245
|
}
|
|
154
246
|
}}
|
|
155
247
|
>
|
|
@@ -36,6 +36,9 @@ const DEFAULT_ANIM = {
|
|
|
36
36
|
|
|
37
37
|
const WALK_PATH = [1, 2, 3, 4, 3, 2, 1, 0, -1, -2, -3, -2, -1, 0];
|
|
38
38
|
|
|
39
|
+
const FLASH_COLORS = ["#FF006E", "#FFBE0B", "#8338EC", "#3A86FF", "#FB5607", "#06FFA5", "#FF4081", "#00E5FF"];
|
|
40
|
+
const DRAG_MSGS = ["ᶠᵃⁿᵍ!..", "ᵏᵃⁱ~..", "ᵇᵘᶠᵃⁿᵍ~..", "ʷᵒ~..", "ⁿⁱᵘ~..", "ᵃᵃ~.."];
|
|
41
|
+
|
|
39
42
|
function getFrameLines(pack: MascotPack, frameName: string): string[] {
|
|
40
43
|
const frames = pack.frames as Record<string, string[] | undefined>;
|
|
41
44
|
return frames[frameName] ?? frames["default"] ?? [];
|
|
@@ -72,6 +75,19 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
72
75
|
const [walkEnabled, setWalkEnabled] = createSignal(anim.walkEnabled ?? true);
|
|
73
76
|
const [dragging, setDraggingSignal] = createSignal(false);
|
|
74
77
|
const [celebrate, setCelebrate] = createSignal<{ text: string; count: number } | null>(null);
|
|
78
|
+
const [flashColor, setFlashColor] = createSignal<string | null>(null);
|
|
79
|
+
const [dragMsg, setDragMsg] = createSignal<string | null>(null);
|
|
80
|
+
|
|
81
|
+
let flashTimer: ReturnType<typeof setInterval> | null = null;
|
|
82
|
+
let dragMsgTimer: ReturnType<typeof setInterval> | null = null;
|
|
83
|
+
|
|
84
|
+
const stopFlash = () => {
|
|
85
|
+
if (flashTimer) { clearInterval(flashTimer); flashTimer = null; }
|
|
86
|
+
};
|
|
87
|
+
const stopDragMsg = () => {
|
|
88
|
+
if (dragMsgTimer) { clearInterval(dragMsgTimer); dragMsgTimer = null; }
|
|
89
|
+
setDragMsg(null);
|
|
90
|
+
};
|
|
75
91
|
|
|
76
92
|
let idleSleepTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
77
93
|
|
|
@@ -230,6 +246,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
230
246
|
if (idleSleepTimeout) clearTimeout(idleSleepTimeout);
|
|
231
247
|
if (walkInterval) clearInterval(walkInterval);
|
|
232
248
|
for (const t of effectTimers) clearInterval(t);
|
|
249
|
+
stopFlash();
|
|
250
|
+
stopDragMsg();
|
|
233
251
|
});
|
|
234
252
|
|
|
235
253
|
// ─── Render ───
|
|
@@ -241,6 +259,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
241
259
|
currentState();
|
|
242
260
|
dragging();
|
|
243
261
|
celebrate();
|
|
262
|
+
flashColor();
|
|
263
|
+
dragMsg();
|
|
244
264
|
|
|
245
265
|
for (const [, [get]] of extraSignals) {
|
|
246
266
|
get();
|
|
@@ -276,11 +296,14 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
276
296
|
const top = jumpOffset();
|
|
277
297
|
const left = offset > 0 ? offset : 0;
|
|
278
298
|
const cel = celebrate();
|
|
299
|
+
const dm = dragMsg();
|
|
300
|
+
const color = flashColor() ?? fg;
|
|
279
301
|
|
|
280
302
|
return (
|
|
281
303
|
<box flexDirection="column" left={left} top={top}>
|
|
282
|
-
{cel ? <text fg={
|
|
283
|
-
{
|
|
304
|
+
{cel ? <text fg={color}>{cel.text}</text> : null}
|
|
305
|
+
{dm ? <text fg="#FF4081">{dm}</text> : null}
|
|
306
|
+
{renderLines(lines, color)}
|
|
284
307
|
</box>
|
|
285
308
|
);
|
|
286
309
|
};
|
|
@@ -297,6 +320,15 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
297
320
|
walkTimeout = scheduleNextWalk();
|
|
298
321
|
jumpTimeout = scheduleNextJump();
|
|
299
322
|
}
|
|
323
|
+
|
|
324
|
+
if (s === "thinking" || s === "busy") {
|
|
325
|
+
stopFlash();
|
|
326
|
+
flashTimer = setInterval(() => {
|
|
327
|
+
setFlashColor(FLASH_COLORS[Math.floor(Math.random() * FLASH_COLORS.length)]);
|
|
328
|
+
}, 120);
|
|
329
|
+
} else {
|
|
330
|
+
stopFlash();
|
|
331
|
+
}
|
|
300
332
|
};
|
|
301
333
|
|
|
302
334
|
const toggleWalk = () => {
|
|
@@ -312,13 +344,18 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
312
344
|
const setDragging = (v: boolean) => {
|
|
313
345
|
setDraggingSignal(v);
|
|
314
346
|
if (v) {
|
|
315
|
-
// 睡着时被拖拽 → 惊醒到 idle,切回 default 帧后手臂 ┃███┃ 才能被扇手渲染匹配
|
|
316
347
|
if (currentState() === "sleeping") {
|
|
317
348
|
setState("idle");
|
|
318
349
|
}
|
|
319
350
|
setJumpOffset(-1);
|
|
351
|
+
stopDragMsg();
|
|
352
|
+
setDragMsg(DRAG_MSGS[Math.floor(Math.random() * DRAG_MSGS.length)]);
|
|
353
|
+
dragMsgTimer = setInterval(() => {
|
|
354
|
+
setDragMsg(DRAG_MSGS[Math.floor(Math.random() * DRAG_MSGS.length)]);
|
|
355
|
+
}, 800);
|
|
320
356
|
} else {
|
|
321
357
|
setJumpOffset(0);
|
|
358
|
+
stopDragMsg();
|
|
322
359
|
}
|
|
323
360
|
};
|
|
324
361
|
|