koishi-plugin-ll-bns-fortune 0.2.2 → 0.3.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.
Binary file
package/lib/data.js CHANGED
@@ -90,26 +90,38 @@ export const DEFAULT_MAPS = [
90
90
  /**
91
91
  * 排版模板:{xxx} 会被替换成实际内容。
92
92
  *
93
- * 行首可以加的标记:
93
+ * 行首可以加的标记(可以叠加,顺序随意):
94
94
  * [left] / [center] 指定这一行的对齐方式(默认跟随 panel.align)
95
95
  * [size=0.85] 指定这一行的字号倍率(默认 1)
96
+ * [card] 这一行当「卡头」渲染:
97
+ * 左边画一个圆角徽章(字与颜色见「卡片样式」设置),
98
+ * 标题排在徽章右侧,下面画一条分隔线
96
99
  *
97
100
  * 字号层级(size 标记 = 相对整体字号的倍率):
98
101
  * 五角星 —— 由 panel.starSize 单独控制
99
- * 标题行 0.86 —— 比正文大一点,作为抬头
100
- * 正文 0.74 —— 比标题小,避免抢戏
101
- * 所以整体字号(panel.charsPerLine 决定的那个)应当按「最大的一行」去调。
102
+ * 抬头 0.82 —— 「今日运势指数为」这类说明
103
+ * 卡头 1.0 —— 宜 / 忌 / 地图 的标题,最大
104
+ * 说明 0.72 —— 卡头下面的小字
102
105
  */
103
106
  export const DEFAULT_FORMAT = [
104
- '[center][size=0.86]您今日的运势指数为:',
107
+ '[center][size=0.82]您今日的运势指数为:',
105
108
  '[center]{stars}',
106
- '[size=0.74]今日幸运频道:{channel}',
107
- '[size=0.74]宜{good}:{goodText}',
108
- '[size=0.74]忌{bad}:{badText}',
109
- '[size=0.74]今日最佳玄学地图是:{map}',
110
- '[size=0.74]{mapText}',
109
+ '[card]{good}',
110
+ '[size=0.72]{goodText}',
111
+ '[card]{bad}',
112
+ '[size=0.72]{badText}',
113
+ '[card]地图 {map}',
114
+ '[size=0.72]{mapText}',
111
115
  ].join('\n')
112
116
 
117
+ /** 卡片徽章的默认字与颜色(设置页里「卡片样式」可覆盖) */
118
+ export const DEFAULT_CARDS = {
119
+ good: { badge: '宜', color: '#2E7D6B' },
120
+ bad: { badge: '忌', color: '#3E4A5A' },
121
+ map: { badge: '图', color: '#8C5A2B' },
122
+ summary: { badge: '', color: '#C8434F' },
123
+ }
124
+
113
125
  /** 地图下面的那句补充说明。留空则模板里的 {mapText} 一行自动省略 */
114
126
  export const DEFAULT_MAP_TEXT = ''
115
127
 
package/lib/index.js CHANGED
@@ -62,7 +62,7 @@ function assetPath (name) {
62
62
  return candidates[0]
63
63
  }
64
64
 
65
- const DEFAULT_BG = assetPath('bg.png')
65
+ const DEFAULT_BG = assetPath('bg-panel.png')
66
66
 
67
67
  /* ------------------------------------------------------------------ *
68
68
  * 配置
@@ -73,7 +73,7 @@ const panelSchema = Schema.object({
73
73
  Schema.const('left').description('左对齐(和参考排版一致)'),
74
74
  Schema.const('center').description('居中'),
75
75
  ]).default('left').description('整体对齐方式。想单独控制某一行,可在模板行首写 [left]/[center]'),
76
- charsPerLine: Schema.number().role('slider').min(6).max(40).step(1).default(14)
76
+ charsPerLine: Schema.number().role('slider').min(6).max(40).step(1).default(18)
77
77
  .description('字号大小:每行大约放几个字。<b>这是最常调的一项</b>,调小字更大'),
78
78
  blockFill: Schema.number().role('slider').min(1).max(1.5).step(0.01).default(1.08)
79
79
  .description('松紧度:1 = 撑满面板,调大四周留白更多(嫌挤就往大调)'),
@@ -86,12 +86,12 @@ const panelSchema = Schema.object({
86
86
 
87
87
  // ↓ 换底图才需要动,折叠起来
88
88
  geometry: Schema.object({
89
- x: Schema.number().role('ratio').default(0.607).description('面板左边缘:占整图宽度的比例'),
90
- y: Schema.number().role('ratio').default(0.180).description('面板上边缘:占整图高度的比例'),
91
- w: Schema.number().role('ratio').default(0.312).description('面板宽度:占整图宽度的比例'),
92
- h: Schema.number().role('ratio').default(0.668).description('面板高度:占整图高度的比例'),
93
- paddingX: Schema.number().role('ratio').default(0.035).description('面板内左右留白:占面板宽度的比例'),
94
- paddingY: Schema.number().role('ratio').default(0.035).description('面板内上下留白:占面板高度的比例'),
89
+ x: Schema.number().role('ratio').default(0.556).description('面板左边缘:占整图宽度的比例'),
90
+ y: Schema.number().role('ratio').default(0.163).description('面板上边缘:占整图高度的比例'),
91
+ w: Schema.number().role('ratio').default(0.375).description('面板宽度:占整图宽度的比例'),
92
+ h: Schema.number().role('ratio').default(0.697).description('面板高度:占整图高度的比例'),
93
+ paddingX: Schema.number().role('ratio').default(0.045).description('面板内左右留白:占面板宽度的比例'),
94
+ paddingY: Schema.number().role('ratio').default(0.05).description('面板内上下留白:占面板高度的比例'),
95
95
  measureRatio: Schema.number().min(0.5).max(1).step(0.01).default(0.98)
96
96
  .description('文字块最大宽度占面板宽度的比例'),
97
97
  }).collapse().description('面板位置(默认已按内置底图量好,换底图时才需要改)'),
@@ -106,6 +106,18 @@ const starColorsSchema = Schema.object({
106
106
  .description('未点亮星星的描边颜色,按顺序循环取用'),
107
107
  }).description('五角星配色')
108
108
 
109
+ const cardThemeSchema = Schema.object({
110
+ badge: Schema.string().default('').description('徽章里的字(留空 = 只画一个空轮廓)'),
111
+ color: Schema.string().role('color').default('#3E4A5A').description('徽章颜色(分隔线也用这个色)'),
112
+ }).description('单张卡片的样式')
113
+
114
+ const cardSchema = Schema.object({
115
+ good: cardThemeSchema.default({ badge: '宜', color: '#2E7D6B' }).description('「宜」那一张'),
116
+ bad: cardThemeSchema.default({ badge: '忌', color: '#3E4A5A' }).description('「忌」那一张'),
117
+ map: cardThemeSchema.default({ badge: '图', color: '#8C5A2B' }).description('「地图」那一张'),
118
+ summary: cardThemeSchema.default({ badge: '', color: '#C8434F' }).description('「小结」那一张(模板里用 [card] 触发)'),
119
+ }).description('卡片徽章与颜色。模板里给某一行加 [card] 就会按这里的样式渲染')
120
+
109
121
  const starWeightSchema = Schema.object({
110
122
  maxStars: Schema.number().role('slider').min(3).max(10).step(1).default(data.DEFAULT_MAX_STARS)
111
123
  .description('一共有几颗星(满星 = 这个数),改这里等于改满分'),
@@ -207,11 +219,12 @@ exports.Config = Schema.intersect([
207
219
  panel: panelSchema,
208
220
  colors: colorsSchema,
209
221
  starColors: starColorsSchema,
222
+ cards: cardSchema,
210
223
 
211
224
  renderAdvanced: Schema.object({
212
225
  background: Schema.string().role('textarea').default(DEFAULT_BG)
213
226
  .description('底图:支持 http(s) 直链、绝对路径、插件相对路径。留空则用内置底图'),
214
- outputWidth: Schema.number().role('slider').min(0).max(2560).step(10).default(1280)
227
+ outputWidth: Schema.number().role('slider').min(0).max(2560).step(10).default(1520)
215
228
  .description('输出图片宽度(像素)。默认 1280 出图最快;想要更锐利的文字可调到 1920'),
216
229
  imageFormat: Schema.union([
217
230
  Schema.const('jpeg').description('JPEG(编码最快,推荐)'),
@@ -310,12 +323,16 @@ function adv (config) {
310
323
 
311
324
  function renderCfg (config) {
312
325
  return Object.assign({
313
- background: DEFAULT_BG, outputWidth: 1280, imageFormat: 'jpeg', imageQuality: 92,
326
+ background: DEFAULT_BG, outputWidth: 1520, imageFormat: 'jpeg', imageQuality: 92,
314
327
  renderScale: 2, fallbackText: true, showPanelBackground: false, panelColor: '#F6E7DA',
315
328
  panelOpacity: 0, decorations: [], fontFamilies: renderer.defaultFamilies(),
316
329
  }, config && config.renderAdvanced)
317
330
  }
318
331
 
332
+ function cardCfg (config) {
333
+ return Object.assign({}, data.DEFAULT_CARDS, config && config.cards)
334
+ }
335
+
319
336
  function storageCfg (config) {
320
337
  return Object.assign({ databaseCache: true }, config && config.storage)
321
338
  }
@@ -330,8 +347,8 @@ function kwMore (config) {
330
347
  /** 面板几何(折叠组,缺省用内置底图量好的值) */
331
348
  function panelGeom (config) {
332
349
  return Object.assign({
333
- x: 0.607, y: 0.180, w: 0.312, h: 0.668,
334
- paddingX: 0.035, paddingY: 0.035, measureRatio: 0.98,
350
+ x: 0.556, y: 0.163, w: 0.375, h: 0.697,
351
+ paddingX: 0.06, paddingY: 0.05, measureRatio: 0.98,
335
352
  }, config && config.panel && config.panel.geometry)
336
353
  }
337
354
 
@@ -533,6 +550,7 @@ exports.apply = function apply (ctx, config) {
533
550
  // ⚠️ 必须定义在所有使用者之前:const 有暂时性死区,
534
551
  // 放到后面会在启动时直接抛 "Cannot access 'xxx' before initialization"。
535
552
  const geom = panelGeom(config)
553
+ const cardTheme = cardCfg(config)
536
554
  const render = renderCfg(config)
537
555
  const storage = storageCfg(config)
538
556
  const keywordMore = kwMore(config)
@@ -672,6 +690,7 @@ exports.apply = function apply (ctx, config) {
672
690
  starSpacing: Number(config.panel.starSpacing),
673
691
  starColors: (config.starColors && config.starColors.fill) || null,
674
692
  starEmptyColors: (config.starColors && config.starColors.empty) || null,
693
+ cards: cardTheme,
675
694
  outputWidth: Number(render.outputWidth) || 0,
676
695
  color: render.panelColor,
677
696
  opacity: render.showPanelBackground ? Number(render.panelOpacity) : 0,
package/lib/render.js CHANGED
@@ -174,16 +174,46 @@ async function loadImageBuffer (ctx, target, relativeBase) {
174
174
  /** 这些占位符即使和上一行重复也不要删(否则整行都会消失) */
175
175
  const SINGLE_TOKEN_BLOCKLIST = ['stars', 'map', 'mapText', 'date', 'userId']
176
176
 
177
+ /** 徽章尺寸 = 字号 × 这个比例(1.55 约等于参考图里的比例) */
178
+ const CARD_BADGE_RATIO = 1.55
179
+
180
+ /** 卡片主题默认值:徽章上的字 + 颜色。可在设置页覆盖 */
181
+ const DEFAULT_CARDS = {
182
+ good: { badge: '宜', color: '#2E7D6B', titleColor: null },
183
+ bad: { badge: '忌', color: '#3E4A5A', titleColor: null },
184
+ map: { badge: '', color: '#B0306A', titleColor: null },
185
+ summary: { badge: '', color: '#C8434F', titleColor: null },
186
+ }
187
+
188
+ /** 根据这一行用的占位符,挑对应的卡片主题(徽章字/颜色) */
189
+ function cardStyle (tokens, theme) {
190
+ const has = (k) => tokens.indexOf(k) !== -1
191
+ if (has('good') || has('goodText')) return Object.assign({}, DEFAULT_CARDS.good, theme.good)
192
+ if (has('bad') || has('badText')) return Object.assign({}, DEFAULT_CARDS.bad, theme.bad)
193
+ if (has('map')) return Object.assign({}, DEFAULT_CARDS.map, theme.map)
194
+ if (has('mapText')) return Object.assign({}, DEFAULT_CARDS.summary, theme.summary)
195
+ return Object.assign({}, DEFAULT_CARDS.summary, theme.summary)
196
+ }
197
+
177
198
  /** 模板行首的对齐标记:'[left]xxx' / '[center]xxx',中文「[左]」「[中]」也行 */
178
199
  const ALIGN_MARKER = /^\[(left|center|左|中)\]/i
179
200
  /** 行首的字号倍率标记:'[size=0.85]xxx',让某一行比整体字号小一点 */
180
201
  const SIZE_MARKER = /^\[size\s*=\s*([\d.]+)\]/i
202
+ /** 行首的卡片标记:'[card]xxx' —— 这一行当卡头渲染(圆角徽章 + 大标题 + 下划分隔线) */
203
+ const CARD_MARKER = /^\[card\]/i
181
204
 
182
205
  function parseLineMarkers (line, fallbackAlign, fallbackSize) {
183
206
  let text = line
184
207
  let align = fallbackAlign
185
208
  let size = fallbackSize
186
- for (let i = 0; i < 4; i++) {
209
+ let card = false
210
+ for (let i = 0; i < 5; i++) {
211
+ const cardMatched = text.match(CARD_MARKER)
212
+ if (cardMatched) {
213
+ card = true
214
+ text = text.slice(cardMatched[0].length)
215
+ continue
216
+ }
187
217
  const alignMatched = text.match(ALIGN_MARKER)
188
218
  if (alignMatched) {
189
219
  const raw = alignMatched[1].toLowerCase()
@@ -200,7 +230,45 @@ function parseLineMarkers (line, fallbackAlign, fallbackSize) {
200
230
  }
201
231
  break
202
232
  }
203
- return { align, size, text }
233
+ return { align, size, card, text }
234
+ }
235
+
236
+ /**
237
+ * 画一个圆角方形徽章。
238
+ * text 为空时只画轮廓(无字徽章,用于「小结」那种)。
239
+ */
240
+ function drawBadge (ctx, x, centerY, size, color, text, fontFamilyList) {
241
+ const radius = size * 0.28
242
+ const left = x
243
+ const top = centerY - size / 2
244
+ ctx.save()
245
+ ctx.beginPath()
246
+ ctx.moveTo(left + radius, top)
247
+ ctx.lineTo(left + size - radius, top)
248
+ ctx.quadraticCurveTo(left + size, top, left + size, top + radius)
249
+ ctx.lineTo(left + size, top + size - radius)
250
+ ctx.quadraticCurveTo(left + size, top + size, left + size - radius, top + size)
251
+ ctx.lineTo(left + radius, top + size)
252
+ ctx.quadraticCurveTo(left, top + size, left, top + size - radius)
253
+ ctx.lineTo(left, top + radius)
254
+ ctx.quadraticCurveTo(left, top, left + radius, top)
255
+ ctx.closePath()
256
+
257
+ if (text) {
258
+ ctx.fillStyle = color
259
+ ctx.fill()
260
+ ctx.fillStyle = '#FFFFFF'
261
+ ctx.font = buildFont(size * 0.56, 'bold', fontFamilyList)
262
+ ctx.textAlign = 'center'
263
+ ctx.textBaseline = 'middle'
264
+ ctx.fillText(text, left + size / 2, centerY + size * 0.03)
265
+ ctx.textAlign = 'left'
266
+ } else {
267
+ ctx.strokeStyle = color
268
+ ctx.lineWidth = Math.max(1, size * 0.1)
269
+ ctx.stroke()
270
+ }
271
+ ctx.restore()
204
272
  }
205
273
 
206
274
  /**
@@ -233,6 +301,7 @@ function detectLabelToken (segments) {
233
301
  function buildLines (template, values, colors, ratio, options) {
234
302
  const opts = options || {}
235
303
  const fallbackAlign = opts.align === 'center' ? 'center' : 'left'
304
+ const cardTheme = opts.cards || DEFAULT_CARDS
236
305
  const lines = []
237
306
  let lastToken = null
238
307
  let lastValue = null
@@ -304,6 +373,7 @@ function buildLines (template, values, colors, ratio, options) {
304
373
  size: marked.size,
305
374
  align: marked.align,
306
375
  isStars: tokens.length === 1 && tokens[0] === 'stars',
376
+ card: marked.card ? cardStyle(tokens, cardTheme) : null,
307
377
  })
308
378
  }
309
379
  return lines
@@ -591,6 +661,7 @@ async function renderFortune (ctx, options) {
591
661
 
592
662
  // 整图对齐方式:left(默认,和参考排版一致)/ center
593
663
  const align = panel.align === 'center' ? 'center' : 'left'
664
+ const cardTheme = Object.assign({}, DEFAULT_CARDS, panel.cards)
594
665
  const starOptions = {
595
666
  size: Number(panel.starSize) || 1.15,
596
667
  spacing: Number(panel.starSpacing) || 1.32,
@@ -613,9 +684,24 @@ async function renderFortune (ctx, options) {
613
684
  const buildAndMeasure = (fontSize) => {
614
685
  const laid = []
615
686
  let finalWidth = measureWidth
616
- for (const line of buildLines(template, values, colors, 1, { align })) {
687
+ let lastCard = null
688
+ for (const line of buildLines(template, values, colors, 1, { align, cards: cardTheme })) {
617
689
  const spec = fontSize * (line.ratio || 1) * (line.size || 1)
618
690
 
691
+ // 卡头:左边留出徽章的位置,标题在其右侧起排
692
+ let indent = 0
693
+ let badge = null
694
+ if (line.card) {
695
+ const badgeSize = spec * CARD_BADGE_RATIO
696
+ indent = badgeSize + spec * 0.42
697
+ badge = {
698
+ size: badgeSize,
699
+ color: line.card.color,
700
+ text: line.card.badge || '',
701
+ dividerColor: line.card.divider || line.card.color,
702
+ }
703
+ }
704
+
619
705
  // 整行星:不排版文字,交给绘制阶段画彩色五角星
620
706
  if (line.isStars) {
621
707
  // 星星行必须自己兜住宽度:整行星不参与换行,字号再大也只会把星行撑出面板,
@@ -630,22 +716,27 @@ async function renderFortune (ctx, options) {
630
716
  height: sprite.h * 1.5,
631
717
  align: 'center',
632
718
  isStars: true,
719
+ badge: null,
720
+ indent: 0,
721
+ divider: false,
633
722
  })
723
+ lastCard = null
634
724
  continue
635
725
  }
636
726
 
637
- let chunks = layoutParagraph(line, finalWidth, g, spec)
727
+ const lineWidth = Math.max(40, finalWidth - indent)
728
+ let chunks = layoutParagraph(line, lineWidth, g, spec)
638
729
 
639
730
  // 孤字控制:如果最后一行只剩一两个字,被硬挤下来会很难看,
640
731
  // 就把排版宽度一点点收窄,逼出更均衡的断行(最多试 25 次)
641
732
  if (chunks.length > 1) {
642
- const natural = layoutParagraph(line, measureWidth, g, spec)
733
+ const natural = layoutParagraph(line, lineWidth, g, spec)
643
734
  const tail = natural[natural.length - 1].map(run => run.char).join('')
644
735
  if (tail.length <= 2 && natural.length <= 4) {
645
- finalWidth = measureWidth
736
+ finalWidth = lineWidth
646
737
  for (let i = 0; i < 25; i++) {
647
- finalWidth -= Math.max(1, measureWidth * 0.02)
648
- if (finalWidth < measureWidth * 0.55) break
738
+ finalWidth -= Math.max(1, lineWidth * 0.02)
739
+ if (finalWidth < lineWidth * 0.55) break
649
740
  const attempt = layoutParagraph(line, finalWidth, g, spec)
650
741
  const attemptTail = attempt[attempt.length - 1].map(run => run.char).join('')
651
742
  if (attemptTail.length > 2 || attempt.length > natural.length + 1) {
@@ -657,6 +748,9 @@ async function renderFortune (ctx, options) {
657
748
  }
658
749
  }
659
750
 
751
+ // 卡头下方的分隔线:只在「接下来还有内容」时画,最后一段不画
752
+ const divider = !!(line.card && lastCard !== null)
753
+
660
754
  for (let i = 0; i < chunks.length; i++) {
661
755
  laid.push({
662
756
  runs: chunks[i],
@@ -665,23 +759,46 @@ async function renderFortune (ctx, options) {
665
759
  height: spec * lineHeightRatio,
666
760
  align: line.align,
667
761
  isStars: false,
762
+ badge: i === 0 ? badge : null,
763
+ indent,
764
+ divider: divider && i === 0,
668
765
  })
669
766
  }
767
+ lastCard = line.card ? true : null
670
768
  }
671
769
  const total = laid.reduce((sum, line) => sum + line.gap * fontSize + line.height, 0)
672
770
  return { lines: laid, total }
673
771
  }
674
772
 
675
773
  // 先按 baseFont 量一次,再二分放大。
676
- // 上限给到 baseFont 的 2.4 倍:baseFont 是「每行几个字」的估算值,内容少的时候
677
- // (比如宜忌没写详细文案)可以放得更大,把面板填得更饱满;放大会触发换行,
678
- // 换行后总高会变,所以二分本身会收敛到一个合适的值。
774
+ // 两条约束同时生效:
775
+ // ① 高度:撑满可用高度(内容少时字就大,填满面板)
776
+ // ② 宽度:不许把「内容最长的那一行」撑到换行 —— 没有这条的话,
777
+ // 自动放大只看高度,长地图名会被硬挤成两行
679
778
  const MAX_FONT_SCALE = 2.4
680
- // 只允许撑到可用高度的 97%:留一点余量,避免笔画降部顶到纸框上
681
779
  const FIT_LIMIT = 0.97
682
- const unit = buildAndMeasure(baseFont)
683
- let fontSize = baseFont
684
- if (unit.total > 0 && unit.total < textHeight) {
780
+ const fitsWidth = (fs) => {
781
+ // 注意:不能量 buildAndMeasure 的结果 —— 那里已经按宽度换过行了,
782
+ // 每段必然「不超宽」,等于没检查。要量的是「这一行原始内容」的自然宽度。
783
+ for (const line of buildLines(template, values, colors, 1, { align, cards: cardTheme })) {
784
+ if (line.isStars) continue
785
+ const spec = fs * (line.ratio || 1) * (line.size || 1)
786
+ let natural = 0
787
+ for (const segment of line.segments) {
788
+ g.font = buildFont(spec * (segment.size || 1), segment.bold ? 'bold' : '', currentFamilies)
789
+ natural += g.measureText(segment.text).width
790
+ }
791
+ const indent = line.card ? spec * CARD_BADGE_RATIO + spec * 0.42 : 0
792
+ if (natural > Math.max(40, measureWidth - indent) + 0.5) return false
793
+ }
794
+ return true
795
+ }
796
+ const findFontSize = () => {
797
+ const unit = buildAndMeasure(baseFont)
798
+ if (unit.total <= 0) return baseFont
799
+ if (unit.total >= textHeight) {
800
+ return baseFont * Math.max(0.5, (textHeight * FIT_LIMIT) / unit.total)
801
+ }
685
802
  let low = 1
686
803
  let high = MAX_FONT_SCALE
687
804
  for (let i = 0; i < 14; i++) {
@@ -689,10 +806,15 @@ async function renderFortune (ctx, options) {
689
806
  if (buildAndMeasure(baseFont * mid).total <= textHeight * FIT_LIMIT) low = mid
690
807
  else high = mid
691
808
  }
692
- fontSize = baseFont * low
693
- } else if (unit.total > textHeight) {
694
- fontSize = baseFont * Math.max(0.5, (textHeight * FIT_LIMIT) / unit.total)
809
+ let fs = baseFont * low
810
+ // 宽度回退:每次退 2%,最多退到 baseFont 的 45%
811
+ for (let i = 0; i < 30 && !fitsWidth(fs); i++) {
812
+ fs *= 0.98
813
+ if (fs < baseFont * 0.45) break
814
+ }
815
+ return fs
695
816
  }
817
+ const fontSize = findFontSize()
696
818
 
697
819
  const fitted = buildAndMeasure(fontSize)
698
820
  const lines = fitted.lines
@@ -708,12 +830,13 @@ async function renderFortune (ctx, options) {
708
830
  const rowCenter = cursorY + line.height / 2
709
831
  const lineWidth = line.runs.reduce((sum, run) => sum + run.w, 0)
710
832
 
711
- // 对齐:center 居中(扣掉行尾整角标点占的空白),left 从面板左内侧起排
833
+ // 对齐:center 居中(扣掉行尾整角标点占的空白),left 从面板左内侧起排(卡头再缩进徽章宽度)
712
834
  const lineAlign = line.align === 'center' ? 'center' : align
713
835
  const trim = lineAlign === 'center' ? trailingPunctWidth(line.runs) : 0
836
+ const indent = line.indent || 0
714
837
  let x = lineAlign === 'center'
715
838
  ? panelRect.x + panelRect.w / 2 - (lineWidth - trim) / 2
716
- : panelRect.x + paddingX
839
+ : panelRect.x + paddingX + indent
717
840
 
718
841
  if (line.isStars) {
719
842
  // 整行星:五角星在面板里居中(paintStarSprite 的 x 参数是第 1 颗星的左沿)
@@ -724,6 +847,12 @@ async function renderFortune (ctx, options) {
724
847
  continue
725
848
  }
726
849
 
850
+ // 卡头左边的圆角徽章
851
+ if (line.badge) {
852
+ drawBadge(g, panelRect.x + paddingX, rowCenter,
853
+ line.badge.size, line.badge.color, line.badge.text, currentFamilies)
854
+ }
855
+
727
856
  for (const run of line.runs) {
728
857
  if (run.sprite) {
729
858
  paintStarSprite(g, run, x, rowCenter)
@@ -735,6 +864,23 @@ async function renderFortune (ctx, options) {
735
864
  g.fillText(run.char, x, rowCenter)
736
865
  x += run.w
737
866
  }
867
+
868
+ // 卡头下方的分隔线(只在后面还有内容时画)
869
+ if (line.divider) {
870
+ const left = panelRect.x + paddingX + indent
871
+ const right = panelRect.x + panelRect.w - paddingX
872
+ const y = cursorY + line.height * 0.98
873
+ g.save()
874
+ // 0.28 在宣纸上几乎看不见,加到 0.5 才有参考图那种分隔感
875
+ g.strokeStyle = withAlpha(line.badge ? line.badge.dividerColor : colors.text, 0.5)
876
+ g.lineWidth = Math.max(1, line.font * 0.06)
877
+ g.beginPath()
878
+ g.moveTo(left, y)
879
+ g.lineTo(right, y)
880
+ g.stroke()
881
+ g.restore()
882
+ }
883
+
738
884
  cursorY += line.height
739
885
  }
740
886
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ll-bns-fortune",
3
3
  "description": "剑灵怀旧服每日运势:宜/忌、玄学地图、彩色五角星运势指数,自定义底图出图",
4
- "version": "0.2.2",
4
+ "version": "0.3.0",
5
5
  "main": "lib/index.js",
6
6
  "files": [
7
7
  "lib",
package/assets/bg.png DELETED
Binary file