@gitruck/cli 1.0.2 → 1.0.3

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 (32) hide show
  1. package/AGENT.md +13 -0
  2. package/LICENSE +21 -21
  3. package/README.en.md +626 -540
  4. package/README.md +624 -545
  5. package/contracts/README.md +14 -14
  6. package/contracts/handoff-contracts.json +10 -10
  7. package/dist/index.js +5170 -2033
  8. package/package.json +2 -2
  9. package/skills/gtrk-ai-drama/SKILL.md +1 -1
  10. package/skills/gtrk-cover/SKILL.md +93 -93
  11. package/skills/gtrk-cover/assets/workbench-template.html +955 -955
  12. package/skills/gtrk-cover/references/checklist.md +81 -81
  13. package/skills/gtrk-cover/references/layout-recipes.md +337 -337
  14. package/skills/gtrk-cover/references/prompt-templates.md +119 -119
  15. package/skills/gtrk-cover/references/workbench-guide.md +99 -99
  16. package/skills/gtrk-long2short/SKILL.md +71 -68
  17. package/skills/gtrk-matrix/SKILL.md +40 -7
  18. package/skills/gtrk-mg/SKILL.md +10 -2
  19. package/skills/gtrk-music-visualizer/SKILL.md +76 -76
  20. package/skills/gtrk-splitter/SKILL.md +208 -187
  21. package/skills/gtrk-splitter/references/example-visual-split.json +638 -638
  22. package/skills/gtrk-splitter/references/example-visual-split.md +439 -439
  23. package/skills/gtrk-splitter/references/field-schema.md +138 -134
  24. package/skills/gtrk-style-maker/references/ammo.md +16 -16
  25. package/skills/gtrk-style-maker/references/contracts-ref.md +9 -9
  26. package/skills/gtrk-style-maker/references/seeds/README.md +4 -4
  27. package/skills/gtrk-style-maker/references/seeds/seed-ecom-home-goods.md +37 -37
  28. package/skills/gtrk-style-maker/references/seeds/seed-psych-humanities.md +35 -35
  29. package/skills/gtrk-tools/SKILL.md +132 -132
  30. package/skills/gtrk-transcript/SKILL.md +64 -58
  31. package/skills/gtrk-transcript/agents/openai.yaml +4 -4
  32. package/skills/gtrk-travel-recap/SKILL.md +212 -0
