@mingxy/opencode-mascot 0.2.2 → 0.2.4
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
|
@@ -5,6 +5,7 @@ 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";
|
|
8
9
|
|
|
9
10
|
interface HomeMascotProps {
|
|
10
11
|
mascots: Record<string, MascotPack>;
|
|
@@ -20,26 +21,29 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
20
21
|
const initialName = names[Math.floor(Math.random() * names.length)];
|
|
21
22
|
|
|
22
23
|
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;
|
|
31
24
|
|
|
32
25
|
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
33
26
|
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
34
27
|
renderers[name] = createAnimatedRenderer(pack);
|
|
35
28
|
}
|
|
36
29
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const switchToNext = () => {
|
|
31
|
+
const cur = currentName();
|
|
32
|
+
const idx = names.indexOf(cur);
|
|
33
|
+
setCurrentName(names[(idx + 1) % names.length]);
|
|
41
34
|
};
|
|
42
35
|
|
|
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
|
+
|
|
43
47
|
onCelebrate((newVersion) => {
|
|
44
48
|
renderers[currentName()].celebrateUpdate(newVersion);
|
|
45
49
|
});
|
|
@@ -51,51 +55,7 @@ export function HomeMascot(props: HomeMascotProps): JSX.Element {
|
|
|
51
55
|
alignItems="center"
|
|
52
56
|
zIndex={100}
|
|
53
57
|
flexDirection="column"
|
|
54
|
-
|
|
55
|
-
const now = Date.now();
|
|
56
|
-
if (now - lastClickTime < 300) {
|
|
57
|
-
const cur = currentName();
|
|
58
|
-
const idx = names.indexOf(cur);
|
|
59
|
-
const next = names[(idx + 1) % names.length];
|
|
60
|
-
switchTo(next);
|
|
61
|
-
lastClickTime = 0;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
lastClickTime = now;
|
|
65
|
-
|
|
66
|
-
if (e.modifiers?.alt) {
|
|
67
|
-
dragStartX = e.x;
|
|
68
|
-
dragStartY = e.y;
|
|
69
|
-
dragAnchorX = posX();
|
|
70
|
-
dragAnchorY = posY();
|
|
71
|
-
isDragging = true;
|
|
72
|
-
renderers[currentName()].setDragging(true);
|
|
73
|
-
e.preventDefault();
|
|
74
|
-
e.stopPropagation();
|
|
75
|
-
props.api.renderer.clearSelection();
|
|
76
|
-
}
|
|
77
|
-
}}
|
|
78
|
-
onMouseDrag={(e: any) => {
|
|
79
|
-
if (e.modifiers?.alt && isDragging) {
|
|
80
|
-
setPosX(dragAnchorX + (e.x - dragStartX));
|
|
81
|
-
setPosY(dragAnchorY + (e.y - dragStartY));
|
|
82
|
-
e.preventDefault();
|
|
83
|
-
e.stopPropagation();
|
|
84
|
-
props.api.renderer.clearSelection();
|
|
85
|
-
}
|
|
86
|
-
}}
|
|
87
|
-
onMouseUp={() => {
|
|
88
|
-
if (isDragging) {
|
|
89
|
-
isDragging = false;
|
|
90
|
-
renderers[currentName()].setDragging(false);
|
|
91
|
-
}
|
|
92
|
-
}}
|
|
93
|
-
onMouseDragEnd={() => {
|
|
94
|
-
if (isDragging) {
|
|
95
|
-
isDragging = false;
|
|
96
|
-
renderers[currentName()].setDragging(false);
|
|
97
|
-
}
|
|
98
|
-
}}
|
|
58
|
+
{...mouseProps}
|
|
99
59
|
>
|
|
100
60
|
{renderers[currentName()]?.element() ?? null}
|
|
101
61
|
</box>
|
|
@@ -5,6 +5,7 @@ import type { JSX } from "@opentui/solid";
|
|
|
5
5
|
import type { MascotPack, MascotState, SwitchConfig } 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";
|
|
8
9
|
|
|
9
10
|
interface SidebarMascotProps {
|
|
10
11
|
mascots: Record<string, MascotPack>;
|
|
@@ -28,6 +29,9 @@ const DEFAULT_STATE_MAP: Partial<Record<MascotState, string>> = {
|
|
|
28
29
|
sleeping: "baozi",
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
const MASCOT_WIDTH = 10;
|
|
33
|
+
const MASCOT_HEIGHT = 5;
|
|
34
|
+
|
|
31
35
|
export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
32
36
|
const names = Object.keys(props.mascots);
|
|
33
37
|
const initialName =
|
|
@@ -36,20 +40,29 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
36
40
|
: names[Math.floor(Math.random() * names.length)];
|
|
37
41
|
|
|
38
42
|
const [currentName, setCurrentName] = createSignal(initialName);
|
|
39
|
-
const [posX, setPosX] = createSignal(20);
|
|
40
|
-
const [posY, setPosY] = createSignal(2);
|
|
41
|
-
let dragStartX = 0;
|
|
42
|
-
let dragStartY = 0;
|
|
43
|
-
let dragAnchorX = 0;
|
|
44
|
-
let dragAnchorY = 0;
|
|
45
|
-
let lastClickTime = 0;
|
|
46
|
-
let isDragging = false;
|
|
47
43
|
|
|
48
44
|
const renderers: Record<string, ReturnType<typeof createAnimatedRenderer>> = {};
|
|
49
45
|
for (const [name, pack] of Object.entries(props.mascots)) {
|
|
50
46
|
renderers[name] = createAnimatedRenderer(pack);
|
|
51
47
|
}
|
|
52
48
|
|
|
49
|
+
const switchToNext = () => {
|
|
50
|
+
const cur = currentName();
|
|
51
|
+
const idx = names.indexOf(cur);
|
|
52
|
+
setCurrentName(names[(idx + 1) % names.length]);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const { posX, posY, mouseProps, returnToView } = useDraggableMascot({
|
|
56
|
+
initialX: 20,
|
|
57
|
+
initialY: 2,
|
|
58
|
+
mascotWidth: MASCOT_WIDTH,
|
|
59
|
+
mascotHeight: MASCOT_HEIGHT,
|
|
60
|
+
onSwitch: switchToNext,
|
|
61
|
+
clearSelection: () => props.api.renderer.clearSelection(),
|
|
62
|
+
setDragging: (v) => renderers[currentName()].setDragging(v),
|
|
63
|
+
onReturnComplete: () => renderers[currentName()].bounce(),
|
|
64
|
+
});
|
|
65
|
+
|
|
53
66
|
const switchTo = (name: string) => {
|
|
54
67
|
if (props.mascots[name] && name !== currentName()) {
|
|
55
68
|
setCurrentName(name);
|
|
@@ -69,11 +82,11 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
69
82
|
};
|
|
70
83
|
|
|
71
84
|
props.api.event.on("session.status", (data: unknown) => {
|
|
72
|
-
// Plugin receives: { id, type, properties: { sessionID, status: { type } } }
|
|
73
85
|
const payload = data as { type?: string; properties?: { sessionID?: string; status?: { type?: string } } } | null;
|
|
74
86
|
const statusType = payload?.properties?.status?.type;
|
|
75
87
|
|
|
76
88
|
if (statusType === "busy" || statusType === "retry") {
|
|
89
|
+
returnToView();
|
|
77
90
|
renderers[currentName()].setState("busy");
|
|
78
91
|
} else {
|
|
79
92
|
setStateWithSwitch("idle");
|
|
@@ -113,52 +126,7 @@ export function SidebarMascot(props: SidebarMascotProps): JSX.Element {
|
|
|
113
126
|
alignItems="center"
|
|
114
127
|
zIndex={100}
|
|
115
128
|
flexDirection="column"
|
|
116
|
-
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
if (now - lastClickTime < 300) {
|
|
119
|
-
const cur = currentName();
|
|
120
|
-
const idx = names.indexOf(cur);
|
|
121
|
-
const next = names[(idx + 1) % names.length];
|
|
122
|
-
switchTo(next);
|
|
123
|
-
lastClickTime = 0;
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
lastClickTime = now;
|
|
127
|
-
|
|
128
|
-
if (e.modifiers?.alt) {
|
|
129
|
-
dragStartX = e.x;
|
|
130
|
-
dragStartY = e.y;
|
|
131
|
-
dragAnchorX = posX();
|
|
132
|
-
dragAnchorY = posY();
|
|
133
|
-
isDragging = true;
|
|
134
|
-
renderers[currentName()].setDragging(true);
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
e.stopPropagation();
|
|
137
|
-
props.api.renderer.clearSelection();
|
|
138
|
-
}
|
|
139
|
-
}}
|
|
140
|
-
onMouseDrag={(e: any) => {
|
|
141
|
-
if (e.modifiers?.alt && isDragging) {
|
|
142
|
-
setPosX(dragAnchorX + (e.x - dragStartX));
|
|
143
|
-
setPosY(dragAnchorY + (e.y - dragStartY));
|
|
144
|
-
e.preventDefault();
|
|
145
|
-
e.stopPropagation();
|
|
146
|
-
props.api.renderer.clearSelection();
|
|
147
|
-
}
|
|
148
|
-
}}
|
|
149
|
-
onMouseUp={() => {
|
|
150
|
-
if (isDragging) {
|
|
151
|
-
isDragging = false;
|
|
152
|
-
renderers[currentName()].setDragging(false);
|
|
153
|
-
}
|
|
154
|
-
}}
|
|
155
|
-
onMouseDragEnd={() => {
|
|
156
|
-
if (isDragging) {
|
|
157
|
-
isDragging = false;
|
|
158
|
-
renderers[currentName()].setDragging(false);
|
|
159
|
-
}
|
|
160
|
-
}}
|
|
161
|
-
|
|
129
|
+
{...mouseProps}
|
|
162
130
|
>
|
|
163
131
|
{renderers[currentName()]?.element() ?? null}
|
|
164
132
|
</box>
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
}
|
|
@@ -48,6 +48,7 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
48
48
|
toggleWalk: () => void;
|
|
49
49
|
setDragging: (v: boolean) => void;
|
|
50
50
|
celebrateUpdate: (newVersion: string) => void;
|
|
51
|
+
bounce: () => void;
|
|
51
52
|
} {
|
|
52
53
|
const anim = { ...DEFAULT_ANIM, ...pack.animations };
|
|
53
54
|
const fg = pack.colors?.defaultFg || undefined;
|
|
@@ -268,8 +269,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
268
269
|
|
|
269
270
|
return (
|
|
270
271
|
<box flexDirection="column" left={left} top={top}>
|
|
271
|
-
{renderLines(lines, fg)}
|
|
272
272
|
{cel ? <text fg={fg}>{cel.text}</text> : null}
|
|
273
|
+
{renderLines(lines, fg)}
|
|
273
274
|
</box>
|
|
274
275
|
);
|
|
275
276
|
};
|
|
@@ -314,7 +315,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
314
315
|
// 连续跳跃 + 吐火星文泡泡庆祝更新成功
|
|
315
316
|
const celebrateUpdate = (newVersion: string) => {
|
|
316
317
|
const bubbles = pack.bubbleTexts ?? ["ᵘᵖ~"];
|
|
317
|
-
|
|
318
|
+
setState("happy");
|
|
319
|
+
setFrameOverride("happy");
|
|
318
320
|
|
|
319
321
|
let step = 0;
|
|
320
322
|
const JUMPS = 3;
|
|
@@ -322,6 +324,8 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
322
324
|
if (step >= JUMPS) {
|
|
323
325
|
setJumpOffset(0);
|
|
324
326
|
setCelebrate(null);
|
|
327
|
+
setFrameOverride(null);
|
|
328
|
+
setState("idle");
|
|
325
329
|
return;
|
|
326
330
|
}
|
|
327
331
|
setJumpOffset(step % 2 === 0 ? -2 : 0);
|
|
@@ -333,5 +337,13 @@ export function createAnimatedRenderer(pack: MascotPack): {
|
|
|
333
337
|
tick();
|
|
334
338
|
};
|
|
335
339
|
|
|
336
|
-
|
|
340
|
+
const bounce = () => {
|
|
341
|
+
if (currentState() === "sleeping") setState("idle");
|
|
342
|
+
setJumpOffset(-3);
|
|
343
|
+
setTimeout(() => setJumpOffset(-2), 150);
|
|
344
|
+
setTimeout(() => setJumpOffset(-1), 300);
|
|
345
|
+
setTimeout(() => setJumpOffset(0), 450);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return { element, setState, toggleWalk, setDragging, celebrateUpdate, bounce };
|
|
337
349
|
}
|