koishi-plugin-vsinger-gacha 1.3.0 → 1.4.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
@@ -62,7 +62,10 @@ npm i @napi-rs/canvas
62
62
  - 想一次性缓存全部:`npm run fetch-assets -- --dir data/vsinger-gacha/cards`
63
63
  - 要动态卡 mp4(1.1GB):下载 Release 的 `assets-videos.tar` 解压到 `cards/`
64
64
 
65
- ## 统一渲染系统
65
+ ## 统一渲染系统(浅色玻璃手账风)
66
+
67
+ 所有图片统一走 `lib/render/`:插画背景(`assets/wallpapers/`)+ 白色玻璃面板 + 蓝色强调 + 档位描边卡牌。
68
+ 把喜欢的插画丢进 `assets/wallpapers/` 即可随机当背景;留空则用纯浅色底。
66
69
 
67
70
  所有图片(抽卡结果、图鉴、以后新增的图)都走同一套设计令牌与组件:
68
71
 
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/lib/handbook.js CHANGED
@@ -10,6 +10,9 @@ const path = require('node:path');
10
10
 
11
11
  const FONT_FILE = require('node:path').join(__dirname, '..', 'assets', 'font.ttf');
12
12
  const kit = require('./render/kit');
13
+ const theme = require('./render/theme');
14
+ const COLOR = theme.COLOR;
15
+ const ctx_fs = require('node:fs');
13
16
 
14
17
  let Canvas = null;
15
18
  /** 按需加载画布(没装也不影响抽卡/积分) */
@@ -37,7 +40,7 @@ const TH = 200;
37
40
  const LABEL_H = 62;
38
41
  const GAP = 12;
39
42
  const PAD = 24;
40
- const HEAD = 100;
43
+ const HEAD = 128;
41
44
  const POOL_TITLE_H = 34;
42
45
  const SERIES_H = 44;
43
46
  const COLS_PER_COL = 8;
@@ -106,7 +109,7 @@ async function ensureThumbs(game) {
106
109
  return dir;
107
110
  }
108
111
 
