@akairos/typeset-studio 0.2.2 → 0.2.4

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 (59) hide show
  1. package/README.md +90 -93
  2. package/dist/assets/index-3Ko1UCBU.css +1 -0
  3. package/dist/assets/index-C9J0f4Lf.js +317 -0
  4. package/dist/index.html +2 -2
  5. package/dist/lib/adapter.js +6 -7582
  6. package/dist/lib/canvas-renderer-cxnar75n.js +23 -0
  7. package/dist/lib/font-loader-j28mt43t.js +11 -0
  8. package/dist/lib/index-37x76zdn.js +4 -0
  9. package/dist/lib/index-6c43c7h4.js +3880 -0
  10. package/dist/lib/index-qhpfzwj4.js +303 -0
  11. package/dist/lib/index-t9w3p2p9.js +3366 -0
  12. package/dist/lib/index-zdhqg5f0.js +459 -0
  13. package/dist/lib/index.js +26 -7582
  14. package/dist/lib/render-xxd348m1.js +11 -0
  15. package/package.json +2 -3
  16. package/src/adapter.test.ts +117 -0
  17. package/src/adapter.ts +122 -31
  18. package/src/core/canvas-draw.ts +141 -38
  19. package/src/core/canvas-renderer.test.ts +58 -0
  20. package/src/core/canvas-renderer.ts +2 -1
  21. package/src/core/data-dir.ts +13 -0
  22. package/src/core/engine.ts +74 -36
  23. package/src/core/font-loader.test.ts +20 -0
  24. package/src/core/font-loader.ts +6 -2
  25. package/src/core/image-layout.ts +4 -0
  26. package/src/core/inline-text.ts +6 -4
  27. package/src/core/obstacle-flow.ts +29 -15
  28. package/src/core/page-images.test.ts +90 -0
  29. package/src/core/pretext.ts +4 -2
  30. package/src/core/template.test.ts +38 -1
  31. package/src/core/template.ts +52 -40
  32. package/src/core/types.ts +46 -0
  33. package/src/index.ts +4 -1
  34. package/src/server/auth.ts +18 -13
  35. package/src/server/index.ts +10 -15
  36. package/src/server/routes/render.ts +60 -17
  37. package/src/server/routes/slots.test.ts +96 -0
  38. package/src/server/routes/templates.ts +34 -35
  39. package/src/server/routes/upload.ts +2 -1
  40. package/src/web/App.tsx +15 -4
  41. package/src/web/components/CanvasViewport.tsx +56 -42
  42. package/src/web/components/EditorSidebar.tsx +35 -0
  43. package/src/web/components/TemplateModal.tsx +22 -14
  44. package/src/web/hooks/image-layout.ts +31 -0
  45. package/src/web/hooks/useLocalFonts.ts +10 -1
  46. package/src/web/hooks/useTypeset.ts +119 -32
  47. package/data/templates/.gitkeep +0 -0
  48. package/data/templates/tpl_1791681833354_iofsk.json +0 -46
  49. package/data/templates/tpl_1791682603199_875ny.json +0 -66
  50. package/data/templates/tpl_1791682780929_qnuiy.json +0 -66
  51. package/data/templates/tpl_longform_paper_sage.json +0 -47
  52. package/data/templates/tpl_longform_vintage_news.json +0 -47
  53. package/data/templates/tpl_newspic_poster_cover.json +0 -64
  54. package/data/templates/tpl_square_quote_card.json +0 -46
  55. package/data/templates/tpl_wechat_cover_editorial.json +0 -64
  56. package/data/templates/tpl_wechat_cover_minimal.json +0 -64
  57. package/data/templates/tpl_wide_tech_banner.json +0 -46
  58. package/dist/assets/index-CZ352D7X.css +0 -1
  59. package/dist/assets/index-DDeEGeHD.js +0 -317
@@ -1,8 +1,10 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { clearCache } from "@chenglou/pretext";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
2
3
  import { getDefaultConfig, runTypeset } from "../../core/engine";
