@akairos/typeset-studio 0.2.1 → 0.2.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 (62) hide show
  1. package/README.md +91 -95
  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 -7734
  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-0370qz9e.js +303 -0
  9. package/dist/lib/index-37x76zdn.js +4 -0
  10. package/dist/lib/index-6c43c7h4.js +3880 -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 -7734
  14. package/dist/lib/render-xxd348m1.js +11 -0
  15. package/package.json +3 -7
  16. package/src/adapter.test.ts +117 -0
  17. package/src/adapter.ts +190 -281
  18. package/src/core/canvas-draw.ts +150 -47
  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 +86 -56
  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 -10
  30. package/src/core/template.test.ts +38 -1
  31. package/src/core/template.ts +53 -41
  32. package/src/core/types.ts +46 -0
  33. package/src/core/typography.ts +0 -14
  34. package/src/index.ts +6 -3
  35. package/src/server/auth.ts +18 -13
  36. package/src/server/index.ts +11 -16
  37. package/src/server/routes/fonts.ts +1 -1
  38. package/src/server/routes/render.ts +60 -17
  39. package/src/server/routes/slots.test.ts +96 -0
  40. package/src/server/routes/templates.ts +37 -48
  41. package/src/server/routes/upload.ts +2 -1
  42. package/src/web/App.tsx +15 -6
  43. package/src/web/components/CanvasViewport.tsx +56 -42
  44. package/src/web/components/EditorSidebar.tsx +59 -48
  45. package/src/web/components/Header.tsx +1 -1
  46. package/src/web/components/TemplateModal.tsx +25 -28
  47. package/src/web/hooks/image-layout.ts +31 -0
  48. package/src/web/hooks/useLocalFonts.ts +10 -1
  49. package/src/web/hooks/useTypeset.ts +139 -100
  50. package/data/templates/.gitkeep +0 -0
  51. package/data/templates/tpl_1791681833354_iofsk.json +0 -46
  52. package/data/templates/tpl_1791682603199_875ny.json +0 -66
  53. package/data/templates/tpl_1791682780929_qnuiy.json +0 -66
  54. package/data/templates/tpl_longform_paper_sage.json +0 -47
  55. package/data/templates/tpl_longform_vintage_news.json +0 -47
  56. package/data/templates/tpl_newspic_poster_cover.json +0 -64
  57. package/data/templates/tpl_square_quote_card.json +0 -46
  58. package/data/templates/tpl_wechat_cover_editorial.json +0 -64
  59. package/data/templates/tpl_wechat_cover_minimal.json +0 -64
  60. package/data/templates/tpl_wide_tech_banner.json +0 -46
  61. package/dist/assets/index-CZ352D7X.css +0 -1
  62. package/dist/assets/index-Dpnz-F2g.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,
@@ -27,9 +29,25 @@ const INITIAL_MARKDOWN = `# 大模型纯算法排版深度剖析
27
29
  - 任意画板比例智能自适应
28
30
  `;
29
31
 
