koishi-plugin-kkk 3.7.0 → 3.7.2

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.
@@ -17,10 +17,26 @@ const BILI_BLUE = '#00aeec'
17
17
  /** 顶层分支配色,和思维导图常见做法一致:一条主枝一个颜色 */
18
18
  const BRANCH_COLORS = ['#ff7a45', '#3fbf6f', '#22a6b3', '#a259ff', '#f0b429', '#5a8dee', '#e8608d', '#7f8c8d']
19
19
 
20
+ /**
21
+ * 版面尺寸。
22
+ *
23
+ * **字要大**(用户要求):这张卡片是 1440px 宽的画布,到了手机聊天框里会被等比缩到
24
+ * 几百像素宽,图上文字的实际大小 = 字号 / 画布总宽 —— 所以「把字弄大」只能靠
25
+ * **字号占宽度的比例变大**:
26
+ * - 列宽不再跟着字号涨(涨了等于没变),只把字号提上去;
27
+ * - 每列能显示几个字 = 列宽 / 字号,字号大了就得少显示几个字,
28
+ * 标签改成**按像素宽度裁剪**(中文按 1 个字宽算),不让它压到下一列上去。
29
+ */
20
30
  const COL_WIDTH = 260
21
- const ROW_HEIGHT = 62
31
+ const ROW_HEIGHT = 88
22
32
  const PAD_LEFT = 8
23
- const PAD_TOP = 20
33
+ const PAD_TOP = 24
34
+
35
+ /** 节点文字的字号(选项 / 剧情节点) */
36
+ const CHOICE_FONT = 33
37
+ const NODE_FONT = 34
38
+ /** 一列文字最多能用到哪里:到下一列的圆圈之前就为止(别压到人家的圆上) */
39
+ const LABEL_WIDTH = COL_WIDTH - 45
24
40
 
