dsh-live2dwaifu 0.1.4 → 0.2.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/README.md CHANGED
@@ -13,7 +13,15 @@
13
13
  | 展开 | 一张小卡片,上半身,底下一条气泡装台词 |
14
14
  | 收起 | 缩成一条,只剩名字和一个 ⌃ |
15
15
 
16
- 右下角有个小三角,拖它就能改卡片大小;标题栏的 **◐** 一键换背景(透明 / 深色 / 浅色 / 纸质)。窗口大小、背景、开合状态都记在本机,下次还是这样。
16
+ 按住标题栏能把她拖到屏幕任何地方,右下角那个小三角拖的是大小;标题栏的 **◐** 一键换背景(透明 / 深色 / 浅色 / 纸质)。位置、大小、背景、开合状态都记在本机,下次还是这样。
17
+
18
+ **她会看你**:鼠标在窗口里走,眼珠跟着转,脑袋也偏一点。
19
+
20
+ **能戳**:点她的头会害羞,戳身上会瞪你,点底下就应你一声。
21
+
22
+ **会自己动**:模型里带 `.motion3.json` 的,隔十几秒自己播一个动作。气泡能拖着挪地方,说完过一阵自己淡下去。
23
+
24
+ **跟着聊**:她挂在聊天流上(`conversation.chat.node` 插槽),助手每说一句她就念一句,情绪按话里的词换。
17
25
 
18
26
  ## 三十秒上手
19
27
 
@@ -10,7 +10,10 @@
10
10
  #stage { display: block; width: 100%; height: 100%; }
11
11
  #bubble { position: absolute; left: 12px; right: 12px; bottom: 10px; min-height: 30px; box-sizing: border-box;
12
12
  background: rgba(24,27,33,.94); border: 1px solid #3a4049; border-radius: 10px; padding: 7px 10px;
13
- color: #dfe4ec; font-size: 13px; line-height: 1.45; }
13
+ color: #dfe4ec; font-size: 13px; line-height: 1.45;
14
+ opacity: 0; transition: opacity .35s ease; cursor: grab; user-select: none;
15
+ max-height: 40%; overflow: hidden; touch-action: none; }
16
+ #bubble:active { cursor: grabbing; }
14
17
  #hint { position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
15
18
  text-align: center; padding: 20px; color: #98a0ad; }
16
19
  #hint.show { display: flex; }
package/assets/viewer.js CHANGED
@@ -44312,6 +44312,15 @@ void main(void)\r
44312
44312
  var exps = [];
44313
44313
  var expParams = /* @__PURE__ */ new Map();
44314
44314
  var expName = null;
44315
+ var gazeX = 0;
44316
+ var gazeY = 0;
44317
+ var pokeAt = 0;
44318
+ var sayFade = 0;
44319
+ var motionGroups = [];
44320
+ var nextMotionAt = 0;
44321
+ var POKE_HEAD = ["\u5514\u3002", "\u522B\u6478\u3002", "\u5934\u53D1\u4E71\u4E86\u3002", "\u55EF\u2026\u2026", "\u4F60\u624B\u51C9\u3002"];
44322
+ var POKE_BODY = ["\u770B\u4EC0\u4E48\u770B\u3002", "\u6233\u4EC0\u4E48\u3002", "\u54FC\u3002", "\u522B\u95F9\u3002"];
44323
+ var POKE_LOW = ["\u5728\u5462\u3002", "\u6709\u4E8B\uFF1F", "\u600E\u4E48\u4E86\u3002"];
44315
44324
  var base = {};
44316
44325
  var BASE_IDS = ["ParamMouthForm", "ParamMouthOpenY", "ParamBrowLY", "ParamBrowRY", "ParamBrowLAngle", "ParamBrowRAngle", "ParamEyeLSmile", "ParamEyeRSmile", "ParamEyeLOpen", "ParamEyeROpen", "ParamEyeBallX", "ParamEyeBallY", "ParamAngleX", "ParamAngleY", "ParamAngleZ"];
44317
44326
  for (const id of BASE_IDS) base[id] = 0;
@@ -44361,6 +44370,13 @@ void main(void)\r
44361
44370
  exps = entry.expressions || [];
44362
44371
  expParams = /* @__PURE__ */ new Map();
44363
44372
  expName = null;
