@vmz/plugin-live2d 0.0.20 → 0.0.22

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.
@@ -1,20 +1,40 @@
1
1
  <template>
2
2
  <div class="live2d-host" style={hostStyle}>
3
3
  <div class="live2d-host__mount" id={hostId}></div>
4
- <div if={showProgress && loading} class="live2d-host__progress" role="progressbar" aria-valuenow={progressPercent} aria-valuemin="0" aria-valuemax="100">
4
+ <div
5
+ if={showProgress && loading}
6
+ class="live2d-host__progress"
7
+ role="progressbar"
8
+ aria-valuenow={progressPercent}
9
+ aria-valuemin="0"
10
+ aria-valuemax="100"
11
+ >
5
12
  <span>{progressLabel} · {progressPercent}%</span>
6
13
  </div>
7
14
  </div>
8
15
  </template>
9
16
 
10
17
  <style>
11
- .live2d-host { position: relative; display: inline-block; }
12
- .live2d-host__mount { width: 100%; height: 100%; }
13
- .live2d-host__mount canvas { display: block; width: 100%; height: 100%; }
18
+ .live2d-host {
19
+ position: relative;
20
+ display: inline-block;
21
+ }
22
+ .live2d-host__mount {
23
+ width: 100%;
24
+ height: 100%;
25
+ }
26
+ .live2d-host__mount canvas {
27
+ display: block;
28
+ width: 100%;
29
+ height: 100%;
30
+ }
14
31
  .live2d-host__progress {
15
- position: absolute; inset: auto 0 0; padding: 0.35rem 0.5rem;
32
+ position: absolute;
33
+ inset: auto 0 0;
34
+ padding: 0.35rem 0.5rem;
16
35
  background: rgba(255, 255, 255, 0.92);
17
- font: 600 0.75rem/1.2 ui-monospace, monospace; color: #377ba8;
36
+ font: 600 0.75rem/1.2 ui-monospace, monospace;
37
+ color: #377ba8;
18
38
  }
19
39
  </style>
20
40
 