25
41
  interface Item {
26
42
  id: string
@@ -37,6 +53,73 @@ interface Item {
37
53
  walked?: boolean
38
54
  }
39
55
 
56
+ /**
57
+ * 一个字符大概占多少个字号宽:中文 / 全角算 1 个,空格 0.3,其余 0.5。
58
+ * (不用真的去量字体,够用就行 —— 目的是别让文字压到下一列的圆圈上。)
59
+ */
60
+ const charWidth = (char: string): number => {
61
+ if (/[\u2e80-\ufffd\ufe30-\ufe4f]/.test(char)) return 1
62
+ if (char === ' ') return 0.3
63
+ return 0.5
64
+ }
65
+
66
+ /** 一段文字排出来大概多宽(像素) */
67
+ const lineWidth = (text: string, fontSize: number): number => {
68
+ let total = 0
69
+ for (const char of text) total += charWidth(char) * fontSize
70
+ return total
71
+ }
72
+
73
+ /**
74
+ * 把标签排成**最多两行**,两行都装不下才截断加省略号。
75
+ *
76
+ * 以前是按「14 个字」裁的:中文一个字差不多一个字号宽,14 个字在 34px 字号下要 470 多个像素,
77
+ * 而列宽只有 260px —— 标签会直接压到下一列的圆圈上,字挤在一起更看不清。
78
+ *
79
+ * 为什么要折行而不是直接截断:字号调大以后一列只能放下六七个字,
80
+ * 直接砍会把「当前 · 第二题」这种关键信息切掉;折成两行既保住了字大,也保住了内容
81
+ * (行高已经在 ROW_HEIGHT 里留出来了)。
82
+ *
83
+ * @param label 原始文字
84
+ * @param fontSize 字号(越大能放的字越少)
85
+ * @param extra 额外可用宽度(最后一列右边没有东西,用它多占一点)
86
+ * @param suffix 后缀(「(默认)」这种)——**整块跟着最后一行,绝不从中间断开**
87
+ */
88
+ const wrapLabel = (label: string, fontSize: number, extra = 0, suffix = ''): string[] => {
89
+ const budget = LABEL_WIDTH + extra
90
+ const lines: string[] = []
91
+ let current = ''
92
+ let used = 0
93
+ for (const char of label) {
94
+ const cost = charWidth(char) * fontSize
95
+ if (current && used + cost > budget) {
96
+ lines.push(current)
97
+ current = ''
98
+ used = 0
99
+ // 两行都满了:把第二行截断加省略号
100
+ if (lines.length >= 2) {
101
+ lines[1] = lines[1] + '…'
102
+ break
103
+ }
104
+ }
105
+ current += char
106
+ used += cost
107
+ }
108
+ if (current) {
109
+ if (lines.length >= 2) lines[1] = lines[1] + '…'
110
+ else lines.push(current)
111
+ }
112
+ if (!lines.length) lines.push('')
113
+ /** 后缀单独占一行也行(它很短),实在没地方了就跟在最后一行后面 */
114
+ if (suffix) {
115
+ const last = lines[lines.length - 1]
116
+ if (lineWidth(last + suffix, fontSize) <= budget) lines[lines.length - 1] = last + suffix
117
+ else if (lines.length < 2) lines.push(suffix)
118
+ else lines[lines.length - 1] = last + suffix
119
+ }
120
+ return lines
121
+ }
122
+
40
123
  /** 把剧情图摊平成「一行一项」的列表,并算好每个叶子占用的行号 */
41
124
  const flatten = (root: InteractiveMapNode, path: string[], rootLabel?: string): { items: Item[]; rows: number } => {
42
125
  const items: Item[] = []
@@ -133,7 +216,7 @@ export const BilibiliInteractive: React.FC<PosterProps<BilibiliInteractiveData>>
133
216
  </div>
134
217
  <h1 style={{ margin: '28px 0 0', fontSize: 48, fontWeight: 900, lineHeight: 1.3, wordBreak: 'break-word' }}>{data.title}</h1>
135
218
  {path.length > 0 && (
136
- <div style={{ marginTop: 18, fontSize: 27, fontWeight: 600, opacity: 0.5, wordBreak: 'break-word' }}>已选:{path.join(' → ')}</div>
219
+ <div style={{ marginTop: 18, fontSize: 32, fontWeight: 600, opacity: 0.5, wordBreak: 'break-word' }}>已选:{path.join(' → ')}</div>
137
220
  )}
138
221
 
139
222
  {flat && (
@@ -157,19 +240,26 @@ export const BilibiliInteractive: React.FC<PosterProps<BilibiliInteractiveData>>
157
240
  const x = colX(item)
158
241
  const y = rowY(item.row)
159
242
  const isChoice = item.kind === 'choice'
243
+ const font = isChoice ? CHOICE_FONT : NODE_FONT
244
+ const textX = x + (isChoice ? 22 : 26)
245
+ /** 「(默认)」也算进这一列的可视宽度里,免得它把第二行挤出去 */
246
+ const lines = wrapLabel(item.label, font, item.depth >= depth ? 56 : 0, item.isDefault ? '(默认)' : '')
247
+ /** 一行时基线和以前一样;两行时整块往上挪一点,让两行都贴着圆圈居中 */
248
+ const baseline = lines.length > 1 ? y - font * 0.15 : y + font * 0.35
160
249
  return (
161
250
  <g key={'node-' + item.id}>
162
251
  <circle cx={x} cy={y} r={isChoice ? 9 : 13} fill={item.walked || item.kind === 'root' ? item.color : '#ffffff'} stroke={item.color} strokeWidth={4} />
163
252
  <text
164
- x={x + (isChoice ? 22 : 26)}
165
- y={y + 9}
166
- fontSize={isChoice ? 27 : 29}
253
+ x={textX}
254
+ y={baseline}
255
+ fontSize={font}
167
256
  fontWeight={item.walked || item.kind === 'root' ? 900 : 600}
168
257
  fill={item.walked ? item.color : 'currentColor'}
169
258
  opacity={item.walked || item.kind === 'root' ? 1 : 0.82}
170
259
  >
171
- {item.label.length > 14 ? item.label.slice(0, 14) + '…' : item.label}
172
- {item.isDefault ? '(默认)' : ''}
260
+ {lines.map((line, index) => (
261
+ <tspan key={'line-' + index} x={textX} dy={index === 0 ? 0 : Math.round(font * 1.05)}>{line}</tspan>
262
+ ))}
173
263
  </text>
174
264
  </g>
175
265
  )
@@ -185,14 +275,14 @@ export const BilibiliInteractive: React.FC<PosterProps<BilibiliInteractiveData>>
185
275
  ))}
186
276
 
187
277
  {(data.truncated || graph?.truncated) && (
188
- <div style={{ marginTop: 26, fontSize: 25, fontWeight: 600, opacity: 0.55 }}>剧情太长,图上只画了前半部分分支</div>
278
+ <div style={{ marginTop: 26, fontSize: 30, fontWeight: 600, opacity: 0.55 }}>剧情太长,图上只画了前半部分分支</div>
189
279
  )}
190
- <div style={{ marginTop: 22, display: 'flex', alignItems: 'center', gap: 14, fontSize: 25, fontWeight: 600, opacity: 0.52 }}>
191
- <span style={{ display: 'inline-block', width: 26, height: 5, borderRadius: 999, background: BILI_BLUE }} />
280
+ <div style={{ marginTop: 22, display: 'flex', alignItems: 'center', gap: 14, fontSize: 30, fontWeight: 600, opacity: 0.52 }}>
281
+ <span style={{ display: 'inline-block', width: 30, height: 6, borderRadius: 999, background: BILI_BLUE }} />
192
282
  高亮的是你已经走过的路
193
283
  </div>
194
284
  {data.notice && (
195
- <div style={{ marginTop: 18, borderRadius: 20, padding: '16px 28px', fontSize: 25, fontWeight: 600, opacity: 0.62, background: 'rgba(127,127,127,0.12)' }}>{data.notice}</div>
285
+ <div style={{ marginTop: 18, borderRadius: 20, padding: '16px 28px', fontSize: 30, fontWeight: 600, opacity: 0.62, background: 'rgba(127,127,127,0.12)' }}>{data.notice}</div>
196
286
  )}
197
287
  </div>
198
288
  </DefaultLayout>