@mingxy/opencode-mascot 0.2.4 → 0.2.6
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 +1 -1
- package/src/builtins/baozi/index.ts +0 -7
- package/src/builtins/yueer/index.ts +0 -7
- package/src/components/home-mascot.tsx +50 -13
- package/src/components/sidebar-mascot.tsx +134 -28
- package/src/core/ascii-renderer.tsx +1 -1
- package/src/core/types.ts +0 -26
- package/src/components/use-draggable-mascot.ts +0 -183
package/package.json
CHANGED
|
@@ -200,12 +200,5 @@ export const yueerPack: MascotPack = {
|
|
|
200
200
|
idleTimeout: 90000,
|
|
201
201
|
},
|
|
202
202
|
|
|
203
|
-
sidebar: {
|
|
204
|
-
greetings: ["师尊,月儿在此候命~"],
|
|
205
|
-
busyPhrases: ["铸造法器中..."],
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
bubbleTexts: ["嗯...", "让我想想...", "等等...", "本帝在算..."],
|
|
209
|
-
|
|
210
203
|
effects: yueerEffects,
|
|
211
204
|
};
|
|
@@ -5,7 +5,6 @@ import type { JSX } from "@opentui/solid";
|
|
|
5
5
|
import type { MascotPack } from "../core/types";
|
|
6
6
|
import { createAnimatedRenderer } from "../core/ascii-renderer";
|
|
7
7
|
import { onCelebrate } from "../core/celebration-bus";
|
|
8
|
-
import { useDraggableMascot } from "./use-draggable-mascot";
|
|
9
8
|
|
|
10
9
|
interface HomeMascotProps {
|
|
11
10
|
mascots: Record<string, MascotPack>;
|
|
@@ -21,6 +20,14 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
21
20
|
const initialName = names[Math.floor(Math.random() * names.length)];
|
|
22
21
|
|
|
23
22
|
const [currentName, setCurrentName] = createSignal(initialName);
|
|
23
|
+
const [posX, setPosX] = createSignal(0);
|
|
24
|
+
const [posY, setPosY] = createSignal(0);
|
|
25
|
+
let dragStartX = 0;
|
|
26
|
+
let dragStartY = 0;
|
|
27
|
+
let dragAnchorX = 0;
|
|
28
|
+
let dragAnchorY = 0;
|
|
29
|
+
let lastClickTime = 0;
|
|
30
|
+
let isDragging = false;
|
|
24
31
|
|
|
25
32
|
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
26
33
|
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
@@ -33,17 +40,6 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
33
40
|
setCurrentName(names[(idx + 1) % names.length]);
|
|
34
41
|
};
|
|
35
42
|
|
|
36
|
-
const { posX, posY, mouseProps } = useDraggableMascot({
|
|
37
|
-
initialX: 0,
|
|
38
|
-
initialY: 0,
|
|
39
|
-
mascotWidth: 10,
|
|
40
|
-
mascotHeight: 5,
|
|
41
|
-
onSwitch: switchToNext,
|
|
42
|
-
clearSelection: () => props.api.renderer.clearSelection(),
|
|
43
|
-
setDragging: (v) => renderers[currentName()].setDragging(v),
|
|
44
|
-
enableEdge: false,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
43
|
onCelebrate((newVersion) => {
|
|
48
44
|
renderers[currentName()].celebrateUpdate(newVersion);
|
|
49
45
|
});
|
|
@@ -55,7 +51,48 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
55
51
|
alignItems="center"
|
|
56
52
|
zIndex={100}
|
|
57
53
|
flexDirection="column"
|
|
58
|
-
{
|
|
54
|
+
onMouseDown={(e: any) => {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (now - lastClickTime < 300) {
|
|
57
|
+
switchToNext();
|
|
58
|
+
lastClickTime = 0;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
lastClickTime = now;
|
|
62
|
+
|
|
63
|
+
if (e.modifiers?.alt) {
|
|
64
|
+
dragStartX = e.x;
|
|
65
|
+
dragStartY = e.y;
|
|
66
|
+
dragAnchorX = posX();
|
|
67
|
+
dragAnchorY = posY();
|
|
68
|
+
isDragging = true;
|
|
69
|
+
renderers[currentName()].setDragging(true);
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
props.api.renderer.clearSelection();
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
onMouseDrag={(e: any) => {
|
|
76
|
+
if (e.modifiers?.alt && isDragging) {
|
|
77
|
+
setPosX(dragAnchorX + (e.x - dragStartX));
|
|
78
|
+
setPosY(dragAnchorY + (e.y - dragStartY));
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
props.api.renderer.clearSelection();
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
onMouseUp={() => {
|
|
85
|
+
if (isDragging) {
|
|
86
|
+
isDragging = false;
|
|
87
|
+
renderers[currentName()].setDragging(false);
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
onMouseDragEnd={() => {
|
|
91
|
+
if (isDragging) {
|
|
92
|
+
isDragging = false;
|
|
93
|
+
renderers[currentName()].setDragging(false);
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
59
96
|
>
|
|
60
97
|
{renderers[currentName()]?.element() ?? null}
|
|
61
98
|
</box>
|
|
@@ -1,15 +1,13 @@
|
|
|
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
|
-
import type { MascotPack, MascotState
|
|
5
|
+
import type { MascotPack, MascotState } from "../core/types";
|
|
6
6
|
import { createAnimatedRenderer } from "../core/ascii-renderer";
|
|
7
7
|
import { onCelebrate } from "../core/celebration-bus";
|
|
8
|
-
import { useDraggableMascot } from "./use-draggable-mascot";
|
|
9
8
|
|
|
10
9
|
interface SidebarMascotProps {
|
|
11
10
|
mascots: Record<string, MascotPack>;
|
|
12
|
-
switchConfig?: SwitchConfig;
|
|
13
11
|
initialMascot?: string;
|
|
14
12
|
api: {
|
|
15
13
|
event: {
|
|
@@ -31,6 +29,12 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
|
|
|
31
29
|
|
|
32
30
|
const MASCOT_WIDTH = 10;
|
|
33
31
|
const MASCOT_HEIGHT = 5;
|
|
32
|
+
const PEEK = 2;
|
|
33
|
+
const PEEK_INTERVAL = 1200;
|
|
34
|
+
|
|
35
|
+
function termWidth(): number {
|
|
36
|
+
return (typeof process !== "undefined" && process.stdout?.columns) || 80;
|
|
37
|
+
}
|
|
34
38
|
|
|
35
39
|
export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
36
40
|
const names = Object.keys(props.mascots);
|
|
@@ -40,41 +44,101 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
40
44
|
: names[Math.floor(Math.random() * names.length)];
|
|
41
45
|
|
|
42
46
|
const [currentName, setCurrentName] = createSignal(initialName);
|
|
47
|
+
const [posX, setPosX] = createSignal(20);
|
|
48
|
+
const [posY, setPosY] = createSignal(2);
|
|
49
|
+
let dragStartX = 0;
|
|
50
|
+
let dragStartY = 0;
|
|
51
|
+
let dragAnchorX = 0;
|
|
52
|
+
let dragAnchorY = 0;
|
|
53
|
+
let lastClickTime = 0;
|
|
54
|
+
let isDragging = false;
|
|
55
|
+
let hideSide: "left" | "right" | null = null;
|
|
56
|
+
let peekTimer: ReturnType<typeof setInterval> | null = null;
|
|
57
|
+
let returnTimer: ReturnType<typeof setInterval> | null = null;
|
|
43
58
|
|
|
44
59
|
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
45
60
|
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
46
61
|
renderers[name] = createAnimatedRenderer(pack);
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
const stopPeek = () => {
|
|
65
|
+
if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
|
|
66
|
+
};
|
|
67
|
+
const stopReturn = () => {
|
|
68
|
+
if (returnTimer) { clearInterval(returnTimer); returnTimer = null; }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
onCleanup(() => { stopPeek(); stopReturn(); });
|
|
72
|
+
|
|
49
73
|
const switchToNext = () => {
|
|
50
74
|
const cur = currentName();
|
|
51
75
|
const idx = names.indexOf(cur);
|
|
52
76
|
setCurrentName(names[(idx + 1) % names.length]);
|
|
53
77
|
};
|
|
54
78
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
const startPeek = () => {
|
|
80
|
+
stopPeek();
|
|
81
|
+
let phase = false;
|
|
82
|
+
peekTimer = setInterval(() => {
|
|
83
|
+
phase = !phase;
|
|
84
|
+
const stretch = phase ? PEEK : 0;
|
|
85
|
+
const w = termWidth();
|
|
86
|
+
if (hideSide === "left") {
|
|
87
|
+
setPosX(-(MASCOT_WIDTH - PEEK) + stretch);
|
|
88
|
+
} else if (hideSide === "right") {
|
|
89
|
+
setPosX(w - PEEK - stretch);
|
|
90
|
+
}
|
|
91
|
+
}, PEEK_INTERVAL);
|
|
92
|
+
};
|
|
65
93
|
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
94
|
+
const returnToView = () => {
|
|
95
|
+
if (!hideSide) return;
|
|
96
|
+
stopPeek();
|
|
97
|
+
const w = termWidth();
|
|
98
|
+
const cur = posX();
|
|
99
|
+
const targetX = hideSide === "left" ? 0 : Math.max(0, w - MASCOT_WIDTH);
|
|
100
|
+
const step = targetX > cur ? 2 : -2;
|
|
101
|
+
returnTimer = setInterval(() => {
|
|
102
|
+
const now = posX();
|
|
103
|
+
if (Math.abs(now - targetX) <= 2) {
|
|
104
|
+
setPosX(targetX);
|
|
105
|
+
stopReturn();
|
|
106
|
+
hideSide = null;
|
|
107
|
+
renderers[currentName()].bounce();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setPosX(now + step);
|
|
111
|
+
}, 16);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const checkEdge = () => {
|
|
115
|
+
const w = termWidth();
|
|
116
|
+
const x = posX();
|
|
117
|
+
if (x <= -(MASCOT_WIDTH - PEEK) + 1) {
|
|
118
|
+
hideSide = "left";
|
|
119
|
+
startPeek();
|
|
120
|
+
} else if (x >= w - PEEK - 1) {
|
|
121
|
+
hideSide = "right";
|
|
122
|
+
startPeek();
|
|
69
123
|
}
|
|
70
124
|
};
|
|
71
125
|
|
|
126
|
+
const clampX = (rawX: number): number => {
|
|
127
|
+
const w = termWidth();
|
|
128
|
+
const minX = -(MASCOT_WIDTH - PEEK);
|
|
129
|
+
const maxX = w - PEEK;
|
|
130
|
+
return Math.max(minX, Math.min(rawX, maxX));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const clampY = (rawY: number): number => {
|
|
134
|
+
const h = (typeof process !== "undefined" && process.stdout?.rows) || 24;
|
|
135
|
+
return Math.max(0, Math.min(rawY, h - MASCOT_HEIGHT));
|
|
136
|
+
};
|
|
137
|
+
|
|
72
138
|
const setStateWithSwitch = (s: MascotState) => {
|
|
73
139
|
const cur = currentName();
|
|
74
140
|
renderers[cur].setState(s);
|
|
75
|
-
|
|
76
|
-
const stateMap = props.switchConfig?.onState ?? DEFAULT_STATE_MAP;
|
|
77
|
-
const target = stateMap[s];
|
|
141
|
+
const target = DEFAULT_STATE_MAP[s];
|
|
78
142
|
if (target && target !== cur && props.mascots[target]) {
|
|
79
143
|
setCurrentName(target);
|
|
80
144
|
renderers[target].setState(s);
|
|
@@ -84,9 +148,8 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
84
148
|
props.api.event.on("session.status", (data: unknown) => {
|
|
85
149
|
const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
|
|
86
150
|
const statusType = payload?.properties?.status?.type;
|
|
87
|
-
|
|
88
151
|
if (statusType === "busy" || statusType === "retry") {
|
|
89
|
-
returnToView();
|
|
152
|
+
if (hideSide) returnToView();
|
|
90
153
|
renderers[currentName()].setState("busy");
|
|
91
154
|
} else {
|
|
92
155
|
setStateWithSwitch("idle");
|
|
@@ -101,12 +164,10 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
101
164
|
props.api.event.on("mascot.switch", (data: unknown) => {
|
|
102
165
|
const target = data as { name?: string } | null;
|
|
103
166
|
if (target?.name) {
|
|
104
|
-
|
|
167
|
+
const name = target.name;
|
|
168
|
+
if (props.mascots[name] && name !== currentName()) setCurrentName(name);
|
|
105
169
|
} else {
|
|
106
|
-
|
|
107
|
-
if (others.length > 0) {
|
|
108
|
-
switchTo(others[Math.floor(Math.random() * others.length)]);
|
|
109
|
-
}
|
|
170
|
+
switchToNext();
|
|
110
171
|
}
|
|
111
172
|
});
|
|
112
173
|
|
|
@@ -126,7 +187,52 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
126
187
|
alignItems="center"
|
|
127
188
|
zIndex={100}
|
|
128
189
|
flexDirection="column"
|
|
129
|
-
{
|
|
190
|
+
onMouseDown={(e: any) => {
|
|
191
|
+
if (hideSide) { returnToView(); return; }
|
|
192
|
+
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
if (now - lastClickTime < 300) {
|
|
195
|
+
switchToNext();
|
|
196
|
+
lastClickTime = 0;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
lastClickTime = now;
|
|
200
|
+
|
|
201
|
+
if (e.modifiers?.alt) {
|
|
202
|
+
dragStartX = e.x;
|
|
203
|
+
dragStartY = e.y;
|
|
204
|
+
dragAnchorX = posX();
|
|
205
|
+
dragAnchorY = posY();
|
|
206
|
+
isDragging = true;
|
|
207
|
+
renderers[currentName()].setDragging(true);
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
props.api.renderer.clearSelection();
|
|
211
|
+
}
|
|
212
|
+
}}
|
|
213
|
+
onMouseDrag={(e: any) => {
|
|
214
|
+
if (e.modifiers?.alt && isDragging) {
|
|
215
|
+
setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
|
|
216
|
+
setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
props.api.renderer.clearSelection();
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
onMouseUp={() => {
|
|
223
|
+
if (isDragging) {
|
|
224
|
+
isDragging = false;
|
|
225
|
+
renderers[currentName()].setDragging(false);
|
|
226
|
+
checkEdge();
|
|
227
|
+
}
|
|
228
|
+
}}
|
|
229
|
+
onMouseDragEnd={() => {
|
|
230
|
+
if (isDragging) {
|
|
231
|
+
isDragging = false;
|
|
232
|
+
renderers[currentName()].setDragging(false);
|
|
233
|
+
checkEdge();
|
|
234
|
+
}
|
|
235
|
+
}}
|
|
130
236
|
>
|
|
131
237
|
{renderers[currentName()]?.element() ?? null}
|
|
132
238
|
</box>
|
|
@@ -314,7 +314,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
314
314
|
|
|
315
315
|
// 连续跳跃 + 吐火星文泡泡庆祝更新成功
|
|
316
316
|
const celebrateUpdate = (newVersion: string) => {
|
|
317
|
-
const bubbles =
|
|
317
|
+
const bubbles = ["ᵘᵖ~", "ⁿᵉʷ!", "ʸᵉ~", "ᵍᵒ~", "ᵒᵏ~"];
|
|
318
318
|
setState("happy");
|
|
319
319
|
setFrameOverride("happy");
|
|
320
320
|
|
package/src/core/types.ts
CHANGED
|
@@ -25,14 +25,6 @@ export interface AnimationConfig {
|
|
|
25
25
|
jumpMaxDelay?: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
* Sidebar display configuration.
|
|
30
|
-
*/
|
|
31
|
-
export interface SidebarConfig {
|
|
32
|
-
greetings?: string[];
|
|
33
|
-
busyPhrases?: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
// ─── Effect system types ───
|
|
37
29
|
|
|
38
30
|
/**
|
|
@@ -130,25 +122,7 @@ export interface MascotPack {
|
|
|
130
122
|
};
|
|
131
123
|
|
|
132
124
|
animations?: AnimationConfig;
|
|
133
|
-
sidebar?: SidebarConfig;
|
|
134
125
|
|
|
135
126
|
/** Mascot-specific animation effects (timers + render) */
|
|
136
127
|
effects?: MascotEffects;
|
|
137
|
-
|
|
138
|
-
/** Thinking bubble phrases, shown when mascot is in busy/thinking state */
|
|
139
|
-
bubbleTexts?: string[];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface SwitchConfig {
|
|
143
|
-
onState?: Partial<Record<MascotState, string>>;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* User-facing configuration merged from opencode config file.
|
|
148
|
-
*/
|
|
149
|
-
export interface MascotConfig {
|
|
150
|
-
mascot: string;
|
|
151
|
-
animations: boolean;
|
|
152
|
-
idleSleep: boolean;
|
|
153
|
-
switchConfig?: SwitchConfig;
|
|
154
128
|
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import { createSignal, onCleanup, type Accessor } from "solid-js";
|
|
4
|
-
import { useTerminalDimensions } from "@opentui/solid";
|
|
5
|
-
|
|
6
|
-
type DragState = "normal" | "edge_hidden" | "returning";
|
|
7
|
-
type HideSide = "left" | "right" | null;
|
|
8
|
-
|
|
9
|
-
export interface MouseProps {
|
|
10
|
-
onMouseDown: (e: any) => void;
|
|
11
|
-
onMouseDrag: (e: any) => void;
|
|
12
|
-
onMouseUp: (e: any) => void;
|
|
13
|
-
onMouseDragEnd: (e: any) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface UseDraggableOpts {
|
|
17
|
-
initialX: number;
|
|
18
|
-
initialY: number;
|
|
19
|
-
mascotWidth: number;
|
|
20
|
-
mascotHeight: number;
|
|
21
|
-
onSwitch: () => void;
|
|
22
|
-
clearSelection: () => void;
|
|
23
|
-
setDragging: (v: boolean) => void;
|
|
24
|
-
onReturnComplete?: () => void;
|
|
25
|
-
peekVisible?: number;
|
|
26
|
-
enableEdge?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function useDraggableMascot(opts: UseDraggableOpts): {
|
|
30
|
-
posX: Accessor<number>;
|
|
31
|
-
posY: Accessor<number>;
|
|
32
|
-
mouseProps: MouseProps;
|
|
33
|
-
returnToView: () => void;
|
|
34
|
-
isHidden: () => boolean;
|
|
35
|
-
} {
|
|
36
|
-
const peek = opts.peekVisible ?? 2;
|
|
37
|
-
const enableEdge = opts.enableEdge ?? true;
|
|
38
|
-
|
|
39
|
-
const [posX, setPosX] = createSignal(opts.initialX);
|
|
40
|
-
const [posY, setPosY] = createSignal(opts.initialY);
|
|
41
|
-
const dims = useTerminalDimensions();
|
|
42
|
-
|
|
43
|
-
let dragStartX = 0;
|
|
44
|
-
let dragStartY = 0;
|
|
45
|
-
let dragAnchorX = 0;
|
|
46
|
-
let dragAnchorY = 0;
|
|
47
|
-
let lastClickTime = 0;
|
|
48
|
-
let isDragging = false;
|
|
49
|
-
let state: DragState = "normal";
|
|
50
|
-
let hideSide: HideSide = null;
|
|
51
|
-
let peekTimer: ReturnType<typeof setInterval> | null = null;
|
|
52
|
-
let returnTimer: ReturnType<typeof setInterval> | null = null;
|
|
53
|
-
|
|
54
|
-
const stopPeek = () => {
|
|
55
|
-
if (peekTimer) { clearInterval(peekTimer); peekTimer = null; }
|
|
56
|
-
};
|
|
57
|
-
const stopReturn = () => {
|
|
58
|
-
if (returnTimer) { clearInterval(returnTimer); returnTimer = null; }
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
onCleanup(() => { stopPeek(); stopReturn(); });
|
|
62
|
-
|
|
63
|
-
const clampX = (rawX: number): number => {
|
|
64
|
-
if (!enableEdge) return rawX;
|
|
65
|
-
const { width } = dims();
|
|
66
|
-
const minX = -(opts.mascotWidth - peek);
|
|
67
|
-
const maxX = width - peek;
|
|
68
|
-
return Math.max(minX, Math.min(rawX, maxX));
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const clampY = (rawY: number): number => {
|
|
72
|
-
if (!enableEdge) return rawY;
|
|
73
|
-
const { height } = dims();
|
|
74
|
-
return Math.max(0, Math.min(rawY, height - opts.mascotHeight));
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const startPeek = () => {
|
|
78
|
-
stopPeek();
|
|
79
|
-
state = "edge_hidden";
|
|
80
|
-
let phase = false;
|
|
81
|
-
peekTimer = setInterval(() => {
|
|
82
|
-
phase = !phase;
|
|
83
|
-
const stretch = phase ? 2 : 0;
|
|
84
|
-
const { width } = dims();
|
|
85
|
-
if (hideSide === "left") {
|
|
86
|
-
setPosX(-(opts.mascotWidth - peek) + stretch);
|
|
87
|
-
} else if (hideSide === "right") {
|
|
88
|
-
setPosX(width - peek - stretch);
|
|
89
|
-
}
|
|
90
|
-
}, 1200);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const returnToView = () => {
|
|
94
|
-
if (state !== "edge_hidden") return;
|
|
95
|
-
stopPeek();
|
|
96
|
-
state = "returning";
|
|
97
|
-
const { width } = dims();
|
|
98
|
-
const cur = posX();
|
|
99
|
-
const targetX = hideSide === "left" ? 0 : Math.max(0, width - opts.mascotWidth);
|
|
100
|
-
const step = targetX > cur ? 2 : -2;
|
|
101
|
-
|
|
102
|
-
returnTimer = setInterval(() => {
|
|
103
|
-
const now = posX();
|
|
104
|
-
if (Math.abs(now - targetX) <= 2) {
|
|
105
|
-
setPosX(targetX);
|
|
106
|
-
stopReturn();
|
|
107
|
-
state = "normal";
|
|
108
|
-
hideSide = null;
|
|
109
|
-
opts.onReturnComplete?.();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
setPosX(now + step);
|
|
113
|
-
}, 16);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const checkEdgeOnRelease = () => {
|
|
117
|
-
if (!enableEdge) return;
|
|
118
|
-
const { width } = dims();
|
|
119
|
-
const x = posX();
|
|
120
|
-
if (x <= -(opts.mascotWidth - peek) + 1) {
|
|
121
|
-
hideSide = "left";
|
|
122
|
-
startPeek();
|
|
123
|
-
} else if (x >= width - peek - 1) {
|
|
124
|
-
hideSide = "right";
|
|
125
|
-
startPeek();
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const mouseProps: MouseProps = {
|
|
130
|
-
onMouseDown(e: any) {
|
|
131
|
-
if (state === "edge_hidden") {
|
|
132
|
-
returnToView();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (state === "returning") return;
|
|
136
|
-
|
|
137
|
-
const now = Date.now();
|
|
138
|
-
if (now - lastClickTime < 300) {
|
|
139
|
-
opts.onSwitch();
|
|
140
|
-
lastClickTime = 0;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
lastClickTime = now;
|
|
144
|
-
|
|
145
|
-
if (e.modifiers?.alt) {
|
|
146
|
-
dragStartX = e.x;
|
|
147
|
-
dragStartY = e.y;
|
|
148
|
-
dragAnchorX = posX();
|
|
149
|
-
dragAnchorY = posY();
|
|
150
|
-
isDragging = true;
|
|
151
|
-
opts.setDragging(true);
|
|
152
|
-
e.preventDefault();
|
|
153
|
-
e.stopPropagation();
|
|
154
|
-
opts.clearSelection();
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
onMouseDrag(e: any) {
|
|
158
|
-
if (e.modifiers?.alt && isDragging) {
|
|
159
|
-
setPosX(clampX(dragAnchorX + (e.x - dragStartX)));
|
|
160
|
-
setPosY(clampY(dragAnchorY + (e.y - dragStartY)));
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
e.stopPropagation();
|
|
163
|
-
opts.clearSelection();
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
onMouseUp() {
|
|
167
|
-
if (isDragging) {
|
|
168
|
-
isDragging = false;
|
|
169
|
-
opts.setDragging(false);
|
|
170
|
-
checkEdgeOnRelease();
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
onMouseDragEnd() {
|
|
174
|
-
if (isDragging) {
|
|
175
|
-
isDragging = false;
|
|
176
|
-
opts.setDragging(false);
|
|
177
|
-
checkEdgeOnRelease();
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
return { posX, posY, mouseProps, returnToView, isHidden: () => state === "edge_hidden" };
|
|
183
|
-
}
|