neko-vue 0.1.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/CHANGELOG.md +17 -0
- package/LICENSE +27 -0
- package/README.md +526 -0
- package/dist/NekoPet-B9k9Bf1o.d.mts +262 -0
- package/dist/NekoPet-DMPjoHm0.mjs +402 -0
- package/dist/index-BjAaI8iZ.d.mts +128 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.mjs +5 -0
- package/dist/loadNekoRuntime-CskWI70T.mjs +41 -0
- package/dist/loadNekoRuntime-DLd1lr8Y.d.mts +11 -0
- package/dist/nekoPlacement-DUdnhZoX.mjs +76 -0
- package/dist/nekoPlacement-fXmlU6ys.d.mts +23 -0
- package/dist/nekojsRuntime-DJI-YkCi.mjs +616 -0
- package/dist/placement.d.mts +2 -0
- package/dist/placement.mjs +2 -0
- package/dist/runtime.d.mts +2 -0
- package/dist/runtime.mjs +2 -0
- package/dist/types-Ctrldouo.mjs +50 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +2 -0
- package/dist/vue.d.mts +2 -0
- package/dist/vue.mjs +2 -0
- package/package.json +98 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { n as NekoStartCorner } from "./nekoPlacement-fXmlU6ys.mjs";
|
|
2
|
+
import { c as NekoOptions, s as NekoInstance, t as BehaviorMode } from "./index-BjAaI8iZ.mjs";
|
|
3
|
+
import * as _$vue from "vue";
|
|
4
|
+
import { MaybeRefOrGetter, PropType } from "vue";
|
|
5
|
+
|
|
6
|
+
//#region src/vue/useNeko.d.ts
|
|
7
|
+
/** Whether the pet chases the pointer or stays at the resolved start position. */
|
|
8
|
+
type NekoFollowMode = "follow" | "rest";
|
|
9
|
+
/**
|
|
10
|
+
* Options for {@link useNeko}. Includes all {@link NekoOptions} fields plus placement and
|
|
11
|
+
* wrapper-only flags. Omitted fields are left unset so engine defaults apply where applicable.
|
|
12
|
+
*
|
|
13
|
+
* **`behaviorMode`** is read for the **first** create and when leaving the pet-interaction gate; it
|
|
14
|
+
* is **not** part of the recreate fingerprint. Live mode changes come from **clicking the pet**.
|
|
15
|
+
* If you wrap options in **`computed(() => ({ … }))`** and include `behaviorMode`, changing it still
|
|
16
|
+
* invalidates that computed and may re-run internal watchers — use **`reactive`** for the options
|
|
17
|
+
* object if you need to mutate `behaviorMode` without that effect.
|
|
18
|
+
*/
|
|
19
|
+
type UseNekoOptions = NekoOptions & {
|
|
20
|
+
/**
|
|
21
|
+
* When true (default), run the animation loop after `createNeko`.
|
|
22
|
+
* The engine’s `createNeko` always calls `start()` internally; when this is false or `mode` is `rest`,
|
|
23
|
+
* we call `stop()` immediately after creation.
|
|
24
|
+
*/
|
|
25
|
+
autoStart?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* When true (default), do not load or create the pet if the user prefers reduced motion
|
|
28
|
+
* (see {@link prefersReducedMotion}). Set false to always run (e.g. user override).
|
|
29
|
+
*/
|
|
30
|
+
respectReducedMotion?: boolean; /** Place the pet at a viewport corner unless `startX` / `startY` override that axis. */
|
|
31
|
+
startCorner?: NekoStartCorner;
|
|
32
|
+
/**
|
|
33
|
+
* Use this element’s top-left (viewport) for `startX`/`startY` when those are omitted.
|
|
34
|
+
* When set, **`createNeko` is deferred** until the element is non-null and
|
|
35
|
+
* `getBoundingClientRect()` has **positive width and height** (avoids a bogus spawn while the anchor is still mounting or collapsed).
|
|
36
|
+
*/
|
|
37
|
+
anchorRef?: MaybeRefOrGetter<HTMLElement | null | undefined>;
|
|
38
|
+
/**
|
|
39
|
+
* `document.querySelector` (client only). Prefer `anchorRef` in `setup`.
|
|
40
|
+
* Uses the same **layout gate** as `anchorRef`: no create until a match exists with non-zero size.
|
|
41
|
+
*/
|
|
42
|
+
anchorSelector?: string; /** `follow` = chase; `rest` = remain at resolved home position (animation loop stopped). */
|
|
43
|
+
mode?: MaybeRefOrGetter<NekoFollowMode>; /** When true, logs placement and lifecycle to the console with prefix `[neko-vue]`. */
|
|
44
|
+
debug?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* When true, the pet starts **still** (`rest`) with a one-time setup so the **first** pointer down
|
|
47
|
+
* on the sprite wakes **follow** (chase) **without** consuming the built-in click-to-cycle step.
|
|
48
|
+
* Further pointer downs use the full click cycle (through stay still and return home & stay).
|
|
49
|
+
*
|
|
50
|
+
* While waiting for that first interaction, `behaviorMode` is forced to {@link BehaviorMode.StayStill} so the
|
|
51
|
+
* engine idles even if a frame runs before the wrapper calls `stop()`; we intercept the first pointer instead.
|
|
52
|
+
* `allowBehaviorChange` is forced **true** during the wait so the sprite stays clickable.
|
|
53
|
+
*/
|
|
54
|
+
restUntilFirstPetInteraction?: boolean;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Create a pet instance on mount and destroy it on unmount.
|
|
58
|
+
* Options may be reactive; when they change, the previous instance is destroyed and a new one is created.
|
|
59
|
+
*
|
|
60
|
+
* **Viewport-fixed pet:** the sprite lives on `document`, not under your component tree. This composable
|
|
61
|
+
* only manages **lifecycle** and **placement inputs** (`startX`/`startY`, corners, anchor); it does not
|
|
62
|
+
* reparent the engine’s DOM without upstream support.
|
|
63
|
+
*
|
|
64
|
+
* The engine’s `createNeko` always calls `start()`; this composable may call `stop()` right after to honor
|
|
65
|
+
* `autoStart`, `mode: 'rest'`, or both.
|
|
66
|
+
*/
|
|
67
|
+
declare function useNeko(options?: MaybeRefOrGetter<UseNekoOptions | undefined>): {
|
|
68
|
+
/** Live `NekoInstance` after load and `createNeko`, or null before ready / after destroy. */instance: _$vue.ShallowRef<NekoInstance | null, NekoInstance | null>; /** Set when runtime load or `createNeko` fails; cleared on successful recreate. */
|
|
69
|
+
error: _$vue.Ref<Error | null, Error | null>; /** True once `createNeko` has run successfully for the current mount cycle. */
|
|
70
|
+
isReady: _$vue.Ref<boolean, boolean>; /** True when the pet was skipped because `respectReducedMotion` matched user preference. */
|
|
71
|
+
skippedForReducedMotion: _$vue.Ref<boolean, boolean>; /** Current `follow` / `rest` mode (readonly); drive with {@link setMode} or a reactive `mode` in options. */
|
|
72
|
+
mode: Readonly<_$vue.Ref<NekoFollowMode, NekoFollowMode>>; /** True after the first pet pointer-down when `restUntilFirstPetInteraction` is enabled. */
|
|
73
|
+
petInteractionAwake: Readonly<_$vue.Ref<boolean, boolean>>; /** Imperatively set `follow` or `rest` (updates internal `mode` ref used by the pet). */
|
|
74
|
+
setMode: (m: NekoFollowMode) => void;
|
|
75
|
+
restAtOrigin: () => void;
|
|
76
|
+
resumeFollow: () => void; /** Stop, destroy the instance, and clear ready state (does not tear down composable watchers). */
|
|
77
|
+
destroy: () => void;
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/vue/NekoPet.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* Mounts the desktop pet on the client. Renders a minimal hidden root node for Vue; the engine draws
|
|
83
|
+
* **viewport-fixed** on `document` — not as a child of your layout. Placement is **lifecycle + options**
|
|
84
|
+
* (`startX` / `startY`, `startCorner`, `anchorRef` / `anchorSelector`); a true DOM parent for the sprite
|
|
85
|
+
* would require upstream engine changes.
|
|
86
|
+
*/
|
|
87
|
+
declare const _default: _$vue.DefineComponent<_$vue.ExtractPropTypes<{
|
|
88
|
+
speed: NumberConstructor;
|
|
89
|
+
fps: NumberConstructor;
|
|
90
|
+
/**
|
|
91
|
+
* Initial {@link BehaviorMode} at create time. Clicks on the pet change the live mode when
|
|
92
|
+
* {@link allowBehaviorChange} is true; updating this prop does not recreate or override the
|
|
93
|
+
* current mode.
|
|
94
|
+
*/
|
|
95
|
+
behaviorMode: PropType<BehaviorMode | undefined>;
|
|
96
|
+
/**
|
|
97
|
+
* Distance (px) at which the pet counts as idle for behavior logic (engine default: 6).
|
|
98
|
+
*/
|
|
99
|
+
idleThreshold: NumberConstructor;
|
|
100
|
+
/**
|
|
101
|
+
* Chase mode: stay at least this many pixels from the pointer (omit or 0 for default snap-to-cursor).
|
|
102
|
+
*/
|
|
103
|
+
cursorStandoffPx: NumberConstructor;
|
|
104
|
+
/**
|
|
105
|
+
* When clicking the pet may cycle {@link BehaviorMode}. Omit this prop to use the engine default
|
|
106
|
+
* (`true`). A plain optional boolean prop would be `false` when absent; here `default: undefined`
|
|
107
|
+
* so `createNeko` omits the field and engine defaults apply.
|
|
108
|
+
*/
|
|
109
|
+
allowBehaviorChange: {
|
|
110
|
+
type: BooleanConstructor;
|
|
111
|
+
default: undefined;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Pet-click mode order when {@link allowBehaviorChange} is true. Omit for the bundled default
|
|
115
|
+
* seven-step cycle.
|
|
116
|
+
*/
|
|
117
|
+
behaviorCycle: PropType<BehaviorMode[] | undefined>; /** Initial X in viewport pixels. `0` is valid; the engine treats omitted as `0`. */
|
|
118
|
+
startX: NumberConstructor; /** Initial Y in viewport pixels. `0` is valid; the engine treats omitted as `0`. */
|
|
119
|
+
startY: NumberConstructor;
|
|
120
|
+
/**
|
|
121
|
+
* When true (default), keep the animation loop running after `createNeko`. The engine always calls
|
|
122
|
+
* `start()` inside `createNeko`; when false or when {@link mode} is `rest`, the wrapper calls
|
|
123
|
+
* `stop()` right after creation.
|
|
124
|
+
*/
|
|
125
|
+
autoStart: {
|
|
126
|
+
type: BooleanConstructor;
|
|
127
|
+
default: boolean;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* When true (default), skip loading and creating the pet if the user prefers reduced motion
|
|
131
|
+
* (see {@link prefersReducedMotion}). Set false to always run (e.g. after explicit user consent).
|
|
132
|
+
*/
|
|
133
|
+
respectReducedMotion: {
|
|
134
|
+
type: BooleanConstructor;
|
|
135
|
+
default: boolean;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Place the pet at a viewport corner for any axis where {@link startX} / {@link startY} are omitted.
|
|
139
|
+
*/
|
|
140
|
+
startCorner: PropType<NekoStartCorner | undefined>;
|
|
141
|
+
/**
|
|
142
|
+
* `document.querySelector` for an anchor element (client only). When set, `startX`/`startY` for
|
|
143
|
+
* omitted axes use the element’s top-left in viewport space. `createNeko` waits until a match exists
|
|
144
|
+
* with positive width and height. Prefer {@link useNeko}'s `anchorRef` in script when you have a ref.
|
|
145
|
+
*/
|
|
146
|
+
anchorSelector: StringConstructor;
|
|
147
|
+
/**
|
|
148
|
+
* `follow` — chase / run behaviors (loop running). `rest` — stay at the resolved home position
|
|
149
|
+
* (loop stopped after each create).
|
|
150
|
+
*/
|
|
151
|
+
mode: {
|
|
152
|
+
type: PropType<NekoFollowMode>;
|
|
153
|
+
default: string;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* When true, start in `rest`; the first pointer down on the sprite switches to `follow` without
|
|
157
|
+
* consuming the engine’s first click-to-cycle step. Later clicks use the full cycle (including stay still and return home).
|
|
158
|
+
* While waiting, {@link behaviorMode} is forced to {@link BehaviorMode.StayStill} and
|
|
159
|
+
* {@link allowBehaviorChange} to true so the sprite stays clickable.
|
|
160
|
+
*/
|
|
161
|
+
restUntilFirstPetInteraction: {
|
|
162
|
+
type: BooleanConstructor;
|
|
163
|
+
default: undefined;
|
|
164
|
+
}; /** Log placement and recreate steps to the console with prefix `[neko-vue]`. */
|
|
165
|
+
debug: {
|
|
166
|
+
type: BooleanConstructor;
|
|
167
|
+
default: boolean;
|
|
168
|
+
};
|
|
169
|
+
}>, () => _$vue.VNode<_$vue.RendererNode, _$vue.RendererElement, {
|
|
170
|
+
[key: string]: any;
|
|
171
|
+
}>, {}, {}, {}, _$vue.ComponentOptionsMixin, _$vue.ComponentOptionsMixin, {}, string, _$vue.PublicProps, Readonly<_$vue.ExtractPropTypes<{
|
|
172
|
+
speed: NumberConstructor;
|
|
173
|
+
fps: NumberConstructor;
|
|
174
|
+
/**
|
|
175
|
+
* Initial {@link BehaviorMode} at create time. Clicks on the pet change the live mode when
|
|
176
|
+
* {@link allowBehaviorChange} is true; updating this prop does not recreate or override the
|
|
177
|
+
* current mode.
|
|
178
|
+
*/
|
|
179
|
+
behaviorMode: PropType<BehaviorMode | undefined>;
|
|
180
|
+
/**
|
|
181
|
+
* Distance (px) at which the pet counts as idle for behavior logic (engine default: 6).
|
|
182
|
+
*/
|
|
183
|
+
idleThreshold: NumberConstructor;
|
|
184
|
+
/**
|
|
185
|
+
* Chase mode: stay at least this many pixels from the pointer (omit or 0 for default snap-to-cursor).
|
|
186
|
+
*/
|
|
187
|
+
cursorStandoffPx: NumberConstructor;
|
|
188
|
+
/**
|
|
189
|
+
* When clicking the pet may cycle {@link BehaviorMode}. Omit this prop to use the engine default
|
|
190
|
+
* (`true`). A plain optional boolean prop would be `false` when absent; here `default: undefined`
|
|
191
|
+
* so `createNeko` omits the field and engine defaults apply.
|
|
192
|
+
*/
|
|
193
|
+
allowBehaviorChange: {
|
|
194
|
+
type: BooleanConstructor;
|
|
195
|
+
default: undefined;
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Pet-click mode order when {@link allowBehaviorChange} is true. Omit for the bundled default
|
|
199
|
+
* seven-step cycle.
|
|
200
|
+
*/
|
|
201
|
+
behaviorCycle: PropType<BehaviorMode[] | undefined>; /** Initial X in viewport pixels. `0` is valid; the engine treats omitted as `0`. */
|
|
202
|
+
startX: NumberConstructor; /** Initial Y in viewport pixels. `0` is valid; the engine treats omitted as `0`. */
|
|
203
|
+
startY: NumberConstructor;
|
|
204
|
+
/**
|
|
205
|
+
* When true (default), keep the animation loop running after `createNeko`. The engine always calls
|
|
206
|
+
* `start()` inside `createNeko`; when false or when {@link mode} is `rest`, the wrapper calls
|
|
207
|
+
* `stop()` right after creation.
|
|
208
|
+
*/
|
|
209
|
+
autoStart: {
|
|
210
|
+
type: BooleanConstructor;
|
|
211
|
+
default: boolean;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* When true (default), skip loading and creating the pet if the user prefers reduced motion
|
|
215
|
+
* (see {@link prefersReducedMotion}). Set false to always run (e.g. after explicit user consent).
|
|
216
|
+
*/
|
|
217
|
+
respectReducedMotion: {
|
|
218
|
+
type: BooleanConstructor;
|
|
219
|
+
default: boolean;
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Place the pet at a viewport corner for any axis where {@link startX} / {@link startY} are omitted.
|
|
223
|
+
*/
|
|
224
|
+
startCorner: PropType<NekoStartCorner | undefined>;
|
|
225
|
+
/**
|
|
226
|
+
* `document.querySelector` for an anchor element (client only). When set, `startX`/`startY` for
|
|
227
|
+
* omitted axes use the element’s top-left in viewport space. `createNeko` waits until a match exists
|
|
228
|
+
* with positive width and height. Prefer {@link useNeko}'s `anchorRef` in script when you have a ref.
|
|
229
|
+
*/
|
|
230
|
+
anchorSelector: StringConstructor;
|
|
231
|
+
/**
|
|
232
|
+
* `follow` — chase / run behaviors (loop running). `rest` — stay at the resolved home position
|
|
233
|
+
* (loop stopped after each create).
|
|
234
|
+
*/
|
|
235
|
+
mode: {
|
|
236
|
+
type: PropType<NekoFollowMode>;
|
|
237
|
+
default: string;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* When true, start in `rest`; the first pointer down on the sprite switches to `follow` without
|
|
241
|
+
* consuming the engine’s first click-to-cycle step. Later clicks use the full cycle (including stay still and return home).
|
|
242
|
+
* While waiting, {@link behaviorMode} is forced to {@link BehaviorMode.StayStill} and
|
|
243
|
+
* {@link allowBehaviorChange} to true so the sprite stays clickable.
|
|
244
|
+
*/
|
|
245
|
+
restUntilFirstPetInteraction: {
|
|
246
|
+
type: BooleanConstructor;
|
|
247
|
+
default: undefined;
|
|
248
|
+
}; /** Log placement and recreate steps to the console with prefix `[neko-vue]`. */
|
|
249
|
+
debug: {
|
|
250
|
+
type: BooleanConstructor;
|
|
251
|
+
default: boolean;
|
|
252
|
+
};
|
|
253
|
+
}>> & Readonly<{}>, {
|
|
254
|
+
allowBehaviorChange: boolean;
|
|
255
|
+
autoStart: boolean;
|
|
256
|
+
respectReducedMotion: boolean;
|
|
257
|
+
mode: NekoFollowMode;
|
|
258
|
+
restUntilFirstPetInteraction: boolean;
|
|
259
|
+
debug: boolean;
|
|
260
|
+
}, {}, {}, {}, string, _$vue.ComponentProvideOptions, true, {}, any>;
|
|
261
|
+
//#endregion
|
|
262
|
+
export { useNeko as i, NekoFollowMode as n, UseNekoOptions as r, _default as t };
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { t as BehaviorMode } from "./types-Ctrldouo.mjs";
|
|
2
|
+
import { n as resolveStartPosition, r as nekoVueDebug } from "./nekoPlacement-DUdnhZoX.mjs";
|
|
3
|
+
import { t as loadNekoRuntime } from "./loadNekoRuntime-CskWI70T.mjs";
|
|
4
|
+
import { computed, defineComponent, h, onBeforeUnmount, onMounted, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue";
|
|
5
|
+
//#region src/utils/prefersReducedMotion.ts
|
|
6
|
+
/**
|
|
7
|
+
* True when the user has requested less motion (OS / browser setting).
|
|
8
|
+
* Safe on SSR (returns false) and when `matchMedia` is missing.
|
|
9
|
+
*/
|
|
10
|
+
function prefersReducedMotion() {
|
|
11
|
+
if (typeof window === "undefined") return false;
|
|
12
|
+
try {
|
|
13
|
+
return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/vue/useNeko.ts
|
|
20
|
+
function pickNekoOptions(o) {
|
|
21
|
+
const { autoStart: _a, respectReducedMotion: _r, startCorner: _c, anchorRef: _ar, anchorSelector: _as, mode: _m, debug: _d, restUntilFirstPetInteraction: _wake, ...neko } = o;
|
|
22
|
+
return neko;
|
|
23
|
+
}
|
|
24
|
+
function resolveAnchorElement(o) {
|
|
25
|
+
if (o.anchorRef != null) {
|
|
26
|
+
const el = toValue(o.anchorRef);
|
|
27
|
+
if (el) return el;
|
|
28
|
+
}
|
|
29
|
+
if (o.anchorSelector && typeof document !== "undefined") return document.querySelector(o.anchorSelector);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** User configured an anchor (ref or selector) — wait for a real, non-collapsed box before `createNeko`. */
|
|
33
|
+
function wantsAnchorBinding(o) {
|
|
34
|
+
return o.anchorRef != null || Boolean(o.anchorSelector);
|
|
35
|
+
}
|
|
36
|
+
function isAnchorLaidOutForCreate(anchorEl) {
|
|
37
|
+
if (!anchorEl) return false;
|
|
38
|
+
const r = anchorEl.getBoundingClientRect();
|
|
39
|
+
const w = r.width > 0 ? r.width : anchorEl.offsetWidth;
|
|
40
|
+
const h = r.height > 0 ? r.height : anchorEl.offsetHeight;
|
|
41
|
+
return w > 0 && h > 0;
|
|
42
|
+
}
|
|
43
|
+
function buildPlacementInput(o) {
|
|
44
|
+
return {
|
|
45
|
+
startX: o.startX,
|
|
46
|
+
startY: o.startY,
|
|
47
|
+
startCorner: o.startCorner,
|
|
48
|
+
anchorElement: resolveAnchorElement(o)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** The engine treats a missing key as its default; strip `undefined` so we never override with implicit Vue prop values. */
|
|
52
|
+
function omitUndefinedNekoFields(opts) {
|
|
53
|
+
return Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0));
|
|
54
|
+
}
|
|
55
|
+
function buildCreateOptions(o, petGateClosed) {
|
|
56
|
+
const debug = o.debug === true;
|
|
57
|
+
const base = pickNekoOptions(o);
|
|
58
|
+
const { startX, startY } = resolveStartPosition(buildPlacementInput(o), debug);
|
|
59
|
+
let merged = {
|
|
60
|
+
...base,
|
|
61
|
+
startX,
|
|
62
|
+
startY
|
|
63
|
+
};
|
|
64
|
+
if (petGateClosed && o.restUntilFirstPetInteraction) merged = {
|
|
65
|
+
...merged,
|
|
66
|
+
behaviorMode: BehaviorMode.StayStill,
|
|
67
|
+
allowBehaviorChange: true
|
|
68
|
+
};
|
|
69
|
+
return omitUndefinedNekoFields(merged);
|
|
70
|
+
}
|
|
71
|
+
function readInstanceBehaviorMode(inst) {
|
|
72
|
+
if (!inst) return;
|
|
73
|
+
const v = inst.behaviorMode;
|
|
74
|
+
return typeof v === "number" ? v : void 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `behaviorMode` in options is only for **initial** create; reactive changes must not recreate.
|
|
78
|
+
* On recreate (anchor/layout/mode…), keep the live engine mode unless we just left the
|
|
79
|
+
* `restUntilFirstPetInteraction` gate (then apply the parent’s initial mode again).
|
|
80
|
+
*/
|
|
81
|
+
function applyBehaviorModeForRecreate(nekoOpts, raw, petGateClosed, priorBm, leavingPetGate) {
|
|
82
|
+
if (petGateClosed && raw.restUntilFirstPetInteraction) return;
|
|
83
|
+
if (leavingPetGate) {
|
|
84
|
+
if (raw.behaviorMode !== void 0) nekoOpts.behaviorMode = raw.behaviorMode;
|
|
85
|
+
else delete nekoOpts.behaviorMode;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (priorBm !== void 0) nekoOpts.behaviorMode = priorBm;
|
|
89
|
+
}
|
|
90
|
+
function anchorElementRectKey(el) {
|
|
91
|
+
const r = el.getBoundingClientRect();
|
|
92
|
+
return [
|
|
93
|
+
r.left,
|
|
94
|
+
r.top,
|
|
95
|
+
r.width,
|
|
96
|
+
r.height
|
|
97
|
+
].map((n) => Math.round(n * 100) / 100).join(",");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a pet instance on mount and destroy it on unmount.
|
|
101
|
+
* Options may be reactive; when they change, the previous instance is destroyed and a new one is created.
|
|
102
|
+
*
|
|
103
|
+
* **Viewport-fixed pet:** the sprite lives on `document`, not under your component tree. This composable
|
|
104
|
+
* only manages **lifecycle** and **placement inputs** (`startX`/`startY`, corners, anchor); it does not
|
|
105
|
+
* reparent the engine’s DOM without upstream support.
|
|
106
|
+
*
|
|
107
|
+
* The engine’s `createNeko` always calls `start()`; this composable may call `stop()` right after to honor
|
|
108
|
+
* `autoStart`, `mode: 'rest'`, or both.
|
|
109
|
+
*/
|
|
110
|
+
function useNeko(options = {}) {
|
|
111
|
+
const instance = shallowRef(null);
|
|
112
|
+
const error = ref(null);
|
|
113
|
+
const isReady = ref(false);
|
|
114
|
+
const skippedForReducedMotion = ref(false);
|
|
115
|
+
const modeRef = ref("follow");
|
|
116
|
+
const anchorLayoutTick = ref(0);
|
|
117
|
+
/** After first pet pointer-down when `restUntilFirstPetInteraction` is enabled. */
|
|
118
|
+
const petInteractionAwake = ref(false);
|
|
119
|
+
/** True while the live instance was created under the pet-interaction gate (ball-chase wait). */
|
|
120
|
+
const petGateSpawnedInstance = ref(false);
|
|
121
|
+
let stopModeWatch;
|
|
122
|
+
let stopMainWatch;
|
|
123
|
+
let stopWakeOptionWatch;
|
|
124
|
+
let mountGen = 0;
|
|
125
|
+
onMounted(() => {
|
|
126
|
+
stopModeWatch = watch(() => toValue(options)?.mode, (m) => {
|
|
127
|
+
if (m !== void 0) modeRef.value = toValue(m);
|
|
128
|
+
}, { immediate: true });
|
|
129
|
+
stopWakeOptionWatch = watch(() => toValue(options)?.restUntilFirstPetInteraction === true, (enabled) => {
|
|
130
|
+
if (!enabled) petInteractionAwake.value = false;
|
|
131
|
+
});
|
|
132
|
+
watchEffect((onCleanup) => {
|
|
133
|
+
if ((toValue(options) ?? {}).restUntilFirstPetInteraction !== true || petInteractionAwake.value) return;
|
|
134
|
+
if (typeof document === "undefined") return;
|
|
135
|
+
const onPointerDown = (e) => {
|
|
136
|
+
const pet = document.querySelector(".neko");
|
|
137
|
+
const t = e.target;
|
|
138
|
+
if (!pet || !(t instanceof Node) || !(pet === t || pet.contains(t))) return;
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
e.stopImmediatePropagation();
|
|
141
|
+
petInteractionAwake.value = true;
|
|
142
|
+
};
|
|
143
|
+
document.addEventListener("mousedown", onPointerDown, true);
|
|
144
|
+
onCleanup(() => {
|
|
145
|
+
document.removeEventListener("mousedown", onPointerDown, true);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
watchEffect((onCleanup) => {
|
|
149
|
+
const raw = toValue(options) ?? {};
|
|
150
|
+
const el = raw.anchorRef != null ? toValue(raw.anchorRef) : null;
|
|
151
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
152
|
+
let lastRectKey = anchorElementRectKey(el);
|
|
153
|
+
const ro = new ResizeObserver(() => {
|
|
154
|
+
const next = anchorElementRectKey(el);
|
|
155
|
+
if (next === lastRectKey) return;
|
|
156
|
+
lastRectKey = next;
|
|
157
|
+
anchorLayoutTick.value++;
|
|
158
|
+
});
|
|
159
|
+
ro.observe(el);
|
|
160
|
+
onCleanup(() => {
|
|
161
|
+
ro.disconnect();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
stopMainWatch = watch(() => {
|
|
165
|
+
const raw = toValue(options) ?? {};
|
|
166
|
+
const rect = resolveAnchorElement(raw)?.getBoundingClientRect();
|
|
167
|
+
return {
|
|
168
|
+
mode: modeRef.value,
|
|
169
|
+
anchorTick: anchorLayoutTick.value,
|
|
170
|
+
anchorRect: rect ? {
|
|
171
|
+
x: rect.x,
|
|
172
|
+
y: rect.y,
|
|
173
|
+
w: rect.width,
|
|
174
|
+
h: rect.height
|
|
175
|
+
} : null,
|
|
176
|
+
petInteractionAwake: petInteractionAwake.value,
|
|
177
|
+
restUntilFirstPetInteraction: raw.restUntilFirstPetInteraction === true,
|
|
178
|
+
speed: raw.speed,
|
|
179
|
+
fps: raw.fps,
|
|
180
|
+
idleThreshold: raw.idleThreshold,
|
|
181
|
+
cursorStandoffPx: raw.cursorStandoffPx,
|
|
182
|
+
behaviorCycle: JSON.stringify(raw.behaviorCycle ?? null),
|
|
183
|
+
allowBehaviorChange: raw.allowBehaviorChange,
|
|
184
|
+
startCorner: raw.startCorner,
|
|
185
|
+
startX: raw.startX,
|
|
186
|
+
startY: raw.startY,
|
|
187
|
+
autoStart: raw.autoStart,
|
|
188
|
+
respectReducedMotion: raw.respectReducedMotion,
|
|
189
|
+
anchorSelector: raw.anchorSelector,
|
|
190
|
+
debug: raw.debug
|
|
191
|
+
};
|
|
192
|
+
}, async () => {
|
|
193
|
+
const gen = ++mountGen;
|
|
194
|
+
const raw = toValue(options) ?? {};
|
|
195
|
+
const debug = raw.debug === true;
|
|
196
|
+
nekoVueDebug(debug, "recreate:enter", {
|
|
197
|
+
gen,
|
|
198
|
+
modeRef: modeRef.value,
|
|
199
|
+
optionsMode: raw.mode !== void 0 ? toValue(raw.mode) : void 0,
|
|
200
|
+
clientWidth: typeof document !== "undefined" ? document.documentElement.clientWidth : null,
|
|
201
|
+
innerHeight: typeof window !== "undefined" ? window.innerHeight : null
|
|
202
|
+
});
|
|
203
|
+
error.value = null;
|
|
204
|
+
isReady.value = false;
|
|
205
|
+
const priorBm = readInstanceBehaviorMode(instance.value);
|
|
206
|
+
const wasPetGateInstance = petGateSpawnedInstance.value;
|
|
207
|
+
const petGateClosed = raw.restUntilFirstPetInteraction === true && !petInteractionAwake.value;
|
|
208
|
+
const leavingPetGate = wasPetGateInstance && petInteractionAwake.value && priorBm === BehaviorMode.StayStill && !petGateClosed;
|
|
209
|
+
if (instance.value) {
|
|
210
|
+
instance.value.stop();
|
|
211
|
+
instance.value.destroy();
|
|
212
|
+
instance.value = null;
|
|
213
|
+
}
|
|
214
|
+
const { autoStart = true, respectReducedMotion: respectMotion = true } = raw;
|
|
215
|
+
if (respectMotion && prefersReducedMotion()) {
|
|
216
|
+
if (gen !== mountGen) return;
|
|
217
|
+
skippedForReducedMotion.value = true;
|
|
218
|
+
error.value = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
skippedForReducedMotion.value = false;
|
|
222
|
+
if (wantsAnchorBinding(raw)) {
|
|
223
|
+
const anchorEl = resolveAnchorElement(raw);
|
|
224
|
+
if (!isAnchorLaidOutForCreate(anchorEl)) {
|
|
225
|
+
const r = anchorEl?.getBoundingClientRect();
|
|
226
|
+
nekoVueDebug(debug, "recreate:defer anchor layout", {
|
|
227
|
+
hasEl: Boolean(anchorEl),
|
|
228
|
+
width: r?.width ?? null,
|
|
229
|
+
height: r?.height ?? null
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const effectiveMode = petGateClosed ? "rest" : modeRef.value;
|
|
235
|
+
const nekoOpts = buildCreateOptions(raw, petGateClosed);
|
|
236
|
+
applyBehaviorModeForRecreate(nekoOpts, raw, petGateClosed, priorBm, leavingPetGate);
|
|
237
|
+
nekoVueDebug(debug, "recreate:createNeko options (payload)", {
|
|
238
|
+
...nekoOpts,
|
|
239
|
+
_effectiveMode: effectiveMode,
|
|
240
|
+
_autoStart: autoStart,
|
|
241
|
+
_gen: gen,
|
|
242
|
+
_petGateClosed: petGateClosed
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const create = await loadNekoRuntime();
|
|
246
|
+
if (gen !== mountGen) {
|
|
247
|
+
const stray = create(nekoOpts);
|
|
248
|
+
stray.stop();
|
|
249
|
+
stray.destroy();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const nekoInstance = create(nekoOpts);
|
|
253
|
+
if (gen !== mountGen) {
|
|
254
|
+
nekoInstance.stop();
|
|
255
|
+
nekoInstance.destroy();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
instance.value = nekoInstance;
|
|
259
|
+
const shouldRun = effectiveMode === "follow" && autoStart !== false;
|
|
260
|
+
if (shouldRun) nekoInstance.start();
|
|
261
|
+
else nekoInstance.stop();
|
|
262
|
+
nekoVueDebug(debug, "recreate:instance ready", {
|
|
263
|
+
shouldRun,
|
|
264
|
+
startX: nekoOpts.startX,
|
|
265
|
+
startY: nekoOpts.startY,
|
|
266
|
+
behaviorMode: nekoOpts.behaviorMode
|
|
267
|
+
});
|
|
268
|
+
isReady.value = true;
|
|
269
|
+
petGateSpawnedInstance.value = petGateClosed;
|
|
270
|
+
} catch (e) {
|
|
271
|
+
if (gen !== mountGen) return;
|
|
272
|
+
error.value = e instanceof Error ? e : new Error(String(e));
|
|
273
|
+
}
|
|
274
|
+
}, {
|
|
275
|
+
flush: "post",
|
|
276
|
+
immediate: true
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
onBeforeUnmount(() => {
|
|
280
|
+
stopModeWatch?.();
|
|
281
|
+
stopWakeOptionWatch?.();
|
|
282
|
+
stopMainWatch?.();
|
|
283
|
+
mountGen++;
|
|
284
|
+
if (instance.value) {
|
|
285
|
+
instance.value.stop();
|
|
286
|
+
instance.value.destroy();
|
|
287
|
+
instance.value = null;
|
|
288
|
+
}
|
|
289
|
+
isReady.value = false;
|
|
290
|
+
skippedForReducedMotion.value = false;
|
|
291
|
+
petInteractionAwake.value = false;
|
|
292
|
+
petGateSpawnedInstance.value = false;
|
|
293
|
+
});
|
|
294
|
+
function setMode(m) {
|
|
295
|
+
modeRef.value = m;
|
|
296
|
+
}
|
|
297
|
+
function destroy() {
|
|
298
|
+
if (instance.value) {
|
|
299
|
+
instance.value.stop();
|
|
300
|
+
instance.value.destroy();
|
|
301
|
+
instance.value = null;
|
|
302
|
+
}
|
|
303
|
+
isReady.value = false;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Sets `mode` to `rest` (stops at the resolved home position after the next internal cycle).
|
|
307
|
+
* If you pass a reactive `mode` in options, prefer updating that ref instead; it stays in sync via watch.
|
|
308
|
+
*/
|
|
309
|
+
function restAtOrigin() {
|
|
310
|
+
setMode("rest");
|
|
311
|
+
}
|
|
312
|
+
/** Sets `mode` to `follow`. */
|
|
313
|
+
function resumeFollow() {
|
|
314
|
+
setMode("follow");
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
instance,
|
|
318
|
+
error,
|
|
319
|
+
isReady,
|
|
320
|
+
skippedForReducedMotion,
|
|
321
|
+
mode: readonly(modeRef),
|
|
322
|
+
petInteractionAwake: readonly(petInteractionAwake),
|
|
323
|
+
setMode,
|
|
324
|
+
restAtOrigin,
|
|
325
|
+
resumeFollow,
|
|
326
|
+
destroy
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/vue/NekoPet.ts
|
|
331
|
+
/**
|
|
332
|
+
* Mounts the desktop pet on the client. Renders a minimal hidden root node for Vue; the engine draws
|
|
333
|
+
* **viewport-fixed** on `document` — not as a child of your layout. Placement is **lifecycle + options**
|
|
334
|
+
* (`startX` / `startY`, `startCorner`, `anchorRef` / `anchorSelector`); a true DOM parent for the sprite
|
|
335
|
+
* would require upstream engine changes.
|
|
336
|
+
*/
|
|
337
|
+
var NekoPet_default = defineComponent({
|
|
338
|
+
name: "NekoPet",
|
|
339
|
+
props: {
|
|
340
|
+
speed: Number,
|
|
341
|
+
fps: Number,
|
|
342
|
+
behaviorMode: Number,
|
|
343
|
+
idleThreshold: Number,
|
|
344
|
+
cursorStandoffPx: Number,
|
|
345
|
+
allowBehaviorChange: {
|
|
346
|
+
type: Boolean,
|
|
347
|
+
default: void 0
|
|
348
|
+
},
|
|
349
|
+
behaviorCycle: Array,
|
|
350
|
+
startX: Number,
|
|
351
|
+
startY: Number,
|
|
352
|
+
autoStart: {
|
|
353
|
+
type: Boolean,
|
|
354
|
+
default: true
|
|
355
|
+
},
|
|
356
|
+
respectReducedMotion: {
|
|
357
|
+
type: Boolean,
|
|
358
|
+
default: true
|
|
359
|
+
},
|
|
360
|
+
startCorner: String,
|
|
361
|
+
anchorSelector: String,
|
|
362
|
+
mode: {
|
|
363
|
+
type: String,
|
|
364
|
+
default: "follow"
|
|
365
|
+
},
|
|
366
|
+
restUntilFirstPetInteraction: {
|
|
367
|
+
type: Boolean,
|
|
368
|
+
default: void 0
|
|
369
|
+
},
|
|
370
|
+
debug: {
|
|
371
|
+
type: Boolean,
|
|
372
|
+
default: false
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
setup(props, { expose }) {
|
|
376
|
+
const { instance } = useNeko(computed(() => ({
|
|
377
|
+
speed: props.speed,
|
|
378
|
+
fps: props.fps,
|
|
379
|
+
behaviorMode: props.behaviorMode,
|
|
380
|
+
idleThreshold: props.idleThreshold,
|
|
381
|
+
cursorStandoffPx: props.cursorStandoffPx,
|
|
382
|
+
allowBehaviorChange: props.allowBehaviorChange,
|
|
383
|
+
behaviorCycle: props.behaviorCycle,
|
|
384
|
+
startX: props.startX,
|
|
385
|
+
startY: props.startY,
|
|
386
|
+
autoStart: props.autoStart,
|
|
387
|
+
respectReducedMotion: props.respectReducedMotion,
|
|
388
|
+
startCorner: props.startCorner,
|
|
389
|
+
anchorSelector: props.anchorSelector || void 0,
|
|
390
|
+
mode: props.mode,
|
|
391
|
+
restUntilFirstPetInteraction: props.restUntilFirstPetInteraction,
|
|
392
|
+
debug: props.debug
|
|
393
|
+
})));
|
|
394
|
+
expose({ instance });
|
|
395
|
+
return () => h("span", {
|
|
396
|
+
class: "neko-vue-root",
|
|
397
|
+
"aria-hidden": true
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
//#endregion
|
|
402
|
+
export { useNeko as n, prefersReducedMotion as r, NekoPet_default as t };
|