3
4
  import { computeStageDimensions, getCanvasSpec } from "../../core/geometry";
4
5
  import { resolveImagePlacement } from "../../core/image-layout";
5
- import { applyTemplateImages } from "../../core/template";
6
+ import { applyTemplateImages, validateTemplateAssets } from "../../core/template";
7
+ import { backgroundImageLayout, restoreImageLayout, snapshotImageLayout, type ImageLayout } from "./image-layout";
6
8
  import {
7
9
  AlgorithmParams,
8
10
  AspectRatioType,
@@ -36,12 +38,14 @@ function resolvePresetImage(
36
38
  ): FlowImage {
37
39
  if (image.isAbsolute || !image.layoutPreset || image.layoutPreset === "custom") return image;
38
40
  const page = image.pageIndex || 1;
39
- const indexOnPage = images.filter((item) => (item.pageIndex || 1) === page)
41
+ const indexOnPage = images.filter((item) => Boolean(item.templateAssetKey) === Boolean(image.templateAssetKey) && (item.pageIndex || 1) === page)
40
42
  .findIndex((item) => item.id === image.id);
41
43
  return resolveImagePlacement(image, indexOnPage, stageWidth, stageHeight);
42
44
  }
43
45
 
44
- export function useTypeset() {
46
+ export function useTypeset(adminKey: string, isVerified: boolean | null) {
47
+ const [error, setError] = useState<string | null>(null);
48
+ const [fontVersion, setFontVersion] = useState(0);
45
49
  const [config, setConfig] = useState<TypesetConfig>(() => getDefaultConfig("longform-3-4"));
46
50
  const aspectRatio = config.aspectRatio;
47
51
  const [markdown, setMarkdown] = useState<string>(INITIAL_MARKDOWN);
@@ -60,18 +64,54 @@ export function useTypeset() {
60
64
  },
61
65
  ]);
62
66
  const [highlightWords, setHighlightWords] = useState<string[]>(["算法排版", "确定性", "空间秩序"]);
67
+ // 上传或应用模板时记录初始布局,背景切换另存一份勾选前的布局。
68
+ const initialImageLayouts = useRef(new Map(images.map((image) => [image.id, snapshotImageLayout(image)])));
69
+ const [backgroundLayouts, setBackgroundLayouts] = useState<Record<string, ImageLayout>>({});
63
70
 
64
71
  // Calculate typeset result synchronously (instant 60fps)
65
- const result: TypesetResult = useMemo(() => {
66
- return runTypeset({
67
- markdown,
68
- images,
69
- config,
70
- highlightWords,
71
- });
72
- }, [markdown, images, config, highlightWords]);
72
+ const { result, layoutError } = useMemo(() => {
73
+ try {
74
+ return { result: runTypeset({ markdown, images, config, highlightWords }), layoutError: null };
75
+ } catch (err) {
76
+ return {
77
+ result: { pages: [], pageCount: 0, canvasSpec: getCanvasSpec(config.aspectRatio), config } satisfies TypesetResult,
78
+ layoutError: err instanceof Error ? err.message : "排版失败",
79
+ };
80
+ }
81
+ }, [markdown, images, config, highlightWords, fontVersion]);
82
+
83
+ useEffect(() => {
84
+ let active = true;
85
+ let refreshTimer: ReturnType<typeof setTimeout> | undefined;
86
+ const refresh = () => {
87
+ if (!active) return;
88
+ clearTimeout(refreshTimer);
89
+ refreshTimer = setTimeout(() => {
90
+ clearCache();
91
+ setFontVersion((version) => version + 1);
92
+ }, 0);
93
+ };
94
+ document.fonts.ready.then(refresh);
95
+ document.fonts.addEventListener("loadingdone", refresh);
96
+ window.addEventListener("typeset-fonts-loaded", refresh);
97
+ return () => {
98
+ active = false;
99
+ clearTimeout(refreshTimer);
100
+ document.fonts.removeEventListener("loadingdone", refresh);
101
+ window.removeEventListener("typeset-fonts-loaded", refresh);
102
+ };
103
+ }, []);
73
104
 
74
105
  useEffect(() => {
106
+ if (!isVerified) return;
107
+ let active = true;
108
+ const headers = { "x-admin-key": adminKey };
109
+ const readTemplate = async (url: string) => {
110
+ const response = await fetch(url, { headers });
111
+ const data = await response.json();
112
+ if (!response.ok || !data.ok) throw new Error(data.message || data.error || "加载模板失败");
113
+ return data;
114
+ };
75
115
  const initFromUrlAndDefaults = async () => {
76
116
  try {
77
117
  const params = new URLSearchParams(window.location.search);
@@ -90,45 +130,43 @@ export function useTypeset() {
90
130
 
91
131
  // 1. Handle explicit template loading
92
132
  if (urlTemplate) {
93
- const res = await fetch(`/api/templates/resolve/${encodeURIComponent(urlTemplate)}`);
94
- const data = await res.json();
133
+ const data = await readTemplate(`/api/templates/resolve/${encodeURIComponent(urlTemplate)}`);
95
134
  if (data.ok && data.template) {
96
- loadTemplate(data.template);
135
+ if (active) loadTemplate(data.template);
97
136
  return;
98
137
  }
99
138
  }
100
139
 
101
140
  // 2. Handle slot loading
102
141
  if (urlSlot) {
103
- const res = await fetch(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
104
- const data = await res.json();
142
+ const data = await readTemplate(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
105
143
  if (data.ok && data.slots?.[urlSlot]?.template) {
106
- loadTemplate(data.slots[urlSlot].template);
144
+ if (active) loadTemplate(data.slots[urlSlot].template);
107
145
  return;
108
146
  }
109
147
  }
110
148
 
111
149
  // 3. If no template or slot in URL, check slot defaults or global default
112
- const res = await fetch(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
113
- const data = await res.json();
150
+ const data = await readTemplate(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
114
151
  if (data.ok && data.slots?.newspic_content?.template) {
115
- loadTemplate(data.slots.newspic_content.template);
152
+ if (active) loadTemplate(data.slots.newspic_content.template);
116
153
  return;
117
154
  }
118
155
 
119
156
  // 4. Fallback default
120
- const defRes = await fetch("/api/templates/default");
121
- const defData = await defRes.json();
157
+ const defData = await readTemplate("/api/templates/default");
122
158
  if (defData.ok && defData.template) {
123
- loadTemplate(defData.template);
159
+ if (active) loadTemplate(defData.template);
124
160
  }
125
- } catch {
126
- // ignore
161
+ } catch (err) {
162
+ if (active) setError(err instanceof Error ? err.message : "加载模板失败");
127
163
  }
128
164
  };
129
165
 
166
+ setError(null);
130
167
  initFromUrlAndDefaults();
131
- }, []);
168
+ return () => { active = false; };
169
+ }, [adminKey, isVerified]);
132
170
 
133
171
  const switchAspectRatio = (ratio: AspectRatioType) => {
134
172
  const spec = getCanvasSpec(ratio);
@@ -193,7 +231,7 @@ export function useTypeset() {
193
231
  config.algorithm.contentBottomGap,
194
232
  );
195
233
  const targetPage = image.pageIndex || 1;
196
- const samePageImages = images.filter((im) => (im.pageIndex || 1) === targetPage);
234
+ const samePageImages = images.filter((im) => !im.templateAssetKey && (im.pageIndex || 1) === targetPage);
197
235
  const indexOnPage = samePageImages.length;
198
236
 
199
237
  const temp: FlowImage = {
@@ -208,14 +246,17 @@ export function useTypeset() {
208
246
  width: resolved.width,
209
247
  height: resolved.height,
210
248
  };
249
+ initialImageLayouts.current.set(newImage.id, snapshotImageLayout(newImage));
211
250
  setImages((prev) => [...prev, newImage]);
212
251
  };
213
252
 
214
253
  const updateImage = (id: string, partial: Partial<FlowImage>) => {
254
+ const previousBackground = partial.isAbsolute === false ? backgroundLayouts[id] : undefined;
255
+ if (previousBackground) clearBackgroundLayout(id);
215
256
  setImages((prev) => {
216
257
  const nextImages = prev.map((img) => {
217
258
  if (img.id !== id) return img;
218
- const merged: FlowImage = { ...img, ...partial };
259
+ const merged = restoreImageLayout(img, { ...previousBackground, ...partial });
219
260
  if (partial.layoutPreset && partial.layoutPreset !== "custom") {
220
261
  // Clear stale preset coordinates when switching presets
221
262
  delete merged.x;
@@ -246,7 +287,40 @@ export function useTypeset() {
246
287
  });
247
288
  };
248
289
 
290
+ // 清除背景状态,避免恢复布局后复选框仍处于选中状态。
291
+ const clearBackgroundLayout = (id: string) => setBackgroundLayouts((prev) => {
292
+ const next = { ...prev };
293
+ delete next[id];
294
+ return next;
295
+ });
296
+
297
+ // 背景复选框只覆盖当前布局,取消时恢复勾选前的快照。
298
+ const toggleImageBackground = (id: string, enabled: boolean) => {
299
+ const image = images.find((item) => item.id === id);
300
+ if (!image) return;
301
+ if (enabled) {
302
+ if (backgroundLayouts[id]) return;
303
+ setBackgroundLayouts((prev) => ({ ...prev, [id]: snapshotImageLayout(image) }));
304
+ updateImage(id, backgroundImageLayout(config));
305
+ } else {
306
+ const layout = backgroundLayouts[id];
307
+ if (!layout) return;
308
+ clearBackgroundLayout(id);
309
+ setImages((prev) => prev.map((item) => item.id === id ? restoreImageLayout(item, layout) : item));
310
+ }
311
+ };
312
+
313
+ // 重置到本次上传或应用模板时的布局,不改内容和固定素材标识。
314
+ const resetImageLayout = (id: string) => {
315
+ const layout = initialImageLayouts.current.get(id);
316
+ if (!layout) return;
317
+ clearBackgroundLayout(id);
318
+ setImages((prev) => prev.map((image) => image.id === id ? restoreImageLayout(image, layout) : image));
319
+ };
320
+
249
321
  const removeImage = (id: string) => {
322
+ initialImageLayouts.current.delete(id);
323
+ clearBackgroundLayout(id);
250
324
  setImages((prev) => {
251
325
  const remaining = prev.filter((img) => img.id !== id);
252
326
  const spec = getCanvasSpec(aspectRatio);
@@ -282,9 +356,18 @@ export function useTypeset() {
282
356
  };
283
357
 
284
358
  const loadTemplate = (tpl: TypesetConfig) => {
285
- setConfig(tpl);
286
- if (tpl.images) {
287
- setImages((prev) => applyTemplateImages(prev, tpl.images));
359
+ try {
360
+ validateTemplateAssets(tpl.images);
361
+ setConfig(tpl);
362
+ setBackgroundLayouts({});
363
+ setImages((prev) => {
364
+ const next = applyTemplateImages(prev, tpl.images);
365
+ initialImageLayouts.current = new Map(next.map((image) => [image.id, snapshotImageLayout(image)]));
366
+ return next;
367
+ });
368
+ setError(null);
369
+ } catch (err) {
370
+ setError(err instanceof Error ? err.message : "模板素材无效");
288
371
  }
289
372
  };
290
373
 
@@ -296,6 +379,7 @@ export function useTypeset() {
296
379
  };
297
380
 
298
381
  return {
382
+ error: layoutError || error,
299
383
  aspectRatio,
300
384
  switchAspectRatio,
301
385
  config,
@@ -305,6 +389,9 @@ export function useTypeset() {
305
389
  images,
306
390
  addImage,
307
391
  updateImage,
392
+ toggleImageBackground,
393
+ resetImageLayout,
394
+ backgroundImageIds: Object.keys(backgroundLayouts),
308
395
  removeImage,
309
396
  moveImage,
310
397
  highlightWords,
File without changes
@@ -1,46 +0,0 @@
1
- {
2
- "name": "封面1",
3
- "aspectRatio": "wechat-cover-2.35",
4
- "algorithm": {
5
- "baseFontSize": 56,
6
- "scaleRatio": 1.3,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "center",
12
- "verticalAlign": "center",
13
- "safeArea": {
14
- "top": 40,
15
- "right": 60,
16
- "bottom": 40,
17
- "left": 60
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#f9fcfa",
22
- "textColor": "#22201d",
23
- "headingColor": "#0f2e22",
24
- "accentColor": "#1d4f39",
25
- "quoteColor": "#474038",
26
- "captionColor": "#71717a",
27
- "watermarkColor": "#52525b",
28
- "cardBg": "#ffffff"
29
- },
30
- "typography": {
31
- "headingFont": "PingFang SC",
32
- "bodyFont": "SourceHanSansSC, sans-serif"
33
- },
34
- "footerText": "TECH ENGINEERING INSIGHTS",
35
- "watermarkText": "DEV // ARCHITECTURE",
36
- "chrome": {
37
- "showHeader": false,
38
- "showWatermark": true,
39
- "showPageNumber": false,
40
- "showFooter": true,
41
- "showFooterDivider": false,
42
- "showFooterText": true,
43
- "showFillRatio": false
44
- },
45
- "id": "tpl_1791681833354_iofsk"
46
- }
@@ -1,66 +0,0 @@
1
- {
2
- "name": "封面2",
3
- "aspectRatio": "wechat-cover-2.35",
4
- "algorithm": {
5
- "baseFontSize": 56,
6
- "scaleRatio": 1.25,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "center",
12
- "verticalAlign": "center",
13
- "safeArea": {
14
- "top": 40,
15
- "right": 60,
16
- "bottom": 40,
17
- "left": 60
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#ffffff",
22
- "textColor": "#171717",
23
- "headingColor": "#171717",
24
- "accentColor": "#059669",
25
- "quoteColor": "#525252",
26
- "captionColor": "#737373",
27
- "watermarkColor": "#a3a3a3",
28
- "cardBg": "#f5f5f5",
29
- "borderSubtle": "#e5e5e5"
30
- },
31
- "typography": {
32
- "headingFont": "PingFang SC",
33
- "bodyFont": "Songti SC"
34
- },
35
- "footerText": "Typeset Studio · 纯算法排版",
36
- "watermarkText": "@Antigravity",
37
- "chrome": {
38
- "showHeader": true,
39
- "showWatermark": true,
40
- "showPageNumber": true,
41
- "showFooter": true,
42
- "showFooterDivider": false,
43
- "showFooterText": true,
44
- "showFillRatio": false
45
- },
46
- "images": [
47
- {
48
- "id": "img_demo_1",
49
- "src": "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&auto=format&fit=crop&q=80",
50
- "alt": "抽象建筑线条",
51
- "caption": "图 1:建筑结构中的空间秩序感",
52
- "x": 1276,
53
- "y": 170,
54
- "width": 700,
55
- "height": 590,
56
- "captionHeight": 40,
57
- "layoutPreset": "custom",
58
- "isAbsolute": true,
59
- "zIndex": 0,
60
- "rotate": -14,
61
- "opacity": 0.65,
62
- "borderRadius": 136
63
- }
64
- ],
65
- "id": "tpl_1791682603199_875ny"
66
- }
@@ -1,66 +0,0 @@
1
- {
2
- "name": "封面2-3",
3
- "aspectRatio": "wechat-cover-2.35",
4
- "algorithm": {
5
- "baseFontSize": 56,
6
- "scaleRatio": 1.25,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "center",
12
- "verticalAlign": "center",
13
- "safeArea": {
14
- "top": 40,
15
- "right": 60,
16
- "bottom": 40,
17
- "left": 60
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#0a0a0a",
22
- "textColor": "#ededed",
23
- "headingColor": "#ffffff",
24
- "accentColor": "#00e676",
25
- "quoteColor": "#a0a0a0",
26
- "captionColor": "#737373",
27
- "watermarkColor": "#a3a3a3",
28
- "cardBg": "#121212",
29
- "borderSubtle": "#e5e5e5"
30
- },
31
- "typography": {
32
- "headingFont": "PingFang SC",
33
- "bodyFont": "Songti SC"
34
- },
35
- "footerText": "Typeset Studio · 纯算法排版",
36
- "watermarkText": "@Antigravity",
37
- "chrome": {
38
- "showHeader": true,
39
- "showWatermark": true,
40
- "showPageNumber": true,
41
- "showFooter": true,
42
- "showFooterDivider": false,
43
- "showFooterText": true,
44
- "showFillRatio": false
45
- },
46
- "images": [
47
- {
48
- "src": "/uploads/1791682691652_fsgtx6.png",
49
- "alt": "头像-捧脸大笑-v4.png",
50
- "caption": "",
51
- "x": 1276,
52
- "y": 170,
53
- "width": 700,
54
- "height": 590,
55
- "captionHeight": 40,
56
- "layoutPreset": "custom",
57
- "id": "img_1791682691661_1zpg",
58
- "isAbsolute": true,
59
- "rotate": -14,
60
- "opacity": 0.65,
61
- "zIndex": 0,
62
- "borderRadius": 136
63
- }
64
- ],
65
- "id": "tpl_1791682780929_qnuiy"
66
- }
@@ -1,47 +0,0 @@
1
- {
2
- "name": "纸白典雅长图文 (3:4)",
3
- "aspectRatio": "longform-3-4",
4
- "algorithm": {
5
- "baseFontSize": 40,
6
- "scaleRatio": 1.25,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "left",
12
- "verticalAlign": "top",
13
- "safeArea": {
14
- "top": 96,
15
- "right": 88,
16
- "bottom": 96,
17
- "left": 88
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#fbfcfb",
22
- "textColor": "#1c1917",
23
- "headingColor": "#0c0a09",
24
- "accentColor": "#047857",
25
- "quoteColor": "#44403c",
26
- "captionColor": "#78716c",
27
- "watermarkColor": "#a8a29e",
28
- "cardBg": "#ffffff"
29
- },
30
- "typography": {
31
- "headingFont": "Songti SC",
32
- "bodyFont": "Songti SC"
33
- },
34
- "footerText": "Typeset Studio · 纯算法空间排版",
35
- "watermarkText": "@ANTIGRAVITY",
36
- "chrome": {
37
- "showHeader": true,
38
- "showWatermark": true,
39
- "showPageNumber": true,
40
- "showFooter": true,
41
- "showFooterDivider": true,
42
- "showFooterText": true,
43
- "showFillRatio": false
44
- },
45
- "id": "tpl_longform_paper_sage",
46
- "slot": "newspic_content"
47
- }
@@ -1,47 +0,0 @@
1
- {
2
- "name": "复古报刊多图 (3:4)",
3
- "aspectRatio": "longform-3-4",
4
- "algorithm": {
5
- "baseFontSize": 38,
6
- "scaleRatio": 1.25,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "left",
12
- "verticalAlign": "top",
13
- "safeArea": {
14
- "top": 96,
15
- "right": 88,
16
- "bottom": 96,
17
- "left": 88
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#f7f4ee",
22
- "textColor": "#262320",
23
- "headingColor": "#141210",
24
- "accentColor": "#991b1b",
25
- "quoteColor": "#57534e",
26
- "captionColor": "#78716c",
27
- "watermarkColor": "#a8a29e",
28
- "cardBg": "#eee9df"
29
- },
30
- "typography": {
31
- "headingFont": "SourceHanSerifSC, serif",
32
- "bodyFont": "SourceHanSerifSC, serif"
33
- },
34
- "footerText": "WEEKLY REPORT · 观察与沉淀",
35
- "watermarkText": "VOL. 42 // THE CHRONICLE",
36
- "chrome": {
37
- "showHeader": true,
38
- "showWatermark": true,
39
- "showPageNumber": true,
40
- "showFooter": true,
41
- "showFooterDivider": true,
42
- "showFooterText": true,
43
- "showFillRatio": false
44
- },
45
- "id": "tpl_longform_vintage_news",
46
- "slot": "newspic_content"
47
- }
@@ -1,64 +0,0 @@
1
- {
2
- "name": "贴图封面海报 (3:4)",
3
- "aspectRatio": "longform-3-4",
4
- "algorithm": {
5
- "baseFontSize": 56,
6
- "scaleRatio": 1.35,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "center",
12
- "verticalAlign": "center",
13
- "safeArea": {
14
- "top": 96,
15
- "right": 88,
16
- "bottom": 96,
17
- "left": 88
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#121212",
22
- "textColor": "#ffffff",
23
- "headingColor": "#ffffff",
24
- "accentColor": "#f59e0b",
25
- "quoteColor": "#a3a3a3",
26
- "captionColor": "#737373",
27
- "watermarkColor": "#525252",
28
- "cardBg": "#1c1c1c"
29
- },
30
- "typography": {
31
- "headingFont": "PingFang SC",
32
- "bodyFont": "Songti SC"
33
- },
34
- "footerText": "Typeset Studio · 贴图精选",
35
- "watermarkText": "FEATURED NEWSPIC",
36
- "chrome": {
37
- "showHeader": true,
38
- "showWatermark": true,
39
- "showPageNumber": false,
40
- "showFooter": true,
41
- "showFooterDivider": false,
42
- "showFooterText": true,
43
- "showFillRatio": false
44
- },
45
- "id": "tpl_newspic_poster_cover",
46
- "slot": "newspic_cover",
47
- "images": [
48
- {
49
- "id": "tpl_img_poster_bg",
50
- "src": "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1200&auto=format&fit=crop&q=80",
51
- "alt": "海报主图",
52
- "isAbsolute": true,
53
- "x": 100,
54
- "y": 780,
55
- "width": 1000,
56
- "height": 650,
57
- "zIndex": 10,
58
- "rotate": 0,
59
- "opacity": 1,
60
- "borderRadius": 20,
61
- "layoutPreset": "custom"
62
- }
63
- ]
64
- }
@@ -1,46 +0,0 @@
1
- {
2
- "name": "金句留白方卡 (1:1)",
3
- "aspectRatio": "square-1-1",
4
- "algorithm": {
5
- "baseFontSize": 50,
6
- "scaleRatio": 1.3,
7
- "lineHeightMultiplier": 1.45,
8
- "obstacleGap": 24,
9
- "targetFillRatio": 0.8,
10
- "contentBottomGap": 20,
11
- "textAlign": "center",
12
- "verticalAlign": "center",
13
- "safeArea": {
14
- "top": 100,
15
- "right": 100,
16
- "bottom": 100,
17
- "left": 100
18
- }
19
- },
20
- "theme": {
21
- "bgColor": "#18181b",
22
- "textColor": "#fafafa",
23
- "headingColor": "#ffffff",
24
- "accentColor": "#f59e0b",
25
- "quoteColor": "#d4d4d8",
26
- "captionColor": "#a1a1aa",
27
- "watermarkColor": "#71717a",
28
- "cardBg": "#27272a"
29
- },
30
- "typography": {
31
- "headingFont": "PingFang SC",
32
- "bodyFont": "Kaiti SC"
33
- },
34
- "footerText": "",
35
- "watermarkText": "",
36
- "chrome": {
37
- "showHeader": false,
38
- "showWatermark": false,
39
- "showPageNumber": false,
40
- "showFooter": false,
41
- "showFooterDivider": false,
42
- "showFooterText": false,
43
- "showFillRatio": false
44
- },
45
- "id": "tpl_square_quote_card"
46
- }