@zzclub/pipeline 0.1.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.
Files changed (98) hide show
  1. package/README.md +559 -0
  2. package/dist/assets/imgx/assets/browser/obstacle-flow.d.ts +27 -0
  3. package/dist/assets/imgx/assets/browser/obstacle-flow.js +326 -0
  4. package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
  5. package/dist/assets/imgx/assets/icons/clover.svg +15 -0
  6. package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
  7. package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
  8. package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
  9. package/dist/assets/imgx/assets/icons/openclaw-logo.svg +22 -0
  10. package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
  11. package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +25 -0
  12. package/dist/assets/imgx/assets/templates/ascii-portrait-3-4.html +342 -0
  13. package/dist/assets/imgx/assets/templates/ascii-portrait-tile.html +144 -0
  14. package/dist/assets/imgx/assets/templates/longform-3-4.html +179 -0
  15. package/dist/assets/imgx/assets/templates/poster-3-4.html +359 -0
  16. package/dist/assets/imgx/assets/templates/tips-3-4.html +300 -0
  17. package/dist/assets/imgx/assets/templates/wechat-cover-split.html +307 -0
  18. package/dist/assets/imgx/assets/templates/x-like-posts.html +246 -0
  19. package/dist/assets/wechat-preview/assets/browser-dist/.vite/manifest.json +314 -0
  20. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  21. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  22. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  23. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  24. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  25. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  26. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  27. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  28. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  29. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  30. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  31. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  32. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  33. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  34. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  35. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  36. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  37. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  38. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  39. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  40. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  41. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  42. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  43. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  46. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  47. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  48. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  49. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  50. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  51. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  52. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  53. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  54. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  55. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  56. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  57. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  58. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  59. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  60. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  61. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  62. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  63. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  64. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  65. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  66. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  67. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  68. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  69. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  70. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  71. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  72. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  73. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  74. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  75. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  76. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  77. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  78. package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  79. package/dist/assets/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
  80. package/dist/assets/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +1 -0
  81. package/dist/assets/wechat-preview/assets/browser-dist/editor-export.js +969 -0
  82. package/dist/assets/wechat-preview/assets/templates/export-shell.html +38 -0
  83. package/dist/cli.js +404 -0
  84. package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +33 -0
  85. package/dist/node_modules/@chenglou/pretext/dist/analysis.js +1063 -0
  86. package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +1 -0
  87. package/dist/node_modules/@chenglou/pretext/dist/bidi.js +175 -0
  88. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +4 -0
  89. package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +979 -0
  90. package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +70 -0
  91. package/dist/node_modules/@chenglou/pretext/dist/layout.js +496 -0
  92. package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +36 -0
  93. package/dist/node_modules/@chenglou/pretext/dist/line-break.js +820 -0
  94. package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +28 -0
  95. package/dist/node_modules/@chenglou/pretext/dist/measurement.js +219 -0
  96. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +51 -0
  97. package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +401 -0
  98. package/package.json +42 -0