44373
+ try {
44374
+ const defs = model.internalModel && model.internalModel.motionManager && model.internalModel.motionManager.definitions || {};
44375
+ motionGroups = Object.keys(defs).filter((k2) => Array.isArray(defs[k2]) && defs[k2].length);
44376
+ } catch {
44377
+ motionGroups = [];
44378
+ }
44379
+ nextMotionAt = performance.now() + 6e3;
44364
44380
  renderExps();
44365
44381
  fit();
44366
44382
  applyMood("\u5E73\u9759");
@@ -44440,13 +44456,22 @@ void main(void)\r
44440
44456
  function say(text, m2) {
44441
44457
  if (m2) applyMood(m2);
44442
44458
  const bubble = $2("bubble");
44459
+ let s2 = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
44460
+ if (s2.length > 180) s2 = s2.slice(0, 178) + "\u2026";
44443
44461
  bubble.textContent = "";
44462
+ bubble.style.opacity = "1";
44463
+ clearTimeout(sayFade);
44444
44464
  let i2 = 0;
44445
44465
  const t2 = setInterval(() => {
44446
- bubble.textContent = text.slice(0, ++i2);
44466
+ bubble.textContent = s2.slice(0, ++i2);
44447
44467
  mouth = 0.14;
44448
- if (i2 >= text.length) clearInterval(t2);
44449
- }, 65);
44468
+ if (i2 >= s2.length) {
44469
+ clearInterval(t2);
44470
+ sayFade = setTimeout(() => {
44471
+ bubble.style.opacity = "0";
44472
+ }, Math.min(14e3, 4e3 + s2.length * 110));
44473
+ }
44474
+ }, 55);
44450
44475
  }
44451
44476
  var idleIdx = 0;
44452
44477
  function idleTick() {
@@ -44483,17 +44508,33 @@ void main(void)\r
44483
44508
  }
44484
44509
  }
44485
44510
  setP("ParamBreath", (Math.sin(t2 * 1.1) + 1) / 2);
44511
+ const gx = Math.max(-1, Math.min(1, gazeX));
44512
+ const gy = Math.max(-1, Math.min(1, gazeY));
44513
+ setP("ParamEyeBallX", gx * 0.9);
44514
+ setP("ParamEyeBallY", -gy * 0.65);
44486
44515
  if (!window.__holdHead) {
44487
- setP("ParamAngleX", Math.sin(t2 * 0.43) * 4);
44488
- setP("ParamAngleY", Math.sin(t2 * 0.31 + 1) * 3);
44489
- setP("ParamBodyAngleX", Math.sin(t2 * 0.29) * 2);
44516
+ setP("ParamAngleX", Math.sin(t2 * 0.43) * 1.4 + gx * 7);
44517
+ setP("ParamAngleY", Math.sin(t2 * 0.31 + 1) * 1.2 - gy * 5);
44518
+ setP("ParamBodyAngleX", Math.sin(t2 * 0.29) * 2 + gx * 2);
44490
44519
  }
44520
+ maybeMotion();
44491
44521
  if (mouth > 0) {
44492
44522
  mouth = Math.max(0, mouth - app.ticker.deltaMS / 1e3);
44493
44523
  setP("ParamMouthOpenY", (Math.sin(t2 * 18) * 0.5 + 0.5) * 0.26 + 0.02);
44494
44524
  }
44495
44525
  core.update();
44496
44526
  });
44527
+ function maybeMotion() {
44528
+ if (!model || !motionGroups.length) return;
44529
+ const now = performance.now();
44530
+ if (now < nextMotionAt) return;
44531
+ nextMotionAt = now + 12e3 + Math.random() * 16e3;
44532
+ const g2 = motionGroups.indexOf("Idle") >= 0 ? "Idle" : motionGroups[Math.floor(Math.random() * motionGroups.length)];
44533
+ try {
44534
+ model.motion(g2, void 0, 3);
44535
+ } catch {
44536
+ }
44537
+ }
44497
44538
  window.addEventListener("resize", () => setTimeout(fit, 60));
44498
44539
  refresh(true).then(() => {
44499
44540
  if (state.models.length) setTimeout(idleTick, 2500);
@@ -44562,6 +44603,62 @@ void main(void)\r
44562
44603
  const d2 = e2.data || {};
44563
44604
  if (d2.type === "dsh-live2d/bg") applyBg(String(d2.bg || "none"));
44564
44605
  });