@@ -1,955 +1,955 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>示例标题 · 封面排版工作台</title>
6
- <!-- ↑ 替换位:当期 T1 -->
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <!-- 替换位:字体族按栏目风格资产增删(衬线/黑体/手写体等) -->
10
- <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@700;900&family=Noto+Sans+SC:wght@400;700;900&family=Cormorant+Garamond:ital,wght@0,500;1,400&family=JetBrains+Mono:wght@400;600&family=Long+Cang&display=swap" rel="stylesheet">
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
12
- <style>
13
- *, *::before, *::after { box-sizing: border-box; }
14
- /* 替换位:色板变量——变量名固定、换值即换装(语义见 workbench-guide §3)。以下为中性默认值 */
15
- :root {
16
- --midnight: #14161E;
17
- --indigo: #232838;
18
- --amber: #7A869E;
19
- --amber-soft: #9AA5BC;
20
- --bone: #ECEEF3;
21
- --ink: #0A0C12;
22
- --panel: #161923;
23
- --panel-2: #1E2230;
24
- --border: #2C3145;
25
- --muted: #8B92A8;
26
- --text: #E4E7F0;
27
- }
28
- html, body {
29
- margin: 0; padding: 0;
30
- background: #07080D; color: var(--text);
31
- font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
32
- font-size: 14px; min-height: 100vh; overflow: hidden;
33
- }
34
-
35
- header.app-head {
36
- padding: 20px 32px 14px;
37
- border-bottom: 1px solid var(--border);
38
- display: flex; align-items: center; justify-content: space-between;
39
- gap: 24px; flex-wrap: wrap;
40
- }
41
- header h1 {
42
- font-family: "Noto Sans SC", sans-serif;
43
- font-weight: 900; font-size: 20px; letter-spacing: 2px;
44
- margin: 0; color: var(--bone);
45
- }
46
- header h1 .ink { color: var(--amber); }
47
- header .sub { font-size: 11px; color: var(--muted); letter-spacing: 1px; margin-left: 12px; }
48
- header .meta { display: flex; gap: 8px; align-items: center; font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--muted); }
49
- header .meta .pill { border: 1px solid var(--border); padding: 4px 10px; border-radius: 999px; color: var(--amber-soft); }
50
-
51
- main {
52
- display: grid;
53
- grid-template-columns: 220px 1fr 340px;
54
- min-height: calc(100vh - 60px);
55
- }
56
- .tabs { border-right: 1px solid var(--border); padding: 20px 14px; background: #0A0C13; overflow-y: auto; max-height: calc(100vh - 60px); }
57
- .tab-title { font-size: 10px; letter-spacing: 2.5px; color: var(--muted); margin: 0 0 10px 4px; text-transform: uppercase; font-family: "JetBrains Mono", monospace; }
58
- .tab {
59
- display: block; width: 100%; text-align: left;
60
- padding: 12px; background: transparent;
61
- border: 1px solid transparent; color: var(--text);
62
- cursor: pointer; border-radius: 5px; margin-bottom: 6px;
63
- font-family: inherit; font-size: 13px; transition: all 0.15s;
64
- }
65
- .tab:hover { background: var(--panel); border-color: var(--border); }
66
- .tab.active { background: var(--panel-2); border-color: var(--amber); color: var(--bone); }
67
- .tab .ratio { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--amber); letter-spacing: 1px; }
68
- .tab .desc { font-size: 10px; color: var(--muted); margin-top: 3px; }
69
- .tabs .divider { height: 1px; background: var(--border); margin: 16px 0; }
70
-
71
- .upload-btn {
72
- display: block; width: 100%; padding: 10px; border-radius: 5px;
73
- background: transparent; border: 1px dashed var(--border); color: var(--muted);
74
- cursor: pointer; font-size: 11px; font-family: inherit;
75
- transition: all 0.15s; text-align: center;
76
- }
77
- .upload-btn:hover { border-color: var(--amber); color: var(--amber); }
78
- .upload-btn input { display: none; }
79
- .hint { font-size: 10px; color: var(--muted); line-height: 1.7; margin-top: 10px; padding: 10px; background: var(--panel); border-radius: 4px; border-left: 2px solid var(--amber); }
80
- .reset-btn {
81
- display: block; width: 100%; padding: 9px; margin-top: 8px;
82
- background: transparent; color: var(--muted);
83
- border: 1px solid var(--border); border-radius: 5px;
84
- font-family: inherit; font-size: 11px; cursor: pointer; letter-spacing: 1px;
85
- transition: all 0.15s;
86
- }
87
- .reset-btn:hover { color: var(--amber); border-color: var(--amber); }
88
-
89
- .stage {
90
- padding: 26px 32px 50px;
91
- display: flex; align-items: flex-start; justify-content: center;
92
- background: radial-gradient(ellipse at top, rgba(35,40,56,0.25), transparent 60%), #07080D;
93
- overflow: auto;
94
- }
95
- /* 工程注意:transform 只缩视觉不缩布局——占位靠 fitStage 的负 margin 收缩,勿改回直接设小宽高 */
96
- .canvas-wrap {
97
- position: relative;
98
- box-shadow: 0 20px 60px rgba(0,0,0,0.6), 0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(122,134,158,0.14);
99
- transform-origin: top left;
100
- }
101
- .canvas-wrap .dim-tag { position: absolute; top: -24px; left: 0; font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--muted); letter-spacing: 1.5px; }
102
- .canvas-wrap .dim-tag .num { color: var(--amber); }
103
-
104
- .cover { position: relative; overflow: hidden; background: var(--midnight); font-family: "Noto Sans SC", sans-serif; }
105
- .cover[data-size="16_9"] { width: 2400px; height: 1350px; }
106
- .cover[data-size="4_3"] { width: 2400px; height: 1800px; }
107
- .cover[data-size="3_4"] { width: 1500px; height: 2000px; }
108
- .cover .bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; user-select: none; pointer-events: none; }
109
- .cover { display: none; }
110
- .cover.active { display: block; }
111
-
112
- /* 所有文字图层的通用交互样式(引擎层,勿改) */
113
- .layer-text {
114
- position: absolute;
115
- cursor: grab;
116
- user-select: none;
117
- transition: outline 0.12s;
118
- outline: 0 dashed transparent;
119
- outline-offset: 8px;
120
- white-space: nowrap;
121
- }
122
- .layer-text:hover { outline: 3px dashed rgba(122,134,158,0.5); }
123
- .layer-text.selected { outline: 5px dashed rgba(122,134,158,0.95); }
124
- .layer-text.dragging { cursor: grabbing; outline: 5px solid rgba(122,134,158,0.95); }
125
-
126
- /* 替换位:T1 主字风格——字体/描边/发光按栏目风格资产注入;发光 rgba 与 applyT1Style() 内保持同色 */
127
- .t1 {
128
- font-family: "Noto Sans SC", sans-serif;
129
- font-weight: 900;
130
- color: #FFFFFF;
131
- line-height: 1;
132
- letter-spacing: 0;
133
- z-index: 10;
134
- -webkit-text-stroke: 2px var(--midnight);
135
- text-shadow:
136
- 0 0 1px var(--midnight),
137
- 0 0 2px var(--midnight),
138
- 0 0 3px var(--midnight),
139
- 0 0 20px rgba(200,210,230,0.14),
140
- 0 0 45px rgba(200,210,230,0.10),
141
- 0 8px 24px rgba(0,0,0,0.55);
142
- }
143
-
144
- /* 替换位:T2 副标题风格 */
145
- .t2 {
146
- font-family: "Noto Sans SC", sans-serif;
147
- font-weight: 700;
148
- color: rgba(236,238,243,0.88);
149
- letter-spacing: 0.4em;
150
- text-shadow: 0 2px 8px rgba(0,0,0,0.6);
151
- z-index: 9;
152
- }
153
- /* 替换位:T5 装饰英文风格 */
154
- .t5 {
155
- font-family: "Cormorant Garamond", serif;
156
- font-style: italic;
157
- font-weight: 500;
158
- color: rgba(236,238,243,0.5);
159
- letter-spacing: 0.3em;
160
- text-transform: uppercase;
161
- z-index: 8;
162
- }
163
- /* 替换位:T4 品牌戳风格(手写签名感;用户没给栏目名则 visible:false、不编内容) */
164
- .t4 {
165
- font-family: "Long Cang", cursive;
166
- font-weight: 400;
167
- color: rgba(236,238,243,0.72);
168
- letter-spacing: 0.12em;
169
- text-shadow: 0 2px 8px rgba(0,0,0,0.6);
170
- z-index: 8;
171
- }
172
-
173
- /* 装饰件:薄内框(默认关,按当期决策开)。浅底尺寸的深色覆写见 workbench-guide §5 */
174
- .inner-frame {
175
- position: absolute; inset: 50px;
176
- border: 2px solid rgba(236,238,243,0.30);
177
- pointer-events: none; z-index: 5;
178
- }
179
-
180
- /* 装饰件:四角断线框(默认关) */
181
- .corner {
182
- position: absolute; width: 80px; height: 80px;
183
- border: 1.5px solid rgba(236,238,243,0.28);
184
- pointer-events: none; z-index: 5;
185
- }
186
- .corner.tl { top: 60px; left: 60px; border-right: 0; border-bottom: 0; }
187
- .corner.tr { top: 60px; right: 60px; border-left: 0; border-bottom: 0; }
188
- .corner.bl { bottom: 60px; left: 60px; border-right: 0; border-top: 0; }
189
- .corner.br { bottom: 60px; right: 60px; border-left: 0; border-top: 0; }
190
-
191
- .controls { border-left: 1px solid var(--border); padding: 20px 16px; background: #0A0C13; overflow-y: auto; max-height: calc(100vh - 60px); }
192
- .ctrl-group { margin-bottom: 18px; }
193
- .ctrl-group h3 {
194
- font-size: 10px; letter-spacing: 2.5px; color: var(--muted);
195
- margin: 0 0 10px; text-transform: uppercase;
196
- font-family: "JetBrains Mono", monospace;
197
- display: flex; align-items: center; gap: 8px;
198
- }
199
- .ctrl-group h3::after { content: ''; flex: 1; height: 1px; background: var(--border); }
200
- .row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; gap: 8px; }
201
- .row label { font-size: 12px; color: var(--text); min-width: 60px; flex: 0 0 auto; }
202
- .row .val { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--amber); min-width: 44px; text-align: right; }
203
- .row input[type="range"] { flex: 1; accent-color: var(--amber); height: 3px; }
204
- .row input[type="text"], .row select {
205
- flex: 1; background: var(--panel); color: var(--text);
206
- border: 1px solid var(--border); border-radius: 4px;
207
- padding: 6px 8px; font-family: inherit; font-size: 12px;
208
- }
209
- .row input[type="text"]:focus, .row select:focus { outline: none; border-color: var(--amber); }
210
- .row input[type="checkbox"] { accent-color: var(--amber); }
211
-
212
- /* 通用 layer block(引擎层,勿改) */
213
- .layer-block {
214
- border: 1px solid var(--border);
215
- border-radius: 6px;
216
- padding: 9px 11px;
217
- margin-bottom: 7px;
218
- background: var(--panel);
219
- cursor: pointer;
220
- transition: all 0.15s;
221
- }
222
- .layer-block:hover { border-color: rgba(122,134,158,0.55); }
223
- .layer-block.selected { border-color: var(--amber); background: var(--panel-2); box-shadow: 0 0 0 1px rgba(122,134,158,0.35); }
224
- .layer-block .head {
225
- display: flex; align-items: center; justify-content: space-between;
226
- margin-bottom: 7px; gap: 6px;
227
- }
228
- .layer-block .char-preview {
229
- font-family: "Noto Sans SC", sans-serif;
230
- font-weight: 900; font-size: 22px;
231
- color: var(--bone);
232
- width: 30px; height: 30px;
233
- display: inline-flex; align-items: center; justify-content: center;
234
- background: var(--midnight); border-radius: 4px;
235
- flex-shrink: 0;
236
- }
237
- .layer-block .tag-preview {
238
- font-family: "JetBrains Mono", monospace;
239
- font-size: 10px; color: var(--amber);
240
- padding: 6px 10px;
241
- background: var(--midnight); border-radius: 4px;
242
- letter-spacing: 1px; flex-shrink: 0;
243
- }
244
- .layer-block .pos-tag {
245
- font-family: "JetBrains Mono", monospace;
246
- font-size: 10px; color: var(--muted);
247
- letter-spacing: 1px;
248
- }
249
- .layer-block .vis-toggle {
250
- font-family: "JetBrains Mono", monospace;
251
- font-size: 10px; color: var(--muted);
252
- display: flex; align-items: center; gap: 4px;
253
- }
254
- .layer-block .mini-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
255
- .layer-block .mini-row:last-child { margin-bottom: 0; }
256
- .layer-block .mini-row label { font-size: 10px; color: var(--muted); min-width: 14px; font-family: "JetBrains Mono", monospace; }
257
- .layer-block .mini-row input[type="number"] {
258
- width: 66px;
259
- background: var(--ink); color: var(--amber);
260
- border: 1px solid var(--border); border-radius: 3px;
261
- padding: 4px 6px;
262
- font-family: "JetBrains Mono", monospace; font-size: 11px;
263
- }
264
- .layer-block .mini-row input[type="number"]:focus { outline: none; border-color: var(--amber); }
265
- .layer-block .mini-row input[type="text"] {
266
- flex: 1; background: var(--ink); color: var(--text);
267
- border: 1px solid var(--border); border-radius: 3px;
268
- padding: 4px 6px;
269
- font-family: inherit; font-size: 11px;
270
- }
271
- .layer-block .mini-row input[type="text"]:focus { outline: none; border-color: var(--amber); }
272
-
273
- .export-btn {
274
- display: block; width: 100%;
275
- padding: 13px; margin-top: 6px;
276
- background: var(--amber); color: #fff;
277
- border: none; border-radius: 6px;
278
- font-family: inherit; font-weight: 700; font-size: 13px;
279
- cursor: pointer; letter-spacing: 2px; transition: all 0.15s;
280
- }
281
- .export-btn:hover { background: var(--amber-soft); transform: translateY(-1px); }
282
- .export-btn:disabled { background: var(--border); color: var(--muted); cursor: wait; transform: none; }
283
-
284
- .notes { font-size: 10px; color: var(--muted); line-height: 1.75; padding: 10px; background: var(--panel); border-radius: 4px; border-left: 2px solid var(--amber); }
285
- .notes strong { color: var(--amber-soft); }
286
- .notes kbd { background: var(--ink); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px; font-size: 10px; color: var(--amber-soft); font-family: "JetBrains Mono", monospace; }
287
-
288
- @media (max-width: 1200px) {
289
- main { grid-template-columns: 200px 1fr 300px; }
290
- }
291
- @media (max-width: 900px) {
292
- main { grid-template-columns: 1fr; }
293
- .tabs, .controls { border: none; max-height: none; }
294
- .tabs { display: flex; overflow-x: auto; padding: 12px; }
295
- .tab { flex-shrink: 0; width: 160px; margin-right: 6px; margin-bottom: 0; }
296
- }
297
- </style>
298
- </head>
299
- <body>
300
-
301
- <header class="app-head">
302
- <div>
303
- <!-- 替换位:当期 T1 与英文副题 -->
304
- <h1>示例 <span class="ink">·</span> 标题<span class="sub">TEMPLATE — 封面排版工作台</span></h1>
305
- </div>
306
- <div class="meta">
307
- <!-- 替换位:布局名 / 色板名 -->
308
- <span class="pill">布局待定</span>
309
- <span class="pill">色板待注入</span>
310
- <span class="pill">拖拽 / 滚轮 / 一键导出</span>
311
- </div>
312
- </header>
313
-
314
- <main>
315
- <aside class="tabs">
316
- <p class="tab-title">// 尺寸</p>
317
- <button class="tab active" data-size="16_9">
318
- <div class="ratio">16 : 9</div>
319
- <div class="desc">横版 · 布局按当期标注</div>
320
- </button>
321
- <button class="tab" data-size="4_3">
322
- <div class="ratio">4 : 3</div>
323
- <div class="desc">横版饱满 · 布局按当期标注</div>
324
- </button>
325
- <button class="tab" data-size="3_4">
326
- <div class="ratio">3 : 4</div>
327
- <div class="desc">竖版 · 布局按当期标注</div>
328
- </button>
329
-
330
- <div class="divider"></div>
331
- <p class="tab-title">// 替换背景</p>
332
- <label class="upload-btn">
333
- 上传当前尺寸的背景图
334
- <input type="file" id="bg-upload" accept="image/*">
335
- </label>
336
- <p class="hint">
337
- 背景图已默认载入三张。抽到新版本想替换,在此上传只影响当前激活尺寸。
338
- </p>
339
-
340
- <div class="divider"></div>
341
- <button class="reset-btn" id="resetAllBtn">↺ 恢复当前尺寸默认布局</button>
342
- </aside>
343
-
344
- <section class="stage" id="stage">
345
- <div class="canvas-wrap" id="canvasWrap">
346
- <div class="dim-tag"><span id="dimLabel">2400 × 1350</span> <span class="num">— 16:9</span></div>
347
-
348
- <!-- 替换位:char* div 数量与 T1_KEYS 一致(当期 T1 字数驱动),initial 逐字填 -->
349
- <div class="cover active" data-size="16_9">
350
- <img class="bg" src="__BG_16_9__" alt="">
351
- <div class="inner-frame"></div>
352
- <div class="corner tl"></div><div class="corner tr"></div>
353
- <div class="corner bl"></div><div class="corner br"></div>
354
- <div class="layer-text t1" data-layer-key="char1">示</div>
355
- <div class="layer-text t1" data-layer-key="char2">例</div>
356
- <div class="layer-text t1" data-layer-key="char3">标</div>
357
- <div class="layer-text t1" data-layer-key="char4">题</div>
358
- <div class="layer-text t2" data-layer-key="t2">副标题钩子示例</div>
359
- <div class="layer-text t5" data-layer-key="t5">SUBTITLE</div>
360
- <div class="layer-text t4" data-layer-key="t4">品牌戳未提供</div>
361
- </div>
362
-
363
- <div class="cover" data-size="4_3">
364
- <img class="bg" src="__BG_4_3__" alt="">
365
- <div class="inner-frame"></div>
366
- <div class="corner tl"></div><div class="corner tr"></div>
367
- <div class="corner bl"></div><div class="corner br"></div>
368
- <div class="layer-text t1" data-layer-key="char1">示</div>
369
- <div class="layer-text t1" data-layer-key="char2">例</div>
370
- <div class="layer-text t1" data-layer-key="char3">标</div>
371
- <div class="layer-text t1" data-layer-key="char4">题</div>
372
- <div class="layer-text t2" data-layer-key="t2">副标题钩子示例</div>
373
- <div class="layer-text t5" data-layer-key="t5">SUBTITLE</div>
374
- <div class="layer-text t4" data-layer-key="t4">品牌戳未提供</div>
375
- </div>
376
-
377
- <div class="cover" data-size="3_4">
378
- <img class="bg" src="__BG_3_4__" alt="">
379
- <div class="inner-frame"></div>
380
- <div class="corner tl"></div><div class="corner tr"></div>
381
- <div class="corner bl"></div><div class="corner br"></div>
382
- <div class="layer-text t1" data-layer-key="char1">示</div>
383
- <div class="layer-text t1" data-layer-key="char2">例</div>
384
- <div class="layer-text t1" data-layer-key="char3">标</div>
385
- <div class="layer-text t1" data-layer-key="char4">题</div>
386
- <div class="layer-text t2" data-layer-key="t2">副标题钩子示例</div>
387
- <div class="layer-text t5" data-layer-key="t5">SUBTITLE</div>
388
- <div class="layer-text t4" data-layer-key="t4">品牌戳未提供</div>
389
- </div>
390
- </div>
391
- </section>
392
-
393
- <aside class="controls">
394
- <div class="ctrl-group">
395
- <h3>T1 文案</h3>
396
- <div class="row">
397
- <!-- 替换位:value 与 maxlength 按当期 T1;与 state.t1Text 保持一致 -->
398
- <input type="text" id="t1-text" value="示例标题" maxlength="4"
399
- style="font-family:'Noto Sans SC',sans-serif; font-weight:700; letter-spacing:0.3em; font-size:14px;">
400
- </div>
401
- <p class="notes" style="margin-top:6px;">
402
- 改这里 <strong>同步到全部三张</strong>;每字独立位置/字号在下方调。
403
- </p>
404
- </div>
405
-
406
- <div class="ctrl-group">
407
- <h3>T1 逐字微调 <span style="font-size:9px; color:var(--amber); margin-left:4px;">画布上可拖</span></h3>
408
- <div id="char-blocks"></div>
409
- </div>
410
-
411
- <div class="ctrl-group">
412
- <h3>T1 全局样式</h3>
413
- <div class="row">
414
- <label>描边粗细</label>
415
- <input type="range" id="t1-stroke" min="0" max="8" step="0.5" value="2">
416
- <span class="val" id="t1-stroke-val">2</span>
417
- </div>
418
- <div class="row">
419
- <!-- 替换位:发光名与默认值跟色板叫(银辉/琥珀/…) -->
420
- <label>外发光</label>
421
- <input type="range" id="t1-glow" min="0" max="100" step="5" value="20">
422
- <span class="val" id="t1-glow-val">20%</span>
423
- </div>
424
- <div class="row">
425
- <label>字色</label>
426
- <!-- 替换位:字色选项按栏目色板给 2~3 档 -->
427
- <select id="t1-color">
428
- <option value="#FFFFFF" selected>纯白 #FFFFFF</option>
429
- <option value="#ECEEF3">浅灰白 #ECEEF3</option>
430
- <option value="#7A869E">强调色 #7A869E</option>
431
- </select>
432
- </div>
433
- <div class="row">
434
- <label>统一字号</label>
435
- <input type="range" id="t1-size-all" min="280" max="800" step="10" value="520">
436
- <span class="val" id="t1-size-all-val">520</span>
437
- </div>
438
- </div>
439
-
440
- <div class="ctrl-group">
441
- <h3>附加图层 <span style="font-size:9px; color:var(--amber); margin-left:4px;">同样可拖</span></h3>
442
- <div id="sub-blocks"></div>
443
- </div>
444
-
445
- <div class="ctrl-group">
446
- <h3>装饰件</h3>
447
- <!-- 替换位:默认开关按当期装饰件决策;不用的装饰件保持关闭即可 -->
448
- <div class="row">
449
- <label>薄内框</label>
450
- <input type="checkbox" id="inner-show">
451
- </div>
452
- <div class="row">
453
- <label>四角断线框</label>
454
- <input type="checkbox" id="corner-show">
455
- </div>
456
- </div>
457
-
458
- <div class="ctrl-group">
459
- <h3>导出</h3>
460
- <button class="export-btn" id="exportBtn">导出当前封面 PNG</button>
461
- <button class="export-btn" id="exportAllBtn" style="margin-top:8px; background: transparent; color: var(--amber); border: 1.5px solid var(--amber);">一键导出全部三张</button>
462
- <p class="notes" style="margin-top: 10px;">
463
- <strong>导出规格:</strong>原分辨率 PNG<br>
464
- 16:9 → 2400 × 1350<br>
465
- 4 : 3 → 2400 × 1800<br>
466
- 3 : 4 → 1500 × 2000<br><br>
467
- <strong>操作提示:</strong><br>
468
- · 画布上 <kbd>拖任何字</kbd> 直接改位置<br>
469
- · 点字选中后 <kbd>↑↓←→</kbd> 精调 1px<br>
470
- · 按住 <kbd>Shift</kbd> + 方向键走 10px<br>
471
- · 数值框选中后 <kbd>滚轮</kbd> 微调,<kbd>Shift</kbd>+滚轮 ×10<br>
472
- · 一键导出首次会请求「允许下载多个文件」,点允许即可<br>
473
- · 布局状态在内存里,调好就导出、导出前别刷新<br>
474
- </p>
475
- </div>
476
- </aside>
477
- </main>
478
-
479
- <script>
480
- // ===== 替换位:T1 字数驱动(当期 T1 为 N 字 → T1_KEYS 列 N 项 + 各 cover 放 N 个 char div + maxlength=N + DEFAULTS 各含 N 条)=====
481
- const T1_KEYS = ['char1', 'char2', 'char3', 'char4'];
482
- const T1_LEN = T1_KEYS.length;
483
-
484
- // ===== 替换位:默认布局配方——从 layout-recipes 选配方起步,按实际画面气口微调;注意分发平台安全区 =====
485
- const DEFAULTS = {
486
- '16_9': {
487
- canvas: { w: 2400, h: 1350 },
488
- layers: {
489
- char1: { type: 't1', pos: '左上', left: 140, top: 200, size: 520 },
490
- char2: { type: 't1', pos: '左下', left: 260, top: 720, size: 520 },
491
- char3: { type: 't1', pos: '右上', left: 1740, top: 200, size: 520 },
492
- char4: { type: 't1', pos: '右下', left: 1620, top: 720, size: 520 },
493
- t2: { type: 't2', pos: '底部居中', text: '副标题钩子示例', left: 910, top: 1130, size: 46, visible: true },
494
- t5: { type: 't5', pos: '顶部居中', text: 'SUBTITLE', left: 1020, top: 80, size: 34, visible: true },
495
- t4: { type: 't4', pos: '左下角', text: '品牌戳未提供', left: 70, top: 1156, size: 44, visible: false },
496
- },
497
- },
498
- '4_3': {
499
- canvas: { w: 2400, h: 1800 },
500
- layers: {
501
- char1: { type: 't1', pos: '左上', left: 110, top: 150, size: 560 },
502
- char2: { type: 't1', pos: '左中', left: 300, top: 700, size: 560 },
503
- char3: { type: 't1', pos: '右中', left: 1540, top: 540, size: 560 },
504
- char4: { type: 't1', pos: '右下', left: 1730, top: 1090, size: 560 },
505
- t2: { type: 't2', pos: '右下角', text: '副标题钩子示例', left: 1680, top: 1680, size: 44, visible: true },
506
- t5: { type: 't5', pos: '左上角', text: 'SUBTITLE', left: 130, top: 75, size: 36, visible: true },
507
- t4: { type: 't4', pos: '左下角', text: '品牌戳未提供', left: 150, top: 1700, size: 40, visible: false },
508
- },
509
- },
510
- '3_4': {
511
- canvas: { w: 1500, h: 2000 },
512
- layers: {
513
- char1: { type: 't1', pos: '左上', left: 180, top: 140, size: 440 },
514
- char2: { type: 't1', pos: '右上', left: 880, top: 140, size: 440 },
515
- char3: { type: 't1', pos: '左下', left: 180, top: 1380, size: 440 },
516
- char4: { type: 't1', pos: '右下', left: 880, top: 1380, size: 440 },
517
- t2: { type: 't2', pos: '中段留白', text: '副标题钩子示例', left: 475, top: 620, size: 44, visible: true },
518
- t5: { type: 't5', pos: '底部居中', text: 'SUBTITLE', left: 550, top: 1880, size: 34, visible: true },
519
- t4: { type: 't4', pos: '左上角', text: '品牌戳未提供', left: 100, top: 40, size: 38, visible: false },
520
- },
521
- },
522
- };
523
-
524
- // 替换位:附加图层组(加图层四步见 workbench-guide §4)
525
- const SUB_LABELS = { t2: 'T2 副标题', t5: 'T5 英文', t4: 'T4 品牌戳' };
526
- const SUB_KEYS = ['t2', 't5', 't4'];
527
-
528
- const state = {
529
- activeSize: '16_9',
530
- t1Text: '示例标题', // 替换位:与 #t1-text 的 value 一致
531
- selectedLayerKey: null,
532
- data: JSON.parse(JSON.stringify(DEFAULTS)),
533
- currentScale: 1,
534
- };
535
-
536
- function fitStage() {
537
- const stage = document.getElementById('stage');
538
- const wrap = document.getElementById('canvasWrap');
539
- const size = DEFAULTS[state.activeSize].canvas;
540
- const headH = document.querySelector('header.app-head').offsetHeight;
541
- const availW = stage.clientWidth - 64;
542
- const availH = window.innerHeight - headH - 120;
543
- const scale = Math.min(availW / size.w, availH / size.h, 1);
544
- state.currentScale = scale;
545
- wrap.style.transform = `scale(${scale})`;
546
- wrap.style.width = size.w + 'px';
547
- wrap.style.height = size.h + 'px';
548
- // 负 margin 把布局占位收缩到缩放后尺寸(transform 只缩视觉不缩布局,占位不缩会把页面撑出滚动条)
549
- wrap.style.marginRight = -(size.w * (1 - scale)) + 'px';
550
- wrap.style.marginBottom = -(size.h * (1 - scale)) + 'px';
551
- document.getElementById('dimLabel').textContent = `${size.w} × ${size.h}`;
552
- document.querySelector('.dim-tag .num').textContent = `— ${state.activeSize.replace('_', ':')}`;
553
- }
554
- window.addEventListener('resize', fitStage);
555
-
556
- // ===== 应用图层位置和字号 =====
557
- function applyLayers() {
558
- const cover = document.querySelector('.cover.active');
559
- const layers = state.data[state.activeSize].layers;
560
- Object.entries(layers).forEach(([key, ls]) => {
561
- const el = cover.querySelector(`.layer-text[data-layer-key="${key}"]`);
562
- if (!el) return;
563
- el.style.left = ls.left + 'px';
564
- el.style.top = ls.top + 'px';
565
- el.style.fontSize = ls.size + 'px';
566
- // 非 T1 图层的 text 和 visible(对任意附加图层通用)
567
- if (ls.type !== 't1') {
568
- document.querySelectorAll(`.cover .layer-text[data-layer-key="${key}"]`).forEach(n => {
569
- n.textContent = ls.text;
570
- n.style.display = ls.visible ? 'block' : 'none';
571
- });
572
- }
573
- });
574
- }
575
-
576
- function applyT1Text() {
577
- const raw = document.getElementById('t1-text').value;
578
- const text = (raw + ' '.repeat(T1_LEN)).slice(0, T1_LEN);
579
- state.t1Text = text;
580
- document.querySelectorAll('.cover').forEach(cover => {
581
- T1_KEYS.forEach((k, i) => {
582
- const el = cover.querySelector(`.layer-text[data-layer-key="${k}"]`);
583
- if (el) el.textContent = text.charAt(i);
584
- });
585
- });
586
- document.querySelectorAll('.char-block .char-preview').forEach((el, i) => {
587
- el.textContent = text.charAt(i);
588
- });
589
- }
590
-
591
- function applyT1Style() {
592
- const stroke = document.getElementById('t1-stroke').value;
593
- const glow = document.getElementById('t1-glow').value / 100;
594
- const color = document.getElementById('t1-color').value;
595
- document.getElementById('t1-stroke-val').textContent = stroke;
596
- document.getElementById('t1-glow-val').textContent = (glow * 100 | 0) + '%';
597
- document.querySelectorAll('.t1').forEach(el => {
598
- el.style.webkitTextStroke = `${stroke}px var(--midnight)`;
599
- el.style.color = color;
600
- // 替换位:发光 rgba 与 CSS .t1 的 text-shadow 保持同色
601
- el.style.textShadow = `
602
- 0 0 1px var(--midnight),
603
- 0 0 2px var(--midnight),
604
- 0 0 3px var(--midnight),
605
- 0 0 20px rgba(200,210,230,${glow * 0.7}),
606
- 0 0 45px rgba(200,210,230,${glow * 0.5}),
607
- 0 8px 24px rgba(0,0,0,0.55)
608
- `;
609
- });
610
- }
611
-
612
- function applyT1SizeAll() {
613
- const size = parseInt(document.getElementById('t1-size-all').value);
614
- document.getElementById('t1-size-all-val').textContent = size;
615
- T1_KEYS.forEach(k => {
616
- state.data[state.activeSize].layers[k].size = size;
617
- });
618
- applyLayers();
619
- renderCharBlocks();
620
- }
621
-
622
- function applyCorner() {
623
- const show = document.getElementById('corner-show').checked;
624
- document.querySelectorAll('.cover.active .corner').forEach(c => {
625
- c.style.display = show ? 'block' : 'none';
626
- });
627
- }
628
-
629
- function applyInner() {
630
- const show = document.getElementById('inner-show').checked;
631
- document.querySelectorAll('.cover.active .inner-frame').forEach(c => {
632
- c.style.display = show ? 'block' : 'none';
633
- });
634
- }
635
-
636
- // ===== 右栏 T1 逐字 block =====
637
- function renderCharBlocks() {
638
- const container = document.getElementById('char-blocks');
639
- const layers = state.data[state.activeSize].layers;
640
- container.innerHTML = '';
641
- T1_KEYS.forEach((k, i) => {
642
- const ls = layers[k];
643
- const block = document.createElement('div');
644
- block.className = 'layer-block';
645
- block.dataset.layerKey = k;
646
- if (state.selectedLayerKey === k) block.classList.add('selected');
647
- block.innerHTML = `
648
- <div class="head">
649
- <span class="char-preview">${state.t1Text.charAt(i)}</span>
650
- <span class="pos-tag">${ls.pos}</span>
651
- </div>
652
- <div class="mini-row">
653
- <label>X</label>
654
- <input type="number" data-field="left" value="${Math.round(ls.left)}" step="5">
655
- <label>Y</label>
656
- <input type="number" data-field="top" value="${Math.round(ls.top)}" step="5">
657
- </div>
658
- <div class="mini-row">
659
- <label>号</label>
660
- <input type="number" data-field="size" value="${ls.size}" step="10" min="100" max="900">
661
- </div>
662
- `;
663
- bindLayerBlock(block, ls, k);
664
- container.appendChild(block);
665
- });
666
- }
667
-
668
- // ===== 右栏附加图层 block =====
669
- function renderSubBlocks() {
670
- const container = document.getElementById('sub-blocks');
671
- const layers = state.data[state.activeSize].layers;
672
- container.innerHTML = '';
673
- SUB_KEYS.forEach(k => {
674
- const ls = layers[k];
675
- const block = document.createElement('div');
676
- block.className = 'layer-block';
677
- block.dataset.layerKey = k;
678
- if (state.selectedLayerKey === k) block.classList.add('selected');
679
- const labelText = SUB_LABELS[k];
680
- block.innerHTML = `
681
- <div class="head">
682
- <span class="tag-preview">${labelText}</span>
683
- <span class="pos-tag">${ls.pos}</span>
684
- <label class="vis-toggle" title="显示 / 隐藏">
685
- <input type="checkbox" data-field="visible" ${ls.visible ? 'checked' : ''}> 显示
686
- </label>
687
- </div>
688
- <div class="mini-row">
689
- <input type="text" data-field="text" value="${escapeHtml(ls.text)}">
690
- </div>
691
- <div class="mini-row">
692
- <label>X</label>
693
- <input type="number" data-field="left" value="${Math.round(ls.left)}" step="5">
694
- <label>Y</label>
695
- <input type="number" data-field="top" value="${Math.round(ls.top)}" step="5">
696
- </div>
697
- <div class="mini-row">
698
- <label>号</label>
699
- <input type="number" data-field="size" value="${ls.size}" step="2" min="14" max="200">
700
- </div>
701
- `;
702
- bindLayerBlock(block, ls, k);
703
- container.appendChild(block);
704
- });
705
- }
706
-
707
- function bindLayerBlock(block, ls, key) {
708
- block.addEventListener('click', (e) => {
709
- if (e.target.tagName === 'INPUT') return;
710
- selectLayer(key);
711
- });
712
- block.querySelectorAll('input').forEach(input => {
713
- input.addEventListener('click', (e) => e.stopPropagation());
714
- input.addEventListener('focus', () => selectLayer(key));
715
- if (input.type === 'number') {
716
- input.addEventListener('wheel', (e) => {
717
- if (document.activeElement !== input) return;
718
- e.preventDefault();
719
- const step = (parseFloat(input.step) || 1) * (e.shiftKey ? 10 : 1);
720
- const dir = e.deltaY < 0 ? 1 : -1;
721
- input.value = String((parseFloat(input.value) || 0) + dir * step);
722
- input.dispatchEvent(new Event('input'));
723
- }, { passive: false });
724
- }
725
- input.addEventListener('input', (e) => {
726
- const field = e.target.dataset.field;
727
- if (field === 'visible') {
728
- ls.visible = e.target.checked;
729
- } else if (field === 'text') {
730
- ls.text = e.target.value;
731
- } else {
732
- const val = parseFloat(e.target.value);
733
- if (!isNaN(val)) ls[field] = val;
734
- }
735
- applyLayers();
736
- });
737
- });
738
- }
739
-
740
- function escapeHtml(s) {
741
- return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
742
- }
743
-
744
- function selectLayer(key) {
745
- state.selectedLayerKey = key;
746
- document.querySelectorAll('.layer-text').forEach(el => el.classList.remove('selected'));
747
- document.querySelectorAll('.layer-block').forEach(el => el.classList.remove('selected'));
748
- if (key) {
749
- const el = document.querySelector(`.cover.active .layer-text[data-layer-key="${key}"]`);
750
- if (el) el.classList.add('selected');
751
- const block = document.querySelector(`.layer-block[data-layer-key="${key}"]`);
752
- if (block) block.classList.add('selected');
753
- }
754
- }
755
-
756
- // ===== 拖拽(引擎层,勿改) =====
757
- let dragState = null;
758
- document.addEventListener('mousedown', (e) => {
759
- const layer = e.target.closest('.layer-text');
760
- if (!layer || !layer.closest('.cover.active')) return;
761
- const key = layer.dataset.layerKey;
762
- selectLayer(key);
763
- const ls = state.data[state.activeSize].layers[key];
764
- dragState = {
765
- key, startX: e.clientX, startY: e.clientY,
766
- startLeft: ls.left, startTop: ls.top,
767
- };
768
- layer.classList.add('dragging');
769
- e.preventDefault();
770
- });
771
- document.addEventListener('mousemove', (e) => {
772
- if (!dragState) return;
773
- const dx = (e.clientX - dragState.startX) / state.currentScale;
774
- const dy = (e.clientY - dragState.startY) / state.currentScale;
775
- const ls = state.data[state.activeSize].layers[dragState.key];
776
- ls.left = dragState.startLeft + dx;
777
- ls.top = dragState.startTop + dy;
778
- applyLayers();
779
- const block = document.querySelector(`.layer-block[data-layer-key="${dragState.key}"]`);
780
- if (block) {
781
- const lInput = block.querySelector('input[data-field="left"]');
782
- const tInput = block.querySelector('input[data-field="top"]');
783
- if (lInput) lInput.value = Math.round(ls.left);
784
- if (tInput) tInput.value = Math.round(ls.top);
785
- }
786
- });
787
- document.addEventListener('mouseup', () => {
788
- if (dragState) {
789
- document.querySelectorAll('.layer-text.dragging').forEach(el => el.classList.remove('dragging'));
790
- dragState = null;
791
- }
792
- });
793
-
794
- // ===== 方向键(引擎层,勿改) =====
795
- document.addEventListener('keydown', (e) => {
796
- if (!state.selectedLayerKey) return;
797
- const tag = document.activeElement?.tagName;
798
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
799
- const step = e.shiftKey ? 10 : 1;
800
- const ls = state.data[state.activeSize].layers[state.selectedLayerKey];
801
- if (!ls) return;
802
- let handled = true;
803
- if (e.key === 'ArrowLeft') ls.left -= step;
804
- else if (e.key === 'ArrowRight') ls.left += step;
805
- else if (e.key === 'ArrowUp') ls.top -= step;
806
- else if (e.key === 'ArrowDown') ls.top += step;
807
- else handled = false;
808
- if (handled) {
809
- e.preventDefault();
810
- applyLayers();
811
- renderCharBlocks();
812
- renderSubBlocks();
813
- selectLayer(state.selectedLayerKey);
814
- }
815
- });
816
-
817
- // ===== tab 切换 =====
818
- document.querySelectorAll('.tab').forEach(tab => {
819
- tab.addEventListener('click', () => {
820
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
821
- document.querySelectorAll('.cover').forEach(c => c.classList.remove('active'));
822
- tab.classList.add('active');
823
- state.activeSize = tab.dataset.size;
824
- document.querySelector(`.cover[data-size="${state.activeSize}"]`).classList.add('active');
825
- state.selectedLayerKey = null;
826
- const defSize = DEFAULTS[state.activeSize].layers.char1.size;
827
- document.getElementById('t1-size-all').value = defSize;
828
- document.getElementById('t1-size-all-val').textContent = defSize;
829
- fitStage();
830
- applyLayers();
831
- applyT1Style();
832
- applyCorner();
833
- applyInner();
834
- renderCharBlocks();
835
- renderSubBlocks();
836
- });
837
- });
838
-
839
- document.getElementById('t1-text').addEventListener('input', applyT1Text);
840
- ['t1-stroke', 't1-glow', 't1-color'].forEach(id =>
841
- document.getElementById(id).addEventListener('input', applyT1Style));
842
- document.getElementById('t1-size-all').addEventListener('input', applyT1SizeAll);
843
- document.getElementById('corner-show').addEventListener('input', applyCorner);
844
- document.getElementById('inner-show').addEventListener('input', applyInner);
845
-
846
- document.getElementById('resetAllBtn').addEventListener('click', () => {
847
- state.data[state.activeSize] = JSON.parse(JSON.stringify(DEFAULTS[state.activeSize]));
848
- state.selectedLayerKey = null;
849
- applyLayers();
850
- renderCharBlocks();
851
- renderSubBlocks();
852
- });
853
-
854
- document.getElementById('bg-upload').addEventListener('change', e => {
855
- const file = e.target.files[0];
856
- if (!file) return;
857
- const reader = new FileReader();
858
- reader.onload = ev => {
859
- document.querySelector('.cover.active .bg').src = ev.target.result;
860
- };
861
- reader.readAsDataURL(file);
862
- });
863
-
864
- // ===== 导出(引擎层,勿改) =====
865
- document.getElementById('exportBtn').addEventListener('click', async () => {
866
- const btn = document.getElementById('exportBtn');
867
- btn.disabled = true;
868
- btn.textContent = '正在渲染…';
869
- const cover = document.querySelector('.cover.active');
870
- const wrap = document.getElementById('canvasWrap');
871
- const originalTransform = wrap.style.transform;
872
- wrap.style.transform = 'scale(1)';
873
- document.querySelectorAll('.layer-text').forEach(el => el.classList.remove('selected', 'dragging'));
874
- try {
875
- const canvas = await html2canvas(cover, {
876
- backgroundColor: null, scale: 1, useCORS: true, logging: false,
877
- width: cover.offsetWidth, height: cover.offsetHeight,
878
- });
879
- const link = document.createElement('a');
880
- const txt = state.t1Text.replace(/ /g, '').trim() || '封面';
881
- link.download = `${txt}_${state.activeSize.replace('_', 'x')}.png`;
882
- link.href = canvas.toDataURL('image/png');
883
- link.click();
884
- btn.textContent = '✓ 导出成功';
885
- setTimeout(() => { btn.textContent = '导出当前封面 PNG'; btn.disabled = false; }, 1500);
886
- } catch (err) {
887
- console.error(err);
888
- btn.textContent = '导出失败,看控制台';
889
- btn.disabled = false;
890
- } finally {
891
- wrap.style.transform = originalTransform;
892
- if (state.selectedLayerKey) selectLayer(state.selectedLayerKey);
893
- }
894
- });
895
-
896
- // ===== 一键导出全部三张(引擎层,勿改) =====
897
- document.getElementById('exportAllBtn').addEventListener('click', async () => {
898
- const btn = document.getElementById('exportAllBtn');
899
- const originalSize = state.activeSize;
900
- btn.disabled = true;
901
- const wrap = document.getElementById('canvasWrap');
902
- const originalTransform = wrap.style.transform;
903
- wrap.style.transform = 'scale(1)';
904
- document.querySelectorAll('.layer-text').forEach(el => el.classList.remove('selected', 'dragging'));
905
- try {
906
- for (const size of ['16_9', '4_3', '3_4']) {
907
- btn.textContent = `正在渲染 ${size.replace('_', ':')} …`;
908
- document.querySelectorAll('.cover').forEach(c => c.classList.remove('active'));
909
- const cover = document.querySelector(`.cover[data-size="${size}"]`);
910
- cover.classList.add('active');
911
- state.activeSize = size;
912
- applyLayers();
913
- applyCorner();
914
- applyInner();
915
- await new Promise(r => setTimeout(r, 80));
916
- const canvas = await html2canvas(cover, {
917
- backgroundColor: null, scale: 1, useCORS: true, logging: false,
918
- width: cover.offsetWidth, height: cover.offsetHeight,
919
- });
920
- const link = document.createElement('a');
921
- const txt = state.t1Text.replace(/ /g, '').trim() || '封面';
922
- link.download = `${txt}_${size.replace('_', 'x')}.png`;
923
- link.href = canvas.toDataURL('image/png');
924
- link.click();
925
- await new Promise(r => setTimeout(r, 400));
926
- }
927
- btn.textContent = '✓ 三张已导出';
928
- } catch (err) {
929
- console.error(err);
930
- btn.textContent = '导出失败,看控制台';
931
- } finally {
932
- wrap.style.transform = originalTransform;
933
- const backTab = document.querySelector(`.tab[data-size="${originalSize}"]`);
934
- if (backTab) backTab.click();
935
- setTimeout(() => { btn.textContent = '一键导出全部三张'; btn.disabled = false; }, 1800);
936
- }
937
- });
938
-
939
- function init() {
940
- fitStage();
941
- applyT1Text();
942
- applyLayers();
943
- applyT1Style();
944
- applyCorner();
945
- applyInner();
946
- renderCharBlocks();
947
- renderSubBlocks();
948
- }
949
- document.fonts.ready.then(init);
950
- setTimeout(fitStage, 100);
951
- setTimeout(fitStage, 600);
952
- </script>
953
-
954
- </body>
955
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>示例标题 · 封面排版工作台</title>
6
+ <!-- ↑ 替换位:当期 T1 -->
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <!-- 替换位:字体族按栏目风格资产增删(衬线/黑体/手写体等) -->
10
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@700;900&family=Noto+Sans+SC:wght@400;700;900&family=Cormorant+Garamond:ital,wght@0,500;1,400&family=JetBrains+Mono:wght@400;600&family=Long+Cang&display=swap" rel="stylesheet">
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
12
+ <style>
13
+ *, *::before, *::after { box-sizing: border-box; }
14
+ /* 替换位:色板变量——变量名固定、换值即换装(语义见 workbench-guide §3)。以下为中性默认值 */
15
+ :root {
16
+ --midnight: #14161E;
17
+ --indigo: #232838;
18
+ --amber: #7A869E;
19
+ --amber-soft: #9AA5BC;
20
+ --bone: #ECEEF3;
21
+ --ink: #0A0C12;
22
+ --panel: #161923;
23
+ --panel-2: #1E2230;
24
+ --border: #2C3145;
25
+ --muted: #8B92A8;
26
+ --text: #E4E7F0;
27
+ }
28
+ html, body {
29
+ margin: 0; padding: 0;
30
+ background: #07080D; color: var(--text);
31
+ font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
32
+ font-size: 14px; min-height: 100vh; overflow: hidden;
33
+ }
34
+
35
+ header.app-head {
36
+ padding: 20px 32px 14px;
37
+ border-bottom: 1px solid var(--border);
38
+ display: flex; align-items: center; justify-content: space-between;
39
+ gap: 24px; flex-wrap: wrap;
40
+ }
41
+ header h1 {
42
+ font-family: "Noto Sans SC", sans-serif;
43
+ font-weight: 900; font-size: 20px; letter-spacing: 2px;
44
+ margin: 0; color: var(--bone);
45
+ }
46
+ header h1 .ink { color: var(--amber); }
47
+ header .sub { font-size: 11px; color: var(--muted); letter-spacing: 1px; margin-left: 12px; }
48
+ header .meta { display: flex; gap: 8px; align-items: center; font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--muted); }
49
+ header .meta .pill { border: 1px solid var(--border); padding: 4px 10px; border-radius: 999px; color: var(--amber-soft); }
50
+
51
+ main {
52
+ display: grid;
53
+ grid-template-columns: 220px 1fr 340px;
54
+ min-height: calc(100vh - 60px);
55
+ }
56
+ .tabs { border-right: 1px solid var(--border); padding: 20px 14px; background: #0A0C13; overflow-y: auto; max-height: calc(100vh - 60px); }
57
+ .tab-title { font-size: 10px; letter-spacing: 2.5px; color: var(--muted); margin: 0 0 10px 4px; text-transform: uppercase; font-family: "JetBrains Mono", monospace; }
58
+ .tab {
59
+ display: block; width: 100%; text-align: left;
60
+ padding: 12px; background: transparent;
61
+ border: 1px solid transparent; color: var(--text);
62
+ cursor: pointer; border-radius: 5px; margin-bottom: 6px;
63
+ font-family: inherit; font-size: 13px; transition: all 0.15s;
64
+ }
65
+ .tab:hover { background: var(--panel); border-color: var(--border); }
66
+ .tab.active { background: var(--panel-2); border-color: var(--amber); color: var(--bone); }
67
+ .tab .ratio { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--amber); letter-spacing: 1px; }
68
+ .tab .desc { font-size: 10px; color: var(--muted); margin-top: 3px; }
69
+ .tabs .divider { height: 1px; background: var(--border); margin: 16px 0; }
70
+
71
+ .upload-btn {
72
+ display: block; width: 100%; padding: 10px; border-radius: 5px;
73
+ background: transparent; border: 1px dashed var(--border); color: var(--muted);
74
+ cursor: pointer; font-size: 11px; font-family: inherit;
75
+ transition: all 0.15s; text-align: center;
76
+ }
77
+ .upload-btn:hover { border-color: var(--amber); color: var(--amber); }
78
+ .upload-btn input { display: none; }
79
+ .hint { font-size: 10px; color: var(--muted); line-height: 1.7; margin-top: 10px; padding: 10px; background: var(--panel); border-radius: 4px; border-left: 2px solid var(--amber); }
80
+ .reset-btn {
81
+ display: block; width: 100%; padding: 9px; margin-top: 8px;
82
+ background: transparent; color: var(--muted);
83
+ border: 1px solid var(--border); border-radius: 5px;
84
+ font-family: inherit; font-size: 11px; cursor: pointer; letter-spacing: 1px;
85
+ transition: all 0.15s;
86
+ }
87
+ .reset-btn:hover { color: var(--amber); border-color: var(--amber); }
88
+
89
+ .stage {
90
+ padding: 26px 32px 50px;
91
+ display: flex; align-items: flex-start; justify-content: center;
92
+ background: radial-gradient(ellipse at top, rgba(35,40,56,0.25), transparent 60%), #07080D;
93
+ overflow: auto;
94
+ }
95
+ /* 工程注意:transform 只缩视觉不缩布局——占位靠 fitStage 的负 margin 收缩,勿改回直接设小宽高 */
96
+ .canvas-wrap {
97
+ position: relative;
98
+ box-shadow: 0 20px 60px rgba(0,0,0,0.6), 0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(122,134,158,0.14);
99
+ transform-origin: top left;
100
+ }
101
+ .canvas-wrap .dim-tag { position: absolute; top: -24px; left: 0; font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--muted); letter-spacing: 1.5px; }
102
+ .canvas-wrap .dim-tag .num { color: var(--amber); }
103
+
104
+ .cover { position: relative; overflow: hidden; background: var(--midnight); font-family: "Noto Sans SC", sans-serif; }
105
+ .cover[data-size="16_9"] { width: 2400px; height: 1350px; }
106
+ .cover[data-size="4_3"] { width: 2400px; height: 1800px; }
107
+ .cover[data-size="3_4"] { width: 1500px; height: 2000px; }
108
+ .cover .bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; user-select: none; pointer-events: none; }
109
+ .cover { display: none; }
110
+ .cover.active { display: block; }
111
+
112
+ /* 所有文字图层的通用交互样式(引擎层,勿改) */
113
+ .layer-text {
114
+ position: absolute;
115
+ cursor: grab;
116
+ user-select: none;
117
+ transition: outline 0.12s;
118
+ outline: 0 dashed transparent;
119
+ outline-offset: 8px;
120
+ white-space: nowrap;
121
+ }
122
+ .layer-text:hover { outline: 3px dashed rgba(122,134,158,0.5); }
123
+ .layer-text.selected { outline: 5px dashed rgba(122,134,158,0.95); }
124
+ .layer-text.dragging { cursor: grabbing; outline: 5px solid rgba(122,134,158,0.95); }
125
+
126
+ /* 替换位:T1 主字风格——字体/描边/发光按栏目风格资产注入;发光 rgba 与 applyT1Style() 内保持同色 */
127
+ .t1 {
128
+ font-family: "Noto Sans SC", sans-serif;
129
+ font-weight: 900;
130
+ color: #FFFFFF;
131
+ line-height: 1;
132
+ letter-spacing: 0;
133
+ z-index: 10;
134
+ -webkit-text-stroke: 2px var(--midnight);
135
+ text-shadow:
136
+ 0 0 1px var(--midnight),
137
+ 0 0 2px var(--midnight),
138
+ 0 0 3px var(--midnight),
139
+ 0 0 20px rgba(200,210,230,0.14),
140
+ 0 0 45px rgba(200,210,230,0.10),
141
+ 0 8px 24px rgba(0,0,0,0.55);
142
+ }
143
+
144
+ /* 替换位:T2 副标题风格 */
145
+ .t2 {
146
+ font-family: "Noto Sans SC", sans-serif;
147
+ font-weight: 700;
148
+ color: rgba(236,238,243,0.88);
149
+ letter-spacing: 0.4em;
150
+ text-shadow: 0 2px 8px rgba(0,0,0,0.6);
151
+ z-index: 9;
152
+ }
153
+ /* 替换位:T5 装饰英文风格 */
154
+ .t5 {
155
+ font-family: "Cormorant Garamond", serif;
156
+ font-style: italic;
157
+ font-weight: 500;
158
+ color: rgba(236,238,243,0.5);
159
+ letter-spacing: 0.3em;
160
+ text-transform: uppercase;
161
+ z-index: 8;
162
+ }
163
+ /* 替换位:T4 品牌戳风格(手写签名感;用户没给栏目名则 visible:false、不编内容) */
164
+ .t4 {
165
+ font-family: "Long Cang", cursive;
166
+ font-weight: 400;
167
+ color: rgba(236,238,243,0.72);
168
+ letter-spacing: 0.12em;
169
+ text-shadow: 0 2px 8px rgba(0,0,0,0.6);
170
+ z-index: 8;
171
+ }
172
+
173
+ /* 装饰件:薄内框(默认关,按当期决策开)。浅底尺寸的深色覆写见 workbench-guide §5 */
174
+ .inner-frame {
175
+ position: absolute; inset: 50px;
176
+ border: 2px solid rgba(236,238,243,0.30);
177
+ pointer-events: none; z-index: 5;
178
+ }
179
+
180
+ /* 装饰件:四角断线框(默认关) */
181
+ .corner {
182
+ position: absolute; width: 80px; height: 80px;
183
+ border: 1.5px solid rgba(236,238,243,0.28);
184
+ pointer-events: none; z-index: 5;
185
+ }
186
+ .corner.tl { top: 60px; left: 60px; border-right: 0; border-bottom: 0; }
187
+ .corner.tr { top: 60px; right: 60px; border-left: 0; border-bottom: 0; }
188
+ .corner.bl { bottom: 60px; left: 60px; border-right: 0; border-top: 0; }
189
+ .corner.br { bottom: 60px; right: 60px; border-left: 0; border-top: 0; }
190
+
191
+ .controls { border-left: 1px solid var(--border); padding: 20px 16px; background: #0A0C13; overflow-y: auto; max-height: calc(100vh - 60px); }
192
+ .ctrl-group { margin-bottom: 18px; }
193
+ .ctrl-group h3 {
194
+ font-size: 10px; letter-spacing: 2.5px; color: var(--muted);
195
+ margin: 0 0 10px; text-transform: uppercase;
196
+ font-family: "JetBrains Mono", monospace;
197
+ display: flex; align-items: center; gap: 8px;
198
+ }
199
+ .ctrl-group h3::after { content: ''; flex: 1; height: 1px; background: var(--border); }
200
+ .row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; gap: 8px; }
201
+ .row label { font-size: 12px; color: var(--text); min-width: 60px; flex: 0 0 auto; }
202
+ .row .val { font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--amber); min-width: 44px; text-align: right; }
203
+ .row input[type="range"] { flex: 1; accent-color: var(--amber); height: 3px; }
204
+ .row input[type="text"], .row select {
205
+ flex: 1; background: var(--panel); color: var(--text);
206
+ border: 1px solid var(--border); border-radius: 4px;
207
+ padding: 6px 8px; font-family: inherit; font-size: 12px;
208
+ }
209
+ .row input[type="text"]:focus, .row select:focus { outline: none; border-color: var(--amber); }
210
+ .row input[type="checkbox"] { accent-color: var(--amber); }
211
+
212
+ /* 通用 layer block(引擎层,勿改) */
213
+ .layer-block {
214
+ border: 1px solid var(--border);
215
+ border-radius: 6px;
216
+ padding: 9px 11px;
217
+ margin-bottom: 7px;
218
+ background: var(--panel);
219
+ cursor: pointer;
220
+ transition: all 0.15s;
221
+ }
222
+ .layer-block:hover { border-color: rgba(122,134,158,0.55); }
223
+ .layer-block.selected { border-color: var(--amber); background: var(--panel-2); box-shadow: 0 0 0 1px rgba(122,134,158,0.35); }
224
+ .layer-block .head {
225
+ display: flex; align-items: center; justify-content: space-between;
226
+ margin-bottom: 7px; gap: 6px;
227
+ }
228
+ .layer-block .char-preview {
229
+ font-family: "Noto Sans SC", sans-serif;
230
+ font-weight: 900; font-size: 22px;
231
+ color: var(--bone);
232
+ width: 30px; height: 30px;
233
+ display: inline-flex; align-items: center; justify-content: center;
234
+ background: var(--midnight); border-radius: 4px;
235
+ flex-shrink: 0;
236
+ }
237
+ .layer-block .tag-preview {
238
+ font-family: "JetBrains Mono", monospace;
239
+ font-size: 10px; color: var(--amber);
240
+ padding: 6px 10px;
241
+ background: var(--midnight); border-radius: 4px;
242
+ letter-spacing: 1px; flex-shrink: 0;
243
+ }
244
+ .layer-block .pos-tag {
245
+ font-family: "JetBrains Mono", monospace;
246
+ font-size: 10px; color: var(--muted);
247
+ letter-spacing: 1px;
248
+ }
249
+ .layer-block .vis-toggle {
250
+ font-family: "JetBrains Mono", monospace;
251
+ font-size: 10px; color: var(--muted);
252
+ display: flex; align-items: center; gap: 4px;
253
+ }
254
+ .layer-block .mini-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
255
+ .layer-block .mini-row:last-child { margin-bottom: 0; }
256
+ .layer-block .mini-row label { font-size: 10px; color: var(--muted); min-width: 14px; font-family: "JetBrains Mono", monospace; }
257
+ .layer-block .mini-row input[type="number"] {
258
+ width: 66px;
259
+ background: var(--ink); color: var(--amber);
260
+ border: 1px solid var(--border); border-radius: 3px;
261
+ padding: 4px 6px;
262
+ font-family: "JetBrains Mono", monospace; font-size: 11px;
263
+ }
264
+ .layer-block .mini-row input[type="number"]:focus { outline: none; border-color: var(--amber); }
265
+ .layer-block .mini-row input[type="text"] {
266
+ flex: 1; background: var(--ink); color: var(--text);
267
+ border: 1px solid var(--border); border-radius: 3px;
268
+ padding: 4px 6px;
269
+ font-family: inherit; font-size: 11px;
270
+ }
271
+ .layer-block .mini-row input[type="text"]:focus { outline: none; border-color: var(--amber); }
272
+
273
+ .export-btn {
274
+ display: block; width: 100%;
275
+ padding: 13px; margin-top: 6px;
276
+ background: var(--amber); color: #fff;
277
+ border: none; border-radius: 6px;
278
+ font-family: inherit; font-weight: 700; font-size: 13px;
279
+ cursor: pointer; letter-spacing: 2px; transition: all 0.15s;
280
+ }
281
+ .export-btn:hover { background: var(--amber-soft); transform: translateY(-1px); }
282
+ .export-btn:disabled { background: var(--border); color: var(--muted); cursor: wait; transform: none; }
283
+
284
+ .notes { font-size: 10px; color: var(--muted); line-height: 1.75; padding: 10px; background: var(--panel); border-radius: 4px; border-left: 2px solid var(--amber); }
285
+ .notes strong { color: var(--amber-soft); }
286
+ .notes kbd { background: var(--ink); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px; font-size: 10px; color: var(--amber-soft); font-family: "JetBrains Mono", monospace; }
287
+
288
+ @media (max-width: 1200px) {
289
+ main { grid-template-columns: 200px 1fr 300px; }
290
+ }
291
+ @media (max-width: 900px) {
292
+ main { grid-template-columns: 1fr; }
293
+ .tabs, .controls { border: none; max-height: none; }
294
+ .tabs { display: flex; overflow-x: auto; padding: 12px; }
295
+ .tab { flex-shrink: 0; width: 160px; margin-right: 6px; margin-bottom: 0; }
296
+ }
297
+ </style>
298
+ </head>
299
+ <body>
300
+
301
+ <header class="app-head">
302
+ <div>
303
+ <!-- 替换位:当期 T1 与英文副题 -->
304
+ <h1>示例 <span class="ink">·</span> 标题<span class="sub">TEMPLATE — 封面排版工作台</span></h1>
305
+ </div>
306
+ <div class="meta">
307
+ <!-- 替换位:布局名 / 色板名 -->
308
+ <span class="pill">布局待定</span>
309
+ <span class="pill">色板待注入</span>
310
+ <span class="pill">拖拽 / 滚轮 / 一键导出</span>
311
+ </div>
312
+ </header>
313
+
314
+ <main>
315
+ <aside class="tabs">
316
+ <p class="tab-title">// 尺寸</p>
317
+ <button class="tab active" data-size="16_9">
318
+ <div class="ratio">16 : 9</div>
319
+ <div class="desc">横版 · 布局按当期标注</div>
320
+ </button>
321
+ <button class="tab" data-size="4_3">
322
+ <div class="ratio">4 : 3</div>
323
+ <div class="desc">横版饱满 · 布局按当期标注</div>
324
+ </button>
325
+ <button class="tab" data-size="3_4">
326
+ <div class="ratio">3 : 4</div>
327
+ <div class="desc">竖版 · 布局按当期标注</div>
328
+ </button>
329
+
330
+ <div class="divider"></div>
331
+ <p class="tab-title">// 替换背景</p>
332
+ <label class="upload-btn">
333
+ 上传当前尺寸的背景图
334
+ <input type="file" id="bg-upload" accept="image/*">
335
+ </label>
336
+ <p class="hint">
337
+ 背景图已默认载入三张。抽到新版本想替换,在此上传只影响当前激活尺寸。
338
+ </p>
339
+
340
+ <div class="divider"></div>
341
+ <button class="reset-btn" id="resetAllBtn">↺ 恢复当前尺寸默认布局</button>
342
+ </aside>
343
+
344
+ <section class="stage" id="stage">
345
+ <div class="canvas-wrap" id="canvasWrap">
346
+ <div class="dim-tag"><span id="dimLabel">2400 × 1350</span> <span class="num">— 16:9</span></div>
347
+
348
+ <!-- 替换位:char* div 数量与 T1_KEYS 一致(当期 T1 字数驱动),initial 逐字填 -->
349
+ <div class="cover active" data-size="16_9">
350
+ <img class="bg" src="__BG_16_9__" alt="">
351
+ <div class="inner-frame"></div>
352
+ <div class="corner tl"></div><div class="corner tr"></div>
353
+ <div class="corner bl"></div><div class="corner br"></div>
354
+ <div class="layer-text t1" data-layer-key="char1">示</div>
355
+ <div class="layer-text t1" data-layer-key="char2">例</div>
356
+ <div class="layer-text t1" data-layer-key="char3">标</div>
357
+ <div class="layer-text t1" data-layer-key="char4">题</div>
358
+ <div class="layer-text t2" data-layer-key="t2">副标题钩子示例</div>
359
+ <div class="layer-text t5" data-layer-key="t5">SUBTITLE</div>
360
+ <div class="layer-text t4" data-layer-key="t4">品牌戳未提供</div>
361
+ </div>
362
+
363
+ <div class="cover" data-size="4_3">
364
+ <img class="bg" src="__BG_4_3__" alt="">
365
+ <div class="inner-frame"></div>
366
+ <div class="corner tl"></div><div class="corner tr"></div>
367
+ <div class="corner bl"></div><div class="corner br"></div>
368
+ <div class="layer-text t1" data-layer-key="char1">示</div>
369
+ <div class="layer-text t1" data-layer-key="char2">例</div>
370
+ <div class="layer-text t1" data-layer-key="char3">标</div>
371
+ <div class="layer-text t1" data-layer-key="char4">题</div>
372
+ <div class="layer-text t2" data-layer-key="t2">副标题钩子示例</div>
373
+ <div class="layer-text t5" data-layer-key="t5">SUBTITLE</div>
374
+ <div class="layer-text t4" data-layer-key="t4">品牌戳未提供</div>
375
+ </div>
376
+
377
+ <div class="cover" data-size="3_4">
378
+ <img class="bg" src="__BG_3_4__" alt="">
379
+ <div class="inner-frame"></div>
380
+ <div class="corner tl"></div><div class="corner tr"></div>
381
+ <div class="corner bl"></div><div class="corner br"></div>
382
+ <div class="layer-text t1" data-layer-key="char1">示</div>
383
+ <div class="layer-text t1" data-layer-key="char2">例</div>
384
+ <div class="layer-text t1" data-layer-key="char3">标</div>
385
+ <div class="layer-text t1" data-layer-key="char4">题</div>
386
+ <div class="layer-text t2" data-layer-key="t2">副标题钩子示例</div>
387
+ <div class="layer-text t5" data-layer-key="t5">SUBTITLE</div>
388
+ <div class="layer-text t4" data-layer-key="t4">品牌戳未提供</div>
389
+ </div>
390
+ </div>
391
+ </section>
392
+
393
+ <aside class="controls">
394
+ <div class="ctrl-group">
395
+ <h3>T1 文案</h3>
396
+ <div class="row">
397
+ <!-- 替换位:value 与 maxlength 按当期 T1;与 state.t1Text 保持一致 -->
398
+ <input type="text" id="t1-text" value="示例标题" maxlength="4"
399
+ style="font-family:'Noto Sans SC',sans-serif; font-weight:700; letter-spacing:0.3em; font-size:14px;">
400
+ </div>
401
+ <p class="notes" style="margin-top:6px;">
402
+ 改这里 <strong>同步到全部三张</strong>;每字独立位置/字号在下方调。
403
+ </p>
404
+ </div>
405
+
406
+ <div class="ctrl-group">
407
+ <h3>T1 逐字微调 <span style="font-size:9px; color:var(--amber); margin-left:4px;">画布上可拖</span></h3>
408
+ <div id="char-blocks"></div>
409
+ </div>
410
+
411
+ <div class="ctrl-group">
412
+ <h3>T1 全局样式</h3>
413
+ <div class="row">
414
+ <label>描边粗细</label>
415
+ <input type="range" id="t1-stroke" min="0" max="8" step="0.5" value="2">
416
+ <span class="val" id="t1-stroke-val">2</span>
417
+ </div>
418
+ <div class="row">
419
+ <!-- 替换位:发光名与默认值跟色板叫(银辉/琥珀/…) -->
420
+ <label>外发光</label>
421
+ <input type="range" id="t1-glow" min="0" max="100" step="5" value="20">
422
+ <span class="val" id="t1-glow-val">20%</span>
423
+ </div>
424
+ <div class="row">
425
+ <label>字色</label>
426
+ <!-- 替换位:字色选项按栏目色板给 2~3 档 -->
427
+ <select id="t1-color">
428
+ <option value="#FFFFFF" selected>纯白 #FFFFFF</option>
429
+ <option value="#ECEEF3">浅灰白 #ECEEF3</option>
430
+ <option value="#7A869E">强调色 #7A869E</option>
431
+ </select>
432
+ </div>
433
+ <div class="row">
434
+ <label>统一字号</label>
435
+ <input type="range" id="t1-size-all" min="280" max="800" step="10" value="520">
436
+ <span class="val" id="t1-size-all-val">520</span>
437
+ </div>
438
+ </div>
439
+
440
+ <div class="ctrl-group">
441
+ <h3>附加图层 <span style="font-size:9px; color:var(--amber); margin-left:4px;">同样可拖</span></h3>
442
+ <div id="sub-blocks"></div>
443
+ </div>
444
+
445
+ <div class="ctrl-group">
446
+ <h3>装饰件</h3>
447
+ <!-- 替换位:默认开关按当期装饰件决策;不用的装饰件保持关闭即可 -->
448
+ <div class="row">
449
+ <label>薄内框</label>
450
+ <input type="checkbox" id="inner-show">
451
+ </div>
452
+ <div class="row">
453
+ <label>四角断线框</label>
454
+ <input type="checkbox" id="corner-show">
455
+ </div>
456
+ </div>
457
+
458
+ <div class="ctrl-group">
459
+ <h3>导出</h3>
460
+ <button class="export-btn" id="exportBtn">导出当前封面 PNG</button>
461
+ <button class="export-btn" id="exportAllBtn" style="margin-top:8px; background: transparent; color: var(--amber); border: 1.5px solid var(--amber);">一键导出全部三张</button>
462
+ <p class="notes" style="margin-top: 10px;">
463
+ <strong>导出规格:</strong>原分辨率 PNG<br>
464
+ 16:9 → 2400 × 1350<br>
465
+ 4 : 3 → 2400 × 1800<br>
466
+ 3 : 4 → 1500 × 2000<br><br>
467
+ <strong>操作提示:</strong><br>
468
+ · 画布上 <kbd>拖任何字</kbd> 直接改位置<br>
469
+ · 点字选中后 <kbd>↑↓←→</kbd> 精调 1px<br>
470
+ · 按住 <kbd>Shift</kbd> + 方向键走 10px<br>
471
+ · 数值框选中后 <kbd>滚轮</kbd> 微调,<kbd>Shift</kbd>+滚轮 ×10<br>
472
+ · 一键导出首次会请求「允许下载多个文件」,点允许即可<br>
473
+ · 布局状态在内存里,调好就导出、导出前别刷新<br>
474
+ </p>
475
+ </div>
476
+ </aside>
477
+ </main>
478
+
479
+ <script>
480
+ // ===== 替换位:T1 字数驱动(当期 T1 为 N 字 → T1_KEYS 列 N 项 + 各 cover 放 N 个 char div + maxlength=N + DEFAULTS 各含 N 条)=====
481
+ const T1_KEYS = ['char1', 'char2', 'char3', 'char4'];
482
+ const T1_LEN = T1_KEYS.length;
483
+
484
+ // ===== 替换位:默认布局配方——从 layout-recipes 选配方起步,按实际画面气口微调;注意分发平台安全区 =====
485
+ const DEFAULTS = {
486
+ '16_9': {
487
+ canvas: { w: 2400, h: 1350 },
488
+ layers: {
489
+ char1: { type: 't1', pos: '左上', left: 140, top: 200, size: 520 },
490
+ char2: { type: 't1', pos: '左下', left: 260, top: 720, size: 520 },
491
+ char3: { type: 't1', pos: '右上', left: 1740, top: 200, size: 520 },
492
+ char4: { type: 't1', pos: '右下', left: 1620, top: 720, size: 520 },
493
+ t2: { type: 't2', pos: '底部居中', text: '副标题钩子示例', left: 910, top: 1130, size: 46, visible: true },
494
+ t5: { type: 't5', pos: '顶部居中', text: 'SUBTITLE', left: 1020, top: 80, size: 34, visible: true },
495
+ t4: { type: 't4', pos: '左下角', text: '品牌戳未提供', left: 70, top: 1156, size: 44, visible: false },
496
+ },
497
+ },
498
+ '4_3': {
499
+ canvas: { w: 2400, h: 1800 },
500
+ layers: {
501
+ char1: { type: 't1', pos: '左上', left: 110, top: 150, size: 560 },
502
+ char2: { type: 't1', pos: '左中', left: 300, top: 700, size: 560 },
503
+ char3: { type: 't1', pos: '右中', left: 1540, top: 540, size: 560 },
504
+ char4: { type: 't1', pos: '右下', left: 1730, top: 1090, size: 560 },
505
+ t2: { type: 't2', pos: '右下角', text: '副标题钩子示例', left: 1680, top: 1680, size: 44, visible: true },
506
+ t5: { type: 't5', pos: '左上角', text: 'SUBTITLE', left: 130, top: 75, size: 36, visible: true },
507
+ t4: { type: 't4', pos: '左下角', text: '品牌戳未提供', left: 150, top: 1700, size: 40, visible: false },
508
+ },
509
+ },
510
+ '3_4': {
511
+ canvas: { w: 1500, h: 2000 },
512
+ layers: {
513
+ char1: { type: 't1', pos: '左上', left: 180, top: 140, size: 440 },
514
+ char2: { type: 't1', pos: '右上', left: 880, top: 140, size: 440 },
515
+ char3: { type: 't1', pos: '左下', left: 180, top: 1380, size: 440 },
516
+ char4: { type: 't1', pos: '右下', left: 880, top: 1380, size: 440 },
517
+ t2: { type: 't2', pos: '中段留白', text: '副标题钩子示例', left: 475, top: 620, size: 44, visible: true },
518
+ t5: { type: 't5', pos: '底部居中', text: 'SUBTITLE', left: 550, top: 1880, size: 34, visible: true },
519
+ t4: { type: 't4', pos: '左上角', text: '品牌戳未提供', left: 100, top: 40, size: 38, visible: false },
520
+ },
521
+ },
522
+ };
523
+
524
+ // 替换位:附加图层组(加图层四步见 workbench-guide §4)
525
+ const SUB_LABELS = { t2: 'T2 副标题', t5: 'T5 英文', t4: 'T4 品牌戳' };
526
+ const SUB_KEYS = ['t2', 't5', 't4'];
527
+
528
+ const state = {
529
+ activeSize: '16_9',
530
+ t1Text: '示例标题', // 替换位:与 #t1-text 的 value 一致
531
+ selectedLayerKey: null,
532
+ data: JSON.parse(JSON.stringify(DEFAULTS)),
533
+ currentScale: 1,
534
+ };
535
+
536
+ function fitStage() {
537
+ const stage = document.getElementById('stage');
538
+ const wrap = document.getElementById('canvasWrap');
539
+ const size = DEFAULTS[state.activeSize].canvas;
540
+ const headH = document.querySelector('header.app-head').offsetHeight;
541
+ const availW = stage.clientWidth - 64;
542
+ const availH = window.innerHeight - headH - 120;
543
+ const scale = Math.min(availW / size.w, availH / size.h, 1);
544
+ state.currentScale = scale;
545
+ wrap.style.transform = `scale(${scale})`;
546
+ wrap.style.width = size.w + 'px';
547
+ wrap.style.height = size.h + 'px';
548
+ // 负 margin 把布局占位收缩到缩放后尺寸(transform 只缩视觉不缩布局,占位不缩会把页面撑出滚动条)
549
+ wrap.style.marginRight = -(size.w * (1 - scale)) + 'px';
550
+ wrap.style.marginBottom = -(size.h * (1 - scale)) + 'px';
551
+ document.getElementById('dimLabel').textContent = `${size.w} × ${size.h}`;
552
+ document.querySelector('.dim-tag .num').textContent = `— ${state.activeSize.replace('_', ':')}`;
553
+ }
554
+ window.addEventListener('resize', fitStage);
555
+
556
+ // ===== 应用图层位置和字号 =====
557
+ function applyLayers() {
558
+ const cover = document.querySelector('.cover.active');
559
+ const layers = state.data[state.activeSize].layers;
560
+ Object.entries(layers).forEach(([key, ls]) => {
561
+ const el = cover.querySelector(`.layer-text[data-layer-key="${key}"]`);
562
+ if (!el) return;
563
+ el.style.left = ls.left + 'px';
564
+ el.style.top = ls.top + 'px';
565
+ el.style.fontSize = ls.size + 'px';
566
+ // 非 T1 图层的 text 和 visible(对任意附加图层通用)
567
+ if (ls.type !== 't1') {
568
+ document.querySelectorAll(`.cover .layer-text[data-layer-key="${key}"]`).forEach(n => {
569
+ n.textContent = ls.text;
570
+ n.style.display = ls.visible ? 'block' : 'none';
571
+ });
572
+ }
573
+ });
574
+ }
575
+
576
+ function applyT1Text() {
577
+ const raw = document.getElementById('t1-text').value;
578
+ const text = (raw + ' '.repeat(T1_LEN)).slice(0, T1_LEN);
579
+ state.t1Text = text;
580
+ document.querySelectorAll('.cover').forEach(cover => {
581
+ T1_KEYS.forEach((k, i) => {
582
+ const el = cover.querySelector(`.layer-text[data-layer-key="${k}"]`);
583
+ if (el) el.textContent = text.charAt(i);
584
+ });
585
+ });
586
+ document.querySelectorAll('.char-block .char-preview').forEach((el, i) => {
587
+ el.textContent = text.charAt(i);
588
+ });
589
+ }
590
+
591
+ function applyT1Style() {
592
+ const stroke = document.getElementById('t1-stroke').value;
593
+ const glow = document.getElementById('t1-glow').value / 100;
594
+ const color = document.getElementById('t1-color').value;
595
+ document.getElementById('t1-stroke-val').textContent = stroke;
596
+ document.getElementById('t1-glow-val').textContent = (glow * 100 | 0) + '%';
597
+ document.querySelectorAll('.t1').forEach(el => {
598
+ el.style.webkitTextStroke = `${stroke}px var(--midnight)`;
599
+ el.style.color = color;
600
+ // 替换位:发光 rgba 与 CSS .t1 的 text-shadow 保持同色
601
+ el.style.textShadow = `
602
+ 0 0 1px var(--midnight),
603
+ 0 0 2px var(--midnight),
604
+ 0 0 3px var(--midnight),
605
+ 0 0 20px rgba(200,210,230,${glow * 0.7}),
606
+ 0 0 45px rgba(200,210,230,${glow * 0.5}),
607
+ 0 8px 24px rgba(0,0,0,0.55)
608
+ `;
609
+ });
610
+ }
611
+
612
+ function applyT1SizeAll() {
613
+ const size = parseInt(document.getElementById('t1-size-all').value);
614
+ document.getElementById('t1-size-all-val').textContent = size;
615
+ T1_KEYS.forEach(k => {
616
+ state.data[state.activeSize].layers[k].size = size;
617
+ });
618
+ applyLayers();
619
+ renderCharBlocks();
620
+ }
621
+
622
+ function applyCorner() {
623
+ const show = document.getElementById('corner-show').checked;
624
+ document.querySelectorAll('.cover.active .corner').forEach(c => {
625
+ c.style.display = show ? 'block' : 'none';
626
+ });
627
+ }
628
+
629
+ function applyInner() {
630
+ const show = document.getElementById('inner-show').checked;
631
+ document.querySelectorAll('.cover.active .inner-frame').forEach(c => {
632
+ c.style.display = show ? 'block' : 'none';
633
+ });
634
+ }
635
+
636
+ // ===== 右栏 T1 逐字 block =====
637
+ function renderCharBlocks() {
638
+ const container = document.getElementById('char-blocks');
639
+ const layers = state.data[state.activeSize].layers;
640
+ container.innerHTML = '';
641
+ T1_KEYS.forEach((k, i) => {
642
+ const ls = layers[k];
643
+ const block = document.createElement('div');
644
+ block.className = 'layer-block';
645
+ block.dataset.layerKey = k;
646
+ if (state.selectedLayerKey === k) block.classList.add('selected');
647
+ block.innerHTML = `
648
+ <div class="head">
649
+ <span class="char-preview">${state.t1Text.charAt(i)}</span>
650
+ <span class="pos-tag">${ls.pos}</span>
651
+ </div>
652
+ <div class="mini-row">
653
+ <label>X</label>
654
+ <input type="number" data-field="left" value="${Math.round(ls.left)}" step="5">
655
+ <label>Y</label>
656
+ <input type="number" data-field="top" value="${Math.round(ls.top)}" step="5">
657
+ </div>
658
+ <div class="mini-row">
659
+ <label>号</label>
660
+ <input type="number" data-field="size" value="${ls.size}" step="10" min="100" max="900">
661
+ </div>
662
+ `;
663
+ bindLayerBlock(block, ls, k);
664
+ container.appendChild(block);
665
+ });
666
+ }
667
+
668
+ // ===== 右栏附加图层 block =====
669
+ function renderSubBlocks() {
670
+ const container = document.getElementById('sub-blocks');
671
+ const layers = state.data[state.activeSize].layers;
672
+ container.innerHTML = '';
673
+ SUB_KEYS.forEach(k => {
674
+ const ls = layers[k];
675
+ const block = document.createElement('div');
676
+ block.className = 'layer-block';
677
+ block.dataset.layerKey = k;
678
+ if (state.selectedLayerKey === k) block.classList.add('selected');
679
+ const labelText = SUB_LABELS[k];
680
+ block.innerHTML = `
681
+ <div class="head">
682
+ <span class="tag-preview">${labelText}</span>
683
+ <span class="pos-tag">${ls.pos}</span>
684
+ <label class="vis-toggle" title="显示 / 隐藏">
685
+ <input type="checkbox" data-field="visible" ${ls.visible ? 'checked' : ''}> 显示
686
+ </label>
687
+ </div>
688
+ <div class="mini-row">
689
+ <input type="text" data-field="text" value="${escapeHtml(ls.text)}">
690
+ </div>
691
+ <div class="mini-row">
692
+ <label>X</label>
693
+ <input type="number" data-field="left" value="${Math.round(ls.left)}" step="5">
694
+ <label>Y</label>
695
+ <input type="number" data-field="top" value="${Math.round(ls.top)}" step="5">
696
+ </div>
697
+ <div class="mini-row">
698
+ <label>号</label>
699
+ <input type="number" data-field="size" value="${ls.size}" step="2" min="14" max="200">
700
+ </div>
701
+ `;
702
+ bindLayerBlock(block, ls, k);
703
+ container.appendChild(block);
704
+ });
705
+ }
706
+
707
+ function bindLayerBlock(block, ls, key) {
708
+ block.addEventListener('click', (e) => {
709
+ if (e.target.tagName === 'INPUT') return;
710
+ selectLayer(key);
711
+ });
712
+ block.querySelectorAll('input').forEach(input => {
713
+ input.addEventListener('click', (e) => e.stopPropagation());
714
+ input.addEventListener('focus', () => selectLayer(key));
715
+ if (input.type === 'number') {
716
+ input.addEventListener('wheel', (e) => {
717
+ if (document.activeElement !== input) return;
718
+ e.preventDefault();
719
+ const step = (parseFloat(input.step) || 1) * (e.shiftKey ? 10 : 1);
720
+ const dir = e.deltaY < 0 ? 1 : -1;
721
+ input.value = String((parseFloat(input.value) || 0) + dir * step);
722
+ input.dispatchEvent(new Event('input'));
723
+ }, { passive: false });
724
+ }
725
+ input.addEventListener('input', (e) => {
726
+ const field = e.target.dataset.field;
727
+ if (field === 'visible') {
728
+ ls.visible = e.target.checked;
729
+ } else if (field === 'text') {
730
+ ls.text = e.target.value;
731
+ } else {
732
+ const val = parseFloat(e.target.value);
733
+ if (!isNaN(val)) ls[field] = val;
734
+ }
735
+ applyLayers();
736
+ });
737
+ });
738
+ }
739
+
740
+ function escapeHtml(s) {
741
+ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
742
+ }
743
+
744
+ function selectLayer(key) {
745
+ state.selectedLayerKey = key;
746
+ document.querySelectorAll('.layer-text').forEach(el => el.classList.remove('selected'));
747
+ document.querySelectorAll('.layer-block').forEach(el => el.classList.remove('selected'));
748
+ if (key) {
749
+ const el = document.querySelector(`.cover.active .layer-text[data-layer-key="${key}"]`);
750
+ if (el) el.classList.add('selected');
751
+ const block = document.querySelector(`.layer-block[data-layer-key="${key}"]`);
752
+ if (block) block.classList.add('selected');
753
+ }
754
+ }
755
+
756
+ // ===== 拖拽(引擎层,勿改) =====
757
+ let dragState = null;
758
+ document.addEventListener('mousedown', (e) => {
759
+ const layer = e.target.closest('.layer-text');
760
+ if (!layer || !layer.closest('.cover.active')) return;
761
+ const key = layer.dataset.layerKey;
762
+ selectLayer(key);
763
+ const ls = state.data[state.activeSize].layers[key];
764
+ dragState = {
765
+ key, startX: e.clientX, startY: e.clientY,
766
+ startLeft: ls.left, startTop: ls.top,
767
+ };
768
+ layer.classList.add('dragging');
769
+ e.preventDefault();
770
+ });
771
+ document.addEventListener('mousemove', (e) => {
772
+ if (!dragState) return;
773
+ const dx = (e.clientX - dragState.startX) / state.currentScale;
774
+ const dy = (e.clientY - dragState.startY) / state.currentScale;
775
+ const ls = state.data[state.activeSize].layers[dragState.key];
776
+ ls.left = dragState.startLeft + dx;
777
+ ls.top = dragState.startTop + dy;
778
+ applyLayers();
779
+ const block = document.querySelector(`.layer-block[data-layer-key="${dragState.key}"]`);
780
+ if (block) {
781
+ const lInput = block.querySelector('input[data-field="left"]');
782
+ const tInput = block.querySelector('input[data-field="top"]');
783
+ if (lInput) lInput.value = Math.round(ls.left);
784
+ if (tInput) tInput.value = Math.round(ls.top);
785
+ }
786
+ });
787
+ document.addEventListener('mouseup', () => {
788
+ if (dragState) {
789
+ document.querySelectorAll('.layer-text.dragging').forEach(el => el.classList.remove('dragging'));
790
+ dragState = null;
791
+ }
792
+ });
793
+
794
+ // ===== 方向键(引擎层,勿改) =====
795
+ document.addEventListener('keydown', (e) => {
796
+ if (!state.selectedLayerKey) return;
797
+ const tag = document.activeElement?.tagName;
798
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
799
+ const step = e.shiftKey ? 10 : 1;
800
+ const ls = state.data[state.activeSize].layers[state.selectedLayerKey];
801
+ if (!ls) return;
802
+ let handled = true;
803
+ if (e.key === 'ArrowLeft') ls.left -= step;
804
+ else if (e.key === 'ArrowRight') ls.left += step;
805
+ else if (e.key === 'ArrowUp') ls.top -= step;
806
+ else if (e.key === 'ArrowDown') ls.top += step;
807
+ else handled = false;
808
+ if (handled) {
809
+ e.preventDefault();
810
+ applyLayers();
811
+ renderCharBlocks();
812
+ renderSubBlocks();
813
+ selectLayer(state.selectedLayerKey);
814
+ }
815
+ });
816
+
817
+ // ===== tab 切换 =====
818
+ document.querySelectorAll('.tab').forEach(tab => {
819
+ tab.addEventListener('click', () => {
820
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
821
+ document.querySelectorAll('.cover').forEach(c => c.classList.remove('active'));
822
+ tab.classList.add('active');
823
+ state.activeSize = tab.dataset.size;
824
+ document.querySelector(`.cover[data-size="${state.activeSize}"]`).classList.add('active');
825
+ state.selectedLayerKey = null;
826
+ const defSize = DEFAULTS[state.activeSize].layers.char1.size;
827
+ document.getElementById('t1-size-all').value = defSize;
828
+ document.getElementById('t1-size-all-val').textContent = defSize;
829
+ fitStage();
830
+ applyLayers();
831
+ applyT1Style();
832
+ applyCorner();
833
+ applyInner();
834
+ renderCharBlocks();
835
+ renderSubBlocks();
836
+ });
837
+ });
838
+
839
+ document.getElementById('t1-text').addEventListener('input', applyT1Text);
840
+ ['t1-stroke', 't1-glow', 't1-color'].forEach(id =>
841
+ document.getElementById(id).addEventListener('input', applyT1Style));
842
+ document.getElementById('t1-size-all').addEventListener('input', applyT1SizeAll);
843
+ document.getElementById('corner-show').addEventListener('input', applyCorner);
844
+ document.getElementById('inner-show').addEventListener('input', applyInner);
845
+
846
+ document.getElementById('resetAllBtn').addEventListener('click', () => {
847
+ state.data[state.activeSize] = JSON.parse(JSON.stringify(DEFAULTS[state.activeSize]));
848
+ state.selectedLayerKey = null;
849
+ applyLayers();
850
+ renderCharBlocks();
851
+ renderSubBlocks();
852
+ });
853
+
854
+ document.getElementById('bg-upload').addEventListener('change', e => {
855
+ const file = e.target.files[0];
856
+ if (!file) return;
857
+ const reader = new FileReader();
858
+ reader.onload = ev => {
859
+ document.querySelector('.cover.active .bg').src = ev.target.result;
860
+ };
861
+ reader.readAsDataURL(file);
862
+ });
863
+
864
+ // ===== 导出(引擎层,勿改) =====
865
+ document.getElementById('exportBtn').addEventListener('click', async () => {
866
+ const btn = document.getElementById('exportBtn');
867
+ btn.disabled = true;
868
+ btn.textContent = '正在渲染…';
869
+ const cover = document.querySelector('.cover.active');
870
+ const wrap = document.getElementById('canvasWrap');
871
+ const originalTransform = wrap.style.transform;
872
+ wrap.style.transform = 'scale(1)';
873
+ document.querySelectorAll('.layer-text').forEach(el => el.classList.remove('selected', 'dragging'));
874
+ try {
875
+ const canvas = await html2canvas(cover, {
876
+ backgroundColor: null, scale: 1, useCORS: true, logging: false,
877
+ width: cover.offsetWidth, height: cover.offsetHeight,
878
+ });
879
+ const link = document.createElement('a');
880
+ const txt = state.t1Text.replace(/ /g, '').trim() || '封面';
881
+ link.download = `${txt}_${state.activeSize.replace('_', 'x')}.png`;
882
+ link.href = canvas.toDataURL('image/png');
883
+ link.click();
884
+ btn.textContent = '✓ 导出成功';
885
+ setTimeout(() => { btn.textContent = '导出当前封面 PNG'; btn.disabled = false; }, 1500);
886
+ } catch (err) {
887
+ console.error(err);
888
+ btn.textContent = '导出失败,看控制台';
889
+ btn.disabled = false;
890
+ } finally {
891
+ wrap.style.transform = originalTransform;
892
+ if (state.selectedLayerKey) selectLayer(state.selectedLayerKey);
893
+ }
894
+ });
895
+
896
+ // ===== 一键导出全部三张(引擎层,勿改) =====
897
+ document.getElementById('exportAllBtn').addEventListener('click', async () => {
898
+ const btn = document.getElementById('exportAllBtn');
899
+ const originalSize = state.activeSize;
900
+ btn.disabled = true;
901
+ const wrap = document.getElementById('canvasWrap');
902
+ const originalTransform = wrap.style.transform;
903
+ wrap.style.transform = 'scale(1)';
904
+ document.querySelectorAll('.layer-text').forEach(el => el.classList.remove('selected', 'dragging'));
905
+ try {
906
+ for (const size of ['16_9', '4_3', '3_4']) {
907
+ btn.textContent = `正在渲染 ${size.replace('_', ':')} …`;
908
+ document.querySelectorAll('.cover').forEach(c => c.classList.remove('active'));
909
+ const cover = document.querySelector(`.cover[data-size="${size}"]`);
910
+ cover.classList.add('active');
911
+ state.activeSize = size;
912
+ applyLayers();
913
+ applyCorner();
914
+ applyInner();
915
+ await new Promise(r => setTimeout(r, 80));
916
+ const canvas = await html2canvas(cover, {
917
+ backgroundColor: null, scale: 1, useCORS: true, logging: false,
918
+ width: cover.offsetWidth, height: cover.offsetHeight,
919
+ });
920
+ const link = document.createElement('a');
921
+ const txt = state.t1Text.replace(/ /g, '').trim() || '封面';
922
+ link.download = `${txt}_${size.replace('_', 'x')}.png`;
923
+ link.href = canvas.toDataURL('image/png');
924
+ link.click();
925
+ await new Promise(r => setTimeout(r, 400));
926
+ }
927
+ btn.textContent = '✓ 三张已导出';
928
+ } catch (err) {
929
+ console.error(err);
930
+ btn.textContent = '导出失败,看控制台';
931
+ } finally {
932
+ wrap.style.transform = originalTransform;
933
+ const backTab = document.querySelector(`.tab[data-size="${originalSize}"]`);
934
+ if (backTab) backTab.click();
935
+ setTimeout(() => { btn.textContent = '一键导出全部三张'; btn.disabled = false; }, 1800);
936
+ }
937
+ });
938
+
939
+ function init() {
940
+ fitStage();
941
+ applyT1Text();
942
+ applyLayers();
943
+ applyT1Style();
944
+ applyCorner();
945
+ applyInner();
946
+ renderCharBlocks();
947
+ renderSubBlocks();
948
+ }
949
+ document.fonts.ready.then(init);
950
+ setTimeout(fitStage, 100);
951
+ setTimeout(fitStage, 600);
952
+ </script>
953
+
954
+ </body>
955
+ </html>