neko-vue 0.1.6 → 0.1.7
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 +11 -7
- package/README.md +65 -50
- package/dist/{NekoPet-BWrleApv.d.mts → NekoPet-C1UcoSGo.d.mts} +11 -2
- package/dist/{NekoPet-CsvPuZow.mjs → NekoPet-CLH3uMg0.mjs} +19 -6
- package/dist/{index-BGxU7veJ.d.mts → index-CJBe9Tds.d.mts} +43 -11
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/{loadNekoRuntime-BduhAtZ8.mjs → loadNekoRuntime-CarfwqJ5.mjs} +1 -1
- package/dist/{loadNekoRuntime-BqOla6to.d.mts → loadNekoRuntime-DdKHhZUx.d.mts} +1 -1
- package/dist/{nekoPlacement-CW-BnKgW.mjs → nekoPlacement-DVX15n-h.mjs} +1 -1
- package/dist/{nekojsRuntime-CiPaD_IV.mjs → nekojsRuntime-2Ax6jUB6.mjs} +539 -390
- package/dist/placement.mjs +1 -1
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.mjs +1 -1
- package/dist/{types-De8imAgT.mjs → types-DGv86pPP.mjs} +2 -2
- package/dist/types.d.mts +1 -1
- package/dist/types.mjs +1 -1
- package/dist/vue.d.mts +1 -1
- package/dist/vue.mjs +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
## [0.1.
|
|
5
|
+
## [0.1.7](https://github.com/AscaL/neko-vue/compare/v0.1.6...v0.1.7) (2026-04-06)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
6
8
|
|
|
9
|
+
- add options for behavior tracking and display in Neko components ([43a72b0](https://github.com/AscaL/neko-vue/commit/43a72b007b6f9da81441cfb1955b4ad9a92a08ef))
|
|
10
|
+
- implement NekoEngineMovement and viewport handling for enhanced pet behavior and movement logic ([2dd8484](https://github.com/AscaL/neko-vue/commit/2dd848413050e2e51bc7f6e215ed6c6f4bb05407))
|
|
11
|
+
|
|
12
|
+
## [0.1.6](https://github.com/AscaL/neko-vue/compare/v0.1.5...v0.1.6) (2026-04-05)
|
|
7
13
|
|
|
8
14
|
### Features
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
- add NekoEngineApi and NekoEngineState types for enhanced engine functionality ([138ca8d](https://github.com/AscaL/neko-vue/commit/138ca8d2486ec6e8ace114803db32bea0a14548b))
|
|
11
17
|
|
|
12
18
|
## [0.1.5](https://github.com/AscaL/neko-vue/compare/v0.1.4...v0.1.5) (2026-04-04)
|
|
13
19
|
|
|
14
|
-
|
|
15
20
|
### Features
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
- implement viewport clamping for Neko sprite and enhance resize handling with tests ([086d2ca](https://github.com/AscaL/neko-vue/commit/086d2caddc44afd1043c18c633b9ab02cf505598))
|
|
18
23
|
|
|
19
24
|
## [0.1.4](https://github.com/AscaL/neko-vue/compare/v0.1.3...v0.1.4) (2026-04-04)
|
|
20
25
|
|
|
@@ -22,11 +27,10 @@ All notable changes to this project will be documented in this file. See [commit
|
|
|
22
27
|
|
|
23
28
|
## [0.1.2](https://github.com/AscaL/neko-vue/compare/v0.1.1...v0.1.2) (2026-04-04)
|
|
24
29
|
|
|
25
|
-
|
|
26
30
|
### Features
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
- add new behavior cycle types and utility functions to enhance behavior mode handling ([c5dd242](https://github.com/AscaL/neko-vue/commit/c5dd242d27164085865665521d79acd6b3fd8b89))
|
|
33
|
+
- enhance NekoPet component with public props and improve behavior mode handling in runtime ([53ed755](https://github.com/AscaL/neko-vue/commit/53ed7553fea2078c5ff648c5753a87420eb76b32))
|
|
30
34
|
|
|
31
35
|
## [0.1.1](https://github.com/AscaL/neko-vue/compare/v0.1.0...v0.1.1) (2026-04-04)
|
|
32
36
|
|
package/README.md
CHANGED
|
@@ -18,24 +18,24 @@ Vue 3 integration for a **viewport-fixed** desktop pet: **`NekoPet`**, **`useNek
|
|
|
18
18
|
|
|
19
19
|
The **playground** is the canonical integration reference:
|
|
20
20
|
|
|
21
|
-
| Route
|
|
22
|
-
|
|
|
23
|
-
| `/`
|
|
24
|
-
| `/composable` | [`playground/src/DemoUseNekoAnchor.vue`](./playground/src/DemoUseNekoAnchor.vue) | `useNeko` + `
|
|
25
|
-
| `/customize`
|
|
21
|
+
| Route | File | Purpose |
|
|
22
|
+
| ------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
23
|
+
| `/` | [`playground/src/DemoNekoPet.vue`](./playground/src/DemoNekoPet.vue) | `<NekoPet />`, `show-behavior-on-click`, `@behavior-mode-change`, HUD + last emit |
|
|
24
|
+
| `/composable` | [`playground/src/DemoUseNekoAnchor.vue`](./playground/src/DemoUseNekoAnchor.vue) | `useNeko` + anchor; toggles for `showBehaviorOnClick` / `onBehaviorModeChange` logger |
|
|
25
|
+
| `/customize` | [`playground/src/DemoCustomize.vue`](./playground/src/DemoCustomize.vue) | Sandbox: placement, `behaviorCycle`, `cursorStandoffPx`, **Apply** |
|
|
26
26
|
|
|
27
27
|
Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`playground/src/App.vue`](./playground/src/App.vue), [`playground/src/PlaygroundLiveStats.vue`](./playground/src/PlaygroundLiveStats.vue).
|
|
28
28
|
|
|
29
29
|
### `NekoPet` vs `useNeko`
|
|
30
30
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
| **`NekoPet`** | Props
|
|
34
|
-
| **`useNeko`** | **`anchorRef`**, **`instance` / `isReady` / `error`**, **`setMode`**, **`destroy`**, **`petInteractionAwake`**,
|
|
31
|
+
| | Use when |
|
|
32
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| **`NekoPet`** | Props + **`@behavior-mode-change`**; optional **`show-behavior-on-click`** built-in label. Anchor via **`anchor-selector`** only (no template ref). |
|
|
34
|
+
| **`useNeko`** | **`anchorRef`**, **`instance` / `isReady` / `error`**, **`setMode`**, **`destroy`**, **`petInteractionAwake`**, plus all **`NekoOptions`** (e.g. **`onBehaviorModeChange`**, **`showBehaviorOnClick`**) in reactive options. |
|
|
35
35
|
|
|
36
36
|
`NekoPet` forwards props into `useNeko` ([`src/vue/NekoPet.ts`](./src/vue/NekoPet.ts)); core logic: [`src/vue/useNeko.ts`](./src/vue/useNeko.ts).
|
|
37
37
|
|
|
38
|
-
**Imports:** `import { NekoPet, useNeko, … } from "neko-vue"`. In templates, multi-word props use **kebab-case** (`start-corner`, `behavior-mode`, `behavior-cycle`, `cursor-standoff-px`, `anchor-selector`, `respect-reduced-motion`, `rest-until-first-pet-interaction`, …).
|
|
38
|
+
**Imports:** `import { NekoPet, useNeko, … } from "neko-vue"`. In templates, multi-word props use **kebab-case** (`start-corner`, `behavior-mode`, `behavior-cycle`, `cursor-standoff-px`, `anchor-selector`, `respect-reduced-motion`, `rest-until-first-pet-interaction`, `show-behavior-on-click`, …). Listen for **`@behavior-mode-change`** on **`NekoPet`** for the new **`BehaviorMode`** after each click cycle.
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -43,10 +43,12 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
|
|
|
43
43
|
|
|
44
44
|
- **Pet is not in your DOM tree** — it is fixed to the viewport; “placement” is **`startX` / `startY`**, **`startCorner`**, **`anchorRef`** / **`anchorSelector`** (see [`src/placement/nekoPlacement.ts`](./src/placement/nekoPlacement.ts)).
|
|
45
45
|
- **Per axis:** explicit coord → anchor top-left → corner → `0`. **`0`** is valid. Corner math uses **`NEKOJS_SPRITE_SIZE` (32)**.
|
|
46
|
-
- **
|
|
46
|
+
- **Viewport bounds:** Runtime [`readViewportBounds`](./src/runtime/nekoViewport.ts) uses **`visualViewport`** when it reports **positive** width and height (typical mobile chrome), otherwise **`document.documentElement.clientWidth`** and **`window.innerHeight`**. The pet loop runs on **`requestAnimationFrame`**; **`window`** **resize** and **`visualViewport`** **`resize`/`scroll`** are coalesced with rAF. On those layout updates, **`home`** and the sprite snap to **initial placement** clamped into the new bounds (all **`behaviorMode`** values, **`follow`** or **`rest`**).
|
|
47
|
+
- **Anchor gate (`useNeko` / `NekoPet`):** If **`anchorRef`** or **`anchorSelector`** is set, creation is **deferred** until the resolved element exists and has **positive** layout size (`getBoundingClientRect` / `offsetWidth`×`offsetHeight`). Low-level **`createNeko()`** itself does not perform this wait—it appends the pet to **`document.body`** immediately. **`ResizeObserver`** (for recreate when the anchor box changes) runs in **`useNeko`** only when **`anchorRef`** is set, not for **`anchorSelector`** alone.
|
|
47
48
|
- **`mode`:** **`follow`** (loop on) vs **`rest`** (stop at resolved home after create). Imperative: **`setMode`**, **`restAtOrigin`**, **`resumeFollow`**, or reactive **`mode`** in options. Changing placement-related options **recreates** the instance (no teleport API).
|
|
48
|
-
- **`behaviorMode`:** **Initial** create (+ special cases leaving the first-click gate); **live** mode advances by **clicking the cat** (if **`allowBehaviorChange`**). On recreate, the **previous engine mode** is kept unless leaving that gate (`applyBehaviorModeForRecreate` in [`src/vue/useNeko.ts`](./src/vue/useNeko.ts)). Enum **`BehaviorMode`** (ids **0…6**
|
|
49
|
+
- **`behaviorMode`:** **Initial** create (+ special cases leaving the first-click gate); **live** mode advances by **clicking the cat** (if **`allowBehaviorChange`**). On recreate, the **previous engine mode** is kept unless leaving that gate (`applyBehaviorModeForRecreate` in [`src/vue/useNeko.ts`](./src/vue/useNeko.ts)). Enum **`BehaviorMode`** (ids **0…6**), **`BEHAVIOR_MODES_IN_ORDER`**, **`DEFAULT_NEKO_BEHAVIOR_CYCLE`**, **`BehaviorCycle`**, **`isBehaviorMode`** — [`src/types/index.ts`](./src/types/index.ts); per-tick behaviors and movement state machine — [`src/runtime/nekoEngineMovement.ts`](./src/runtime/nekoEngineMovement.ts). **Readable strings (HUD / logs / hovers):** **`formatBehaviorMode`**, **`behaviorModeEnumName`**, **`BEHAVIOR_MODE_LABELS`**. **Stable cycle typing** (avoid inferred `0 \| 2 \| 1` unions): **`behaviorCycleOf(BehaviorMode.ChaseMouse, …)`**. **`NekoPet`** is cast to **`DefineComponent<NekoPetPublicProps>`** so Volar shows **`BehaviorMode`** on props, not plain `number`. Custom click order: **`behaviorCycle`**; invalid ids stripped in [`normalizeBehaviorCycle`](./src/runtime/nekojsRuntime.ts).
|
|
49
50
|
- **`allowBehaviorChange`:** On `NekoPet`, default **`undefined`** so the field is omitted and the engine default **`true`** applies (see props table below).
|
|
51
|
+
- **Click → next behavior:** When **`allowBehaviorChange`** is true, each pet **`mousedown`** advances **`behaviorCycle`**. Listen with **`onBehaviorModeChange`** in **`createNeko`** / **`useNeko`** options, or **`@behavior-mode-change`** on **`NekoPet`**. Set **`showBehaviorOnClick`** (or **`NekoOptions.showBehaviorOnClick`**) to show a short built-in **`BEHAVIOR_MODE_LABELS`** hint above the sprite.
|
|
50
52
|
- **`restUntilFirstPetInteraction`:** First pointer-down wakes **`follow`** without consuming the first cycle step; then normal cycle. **`petInteractionAwake`** tracks this.
|
|
51
53
|
- **Reduced motion:** Default **skip** load + create; **`skippedForReducedMotion`**. Opt out: **`respectReducedMotion: false`** (use with care). Helper: **`prefersReducedMotion()`** ([`src/utils/prefersReducedMotion.ts`](./src/utils/prefersReducedMotion.ts)).
|
|
52
54
|
- **SSR:** Never run **`loadNekoRuntime`**, **`useNeko`**, or mount **`NekoPet`** on the server. **Nuxt 4:** [`.client.vue` + `#components` / auto-import](https://nuxt.com/docs/4.x/guide/directory-structure/components#client-components) and/or [`<ClientOnly>`](https://nuxt.com/docs/4.x/api/components/client-only).
|
|
@@ -59,37 +61,46 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
|
|
|
59
61
|
|
|
60
62
|
`defineComponent` in [`src/vue/NekoPet.ts`](./src/vue/NekoPet.ts). Exposes **`instance`** (**`shallowRef`**, unwrapped on parent component ref like **`useNeko`’s `instance`**).
|
|
61
63
|
|
|
62
|
-
| Script
|
|
63
|
-
|
|
|
64
|
-
| `speed`
|
|
65
|
-
| `fps`
|
|
66
|
-
| `behaviorMode`
|
|
67
|
-
| `idleThreshold`
|
|
68
|
-
| `cursorStandoffPx`
|
|
69
|
-
| `allowBehaviorChange`
|
|
70
|
-
| `behaviorCycle`
|
|
71
|
-
| `startX` / `startY`
|
|
72
|
-
| `autoStart`
|
|
73
|
-
| `respectReducedMotion`
|
|
74
|
-
| `startCorner`
|
|
75
|
-
| `anchorSelector`
|
|
76
|
-
| `mode`
|
|
77
|
-
| `restUntilFirstPetInteraction` | `rest-until-first-pet-interaction` | **`undefined`** | First click wakes follow.
|
|
78
|
-
| `debug`
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
| Script | Template | Default | Notes |
|
|
65
|
+
| ------------------------------ | ---------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `speed` | `speed` | omit → **24** | Logic tick step; ~5 ticks/s in engine. |
|
|
67
|
+
| `fps` | `fps` | omit → **120** | Nominal display rate: the engine steps `update()` from **`requestAnimationFrame`** using `1000 / fps` ms. |
|
|
68
|
+
| `behaviorMode` | `behavior-mode` | omit | Initial only; clicks change live mode. |
|
|
69
|
+
| `idleThreshold` | `idle-threshold` | omit → **6** | Idle distance (px). |
|
|
70
|
+
| `cursorStandoffPx` | `cursor-standoff-px` | omit / 0 | Chase: min distance from pointer. |
|
|
71
|
+
| `allowBehaviorChange` | `allow-behavior-change` | **`undefined`** | `undefined` → engine default **true**; `false` disables click cycle. |
|
|
72
|
+
| `behaviorCycle` | `behavior-cycle` | omit | → **`DEFAULT_NEKO_BEHAVIOR_CYCLE`**. |
|
|
73
|
+
| `startX` / `startY` | `start-x` / `start-y` | omit | Home coords; **0** valid. |
|
|
74
|
+
| `autoStart` | `auto-start` | **true** | Loop after create unless **`rest`** or **false**. |
|
|
75
|
+
| `respectReducedMotion` | `respect-reduced-motion` | **true** | Skip when user prefers reduced motion. |
|
|
76
|
+
| `startCorner` | `start-corner` | omit | `top-left` … `bottom-right` for missing axes. |
|
|
77
|
+
| `anchorSelector` | `anchor-selector` | omit | `querySelector`; prefer **`anchorRef`** in **`useNeko`**. |
|
|
78
|
+
| `mode` | `mode` | **follow** | `follow` \| `rest`. |
|
|
79
|
+
| `restUntilFirstPetInteraction` | `rest-until-first-pet-interaction` | **`undefined`** | First click wakes follow. |
|
|
80
|
+
| `debug` | `debug` | **false** | **`[neko-vue]`** placement / recreate logs. |
|
|
81
|
+
| `showBehaviorOnClick` | `show-behavior-on-click` | **false** | Built-in label above the sprite after each click cycle (needs **`allowBehaviorChange`**). |
|
|
82
|
+
|
|
83
|
+
**Emits (`NekoPet`):** **`behaviorModeChange`** — payload is the new **`BehaviorMode`** after each pet click advances the cycle (same timing as **`onBehaviorModeChange`** on **`createNeko`** / **`useNeko`**).
|
|
84
|
+
|
|
85
|
+
**Option groups:** Engine — `speed`, `fps`, `behaviorMode`, `idleThreshold`, `cursorStandoffPx`, `behaviorCycle`, `allowBehaviorChange`, `startX`/`startY`, **`onBehaviorModeChange`**, **`showBehaviorOnClick`**. Placement — `startCorner`, `anchorRef` (composable) / `anchorSelector` (component). Loop — `mode`, `autoStart`, `respectReducedMotion`, `restUntilFirstPetInteraction`. Most engine/placement changes **recreate**; **`behaviorMode`** in options does not drive recreate except first create / gate exit. **`onBehaviorModeChange`** identity changes also **recreate** (use a stable handler if undesired).
|
|
81
86
|
|
|
82
87
|
### `useNeko()` return value
|
|
83
88
|
|
|
84
|
-
| Name
|
|
85
|
-
|
|
|
86
|
-
| `instance`
|
|
87
|
-
| `error`
|
|
88
|
-
| `isReady`
|
|
89
|
-
| `skippedForReducedMotion`
|
|
90
|
-
| `mode`
|
|
91
|
-
| `petInteractionAwake`
|
|
92
|
-
| `setMode`, `restAtOrigin`, `resumeFollow`, `destroy` | Imperative
|
|
89
|
+
| Name | Type / role |
|
|
90
|
+
| ---------------------------------------------------- | ------------------------------------------------------ |
|
|
91
|
+
| `instance` | `ShallowRef<NekoInstance \| null>` |
|
|
92
|
+
| `error` | `Ref<Error \| null>` |
|
|
93
|
+
| `isReady` | `Ref<boolean>` |
|
|
94
|
+
| `skippedForReducedMotion` | `Ref<boolean>` |
|
|
95
|
+
| `mode` | Readonly ref `follow` \| `rest` |
|
|
96
|
+
| `petInteractionAwake` | Readonly ref (with **`restUntilFirstPetInteraction`**) |
|
|
97
|
+
| `setMode`, `restAtOrigin`, `resumeFollow`, `destroy` | Imperative |
|
|
98
|
+
|
|
99
|
+
Options are **`UseNekoOptions`** = **`NekoOptions`** + placement / loop flags (`startCorner`, `anchorRef`, `mode`, …). Programmatic **`createNeko(opts)`** uses the same engine fields: **`onBehaviorModeChange`** (new mode after each click cycle), **`showBehaviorOnClick`** (built-in hint). Changing the **function identity** of **`onBehaviorModeChange`** or toggling **`showBehaviorOnClick`** **recreates** the pet; use a **stable** handler (e.g. a `function` declared once in `setup`) if you need to avoid that.
|
|
100
|
+
|
|
101
|
+
### `NekoOptions` (engine / `createNeko`)
|
|
102
|
+
|
|
103
|
+
Full interface: [`src/types/index.ts`](./src/types/index.ts). Besides motion and placement, notable fields include **`allowBehaviorChange`**, **`behaviorCycle`**, **`onBehaviorModeChange`**, and **`showBehaviorOnClick`**. The bundled runtime implements these in [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts) (`cycleBehavior`, hint element class **`neko-behavior-hint`**).
|
|
93
104
|
|
|
94
105
|
### `loadNekoRuntime()`
|
|
95
106
|
|
|
@@ -99,13 +110,13 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
|
|
|
99
110
|
|
|
100
111
|
[`package.json` `exports`](./package.json) + pack entries [`src/entries/*.ts`](./src/entries/) ([`vite.config.ts`](./vite.config.ts) **`pack.entry`**, **`pack.exports: false`**).
|
|
101
112
|
|
|
102
|
-
| Import
|
|
103
|
-
|
|
|
104
|
-
| `neko-vue`
|
|
105
|
-
| `neko-vue/types`
|
|
106
|
-
| `neko-vue/placement` | `cornerToStartXY`, `resolveStartPosition`, corners / input types
|
|
107
|
-
| `neko-vue/runtime`
|
|
108
|
-
| `neko-vue/vue`
|
|
113
|
+
| Import | Exposes |
|
|
114
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `neko-vue` | Full API ([`src/index.ts`](./src/index.ts)) |
|
|
116
|
+
| `neko-vue/types` | Same as root type exports: behavior helpers **`formatBehaviorMode`**, **`behaviorModeEnumName`**, **`BEHAVIOR_MODE_LABELS`**, **`behaviorCycleOf`**, plus `BehaviorMode`, `BehaviorCycle`, etc. — no Vue |
|
|
117
|
+
| `neko-vue/placement` | `cornerToStartXY`, `resolveStartPosition`, corners / input types |
|
|
118
|
+
| `neko-vue/runtime` | `loadNekoRuntime` only |
|
|
119
|
+
| `neko-vue/vue` | `useNeko`, `NekoPet`, **`NekoPetPublicProps`** (typed props for Volar), option types |
|
|
109
120
|
|
|
110
121
|
---
|
|
111
122
|
|
|
@@ -113,8 +124,10 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
|
|
|
113
124
|
|
|
114
125
|
- **Nothing draws:** Confirm the runtime **chunk** loads (Network). Check console.
|
|
115
126
|
- **Skipped pet:** **`prefers-reduced-motion: reduce`** — intentional unless you set **`respectReducedMotion: false`** (with consent).
|
|
116
|
-
- **Wrong corner on first paint:** Viewport **`clientWidth` / `innerHeight`** can be **0** briefly; **`debug: true`** logs resolved coords.
|
|
117
|
-
- **
|
|
127
|
+
- **Wrong corner on first paint:** Viewport **`clientWidth` / `innerHeight`** (or **`visualViewport`**) can be **0** briefly; **`debug: true`** logs resolved coords.
|
|
128
|
+
- **Pet jumps when the window resizes:** Expected. Any layout change handled by the engine moves **`home`** and the sprite to **initial placement** clamped inside the new bounds (every **`behaviorMode`**, **`follow`** or **`rest`**). See [`nekoViewport.ts`](./src/runtime/nekoViewport.ts) + [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts); coverage in [`tests/neko.test.ts`](./tests/neko.test.ts) (`Neko resize`).
|
|
129
|
+
- **Pet recreates on every parent render:** Often an **inline** **`onBehaviorModeChange`** (or other options) getting a **new function identity** each render — use a **stable** reference (see **`useNeko`** note above).
|
|
130
|
+
- **Listeners / timers:** Unmount should call **`stop`** + **`destroy`**; engine uses **`AbortController`**, **`requestAnimationFrame`** for the draw loop, coalesced rAF for layout clamping, and clears behavior-hint timers ([`src/runtime/nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts)).
|
|
118
131
|
|
|
119
132
|
---
|
|
120
133
|
|
|
@@ -122,11 +135,13 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
|
|
|
122
135
|
|
|
123
136
|
Classic “one-file” neko demos often use a **GIF** or scalable strip. This engine uses **fixed 32×32** cells—use **`NEKOJS_SPRITE_SIZE`** for layout math. **HMR** in dev can briefly duplicate pets; **full refresh** fixes it. The runtime loads via a **single shared** dynamic import (separate bundler chunk); no alternate script URL at runtime.
|
|
124
137
|
|
|
138
|
+
Logic ticks and movement live in [`nekoEngineMovement.ts`](./src/runtime/nekoEngineMovement.ts); viewport sizing in [`nekoViewport.ts`](./src/runtime/nekoViewport.ts); **`createNeko`** / DOM / lifecycle / click-cycle UI in [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts). The display loop is **rAF**-driven at the configured **`fps`**; in-engine logic still advances at the original ~**5** “original ticks” per second inside `update()`. Optional **`showBehaviorOnClick`** injects a **`.neko-behavior-hint`** child on the sprite root (overridable with your own CSS if needed).
|
|
139
|
+
|
|
125
140
|
---
|
|
126
141
|
|
|
127
142
|
## Development
|
|
128
143
|
|
|
129
|
-
- **Layout:** `src/types`, `src/runtime`, `src/vue`, `src/placement`, `src/utils`, `src/entries` (subpath barrels), root [`src/index.ts`](./src/index.ts).
|
|
144
|
+
- **Layout:** `src/types`, `src/runtime`, `src/vue`, `src/placement`, `src/utils`, `src/entries` (subpath barrels), root [`src/index.ts`](./src/index.ts). **Runtime:** [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts), [`nekoEngineMovement.ts`](./src/runtime/nekoEngineMovement.ts), [`nekoViewport.ts`](./src/runtime/nekoViewport.ts), [`loadNekoRuntime.ts`](./src/runtime/loadNekoRuntime.ts), [`nekoSpritesData.ts`](./src/runtime/nekoSpritesData.ts). Maintainer notes: [`plan.md`](./plan.md).
|
|
130
145
|
- **Playground:** run / HMR / port **5175** — [`playground/README.md`](./playground/README.md).
|
|
131
146
|
- **Scripts:** [`package.json`](./package.json) — `vp test`; `vp run check` & `vp run check:playground` (or `bun run check:all`); `vp pack`; `bun run playground:dev`. Do **not** run bare **`vp check`** on the whole tree (playground would resolve wrong `node_modules`).
|
|
132
147
|
- **CI:** [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as NekoStartCorner } from "./nekoPlacement-fXmlU6ys.mjs";
|
|
2
|
-
import { f as NekoInstance, i as BehaviorMode, p as NekoOptions, r as BehaviorCycle } from "./index-
|
|
2
|
+
import { f as NekoInstance, i as BehaviorMode, p as NekoOptions, r as BehaviorCycle } from "./index-CJBe9Tds.mjs";
|
|
3
3
|
import * as _$vue from "vue";
|
|
4
4
|
import { DefineComponent, MaybeRefOrGetter } from "vue";
|
|
5
5
|
|
|
@@ -15,6 +15,10 @@ type NekoFollowMode = "follow" | "rest";
|
|
|
15
15
|
* If you wrap options in **`computed(() => ({ … }))`** and include `behaviorMode`, changing it still
|
|
16
16
|
* invalidates that computed and may re-run internal watchers — use **`reactive`** for the options
|
|
17
17
|
* object if you need to mutate `behaviorMode` without that effect.
|
|
18
|
+
*
|
|
19
|
+
* **`onBehaviorModeChange`** and **`showBehaviorOnClick`** are part of the recreate fingerprint; use a
|
|
20
|
+
* **stable** `onBehaviorModeChange` reference (e.g. a top-level function) if you do not want the pet
|
|
21
|
+
* recreated on every parent render.
|
|
18
22
|
*/
|
|
19
23
|
type UseNekoOptions = NekoOptions & {
|
|
20
24
|
/**
|
|
@@ -86,7 +90,7 @@ declare function useNeko(options?: MaybeRefOrGetter<UseNekoOptions | undefined>)
|
|
|
86
90
|
interface NekoPetPublicProps {
|
|
87
91
|
/** Pixels per engine logic tick (omit → default **24**). */
|
|
88
92
|
speed?: number;
|
|
89
|
-
/**
|
|
93
|
+
/** Nominal display rate — engine steps from `requestAnimationFrame` at `1000 / fps` ms (omit → **120**). */
|
|
90
94
|
fps?: number;
|
|
91
95
|
/**
|
|
92
96
|
* Initial {@link BehaviorMode} at create time. Clicks on the pet change the live mode when
|
|
@@ -146,6 +150,11 @@ interface NekoPetPublicProps {
|
|
|
146
150
|
restUntilFirstPetInteraction?: boolean;
|
|
147
151
|
/** Log placement / recreate steps with prefix `[neko-vue]`. */
|
|
148
152
|
debug?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* When true, a short built-in label appears above the sprite after each click-to-cycle step
|
|
155
|
+
* (requires {@link allowBehaviorChange}).
|
|
156
|
+
*/
|
|
157
|
+
showBehaviorOnClick?: boolean;
|
|
149
158
|
}
|
|
150
159
|
/**
|
|
151
160
|
* Mounts the desktop pet on the client. Renders a minimal hidden root node for Vue; the engine draws
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as BehaviorMode, u as isBehaviorMode } from "./types-
|
|
2
|
-
import { n as resolveStartPosition, r as nekoVueDebug } from "./nekoPlacement-
|
|
3
|
-
import { t as loadNekoRuntime } from "./loadNekoRuntime-
|
|
1
|
+
import { r as BehaviorMode, u as isBehaviorMode } from "./types-DGv86pPP.mjs";
|
|
2
|
+
import { n as resolveStartPosition, r as nekoVueDebug } from "./nekoPlacement-DVX15n-h.mjs";
|
|
3
|
+
import { t as loadNekoRuntime } from "./loadNekoRuntime-CarfwqJ5.mjs";
|
|
4
4
|
import { computed, defineComponent, h, onBeforeUnmount, onMounted, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue";
|
|
5
5
|
//#region src/utils/prefersReducedMotion.ts
|
|
6
6
|
/**
|
|
@@ -134,6 +134,7 @@ function useNeko(options = {}) {
|
|
|
134
134
|
if ((toValue(options) ?? {}).restUntilFirstPetInteraction !== true || petInteractionAwake.value) return;
|
|
135
135
|
if (typeof document === "undefined") return;
|
|
136
136
|
const onPointerDown = (e) => {
|
|
137
|
+
if (petInteractionAwake.value) return;
|
|
137
138
|
const pet = document.querySelector(".neko");
|
|
138
139
|
const t = e.target;
|
|
139
140
|
if (!pet || !(t instanceof Node) || !(pet === t || pet.contains(t))) return;
|
|
@@ -188,7 +189,9 @@ function useNeko(options = {}) {
|
|
|
188
189
|
autoStart: raw.autoStart,
|
|
189
190
|
respectReducedMotion: raw.respectReducedMotion,
|
|
190
191
|
anchorSelector: raw.anchorSelector,
|
|
191
|
-
debug: raw.debug
|
|
192
|
+
debug: raw.debug,
|
|
193
|
+
showBehaviorOnClick: raw.showBehaviorOnClick === true,
|
|
194
|
+
onBehaviorModeChange: raw.onBehaviorModeChange
|
|
192
195
|
};
|
|
193
196
|
}, async () => {
|
|
194
197
|
const gen = ++mountGen;
|
|
@@ -337,6 +340,7 @@ function useNeko(options = {}) {
|
|
|
337
340
|
*/
|
|
338
341
|
var NekoPet_default = defineComponent({
|
|
339
342
|
name: "NekoPet",
|
|
343
|
+
emits: { behaviorModeChange: (mode) => isBehaviorMode(mode) },
|
|
340
344
|
props: {
|
|
341
345
|
speed: Number,
|
|
342
346
|
fps: Number,
|
|
@@ -371,9 +375,16 @@ var NekoPet_default = defineComponent({
|
|
|
371
375
|
debug: {
|
|
372
376
|
type: Boolean,
|
|
373
377
|
default: false
|
|
378
|
+
},
|
|
379
|
+
showBehaviorOnClick: {
|
|
380
|
+
type: Boolean,
|
|
381
|
+
default: false
|
|
374
382
|
}
|
|
375
383
|
},
|
|
376
|
-
setup(props, { expose }) {
|
|
384
|
+
setup(props, { expose, emit }) {
|
|
385
|
+
const notifyBehaviorModeChange = (mode) => {
|
|
386
|
+
emit("behaviorModeChange", mode);
|
|
387
|
+
};
|
|
377
388
|
const { instance } = useNeko(computed(() => ({
|
|
378
389
|
speed: props.speed,
|
|
379
390
|
fps: props.fps,
|
|
@@ -390,7 +401,9 @@ var NekoPet_default = defineComponent({
|
|
|
390
401
|
anchorSelector: props.anchorSelector || void 0,
|
|
391
402
|
mode: props.mode,
|
|
392
403
|
restUntilFirstPetInteraction: props.restUntilFirstPetInteraction,
|
|
393
|
-
debug: props.debug
|
|
404
|
+
debug: props.debug,
|
|
405
|
+
showBehaviorOnClick: props.showBehaviorOnClick,
|
|
406
|
+
onBehaviorModeChange: notifyBehaviorModeChange
|
|
394
407
|
})));
|
|
395
408
|
expose({ instance });
|
|
396
409
|
return () => h("span", {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//#region src/types/index.d.ts
|
|
2
|
-
/** Sprite edge length in CSS pixels for viewport bounds (
|
|
2
|
+
/** Sprite edge length in CSS pixels for viewport bounds (see runtime `readViewportBounds`). */
|
|
3
3
|
declare const NEKOJS_SPRITE_SIZE: 32;
|
|
4
4
|
/**
|
|
5
|
-
* Pet behavior / AI mode. Numeric values **must** match the bundled engine (`src/runtime/
|
|
5
|
+
* Pet behavior / AI mode. Numeric values **must** match the bundled engine (`src/runtime/nekoEngineMovement.ts`).
|
|
6
6
|
* Click cycles through {@link NekoOptions.behaviorCycle} when `allowBehaviorChange` is true.
|
|
7
7
|
*/
|
|
8
8
|
declare enum BehaviorMode {
|
|
@@ -70,8 +70,9 @@ declare function behaviorCycleOf(...modes: BehaviorMode[]): BehaviorCycle;
|
|
|
70
70
|
*
|
|
71
71
|
* Numeric defaults use **nullish coalescing** (`??`): omit or pass `undefined` for engine defaults;
|
|
72
72
|
* **`0` is a real value** for `speed`, `fps`, `idleThreshold`, `startX`, and `startY` on this interface.
|
|
73
|
-
*
|
|
74
|
-
* `window.innerHeight
|
|
73
|
+
* Bounds: `visualViewport` width/height (when available and positive) or else
|
|
74
|
+
* `document.documentElement.clientWidth` / `window.innerHeight`, each minus {@link NEKOJS_SPRITE_SIZE}
|
|
75
|
+
* (see runtime `readViewportBounds`).
|
|
75
76
|
*/
|
|
76
77
|
interface NekoOptions {
|
|
77
78
|
/**
|
|
@@ -79,7 +80,8 @@ interface NekoOptions {
|
|
|
79
80
|
*/
|
|
80
81
|
speed?: number;
|
|
81
82
|
/**
|
|
82
|
-
*
|
|
83
|
+
* Nominal display rate (default **120** when omitted). The bundled engine steps `update()` from
|
|
84
|
+
* `requestAnimationFrame` using `1000 / fps` ms between steps.
|
|
83
85
|
*/
|
|
84
86
|
fps?: number;
|
|
85
87
|
/**
|
|
@@ -115,15 +117,27 @@ interface NekoOptions {
|
|
|
115
117
|
* Initial Y position. `0` is valid; omit for default `0`.
|
|
116
118
|
*/
|
|
117
119
|
startY?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Called after each pet click advances the live {@link behaviorMode} (when
|
|
122
|
+
* {@link allowBehaviorChange} is true). Receives the **new** mode. Captured when the instance is
|
|
123
|
+
* created; changing this reference in `useNeko` options triggers recreate (use a stable function
|
|
124
|
+
* if you want to avoid that).
|
|
125
|
+
*/
|
|
126
|
+
onBehaviorModeChange?: (mode: BehaviorMode) => void;
|
|
127
|
+
/**
|
|
128
|
+
* When true, a short built-in label appears above the sprite with the new behavior’s
|
|
129
|
+
* {@link BEHAVIOR_MODE_LABELS} text after each successful click cycle step.
|
|
130
|
+
*/
|
|
131
|
+
showBehaviorOnClick?: boolean;
|
|
118
132
|
}
|
|
119
133
|
/**
|
|
120
134
|
* Minimal handle returned by {@link createNeko} / {@link loadNekoRuntime}. The bundled engine may
|
|
121
135
|
* expose additional helpers (e.g. `isIdle`).
|
|
122
136
|
*/
|
|
123
137
|
interface NekoInstance {
|
|
124
|
-
/** Starts or resumes the animation
|
|
138
|
+
/** Starts or resumes the `requestAnimationFrame` animation loop. */
|
|
125
139
|
start(): void;
|
|
126
|
-
/** Stops the
|
|
140
|
+
/** Stops the animation loop without removing the pet from the DOM. */
|
|
127
141
|
stop(): void;
|
|
128
142
|
/** Stops the loop, removes listeners, and deletes the pet element. */
|
|
129
143
|
destroy(): void;
|
|
@@ -142,6 +156,16 @@ interface NekoInstance {
|
|
|
142
156
|
*/
|
|
143
157
|
homeX?: number;
|
|
144
158
|
homeY?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Bundled engine only — whether the sprite is in a stationary / idle animation state.
|
|
161
|
+
* Test mocks may omit.
|
|
162
|
+
*/
|
|
163
|
+
isIdle?(): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Bundled engine only — assign PNG/Data URL frames for the sprite. Used internally by `createNeko`.
|
|
166
|
+
* Test mocks may omit.
|
|
167
|
+
*/
|
|
168
|
+
setSprites?(sprites: readonly string[]): void;
|
|
145
169
|
}
|
|
146
170
|
/** All mutable fields for the viewport-fixed pet engine (closure-scoped; used by `nekojsRuntime`). */
|
|
147
171
|
type NekoEngineState = {
|
|
@@ -170,6 +194,7 @@ type NekoEngineState = {
|
|
|
170
194
|
mouseY: number | null;
|
|
171
195
|
hasMouseMoved: boolean;
|
|
172
196
|
element: HTMLDivElement;
|
|
197
|
+
spriteImg: HTMLImageElement;
|
|
173
198
|
spriteImages: string[];
|
|
174
199
|
allowBehaviorChange: boolean;
|
|
175
200
|
animationTable: [number, number][];
|
|
@@ -177,17 +202,24 @@ type NekoEngineState = {
|
|
|
177
202
|
ballX: number;
|
|
178
203
|
ballY: number;
|
|
179
204
|
ballVX: number;
|
|
180
|
-
ballVY: number;
|
|
205
|
+
ballVY: number; /** Set when ball-chase mode has spawned the invisible ball; cleared when leaving that mode. */
|
|
206
|
+
ballActive: boolean;
|
|
181
207
|
running: boolean;
|
|
182
|
-
|
|
208
|
+
animationFrameId: number | null;
|
|
183
209
|
tickAccumulator: number;
|
|
184
210
|
actionCount: number;
|
|
185
211
|
lastMoveDX: number;
|
|
186
212
|
lastMoveDY: number;
|
|
187
213
|
cursorStandoffPx: number;
|
|
188
|
-
behaviorCycle: readonly BehaviorMode[];
|
|
214
|
+
behaviorCycle: readonly BehaviorMode[]; /** Initial spawn/home from options; re-clamped on resize so the viewport can grow again. */
|
|
215
|
+
placementHomeX: number;
|
|
216
|
+
placementHomeY: number;
|
|
189
217
|
homeX: number;
|
|
190
|
-
homeY: number;
|
|
218
|
+
homeY: number; /** From {@link NekoOptions.onBehaviorModeChange}; invoked after each click cycle step. */
|
|
219
|
+
onBehaviorModeChange?: (mode: BehaviorMode) => void; /** From {@link NekoOptions.showBehaviorOnClick}. */
|
|
220
|
+
showBehaviorOnClick: boolean; /** Built-in behavior label element; cleaned up in `destroy`. */
|
|
221
|
+
behaviorHintEl: HTMLDivElement | null; /** Browser timer id from `window.setTimeout` (numeric in DOM typings). */
|
|
222
|
+
behaviorHintTimeoutId: number | null;
|
|
191
223
|
};
|
|
192
224
|
/** Full engine handle: {@link NekoInstance} plus internal helpers (`setSprites`, `isIdle`). */
|
|
193
225
|
type NekoEngineApi = NekoInstance & {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as resolveStartPosition, n as NekoStartCorner, r as cornerToStartXY } from "./nekoPlacement-fXmlU6ys.mjs";
|
|
2
|
-
import { _ as isBehaviorMode, a as BehaviorModes, c as LoadNekoRuntimeOptions, d as NekoEngineState, f as NekoInstance, g as formatBehaviorMode, h as behaviorModeEnumName, i as BehaviorMode, l as NEKOJS_SPRITE_SIZE, m as behaviorCycleOf, n as BEHAVIOR_MODE_LABELS, o as CreateNekoFn, p as NekoOptions, r as BehaviorCycle, s as DEFAULT_NEKO_BEHAVIOR_CYCLE, t as BEHAVIOR_MODES_IN_ORDER, u as NekoEngineApi } from "./index-
|
|
3
|
-
import { t as loadNekoRuntime } from "./loadNekoRuntime-
|
|
4
|
-
import { a as useNeko, i as UseNekoOptions, n as _default, r as NekoFollowMode, t as NekoPetPublicProps } from "./NekoPet-
|
|
2
|
+
import { _ as isBehaviorMode, a as BehaviorModes, c as LoadNekoRuntimeOptions, d as NekoEngineState, f as NekoInstance, g as formatBehaviorMode, h as behaviorModeEnumName, i as BehaviorMode, l as NEKOJS_SPRITE_SIZE, m as behaviorCycleOf, n as BEHAVIOR_MODE_LABELS, o as CreateNekoFn, p as NekoOptions, r as BehaviorCycle, s as DEFAULT_NEKO_BEHAVIOR_CYCLE, t as BEHAVIOR_MODES_IN_ORDER, u as NekoEngineApi } from "./index-CJBe9Tds.mjs";
|
|
3
|
+
import { t as loadNekoRuntime } from "./loadNekoRuntime-DdKHhZUx.mjs";
|
|
4
|
+
import { a as useNeko, i as UseNekoOptions, n as _default, r as NekoFollowMode, t as NekoPetPublicProps } from "./NekoPet-C1UcoSGo.mjs";
|
|
5
5
|
|
|
6
6
|
//#region src/utils/debugLog.d.ts
|
|
7
7
|
/** Opt-in `console` helper; prefix keeps DevTools filter easy. */
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as DEFAULT_NEKO_BEHAVIOR_CYCLE, c as behaviorModeEnumName, i as BehaviorModes, l as formatBehaviorMode, n as BEHAVIOR_MODE_LABELS, o as NEKOJS_SPRITE_SIZE, r as BehaviorMode, s as behaviorCycleOf, t as BEHAVIOR_MODES_IN_ORDER, u as isBehaviorMode } from "./types-
|
|
2
|
-
import { n as resolveStartPosition, r as nekoVueDebug, t as cornerToStartXY } from "./nekoPlacement-
|
|
3
|
-
import { t as loadNekoRuntime } from "./loadNekoRuntime-
|
|
4
|
-
import { n as useNeko, r as prefersReducedMotion, t as NekoPet_default } from "./NekoPet-
|
|
1
|
+
import { a as DEFAULT_NEKO_BEHAVIOR_CYCLE, c as behaviorModeEnumName, i as BehaviorModes, l as formatBehaviorMode, n as BEHAVIOR_MODE_LABELS, o as NEKOJS_SPRITE_SIZE, r as BehaviorMode, s as behaviorCycleOf, t as BEHAVIOR_MODES_IN_ORDER, u as isBehaviorMode } from "./types-DGv86pPP.mjs";
|
|
2
|
+
import { n as resolveStartPosition, r as nekoVueDebug, t as cornerToStartXY } from "./nekoPlacement-DVX15n-h.mjs";
|
|
3
|
+
import { t as loadNekoRuntime } from "./loadNekoRuntime-CarfwqJ5.mjs";
|
|
4
|
+
import { n as useNeko, r as prefersReducedMotion, t as NekoPet_default } from "./NekoPet-CLH3uMg0.mjs";
|
|
5
5
|
export { BEHAVIOR_MODES_IN_ORDER, BEHAVIOR_MODE_LABELS, BehaviorMode, BehaviorModes, DEFAULT_NEKO_BEHAVIOR_CYCLE, NEKOJS_SPRITE_SIZE, NekoPet_default as NekoPet, behaviorCycleOf, behaviorModeEnumName, cornerToStartXY, formatBehaviorMode, isBehaviorMode, loadNekoRuntime, nekoVueDebug, prefersReducedMotion, resolveStartPosition, useNeko };
|
|
@@ -12,7 +12,7 @@ let bundledLoadPromise = null;
|
|
|
12
12
|
* Dynamically imports the bundled typed runtime (`./nekojsRuntime.ts`). Defines `window.createNeko`.
|
|
13
13
|
*/
|
|
14
14
|
async function importBundledNeko() {
|
|
15
|
-
await import("./nekojsRuntime-
|
|
15
|
+
await import("./nekojsRuntime-2Ax6jUB6.mjs");
|
|
16
16
|
const fn = getCreateNekoFromGlobal();
|
|
17
17
|
if (!fn) throw new Error("neko-vue: bundled neko.js did not define `createNeko`.");
|
|
18
18
|
return fn;
|