44606
+ window.addEventListener("pointermove", (e2) => {
44607
+ if (e2.target && e2.target.id === "bubble") return;
44608
+ gazeX = e2.clientX / Math.max(1, window.innerWidth) * 2 - 1;
44609
+ gazeY = e2.clientY / Math.max(1, window.innerHeight) * 2 - 1;
44610
+ });
44611
+ window.addEventListener("pointerleave", () => {
44612
+ gazeX = 0;
44613
+ gazeY = 0;
44614
+ });
44615
+ var pokeIdx = 0;
44616
+ $2("stage").addEventListener("pointerdown", (e2) => {
44617
+ if (!core) return;
44618
+ const now = Date.now();
44619
+ if (now - pokeAt < 800) return;
44620
+ pokeAt = now;
44621
+ const h2 = Math.max(1, app.renderer.height);
44622
+ const rel = e2.clientY / h2;
44623
+ if (rel < 0.42) say(POKE_HEAD[pokeIdx++ % POKE_HEAD.length], "\u5BB3\u7F9E");
44624
+ else if (rel < 0.75) say(POKE_BODY[pokeIdx++ % POKE_BODY.length], "\u751F\u6C14");
44625
+ else say(POKE_LOW[pokeIdx++ % POKE_LOW.length], "\u5F00\u5FC3");
44626
+ });
44627
+ (function dragBubble() {
44628
+ const b2 = $2("bubble");
44629
+ if (!b2) return;
44630
+ let bx = 0;
44631
+ let by = 0;
44632
+ try {
44633
+ const s2 = JSON.parse(localStorage.getItem("dsh-l2d-bub") || "{}");
44634
+ bx = Number(s2.x) || 0;
44635
+ by = Number(s2.y) || 0;
44636
+ b2.style.transform = "translate(" + bx + "px," + by + "px)";
44637
+ } catch {
44638
+ }
44639
+ b2.addEventListener("pointerdown", (e2) => {
44640
+ e2.preventDefault();
44641
+ const sx = e2.clientX;
44642
+ const sy = e2.clientY;
44643
+ const x0 = bx;
44644
+ const y0 = by;
44645
+ const move = (ev) => {
44646
+ bx = x0 + ev.clientX - sx;
44647
+ by = y0 + ev.clientY - sy;
44648
+ b2.style.transform = "translate(" + bx + "px," + by + "px)";
44649
+ };
44650
+ const up = () => {
44651
+ window.removeEventListener("pointermove", move);
44652
+ window.removeEventListener("pointerup", up);
44653
+ try {
44654
+ localStorage.setItem("dsh-l2d-bub", JSON.stringify({ x: Math.round(bx), y: Math.round(by) }));
44655
+ } catch {
44656
+ }
44657
+ };
44658
+ window.addEventListener("pointermove", move);
44659
+ window.addEventListener("pointerup", up);
44660
+ });
44661
+ })();
44565
44662
  })();