109
- async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDir }) {
112
+ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDir, wallpaperDir }) {
110
113
  const Canvas = getCanvas();
111
114
 
112
115
  // ★ 优先用包内自带缩略图(assets/thumbs,423 张约 4.7MB),缺失才现场生成
@@ -200,28 +203,26 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
200
203
  const canvas = Canvas.createCanvas(W, H);
201
204
  const g = canvas.getContext('2d');
202
205
 
203
- // 背景:液态玻璃(渐变 + 光晕 + 噪点)
204
- const bg = g.createLinearGradient(0, 0, W, H);
205
- bg.addColorStop(0, '#101a2e');
206
- bg.addColorStop(0.45, '#16304f');
207
- bg.addColorStop(1, '#0e1626');
208
- g.fillStyle = bg;
209
- g.fillRect(0, 0, W, H);
210
- const glow = g.createRadialGradient(W * 0.2, H * 0.1, 10, W * 0.2, H * 0.1, Math.max(W, H) * 0.8);
211
- glow.addColorStop(0, 'rgba(120,180,255,0.35)');
212
- glow.addColorStop(1, 'rgba(120,180,255,0)');
213
- g.fillStyle = glow;
214
- g.fillRect(0, 0, W, H);
206
+ // 背景:插画 + 白蒙层(统一渲染系统)
207
+ const wall = wallpaperDir ? (() => {
208
+ try {
209
+ const list = ctx_fs.readdirSync(wallpaperDir).filter((f) => /\.(jpe?g|png|webp)$/i.test(f));
210
+ return list.length ? require('node:path').join(wallpaperDir, list[Math.floor(Math.random() * list.length)]) : null;
211
+ } catch { return null; }
212
+ })() : null;
213
+ await kit.background(g, Canvas, W, H, wall);
215
214
 
216
- // 标题
217
- g.fillStyle = '#ffffff';
218
- g.font = 'bold 44px VSingerSans, sans-serif';
219
- g.fillText('VSinger 收藏集 · 我的图鉴', PAD, 56);
215
+ // 标题(统一组件)
220
216
  const gotN = Object.keys(owned).length;
221
217
  const dupN = Object.values(owned).reduce((a, b) => a + Math.max(0, b - 1), 0);
222
- g.font = '22px VSingerSans, sans-serif';
223
- g.fillStyle = 'rgba(235,240,250,0.85)';
224
- g.fillText(`${displayName} 已收集 ${gotN}/${game.cards.length} 重复 ${dupN} 张`, PAD, 88);
218
+ kit.header(g, {
219
+ x: PAD, y: 34, W: W - PAD * 2,
220
+ title: 'VSinger 收藏集 · 我的图鉴',
221
+ subtitle: `${displayName} 已收集 ${gotN}/${game.cards.length} 重复 ${dupN} 张`,
222
+ badge: `${gotN}/${game.cards.length}`,
223
+ badgeSub: '收集进度',
224
+ progress: game.cards.length ? gotN / game.cards.length : 0,
225
+ });
225
226
 
226
227
  for (let ci = 0; ci < part.length; ci++) {
227
228
  const grp = part[ci];
@@ -230,20 +231,24 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
230
231
  let prevAct = null;
231
232
  for (const blk of grp) {
232
233
  if (blk.act !== prevAct) {
233
- g.fillStyle = SERIES_COLOR[blk.act] || '#3c5a96';
234
- g.fillRect(x0, y, COLW, SERIES_H - 8);
235
- g.fillStyle = '#fff';
236
- g.font = 'bold 22px VSingerSans, sans-serif';
237
- g.fillText(blk.act, x0 + 12, y + 32);
234
+ kit.panel(g, x0, y, COLW, SERIES_H - 8, { radius: 14, fill: 'rgba(255,255,255,0.88)' });
235
+ g.fillStyle = COLOR.accent;
236
+ kit.roundRect(g, x0 + 12, y + 12, 5, SERIES_H - 32, 3);
237
+ g.fill();
238
+ g.fillStyle = COLOR.text;
239
+ g.font = 'bold 21px VSingerSans, sans-serif';
240
+ g.fillText(blk.act, x0 + 26, y + 22);
238
241
  y += SERIES_H;
239
242
  prevAct = blk.act;
240
243
  }
241
244
  // 池标题(玻璃条)
242
- g.fillStyle = 'rgba(8,14,30,0.85)';
243
- g.fillRect(x0, y, COLW, POOL_TITLE_H - 10);
244
- g.fillStyle = '#ffd682';
245
- g.font = 'bold 22px VSingerSans, sans-serif';
246
- g.fillText(`${blk.pool} ${blk.got}/${blk.arr.length}`, x0 + 8, y + 24);
245
+ kit.panel(g, x0, y, COLW, POOL_TITLE_H - 10, { radius: 12, fill: 'rgba(255,255,255,0.78)' });
246
+ g.fillStyle = COLOR.accent;
247
+ g.font = 'bold 21px VSingerSans, sans-serif';
248
+ g.fillText(`${blk.pool}`, x0 + 12, y + 12);
249
+ g.fillStyle = COLOR.textSub;
250
+ g.font = '18px VSingerSans, sans-serif';
251
+ g.fillText(`${blk.got}/${blk.arr.length}`, x0 + COLW - 82, y + 14);
247
252
 
248
253
  for (let i = 0; i < blk.arr.length; i++) {
249
254
  const c = blk.arr[i];
package/lib/index.js CHANGED
@@ -368,6 +368,11 @@ function apply(ctx, config) {
368
368
  title,
369
369
  outDir: path.join(dataDir, 'out'),
370
370
  tagsDir,
371
+ wallpaperDir: config.wallpaperDir
372
+ ? config.wallpaperDir
373
+ : path.join(__dirname, '..', 'assets', 'wallpapers'),
374
+ badge: String(Math.round(Number(store.pu(uid).points || 0))),
375
+ badgeSub: '当前积分',
371
376
  thumbsDir: path.join(assetsDir, 'thumbs'),
372
377
  });
373
378
  await session.send(fileEl(out, 'image/jpeg', config.imageMode));
package/lib/render/kit.js CHANGED
@@ -1,12 +1,13 @@
1
1
  'use strict';
2
- /** 渲染基础组件:背景 / 面板 / 卡片 / 角标 / 标题 / 文本 */
2
+ /** 渲染基础组件:浅色玻璃拟态 */
3
+ const path = require('node:path');
4
+ const fs = require('node:fs');
3
5
  const { FONT, COLOR, TIER, RADIUS, SPACE, FONT_SIZE, tierOf } = require('./theme');
4
6
 
5
7
  function roundRect(g, x, y, w, h, r) {
6
8
  g.beginPath();
7
- if (g.roundRect) {
8
- g.roundRect(x, y, w, h, r);
9
- } else {
9
+ if (g.roundRect) g.roundRect(x, y, w, h, r);
10
+ else {
10
11
  g.moveTo(x + r, y);
11
12
  g.arcTo(x + w, y, x + w, y + h, r);
12
13
  g.arcTo(x + w, y + h, x, y + h, r);
@@ -17,53 +18,109 @@ function roundRect(g, x, y, w, h, r) {
17
18
  return g;
18
19
  }
19
20
 
20
- /** 背景:深蓝渐变 + 顶部柔光 + 底部压暗 */
21
- function background(g, W, H) {
22
- const bg = g.createLinearGradient(0, 0, W * 0.6, H);
23
- bg.addColorStop(0, COLOR.bg2);
24
- bg.addColorStop(0.55, COLOR.bg1);
25
- bg.addColorStop(1, COLOR.bg0);
26
- g.fillStyle = bg;
21
+ /** 背景:插画铺满 + 模糊 + 白色蒙层 */
22
+ async function background(g, Canvas, W, H, wallpaper) {
23
+ g.fillStyle = '#eef3fa';
27
24
  g.fillRect(0, 0, W, H);
28
25
 
29
- const glow = g.createRadialGradient(W * 0.25, 0, 10, W * 0.25, 0, Math.max(W, H) * 0.75);
30
- glow.addColorStop(0, COLOR.glow);
31
- glow.addColorStop(1, 'rgba(120,180,255,0)');
32
- g.fillStyle = glow;
26
+ if (wallpaper && fs.existsSync(wallpaper)) {
27
+ try {
28
+ const img = await Canvas.loadImage(wallpaper);
29
+ // 先用小尺寸绘制再放大 → 廉价模糊
30
+ const SW = Math.max(24, Math.round(W / 18));
31
+ const SH = Math.round(SW * (H / W));
32
+ const small = Canvas.createCanvas(SW, SH);
33
+ const sg = small.getContext('2d');
34
+ sg.drawImage(img, 0, 0, SW, SH);
35
+ g.save();
36
+ g.globalAlpha = 0.72;
37
+ g.drawImage(small, 0, 0, W, H);
38
+ g.restore();
39
+ } catch { /* 忽略 */ }
40
+ }
41
+
42
+ // 白色蒙层(左上更亮,保证文字清晰)
43
+ const grad = g.createLinearGradient(0, 0, W, H);
44
+ grad.addColorStop(0, 'rgba(255,255,255,0.82)');
45
+ grad.addColorStop(0.55, 'rgba(255,255,255,0.68)');
46
+ grad.addColorStop(1, 'rgba(235,244,255,0.78)');
47
+ g.fillStyle = grad;
33
48
  g.fillRect(0, 0, W, H);
34
49
  }
35
50
 
36
- /** 毛玻璃面板 */
51
+ /** 白玻璃面板 */
37
52
  function panel(g, x, y, w, h, opts = {}) {
38
53
  const r = opts.radius == null ? RADIUS.lg : opts.radius;
39
54
  g.save();
55
+ g.shadowColor = COLOR.shadow;
56
+ g.shadowBlur = 18;
57
+ g.shadowOffsetY = 4;
40
58
  roundRect(g, x, y, w, h, r);
41
59
  g.fillStyle = opts.fill || COLOR.panel;
42
60
  g.fill();
61
+ g.restore();
62
+ g.save();
63
+ roundRect(g, x, y, w, h, r);
43
64
  g.strokeStyle = opts.stroke || COLOR.panelBorder;
44
- g.lineWidth = opts.lineWidth || 1;
65
+ g.lineWidth = 1.5;
45
66
  g.stroke();
46
67
  g.restore();
47
68
  }
48
69
 
49
- /** 标题区:主标题 + 副标题 + 强调短线 */
50
- function header(g, x, y, W, title, subtitle) {
51
- g.textBaseline = 'top';
70
+ /**
71
+ * 头部(仿手账卡):图标 + 标题 + 副标题 + 右侧数字徽章 + 进度条
72
+ */
73
+ function header(g, o) {
74
+ const x = o.x;
75
+ const y = o.y;
76
+ const W = o.W;
77
+
78
+ // 圆形图标
79
+ const R = 22;
80
+ g.save();
81
+ g.beginPath();
82
+ g.arc(x + R, y + R + 4, R, 0, Math.PI * 2);
52
83
  g.fillStyle = COLOR.accent;
53
- roundRect(g, x, y + 4, 6, 34, 3);
54
84
  g.fill();
85
+ g.fillStyle = '#fff';
86
+ g.font = `bold 22px ${FONT}`;
87
+ g.textBaseline = 'middle';
88
+ g.fillText('♪', x + R - 8, y + R + 4);
89
+ g.restore();
55
90
 
91
+ g.textBaseline = 'top';
56
92
  g.fillStyle = COLOR.text;
57
93
  g.font = `bold ${FONT_SIZE.h1}px ${FONT}`;
58
- const maxW = W - 40;
59
- let t = String(title || '');
60
- while (t.length > 2 && g.measureText(t).width > maxW) t = t.slice(0, -1);
61
- g.fillText(t.length < String(title).length ? t.slice(0, -1) + '…' : t, x + 16, y);
94
+ g.fillText(fit(g, o.title, `bold ${FONT_SIZE.h1}px ${FONT}`, W - 260), x + R * 2 + 14, y);
62
95
 
63
- if (subtitle) {
96
+ if (o.subtitle) {
64
97
  g.fillStyle = COLOR.textSub;
65
98
  g.font = `${FONT_SIZE.body}px ${FONT}`;
66
- g.fillText(String(subtitle), x + 16, y + 42);
99
+ g.fillText(o.subtitle, x + R * 2 + 14, y + 40);
100
+ }
101
+
102
+ // 右侧徽章
103
+ if (o.badge != null) {
104
+ const bw = 118;
105
+ const bh = 58;
106
+ const bx = x + W - bw;
107
+ panel(g, bx, y - 4, bw, bh, { radius: RADIUS.md, fill: 'rgba(255,255,255,0.95)' });
108
+ g.fillStyle = COLOR.accent;
109
+ g.font = `bold 26px ${FONT}`;
110
+ g.fillText(String(o.badge), bx + 14, y + 6);
111
+ g.fillStyle = COLOR.textSub;
112
+ g.font = `12px ${FONT}`;
113
+ g.fillText(String(o.badgeSub || ''), bx + 14, y + 36);
114
+ // 进度条
115
+ const pct = Math.max(0, Math.min(1, Number(o.progress || 0)));
116
+ g.fillStyle = 'rgba(74,144,217,0.15)';
117
+ roundRect(g, bx + 10, y + bh - 6, bw - 20, 5, 3);
118
+ g.fill();
119
+ if (pct > 0) {
120
+ g.fillStyle = COLOR.accent;
121
+ roundRect(g, bx + 10, y + bh - 6, Math.max(4, (bw - 20) * pct), 5, 3);
122
+ g.fill();
123
+ }
67
124
  }
68
125
  }
69
126
 
@@ -77,10 +134,7 @@ function fit(g, text, font, maxW) {
77
134
  return t + '…';
78
135
  }
79
136
 
80
- /**
81
- * 卡牌组件(图鉴 / 结果图共用)
82
- * @param {object} o {img, name, rate, tier, laser, count, x, y, w, h, owned}
83
- */
137
+ /** 卡牌组件 */
84
138
  async function card(g, Canvas, o) {
85
139
  const w = o.w || 170;
86
140
  const h = o.h || Math.round(w * 1.43);
@@ -88,44 +142,38 @@ async function card(g, Canvas, o) {
88
142
  const tier = o.tier == null ? tierOf({ card_scarcity: o.scarcity }) : o.tier;
89
143
  const tc = (TIER[tier] || TIER[10]).color;
90
144
 
91
- // 底 + 图
92
145
  g.save();
93
146
  roundRect(g, o.x, o.y, w, h, r);
94
147
  g.clip();
95
148
  g.fillStyle = COLOR.cardBg;
96
149
  g.fillRect(o.x, o.y, w, h);
97
150
  if (o.img) {
98
- try {
99
- g.drawImage(o.img, o.x, o.y, w, h);
100
- } catch { /* 忽略 */ }
151
+ try { g.drawImage(o.img, o.x, o.y, w, h); } catch { /* 忽略 */ }
101
152
  }
102
153
  if (o.owned === false) {
103
- g.fillStyle = 'rgba(0,0,0,0.42)';
154
+ g.fillStyle = 'rgba(255,255,255,0.38)';
104
155
  g.fillRect(o.x, o.y, w, h);
105
156
  }
106
157
  g.restore();
107
158
 
108
- // 边框(档位色 + 拥有加粗)
109
159
  g.save();
110
160
  roundRect(g, o.x + 1, o.y + 1, w - 2, h - 2, r - 1);
111
- g.strokeStyle = o.owned === false ? 'rgba(160,166,180,0.55)' : tc;
161
+ g.strokeStyle = o.owned === false ? 'rgba(150,165,185,0.45)' : tc;
112
162
  g.lineWidth = o.owned === false ? 1.5 : 2.5;
113
163
  g.stroke();
114
164
  g.restore();
115
165
 
116
- // 档位角标
117
- const tagFile = o.tagsDir ? require('node:path').join(o.tagsDir, (TIER[tier] || {}).tag || '') : '';
118
- if ((TIER[tier] || {}).tag && o.tagsDir && require('node:fs').existsSync(tagFile)) {
166
+ const tagFile = o.tagsDir ? path.join(o.tagsDir, (TIER[tier] || {}).tag || '') : '';
167
+ if ((TIER[tier] || {}).tag && o.tagsDir && fs.existsSync(tagFile)) {
119
168
  try {
120
169
  const ti = await Canvas.loadImage(tagFile);
121
170
  const tw = w * 0.46;
122
171
  g.drawImage(ti, o.x + 4, o.y + 4, tw, (ti.height / ti.width) * tw);
123
172
  } catch { /* 忽略 */ }
124
173
  }
125
- // 镭射标
126
174
  if (o.laser && o.tagsDir) {
127
- const holo = require('node:path').join(o.tagsDir, 'tag_holo.png');
128
- if (require('node:fs').existsSync(holo)) {
175
+ const holo = path.join(o.tagsDir, 'tag_holo.png');
176
+ if (fs.existsSync(holo)) {
129
177
  try {
130
178
  const hi = await Canvas.loadImage(holo);
131
179
  const hw = w * 0.36;
@@ -134,9 +182,8 @@ async function card(g, Canvas, o) {
134
182
  } catch { /* 忽略 */ }
135
183
  }
136
184
  }
137
- // 重复次数
138
185
  if (o.count > 1) {
139
- g.fillStyle = 'rgba(0,0,0,0.72)';
186
+ g.fillStyle = 'rgba(31,42,58,0.82)';
140
187
  roundRect(g, o.x + w - 46, o.y + 6, 40, 22, 8);
141
188
  g.fill();
142
189
  g.fillStyle = '#8cffaa';
@@ -144,7 +191,6 @@ async function card(g, Canvas, o) {
144
191
  g.fillText(`×${o.count}`, o.x + w - 40, o.y + 9);
145
192
  }
146
193
 
147
- // 名称 / 档位概率
148
194
  g.fillStyle = o.owned === false ? COLOR.textDim : COLOR.text;
149
195
  g.font = `bold ${o.nameSize || FONT_SIZE.h3}px ${FONT}`;
150
196
  g.fillText(fit(g, o.name, `bold ${o.nameSize || FONT_SIZE.h3}px ${FONT}`, w), o.x, o.y + h + 8);
@@ -1,20 +1,18 @@
1
1
  'use strict';
2
- /** 抽卡结果布局(统一渲染系统) */
2
+ /** 抽卡结果布局:浅色玻璃手账风 */
3
3
  const path = require('node:path');
4
4
  const fs = require('node:fs');
5
5
  const theme = require('./theme');
6
6
  const kit = require('./kit');
7
7
 
8
- /**
9
- * @param {object} o
10
- * @param {object} o.Canvas @napi-rs/canvas
11
- * @param {Array} o.results [{hash|h, card, laser}]
12
- * @param {string} o.title
13
- * @param {string} o.outDir
14
- * @param {string} o.tagsDir
15
- * @param {string} o.thumbsDir
16
- * @param {number} [o.thumbW]
17
- */
8
+ function pickWallpaper(dir) {
9
+ try {
10
+ const files = fs.readdirSync(dir).filter((f) => /\.(jpe?g|png|webp)$/i.test(f));
11
+ if (!files.length) return null;
12
+ return path.join(dir, files[Math.floor(Math.random() * files.length)]);
13
+ } catch { return null; }
14
+ }
15
+
18
16
  async function render(o) {
19
17
  const { Canvas, tagsDir, thumbsDir, outDir } = o;
20
18
  const results = o.results || [];
@@ -26,18 +24,38 @@ async function render(o) {
26
24
  const label = theme.CARD.label;
27
25
  const gap = theme.CARD.gap;
28
26
  const pad = theme.SPACE.xl;
29
- const head = 120;
27
+ const head = 132;
28
+ const panelPad = 20;
30
29
 
31
- const W = Math.max(560, pad * 2 + perRow * cw + (perRow - 1) * gap);
32
- const H = head + rows * (ch + label + gap) + pad;
30
+ const W = Math.max(640, pad * 2 + panelPad * 2 + perRow * cw + (perRow - 1) * gap);
31
+ const H = head + rows * (ch + label + gap) + panelPad * 2 + pad;
33
32
 
34
33
  const canvas = Canvas.createCanvas(W, H);
35
34
  const g = canvas.getContext('2d');
36
- kit.background(g, W, H);
37
- kit.header(g, pad - 16, 30, W, o.title, `共 ${n} 张 | 抽卡结果`);
35
+
36
+ const wall = o.wallpaperDir ? pickWallpaper(o.wallpaperDir) : null;
37
+ await kit.background(g, Canvas, W, H, wall);
38
+
39
+ kit.header(g, {
40
+ x: pad,
41
+ y: 28,
42
+ W: W - pad * 2,
43
+ title: o.title,
44
+ subtitle: `共 ${n} 张 | 抽卡结果`,
45
+ badge: o.badge != null ? o.badge : '',
46
+ badgeSub: o.badgeSub || '',
47
+ progress: o.progress || 0,
48
+ });
49
+
50
+ // 白色玻璃大面板
51
+ const px = pad;
52
+ const py = head - 12;
53
+ const pw = W - pad * 2;
54
+ const ph = rows * (ch + label + gap) + panelPad * 2 - gap;
55
+ kit.panel(g, px, py, pw, ph, { radius: theme.RADIUS.xl, fill: 'rgba(255,255,255,0.72)' });
38
56
 
39
57
  const rowW = perRow * cw + (perRow - 1) * gap;
40
- const x0 = Math.round((W - rowW) / 2);
58
+ const x0 = px + Math.round((pw - rowW) / 2);
41
59
 
42
60
  for (let i = 0; i < results.length; i++) {
43
61
  const r = results[i];
@@ -45,7 +63,7 @@ async function render(o) {
45
63
  const col = i % perRow;
46
64
  const row = Math.floor(i / perRow);
47
65
  const x = x0 + col * (cw + gap);
48
- const y = head + row * (ch + label + gap);
66
+ const y = py + panelPad + row * (ch + label + gap);
49
67
 
50
68
  let img = null;
51
69
  const f = hash ? path.join(thumbsDir, hash + '.jpg') : '';
@@ -61,12 +79,12 @@ async function render(o) {
61
79
  laser: Boolean(r.laser),
62
80
  tagsDir,
63
81
  owned: true,
64
- nameSize: cw >= 260 ? 22 : 18,
65
- metaSize: cw >= 260 ? 17 : 14,
82
+ nameSize: cw >= 260 ? 22 : 17,
83
+ metaSize: cw >= 260 ? 17 : 13,
66
84
  });
67
85
  }
68
86
 
69
- kit.footer(g, W, H, 'VSinger 收藏集 · 抽卡结果');
87
+ kit.footer(g, W, H, 'VSinger 收藏集 · 天依抽卡手账');
70
88
 
71
89
  fs.mkdirSync(outDir, { recursive: true });
72
90
  const out = path.join(outDir, `draw_${Date.now()}.jpg`);
@@ -1,35 +1,34 @@
1
1
  'use strict';
2
- /** 设计令牌:所有渲染图共用这一套 */
2
+ /** 设计令牌:浅色玻璃拟态(仿星轨手账) */
3
3
  const FONT = 'VSingerSans, sans-serif';
4
4
 
5
5
  const COLOR = {
6
- bg0: '#0d1524',
7
- bg1: '#16263f',
8
- bg2: '#111c33',
9
- glow: 'rgba(120,180,255,0.28)',
10
- text: '#ffffff',
11
- textSub: 'rgba(200,215,240,0.72)',
12
- textDim: 'rgba(200,207,220,0.55)',
13
- panel: 'rgba(16,24,40,0.72)',
14
- panelBorder: 'rgba(255,255,255,0.08)',
15
- accent: '#66ccff',
16
- accentSoft: 'rgba(102,204,255,0.18)',
17
- cardBg: '#1b2436',
18
- cardBorder: 'rgba(255,255,255,0.10)',
6
+ // 浅色系
7
+ paper: '#ffffff',
8
+ text: '#1f2a3a',
9
+ textSub: '#5d6b80',
10
+ textDim: '#8b98ab',
11
+ accent: '#4a90d9',
12
+ accentSoft: 'rgba(74,144,217,0.12)',
13
+ panel: 'rgba(255,255,255,0.82)',
14
+ panelBorder: 'rgba(120,160,210,0.28)',
15
+ cardBg: '#f2f5fa',
16
+ cardBorder: 'rgba(120,150,190,0.25)',
17
+ shadow: 'rgba(60,90,130,0.18)',
18
+ // 深色兜底(保留给旧主题)
19
+ bg0: '#0d1524', bg1: '#16263f', bg2: '#111c33', glow: 'rgba(120,180,255,0.28)',
19
20
  };
20
21
 
21
22
  const TIER = {
22
- 40: { name: '大隐藏', color: '#ff6d84', tag: 'tag_40.webp' },
23
- 30: { name: '小隐藏', color: '#ffb44d', tag: 'tag_30.webp' },
24
- 20: { name: '稀有', color: '#8fc0ff', tag: null },
25
- 10: { name: '普通', color: '#c3cad8', tag: null },
23
+ 40: { name: '大隐藏', color: '#e0607a', tag: 'tag_40.webp' },
24
+ 30: { name: '小隐藏', color: '#e09a3c', tag: 'tag_30.webp' },
25
+ 20: { name: '稀有', color: '#4a90d9', tag: null },
26
+ 10: { name: '普通', color: '#8b98ab', tag: null },
26
27
  };
27
28
 
28
- const RADIUS = { sm: 8, md: 12, lg: 16, xl: 22 };
29
+ const RADIUS = { sm: 10, md: 14, lg: 20, xl: 28 };
29
30
  const SPACE = { xs: 8, sm: 12, md: 16, lg: 24, xl: 32 };
30
- const FONT_SIZE = { h1: 32, h2: 24, h3: 19, body: 16, meta: 14, tiny: 12 };
31
-
32
- /** 卡片几何(图鉴与结果图共用) */
31
+ const FONT_SIZE = { h1: 30, h2: 24, h3: 18, body: 16, meta: 14, tiny: 12 };
33
32
  const CARD = { w: 170, h: 243, label: 58, gap: 16 };
34
33
 
35
34
  function tierOf(card) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-vsinger-gacha",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "VSinger 收藏集抽卡 / 签到积分 / 图鉴(Koishi 插件,一次性安装)",
5
5
  "main": "lib/index.js",
6
6
  "files": [