30
- export function useTypeset() {
31
- const [aspectRatio, setAspectRatio] = useState<AspectRatioType>("longform-3-4");
32
+ // 统一预设配图的页内序号计算,手动坐标和自由图层保持原样。
33
+ function resolvePresetImage(
34
+ image: FlowImage,
35
+ images: FlowImage[],
36
+ stageWidth: number,
37
+ stageHeight: number,
38
+ ): FlowImage {
39
+ if (image.isAbsolute || !image.layoutPreset || image.layoutPreset === "custom") return image;
40
+ const page = image.pageIndex || 1;
41
+ const indexOnPage = images.filter((item) => Boolean(item.templateAssetKey) === Boolean(image.templateAssetKey) && (item.pageIndex || 1) === page)
42
+ .findIndex((item) => item.id === image.id);
43
+ return resolveImagePlacement(image, indexOnPage, stageWidth, stageHeight);
44
+ }
45
+
46
+ export function useTypeset(adminKey: string, isVerified: boolean | null) {
47
+ const [error, setError] = useState<string | null>(null);
48
+ const [fontVersion, setFontVersion] = useState(0);
32
49
  const [config, setConfig] = useState<TypesetConfig>(() => getDefaultConfig("longform-3-4"));
50
+ const aspectRatio = config.aspectRatio;
33
51
  const [markdown, setMarkdown] = useState<string>(INITIAL_MARKDOWN);
34
52
  const [images, setImages] = useState<FlowImage[]>([
35
53
  {
@@ -46,18 +64,54 @@ export function useTypeset() {
46
64
  },
47
65
  ]);
48
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>>({});
49
70
 
50
71
  // Calculate typeset result synchronously (instant 60fps)
51
- const result: TypesetResult = useMemo(() => {
52
- return runTypeset({
53
- markdown,
54
- images,
55
- config,
56
- highlightWords,
57
- });
58
- }, [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
+ }, []);
59
104
 
60
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
+ };
61
115
  const initFromUrlAndDefaults = async () => {
62
116
  try {
63
117
  const params = new URLSearchParams(window.location.search);
@@ -76,48 +130,45 @@ export function useTypeset() {
76
130
 
77
131
  // 1. Handle explicit template loading
78
132
  if (urlTemplate) {
79
- const res = await fetch(`/api/templates/resolve/${encodeURIComponent(urlTemplate)}`);
80
- const data = await res.json();
133
+ const data = await readTemplate(`/api/templates/resolve/${encodeURIComponent(urlTemplate)}`);
81
134
  if (data.ok && data.template) {
82
- loadTemplate(data.template);
135
+ if (active) loadTemplate(data.template);
83
136
  return;
84
137
  }
85
138
  }
86
139
 
87
140
  // 2. Handle slot loading
88
141
  if (urlSlot) {
89
- const res = await fetch(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
90
- const data = await res.json();
142
+ const data = await readTemplate(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
91
143
  if (data.ok && data.slots?.[urlSlot]?.template) {
92
- loadTemplate(data.slots[urlSlot].template);
144
+ if (active) loadTemplate(data.slots[urlSlot].template);
93
145
  return;
94
146
  }
95
147
  }
96
148
 
97
149
  // 3. If no template or slot in URL, check slot defaults or global default
98
- const res = await fetch(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
99
- const data = await res.json();
150
+ const data = await readTemplate(`/api/templates/slots?account=${encodeURIComponent(urlAccount)}`);
100
151
  if (data.ok && data.slots?.newspic_content?.template) {
101
- loadTemplate(data.slots.newspic_content.template);
152
+ if (active) loadTemplate(data.slots.newspic_content.template);
102
153
  return;
103
154
  }
104
155
 
105
156
  // 4. Fallback default
106
- const defRes = await fetch("/api/templates/default");
107
- const defData = await defRes.json();
157
+ const defData = await readTemplate("/api/templates/default");
108
158
  if (defData.ok && defData.template) {
109
- loadTemplate(defData.template);
159
+ if (active) loadTemplate(defData.template);
110
160
  }
111
- } catch {
112
- // ignore
161
+ } catch (err) {
162
+ if (active) setError(err instanceof Error ? err.message : "加载模板失败");
113
163
  }
114
164
  };
115
165
 
166
+ setError(null);
116
167
  initFromUrlAndDefaults();
117
- }, []);
168
+ return () => { active = false; };
169
+ }, [adminKey, isVerified]);
118
170
 
119
171
  const switchAspectRatio = (ratio: AspectRatioType) => {
120
- setAspectRatio(ratio);
121
172
  const spec = getCanvasSpec(ratio);
122
173
  const nextDefault = getDefaultConfig(ratio);
123
174
  const { stageWidth, stageHeight } = computeStageDimensions(
@@ -125,24 +176,7 @@ export function useTypeset() {
125
176
  nextDefault.algorithm.safeArea,
126
177
  nextDefault.algorithm.contentBottomGap,
127
178
  );
128
- setImages((prev) =>
129
- prev.map((img) => {
130
- if (!img.isAbsolute && img.layoutPreset && img.layoutPreset !== "custom") {
131
- const page = img.pageIndex || 1;
132
- const samePageList = prev.filter((im) => (im.pageIndex || 1) === page);
133
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === img.id));
134
- const resolved = resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight);
135
- return {
136
- ...img,
137
- x: resolved.x,
138
- y: resolved.y,
139
- width: resolved.width,
140
- height: resolved.height,
141
- };
142
- }
143
- return img;
144
- }),
145
- );
179
+ setImages((prev) => prev.map((image) => resolvePresetImage(image, prev, stageWidth, stageHeight)));
146
180
  setConfig((prev) => {
147
181
  return {
148
182
  ...prev,
@@ -197,7 +231,7 @@ export function useTypeset() {
197
231
  config.algorithm.contentBottomGap,
198
232
  );
199
233
  const targetPage = image.pageIndex || 1;
200
- const samePageImages = images.filter((im) => (im.pageIndex || 1) === targetPage);
234
+ const samePageImages = images.filter((im) => !im.templateAssetKey && (im.pageIndex || 1) === targetPage);
201
235
  const indexOnPage = samePageImages.length;
202
236
 
203
237
  const temp: FlowImage = {
@@ -212,14 +246,17 @@ export function useTypeset() {
212
246
  width: resolved.width,
213
247
  height: resolved.height,
214
248
  };
249
+ initialImageLayouts.current.set(newImage.id, snapshotImageLayout(newImage));
215
250
  setImages((prev) => [...prev, newImage]);
216
251
  };
217
252
 
218
253
  const updateImage = (id: string, partial: Partial<FlowImage>) => {
254
+ const previousBackground = partial.isAbsolute === false ? backgroundLayouts[id] : undefined;
255
+ if (previousBackground) clearBackgroundLayout(id);
219
256
  setImages((prev) => {
220
257
  const nextImages = prev.map((img) => {
221
258
  if (img.id !== id) return img;
222
- const merged: FlowImage = { ...img, ...partial };
259
+ const merged = restoreImageLayout(img, { ...previousBackground, ...partial });
223
260
  if (partial.layoutPreset && partial.layoutPreset !== "custom") {
224
261
  // Clear stale preset coordinates when switching presets
225
262
  delete merged.x;
@@ -235,35 +272,55 @@ export function useTypeset() {
235
272
 
236
273
  // If non-custom flow preset, recalculate clean coordinates based on its page index
237
274
  if (!updatedImg.isAbsolute && updatedImg.layoutPreset && updatedImg.layoutPreset !== "custom") {
238
- const page = updatedImg.pageIndex || 1;
239
- const samePageList = nextImages.filter((im) => (im.pageIndex || 1) === page);
240
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === id));
241
-
242
275
  const spec = getCanvasSpec(aspectRatio);
243
276
  const { stageWidth, stageHeight } = computeStageDimensions(
244
277
  spec,
245
278
  config.algorithm.safeArea,
246
279
  config.algorithm.contentBottomGap,
247
280
  );
248
- const resolved = resolveImagePlacement(updatedImg, indexOnPage, stageWidth, stageHeight);
249
- return nextImages.map((im) =>
250
- im.id === id
251
- ? {
252
- ...im,
253
- x: resolved.x,
254
- y: resolved.y,
255
- width: resolved.width,
256
- height: resolved.height,
257
- }
258
- : im,
259
- );
281
+ return nextImages.map((image) => image.id === id
282
+ ? resolvePresetImage(image, nextImages, stageWidth, stageHeight)
283
+ : image);
260
284
  }
261
285
 
262
286
  return nextImages;
263
287
  });
264
288
  };
265
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
+
266
321
  const removeImage = (id: string) => {
322
+ initialImageLayouts.current.delete(id);
323
+ clearBackgroundLayout(id);
267
324
  setImages((prev) => {
268
325
  const remaining = prev.filter((img) => img.id !== id);
269
326
  const spec = getCanvasSpec(aspectRatio);
@@ -272,22 +329,7 @@ export function useTypeset() {
272
329
  config.algorithm.safeArea,
273
330
  config.algorithm.contentBottomGap,
274
331
  );
275
- return remaining.map((img) => {
276
- if (!img.isAbsolute && img.layoutPreset && img.layoutPreset !== "custom") {
277
- const page = img.pageIndex || 1;
278
- const samePageList = remaining.filter((im) => (im.pageIndex || 1) === page);
279
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === img.id));
280
- const resolved = resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight);
281
- return {
282
- ...img,
283
- x: resolved.x,
284
- y: resolved.y,
285
- width: resolved.width,
286
- height: resolved.height,
287
- };
288
- }
289
- return img;
290
- });
332
+ return remaining.map((image) => resolvePresetImage(image, remaining, stageWidth, stageHeight));
291
333
  });
292
334
  };
293
335
 
@@ -309,30 +351,23 @@ export function useTypeset() {
309
351
  config.algorithm.contentBottomGap,
310
352
  );
311
353
 
312
- return next.map((img) => {
313
- if (!img.isAbsolute && img.layoutPreset && img.layoutPreset !== "custom") {
314
- const page = img.pageIndex || 1;
315
- const samePageList = next.filter((im) => (im.pageIndex || 1) === page);
316
- const indexOnPage = Math.max(0, samePageList.findIndex((im) => im.id === img.id));
317
- const resolved = resolveImagePlacement(img, indexOnPage, stageWidth, stageHeight);
318
- return {
319
- ...img,
320
- x: resolved.x,
321
- y: resolved.y,
322
- width: resolved.width,
323
- height: resolved.height,
324
- };
325
- }
326
- return img;
327
- });
354
+ return next.map((image) => resolvePresetImage(image, next, stageWidth, stageHeight));
328
355
  });
329
356
  };
330
357
 
331
358
  const loadTemplate = (tpl: TypesetConfig) => {
332
- setConfig(tpl);
333
- setAspectRatio(tpl.aspectRatio);
334
- if (tpl.images) {
335
- 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 : "模板素材无效");
336
371
  }
337
372
  };
338
373
 
@@ -344,6 +379,7 @@ export function useTypeset() {
344
379
  };
345
380
 
346
381
  return {
382
+ error: layoutError || error,
347
383
  aspectRatio,
348
384
  switchAspectRatio,
349
385
  config,
@@ -353,6 +389,9 @@ export function useTypeset() {
353
389
  images,
354
390
  addImage,
355
391
  updateImage,
392
+ toggleImageBackground,
393
+ resetImageLayout,
394
+ backgroundImageIds: Object.keys(backgroundLayouts),
356
395
  removeImage,
357
396
  moveImage,
358
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
- }