koishi-plugin-ciyi 2.2.0 → 2.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.
- package/CHANGELOG.md +4 -0
- package/DESIGN_SYSTEM.md +9 -9
- package/README.md +2 -2
- package/lib/index.js +377 -660
- package/lib/ux.d.ts +4 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/DESIGN_SYSTEM.md
CHANGED
|
@@ -7,18 +7,18 @@
|
|
|
7
7
|
- 视觉、组件、形状、排版与语义颜色以 [Material 3 / Expressive](https://m3.material.io/) 为准。
|
|
8
8
|
- [Apple HIG](https://developer.apple.com/design/human-interface-guidelines/accessibility) 用于操作反馈、平台惯例、可恢复性与可访问性,不引入 Apple 视觉组件。
|
|
9
9
|
- [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) 用于非文本内容、颜色与对比度、时间限制、输入错误和破坏性操作审查。
|
|
10
|
-
- 色彩使用 Google Material Color Utilities 0.4.0 的 HCT 与 Tonal Spot 动态角色,明确使用 2025 色彩规范。历史 `lch` API 名仅用于源码兼容,计算已经统一为 HCT
|
|
10
|
+
- 色彩使用 Google Material Color Utilities 0.4.0 的 HCT 与 Tonal Spot 动态角色,明确使用 2025 色彩规范。历史 `lch` API 名仅用于源码兼容,计算已经统一为 HCT。彩色角色(primary / secondary / tertiary / error 及其容器)走官方 Tonal Spot;中性面(surface 系列、on-surface 系列、outline 系列)由 HCT 按本设计系统的色调重算——表面留住一点源色味道、文字取深色档,避免成片近白、文字偏浅的发晃感。
|
|
11
11
|
- `m3.ts` 是角色颜色、字阶、间距、圆角、层级和基础组件的唯一源文件。`material-color*.ts` 是固定版本的官方算法生成物,授权见 `THIRD_PARTY_NOTICES.md`。
|
|
12
12
|
- 不额外添加静态截图无法表达的动画;基础 CSS 禁用减少动态效果偏好下的动画和过渡。
|
|
13
13
|
|
|
14
14
|
## 输出、交互与内容
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
16
|
+
1. 有渲染图时只发图片,不附同内容的文字,避免在群里重复刷屏;图片生成失败时才退回文字。插件不提供个人显示模式开关。
|
|
17
|
+
2. 每个插件的主指令都带描述,`help` 列表右侧不留空;主指令先于任何子指令注册(Koishi 会忽略已存在指令的描述)。文本回复不能把后续条目无提示地压掉;较长管理列表分页。
|
|
18
18
|
3. 裸输入必须符合词法、对局状态及参与条件。多个插件匹配时不执行,要求明确完整指令;完整指令继续可用。
|
|
19
19
|
4. 回复说明当前结果、未完成原因和可执行的下一步,不用管理员日志替代用户可用的反馈。退款、赔付与渲染失败不能伪装成功。
|
|
20
20
|
5. 破坏性清空需展示范围、不可撤销性和限时确认码。确认码限制到同一机器人、平台、用户与频道,使用后立即失效。
|
|
21
|
-
6.
|
|
21
|
+
6. 多轮输入等待 5 分钟,提问里写明可回复的内容与「取消」,不再追加计时说明。对局内的短窗口(如 21 点保险、投降)提供显式跳过,全员表态后立即进入下一阶段,而不是提供「延长」。
|
|
22
22
|
7. 货币发放先写入核对记录,入账结果不明确时保留待核对状态,不自动重发。管理员核对实际流水并按需人工补发后标记确认;确认操作自身不转账。
|
|
23
23
|
|
|
24
24
|
## 内容边界
|
|
@@ -44,7 +44,7 @@ node markdown-to-image-service/scripts/sync-design-system.mjs --check
|
|
|
44
44
|
|
|
45
45
|
官方色彩算法可在源仓库安装开发依赖后通过 `node scripts/build-material-colors.mjs` 重建,再同步消费者。
|
|
46
46
|
|
|
47
|
-
回归检查覆盖:24 个源色相的明暗角色对比度、奖牌前景、图表填充/轨道差异、浏览器/服务端 HCT
|
|
47
|
+
回归检查覆盖:24 个源色相的明暗角色对比度、奖牌前景、图表填充/轨道差异、浏览器/服务端 HCT 一致性、主指令描述、输入冲突、入账异常、清空确认和 Wordle 并发/渲染失败。工作区截图脚本为 `scripts/verify-rendering.ts`,需 Puppeteer Core 和可用 Chromium。
|
|
48
48
|
|
|
49
49
|
## 本轮改动对应关系
|
|
50
50
|
|
|
@@ -53,9 +53,9 @@ node markdown-to-image-service/scripts/sync-design-system.mjs --check
|
|
|
53
53
|
| 共享设计规范 | 官方 HCT、2025 动态颜色角色、共享字阶与间距、同步与哈希校验 |
|
|
54
54
|
| Markdown | M3 明暗主题;旧主题名按明暗迁移;代码高亮与 Mermaid 使用 M3 角色;长代码可换行 |
|
|
55
55
|
| message-counter / monetary-rank | M3 角色与共享图表字阶、HCT 头像配色、完整文字榜单;保留昵称截断 |
|
|
56
|
-
| Wordle | 替换复制的 NYT
|
|
57
|
-
| MCDLE |
|
|
56
|
+
| Wordle | 替换复制的 NYT 外观;状态符号与图例;明暗角色;异常释放并发锁;安全解析模式编号;裸词只在确定是猜测时回应 |
|
|
57
|
+
| MCDLE | 猜测按行排成一张表,方便逐项对照;单句页脚居中 |
|
|
58
58
|
| 2048 / ciyi | 数字与奖牌前景按实际对比度选色 |
|
|
59
|
-
| 斗牛 / 21 点 |
|
|
59
|
+
| 斗牛 / 21 点 | 付款核对记录、如实反馈退款失败、明确下注金额、可显式跳过的短窗口 |
|
|
60
60
|
| sentinel | 完整封印列表按每页 10 位展示 |
|
|
61
|
-
| 其余插件 |
|
|
61
|
+
| 其余插件 | 渲染失败退回文字、可取消输入与上下文反馈 |
|
package/README.md
CHANGED
|
@@ -27,6 +27,6 @@ yarn add koishi-plugin-ciyi
|
|
|
27
27
|
|
|
28
28
|
## 显示与交互
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
有渲染图时只发图片,不再附带同内容的文字;图片生成失败时才退回文字。作品素材与感官测试的适用边界见 [设计系统](./DESIGN_SYSTEM.md)。
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
本次更新:四张卡片重新排版:页脚贴合卡片圆角、提示不再被截成省略号,图例单独成行,结果卡去掉压边的字印,量高与绘制共用同一组版式常量。 主指令在 help 列表里补回描述;去掉「.显示」显示模式指令,有图只发图、出图失败才退回文字;多轮输入不再追加计时说明。
|
package/lib/index.js
CHANGED
|
@@ -36,27 +36,13 @@ var import_koishi = require("koishi");
|
|
|
36
36
|
var KEY = Symbol.for("araea.koishi.ux.v1");
|
|
37
37
|
function state(ctx) {
|
|
38
38
|
const root = ctx.root;
|
|
39
|
-
return root[KEY] ??= {
|
|
39
|
+
return root[KEY] ??= { owners: /* @__PURE__ */ new Set() };
|
|
40
40
|
}
|
|
41
41
|
__name(state, "state");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const shared = state(ctx);
|
|
45
|
-
ctx.command(`${command}.显示 [mode:string]`, "选择图文或文字输出").action(({ session }, mode) => {
|
|
46
|
-
if (!mode) return `当前:${shared.modes.get(userKey(session)) ? "文字" : "图文"}。发送「${command}.显示 文字」或「${command}.显示 图文」。设置对本机器人各插件生效,重启后恢复图文。`;
|
|
47
|
-
if (!["文字", "图文"].includes(mode)) return "请选择「文字」或「图文」。";
|
|
48
|
-
shared.modes.set(userKey(session), mode === "文字");
|
|
49
|
-
return `已切换为${mode}输出。`;
|
|
50
|
-
});
|
|
51
|
-
return {
|
|
52
|
-
textOnly: /* @__PURE__ */ __name((session) => shared.modes.get(userKey(session)) === true, "textOnly"),
|
|
53
|
-
present(session, image, text) {
|
|
54
|
-
if (!image || shared.modes.get(userKey(session))) return text;
|
|
55
|
-
return [...import_koishi.h.normalize(image), (0, import_koishi.h)("p", {}, import_koishi.h.normalize(text))];
|
|
56
|
-
}
|
|
57
|
-
};
|
|
42
|
+
function present(image, text) {
|
|
43
|
+
return image ? import_koishi.h.normalize(image) : text;
|
|
58
44
|
}
|
|
59
|
-
__name(
|
|
45
|
+
__name(present, "present");
|
|
60
46
|
function registerDirectInput(ctx, name2, matches) {
|
|
61
47
|
const owner = { name: name2, matches };
|
|
62
48
|
state(ctx).owners.add(owner);
|
|
@@ -5103,9 +5089,13 @@ function onColor(background) {
|
|
|
5103
5089
|
}
|
|
5104
5090
|
__name(onColor, "onColor");
|
|
5105
5091
|
var SPACING = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32 };
|
|
5092
|
+
var NEUTRAL_CHROMA = 6;
|
|
5093
|
+
var NEUTRAL_VARIANT_CHROMA = 12;
|
|
5106
5094
|
function scheme(hue, dark = false, options = {}) {
|
|
5107
5095
|
const source = Hct.from(hue, options.chroma ?? 56, 50);
|
|
5108
5096
|
const dynamic = new SchemeTonalSpot(source, dark, 0, "2025");
|
|
5097
|
+
const neutral = /* @__PURE__ */ __name((tone) => hexFromArgb(Hct.from(hue, NEUTRAL_CHROMA, tone).toInt()), "neutral");
|
|
5098
|
+
const variant = /* @__PURE__ */ __name((tone) => hexFromArgb(Hct.from(hue, NEUTRAL_VARIANT_CHROMA, tone).toInt()), "variant");
|
|
5109
5099
|
return {
|
|
5110
5100
|
primary: hexFromArgb(dynamic.primary),
|
|
5111
5101
|
onPrimary: hexFromArgb(dynamic.onPrimary),
|
|
@@ -5123,23 +5113,23 @@ function scheme(hue, dark = false, options = {}) {
|
|
|
5123
5113
|
onError: hexFromArgb(dynamic.onError),
|
|
5124
5114
|
errorContainer: hexFromArgb(dynamic.errorContainer),
|
|
5125
5115
|
onErrorContainer: hexFromArgb(dynamic.onErrorContainer),
|
|
5126
|
-
background:
|
|
5127
|
-
onBackground:
|
|
5128
|
-
surface:
|
|
5129
|
-
onSurface:
|
|
5130
|
-
surfaceVariant:
|
|
5131
|
-
onSurfaceVariant:
|
|
5132
|
-
surfaceDim:
|
|
5133
|
-
surfaceBright:
|
|
5134
|
-
surfaceContainerLowest:
|
|
5135
|
-
surfaceContainerLow:
|
|
5136
|
-
surfaceContainer:
|
|
5137
|
-
surfaceContainerHigh:
|
|
5138
|
-
surfaceContainerHighest:
|
|
5139
|
-
outline:
|
|
5140
|
-
outlineVariant:
|
|
5141
|
-
inverseSurface:
|
|
5142
|
-
inverseOnSurface:
|
|
5116
|
+
background: dark ? neutral(6) : neutral(97),
|
|
5117
|
+
onBackground: dark ? neutral(90) : neutral(10),
|
|
5118
|
+
surface: dark ? neutral(6) : neutral(97),
|
|
5119
|
+
onSurface: dark ? neutral(90) : neutral(10),
|
|
5120
|
+
surfaceVariant: dark ? variant(30) : variant(90),
|
|
5121
|
+
onSurfaceVariant: dark ? variant(80) : variant(30),
|
|
5122
|
+
surfaceDim: dark ? neutral(6) : neutral(88),
|
|
5123
|
+
surfaceBright: dark ? neutral(24) : neutral(98),
|
|
5124
|
+
surfaceContainerLowest: dark ? neutral(4) : neutral(100),
|
|
5125
|
+
surfaceContainerLow: dark ? neutral(10) : neutral(96),
|
|
5126
|
+
surfaceContainer: dark ? neutral(12) : neutral(94),
|
|
5127
|
+
surfaceContainerHigh: dark ? neutral(17) : neutral(92),
|
|
5128
|
+
surfaceContainerHighest: dark ? neutral(22) : neutral(90),
|
|
5129
|
+
outline: dark ? variant(60) : variant(50),
|
|
5130
|
+
outlineVariant: dark ? variant(30) : variant(80),
|
|
5131
|
+
inverseSurface: dark ? neutral(90) : neutral(20),
|
|
5132
|
+
inverseOnSurface: dark ? neutral(20) : neutral(95),
|
|
5143
5133
|
inversePrimary: hexFromArgb(dynamic.inversePrimary),
|
|
5144
5134
|
scrim: hexFromArgb(dynamic.scrim),
|
|
5145
5135
|
shadow: hexFromArgb(dynamic.shadow)
|
|
@@ -5335,12 +5325,86 @@ function textLeftFit(ctx, text, x, y, maxWidth, font, color, offsetY = 0) {
|
|
|
5335
5325
|
textLeft(ctx, ellipsize(text, maxWidth, fontSizeOf(font)), x, y, font, color, offsetY);
|
|
5336
5326
|
}
|
|
5337
5327
|
__name(textLeftFit, "textLeftFit");
|
|
5338
|
-
|
|
5339
|
-
|
|
5328
|
+
var L = {
|
|
5329
|
+
margin: 16,
|
|
5330
|
+
pad: 24,
|
|
5331
|
+
gap: 20,
|
|
5332
|
+
headerTop: 20,
|
|
5333
|
+
seal: 48,
|
|
5334
|
+
headerBottom: 18,
|
|
5335
|
+
footerPadY: 14,
|
|
5336
|
+
footerLine: 20,
|
|
5337
|
+
sectionTitle: 20,
|
|
5338
|
+
sectionGap: 12
|
|
5339
|
+
};
|
|
5340
|
+
var HEADER_H = L.headerTop + L.seal + L.headerBottom;
|
|
5341
|
+
function cornerRect(ctx, x, y, w, h3, [tl, tr, br, bl]) {
|
|
5342
|
+
ctx.beginPath();
|
|
5343
|
+
ctx.moveTo(x + tl, y);
|
|
5344
|
+
ctx.lineTo(x + w - tr, y);
|
|
5345
|
+
if (tr) ctx.arcTo(x + w, y, x + w, y + tr, tr);
|
|
5346
|
+
else ctx.lineTo(x + w, y);
|
|
5347
|
+
ctx.lineTo(x + w, y + h3 - br);
|
|
5348
|
+
if (br) ctx.arcTo(x + w, y + h3, x + w - br, y + h3, br);
|
|
5349
|
+
else ctx.lineTo(x + w, y + h3);
|
|
5350
|
+
ctx.lineTo(x + bl, y + h3);
|
|
5351
|
+
if (bl) ctx.arcTo(x, y + h3, x, y + h3 - bl, bl);
|
|
5352
|
+
else ctx.lineTo(x, y + h3);
|
|
5353
|
+
ctx.lineTo(x, y + tl);
|
|
5354
|
+
if (tl) ctx.arcTo(x, y, x + tl, y, tl);
|
|
5355
|
+
else ctx.lineTo(x, y);
|
|
5356
|
+
ctx.closePath();
|
|
5357
|
+
}
|
|
5358
|
+
__name(cornerRect, "cornerRect");
|
|
5359
|
+
function hline(ctx, x0, x1, y, color, dash) {
|
|
5360
|
+
ctx.save();
|
|
5361
|
+
ctx.strokeStyle = color;
|
|
5362
|
+
ctx.lineWidth = s(1);
|
|
5363
|
+
if (dash) ctx.setLineDash(dash);
|
|
5364
|
+
ctx.beginPath();
|
|
5365
|
+
ctx.moveTo(x0, y);
|
|
5366
|
+
ctx.lineTo(x1, y);
|
|
5367
|
+
ctx.stroke();
|
|
5368
|
+
ctx.restore();
|
|
5369
|
+
}
|
|
5370
|
+
__name(hline, "hline");
|
|
5371
|
+
function footerRows(innerW, lines) {
|
|
5372
|
+
const fs = s(TYPE.labelMedium.size);
|
|
5373
|
+
const avail = innerW - s(L.pad) * 2 - s(24);
|
|
5374
|
+
const rows = [];
|
|
5375
|
+
for (const line of lines) {
|
|
5376
|
+
const lw = line.left ? textWidth(line.left, fs) : 0;
|
|
5377
|
+
const rw = line.right ? textWidth(line.right, fs) : 0;
|
|
5378
|
+
if (line.left && line.right && lw + rw > avail) rows.push({ left: line.left }, { left: line.right });
|
|
5379
|
+
else rows.push(line);
|
|
5380
|
+
}
|
|
5381
|
+
return rows;
|
|
5340
5382
|
}
|
|
5341
|
-
__name(
|
|
5383
|
+
__name(footerRows, "footerRows");
|
|
5384
|
+
function footerHeight(innerW, lines) {
|
|
5385
|
+
return s(L.footerPadY) * 2 + footerRows(innerW, lines).length * s(L.footerLine);
|
|
5386
|
+
}
|
|
5387
|
+
__name(footerHeight, "footerHeight");
|
|
5388
|
+
function drawFooter(ctx, ix, iy, iw, ih, lines) {
|
|
5389
|
+
const rows = footerRows(iw, lines);
|
|
5390
|
+
const h3 = footerHeight(iw, lines);
|
|
5391
|
+
const y = iy + ih - h3;
|
|
5392
|
+
const r = s(SHAPE.extraLarge);
|
|
5393
|
+
ctx.fillStyle = C.surfaceContainerLow;
|
|
5394
|
+
cornerRect(ctx, ix, y, iw, h3, [0, 0, r, r]);
|
|
5395
|
+
ctx.fill();
|
|
5396
|
+
hline(ctx, ix, ix + iw, y, C.outlineVariant);
|
|
5397
|
+
const font = `${s(TYPE.labelMedium.size)}px ${FONT_SANS}`;
|
|
5398
|
+
const maxW = iw - s(L.pad) * 2;
|
|
5399
|
+
rows.forEach((row, i) => {
|
|
5400
|
+
const cy = y + s(L.footerPadY) + s(L.footerLine) * (i + 0.5);
|
|
5401
|
+
if (row.left) textLeft(ctx, ellipsize(row.left, maxW, s(TYPE.labelMedium.size)), ix + s(L.pad), cy, font, C.onSurfaceVariant);
|
|
5402
|
+
if (row.right) textRight(ctx, row.right, ix + iw - s(L.pad), cy, font, C.onSurfaceVariant);
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
__name(drawFooter, "drawFooter");
|
|
5342
5406
|
async function toPng(service, innerW, innerH, draw) {
|
|
5343
|
-
const margin = s(
|
|
5407
|
+
const margin = s(L.margin);
|
|
5344
5408
|
const canvasW = margin * 2 + innerW;
|
|
5345
5409
|
const canvasH = margin * 2 + innerH;
|
|
5346
5410
|
const canvas = await service.createCanvas(canvasW, canvasH);
|
|
@@ -5348,7 +5412,7 @@ async function toPng(service, innerW, innerH, draw) {
|
|
|
5348
5412
|
try {
|
|
5349
5413
|
ctx.fillStyle = C.surface;
|
|
5350
5414
|
ctx.fillRect(0, 0, canvasW, canvasH);
|
|
5351
|
-
|
|
5415
|
+
fillRound(ctx, margin, margin, innerW, innerH, s(SHAPE.extraLarge), C.surfaceContainerLowest);
|
|
5352
5416
|
draw(ctx, margin, margin, innerW, innerH);
|
|
5353
5417
|
return await canvas.toBuffer("image/png");
|
|
5354
5418
|
} finally {
|
|
@@ -5446,700 +5510,354 @@ function drawWordGrid(ctx, x, y, word, size, mask) {
|
|
|
5446
5510
|
}
|
|
5447
5511
|
__name(drawWordGrid, "drawWordGrid");
|
|
5448
5512
|
function drawSeal(ctx, x, y, size, lines) {
|
|
5449
|
-
fillRound(ctx, x, y, size, size, s(SHAPE.
|
|
5450
|
-
const fs = s(TYPE.
|
|
5451
|
-
const lh = fs * 1.
|
|
5452
|
-
|
|
5453
|
-
let cy = y + size / 2 - total / 2 + lh / 2;
|
|
5513
|
+
fillRound(ctx, x, y, size, size, s(SHAPE.large), C.primary);
|
|
5514
|
+
const fs = s(TYPE.titleMedium.size);
|
|
5515
|
+
const lh = fs * 1.1;
|
|
5516
|
+
let cy = y + size / 2 - lh * lines.length / 2 + lh / 2;
|
|
5454
5517
|
for (const line of lines) {
|
|
5455
|
-
textCenter(ctx, line, x + size / 2, cy, `${EMPHASIZED_WEIGHT.title} ${fs}px ${FONT_SANS}`, C.onPrimary);
|
|
5518
|
+
textCenter(ctx, line, x + size / 2, cy, `${EMPHASIZED_WEIGHT.title} ${fs}px ${FONT_SANS}`, C.onPrimary, cjkOffset(fs));
|
|
5456
5519
|
cy += lh;
|
|
5457
5520
|
}
|
|
5458
5521
|
}
|
|
5459
5522
|
__name(drawSeal, "drawSeal");
|
|
5460
|
-
function drawHeader(ctx,
|
|
5461
|
-
const
|
|
5462
|
-
const
|
|
5463
|
-
const
|
|
5464
|
-
|
|
5465
|
-
const
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
ctx,
|
|
5470
|
-
"词意",
|
|
5471
|
-
brandX,
|
|
5472
|
-
innerY + padTop + s(14),
|
|
5473
|
-
`${EMPHASIZED_WEIGHT.headline} ${s(TYPE.headlineMedium.size)}px ${FONT_SANS}`,
|
|
5474
|
-
C.onSurface
|
|
5475
|
-
);
|
|
5476
|
-
textLeft(
|
|
5477
|
-
ctx,
|
|
5478
|
-
"ci yi",
|
|
5479
|
-
brandX,
|
|
5480
|
-
innerY + padTop + s(36),
|
|
5481
|
-
`italic ${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
5482
|
-
C.onSurfaceVariant
|
|
5483
|
-
);
|
|
5484
|
-
const subRight = right ? innerX + innerW - padX - s(150) : innerX + innerW - padX;
|
|
5485
|
-
textLeftFit(
|
|
5486
|
-
ctx,
|
|
5487
|
-
sub,
|
|
5488
|
-
brandX,
|
|
5489
|
-
innerY + padTop + s(58),
|
|
5490
|
-
subRight - brandX,
|
|
5491
|
-
`${s(TYPE.bodySmall.size)}px ${FONT_SANS}`,
|
|
5492
|
-
C.onSurfaceVariant
|
|
5493
|
-
);
|
|
5523
|
+
function drawHeader(ctx, ix, iy, iw, sub, right) {
|
|
5524
|
+
const x = ix + s(L.pad);
|
|
5525
|
+
const top = iy + s(L.headerTop);
|
|
5526
|
+
const seal = s(L.seal);
|
|
5527
|
+
drawSeal(ctx, x, top, seal, ["词", "意"]);
|
|
5528
|
+
const tx = x + seal + s(16);
|
|
5529
|
+
const rightW = right ? Math.max(textWidth(right.big, s(TYPE.headlineMedium.size)), textWidth(right.cap, s(TYPE.labelMedium.size))) + s(24) : 0;
|
|
5530
|
+
textLeft(ctx, "词意", tx, top + s(14), `${EMPHASIZED_WEIGHT.headline} ${s(TYPE.titleLarge.size)}px ${FONT_SANS}`, C.onSurface);
|
|
5531
|
+
textLeftFit(ctx, sub, tx, top + s(37), ix + iw - s(L.pad) - rightW - tx, `${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5494
5532
|
if (right) {
|
|
5495
|
-
const rx =
|
|
5496
|
-
textRight(
|
|
5497
|
-
|
|
5498
|
-
right.big,
|
|
5499
|
-
rx,
|
|
5500
|
-
innerY + padTop + s(16),
|
|
5501
|
-
`${EMPHASIZED_WEIGHT.headline} ${s(TYPE.headlineLarge.size)}px ${FONT_NUM}`,
|
|
5502
|
-
C.primary
|
|
5503
|
-
);
|
|
5504
|
-
textRight(
|
|
5505
|
-
ctx,
|
|
5506
|
-
right.cap,
|
|
5507
|
-
rx,
|
|
5508
|
-
innerY + padTop + s(44),
|
|
5509
|
-
`${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
5510
|
-
C.onSurfaceVariant
|
|
5511
|
-
);
|
|
5533
|
+
const rx = ix + iw - s(L.pad);
|
|
5534
|
+
textRight(ctx, right.big, rx, top + s(16), `${EMPHASIZED_WEIGHT.headline} ${s(TYPE.headlineMedium.size)}px ${FONT_NUM}`, C.primary);
|
|
5535
|
+
textRight(ctx, right.cap, rx, top + s(40), `${s(TYPE.labelMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5512
5536
|
}
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
ctx.lineWidth = s(1);
|
|
5516
|
-
ctx.beginPath();
|
|
5517
|
-
ctx.moveTo(innerX, lineY);
|
|
5518
|
-
ctx.lineTo(innerX + innerW, lineY);
|
|
5519
|
-
ctx.stroke();
|
|
5520
|
-
fillRound(ctx, innerX + padX, lineY - s(1.5), s(54), s(3), s(SHAPE.full), C.primary);
|
|
5521
|
-
return h3;
|
|
5537
|
+
hline(ctx, ix, ix + iw, iy + s(HEADER_H), C.outlineVariant);
|
|
5538
|
+
return s(HEADER_H);
|
|
5522
5539
|
}
|
|
5523
5540
|
__name(drawHeader, "drawHeader");
|
|
5524
|
-
function
|
|
5525
|
-
const
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
fillRound(
|
|
5530
|
-
ctx,
|
|
5531
|
-
innerX + inset,
|
|
5532
|
-
y,
|
|
5533
|
-
innerW - inset * 2,
|
|
5534
|
-
h3 + padY * 2,
|
|
5535
|
-
s(SHAPE.large),
|
|
5536
|
-
C.surfaceContainerLow
|
|
5537
|
-
);
|
|
5538
|
-
ctx.strokeStyle = C.outlineVariant;
|
|
5539
|
-
ctx.lineWidth = s(1);
|
|
5540
|
-
ctx.beginPath();
|
|
5541
|
-
ctx.moveTo(innerX, y);
|
|
5542
|
-
ctx.lineTo(innerX + innerW, y);
|
|
5543
|
-
ctx.stroke();
|
|
5544
|
-
let lx = innerX + padX;
|
|
5545
|
-
const cy = y + padY + h3 / 2;
|
|
5546
|
-
for (const { tier, range } of legendItems) {
|
|
5547
|
-
fillRound(ctx, lx, cy - s(4.5), s(9), s(9), s(SHAPE.full), tier.color);
|
|
5548
|
-
lx += s(9) + s(6);
|
|
5549
|
-
textLeft(ctx, tier.name, lx, cy, `${s(TYPE.labelSmall.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5550
|
-
lx += textWidth(tier.name, s(TYPE.labelSmall.size)) + s(4);
|
|
5551
|
-
textLeft(ctx, range, lx, cy, `${s(TYPE.labelSmall.size)}px ${FONT_NUM}`, C.onSurfaceVariant);
|
|
5552
|
-
lx += textWidth(range, s(TYPE.labelSmall.size)) + s(13);
|
|
5553
|
-
}
|
|
5554
|
-
const tipFont = `${s(TYPE.labelSmall.size)}px ${FONT_SANS}`;
|
|
5555
|
-
const maxTipWidth = legendItems.length ? s(145) : innerW - padX * 2;
|
|
5556
|
-
textRight(
|
|
5557
|
-
ctx,
|
|
5558
|
-
ellipsize(tip, maxTipWidth, s(TYPE.labelSmall.size)),
|
|
5559
|
-
innerX + innerW - padX,
|
|
5560
|
-
cy,
|
|
5561
|
-
tipFont,
|
|
5562
|
-
C.onSurfaceVariant
|
|
5563
|
-
);
|
|
5564
|
-
return h3 + padY * 2;
|
|
5565
|
-
}
|
|
5566
|
-
__name(drawFooter, "drawFooter");
|
|
5567
|
-
function drawLegendFull(ctx, x, y, w) {
|
|
5568
|
-
const colW = w / 2;
|
|
5569
|
-
const rowH = s(30);
|
|
5570
|
-
for (let i = 0; i < TIERS.length; i++) {
|
|
5571
|
-
const t = TIERS[i];
|
|
5572
|
-
const cx = x + i % 2 * colW;
|
|
5573
|
-
const cy = y + Math.floor(i / 2) * rowH;
|
|
5574
|
-
fillRound(ctx, cx, cy - s(4.5), s(9), s(9), s(SHAPE.full), t.color);
|
|
5575
|
-
textLeft(ctx, t.name, cx + s(15), cy, `${s(TYPE.labelSmall.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5576
|
-
textLeft(ctx, tierRange(i), cx + s(52), cy, `${s(TYPE.labelSmall.size)}px ${FONT_NUM}`, C.onSurfaceVariant);
|
|
5577
|
-
textLeft(ctx, t.note, cx + s(100), cy, `${s(TYPE.labelSmall.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5578
|
-
}
|
|
5579
|
-
return rowH * Math.ceil(TIERS.length / 2) + s(8);
|
|
5580
|
-
}
|
|
5581
|
-
__name(drawLegendFull, "drawLegendFull");
|
|
5582
|
-
function drawSectionTitle(ctx, x, y, w, title) {
|
|
5583
|
-
fillRound(ctx, x, y - s(1), s(3), s(12), s(SHAPE.full), C.primary);
|
|
5584
|
-
textLeft(
|
|
5585
|
-
ctx,
|
|
5586
|
-
title,
|
|
5587
|
-
x + s(15),
|
|
5588
|
-
y + s(5),
|
|
5589
|
-
`${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelMedium.size)}px ${FONT_SANS}`,
|
|
5590
|
-
C.onSurfaceVariant
|
|
5591
|
-
);
|
|
5592
|
-
const tw = textWidth(title, s(TYPE.labelMedium.size));
|
|
5593
|
-
ctx.strokeStyle = C.outlineVariant;
|
|
5594
|
-
ctx.lineWidth = s(1);
|
|
5595
|
-
ctx.beginPath();
|
|
5596
|
-
ctx.moveTo(x + s(15) + tw + s(9), y + s(5));
|
|
5597
|
-
ctx.lineTo(x + w, y + s(5));
|
|
5598
|
-
ctx.stroke();
|
|
5599
|
-
return s(24);
|
|
5541
|
+
function drawSectionTitle(ctx, x, y, title) {
|
|
5542
|
+
const h3 = s(L.sectionTitle);
|
|
5543
|
+
fillRound(ctx, x, y + h3 / 2 - s(7), s(4), s(14), s(SHAPE.full), C.primary);
|
|
5544
|
+
textLeft(ctx, title, x + s(12), y + h3 / 2, `${EMPHASIZED_WEIGHT.title} ${s(TYPE.titleSmall.size)}px ${FONT_SANS}`, C.onSurface);
|
|
5545
|
+
return h3 + s(L.sectionGap);
|
|
5600
5546
|
}
|
|
5601
5547
|
__name(drawSectionTitle, "drawSectionTitle");
|
|
5602
5548
|
function drawPanel(ctx, x, y, w, h3) {
|
|
5603
5549
|
fillRound(ctx, x, y, w, h3, s(SHAPE.large), C.surfaceContainer);
|
|
5604
5550
|
}
|
|
5605
5551
|
__name(drawPanel, "drawPanel");
|
|
5552
|
+
var NEAR_BAR_W = 80;
|
|
5606
5553
|
function drawNearBar(ctx, x, y, pct, tier) {
|
|
5607
|
-
const barW = s(
|
|
5554
|
+
const barW = s(NEAR_BAR_W);
|
|
5608
5555
|
const barH = s(8);
|
|
5609
5556
|
const gap = s(3);
|
|
5610
5557
|
const fillW = Math.max(barH, barW * pct / 100);
|
|
5611
5558
|
const top = y - barH / 2;
|
|
5612
5559
|
fillRound(ctx, x, top, fillW, barH, s(SHAPE.full), tier.color);
|
|
5613
5560
|
if (fillW + gap < barW) {
|
|
5614
|
-
fillRound(
|
|
5615
|
-
ctx,
|
|
5616
|
-
x + fillW + gap,
|
|
5617
|
-
top,
|
|
5618
|
-
barW - fillW - gap,
|
|
5619
|
-
barH,
|
|
5620
|
-
s(SHAPE.full),
|
|
5621
|
-
C.surfaceContainerHighest
|
|
5622
|
-
);
|
|
5561
|
+
fillRound(ctx, x + fillW + gap, top, barW - fillW - gap, barH, s(SHAPE.full), C.surfaceContainerHighest);
|
|
5623
5562
|
}
|
|
5624
|
-
textLeft(
|
|
5625
|
-
ctx,
|
|
5626
|
-
tier.name,
|
|
5627
|
-
x + barW + s(9),
|
|
5628
|
-
y,
|
|
5629
|
-
`${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelMedium.size)}px ${FONT_SANS}`,
|
|
5630
|
-
tier.color
|
|
5631
|
-
);
|
|
5563
|
+
textLeft(ctx, tier.name, x + barW + s(10), y, `${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelLarge.size)}px ${FONT_SANS}`, tier.color);
|
|
5632
5564
|
}
|
|
5633
5565
|
__name(drawNearBar, "drawNearBar");
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
const
|
|
5645
|
-
return
|
|
5566
|
+
var TABLE = { head: 32, row: 56, gap: 32 };
|
|
5567
|
+
var COLS = [
|
|
5568
|
+
{ key: "no", label: "序", w: 30 },
|
|
5569
|
+
{ key: "nb", label: "更近的词", w: 84 },
|
|
5570
|
+
{ key: "gw", label: "猜测", w: 96 },
|
|
5571
|
+
{ key: "fb", label: "更远的词", w: 84 },
|
|
5572
|
+
{ key: "rk", label: "排名", w: 88 },
|
|
5573
|
+
{ key: "mt", label: "亲疏", w: 136 }
|
|
5574
|
+
];
|
|
5575
|
+
function boardHeight(rows) {
|
|
5576
|
+
const gaps = rows.filter((r) => r.gapBefore).length;
|
|
5577
|
+
return s(TABLE.head) + rows.length * s(TABLE.row) + gaps * s(TABLE.gap);
|
|
5646
5578
|
}
|
|
5647
|
-
__name(
|
|
5648
|
-
function drawBoardTable(ctx, x, y, w,
|
|
5649
|
-
const
|
|
5650
|
-
const
|
|
5651
|
-
const
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
const
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
const rk0 = nb1 + cols.nb;
|
|
5669
|
-
const mt0 = rk0 + cols.rk;
|
|
5670
|
-
const noX = bx + s(2);
|
|
5671
|
-
const meterBlockW = s(88) + s(9) + textWidth("咫尺", s(TYPE.labelMedium.size));
|
|
5672
|
-
const mtX = mt0 + Math.max(0, (cols.mt - meterBlockW) / 2);
|
|
5673
|
-
const headY = y;
|
|
5674
|
-
const fs = s(TYPE.labelSmall.size);
|
|
5675
|
-
textLeft(ctx, "序", noX, headY, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5676
|
-
textCenter(ctx, "更近 ◀", nb0 + cols.nb / 2, headY, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5677
|
-
textCenter(ctx, "猜测", gw0 + cols.gw / 2, headY, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5678
|
-
textCenter(ctx, "▶ 更远", nb1 + cols.nb / 2, headY, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5679
|
-
textCenter(ctx, "排名", rk0 + cols.rk / 2, headY, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5680
|
-
textCenter(ctx, "亲疏", mt0 + cols.mt / 2, headY, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5681
|
-
let ry = y + s(22);
|
|
5579
|
+
__name(boardHeight, "boardHeight");
|
|
5580
|
+
function drawBoardTable(ctx, x, y, w, rows, total) {
|
|
5581
|
+
const sum = COLS.reduce((a, c) => a + c.w, 0);
|
|
5582
|
+
const scale = w / s(sum);
|
|
5583
|
+
const edges = {};
|
|
5584
|
+
let cx = x;
|
|
5585
|
+
for (const col of COLS) {
|
|
5586
|
+
const cw = s(col.w) * scale;
|
|
5587
|
+
edges[col.key] = { x: cx, w: cw };
|
|
5588
|
+
cx += cw;
|
|
5589
|
+
}
|
|
5590
|
+
const mid = /* @__PURE__ */ __name((key) => edges[key].x + edges[key].w / 2, "mid");
|
|
5591
|
+
const labelFont = `${s(TYPE.labelMedium.size)}px ${FONT_SANS}`;
|
|
5592
|
+
const headY = y + s(TABLE.head) / 2;
|
|
5593
|
+
textLeft(ctx, "序", x + s(8), headY, labelFont, C.onSurfaceVariant);
|
|
5594
|
+
for (const col of COLS.slice(1, 5)) textCenter(ctx, col.label, mid(col.key), headY, labelFont, C.onSurfaceVariant);
|
|
5595
|
+
const meterW = s(NEAR_BAR_W) + s(10) + textWidth("咫尺", s(TYPE.labelLarge.size));
|
|
5596
|
+
const meterX = edges.mt.x + Math.max(0, (edges.mt.w - meterW) / 2);
|
|
5597
|
+
textCenter(ctx, "亲疏", meterX + meterW / 2, headY, labelFont, C.onSurfaceVariant);
|
|
5598
|
+
hline(ctx, x, x + w, y + s(TABLE.head), C.outline);
|
|
5599
|
+
let ry = y + s(TABLE.head);
|
|
5682
5600
|
let ordinal = 0;
|
|
5683
|
-
for (const row of
|
|
5601
|
+
for (const row of rows) {
|
|
5684
5602
|
if (row.gapBefore) {
|
|
5685
|
-
ry
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
);
|
|
5694
|
-
ctx.strokeStyle = C.outlineVariant;
|
|
5695
|
-
ctx.beginPath();
|
|
5696
|
-
ctx.moveTo(cx, ry + s(20));
|
|
5697
|
-
ctx.lineTo(cx + tableW, ry + s(20));
|
|
5698
|
-
ctx.stroke();
|
|
5699
|
-
ry += s(28);
|
|
5603
|
+
const cy = ry + s(TABLE.gap) / 2;
|
|
5604
|
+
const label = `中间还有 ${row.gapBefore} 词`;
|
|
5605
|
+
const lw = textWidth(label, s(TYPE.labelMedium.size)) + s(24);
|
|
5606
|
+
hline(ctx, x, x + w / 2 - lw / 2, cy, C.outlineVariant, [s(4), s(4)]);
|
|
5607
|
+
hline(ctx, x + w / 2 + lw / 2, x + w, cy, C.outlineVariant, [s(4), s(4)]);
|
|
5608
|
+
textCenter(ctx, label, x + w / 2, cy, labelFont, C.onSurfaceVariant);
|
|
5609
|
+
ry += s(TABLE.gap);
|
|
5610
|
+
} else if (ordinal > 0) {
|
|
5611
|
+
hline(ctx, x, x + w, ry, C.outlineVariant);
|
|
5700
5612
|
}
|
|
5701
5613
|
ordinal += 1;
|
|
5702
5614
|
const { history, fresh } = row;
|
|
5703
5615
|
const tier = tierOf(history.rank);
|
|
5704
|
-
const
|
|
5616
|
+
const rowH = s(TABLE.row);
|
|
5705
5617
|
const midY = ry + rowH / 2;
|
|
5706
5618
|
if (fresh) {
|
|
5707
|
-
fillRound(ctx,
|
|
5708
|
-
fillRound(ctx,
|
|
5619
|
+
fillRound(ctx, x, ry + s(3), w, rowH - s(6), s(SHAPE.medium), FRESH);
|
|
5620
|
+
fillRound(ctx, x, ry + s(12), s(4), rowH - s(24), s(SHAPE.full), C.primary);
|
|
5709
5621
|
}
|
|
5710
|
-
ctx.
|
|
5711
|
-
ctx.
|
|
5712
|
-
ctx.
|
|
5713
|
-
ctx.
|
|
5714
|
-
ctx.lineTo(cx + tableW, ry);
|
|
5715
|
-
ctx.stroke();
|
|
5716
|
-
textLeft(
|
|
5717
|
-
ctx,
|
|
5718
|
-
String(ordinal).padStart(2, "0"),
|
|
5719
|
-
noX,
|
|
5720
|
-
midY,
|
|
5721
|
-
`${s(TYPE.labelMedium.size)}px ${FONT_NUM}`,
|
|
5722
|
-
C.onSurfaceVariant
|
|
5723
|
-
);
|
|
5724
|
-
const nbX = nb0 + (cols.nb - gridWidth(history.leftHint, "sm")) / 2;
|
|
5725
|
-
drawWordGrid(ctx, nbX, midY - s(13), history.leftHint, "sm", 0);
|
|
5726
|
-
const gwX = gw0 + (cols.gw - gridWidth(history.guess, "md")) / 2;
|
|
5727
|
-
drawWordGrid(ctx, gwX, midY - s(17), history.guess, "md");
|
|
5728
|
-
const rbX = nb1 + (cols.nb - gridWidth(history.rightHint, "sm")) / 2;
|
|
5729
|
-
drawWordGrid(ctx, rbX, midY - s(13), history.rightHint, "sm", 1);
|
|
5622
|
+
textLeft(ctx, String(ordinal).padStart(2, "0"), x + s(8), midY, `${s(TYPE.labelLarge.size)}px ${FONT_NUM}`, C.onSurfaceVariant);
|
|
5623
|
+
drawWordGrid(ctx, mid("nb") - gridWidth(history.leftHint, "sm") / 2, midY - s(GRID.sm.cell) / 2, history.leftHint, "sm", 0);
|
|
5624
|
+
drawWordGrid(ctx, mid("gw") - gridWidth(history.guess, "md") / 2, midY - s(GRID.md.cell) / 2, history.guess, "md");
|
|
5625
|
+
drawWordGrid(ctx, mid("fb") - gridWidth(history.rightHint, "sm") / 2, midY - s(GRID.sm.cell) / 2, history.rightHint, "sm", 1);
|
|
5730
5626
|
const rankText = String(history.rank);
|
|
5731
|
-
const
|
|
5732
|
-
const
|
|
5733
|
-
const
|
|
5734
|
-
const
|
|
5735
|
-
|
|
5736
|
-
textLeft(ctx, "#",
|
|
5737
|
-
|
|
5738
|
-
drawNearBar(ctx, mtX, midY, pct, tier);
|
|
5627
|
+
const hashFs = s(TYPE.labelLarge.size);
|
|
5628
|
+
const rankFs = s(TYPE.titleLarge.size);
|
|
5629
|
+
const block = textWidth("#", hashFs) + s(2) + textWidth(rankText, rankFs);
|
|
5630
|
+
const left = mid("rk") - block / 2;
|
|
5631
|
+
textLeft(ctx, "#", left, midY, `${hashFs}px ${FONT_NUM}`, C.onSurfaceVariant);
|
|
5632
|
+
textLeft(ctx, rankText, left + textWidth("#", hashFs) + s(2), midY, `${EMPHASIZED_WEIGHT.title} ${rankFs}px ${FONT_NUM}`, tier.color);
|
|
5633
|
+
drawNearBar(ctx, meterX, midY, nearness(history.rank, total), tier);
|
|
5739
5634
|
ry += rowH;
|
|
5740
5635
|
}
|
|
5741
5636
|
return ry - y;
|
|
5742
5637
|
}
|
|
5743
5638
|
__name(drawBoardTable, "drawBoardTable");
|
|
5639
|
+
var LEGEND_H = 28;
|
|
5640
|
+
function drawLegendRow(ctx, x, y, w) {
|
|
5641
|
+
const slot = w / TIERS.length;
|
|
5642
|
+
const fs = s(TYPE.labelMedium.size);
|
|
5643
|
+
const cy = y + s(LEGEND_H) / 2;
|
|
5644
|
+
TIERS.forEach((tier, i) => {
|
|
5645
|
+
const label = `${tier.name} ${tierRange(i)}`;
|
|
5646
|
+
const itemW = s(8) + s(6) + textWidth(label, fs);
|
|
5647
|
+
const lx = x + slot * i + (slot - itemW) / 2;
|
|
5648
|
+
fillRound(ctx, lx, cy - s(4), s(8), s(8), s(SHAPE.full), tier.color);
|
|
5649
|
+
textLeft(ctx, tier.name, lx + s(14), cy, `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5650
|
+
textLeft(ctx, tierRange(i), lx + s(14) + textWidth(tier.name + " ", fs), cy, `${fs}px ${FONT_NUM}`, C.onSurfaceVariant);
|
|
5651
|
+
});
|
|
5652
|
+
}
|
|
5653
|
+
__name(drawLegendRow, "drawLegendRow");
|
|
5654
|
+
var BOARD_W = 680;
|
|
5744
5655
|
function renderBoardCard(service, opts) {
|
|
5745
|
-
const
|
|
5656
|
+
const innerW = s(BOARD_W);
|
|
5657
|
+
const footer = [{ left: opts.tip }];
|
|
5658
|
+
const tableH = boardHeight(opts.rows);
|
|
5659
|
+
const innerH = s(HEADER_H) + s(L.gap) + tableH + s(L.gap) + s(LEGEND_H) + s(L.gap) + footerHeight(innerW, footer);
|
|
5746
5660
|
const best = opts.rows.length ? `#${opts.rows.reduce((m, r) => Math.min(m, r.history.rank), Infinity)}` : "—";
|
|
5747
5661
|
return toPng(service, innerW, innerH, (ctx, ix, iy, iw, ih) => {
|
|
5748
|
-
let y = iy + drawHeader(ctx, ix, iy, iw, `每日挑战 · 第 ${opts.attempts} 次猜测`, {
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
y
|
|
5753
|
-
|
|
5754
|
-
drawFooter(
|
|
5755
|
-
ctx,
|
|
5756
|
-
ix,
|
|
5757
|
-
iy + ih - s(60),
|
|
5758
|
-
iw,
|
|
5759
|
-
TIERS.map((tier, i) => ({ tier, range: tierRange(i) })),
|
|
5760
|
-
opts.tip
|
|
5761
|
-
);
|
|
5662
|
+
let y = iy + drawHeader(ctx, ix, iy, iw, `每日挑战 · 第 ${opts.attempts} 次猜测`, { big: best, cap: "当前最佳" });
|
|
5663
|
+
y += s(L.gap);
|
|
5664
|
+
y += drawBoardTable(ctx, ix + s(L.pad), y, iw - s(L.pad) * 2, opts.rows, opts.total);
|
|
5665
|
+
y += s(L.gap);
|
|
5666
|
+
drawLegendRow(ctx, ix + s(L.pad), y, iw - s(L.pad) * 2);
|
|
5667
|
+
drawFooter(ctx, ix, iy, iw, ih, footer);
|
|
5762
5668
|
});
|
|
5763
5669
|
}
|
|
5764
5670
|
__name(renderBoardCard, "renderBoardCard");
|
|
5765
5671
|
function renderIntroCard(service, opts) {
|
|
5766
|
-
const innerW = s(
|
|
5767
|
-
const
|
|
5672
|
+
const innerW = s(BOARD_W);
|
|
5673
|
+
const footer = [{ left: "一日一词,猜中即止,次日零点换题", right: "词库与语义排序来自「词影」" }];
|
|
5674
|
+
const usage2 = [
|
|
5675
|
+
["开始", "ciyi.猜 山水", `开题并报一个两字词${opts.middleware ? ";开题后可直接发词" : ""}`],
|
|
5676
|
+
["切换", "ciyi.裸词 开/关", "改本频道的续猜方式"],
|
|
5677
|
+
["排行", "ciyi.排行榜", "看谁猜中得最多"]
|
|
5678
|
+
];
|
|
5679
|
+
const notes = [
|
|
5680
|
+
"这一行是说:「企业」与今日答案的意思相近程度,排在第 467 位。",
|
|
5681
|
+
"两侧各藏一字:左边是比它更近的词,右边是比它更远的词。",
|
|
5682
|
+
"名次越小,离答案越近;#1 就是答案本身。"
|
|
5683
|
+
];
|
|
5684
|
+
const sample = [{ history: { guess: "企业", rank: 467, leftHint: "良好", rightHint: "地产" } }];
|
|
5685
|
+
const panelPad = 16;
|
|
5686
|
+
const usageRow = 36;
|
|
5687
|
+
const noteLine = 22;
|
|
5688
|
+
const legendRow = 30;
|
|
5689
|
+
const sectionHead = s(L.sectionTitle) + s(L.sectionGap);
|
|
5690
|
+
const usageH = s(panelPad) * 2 + usage2.length * s(usageRow);
|
|
5691
|
+
const readH = s(panelPad) * 2 + boardHeight(sample) + s(12) + notes.length * s(noteLine);
|
|
5692
|
+
const tierH = s(panelPad) * 2 + Math.ceil(TIERS.length / 2) * s(legendRow);
|
|
5693
|
+
const innerH = s(HEADER_H) + s(L.gap) + sectionHead + usageH + s(L.gap) + sectionHead + readH + s(L.gap) + sectionHead + tierH + s(L.gap) + footerHeight(innerW, footer);
|
|
5768
5694
|
return toPng(service, innerW, innerH, (ctx, ix, iy, iw, ih) => {
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5695
|
+
const x = ix + s(L.pad);
|
|
5696
|
+
const w = iw - s(L.pad) * 2;
|
|
5697
|
+
let y = iy + drawHeader(ctx, ix, iy, iw, "按意思远近找词 · 每日一题", { big: opts.words.toLocaleString(), cap: "词库容量" });
|
|
5698
|
+
y += s(L.gap);
|
|
5699
|
+
y += drawSectionTitle(ctx, x, y, "玩法");
|
|
5700
|
+
drawPanel(ctx, x, y, w, usageH);
|
|
5701
|
+
usage2.forEach(([key, command, desc], i) => {
|
|
5702
|
+
const cy = y + s(panelPad) + s(usageRow) * (i + 0.5);
|
|
5703
|
+
textLeft(ctx, key, x + s(panelPad), cy, `${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelLarge.size)}px ${FONT_SANS}`, C.primary);
|
|
5704
|
+
textLeft(ctx, command, x + s(72), cy, `${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`, C.onSurface);
|
|
5705
|
+
textLeftFit(ctx, desc, x + s(200), cy, w - s(200) - s(panelPad), `${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5772
5706
|
});
|
|
5773
|
-
y += s(
|
|
5774
|
-
y += drawSectionTitle(ctx,
|
|
5775
|
-
y
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
);
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
);
|
|
5796
|
-
textLeft(ctx, "切换", ix + s(38), y + s(52), `${s(TYPE.labelLarge.size)}px ${FONT_SANS}`, C.primary);
|
|
5797
|
-
textLeft(
|
|
5798
|
-
ctx,
|
|
5799
|
-
"ciyi.裸词 开/关",
|
|
5800
|
-
ix + s(96),
|
|
5801
|
-
y + s(52),
|
|
5802
|
-
`${s(TYPE.labelLarge.size)}px ${FONT_SANS}`,
|
|
5803
|
-
C.onSurface
|
|
5804
|
-
);
|
|
5805
|
-
textLeftFit(
|
|
5806
|
-
ctx,
|
|
5807
|
-
"临时改本频道的续猜方式",
|
|
5808
|
-
ix + s(230),
|
|
5809
|
-
y + s(52),
|
|
5810
|
-
iw - s(268),
|
|
5811
|
-
`${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`,
|
|
5812
|
-
C.onSurface
|
|
5813
|
-
);
|
|
5814
|
-
textLeft(ctx, "排行", ix + s(38), y + s(80), `${s(TYPE.labelLarge.size)}px ${FONT_SANS}`, C.primary);
|
|
5815
|
-
textLeft(
|
|
5816
|
-
ctx,
|
|
5817
|
-
"ciyi.排行榜",
|
|
5818
|
-
ix + s(96),
|
|
5819
|
-
y + s(80),
|
|
5820
|
-
`${s(TYPE.labelLarge.size)}px ${FONT_SANS}`,
|
|
5821
|
-
C.onSurface
|
|
5822
|
-
);
|
|
5823
|
-
textLeft(
|
|
5824
|
-
ctx,
|
|
5825
|
-
"看谁猜中得最多",
|
|
5826
|
-
ix + s(230),
|
|
5827
|
-
y + s(80),
|
|
5828
|
-
`${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`,
|
|
5829
|
-
C.onSurface
|
|
5830
|
-
);
|
|
5831
|
-
y += p1h + s(19);
|
|
5832
|
-
y += drawSectionTitle(ctx, ix + s(22), y, iw - s(44), "读板");
|
|
5833
|
-
y += s(11);
|
|
5834
|
-
const sampleH = s(172);
|
|
5835
|
-
drawPanel(ctx, ix + s(22), y, iw - s(44), sampleH);
|
|
5836
|
-
drawBoardTable(
|
|
5837
|
-
ctx,
|
|
5838
|
-
ix + s(22),
|
|
5839
|
-
y + s(10),
|
|
5840
|
-
iw - s(44),
|
|
5841
|
-
{
|
|
5842
|
-
rows: [{ history: { guess: "企业", rank: 467, leftHint: "良好", rightHint: "地产" } }],
|
|
5843
|
-
total: opts.total,
|
|
5844
|
-
attempts: 1,
|
|
5845
|
-
tip: ""
|
|
5846
|
-
},
|
|
5847
|
-
s(50)
|
|
5848
|
-
);
|
|
5849
|
-
textLeft(
|
|
5850
|
-
ctx,
|
|
5851
|
-
"这一行是说:「企业」与今日答案的意思相近程度,排在第 467 位。",
|
|
5852
|
-
ix + s(38),
|
|
5853
|
-
y + s(104),
|
|
5854
|
-
`${s(TYPE.bodySmall.size)}px ${FONT_SANS}`,
|
|
5855
|
-
C.onSurfaceVariant
|
|
5856
|
-
);
|
|
5857
|
-
textLeft(
|
|
5858
|
-
ctx,
|
|
5859
|
-
"两侧空格各藏一字:左邻更近,右邻更远。",
|
|
5860
|
-
ix + s(38),
|
|
5861
|
-
y + s(126),
|
|
5862
|
-
`${s(TYPE.bodySmall.size)}px ${FONT_SANS}`,
|
|
5863
|
-
C.onSurfaceVariant
|
|
5864
|
-
);
|
|
5865
|
-
textLeft(
|
|
5866
|
-
ctx,
|
|
5867
|
-
"名次越小,离答案越近;#1 就是答案本身。",
|
|
5868
|
-
ix + s(38),
|
|
5869
|
-
y + s(148),
|
|
5870
|
-
`${s(TYPE.bodySmall.size)}px ${FONT_SANS}`,
|
|
5871
|
-
C.onSurfaceVariant
|
|
5872
|
-
);
|
|
5873
|
-
y += sampleH + s(19);
|
|
5874
|
-
y += drawSectionTitle(ctx, ix + s(22), y, iw - s(44), "亲疏");
|
|
5875
|
-
y += s(11);
|
|
5876
|
-
const tierH = s(112);
|
|
5877
|
-
drawPanel(ctx, ix + s(22), y, iw - s(44), tierH);
|
|
5878
|
-
drawLegendFull(ctx, ix + s(38), y + s(18), iw - s(80));
|
|
5879
|
-
y += tierH;
|
|
5880
|
-
drawFooter(
|
|
5881
|
-
ctx,
|
|
5882
|
-
ix,
|
|
5883
|
-
iy + ih - s(60),
|
|
5884
|
-
iw,
|
|
5885
|
-
[],
|
|
5886
|
-
"词库与语义排序来自「词影」"
|
|
5887
|
-
);
|
|
5888
|
-
textLeft(
|
|
5889
|
-
ctx,
|
|
5890
|
-
"一日一词,猜中即止,次日零点换题",
|
|
5891
|
-
ix + s(22),
|
|
5892
|
-
iy + ih - s(30),
|
|
5893
|
-
`${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
5894
|
-
C.onSurfaceVariant
|
|
5895
|
-
);
|
|
5707
|
+
y += usageH + s(L.gap);
|
|
5708
|
+
y += drawSectionTitle(ctx, x, y, "读板");
|
|
5709
|
+
drawPanel(ctx, x, y, w, readH);
|
|
5710
|
+
let ry = y + s(panelPad);
|
|
5711
|
+
ry += drawBoardTable(ctx, x + s(panelPad), ry, w - s(panelPad) * 2, sample, opts.total) + s(12);
|
|
5712
|
+
for (const note of notes) {
|
|
5713
|
+
textLeftFit(ctx, note, x + s(panelPad), ry + s(noteLine) / 2, w - s(panelPad) * 2, `${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5714
|
+
ry += s(noteLine);
|
|
5715
|
+
}
|
|
5716
|
+
y += readH + s(L.gap);
|
|
5717
|
+
y += drawSectionTitle(ctx, x, y, "亲疏");
|
|
5718
|
+
drawPanel(ctx, x, y, w, tierH);
|
|
5719
|
+
const colW = (w - s(panelPad) * 2) / 2;
|
|
5720
|
+
const fs = s(TYPE.bodyMedium.size);
|
|
5721
|
+
TIERS.forEach((tier, i) => {
|
|
5722
|
+
const cx = x + s(panelPad) + i % 2 * colW;
|
|
5723
|
+
const cy = y + s(panelPad) + s(legendRow) * (Math.floor(i / 2) + 0.5);
|
|
5724
|
+
fillRound(ctx, cx, cy - s(5), s(10), s(10), s(SHAPE.full), tier.color);
|
|
5725
|
+
textLeft(ctx, tier.name, cx + s(18), cy, `${EMPHASIZED_WEIGHT.label} ${fs}px ${FONT_SANS}`, tier.color);
|
|
5726
|
+
textLeft(ctx, tierRange(i), cx + s(60), cy, `${fs}px ${FONT_NUM}`, C.onSurfaceVariant);
|
|
5727
|
+
textLeftFit(ctx, tier.note, cx + s(124), cy, colW - s(132), `${fs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5728
|
+
});
|
|
5729
|
+
drawFooter(ctx, ix, iy, iw, ih, footer);
|
|
5896
5730
|
});
|
|
5897
5731
|
}
|
|
5898
5732
|
__name(renderIntroCard, "renderIntroCard");
|
|
5899
5733
|
function renderWinCard(service, opts) {
|
|
5900
|
-
const innerW = s(
|
|
5901
|
-
const
|
|
5734
|
+
const innerW = s(BOARD_W);
|
|
5735
|
+
const quip = opts.closest === null ? "一击即中,今日无需第二次落笔" : `从 #${opts.closest} 一步跨到 #1`;
|
|
5736
|
+
const footer = [
|
|
5737
|
+
{ left: `${opts.username} 拿下今日一词 · ${quip}` },
|
|
5738
|
+
{ left: opts.canStartToday ? "发送「ciyi.猜 两字词」开启今日新题" : "明日零点换新题 · 发送「ciyi.排行榜」看看榜单" }
|
|
5739
|
+
];
|
|
5740
|
+
const panelPad = 20;
|
|
5741
|
+
const labelH = 20;
|
|
5742
|
+
const answerW = gridWidth(opts.answer, "lg");
|
|
5743
|
+
const chipH = 32;
|
|
5744
|
+
const chipGap = 8;
|
|
5745
|
+
const chipFs = s(TYPE.labelLarge.size);
|
|
5746
|
+
const w = innerW - s(L.pad) * 2;
|
|
5747
|
+
const rightX = s(panelPad) + answerW + s(32);
|
|
5748
|
+
const rightW = w - rightX - s(panelPad);
|
|
5749
|
+
const chips = [];
|
|
5750
|
+
let cx = 0;
|
|
5751
|
+
let row = 0;
|
|
5752
|
+
for (const word of opts.neighbors) {
|
|
5753
|
+
const fitted = ellipsize(word, s(120), chipFs);
|
|
5754
|
+
const cw = textWidth(fitted, chipFs) + s(28);
|
|
5755
|
+
if (cx > 0 && cx + cw > rightW) {
|
|
5756
|
+
cx = 0;
|
|
5757
|
+
row += 1;
|
|
5758
|
+
}
|
|
5759
|
+
chips.push({ word: fitted, x: cx, row, w: cw });
|
|
5760
|
+
cx += cw + s(chipGap);
|
|
5761
|
+
}
|
|
5762
|
+
const chipRows = chips.length ? row + 1 : 1;
|
|
5763
|
+
const leftH = s(labelH) + s(12) + s(GRID.lg.cell);
|
|
5764
|
+
const rightH = s(labelH) + s(12) + chipRows * s(chipH) + (chipRows - 1) * s(chipGap);
|
|
5765
|
+
const revealH = s(panelPad) * 2 + Math.max(leftH, rightH);
|
|
5766
|
+
const statH = 76;
|
|
5767
|
+
const innerH = s(HEADER_H) + s(L.gap) + revealH + s(12) + s(statH) + s(L.gap) + footerHeight(innerW, footer);
|
|
5902
5768
|
return toPng(service, innerW, innerH, (ctx, ix, iy, iw, ih) => {
|
|
5903
|
-
|
|
5904
|
-
y
|
|
5905
|
-
|
|
5906
|
-
drawPanel(ctx,
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
const
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
const pw = textWidth(fittedWord, s(TYPE.labelLarge.size)) + s(20);
|
|
5920
|
-
if (nx !== ax && nx + pw > chipRight) {
|
|
5921
|
-
nx = ax;
|
|
5922
|
-
ny += s(36);
|
|
5923
|
-
}
|
|
5924
|
-
if (ny + s(14) > y + revealH - s(12)) break;
|
|
5925
|
-
fillRound(ctx, nx, ny - s(14), pw, s(28), s(SHAPE.full), C.secondaryContainer);
|
|
5926
|
-
textCenter(
|
|
5927
|
-
ctx,
|
|
5928
|
-
fittedWord,
|
|
5929
|
-
nx + pw / 2,
|
|
5930
|
-
ny,
|
|
5931
|
-
chipFont,
|
|
5932
|
-
C.onSecondaryContainer,
|
|
5933
|
-
cjkOffset(s(TYPE.labelLarge.size))
|
|
5934
|
-
);
|
|
5935
|
-
nx += pw + s(7);
|
|
5769
|
+
const x = ix + s(L.pad);
|
|
5770
|
+
let y = iy + drawHeader(ctx, ix, iy, iw, "每日挑战 · 已猜中");
|
|
5771
|
+
y += s(L.gap);
|
|
5772
|
+
drawPanel(ctx, x, y, w, revealH);
|
|
5773
|
+
const top = y + s(panelPad);
|
|
5774
|
+
const labelFont = `${s(TYPE.labelLarge.size)}px ${FONT_SANS}`;
|
|
5775
|
+
textLeft(ctx, "今日答案", x + s(panelPad), top + s(labelH) / 2, labelFont, C.onSurfaceVariant);
|
|
5776
|
+
drawWordGrid(ctx, x + s(panelPad), top + s(labelH) + s(12), opts.answer, "lg");
|
|
5777
|
+
const rx = x + rightX;
|
|
5778
|
+
if (chips.length) {
|
|
5779
|
+
textLeft(ctx, "意思最近的词", rx, top + s(labelH) / 2, labelFont, C.onSurfaceVariant);
|
|
5780
|
+
const chipTop = top + s(labelH) + s(12);
|
|
5781
|
+
for (const chip of chips) {
|
|
5782
|
+
const cy = chipTop + chip.row * (s(chipH) + s(chipGap));
|
|
5783
|
+
fillRound(ctx, rx + chip.x, cy, chip.w, s(chipH), s(SHAPE.full), C.secondaryContainer);
|
|
5784
|
+
textCenter(ctx, chip.word, rx + chip.x + chip.w / 2, cy + s(chipH) / 2, `${chipFs}px ${FONT_SANS}`, C.onSecondaryContainer, cjkOffset(chipFs));
|
|
5936
5785
|
}
|
|
5937
5786
|
} else {
|
|
5938
|
-
textLeft(
|
|
5939
|
-
ctx,
|
|
5940
|
-
"今日一词,就此收笔。",
|
|
5941
|
-
ax,
|
|
5942
|
-
y + s(50),
|
|
5943
|
-
`${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`,
|
|
5944
|
-
C.onSurfaceVariant
|
|
5945
|
-
);
|
|
5787
|
+
textLeft(ctx, "今日一词,就此收笔。", rx, top + s(labelH) + s(12) + s(GRID.lg.cell) / 2, `${s(TYPE.bodyLarge.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5946
5788
|
}
|
|
5947
|
-
y += revealH + s(
|
|
5948
|
-
const statW = (iw - s(44) - s(20)) / 3;
|
|
5789
|
+
y += revealH + s(12);
|
|
5949
5790
|
const stats = [
|
|
5950
5791
|
{ n: String(opts.attempts), u: "次", c: "本局猜测" },
|
|
5951
5792
|
{ n: opts.closest === null ? "—" : `#${opts.closest}`, u: "", c: "此前最接近" },
|
|
5952
5793
|
{ n: String(opts.score), u: "次", c: "累计猜中" }
|
|
5953
5794
|
];
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
);
|
|
5965
|
-
if (stats[i].u) {
|
|
5966
|
-
textLeft(
|
|
5967
|
-
ctx,
|
|
5968
|
-
stats[i].u,
|
|
5969
|
-
sx + s(14) + textWidth(stats[i].n, s(TYPE.headlineSmall.size)) + s(3),
|
|
5970
|
-
y + s(26),
|
|
5971
|
-
`${s(TYPE.labelMedium.size)}px ${FONT_SANS}`,
|
|
5972
|
-
C.onSurfaceVariant
|
|
5973
|
-
);
|
|
5974
|
-
}
|
|
5975
|
-
textLeft(
|
|
5976
|
-
ctx,
|
|
5977
|
-
stats[i].c,
|
|
5978
|
-
sx + s(14),
|
|
5979
|
-
y + s(44),
|
|
5980
|
-
`${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
5981
|
-
C.onSurfaceVariant
|
|
5982
|
-
);
|
|
5983
|
-
}
|
|
5984
|
-
const quip = opts.closest === null ? "一击即中,今日无需第二次落笔" : `从 #${opts.closest} 一步跨到 #1`;
|
|
5985
|
-
const footerTip = opts.canStartToday ? "ciyi.猜 山水 · 开启今日新题" : "明日零点换新题 · ciyi.排行榜";
|
|
5986
|
-
drawFooter(
|
|
5987
|
-
ctx,
|
|
5988
|
-
ix,
|
|
5989
|
-
iy + ih - s(60),
|
|
5990
|
-
iw,
|
|
5991
|
-
[],
|
|
5992
|
-
footerTip
|
|
5993
|
-
);
|
|
5994
|
-
const footerFont = `${s(TYPE.labelSmall.size)}px ${FONT_SANS}`;
|
|
5995
|
-
const footerTipWidth = textWidth(footerTip, s(TYPE.labelSmall.size));
|
|
5996
|
-
textLeftFit(
|
|
5997
|
-
ctx,
|
|
5998
|
-
`${opts.username} 拿下今日一词 · ${quip}`,
|
|
5999
|
-
ix + s(22),
|
|
6000
|
-
iy + ih - s(30),
|
|
6001
|
-
iw - s(62) - footerTipWidth,
|
|
6002
|
-
footerFont,
|
|
6003
|
-
C.onSurfaceVariant
|
|
6004
|
-
);
|
|
5795
|
+
const statW = (w - s(12) * 2) / 3;
|
|
5796
|
+
stats.forEach((stat, i) => {
|
|
5797
|
+
const sx = x + i * (statW + s(12));
|
|
5798
|
+
drawPanel(ctx, sx, y, statW, s(statH));
|
|
5799
|
+
const numFs = s(TYPE.headlineSmall.size);
|
|
5800
|
+
textLeft(ctx, stat.n, sx + s(16), y + s(28), `${EMPHASIZED_WEIGHT.headline} ${numFs}px ${FONT_NUM}`, C.primary);
|
|
5801
|
+
if (stat.u) textLeft(ctx, stat.u, sx + s(16) + textWidth(stat.n, numFs) + s(4), y + s(31), `${s(TYPE.labelLarge.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5802
|
+
textLeft(ctx, stat.c, sx + s(16), y + s(56), `${s(TYPE.labelMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5803
|
+
});
|
|
5804
|
+
drawFooter(ctx, ix, iy, iw, ih, footer);
|
|
6005
5805
|
});
|
|
6006
5806
|
}
|
|
6007
5807
|
__name(renderWinCard, "renderWinCard");
|
|
6008
5808
|
function renderRankCard(service, opts) {
|
|
6009
|
-
const innerW = s(
|
|
6010
|
-
const
|
|
6011
|
-
const
|
|
6012
|
-
const
|
|
6013
|
-
const
|
|
6014
|
-
const
|
|
6015
|
-
const
|
|
6016
|
-
const hiddenH = opts.hidden > 0 ? s(36) : 0;
|
|
6017
|
-
const footerGap = s(20);
|
|
6018
|
-
const footerH = s(60);
|
|
6019
|
-
const contentH = opts.entries.length ? opts.entries.length * rowH + hiddenH : emptyH;
|
|
6020
|
-
const innerH = headerH + contentGap + contentH + footerGap + footerH;
|
|
5809
|
+
const innerW = s(600);
|
|
5810
|
+
const footer = [{ left: "每猜中一日之词,记一次", right: "发送「ciyi.猜 两字词」参与" }];
|
|
5811
|
+
const rowH = 56;
|
|
5812
|
+
const hiddenH = opts.hidden > 0 ? 40 : 0;
|
|
5813
|
+
const emptyH = 96;
|
|
5814
|
+
const contentH = opts.entries.length ? s(8) * 2 + opts.entries.length * s(rowH) + s(hiddenH) : s(emptyH);
|
|
5815
|
+
const innerH = s(HEADER_H) + s(L.gap) + contentH + s(L.gap) + footerHeight(innerW, footer);
|
|
6021
5816
|
return toPng(service, innerW, innerH, (ctx, ix, iy, iw, ih) => {
|
|
6022
|
-
const
|
|
6023
|
-
const
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
big: String(opts.players),
|
|
6028
|
-
cap: "上榜人数"
|
|
6029
|
-
});
|
|
6030
|
-
y += s(20);
|
|
6031
|
-
drawPanel(ctx, panelX, y, panelW, contentH);
|
|
5817
|
+
const x = ix + s(L.pad);
|
|
5818
|
+
const w = iw - s(L.pad) * 2;
|
|
5819
|
+
let y = iy + drawHeader(ctx, ix, iy, iw, "每日挑战 · 累计猜中", { big: String(opts.players), cap: "上榜人数" });
|
|
5820
|
+
y += s(L.gap);
|
|
5821
|
+
drawPanel(ctx, x, y, w, contentH);
|
|
6032
5822
|
if (!opts.entries.length) {
|
|
6033
|
-
textCenter(
|
|
6034
|
-
|
|
6035
|
-
"排行榜还空着",
|
|
6036
|
-
panelX + panelW / 2,
|
|
6037
|
-
y + s(31),
|
|
6038
|
-
`${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`,
|
|
6039
|
-
C.onSurfaceVariant
|
|
6040
|
-
);
|
|
6041
|
-
textCenter(
|
|
6042
|
-
ctx,
|
|
6043
|
-
"今日第一个猜中的人,会写在这里。",
|
|
6044
|
-
panelX + panelW / 2,
|
|
6045
|
-
y + s(61),
|
|
6046
|
-
`${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`,
|
|
6047
|
-
C.onSurfaceVariant
|
|
6048
|
-
);
|
|
5823
|
+
textCenter(ctx, "排行榜还空着", x + w / 2, y + s(34), `${EMPHASIZED_WEIGHT.title} ${s(TYPE.titleMedium.size)}px ${FONT_SANS}`, C.onSurface);
|
|
5824
|
+
textCenter(ctx, "今日第一个猜中的人,会写在这里。", x + w / 2, y + s(62), `${s(TYPE.bodyMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
6049
5825
|
} else {
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
if (i > 0) {
|
|
6058
|
-
ctx.strokeStyle = C.outlineVariant;
|
|
6059
|
-
ctx.beginPath();
|
|
6060
|
-
ctx.moveTo(contentLeft, ry);
|
|
6061
|
-
ctx.lineTo(contentRight, ry);
|
|
6062
|
-
ctx.stroke();
|
|
6063
|
-
}
|
|
6064
|
-
const posSize = s(30);
|
|
6065
|
-
const px = contentLeft;
|
|
6066
|
-
const py = midY - posSize / 2;
|
|
5826
|
+
const left = x + s(16);
|
|
5827
|
+
const right = x + w - s(16);
|
|
5828
|
+
let ry = y + s(8);
|
|
5829
|
+
opts.entries.forEach((e, i) => {
|
|
5830
|
+
const midY = ry + s(rowH) / 2;
|
|
5831
|
+
if (e.me) fillRound(ctx, x + s(6), ry + s(3), w - s(12), s(rowH) - s(6), s(SHAPE.medium), FRESH);
|
|
5832
|
+
if (i > 0 && !e.me && !opts.entries[i - 1].me) hline(ctx, left, right, ry, C.outlineVariant);
|
|
6067
5833
|
const podium = [MEDAL.gold, MEDAL.silver, MEDAL.bronze];
|
|
6068
5834
|
const pc = i < 3 ? { bg: podium[i], fg: onColor(podium[i]) } : { bg: C.surfaceContainerHighest, fg: C.onSurfaceVariant };
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
String(i + 1),
|
|
6073
|
-
px + posSize / 2,
|
|
6074
|
-
midY,
|
|
6075
|
-
`${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelLarge.size)}px ${FONT_NUM}`,
|
|
6076
|
-
pc.fg
|
|
6077
|
-
);
|
|
6078
|
-
const name2 = e.username || "无名氏";
|
|
6079
|
-
const nameX = px + posSize + s(14);
|
|
6080
|
-
const nameFont = `${s(TYPE.titleMedium.size)}px ${FONT_SANS}`;
|
|
5835
|
+
const pos = s(32);
|
|
5836
|
+
fillRound(ctx, left, midY - pos / 2, pos, pos, s(SHAPE.full), pc.bg);
|
|
5837
|
+
textCenter(ctx, String(i + 1), left + pos / 2, midY, `${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelLarge.size)}px ${FONT_NUM}`, pc.fg);
|
|
6081
5838
|
const score = String(e.score);
|
|
6082
|
-
const
|
|
6083
|
-
const
|
|
6084
|
-
const
|
|
6085
|
-
const
|
|
6086
|
-
const
|
|
6087
|
-
const
|
|
6088
|
-
const
|
|
6089
|
-
|
|
6090
|
-
Math.max(0, scoreLeft - s(16) - nameX - meWidth),
|
|
6091
|
-
s(TYPE.titleMedium.size)
|
|
6092
|
-
);
|
|
6093
|
-
textLeft(ctx, fittedName, nameX, midY, nameFont, C.onSurface);
|
|
5839
|
+
const scoreFs = s(TYPE.titleLarge.size);
|
|
5840
|
+
const unitFs = s(TYPE.labelLarge.size);
|
|
5841
|
+
const scoreRight = right - textWidth("次", unitFs) - s(6);
|
|
5842
|
+
const nameX = left + pos + s(14);
|
|
5843
|
+
const meW = e.me ? textWidth("我", s(TYPE.labelMedium.size)) + s(20) : 0;
|
|
5844
|
+
const nameFs = s(TYPE.titleMedium.size);
|
|
5845
|
+
const name2 = ellipsize(e.username || "无名氏", scoreRight - textWidth(score, scoreFs) - s(20) - nameX - meW, nameFs);
|
|
5846
|
+
textLeft(ctx, name2, nameX, midY, `${nameFs}px ${FONT_SANS}`, C.onSurface);
|
|
6094
5847
|
if (e.me) {
|
|
6095
|
-
const
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
"我",
|
|
6099
|
-
nameX + nw + s(8),
|
|
6100
|
-
midY,
|
|
6101
|
-
`${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
6102
|
-
C.primary
|
|
6103
|
-
);
|
|
5848
|
+
const bx = nameX + textWidth(name2, nameFs) + s(8);
|
|
5849
|
+
fillRound(ctx, bx, midY - s(10), meW - s(8), s(20), s(SHAPE.full), C.primary);
|
|
5850
|
+
textCenter(ctx, "我", bx + (meW - s(8)) / 2, midY, `${EMPHASIZED_WEIGHT.label} ${s(TYPE.labelMedium.size)}px ${FONT_SANS}`, C.onPrimary);
|
|
6104
5851
|
}
|
|
6105
|
-
textRight(
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
midY,
|
|
6110
|
-
`${EMPHASIZED_WEIGHT.title} ${scoreFont}px ${FONT_NUM}`,
|
|
6111
|
-
C.onSurface
|
|
6112
|
-
);
|
|
6113
|
-
textRight(ctx, "次", contentRight, midY, `${unitFont}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
6114
|
-
ry += rowH;
|
|
6115
|
-
}
|
|
5852
|
+
textRight(ctx, score, scoreRight, midY, `${EMPHASIZED_WEIGHT.title} ${scoreFs}px ${FONT_NUM}`, C.onSurface);
|
|
5853
|
+
textRight(ctx, "次", right, midY, `${unitFs}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
5854
|
+
ry += s(rowH);
|
|
5855
|
+
});
|
|
6116
5856
|
if (opts.hidden > 0) {
|
|
6117
|
-
|
|
6118
|
-
ctx,
|
|
6119
|
-
`…… 另有 ${opts.hidden} 人在榜`,
|
|
6120
|
-
contentLeft,
|
|
6121
|
-
ry + s(16),
|
|
6122
|
-
`${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
6123
|
-
C.onSurfaceVariant
|
|
6124
|
-
);
|
|
5857
|
+
textCenter(ctx, `另有 ${opts.hidden} 人在榜`, x + w / 2, ry + s(hiddenH) / 2, `${s(TYPE.labelMedium.size)}px ${FONT_SANS}`, C.onSurfaceVariant);
|
|
6125
5858
|
}
|
|
6126
5859
|
}
|
|
6127
|
-
drawFooter(
|
|
6128
|
-
ctx,
|
|
6129
|
-
ix,
|
|
6130
|
-
iy + ih - s(60),
|
|
6131
|
-
iw,
|
|
6132
|
-
[],
|
|
6133
|
-
"ciyi.猜 山水"
|
|
6134
|
-
);
|
|
6135
|
-
textLeft(
|
|
6136
|
-
ctx,
|
|
6137
|
-
"每猜中一日之词,记一次",
|
|
6138
|
-
panelX,
|
|
6139
|
-
iy + ih - s(30),
|
|
6140
|
-
`${s(TYPE.labelSmall.size)}px ${FONT_SANS}`,
|
|
6141
|
-
C.onSurfaceVariant
|
|
6142
|
-
);
|
|
5860
|
+
drawFooter(ctx, ix, iy, iw, ih, footer);
|
|
6143
5861
|
});
|
|
6144
5862
|
}
|
|
6145
5863
|
__name(renderRankCard, "renderRankCard");
|
|
@@ -6212,7 +5930,6 @@ function resolveMiddlewareSwitch(config, override, action) {
|
|
|
6212
5930
|
}
|
|
6213
5931
|
__name(resolveMiddlewareSwitch, "resolveMiddlewareSwitch");
|
|
6214
5932
|
function apply(ctx, cfg) {
|
|
6215
|
-
const presentation = usePresentation(ctx, "ciyi");
|
|
6216
5933
|
ctx.model.extend(
|
|
6217
5934
|
"ciyi",
|
|
6218
5935
|
{
|
|
@@ -6485,7 +6202,7 @@ function apply(ctx, cfg) {
|
|
|
6485
6202
|
async function send(session, content) {
|
|
6486
6203
|
const ids = await session.send(content);
|
|
6487
6204
|
const messageId = ids?.[0];
|
|
6488
|
-
if (
|
|
6205
|
+
if (!cfg.retractDelay || !messageId) return;
|
|
6489
6206
|
const previous = lastMessage.get(session.channelId);
|
|
6490
6207
|
if (previous) {
|
|
6491
6208
|
const passed = Date.now() - previous.timestamp;
|
|
@@ -6499,12 +6216,12 @@ function apply(ctx, cfg) {
|
|
|
6499
6216
|
}
|
|
6500
6217
|
__name(send, "send");
|
|
6501
6218
|
async function sendCard(session, render, fallback) {
|
|
6502
|
-
const image =
|
|
6219
|
+
const image = await renderCard(render);
|
|
6503
6220
|
const prefix = [];
|
|
6504
6221
|
if (cfg.quoteReply && session.messageId) prefix.push(import_koishi2.h.quote(session.messageId));
|
|
6505
6222
|
if (cfg.atReply) prefix.push(import_koishi2.h.at(session.userId), (0, import_koishi2.h)("p"));
|
|
6506
6223
|
if (image) {
|
|
6507
|
-
await send(session, [...prefix, ...import_koishi2.h.normalize(
|
|
6224
|
+
await send(session, [...prefix, ...import_koishi2.h.normalize(present(image, import_koishi2.h.text(fallback)))]);
|
|
6508
6225
|
return;
|
|
6509
6226
|
}
|
|
6510
6227
|
await send(session, [...prefix, import_koishi2.h.text(fallback)]);
|
package/lib/ux.d.ts
CHANGED
|
@@ -3,18 +3,11 @@ type Owner = {
|
|
|
3
3
|
name: string;
|
|
4
4
|
matches: (session: Session) => boolean | Promise<boolean>;
|
|
5
5
|
};
|
|
6
|
-
/**
|
|
7
|
-
export declare function
|
|
8
|
-
textOnly: (session: Session) => boolean;
|
|
9
|
-
present(session: Session, image: h.Fragment | null | undefined, text: h.Fragment): h.Fragment;
|
|
10
|
-
};
|
|
6
|
+
/** 渲染图优先,只发图片;图片缺失时才退回等价文字。 */
|
|
7
|
+
export declare function present(image: h.Fragment | null | undefined, text: h.Fragment): h.Fragment;
|
|
11
8
|
/** Plugins register their active input grammar; overlapping messages are never executed. */
|
|
12
9
|
export declare function registerDirectInput(ctx: Context, name: string, matches: Owner['matches']): void;
|
|
13
10
|
export declare function directInputConflict(ctx: Context, session: Session): Promise<boolean>;
|
|
14
|
-
/**
|
|
15
|
-
export declare function promptInput(session: Session
|
|
16
|
-
export declare const IMAGE_FAILURE = "\u56FE\u7247\u6682\u65F6\u65E0\u6CD5\u751F\u6210\uFF0C\u5DF2\u4FDD\u7559\u6587\u5B57\u5185\u5BB9\u3002\u53EF\u4EE5\u7A0D\u540E\u91CD\u8BD5\uFF1B\u82E5\u6301\u7EED\u5931\u8D25\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002";
|
|
17
|
-
/** Preserve companion text when image messages are switched to text mode. */
|
|
18
|
-
export declare function withoutImages(content: h.Fragment): h[];
|
|
19
|
-
export declare function isTextOnly(ctx: Context, session: Session): boolean;
|
|
11
|
+
/** 等待下一句回复。提问本身已经写明了可回复的内容与「取消」,这里不再追加说明。 */
|
|
12
|
+
export declare function promptInput(session: Session): Promise<string | undefined>;
|
|
20
13
|
export {};
|