@@ -0,0 +1,342 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <!--
6
+ Template: ascii-portrait-3-4
7
+ Ratio: 3:4 (900×1200)
8
+ 用途: 多字符头像海报
9
+ Parameters:
10
+ {{BG_COLOR}} — 背景色
11
+ {{TITLE}} — 标题
12
+ {{FOOTER_TEXT}} — 底部文字
13
+ {{ICON_PATH}} — 顶部 logo
14
+ {{AVATAR_PATH}} — 头像路径
15
+ {{ASCII_CONFIG_JSON}} — 配置
16
+ {{FONT_PATH}} — 标题字体
17
+ {{BODY_FONT_PATH}} — 正文字体
18
+ -->
19
+ <style>
20
+ @font-face {
21
+ font-family: "AlimamaShuHeiTi";
22
+ src: url("{{FONT_PATH}}") format("truetype");
23
+ }
24
+ @font-face {
25
+ font-family: "LXGWWenKai";
26
+ src: url("{{BODY_FONT_PATH}}") format("truetype");
27
+ font-weight: 400;
28
+ }
29
+ * { margin: 0; padding: 0; box-sizing: border-box; }
30
+ :root {
31
+ --page-bg: #141414;
32
+ --title-color: #f2e7d6;
33
+ --subtitle-color: rgb(242 231 214 / 0.78);
34
+ --frame-glow: radial-gradient(circle at top left, rgb(255 255 255 / 0.04), transparent 34%);
35
+ --frame-fill: linear-gradient(180deg, rgb(255 255 255 / 0.02), rgb(255 255 255 / 0.01));
36
+ --frame-border: rgb(255 255 255 / 0.08);
37
+ --frame-inner: rgb(255 255 255 / 0.03);
38
+ --footer-color: rgb(242 231 214 / 0.72);
39
+ }
40
+ body[data-theme="light"] {
41
+ --page-bg: #f5f0eb;
42
+ --title-color: #b88a37;
43
+ --subtitle-color: #8a7763;
44
+ --frame-glow: radial-gradient(circle at top left, rgb(255 255 255 / 0.14), transparent 36%);
45
+ --frame-fill: linear-gradient(180deg, rgb(255 255 255 / 0.1), rgb(255 255 255 / 0.04));
46
+ --frame-border: rgb(71 57 39 / 0.08);
47
+ --frame-inner: rgb(255 255 255 / 0.08);
48
+ --footer-color: #8a7763;
49
+ }
50
+ body {
51
+ width: 900px;
52
+ height: 1200px;
53
+ background: var(--page-bg);
54
+ color: var(--title-color);
55
+ overflow: hidden;
56
+ position: relative;
57
+ font-family: "LXGWWenKai", "SF Mono", "Menlo", monospace;
58
+ }
59
+ .logo {
60
+ position: absolute;
61
+ top: 42px;
62
+ left: 42px;
63
+ width: 62px;
64
+ height: 62px;
65
+ border-radius: 50%;
66
+ object-fit: cover;
67
+ z-index: 3;
68
+ }
69
+ .title {
70
+ position: absolute;
71
+ top: 124px;
72
+ left: 42px;
73
+ right: 42px;
74
+ font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
75
+ font-size: 54px;
76
+ line-height: 1.1;
77
+ letter-spacing: 0.01em;
78
+ color: var(--title-color);
79
+ z-index: 3;
80
+ }
81
+ .subtitle {
82
+ position: absolute;
83
+ top: 194px;
84
+ left: 42px;
85
+ right: 42px;
86
+ font-size: 22px;
87
+ line-height: 1.45;
88
+ color: var(--subtitle-color);
89
+ z-index: 3;
90
+ }
91
+ .frame {
92
+ position: absolute;
93
+ left: 42px;
94
+ right: 42px;
95
+ top: 246px;
96
+ bottom: 96px;
97
+ border-radius: 30px;
98
+ background:
99
+ var(--frame-glow),
100
+ var(--frame-fill);
101
+ border: 1px solid var(--frame-border);
102
+ box-shadow: inset 0 0 0 1px var(--frame-inner);
103
+ overflow: hidden;
104
+ }
105
+ canvas {
106
+ width: 100%;
107
+ height: 100%;
108
+ display: block;
109
+ }
110
+ .footer {
111
+ position: absolute;
112
+ left: 42px;
113
+ bottom: 34px;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 10px;
117
+ font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
118
+ font-size: 24px;
119
+ color: var(--footer-color);
120
+ z-index: 3;
121
+ }
122
+ .footer img {
123
+ width: 30px;
124
+ height: 30px;
125
+ border-radius: 50%;
126
+ object-fit: cover;
127
+ }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <img class="logo" src="{{ICON_PATH}}" alt="logo">
132
+ <div class="title">{{TITLE}}</div>
133
+ <div class="subtitle">多字符亮度映射头像。不同头像都能自动重建字符轮廓与颜色气质。</div>
134
+ <div class="frame">
135
+ <canvas id="portrait-canvas" width="816" height="858"></canvas>
136
+ </div>
137
+ <div class="footer">
138
+ <img src="{{ICON_PATH}}" alt="footer logo">
139
+ <span>✦ {{FOOTER_TEXT}}</span>
140
+ </div>
141
+
142
+ <script id="ascii-config" type="application/json">{{ASCII_CONFIG_JSON}}</script>
143
+ <script>
144
+ const config = JSON.parse(document.getElementById("ascii-config").textContent || "{}");
145
+ const canvas = document.getElementById("portrait-canvas");
146
+ const ctx = canvas.getContext("2d");
147
+ const img = new Image();
148
+ img.crossOrigin = "anonymous";
149
+ img.src = config.avatarPath;
150
+
151
+ function averageRegion(data, width, x0, y0, sampleW, sampleH) {
152
+ let r = 0;
153
+ let g = 0;
154
+ let b = 0;
155
+ let count = 0;
156
+ for (let y = y0; y < y0 + sampleH; y++) {
157
+ for (let x = x0; x < x0 + sampleW; x++) {
158
+ const idx = (y * width + x) * 4;
159
+ r += data[idx];
160
+ g += data[idx + 1];
161
+ b += data[idx + 2];
162
+ count += 1;
163
+ }
164
+ }
165
+ return {
166
+ r: Math.round(r / count),
167
+ g: Math.round(g / count),
168
+ b: Math.round(b / count),
169
+ };
170
+ }
171
+
172
+ function luminance(color) {
173
+ return (0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b) / 255;
174
+ }
175
+
176
+ function clamp(value, min, max) {
177
+ return Math.max(min, Math.min(max, value));
178
+ }
179
+
180
+ function resolveColumns() {
181
+ const requested = Number(config.columns || 0);
182
+ if (requested > 0) return requested;
183
+ const sourceMin = Math.min(img.width, img.height);
184
+ const widthScale = canvas.width / 816;
185
+ const raw = Math.round((sourceMin / 5) * widthScale);
186
+ return clamp(raw, 54, 84);
187
+ }
188
+
189
+ function resolveFontSize(cols) {
190
+ const requested = Number(config.fontSize || 0);
191
+ if (requested > 0) return requested;
192
+ const cellWidth = canvas.width / cols;
193
+ return clamp(Math.round(cellWidth * 0.92), 11, 16);
194
+ }
195
+
196
+ function resolveLineHeight(fontSize) {
197
+ const requested = Number(config.lineHeight || 0);
198
+ if (requested > 0) return requested;
199
+ return clamp(Math.round(fontSize * 1.08), fontSize + 1, fontSize + 3);
200
+ }
201
+
202
+ function analyzeTheme() {
203
+ const offscreen = document.createElement("canvas");
204
+ offscreen.width = 36;
205
+ offscreen.height = 36;
206
+ const offctx = offscreen.getContext("2d");
207
+
208
+ const srcRatio = img.width / img.height;
209
+ const dstRatio = offscreen.width / offscreen.height;
210
+ let sx = 0;
211
+ let sy = 0;
212
+ let sw = img.width;
213
+ let sh = img.height;
214
+ if (srcRatio > dstRatio) {
215
+ sw = img.height * dstRatio;
216
+ sx = (img.width - sw) / 2;
217
+ } else {
218
+ sh = img.width / dstRatio;
219
+ sy = (img.height - sh) / 2;
220
+ }
221
+
222
+ offctx.drawImage(img, sx, sy, sw, sh, 0, 0, offscreen.width, offscreen.height);
223
+ const { data } = offctx.getImageData(0, 0, offscreen.width, offscreen.height);
224
+
225
+ let centerLum = 0;
226
+ let edgeLum = 0;
227
+ let faceCoreLum = 0;
228
+ let centerCount = 0;
229
+ let edgeCount = 0;
230
+ let faceCoreCount = 0;
231
+ let faceCoreBrightCount = 0;
232
+ let overallLum = 0;
233
+
234
+ for (let y = 0; y < offscreen.height; y++) {
235
+ for (let x = 0; x < offscreen.width; x++) {
236
+ const idx = (y * offscreen.width + x) * 4;
237
+ const color = {
238
+ r: data[idx],
239
+ g: data[idx + 1],
240
+ b: data[idx + 2],
241
+ };
242
+ const light = luminance(color);
243
+ overallLum += light;
244
+
245
+ const inCenter = x >= 9 && x <= 26 && y >= 7 && y <= 27;
246
+ const inEdge = x <= 5 || x >= 30 || y <= 4 || y >= 31;
247
+ const inFaceCore = x >= 13 && x <= 22 && y >= 11 && y <= 23;
248
+
249
+ if (inCenter) {
250
+ centerLum += light;
251
+ centerCount += 1;
252
+ } else if (inEdge) {
253
+ edgeLum += light;
254
+ edgeCount += 1;
255
+ }
256
+
257
+ if (inFaceCore) {
258
+ faceCoreLum += light;
259
+ faceCoreCount += 1;
260
+ if (light >= 0.72) faceCoreBrightCount += 1;
261
+ }
262
+ }
263
+ }
264
+
265
+ const centerAvg = centerCount > 0 ? centerLum / centerCount : 0.5;
266
+ const edgeAvg = edgeCount > 0 ? edgeLum / edgeCount : 0.5;
267
+ const faceCoreAvg = faceCoreCount > 0 ? faceCoreLum / faceCoreCount : centerAvg;
268
+ const faceCoreBrightRatio = faceCoreCount > 0 ? faceCoreBrightCount / faceCoreCount : 0;
269
+ const overallAvg = overallLum / (offscreen.width * offscreen.height);
270
+
271
+ if (faceCoreBrightRatio >= 0.22 || faceCoreAvg >= 0.62) return "dark";
272
+ if (centerAvg >= edgeAvg + 0.08) return "dark";
273
+ if (edgeAvg >= centerAvg + 0.08) return "light";
274
+ return overallAvg > 0.52 ? "dark" : "light";
275
+ }
276
+
277
+ function resolveCanvasBg(theme) {
278
+ if (config.bgColor && config.bgColor !== "auto") return config.bgColor;
279
+ return theme === "dark" ? "#111214" : "#f5f0eb";
280
+ }
281
+
282
+ function drawAsciiPortrait() {
283
+ const chars = String(config.chars || "@#X$%WM*+=-").split("");
284
+ const cols = resolveColumns();
285
+ const fontSize = resolveFontSize(cols);
286
+ const lineHeight = resolveLineHeight(fontSize);
287
+ const rows = Math.floor(canvas.height / lineHeight);
288
+ const cellWidth = canvas.width / cols;
289
+ const gridHeight = rows * lineHeight;
290
+ const offsetY = (canvas.height - gridHeight) / 2;
291
+ const offscreen = document.createElement("canvas");
292
+ offscreen.width = cols;
293
+ offscreen.height = rows;
294
+ const offctx = offscreen.getContext("2d");
295
+
296
+ const srcRatio = img.width / img.height;
297
+ const dstRatio = canvas.width / gridHeight;
298
+ let sx = 0;
299
+ let sy = 0;
300
+ let sw = img.width;
301
+ let sh = img.height;
302
+ if (srcRatio > dstRatio) {
303
+ sw = img.height * dstRatio;
304
+ sx = (img.width - sw) / 2;
305
+ } else {
306
+ sh = img.width / dstRatio;
307
+ sy = (img.height - sh) / 2;
308
+ }
309
+
310
+ offctx.drawImage(img, sx, sy, sw, sh, 0, 0, cols, rows);
311
+ const imageData = offctx.getImageData(0, 0, cols, rows);
312
+ const { data } = imageData;
313
+
314
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
315
+ const theme = analyzeTheme();
316
+ document.body.dataset.theme = theme;
317
+ ctx.fillStyle = resolveCanvasBg(theme);
318
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
319
+
320
+ ctx.font = `700 ${fontSize}px Menlo, Monaco, 'Courier New', monospace`;
321
+ ctx.textBaseline = "top";
322
+ ctx.textAlign = "center";
323
+
324
+ for (let row = 0; row < rows; row++) {
325
+ for (let col = 0; col < cols; col++) {
326
+ const color = averageRegion(data, cols, col, row, 1, 1);
327
+ const light = luminance(color);
328
+ const index = Math.max(0, Math.min(chars.length - 1, Math.floor((1 - light) * (chars.length - 1))));
329
+ const char = chars[index] || " ";
330
+ ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
331
+ ctx.fillText(char, col * cellWidth + cellWidth / 2, offsetY + row * lineHeight);
332
+ }
333
+ }
334
+ }
335
+
336
+ img.onload = () => {
337
+ drawAsciiPortrait();
338
+ document.body.dataset.ready = "1";
339
+ };
340
+ </script>
341
+ </body>
342
+ </html>
@@ -0,0 +1,144 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <!--
6
+ Template: ascii-portrait-tile
7
+ 用途: 原子 ASCII 头像块,可作为组合式障碍物
8
+ -->
9
+ <style>
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+ body {
12
+ width: {{CANVAS_WIDTH}}px;
13
+ height: {{CANVAS_HEIGHT}}px;
14
+ background: {{BG_COLOR}};
15
+ overflow: hidden;
16
+ }
17
+ canvas {
18
+ display: block;
19
+ width: {{CANVAS_WIDTH}}px;
20
+ height: {{CANVAS_HEIGHT}}px;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <canvas id="portrait-canvas" width="{{CANVAS_WIDTH}}" height="{{CANVAS_HEIGHT}}"></canvas>
26
+ <script id="ascii-config" type="application/json">{{ASCII_CONFIG_JSON}}</script>
27
+ <script>
28
+ const config = JSON.parse(document.getElementById("ascii-config").textContent || "{}");
29
+ const canvas = document.getElementById("portrait-canvas");
30
+ const ctx = canvas.getContext("2d");
31
+ const img = new Image();
32
+ img.crossOrigin = "anonymous";
33
+ img.src = config.avatarPath;
34
+
35
+ function averageRegion(data, width, x0, y0, sampleW, sampleH) {
36
+ let r = 0;
37
+ let g = 0;
38
+ let b = 0;
39
+ let count = 0;
40
+ for (let y = y0; y < y0 + sampleH; y++) {
41
+ for (let x = x0; x < x0 + sampleW; x++) {
42
+ const idx = (y * width + x) * 4;
43
+ r += data[idx];
44
+ g += data[idx + 1];
45
+ b += data[idx + 2];
46
+ count += 1;
47
+ }
48
+ }
49
+ return {
50
+ r: Math.round(r / count),
51
+ g: Math.round(g / count),
52
+ b: Math.round(b / count),
53
+ };
54
+ }
55
+
56
+ function luminance(color) {
57
+ return (0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b) / 255;
58
+ }
59
+
60
+ function clamp(value, min, max) {
61
+ return Math.max(min, Math.min(max, value));
62
+ }
63
+
64
+ function resolveColumns() {
65
+ const requested = Number(config.columns || 0);
66
+ if (requested > 0) return requested;
67
+ const sourceMin = Math.min(img.width, img.height);
68
+ const widthScale = canvas.width / 900;
69
+ const raw = Math.round((sourceMin / 5) * widthScale);
70
+ return clamp(raw, 56, 96);
71
+ }
72
+
73
+ function resolveFontSize(cols) {
74
+ const requested = Number(config.fontSize || 0);
75
+ if (requested > 0) return requested;
76
+ const cellWidth = canvas.width / cols;
77
+ return clamp(Math.round(cellWidth * 0.9), 11, 16);
78
+ }
79
+
80
+ function resolveLineHeight(fontSize) {
81
+ const requested = Number(config.lineHeight || 0);
82
+ if (requested > 0) return requested;
83
+ return clamp(Math.round(fontSize * 1.08), fontSize + 1, fontSize + 3);
84
+ }
85
+
86
+ function drawAsciiPortrait() {
87
+ const chars = String(config.chars || "@#X$%WM*+=-").split("");
88
+ const cols = resolveColumns();
89
+ const fontSize = resolveFontSize(cols);
90
+ const lineHeight = resolveLineHeight(fontSize);
91
+ const rows = Math.max(1, Math.floor(canvas.height / lineHeight));
92
+ const cellWidth = canvas.width / cols;
93
+ const gridHeight = rows * lineHeight;
94
+ const offsetY = (canvas.height - gridHeight) / 2;
95
+ const offscreen = document.createElement("canvas");
96
+ offscreen.width = cols;
97
+ offscreen.height = rows;
98
+ const offctx = offscreen.getContext("2d");
99
+
100
+ const srcRatio = img.width / img.height;
101
+ const dstRatio = canvas.width / gridHeight;
102
+ let sx = 0;
103
+ let sy = 0;
104
+ let sw = img.width;
105
+ let sh = img.height;
106
+ if (srcRatio > dstRatio) {
107
+ sw = img.height * dstRatio;
108
+ sx = (img.width - sw) / 2;
109
+ } else {
110
+ sh = img.width / dstRatio;
111
+ sy = (img.height - sh) / 2;
112
+ }
113
+
114
+ offctx.drawImage(img, sx, sy, sw, sh, 0, 0, cols, rows);
115
+ const imageData = offctx.getImageData(0, 0, cols, rows);
116
+ const { data } = imageData;
117
+
118
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
119
+ ctx.fillStyle = config.bgColor || "#111111";
120
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
121
+
122
+ ctx.font = `700 ${fontSize}px Menlo, Monaco, 'Courier New', monospace`;
123
+ ctx.textBaseline = "top";
124
+ ctx.textAlign = "center";
125
+
126
+ for (let row = 0; row < rows; row++) {
127
+ for (let col = 0; col < cols; col++) {
128
+ const color = averageRegion(data, cols, col, row, 1, 1);
129
+ const light = luminance(color);
130
+ const index = Math.max(0, Math.min(chars.length - 1, Math.floor((1 - light) * (chars.length - 1))));
131
+ const char = chars[index] || " ";
132
+ ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
133
+ ctx.fillText(char, col * cellWidth + cellWidth / 2, offsetY + row * lineHeight);
134
+ }
135
+ }
136
+ }
137
+
138
+ img.onload = () => {
139
+ drawAsciiPortrait();
140
+ document.body.dataset.ready = "1";
141
+ };
142
+ </script>
143
+ </body>
144
+ </html>
@@ -0,0 +1,179 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <!--
6
+ Template: longform-3-4
7
+ Ratio: 3:4 (900×1200)
8
+ 用途: 长文正文卡片,支持矩形图片障碍物,正文按行绕排
9
+ Parameters:
10
+ {{THEME_CSS_VARS}} — 主题变量
11
+ {{FOOTER_TEXT}} — 底部文字
12
+ {{ICON_PATH}} — 顶部 logo
13
+ {{AVATAR_PATH}} — 底部头像
14
+ {{FONT_PATH}} — 顶部/底部字体
15
+ {{BODY_FONT_PATH}} — 正文字体
16
+ {{LONGFORM_STAGE_HTML}} — 已排版页 HTML
17
+ -->
18
+ <style>
19
+ @font-face {
20
+ font-family: "AlimamaShuHeiTi";
21
+ src: url("{{FONT_PATH}}") format("truetype");
22
+ }
23
+ @font-face {
24
+ font-family: "LXGWNeoZhiSongPlus";
25
+ src: url("{{BODY_FONT_PATH}}") format("truetype");
26
+ font-weight: 400;
27
+ }
28
+ * { margin: 0; padding: 0; box-sizing: border-box; }
29
+ body {
30
+ position: relative;
31
+ width: var(--page-width);
32
+ height: var(--page-height);
33
+ background-color: var(--page-bg);
34
+ display: grid;
35
+ grid-template-rows:
36
+ calc(var(--body-padding-y) + var(--logo-size) + var(--logo-gap))
37
+ minmax(0, 1fr)
38
+ calc(var(--footer-margin-top) + var(--footer-height) + var(--body-padding-y));
39
+ overflow: hidden;
40
+ color: var(--body-color);
41
+ {{THEME_CSS_VARS}}
42
+ }
43
+ .header-shell {
44
+ width: 100%;
45
+ height: calc(var(--body-padding-y) + var(--logo-size) + var(--logo-gap));
46
+ padding: var(--body-padding-y) var(--body-padding-x) 0;
47
+ flex-shrink: 0;
48
+ }
49
+ .deco-top {
50
+ width: 100%;
51
+ height: var(--logo-size);
52
+ display: flex;
53
+ align-items: flex-start;
54
+ }
55
+ .deco-top img {
56
+ width: var(--logo-size);
57
+ height: var(--logo-size);
58
+ border-radius: 50%;
59
+ object-fit: cover;
60
+ }
61
+ .content {
62
+ min-height: 0;
63
+ width: 100%;
64
+ padding: 0 var(--body-padding-x);
65
+ display: flex;
66
+ align-items: flex-start;
67
+ justify-content: center;
68
+ overflow: hidden;
69
+ }
70
+ .content-stage {
71
+ position: relative;
72
+ width: min(100%, var(--content-width));
73
+ height: min(100%, calc(var(--content-height) - var(--content-bottom-gap)));
74
+ max-width: 100%;
75
+ max-height: 100%;
76
+ }
77
+ .body-line {
78
+ position: absolute;
79
+ left: 0;
80
+ font-family: "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif;
81
+ font-size: 32px;
82
+ line-height: 1.75;
83
+ color: var(--body-color);
84
+ white-space: pre;
85
+ }
86
+ .body-heading {
87
+ position: absolute;
88
+ left: 0;
89
+ font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
90
+ font-size: 38px;
91
+ line-height: 1.47;
92
+ color: var(--accent-color);
93
+ white-space: pre;
94
+ }
95
+ .body-quote {
96
+ position: absolute;
97
+ left: 0;
98
+ padding-left: 20px;
99
+ border-left: 4px solid color-mix(in srgb, var(--accent-color) 38%, white);
100
+ font-family: "LXGWNeoZhiSongPlus", "PingFang SC", "Noto Serif SC", serif;
101
+ font-size: 30px;
102
+ line-height: 1.73;
103
+ color: var(--quote-color);
104
+ white-space: pre;
105
+ }
106
+ .body-list-bullet {
107
+ position: absolute;
108
+ min-width: 24px;
109
+ text-align: left;
110
+ }
111
+ .body-image {
112
+ position: absolute;
113
+ object-fit: cover;
114
+ border-radius: var(--image-radius);
115
+ box-shadow: 0 16px 30px rgb(27 26 24 / 0.12);
116
+ background: #ebe7de;
117
+ }
118
+ .body-caption {
119
+ position: absolute;
120
+ left: 0;
121
+ font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
122
+ font-size: 20px;
123
+ line-height: 1.35;
124
+ color: var(--caption-color);
125
+ white-space: normal;
126
+ text-align: center;
127
+ }
128
+ .body-line strong, .body-heading strong, .body-quote strong {
129
+ font-weight: 700;
130
+ }
131
+ .body-line em, .body-heading em, .body-quote em {
132
+ font-style: italic;
133
+ }
134
+ .footer-shell {
135
+ width: 100%;
136
+ height: 100%;
137
+ padding: var(--footer-margin-top) var(--body-padding-x) var(--body-padding-y);
138
+ }
139
+ .watermark {
140
+ width: 100%;
141
+ min-height: var(--footer-height);
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 10px;
145
+ opacity: var(--watermark-opacity);
146
+ }
147
+ .watermark img {
148
+ width: 32px;
149
+ height: 32px;
150
+ border-radius: 50%;
151
+ object-fit: cover;
152
+ }
153
+ .watermark span {
154
+ font-size: 24px;
155
+ color: var(--watermark-color);
156
+ font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
157
+ letter-spacing: 1px;
158
+ }
159
+ </style>
160
+ </head>
161
+ <body>
162
+ <div class="header-shell">
163
+ <div class="deco-top">
164
+ <img src="{{ICON_PATH}}" alt="icon">
165
+ </div>
166
+ </div>
167
+
168
+ <main class="content">
169
+ <div class="content-stage" id="content-stage">{{LONGFORM_STAGE_HTML}}</div>
170
+ </main>
171
+
172
+ <div class="footer-shell">
173
+ <div class="watermark">
174
+ <img src="{{AVATAR_PATH}}" alt="Jinx">
175
+ <span>{{FOOTER_TEXT}}</span>
176
+ </div>
177
+ </div>
178
+ </body>
179
+ </html>