@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.
- package/README.md +559 -0
- package/dist/assets/imgx/assets/browser/obstacle-flow.d.ts +27 -0
- package/dist/assets/imgx/assets/browser/obstacle-flow.js +326 -0
- package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
- package/dist/assets/imgx/assets/icons/clover.svg +15 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
- package/dist/assets/imgx/assets/icons/openclaw-logo.svg +22 -0
- package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
- package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +25 -0
- package/dist/assets/imgx/assets/templates/ascii-portrait-3-4.html +342 -0
- package/dist/assets/imgx/assets/templates/ascii-portrait-tile.html +144 -0
- package/dist/assets/imgx/assets/templates/longform-3-4.html +179 -0
- package/dist/assets/imgx/assets/templates/poster-3-4.html +359 -0
- package/dist/assets/imgx/assets/templates/tips-3-4.html +300 -0
- package/dist/assets/imgx/assets/templates/wechat-cover-split.html +307 -0
- package/dist/assets/imgx/assets/templates/x-like-posts.html +246 -0
- package/dist/assets/wechat-preview/assets/browser-dist/.vite/manifest.json +314 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
- package/dist/assets/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +1 -0
- package/dist/assets/wechat-preview/assets/browser-dist/editor-export.js +969 -0
- package/dist/assets/wechat-preview/assets/templates/export-shell.html +38 -0
- package/dist/cli.js +404 -0
- package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +33 -0
- package/dist/node_modules/@chenglou/pretext/dist/analysis.js +1063 -0
- package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +1 -0
- package/dist/node_modules/@chenglou/pretext/dist/bidi.js +175 -0
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +4 -0
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +979 -0
- package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +70 -0
- package/dist/node_modules/@chenglou/pretext/dist/layout.js +496 -0
- package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +36 -0
- package/dist/node_modules/@chenglou/pretext/dist/line-break.js +820 -0
- package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +28 -0
- package/dist/node_modules/@chenglou/pretext/dist/measurement.js +219 -0
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +51 -0
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +401 -0
- package/package.json +42 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<!--
|
|
6
|
+
Template: image-3-4
|
|
7
|
+
Ratio: 3:4 (900×1200)
|
|
8
|
+
用途: 3:4 比例通用封面配图
|
|
9
|
+
Parameters (replaced via script before rendering):
|
|
10
|
+
{{MAIN_TEXT_LINE1}} — 第一行文字
|
|
11
|
+
{{MAIN_TEXT_LINE2}} — 第二行文字(可为空)
|
|
12
|
+
{{MAIN_TEXT_LINE3}} — 第三行文字(可为空)
|
|
13
|
+
{{LINE1_CLASS}} — "highlight" 或 ""
|
|
14
|
+
{{LINE2_CLASS}} — "highlight" 或 ""
|
|
15
|
+
{{LINE3_CLASS}} — "highlight" 或 ""
|
|
16
|
+
{{HIGHLIGHT_COLOR}} — 高亮色 hex,默认 #22a854
|
|
17
|
+
{{BG_COLOR}} — 背景色 hex,默认 #e6f5ef
|
|
18
|
+
{{FOOTER_TEXT}} — 底部版权/来源文字
|
|
19
|
+
{{ICON_PATH}} — 顶部图标绝对路径
|
|
20
|
+
-->
|
|
21
|
+
<style>
|
|
22
|
+
@font-face {
|
|
23
|
+
font-family: "AlimamaShuHeiTi";
|
|
24
|
+
src: url("{{FONT_PATH}}") format("truetype");
|
|
25
|
+
}
|
|
26
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
27
|
+
body {
|
|
28
|
+
width: 900px;
|
|
29
|
+
height: 1200px;
|
|
30
|
+
background-color: {{BG_COLOR}};
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
padding: 80px 90px;
|
|
36
|
+
font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
|
|
37
|
+
position: relative;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
opacity: 0;
|
|
40
|
+
transition: opacity 120ms ease;
|
|
41
|
+
}
|
|
42
|
+
body.ready { opacity: 1; }
|
|
43
|
+
.deco-top {
|
|
44
|
+
margin-bottom: 80px;
|
|
45
|
+
align-self: flex-start;
|
|
46
|
+
}
|
|
47
|
+
.deco-top img {
|
|
48
|
+
width: 108px;
|
|
49
|
+
height: 108px;
|
|
50
|
+
border-radius: 50%;
|
|
51
|
+
object-fit: cover;
|
|
52
|
+
}
|
|
53
|
+
.text-block {
|
|
54
|
+
margin-top: 0;
|
|
55
|
+
align-self: stretch;
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
|
58
|
+
.pretext-lines {
|
|
59
|
+
width: 100%;
|
|
60
|
+
}
|
|
61
|
+
.line {
|
|
62
|
+
font-size: var(--poster-font-size, 108px);
|
|
63
|
+
font-weight: 900;
|
|
64
|
+
line-height: var(--poster-line-height, 1.15);
|
|
65
|
+
color: #1a1a1a;
|
|
66
|
+
letter-spacing: var(--poster-letter-spacing, -2px);
|
|
67
|
+
white-space: pre;
|
|
68
|
+
}
|
|
69
|
+
.block-text {
|
|
70
|
+
max-width: var(--poster-text-width, 100%);
|
|
71
|
+
white-space: normal;
|
|
72
|
+
overflow-wrap: break-word;
|
|
73
|
+
}
|
|
74
|
+
.line + .line {
|
|
75
|
+
margin-top: 0;
|
|
76
|
+
}
|
|
77
|
+
.block + .block {
|
|
78
|
+
margin-top: var(--poster-block-gap, 10px);
|
|
79
|
+
}
|
|
80
|
+
.line.highlight { color: {{HIGHLIGHT_COLOR}}; }
|
|
81
|
+
.highlight { color: {{HIGHLIGHT_COLOR}}; }
|
|
82
|
+
.watermark {
|
|
83
|
+
margin-top: 80px;
|
|
84
|
+
align-self: flex-start;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 10px;
|
|
88
|
+
opacity: 0.55;
|
|
89
|
+
}
|
|
90
|
+
.watermark img {
|
|
91
|
+
width: 32px;
|
|
92
|
+
height: 32px;
|
|
93
|
+
border-radius: 50%;
|
|
94
|
+
object-fit: cover;
|
|
95
|
+
}
|
|
96
|
+
.watermark span {
|
|
97
|
+
font-size: 24px;
|
|
98
|
+
color: #555;
|
|
99
|
+
font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
|
|
100
|
+
letter-spacing: 1px;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<div class="deco-top">
|
|
106
|
+
<img src="{{ICON_PATH}}" alt="icon">
|
|
107
|
+
</div>
|
|
108
|
+
<div class="text-block">
|
|
109
|
+
<div class="pretext-lines" id="poster-lines" aria-label="poster text"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="watermark">
|
|
112
|
+
<img src="{{AVATAR_PATH}}" alt="Jinx">
|
|
113
|
+
<span>{{FOOTER_TEXT}}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<script id="poster-config" type="application/json">{{POSTER_CONFIG_JSON}}</script>
|
|
116
|
+
<script type="module">
|
|
117
|
+
import { prepareWithSegments, layoutWithLines } from "{{PRETEXT_MODULE_URL}}";
|
|
118
|
+
|
|
119
|
+
const configNode = document.getElementById("poster-config");
|
|
120
|
+
const linesRoot = document.getElementById("poster-lines");
|
|
121
|
+
const textBlock = document.querySelector(".text-block");
|
|
122
|
+
const iconNode = document.querySelector(".deco-top");
|
|
123
|
+
const watermarkNode = document.querySelector(".watermark");
|
|
124
|
+
if (!(configNode instanceof HTMLScriptElement) || !(linesRoot instanceof HTMLDivElement) || !(textBlock instanceof HTMLDivElement) || !(iconNode instanceof HTMLDivElement) || !(watermarkNode instanceof HTMLDivElement)) {
|
|
125
|
+
throw new Error("poster nodes not found");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const config = JSON.parse(configNode.textContent || "{}");
|
|
129
|
+
const blocks = Array.isArray(config.blocks) ? config.blocks : [];
|
|
130
|
+
const highlightWords = Array.isArray(config.highlightWords) ? config.highlightWords.filter(Boolean) : [];
|
|
131
|
+
|
|
132
|
+
const FONT_FAMILY = '"AlimamaShuHeiTi", "PingFang SC", sans-serif';
|
|
133
|
+
const FONT_WEIGHT = 900;
|
|
134
|
+
const MIN_FONT_SIZE = 34;
|
|
135
|
+
const MAX_FONT_SIZE = 132;
|
|
136
|
+
const FALLBACK_AVAILABLE_HEIGHT = 730;
|
|
137
|
+
|
|
138
|
+
function escapeHtml(value) {
|
|
139
|
+
return value
|
|
140
|
+
.replaceAll("&", "&")
|
|
141
|
+
.replaceAll("<", "<")
|
|
142
|
+
.replaceAll(">", ">")
|
|
143
|
+
.replaceAll('"', """)
|
|
144
|
+
.replaceAll("'", "'");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function escapeRegExp(value) {
|
|
148
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLineHeight(fontSize) {
|
|
152
|
+
if (fontSize >= 108) return Math.round(fontSize * 1.1);
|
|
153
|
+
if (fontSize >= 92) return Math.round(fontSize * 1.08);
|
|
154
|
+
if (fontSize >= 72) return Math.round(fontSize * 1.06);
|
|
155
|
+
if (fontSize >= 56) return Math.round(fontSize * 1.05);
|
|
156
|
+
return Math.round(fontSize * 1.04);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getBlockGap(fontSize) {
|
|
160
|
+
if (fontSize >= 108) return 18;
|
|
161
|
+
if (fontSize >= 84) return 14;
|
|
162
|
+
if (fontSize >= 60) return 10;
|
|
163
|
+
return 8;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getLetterSpacing(fontSize) {
|
|
167
|
+
if (fontSize >= 110) return "-2.8px";
|
|
168
|
+
if (fontSize >= 90) return "-2px";
|
|
169
|
+
if (fontSize >= 70) return "-1px";
|
|
170
|
+
return "-0.4px";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function measureLayout(fontSize, maxWidth) {
|
|
174
|
+
const lineHeight = getLineHeight(fontSize);
|
|
175
|
+
const blockGap = getBlockGap(fontSize);
|
|
176
|
+
const font = `${FONT_WEIGHT} ${fontSize}px ${FONT_FAMILY}`;
|
|
177
|
+
let totalHeight = 0;
|
|
178
|
+
const renderedBlocks = [];
|
|
179
|
+
|
|
180
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
181
|
+
const block = blocks[index];
|
|
182
|
+
const prepared = prepareWithSegments(block.text, font, { whiteSpace: "pre-wrap" });
|
|
183
|
+
const result = layoutWithLines(prepared, maxWidth, lineHeight);
|
|
184
|
+
renderedBlocks.push({
|
|
185
|
+
highlight: !!block.highlight,
|
|
186
|
+
text: block.text,
|
|
187
|
+
lineCount: result.lines.length,
|
|
188
|
+
widths: result.lines.map(line => line.width),
|
|
189
|
+
});
|
|
190
|
+
totalHeight += result.height;
|
|
191
|
+
if (index > 0) totalHeight += blockGap;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
font,
|
|
196
|
+
fontSize,
|
|
197
|
+
lineHeight,
|
|
198
|
+
blockGap,
|
|
199
|
+
maxWidth,
|
|
200
|
+
totalHeight,
|
|
201
|
+
blocks: renderedBlocks,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function countVisibleChars(text) {
|
|
206
|
+
let total = 0;
|
|
207
|
+
total += Array.from(text).filter(ch => ch.trim().length > 0).length;
|
|
208
|
+
return total;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getDesiredLineRange(chars) {
|
|
212
|
+
if (chars <= 8) return [1, 2];
|
|
213
|
+
if (chars <= 16) return [2, 3];
|
|
214
|
+
if (chars <= 28) return [3, 4];
|
|
215
|
+
if (chars <= 44) return [4, 6];
|
|
216
|
+
if (chars <= 64) return [5, 7];
|
|
217
|
+
return [6, 8];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getBlockMaxWidth(chars) {
|
|
221
|
+
if (chars <= 10) return Math.floor(textBlock.clientWidth);
|
|
222
|
+
if (chars <= 22) return Math.floor(textBlock.clientWidth * 0.92);
|
|
223
|
+
if (chars <= 40) return Math.floor(textBlock.clientWidth * 0.86);
|
|
224
|
+
if (chars <= 64) return Math.floor(textBlock.clientWidth * 0.8);
|
|
225
|
+
return Math.floor(textBlock.clientWidth * 0.74);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function scoreLayout(layout, availableHeight, maxWidth) {
|
|
229
|
+
if (layout.totalHeight > availableHeight) return Number.POSITIVE_INFINITY;
|
|
230
|
+
|
|
231
|
+
let score = 0;
|
|
232
|
+
for (const block of layout.blocks) {
|
|
233
|
+
const charCount = countVisibleChars(block.text);
|
|
234
|
+
const [minLines, maxLines] = getDesiredLineRange(charCount);
|
|
235
|
+
const lineCount = block.lineCount;
|
|
236
|
+
if (lineCount < minLines) score += (minLines - lineCount) * 120;
|
|
237
|
+
if (lineCount > maxLines) score += (lineCount - maxLines) * 150;
|
|
238
|
+
|
|
239
|
+
if (block.widths.length > 1) {
|
|
240
|
+
const widest = Math.max(...block.widths);
|
|
241
|
+
const narrowest = Math.min(...block.widths);
|
|
242
|
+
const fillRatio = widest / Math.max(1, maxWidth);
|
|
243
|
+
const raggedRatio = narrowest / Math.max(1, widest);
|
|
244
|
+
score += Math.max(0, 0.78 - fillRatio) * 220;
|
|
245
|
+
score += Math.max(0, 0.58 - raggedRatio) * 200;
|
|
246
|
+
} else if (block.widths.length === 1) {
|
|
247
|
+
const fillRatio = block.widths[0] / Math.max(1, maxWidth);
|
|
248
|
+
if (charCount > 10) score += Math.max(0, fillRatio - 0.88) * 180;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
score += Math.max(0, 0.92 - layout.totalHeight / Math.max(1, availableHeight)) * 30;
|
|
253
|
+
score += layout.fontSize * 0.015;
|
|
254
|
+
return score;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function fitEllipsisBlock(text, font, maxWidth, lineHeight, maxLines) {
|
|
258
|
+
const chars = Array.from(text.trimEnd());
|
|
259
|
+
if (chars.length === 0) return "…";
|
|
260
|
+
for (let end = chars.length; end >= 0; end--) {
|
|
261
|
+
const candidate = `${chars.slice(0, end).join("").trimEnd()}…`;
|
|
262
|
+
const result = layoutWithLines(prepareWithSegments(candidate, font, { whiteSpace: "pre-wrap" }), maxWidth, lineHeight);
|
|
263
|
+
if (result.lines.length <= maxLines) return candidate;
|
|
264
|
+
}
|
|
265
|
+
return "…";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function clampLayout(layout, availableHeight, maxWidth) {
|
|
269
|
+
const output = [];
|
|
270
|
+
let usedHeight = 0;
|
|
271
|
+
|
|
272
|
+
for (let blockIndex = 0; blockIndex < layout.blocks.length; blockIndex++) {
|
|
273
|
+
const block = layout.blocks[blockIndex];
|
|
274
|
+
const blockGap = output.length > 0 ? layout.blockGap : 0;
|
|
275
|
+
const blockHeight = block.lineCount * layout.lineHeight;
|
|
276
|
+
if (usedHeight + blockGap + blockHeight <= availableHeight) {
|
|
277
|
+
if (blockGap > 0) usedHeight += blockGap;
|
|
278
|
+
usedHeight += blockHeight;
|
|
279
|
+
output.push(block);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let remainingHeight = availableHeight - usedHeight - blockGap;
|
|
284
|
+
if (remainingHeight <= 0) break;
|
|
285
|
+
const maxLines = Math.max(1, Math.floor(remainingHeight / layout.lineHeight));
|
|
286
|
+
const needsEllipsis = block.lineCount > maxLines || blockIndex < layout.blocks.length - 1;
|
|
287
|
+
output.push({
|
|
288
|
+
highlight: block.highlight,
|
|
289
|
+
text: needsEllipsis ? fitEllipsisBlock(block.text, layout.font, maxWidth, layout.lineHeight, maxLines) : block.text,
|
|
290
|
+
lineCount: Math.min(block.lineCount, maxLines),
|
|
291
|
+
widths: block.widths,
|
|
292
|
+
});
|
|
293
|
+
usedHeight = availableHeight;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
...layout,
|
|
299
|
+
blocks: output,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function highlightHtml(text) {
|
|
304
|
+
let html = escapeHtml(text);
|
|
305
|
+
for (const word of highlightWords) {
|
|
306
|
+
const safeWord = String(word || "").trim();
|
|
307
|
+
if (!safeWord) continue;
|
|
308
|
+
const escapedWord = escapeHtml(safeWord);
|
|
309
|
+
html = html.replace(new RegExp(escapeRegExp(escapedWord), "g"), `<span class="highlight">${escapedWord}</span>`);
|
|
310
|
+
}
|
|
311
|
+
return html;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderLayout(layout) {
|
|
315
|
+
document.body.style.setProperty("--poster-font-size", `${layout.fontSize}px`);
|
|
316
|
+
document.body.style.setProperty("--poster-line-height", String(layout.lineHeight / layout.fontSize));
|
|
317
|
+
document.body.style.setProperty("--poster-letter-spacing", getLetterSpacing(layout.fontSize));
|
|
318
|
+
document.body.style.setProperty("--poster-block-gap", `${layout.blockGap}px`);
|
|
319
|
+
document.body.style.setProperty("--poster-text-width", `${layout.maxWidth}px`);
|
|
320
|
+
|
|
321
|
+
linesRoot.innerHTML = layout.blocks
|
|
322
|
+
.map(block => {
|
|
323
|
+
const lineClass = block.highlight ? "line block-text highlight" : "line block-text";
|
|
324
|
+
return `<div class="block"><div class="${lineClass}">${highlightHtml(block.text)}</div></div>`;
|
|
325
|
+
})
|
|
326
|
+
.join("");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function boot() {
|
|
330
|
+
await document.fonts.ready;
|
|
331
|
+
const sourceChars = blocks.reduce((sum, block) => sum + Array.from(block.text).filter(ch => ch.trim().length > 0).length, 0);
|
|
332
|
+
const maxWidth = getBlockMaxWidth(sourceChars);
|
|
333
|
+
const iconHeight = Math.ceil(iconNode.getBoundingClientRect().height);
|
|
334
|
+
const watermarkHeight = Math.ceil(watermarkNode.getBoundingClientRect().height);
|
|
335
|
+
const availableHeight = Math.max(
|
|
336
|
+
FALLBACK_AVAILABLE_HEIGHT,
|
|
337
|
+
Math.floor(window.innerHeight - iconHeight - watermarkHeight - 160),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
let chosen = null;
|
|
341
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
342
|
+
for (let fontSize = MAX_FONT_SIZE; fontSize >= MIN_FONT_SIZE; fontSize -= 2) {
|
|
343
|
+
const layout = measureLayout(fontSize, maxWidth);
|
|
344
|
+
const score = scoreLayout(layout, availableHeight, maxWidth);
|
|
345
|
+
if (score < bestScore) {
|
|
346
|
+
bestScore = score;
|
|
347
|
+
chosen = layout;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const finalLayout = clampLayout(chosen, availableHeight, maxWidth);
|
|
352
|
+
renderLayout(finalLayout);
|
|
353
|
+
document.body.classList.add("ready");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
boot();
|
|
357
|
+
</script>
|
|
358
|
+
</body>
|
|
359
|
+
</html>
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<!--
|
|
6
|
+
Template: tips-3-4
|
|
7
|
+
Ratio: 3:4 (900×1200)
|
|
8
|
+
用途: 小红书风格结构化要点卡,支持 3–5 条带标题和描述的技巧列表
|
|
9
|
+
Parameters (replaced via script before rendering):
|
|
10
|
+
{{HIGHLIGHT_COLOR}} — 强调色 hex,默认 #22a854
|
|
11
|
+
{{BG_COLOR}} — 背景色 hex,默认 #e6f5ef
|
|
12
|
+
{{FOOTER_TEXT}} — 底部版权/来源文字
|
|
13
|
+
{{ICON_PATH}} — 顶部图标绝对路径
|
|
14
|
+
{{POSTER_CONFIG_JSON}} — JSON: { blocks, highlightWords, highlightColor, tips }
|
|
15
|
+
{{PRETEXT_MODULE_URL}} — pretext layout.js 绝对路径
|
|
16
|
+
-->
|
|
17
|
+
<style>
|
|
18
|
+
@font-face {
|
|
19
|
+
font-family: "AlimamaShuHeiTi";
|
|
20
|
+
src: url("{{FONT_PATH}}") format("truetype");
|
|
21
|
+
}
|
|
22
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
23
|
+
body {
|
|
24
|
+
width: 900px;
|
|
25
|
+
height: 1200px;
|
|
26
|
+
background-color: {{BG_COLOR}};
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
padding: 60px 80px 52px;
|
|
30
|
+
font-family: "AlimamaShuHeiTi", "PingFang SC", sans-serif;
|
|
31
|
+
position: relative;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
.header {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
margin-bottom: 36px;
|
|
40
|
+
}
|
|
41
|
+
.header-icon {
|
|
42
|
+
width: 80px;
|
|
43
|
+
height: 80px;
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
object-fit: cover;
|
|
46
|
+
}
|
|
47
|
+
.tips-badge {
|
|
48
|
+
font-size: 22px;
|
|
49
|
+
font-weight: 900;
|
|
50
|
+
color: {{HIGHLIGHT_COLOR}};
|
|
51
|
+
border: 2.5px solid {{HIGHLIGHT_COLOR}};
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
padding: 4px 16px;
|
|
54
|
+
letter-spacing: 1px;
|
|
55
|
+
}
|
|
56
|
+
.title-section {
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
width: 100%;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
}
|
|
63
|
+
.title-line {
|
|
64
|
+
font-size: var(--title-font-size, 80px);
|
|
65
|
+
font-weight: 900;
|
|
66
|
+
line-height: var(--title-line-height, 1.1);
|
|
67
|
+
color: #1a1a1a;
|
|
68
|
+
letter-spacing: var(--title-letter-spacing, -1.5px);
|
|
69
|
+
white-space: pre-wrap;
|
|
70
|
+
overflow-wrap: break-word;
|
|
71
|
+
}
|
|
72
|
+
.title-highlight { color: {{HIGHLIGHT_COLOR}}; }
|
|
73
|
+
.divider {
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
height: 1px;
|
|
76
|
+
background: #1a1a1a;
|
|
77
|
+
opacity: 0.15;
|
|
78
|
+
margin: 24px 0;
|
|
79
|
+
}
|
|
80
|
+
.tips-list {
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
justify-content: center;
|
|
85
|
+
gap: 40px;
|
|
86
|
+
list-style: none;
|
|
87
|
+
}
|
|
88
|
+
.tip-item {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: flex-start;
|
|
91
|
+
gap: 20px;
|
|
92
|
+
}
|
|
93
|
+
.tip-number {
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
width: 38px;
|
|
96
|
+
height: 38px;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
background: {{HIGHLIGHT_COLOR}};
|
|
99
|
+
color: #fff;
|
|
100
|
+
font-size: 19px;
|
|
101
|
+
font-weight: 900;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
margin-top: 2px;
|
|
106
|
+
}
|
|
107
|
+
.tip-content {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: 4px;
|
|
111
|
+
flex: 1;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
}
|
|
114
|
+
.tip-title {
|
|
115
|
+
font-size: 28px;
|
|
116
|
+
font-weight: 900;
|
|
117
|
+
color: #1a1a1a;
|
|
118
|
+
line-height: 1.25;
|
|
119
|
+
}
|
|
120
|
+
.tip-desc {
|
|
121
|
+
font-size: 20px;
|
|
122
|
+
font-weight: 400;
|
|
123
|
+
color: #555;
|
|
124
|
+
line-height: 1.45;
|
|
125
|
+
}
|
|
126
|
+
.watermark {
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
margin-top: 28px;
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 10px;
|
|
132
|
+
opacity: 0.55;
|
|
133
|
+
}
|
|
134
|
+
.watermark img {
|
|
135
|
+
width: 32px;
|
|
136
|
+
height: 32px;
|
|
137
|
+
border-radius: 50%;
|
|
138
|
+
object-fit: cover;
|
|
139
|
+
}
|
|
140
|
+
.watermark span {
|
|
141
|
+
font-size: 22px;
|
|
142
|
+
color: #555;
|
|
143
|
+
letter-spacing: 1px;
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
146
|
+
</head>
|
|
147
|
+
<body>
|
|
148
|
+
<div class="header">
|
|
149
|
+
<img class="header-icon" src="{{ICON_PATH}}" alt="icon">
|
|
150
|
+
<div class="tips-badge" id="tips-badge">TIPS</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="title-section" id="title-section">
|
|
153
|
+
<div id="title-root"></div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="divider"></div>
|
|
156
|
+
<ol class="tips-list" id="tips-list"></ol>
|
|
157
|
+
<div class="watermark">
|
|
158
|
+
<img src="{{AVATAR_PATH}}" alt="avatar">
|
|
159
|
+
<span>{{FOOTER_TEXT}}</span>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<script id="poster-config" type="application/json">{{POSTER_CONFIG_JSON}}</script>
|
|
163
|
+
<script type="module">
|
|
164
|
+
import { prepareWithSegments, layoutWithLines } from "{{PRETEXT_MODULE_URL}}";
|
|
165
|
+
|
|
166
|
+
const configNode = document.getElementById("poster-config");
|
|
167
|
+
const titleRoot = document.getElementById("title-root");
|
|
168
|
+
const titleSection = document.getElementById("title-section");
|
|
169
|
+
const tipsList = document.getElementById("tips-list");
|
|
170
|
+
const tipsBadge = document.getElementById("tips-badge");
|
|
171
|
+
const headerEl = document.querySelector(".header");
|
|
172
|
+
const watermarkEl = document.querySelector(".watermark");
|
|
173
|
+
|
|
174
|
+
if (!(configNode instanceof HTMLScriptElement)) throw new Error("poster-config not found");
|
|
175
|
+
|
|
176
|
+
const config = JSON.parse(configNode.textContent || "{}");
|
|
177
|
+
const blocks = Array.isArray(config.blocks) ? config.blocks : [];
|
|
178
|
+
const tips = Array.isArray(config.tips) ? config.tips : [];
|
|
179
|
+
|
|
180
|
+
const FONT_FAMILY = '"AlimamaShuHeiTi", "PingFang SC", sans-serif';
|
|
181
|
+
const FONT_WEIGHT = 900;
|
|
182
|
+
const MIN_FONT_SIZE = 28;
|
|
183
|
+
const MAX_FONT_SIZE = 100;
|
|
184
|
+
const PAD_TOP = 60;
|
|
185
|
+
const PAD_BOTTOM = 52;
|
|
186
|
+
const HEADER_MB = 36;
|
|
187
|
+
const DIVIDER_H = 1;
|
|
188
|
+
const DIVIDER_MV = 48;
|
|
189
|
+
const WATERMARK_MT = 28;
|
|
190
|
+
const MIN_TIPS_RATIO = 0.45;
|
|
191
|
+
const TITLE_V_PAD = 40; /* vertical breathing room above+below title text */
|
|
192
|
+
|
|
193
|
+
function escapeHtml(value) {
|
|
194
|
+
return value
|
|
195
|
+
.replaceAll("&", "&")
|
|
196
|
+
.replaceAll("<", "<")
|
|
197
|
+
.replaceAll(">", ">")
|
|
198
|
+
.replaceAll('"', """)
|
|
199
|
+
.replaceAll("'", "'");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getLineHeight(fontSize) {
|
|
203
|
+
if (fontSize >= 80) return Math.round(fontSize * 1.1);
|
|
204
|
+
if (fontSize >= 60) return Math.round(fontSize * 1.08);
|
|
205
|
+
if (fontSize >= 44) return Math.round(fontSize * 1.06);
|
|
206
|
+
return Math.round(fontSize * 1.05);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getLetterSpacing(fontSize) {
|
|
210
|
+
if (fontSize >= 90) return "-2px";
|
|
211
|
+
if (fontSize >= 70) return "-1.5px";
|
|
212
|
+
if (fontSize >= 50) return "-1px";
|
|
213
|
+
return "-0.4px";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function fillTipsList() {
|
|
217
|
+
tipsList.innerHTML = tips
|
|
218
|
+
.map((tip, i) => {
|
|
219
|
+
const titleHtml = escapeHtml(String(tip.title || ""));
|
|
220
|
+
const descHtml = tip.description
|
|
221
|
+
? `<div class="tip-desc">${escapeHtml(String(tip.description))}</div>`
|
|
222
|
+
: "";
|
|
223
|
+
return `<li class="tip-item"><div class="tip-number">${i + 1}</div><div class="tip-content"><div class="tip-title">${titleHtml}</div>${descHtml}</div></li>`;
|
|
224
|
+
})
|
|
225
|
+
.join("");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderTitle(fontSize) {
|
|
229
|
+
const lh = getLineHeight(fontSize);
|
|
230
|
+
document.body.style.setProperty("--title-font-size", `${fontSize}px`);
|
|
231
|
+
document.body.style.setProperty("--title-line-height", String(lh / fontSize));
|
|
232
|
+
document.body.style.setProperty("--title-letter-spacing", getLetterSpacing(fontSize));
|
|
233
|
+
|
|
234
|
+
const maxWidth = Math.floor(titleSection.clientWidth);
|
|
235
|
+
const font = `${FONT_WEIGHT} ${fontSize}px ${FONT_FAMILY}`;
|
|
236
|
+
|
|
237
|
+
titleRoot.innerHTML = blocks
|
|
238
|
+
.map(block => {
|
|
239
|
+
prepareWithSegments(block.text, font, { whiteSpace: "pre-wrap" });
|
|
240
|
+
const cls = block.highlight ? "title-line title-highlight" : "title-line";
|
|
241
|
+
return `<div class="${cls}">${escapeHtml(block.text)}</div>`;
|
|
242
|
+
})
|
|
243
|
+
.join("");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function pickTitleFontSize(availableHeight) {
|
|
247
|
+
const maxWidth = Math.floor(titleSection.clientWidth);
|
|
248
|
+
for (let fs = MAX_FONT_SIZE; fs >= MIN_FONT_SIZE; fs -= 2) {
|
|
249
|
+
const lh = getLineHeight(fs);
|
|
250
|
+
const font = `${FONT_WEIGHT} ${fs}px ${FONT_FAMILY}`;
|
|
251
|
+
let totalH = 0;
|
|
252
|
+
for (const block of blocks) {
|
|
253
|
+
const prepared = prepareWithSegments(block.text, font, { whiteSpace: "pre-wrap" });
|
|
254
|
+
const result = layoutWithLines(prepared, maxWidth, lh);
|
|
255
|
+
totalH += result.height;
|
|
256
|
+
}
|
|
257
|
+
if (totalH <= availableHeight) return fs;
|
|
258
|
+
}
|
|
259
|
+
return MIN_FONT_SIZE;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function boot() {
|
|
263
|
+
await document.fonts.ready;
|
|
264
|
+
|
|
265
|
+
if (tips.length > 0) tipsBadge.textContent = `${tips.length} TIPS`;
|
|
266
|
+
|
|
267
|
+
tipsList.style.flex = "none";
|
|
268
|
+
fillTipsList();
|
|
269
|
+
|
|
270
|
+
await new Promise(r => requestAnimationFrame(r));
|
|
271
|
+
|
|
272
|
+
const headerH = Math.ceil(headerEl.getBoundingClientRect().height);
|
|
273
|
+
const tipsNatH = Math.ceil(tipsList.getBoundingClientRect().height);
|
|
274
|
+
const wmH = Math.ceil(watermarkEl.getBoundingClientRect().height);
|
|
275
|
+
|
|
276
|
+
const usableHeight = 1200 - PAD_TOP - PAD_BOTTOM - headerH - HEADER_MB - DIVIDER_H - DIVIDER_MV - wmH - WATERMARK_MT;
|
|
277
|
+
const tipsHeight = Math.max(tipsNatH, Math.round(usableHeight * MIN_TIPS_RATIO));
|
|
278
|
+
const availableTitle = Math.max(80, usableHeight - tipsHeight);
|
|
279
|
+
|
|
280
|
+
tipsList.style.flex = "";
|
|
281
|
+
tipsList.style.height = `${tipsHeight}px`;
|
|
282
|
+
|
|
283
|
+
const titleSection = document.getElementById("title-section");
|
|
284
|
+
titleSection.style.height = `${availableTitle}px`;
|
|
285
|
+
|
|
286
|
+
const chosenFs = pickTitleFontSize(availableTitle);
|
|
287
|
+
renderTitle(chosenFs);
|
|
288
|
+
|
|
289
|
+
// Snap title-section to actual content height + breathing room
|
|
290
|
+
await new Promise(r => requestAnimationFrame(r));
|
|
291
|
+
const titleNatH = Math.ceil(titleRoot.getBoundingClientRect().height);
|
|
292
|
+
titleSection.style.height = `${titleNatH + TITLE_V_PAD}px`;
|
|
293
|
+
|
|
294
|
+
document.body.classList.add("ready");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
boot();
|
|
298
|
+
</script>
|
|
299
|
+
</body>
|
|
300
|
+
</html>
|