44566
44663
  /*! Bundled license information:
44567
44664
 
package/lib/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  ;(() => {
2
- const makeFactory = () => {
2
+ const makeFactory = (require) => {
3
3
  const react = require('react');
4
4
  const h = react.createElement;
5
5
  const name = 'dsh-live2d';
@@ -17,7 +17,7 @@
17
17
  display:flex;flex-direction:column}
18
18
  .dsh-l2d-card.collapsed{width:146px!important;height:36px!important}
19
19
  .dsh-l2d-head{display:flex;align-items:center;gap:6px;padding:8px 10px;color:#9aa3b0;font-size:11px;
20
- letter-spacing:.5px;border-bottom:1px solid #2c313a;user-select:none;flex:none}
20
+ letter-spacing:.5px;border-bottom:1px solid #2c313a;user-select:none;flex:none;cursor:move}
21
21
  .dsh-l2d-dot{width:6px;height:6px;border-radius:50%;background:#6aa9ff;flex:none}
22
22
  .dsh-l2d-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
23
23
  .dsh-l2d-btns{margin-left:auto;display:flex;gap:6px;flex:none}
@@ -46,17 +46,31 @@
46
46
  }
47
47
  }
48
48
 
49
+ function readPos() {
50
+ try {
51
+ const p = JSON.parse(localStorage.getItem('dsh-l2d-pos') || '{}');
52
+ return { x: Number(p.x) || 0, y: Number(p.y) || 0 };
53
+ } catch {
54
+ return { x: 0, y: 0 };
55
+ }
56
+ }
57
+
49
58
  function Panel() {
50
59
  const [open, setOpen] = react.useState(() => localStorage.getItem('dsh-l2d-open') !== '0');
51
60
  const [title, setTitle] = react.useState('Live2D');
52
61
  const [bg, setBg] = react.useState(() => localStorage.getItem('dsh-l2d-bg') || 'none');
53
62
  const [size, setSize] = react.useState(readSize);
63
+ const [pos, setPos] = react.useState(readPos);
54
64
  const [nonce, setNonce] = react.useState(0);
55
65
  const ref = react.useRef(null);
56
66
 
57
67
  react.useEffect(() => localStorage.setItem('dsh-l2d-open', open ? '1' : '0'), [open]);
58
68
  react.useEffect(() => localStorage.setItem('dsh-l2d-bg', bg), [bg]);
59
69
  react.useEffect(() => localStorage.setItem('dsh-l2d-size', JSON.stringify(size)), [size]);
70
+ react.useEffect(() => localStorage.setItem('dsh-l2d-pos', JSON.stringify(pos)), [pos]);
71
+ react.useEffect(() => {
72
+ frameWin = (ref.current && ref.current.contentWindow) || null;
73
+ }, [nonce, open]);
60
74
  react.useEffect(() => {
61
75
  if (ref.current && ref.current.contentWindow) {
62
76
  ref.current.contentWindow.postMessage({ type: 'dsh-live2d/bg', bg }, '*');
@@ -92,26 +106,48 @@
92
106
  window.addEventListener('pointerup', up);
93
107
  };
94
108
 
109
+ const startMove = (e) => {
110
+ if (e.button !== undefined && e.button !== 0) return;
111
+ const sx = e.clientX;
112
+ const sy = e.clientY;
113
+ const x0 = pos.x;
114
+ const y0 = pos.y;
115
+ const move = (ev) => {
116
+ const nx = x0 + (ev.clientX - sx);
117
+ const ny = y0 + (ev.clientY - sy);
118
+ setPos({ x: Math.round(nx), y: Math.round(ny) });
119
+ };
120
+ const up = () => {
121
+ window.removeEventListener('pointermove', move);
122
+ window.removeEventListener('pointerup', up);
123
+ };
124
+ window.addEventListener('pointermove', move);
125
+ window.addEventListener('pointerup', up);
126
+ };
127
+ const noDrag = (e) => e.stopPropagation();
128
+
95
129
  const manage = () => ref.current && ref.current.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
96
130
  const cycleBg = () => setBg(BGS[(BGS.indexOf(bg) + 1) % BGS.length]);
97
131
 
98
- const cardStyle = open ? { width: size.w + 'px', height: size.h + 'px' } : {};
132
+ const cardStyle = open
133
+ ? { width: size.w + 'px', height: size.h + 'px', transform: 'translate(' + pos.x + 'px,' + pos.y + 'px)' }
134
+ : { transform: 'translate(' + pos.x + 'px,' + pos.y + 'px)' };
99
135
 
100
136
  return h(
101
137
  'div',
102
138
  { className: 'dsh-l2d-card' + (open ? '' : ' collapsed'), style: cardStyle },
103
139
  h(
104
140
  'div',
105
- { className: 'dsh-l2d-head' },
141
+ { className: 'dsh-l2d-head', onPointerDown: startMove },
106
142
  h('span', { className: 'dsh-l2d-dot' }),
107
143
  h('span', { className: 'dsh-l2d-name' }, title),
108
144
  h(
109
145
  'span',
110
146
  { className: 'dsh-l2d-btns' },
111
- open && h('button', { title: '模型管理', onClick: manage }, '⚙'),
112
- open && h('button', { title: '背景:' + BG_LABEL[bg], onClick: cycleBg }, '◐'),
113
- open && h('button', { title: '刷新', onClick: () => setNonce((n) => n + 1) }, '↻'),
114
- h('button', { title: open ? '收起' : '展开', onClick: () => setOpen(!open) }, open ? '⌄' : '⌃'),
147
+ open && h('button', { title: '模型管理', onPointerDown: noDrag, onClick: manage }, '⚙'),
148
+ open && h('button', { title: '背景:' + BG_LABEL[bg], onPointerDown: noDrag, onClick: cycleBg }, '◐'),
149
+ open && h('button', { title: '刷新', onPointerDown: noDrag, onClick: () => setNonce((n) => n + 1) }, '↻'),
150
+ h('button', { title: open ? '收起' : '展开', onPointerDown: noDrag, onClick: () => setOpen(!open) }, open ? '⌄' : '⌃'),
115
151
  ),
116
152
  ),
117
153
  open
@@ -121,21 +157,89 @@
121
157
  className: 'dsh-l2d-body',
122
158
  src: BASE + '/viewer?embed=1&bg=' + encodeURIComponent(bg),
123
159
  allow: 'autoplay',
160
+ onLoad: () => {
161
+ frameWin = (ref.current && ref.current.contentWindow) || null;
162
+ },
124
163
  })
125
164
  : null,
126
165
  open ? h('div', { className: 'dsh-l2d-grip', onPointerDown: startDrag, title: '拖动改大小' }) : null,
127
166
  );
128
167
  }
129
168
 
169
+ let frameWin = null;
170
+ let lastSaid = '';
171
+ let lastSaidAt = 0;
172
+
173
+ const MOOD_HINTS = [
174
+ ['生气', ['气', '烦', '讨厌', '凭什么']],
175
+ ['委屈', ['委屈', '难过', '对不起', '哭']],
176
+ ['害羞', ['唔', '别看', '害羞', '脸红']],
177
+ ['开心', ['谢谢', '喜欢', '开心', '好耶']],
178
+ ['困', ['困', '睡', '熬夜']],
179
+ ];
180
+
181
+ function guessMood(text) {
182
+ for (let i = 0; i < MOOD_HINTS.length; i++) {
183
+ const words = MOOD_HINTS[i][1];
184
+ for (let j = 0; j < words.length; j++) {
185
+ if (text.indexOf(words[j]) >= 0) return MOOD_HINTS[i][0];
186
+ }
187
+ }
188
+ return '平静';
189
+ }
190
+
191
+ function pushSay(text) {
192
+ const t = String(text || '').replace(/\s+/g, ' ').trim();
193
+ if (!t || t === lastSaid) return;
194
+ if (Date.now() - lastSaidAt < 700) return;
195
+ lastSaid = t;
196
+ lastSaidAt = Date.now();
197
+ if (!frameWin) return;
198
+ try {
199
+ frameWin.postMessage({ type: 'dsh-live2d/say', text: t, mood: guessMood(t) }, '*');
200
+ } catch {}
201
+ }
202
+
203
+ function ChatProbe(props) {
204
+ const anchor = react.useRef(null);
205
+ const role =
206
+ (props && (props.role || props.kind)) ||
207
+ (props && props.node && (props.node.role || props.node.kind)) ||
208
+ (props && props.message && props.message.role) ||
209
+ '';
210
+ const skip = /user|human|prompt/i.test(String(role));
211
+ react.useEffect(() => {
212
+ if (skip) return;
213
+ const el = anchor.current;
214
+ if (!el) return;
215
+ let node = el.parentElement;
216
+ let text = '';
217
+ for (let i = 0; i < 5 && node; i++) {
218
+ const t = (node.textContent || '').trim();
219
+ if (t.length > 3 && t.length < 900) {
220
+ text = t;
221
+ break;
222
+ }
223
+ node = node.parentElement;
224
+ }
225
+ if (text) pushSay(text);
226
+ });
227
+ if (skip) return null;
228
+ return h('i', { ref: anchor, style: { display: 'none' } });
229
+ }
230
+
130
231
  function apply(ctx) {
131
232
  ensureCss();
132
233
  ctx.slots.inject('shell.overlay', function* () {
133
234
  yield ctx.slots.register({ name: 'shell.overlay', id: 'live2d', order: 900 }, Panel);
134
235
  });
236
+ ctx.slots.inject('conversation.chat.node', function* () {
237
+ yield ctx.slots.register({ name: 'conversation.chat.node', id: 'live2d-say', order: 900 }, ChatProbe);
238
+ });
135
239
  }
136
240
 
137
241
  return { apply, inject, name };
138
242
  };
139
243
 
140
- window.__ModuleLoader__.load({ id: 'dsh-live2d', factory: makeFactory() });
244
+ window.__ModuleLoader__.load({ id: 'dsh-live2dwaifu', factory: makeFactory });
141
245
  })();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-live2dwaifu",
3
- "version": "0.1.4",
4
- "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情、窗口大小可拖、背景可换。",
3
+ "version": "0.2.0",
4
+ "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {