koishi-plugin-vsinger-gacha 1.3.0 → 1.4.1

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),缺失才现场生成
@@ -131,12 +134,17 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
131
134
 
132
135
  // 每池一块(顺序填充)
133
136
  const blocks = [];
137
+ let _lastAct = null;
134
138
  for (const [act, pm] of seriesMap) {
135
139
  for (const [pool, arr] of pm) {
136
140
  arr.sort((a, b) => Number(b.v.rate_pct || 0) - Number(a.v.rate_pct || 0));
137
141
  const rows = Math.max(1, Math.ceil(arr.length / COLS_PER_COL));
138
142
  const got = arr.filter((c) => (owned[c.h] || 0) > 0).length;
139
- blocks.push({ act, pool, arr, got, h: POOL_TITLE_H + rows * ROWH + 12 });
143
+ const isNewSeries = act !== _lastAct;
144
+ // ★ 2026-11-05:系列头也要占高度,否则下一块会压到上一块的卡片上
145
+ const h = POOL_TITLE_H + rows * ROWH + 12 + (isNewSeries ? SERIES_H : 0);
146
+ blocks.push({ act, pool, arr, got, series: isNewSeries, h });
147
+ _lastAct = act;
140
148
  }
141
149
  }
142
150
  const totalH = blocks.reduce((a, b) => a + b.h, 0);
@@ -200,28 +208,26 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
200
208
  const canvas = Canvas.createCanvas(W, H);
201
209
  const g = canvas.getContext('2d');
202
210
 
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);
211
+ // 背景:插画 + 白蒙层(统一渲染系统)
212
+ const wall = wallpaperDir ? (() => {
213
+ try {
214
+ const list = ctx_fs.readdirSync(wallpaperDir).filter((f) => /\.(jpe?g|png|webp)$/i.test(f));
215
+ return list.length ? require('node:path').join(wallpaperDir, list[Math.floor(Math.random() * list.length)]) : null;
216
+ } catch { return null; }
217
+ })() : null;
218
+ await kit.background(g, Canvas, W, H, wall);
215
219
 
216
- // 标题
217
- g.fillStyle = '#ffffff';
218
- g.font = 'bold 44px VSingerSans, sans-serif';
219
- g.fillText('VSinger 收藏集 · 我的图鉴', PAD, 56);
220
+ // 标题(统一组件)
220
221
  const gotN = Object.keys(owned).length;
221
222
  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);
223
+ kit.header(g, {
224
+ x: PAD, y: 34, W: W - PAD * 2,
225
+ title: 'VSinger 收藏集 · 我的图鉴',
226
+ subtitle: `${displayName} 已收集 ${gotN}/${game.cards.length} 重复 ${dupN} 张`,
227
+ badge: `${gotN}/${game.cards.length}`,
228
+ badgeSub: '收集进度',
229
+ progress: game.cards.length ? gotN / game.cards.length : 0,
230
+ });
225
231
 
226
232
  for (let ci = 0; ci < part.length; ci++) {
227
233
  const grp = part[ci];
@@ -230,20 +236,24 @@ async function render({ game, store, uid, displayName, outDir, tagsDir, thumbsDi
230
236
  let prevAct = null;
231
237
  for (const blk of grp) {
232
238
  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);
239
+ kit.panel(g, x0, y, COLW, SERIES_H - 8, { radius: 14, fill: 'rgba(255,255,255,0.88)' });
240
+ g.fillStyle = COLOR.accent;
241
+ kit.roundRect(g, x0 + 12, y + 12, 5, SERIES_H - 32, 3);
242
+ g.fill();
243
+ g.fillStyle = COLOR.text;
244
+ g.font = 'bold 21px VSingerSans, sans-serif';
245
+ g.fillText(blk.act, x0 + 26, y + 22);
238
246
  y += SERIES_H;
239
247
  prevAct = blk.act;
240
248
  }
241
249
  // 池标题(玻璃条)
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);
250
+ kit.panel(g, x0, y, COLW, POOL_TITLE_H - 10, { radius: 12, fill: 'rgba(255,255,255,0.78)' });
251
+ g.fillStyle = COLOR.accent;
252
+ g.font = 'bold 21px VSingerSans, sans-serif';
253
+ g.fillText(`${blk.pool}`, x0 + 12, y + 12);
254
+ g.fillStyle = COLOR.textSub;
255
+ g.font = '18px VSingerSans, sans-serif';
256
+ g.fillText(`${blk.got}/${blk.arr.length}`, x0 + COLW - 82, y + 14);
247
257
 
248
258
  for (let i = 0; i < blk.arr.length; i++) {
249
259
  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,113 @@ 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 = 24;
80
+ const cx = x + R;
81
+ const cy = y + R;
82
+ g.save();
83
+ g.beginPath();
84
+ g.arc(cx, cy, R, 0, Math.PI * 2);
52
85
  g.fillStyle = COLOR.accent;
53
- roundRect(g, x, y + 4, 6, 34, 3);
54
86
  g.fill();
87
+ g.fillStyle = '#fff';
88
+ g.font = `bold 26px ${FONT}`;
89
+ g.textAlign = 'center';
90
+ g.textBaseline = 'middle';
91
+ g.fillText('♪', cx, cy + 1);
92
+ g.textAlign = 'left';
93
+ g.restore();
55
94
 
95
+ g.textBaseline = 'top';
56
96
  g.fillStyle = COLOR.text;
57
97
  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);
98
+ g.fillText(fit(g, o.title, `bold ${FONT_SIZE.h1}px ${FONT}`, W - 260), x + R * 2 + 14, y);
62
99
 
63
- if (subtitle) {
100
+ if (o.subtitle) {
64
101
  g.fillStyle = COLOR.textSub;
65
102
  g.font = `${FONT_SIZE.body}px ${FONT}`;
66
- g.fillText(String(subtitle), x + 16, y + 42);
103
+ g.fillText(o.subtitle, x + R * 2 + 14, y + 40);
104
+ }
105
+
106
+ // 右侧徽章
107
+ if (o.badge != null) {
108
+ const bw = 118;
109
+ const bh = 58;
110
+ const bx = x + W - bw;
111
+ panel(g, bx, y - 4, bw, bh, { radius: RADIUS.md, fill: 'rgba(255,255,255,0.95)' });
112
+ g.fillStyle = COLOR.accent;
113
+ g.font = `bold 26px ${FONT}`;
114
+ g.fillText(String(o.badge), bx + 14, y + 6);
115
+ g.fillStyle = COLOR.textSub;
116
+ g.font = `12px ${FONT}`;
117
+ g.fillText(String(o.badgeSub || ''), bx + 14, y + 36);
118
+ // 进度条
119
+ const pct = Math.max(0, Math.min(1, Number(o.progress || 0)));
120
+ g.fillStyle = 'rgba(74,144,217,0.15)';
121
+ roundRect(g, bx + 10, y + bh - 6, bw - 20, 5, 3);
122
+ g.fill();
123
+ if (pct > 0) {
124
+ g.fillStyle = COLOR.accent;
125
+ roundRect(g, bx + 10, y + bh - 6, Math.max(4, (bw - 20) * pct), 5, 3);
126
+ g.fill();
127
+ }
67
128
  }
68
129
  }
69
130
 
@@ -77,10 +138,7 @@ function fit(g, text, font, maxW) {
77
138
  return t + '…';
78
139
  }
79
140
 
80
- /**
81
- * 卡牌组件(图鉴 / 结果图共用)
82
- * @param {object} o {img, name, rate, tier, laser, count, x, y, w, h, owned}
83
- */
141
+ /** 卡牌组件 */
84
142
  async function card(g, Canvas, o) {
85
143
  const w = o.w || 170;
86
144
  const h = o.h || Math.round(w * 1.43);
@@ -88,44 +146,38 @@ async function card(g, Canvas, o) {
88
146
  const tier = o.tier == null ? tierOf({ card_scarcity: o.scarcity }) : o.tier;
89
147
  const tc = (TIER[tier] || TIER[10]).color;
90
148
 
91
- // 底 + 图
92
149
  g.save();
93
150
  roundRect(g, o.x, o.y, w, h, r);
94
151
  g.clip();
95
152
  g.fillStyle = COLOR.cardBg;
96
153
  g.fillRect(o.x, o.y, w, h);
97
154
  if (o.img) {
98
- try {
99
- g.drawImage(o.img, o.x, o.y, w, h);
100
- } catch { /* 忽略 */ }
155
+ try { g.drawImage(o.img, o.x, o.y, w, h); } catch { /* 忽略 */ }
101
156
  }
102
157
  if (o.owned === false) {
103
- g.fillStyle = 'rgba(0,0,0,0.42)';
158
+ g.fillStyle = 'rgba(255,255,255,0.38)';
104
159
  g.fillRect(o.x, o.y, w, h);
105
160
  }
106
161
  g.restore();
107
162
 
108
- // 边框(档位色 + 拥有加粗)
109
163
  g.save();
110
164
  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;
165
+ g.strokeStyle = o.owned === false ? 'rgba(150,165,185,0.45)' : tc;
112
166
  g.lineWidth = o.owned === false ? 1.5 : 2.5;
113
167
  g.stroke();
114
168
  g.restore();
115
169
 
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)) {
170
+ const tagFile = o.tagsDir ? path.join(o.tagsDir, (TIER[tier] || {}).tag || '') : '';
171
+ if ((TIER[tier] || {}).tag && o.tagsDir && fs.existsSync(tagFile)) {
119
172
  try {
120
173
  const ti = await Canvas.loadImage(tagFile);
121
174
  const tw = w * 0.46;
122
175
  g.drawImage(ti, o.x + 4, o.y + 4, tw, (ti.height / ti.width) * tw);
123
176
  } catch { /* 忽略 */ }
124
177
  }
125
- // 镭射标
126
178
  if (o.laser && o.tagsDir) {
127
- const holo = require('node:path').join(o.tagsDir, 'tag_holo.png');
128
- if (require('node:fs').existsSync(holo)) {
179
+ const holo = path.join(o.tagsDir, 'tag_holo.png');
180
+ if (fs.existsSync(holo)) {
129
181
  try {
130
182
  const hi = await Canvas.loadImage(holo);
131
183
  const hw = w * 0.36;
@@ -134,9 +186,8 @@ async function card(g, Canvas, o) {
134
186
  } catch { /* 忽略 */ }
135
187
  }
136
188
  }
137
- // 重复次数
138
189
  if (o.count > 1) {
139
- g.fillStyle = 'rgba(0,0,0,0.72)';
190
+ g.fillStyle = 'rgba(31,42,58,0.82)';
140
191
  roundRect(g, o.x + w - 46, o.y + 6, 40, 22, 8);
141
192
  g.fill();
142
193
  g.fillStyle = '#8cffaa';
@@ -144,7 +195,6 @@ async function card(g, Canvas, o) {
144
195
  g.fillText(`×${o.count}`, o.x + w - 40, o.y + 9);
145
196
  }
146
197
 
147
- // 名称 / 档位概率
148
198
  g.fillStyle = o.owned === false ? COLOR.textDim : COLOR.text;
149
199
  g.font = `bold ${o.nameSize || FONT_SIZE.h3}px ${FONT}`;
150
200
  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.1",
4
4
  "description": "VSinger 收藏集抽卡 / 签到积分 / 图鉴(Koishi 插件,一次性安装)",
5
5
  "main": "lib/index.js",
6
6
  "files": [