@vmz/plugin-live2d 0.0.21 → 0.0.23

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.
@@ -55,8 +55,12 @@ export default class Live2dHost {
55
55
  public capturePresetId = '';
56
56
  /** Called with Live2DRuntime after loadModel succeeds. */
57
57
  public onReady = null;
58
- /** Optional frame profile listener. */
58
+ /** Optional frame profile listener (caller should throttle UI). */
59
59
  public onProfile = null;
60
+ /** Load / mount failure. */
61
+ public onError = null;
62
+ /** Pointer hit area string (often `drawable:N`). */
63
+ public onHit = null;
60
64
 
61
65
  hostId = `live2d-host-${++hostSeq}`;
62
66
  hostStyle = '';
@@ -64,9 +68,10 @@ export default class Live2dHost {
64
68
  progressPercent = 0;
65
69
  progressLabel = 'Loading…';
66
70
  runtime = null;
67
- _raf = 0;
68
- _lastTs = 0;
69
71
  _unsubProfile = null;
72
+ _canvas = null;
73
+ _boundPointer = null;
74
+ _bootGeneration = 0;
70
75
 
71
76
  onMount() {
72
77
  this.hostStyle = `width:${this.width}px;height:${this.height}px`;
@@ -86,27 +91,44 @@ export default class Live2dHost {
86
91
  }
87
92
 
88
93
  teardownRuntime() {
89
- this.stopLoop();
94
+ try {
95
+ this.runtime?.stage?.stop?.();
96
+ } catch {
97
+ /* updateMode may be manual on older callers */
98
+ }
99
+ this.detachPointer();
90
100
  if (typeof this._unsubProfile === 'function') {
91
101
  this._unsubProfile();
92
102
  this._unsubProfile = null;
93
103
  }
94
104
  this.runtime?.destroy?.();
95
105
  this.runtime = null;
106
+ this._canvas = null;
107
+ }
108
+
109
+ detachPointer() {
110
+ if (this._canvas && this._boundPointer) {
111
+ this._canvas.removeEventListener('pointerdown', this._boundPointer);
112
+ }
113
+ this._boundPointer = null;
96
114
  }
97
115
 
98
- stopLoop() {
99
- if (this._raf) cancelAnimationFrame(this._raf);
100
- this._raf = 0;
101
- this._lastTs = 0;
116
+ attachPointer(canvas) {
117
+ this.detachPointer();
118
+ this._boundPointer = (event) => this.onPointerDown(event);
119
+ canvas.addEventListener('pointerdown', this._boundPointer);
102
120
  }
103
121
 
104
- tick(ts) {
105
- if (!this.runtime) return;
106
- const dt = this._lastTs ? (ts - this._lastTs) / 1000 : 0;
107
- this._lastTs = ts;
108
- this.runtime.update(dt);
109
- this._raf = requestAnimationFrame((t) => this.tick(t));
122
+ onPointerDown(event) {
123
+ const runtime = this.runtime;
124
+ const canvas = this._canvas;
125
+ if (!runtime || !canvas || typeof this.onHit !== 'function') return;
126
+ const rect = canvas.getBoundingClientRect();
127
+ if (rect.width <= 0 || rect.height <= 0) return;
128
+ const nx = ((event.clientX - rect.left) / rect.width) * 2 - 1;
129
+ const ny = 1 - ((event.clientY - rect.top) / rect.height) * 2;
130
+ const area = runtime.hitTest?.(nx, ny) ?? null;
131
+ this.onHit(area);
110
132
  }
111
133
 
112
134
  ensureCanvas() {
@@ -120,6 +142,7 @@ export default class Live2dHost {
120
142
  canvas.style.width = `${this.width}px`;
121
143
  canvas.style.height = `${this.height}px`;
122
144
  host.appendChild(canvas);
145
+ this._canvas = canvas;
123
146
  return canvas;
124
147
  }
125
148
 
@@ -132,48 +155,69 @@ export default class Live2dHost {
132
155
  return this.runtime.capturePng(opts);
133
156
  }
134
157
 
158
+ reportError(err) {
159
+ this.loading = false;
160
+ const message = String(err?.message || err || 'load error');
161
+ if (this.exposeCaptureApi && typeof window !== 'undefined') {
162
+ const api = window.__LIVE2D_CAPTURE__;
163
+ if (api) api.error = message;
164
+ }
165
+ if (typeof this.onError === 'function') this.onError(message);
166
+ }
167
+
135
168
  async boot() {
136
169
  if (!this.model) return;
170
+ const generation = ++this._bootGeneration;
137
171
  this.loading = true;
138
172
  this.progressPercent = 0;
139
173
  this.progressLabel = 'loading';
140
174
  try {
141
175
  this.teardownRuntime();
176
+ if (generation !== this._bootGeneration) return;
142
177
  const canvas = this.ensureCanvas();
143
178
  if (!canvas) return;
144
179
  const prefer = Array.isArray(this.prefer)
145
180
  ? this.prefer
146
181
  : ['canvas2d', 'webgl2', 'webgpu'];
147
- const runtime = createLive2D({ prefer, updateMode: 'manual' });
182
+ const runtime = createLive2D({ prefer, updateMode: 'auto' });
148
183
  runtime.events.on('ready', () => {
184
+ if (generation !== this._bootGeneration) return;
149
185
  this.loading = false;
150
186
  this.progressPercent = 100;
151
187
  this.progressLabel = 'ready';
152
188
  if (this.autoplay) {
153
- this._lastTs = 0;
154
- this._raf = requestAnimationFrame((t) => this.tick(t));
189
+ try {
190
+ runtime.stage.start();
191
+ } catch {
192
+ /* already running */
193
+ }
155
194
  }
195
+ this.attachPointer(canvas);
156
196
  if (this.exposeCaptureApi) void this.publishCapture();
157
197
  if (typeof this.onReady === 'function') this.onReady(runtime);
158
198
  });
159
199
  runtime.events.on('error', (p) => {
160
- this.loading = false;
161
- if (this.exposeCaptureApi && typeof window !== 'undefined') {
162
- const api = window.__LIVE2D_CAPTURE__;
163
- if (api) api.error = String(p?.error || 'load error');
164
- }
200
+ if (generation !== this._bootGeneration) return;
201
+ this.reportError(p?.error || 'load error');
165
202
  });
166
203
  if (typeof this.onProfile === 'function') {
167
204
  this._unsubProfile = runtime.events.on('profile', (profile) => {
205
+ if (generation !== this._bootGeneration) return;
168
206
  this.onProfile(profile);
169
207
  });
170
208
  }
171
209
  runtime.mount(canvas);
172
210
  this.progressPercent = 35;
173
211
  await runtime.loadModel(this.model);
212
+ if (generation !== this._bootGeneration) {
213
+ runtime.destroy?.();
214
+ return;
215
+ }
174
216
  this.runtime = runtime;
175
- } catch {
176
- this.loading = false;
217
+ this.progressPercent = 90;
218
+ } catch (err) {
219
+ if (generation !== this._bootGeneration) return;
220
+ this.reportError(err);
177
221
  }
178
222
  }
179
223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/plugin-live2d",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "VMZ Live2D adapter — <Live2dHost> for @doki-land/live2d in VMZ applications",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "test": "vitest run --passWithNoTests"
37
37
  },
38
38
  "dependencies": {
39
- "@doki-land/live2d": "0.0.21",
39
+ "@doki-land/live2d": "0.0.23",
40
40
  "@vmz/plugin": "^0.1.8"
41
41
  }
42
42
  }