@@ -33,6 +53,14 @@ export default class Live2dHost {
33
53
  public showProgress = true;
34
54
  public exposeCaptureApi = false;
35
55
  public capturePresetId = '';
56
+ /** Called with Live2DRuntime after loadModel succeeds. */
57
+ public onReady = null;
58
+ /** Optional frame profile listener (caller should throttle UI). */
59
+ public onProfile = null;
60
+ /** Load / mount failure. */
61
+ public onError = null;
62
+ /** Pointer hit area string (often `drawable:N`). */
63
+ public onHit = null;
36
64
 
37
65
  hostId = `live2d-host-${++hostSeq}`;
38
66
  hostStyle = '';
@@ -40,8 +68,10 @@ export default class Live2dHost {
40
68
  progressPercent = 0;
41
69
  progressLabel = 'Loading…';
42
70
  runtime = null;
43
- _raf = 0;
44
- _lastTs = 0;
71
+ _unsubProfile = null;
72
+ _canvas = null;
73
+ _boundPointer = null;
74
+ _bootGeneration = 0;
45
75
 
46
76
  onMount() {
47
77
  this.hostStyle = `width:${this.width}px;height:${this.height}px`;
@@ -57,23 +87,48 @@ export default class Live2dHost {
57
87
  }
58
88
 
59
89
  onDestroy() {
60
- this.stopLoop();
61
- void this.runtime?.destroy?.();
90
+ this.teardownRuntime();
91
+ }
92
+
93
+ teardownRuntime() {
94
+ try {
95
+ this.runtime?.stage?.stop?.();
96
+ } catch {
97
+ /* updateMode may be manual on older callers */
98
+ }
99
+ this.detachPointer();
100
+ if (typeof this._unsubProfile === 'function') {
101
+ this._unsubProfile();
102
+ this._unsubProfile = null;
103
+ }
104
+ this.runtime?.destroy?.();
62
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;
63
114
  }
64
115
 
65
- stopLoop() {
66
- if (this._raf) cancelAnimationFrame(this._raf);
67
- this._raf = 0;
68
- this._lastTs = 0;
116
+ attachPointer(canvas) {
117
+ this.detachPointer();
118
+ this._boundPointer = (event) => this.onPointerDown(event);
119
+ canvas.addEventListener('pointerdown', this._boundPointer);
69
120
  }
70
121
 
71
- tick(ts) {
72
- if (!this.runtime) return;
73
- const dt = this._lastTs ? (ts - this._lastTs) / 1000 : 0;
74
- this._lastTs = ts;
75
- this.runtime.update(dt);
76
- 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);
77
132
  }
78
133
 
79
134
  ensureCanvas() {
@@ -87,53 +142,85 @@ export default class Live2dHost {
87
142
  canvas.style.width = `${this.width}px`;
88
143
  canvas.style.height = `${this.height}px`;
89
144
  host.appendChild(canvas);
145
+ this._canvas = canvas;
90
146
  return canvas;
91
147
  }
92
148
 
149
+ getRuntime() {
150
+ return this.runtime;
151
+ }
152
+
93
153
  async capturePng(opts = {}) {
94
154
  if (!this.runtime) throw new Error('Live2dHost: runtime not ready');
95
155
  return this.runtime.capturePng(opts);
96
156
  }
97
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
+
98
168
  async boot() {
99
169
  if (!this.model) return;
170
+ const generation = ++this._bootGeneration;
100
171
  this.loading = true;
172
+ this.progressPercent = 0;
173
+ this.progressLabel = 'loading';
101
174
  try {
102
- this.stopLoop();
103
- void this.runtime?.destroy?.();
175
+ this.teardownRuntime();
176
+ if (generation !== this._bootGeneration) return;
104
177
  const canvas = this.ensureCanvas();
105
178
  if (!canvas) return;
106
- const runtime = createLive2D({ prefer: this.prefer, updateMode: 'manual' });
107
- runtime.events.on('progress', (p) => this.onProgress(p));
179
+ const prefer = Array.isArray(this.prefer)
180
+ ? this.prefer
181
+ : ['canvas2d', 'webgl2', 'webgpu'];
182
+ const runtime = createLive2D({ prefer, updateMode: 'auto' });
108
183
  runtime.events.on('ready', () => {
184
+ if (generation !== this._bootGeneration) return;
109
185
  this.loading = false;
186
+ this.progressPercent = 100;
187
+ this.progressLabel = 'ready';
110
188
  if (this.autoplay) {
111
- this._lastTs = 0;
112
- this._raf = requestAnimationFrame((t) => this.tick(t));
189
+ try {
190
+ runtime.stage.start();
191
+ } catch {
192
+ /* already running */
193
+ }
113
194
  }
195
+ this.attachPointer(canvas);
114
196
  if (this.exposeCaptureApi) void this.publishCapture();
197
+ if (typeof this.onReady === 'function') this.onReady(runtime);
115
198
  });
116
199
  runtime.events.on('error', (p) => {
117
- this.loading = false;
118
- if (this.exposeCaptureApi && typeof window !== 'undefined') {
119
- const api = window.__LIVE2D_CAPTURE__;
120
- if (api) api.error = String(p?.error || 'load error');
121
- }
200
+ if (generation !== this._bootGeneration) return;
201
+ this.reportError(p?.error || 'load error');
122
202
  });
123
- await runtime.mount(canvas);
124
- await runtime.load(this.model);
203
+ if (typeof this.onProfile === 'function') {
204
+ this._unsubProfile = runtime.events.on('profile', (profile) => {
205
+ if (generation !== this._bootGeneration) return;
206
+ this.onProfile(profile);
207
+ });
208
+ }
209
+ runtime.mount(canvas);
210
+ this.progressPercent = 35;
211
+ await runtime.loadModel(this.model);
212
+ if (generation !== this._bootGeneration) {
213
+ runtime.destroy?.();
214
+ return;
215
+ }
125
216
  this.runtime = runtime;
126
- } catch {
127
- this.loading = false;
217
+ this.progressPercent = 90;
218
+ } catch (err) {
219
+ if (generation !== this._bootGeneration) return;
220
+ this.reportError(err);
128
221
  }
129
222
  }
130
223
 
131
- onProgress(p) {
132
- const progress = p?.progress ?? 0;
133
- this.progressLabel = p?.stage || 'loading';
134
- this.progressPercent = Math.round(Math.min(1, Math.max(0, progress)) * 100);
135
- }
136
-
137
224
  async publishCapture() {
138
225
  const api = typeof window !== 'undefined' ? window.__LIVE2D_CAPTURE__ : null;
139
226
  if (!api) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/plugin-live2d",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
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.20",
39
+ "@doki-land/live2d": "0.0.22",
40
40
  "@vmz/plugin": "^0.1.8"
41
41
  }
42
42
  }