@akairos/typeset-studio 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/assets/index-JCv_QmUn.css +1 -0
- package/dist/assets/index-X9ezH_RG.js +317 -0
- package/dist/index.html +2 -2
- package/dist/lib/adapter.js +1 -1
- package/dist/lib/{canvas-renderer-cxnar75n.js → canvas-renderer-4jg80agt.js} +1 -1
- package/dist/lib/{index-zdhqg5f0.js → index-bf1zs3mv.js} +291 -54
- package/dist/lib/{index-t9w3p2p9.js → index-hegsbxc0.js} +71 -3
- package/dist/lib/{index-qhpfzwj4.js → index-p8ttqbs6.js} +3 -3
- package/dist/lib/index.js +3 -3
- package/dist/lib/{render-xxd348m1.js → render-3043w6s6.js} +2 -2
- package/package.json +3 -3
- package/src/adapter.ts +1 -1
- package/src/core/canvas-draw.ts +53 -43
- package/src/core/canvas-presets.ts +37 -0
- package/src/core/canvas-renderer.test.ts +56 -0
- package/src/core/canvas-style.ts +149 -0
- package/src/core/engine.ts +2 -0
- package/src/core/types.ts +30 -3
- package/src/index.ts +4 -0
- package/src/server/index.ts +1 -1
- package/src/server/routes/slots.test.ts +22 -0
- package/src/server/routes/templates.ts +15 -0
- package/src/web/App.tsx +27 -7
- package/src/web/components/CanvasStyleInspector.tsx +95 -0
- package/src/web/components/Header.tsx +24 -9
- package/src/web/components/ParamsInspector.tsx +6 -1
- package/src/web/components/SaveTemplateModal.tsx +106 -0
- package/src/web/components/TemplateDrawer.tsx +323 -0
- package/src/web/hooks/useTypeset.ts +3 -1
- package/dist/assets/index-3Ko1UCBU.css +0 -1
- package/dist/assets/index-C9J0f4Lf.js +0 -317
- package/src/web/components/TemplateModal.tsx +0 -393
package/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
rel="stylesheet"
|
|
12
12
|
/>
|
|
13
13
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-web/style.css" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-X9ezH_RG.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-JCv_QmUn.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-[#fafafa] dark:bg-[#000000] text-[#171717] dark:text-[#ededed] antialiased selection:bg-[#eaeaea] selection:text-[#000000] dark:selection:bg-[#333333] dark:selection:text-[#ffffff] overflow-hidden">
|
|
18
18
|
<div id="root"></div>
|
package/dist/lib/adapter.js
CHANGED
|
@@ -60,6 +60,233 @@ function computeStageDimensions(spec, safeArea, contentBottomGap = 20) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// src/core/canvas-style.ts
|
|
64
|
+
var DEFAULT_PATTERN = { pattern: "none", color: "#527561", opacity: 0.12, size: 56 };
|
|
65
|
+
var DEFAULT_BORDER = { style: "none", color: "#527561", endColor: "#d7ad72", width: 3, inset: 32, radius: 24, opacity: 0.8 };
|
|
66
|
+
var PATTERN_LABELS = {
|
|
67
|
+
none: "无花纹",
|
|
68
|
+
lines: "横线",
|
|
69
|
+
grid: "网格",
|
|
70
|
+
dots: "圆点",
|
|
71
|
+
diagonal: "斜线",
|
|
72
|
+
waves: "波浪",
|
|
73
|
+
rings: "同心圆",
|
|
74
|
+
burst: "放射线",
|
|
75
|
+
checker: "棋盘格",
|
|
76
|
+
grain: "纸张颗粒"
|
|
77
|
+
};
|
|
78
|
+
var BORDER_LABELS = {
|
|
79
|
+
none: "无边框",
|
|
80
|
+
solid: "实线",
|
|
81
|
+
dashed: "虚线",
|
|
82
|
+
double: "双线",
|
|
83
|
+
corners: "四角装饰",
|
|
84
|
+
gradient: "渐变描边"
|
|
85
|
+
};
|
|
86
|
+
function validateCanvasStyle(style) {
|
|
87
|
+
if (style === undefined)
|
|
88
|
+
return;
|
|
89
|
+
if (!style || typeof style !== "object" || Array.isArray(style))
|
|
90
|
+
throw new RangeError("无效的背景与边框配置");
|
|
91
|
+
const color = (value) => {
|
|
92
|
+
if (typeof value !== "string" || !/^#[0-9a-f]{6}$/i.test(value))
|
|
93
|
+
throw new RangeError("背景装饰颜色必须是六位十六进制颜色");
|
|
94
|
+
};
|
|
95
|
+
const number = (value, min, max) => {
|
|
96
|
+
if (!Number.isFinite(value) || value < min || value > max)
|
|
97
|
+
throw new RangeError(`背景装饰参数必须在 ${min} 到 ${max} 之间`);
|
|
98
|
+
};
|
|
99
|
+
if (style.fill) {
|
|
100
|
+
const fill = style.fill;
|
|
101
|
+
if (fill.type === "solid")
|
|
102
|
+
color(fill.color);
|
|
103
|
+
else {
|
|
104
|
+
if (fill.type === "linear")
|
|
105
|
+
number(fill.angle, 0, 360);
|
|
106
|
+
else if (fill.type === "radial") {
|
|
107
|
+
number(fill.position?.x, 0, 100);
|
|
108
|
+
number(fill.position?.y, 0, 100);
|
|
109
|
+
} else
|
|
110
|
+
throw new RangeError("未知背景类型");
|
|
111
|
+
if (!Array.isArray(fill.stops) || fill.stops.length < 2 || fill.stops.length > 8)
|
|
112
|
+
throw new RangeError("渐变需要 2 到 8 个颜色节点");
|
|
113
|
+
for (const stop of fill.stops) {
|
|
114
|
+
if (!stop)
|
|
115
|
+
throw new RangeError("无效的渐变节点");
|
|
116
|
+
color(stop.color);
|
|
117
|
+
number(stop.offset, 0, 100);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (style.pattern) {
|
|
122
|
+
const pattern = style.pattern;
|
|
123
|
+
if (!Object.hasOwn(PATTERN_LABELS, pattern.pattern))
|
|
124
|
+
throw new RangeError("未知背景花纹");
|
|
125
|
+
color(pattern.color);
|
|
126
|
+
number(pattern.opacity, 0, 1);
|
|
127
|
+
number(pattern.size, 16, 240);
|
|
128
|
+
}
|
|
129
|
+
if (style.border) {
|
|
130
|
+
const border = style.border;
|
|
131
|
+
if (!Object.hasOwn(BORDER_LABELS, border.style))
|
|
132
|
+
throw new RangeError("未知边框样式");
|
|
133
|
+
color(border.color);
|
|
134
|
+
color(border.endColor);
|
|
135
|
+
number(border.width, 1, 24);
|
|
136
|
+
number(border.inset, 0, 200);
|
|
137
|
+
number(border.radius, 0, 200);
|
|
138
|
+
number(border.opacity, 0, 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function drawCanvasFill(ctx, spec, fill) {
|
|
142
|
+
ctx.save();
|
|
143
|
+
if (fill.type === "solid")
|
|
144
|
+
ctx.fillStyle = fill.color;
|
|
145
|
+
else {
|
|
146
|
+
let gradient;
|
|
147
|
+
if (fill.type === "linear") {
|
|
148
|
+
const angle = fill.angle * Math.PI / 180;
|
|
149
|
+
const distance = Math.abs(Math.cos(angle)) * spec.width / 2 + Math.abs(Math.sin(angle)) * spec.height / 2;
|
|
150
|
+
const dx = Math.cos(angle) * distance;
|
|
151
|
+
const dy = Math.sin(angle) * distance;
|
|
152
|
+
gradient = ctx.createLinearGradient(spec.width / 2 - dx, spec.height / 2 - dy, spec.width / 2 + dx, spec.height / 2 + dy);
|
|
153
|
+
} else {
|
|
154
|
+
const x = spec.width * fill.position.x / 100;
|
|
155
|
+
const y = spec.height * fill.position.y / 100;
|
|
156
|
+
const radius = Math.hypot(Math.max(x, spec.width - x), Math.max(y, spec.height - y));
|
|
157
|
+
gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
158
|
+
}
|
|
159
|
+
for (const stop of fill.stops)
|
|
160
|
+
gradient.addColorStop(stop.offset / 100, stop.color);
|
|
161
|
+
ctx.fillStyle = gradient;
|
|
162
|
+
}
|
|
163
|
+
ctx.fillRect(0, 0, spec.width, spec.height);
|
|
164
|
+
ctx.restore();
|
|
165
|
+
}
|
|
166
|
+
function drawCanvasPattern(ctx, spec, decoration) {
|
|
167
|
+
if (decoration.pattern === "none" || decoration.opacity === 0)
|
|
168
|
+
return;
|
|
169
|
+
const { width, height } = spec;
|
|
170
|
+
const size = Math.max(16, decoration.size);
|
|
171
|
+
ctx.save();
|
|
172
|
+
ctx.strokeStyle = ctx.fillStyle = decoration.color;
|
|
173
|
+
ctx.globalAlpha = decoration.opacity;
|
|
174
|
+
ctx.lineWidth = 1.5;
|
|
175
|
+
ctx.beginPath();
|
|
176
|
+
switch (decoration.pattern) {
|
|
177
|
+
case "grid":
|
|
178
|
+
case "lines":
|
|
179
|
+
if (decoration.pattern === "grid")
|
|
180
|
+
for (let x = size / 2;x < width; x += size) {
|
|
181
|
+
ctx.moveTo(x, 0);
|
|
182
|
+
ctx.lineTo(x, height);
|
|
183
|
+
}
|
|
184
|
+
for (let y = size / 2;y < height; y += size) {
|
|
185
|
+
ctx.moveTo(0, y);
|
|
186
|
+
ctx.lineTo(width, y);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case "dots":
|
|
190
|
+
for (let x = size / 2;x < width; x += size)
|
|
191
|
+
for (let y = size / 2;y < height; y += size) {
|
|
192
|
+
ctx.moveTo(x + 2, y);
|
|
193
|
+
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
case "diagonal":
|
|
197
|
+
for (let x = -height;x < width; x += size) {
|
|
198
|
+
ctx.moveTo(x, 0);
|
|
199
|
+
ctx.lineTo(x + height, height);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
case "waves":
|
|
203
|
+
for (let y = -size;y < height + size; y += size) {
|
|
204
|
+
ctx.moveTo(0, y);
|
|
205
|
+
for (let x = 0;x < width; x += size * 2)
|
|
206
|
+
ctx.bezierCurveTo(x + size * 0.5, y - size * 0.45, x + size * 1.5, y + size * 0.45, x + size * 2, y);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
case "rings":
|
|
210
|
+
for (let r = size;r < Math.hypot(width, height); r += size) {
|
|
211
|
+
ctx.moveTo(width * 0.82 + r, height * 0.18);
|
|
212
|
+
ctx.arc(width * 0.82, height * 0.18, r, 0, Math.PI * 2);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case "burst":
|
|
216
|
+
for (let a = 0;a < Math.PI * 2; a += Math.PI / Math.max(8, Math.round(size / 3))) {
|
|
217
|
+
ctx.moveTo(width * 0.8, height * 0.2);
|
|
218
|
+
ctx.lineTo(width * 0.8 + Math.cos(a) * width * 2, height * 0.2 + Math.sin(a) * height * 2);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
case "checker":
|
|
222
|
+
for (let x = 0;x < width; x += size)
|
|
223
|
+
for (let y = 0;y < height; y += size)
|
|
224
|
+
if ((Math.round(x / size) + Math.round(y / size)) % 2 === 0)
|
|
225
|
+
ctx.rect(x, y, size, size);
|
|
226
|
+
break;
|
|
227
|
+
case "grain": {
|
|
228
|
+
let seed = 42;
|
|
229
|
+
const random = () => {
|
|
230
|
+
seed = Math.imul(seed, 1664525) + 1013904223 >>> 0;
|
|
231
|
+
return seed / 4294967296;
|
|
232
|
+
};
|
|
233
|
+
for (let i = 0;i < width * height / (size * 3); i++) {
|
|
234
|
+
const x = random() * width;
|
|
235
|
+
const y = random() * height;
|
|
236
|
+
ctx.rect(x, y, 0.6 + random() * 1.2, 0.6 + random() * 1.2);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (["dots", "checker", "grain"].includes(decoration.pattern))
|
|
242
|
+
ctx.fill();
|
|
243
|
+
else
|
|
244
|
+
ctx.stroke();
|
|
245
|
+
ctx.restore();
|
|
246
|
+
}
|
|
247
|
+
function drawCanvasBorder(ctx, spec, border) {
|
|
248
|
+
if (!border || border.style === "none" || border.opacity === 0)
|
|
249
|
+
return;
|
|
250
|
+
const inset = Math.min(border.inset + border.width / 2, Math.min(spec.width, spec.height) / 2 - 1);
|
|
251
|
+
const width = spec.width - inset * 2;
|
|
252
|
+
const height = spec.height - inset * 2;
|
|
253
|
+
ctx.save();
|
|
254
|
+
ctx.globalAlpha = border.opacity;
|
|
255
|
+
ctx.lineWidth = border.width;
|
|
256
|
+
ctx.strokeStyle = border.color;
|
|
257
|
+
if (border.style === "gradient") {
|
|
258
|
+
const gradient = ctx.createLinearGradient(0, 0, spec.width, spec.height);
|
|
259
|
+
gradient.addColorStop(0, border.color);
|
|
260
|
+
gradient.addColorStop(1, border.endColor);
|
|
261
|
+
ctx.strokeStyle = gradient;
|
|
262
|
+
}
|
|
263
|
+
if (border.style === "dashed")
|
|
264
|
+
ctx.setLineDash([border.width * 4, border.width * 3]);
|
|
265
|
+
const outline = (gap) => {
|
|
266
|
+
ctx.beginPath();
|
|
267
|
+
ctx.roundRect(inset + gap, inset + gap, width - gap * 2, height - gap * 2, Math.max(0, border.radius - gap));
|
|
268
|
+
ctx.stroke();
|
|
269
|
+
};
|
|
270
|
+
if (border.style === "corners") {
|
|
271
|
+
const length = Math.min(width, height) * 0.12;
|
|
272
|
+
for (const x of [inset, spec.width - inset])
|
|
273
|
+
for (const y of [inset, spec.height - inset]) {
|
|
274
|
+
const dx = x === inset ? 1 : -1;
|
|
275
|
+
const dy = y === inset ? 1 : -1;
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
ctx.moveTo(x + dx * length, y);
|
|
278
|
+
ctx.lineTo(x, y);
|
|
279
|
+
ctx.lineTo(x, y + dy * length);
|
|
280
|
+
ctx.stroke();
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
outline(0);
|
|
284
|
+
if (border.style === "double")
|
|
285
|
+
outline(border.width * 3 + 4);
|
|
286
|
+
}
|
|
287
|
+
ctx.restore();
|
|
288
|
+
}
|
|
289
|
+
|
|
63
290
|
// src/core/canvas-draw.ts
|
|
64
291
|
function resolveCanvasFontFamily(fontName, isServer = false) {
|
|
65
292
|
const rawFirst = (fontName || "").split(",")[0] || "";
|
|
@@ -107,10 +334,13 @@ function resolveCanvasFont(fontDesc, isServer = false) {
|
|
|
107
334
|
async function drawPageToContext(ctx, options) {
|
|
108
335
|
const { page, spec, config, totalPageCount = 1, getImage, isServer = false } = options;
|
|
109
336
|
const { algorithm, theme, chrome } = config;
|
|
337
|
+
validateCanvasStyle(config.canvasStyle);
|
|
110
338
|
ctx.save();
|
|
111
339
|
ctx.fillStyle = theme.bgColor;
|
|
112
340
|
ctx.fillRect(0, 0, spec.width, spec.height);
|
|
113
341
|
ctx.restore();
|
|
342
|
+
if (config.canvasStyle?.fill)
|
|
343
|
+
drawCanvasFill(ctx, spec, config.canvasStyle.fill);
|
|
114
344
|
const coverTheme = config.coverTheme;
|
|
115
345
|
const drawCoverImage = async (image) => {
|
|
116
346
|
if (!image)
|
|
@@ -127,28 +357,30 @@ async function drawPageToContext(ctx, options) {
|
|
|
127
357
|
ctx.restore();
|
|
128
358
|
};
|
|
129
359
|
if (coverTheme) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
360
|
+
if (!config.canvasStyle?.fill) {
|
|
361
|
+
const fill = coverTheme.background.fill;
|
|
362
|
+
ctx.save();
|
|
363
|
+
if (fill.type === "solid")
|
|
364
|
+
ctx.fillStyle = fill.color;
|
|
365
|
+
else {
|
|
366
|
+
let gradient;
|
|
367
|
+
if (fill.type === "linear") {
|
|
368
|
+
const angle = fill.angle * Math.PI / 180;
|
|
369
|
+
const dx = Math.cos(angle) * spec.width / 2;
|
|
370
|
+
const dy = Math.sin(angle) * spec.height / 2;
|
|
371
|
+
gradient = ctx.createLinearGradient(spec.width / 2 - dx, spec.height / 2 - dy, spec.width / 2 + dx, spec.height / 2 + dy);
|
|
372
|
+
} else {
|
|
373
|
+
const x = spec.width * fill.position.x / 100;
|
|
374
|
+
const y = spec.height * fill.position.y / 100;
|
|
375
|
+
gradient = ctx.createRadialGradient(x, y, 0, x, y, Math.hypot(spec.width, spec.height));
|
|
376
|
+
}
|
|
377
|
+
for (const stop of fill.stops)
|
|
378
|
+
gradient.addColorStop(stop.offset / 100, stop.color);
|
|
379
|
+
ctx.fillStyle = gradient;
|
|
145
380
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
ctx.fillStyle = gradient;
|
|
381
|
+
ctx.fillRect(0, 0, spec.width, spec.height);
|
|
382
|
+
ctx.restore();
|
|
149
383
|
}
|
|
150
|
-
ctx.fillRect(0, 0, spec.width, spec.height);
|
|
151
|
-
ctx.restore();
|
|
152
384
|
await drawCoverImage(coverTheme.background.image);
|
|
153
385
|
if (coverTheme.background.overlay) {
|
|
154
386
|
ctx.save();
|
|
@@ -158,48 +390,52 @@ async function drawPageToContext(ctx, options) {
|
|
|
158
390
|
ctx.restore();
|
|
159
391
|
}
|
|
160
392
|
const decoration = coverTheme.decoration;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
393
|
+
if (!config.canvasStyle?.pattern) {
|
|
394
|
+
ctx.save();
|
|
395
|
+
ctx.strokeStyle = ctx.fillStyle = decoration.color;
|
|
396
|
+
ctx.globalAlpha = decoration.opacity;
|
|
397
|
+
const size = Math.max(8, decoration.size);
|
|
398
|
+
ctx.beginPath();
|
|
399
|
+
if (["lines", "grid", "dots"].includes(decoration.pattern)) {
|
|
400
|
+
for (let x = 0;x < spec.width; x += size) {
|
|
401
|
+
if (decoration.pattern === "grid") {
|
|
402
|
+
ctx.moveTo(x, 0);
|
|
403
|
+
ctx.lineTo(x, spec.height);
|
|
404
|
+
}
|
|
405
|
+
if (decoration.pattern === "dots") {
|
|
406
|
+
for (let y = 0;y < spec.height; y += size) {
|
|
407
|
+
ctx.moveTo(x + 2, y);
|
|
408
|
+
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
171
411
|
}
|
|
172
|
-
if (decoration.pattern
|
|
412
|
+
if (decoration.pattern !== "dots") {
|
|
173
413
|
for (let y = 0;y < spec.height; y += size) {
|
|
174
|
-
ctx.moveTo(
|
|
175
|
-
ctx.
|
|
414
|
+
ctx.moveTo(0, y);
|
|
415
|
+
ctx.lineTo(spec.width, y);
|
|
176
416
|
}
|
|
177
417
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
ctx.
|
|
182
|
-
|
|
418
|
+
} else if (decoration.pattern === "rings") {
|
|
419
|
+
for (let radius = size;radius < Math.hypot(spec.width, spec.height); radius += size) {
|
|
420
|
+
ctx.moveTo(spec.width / 2 + radius, spec.height / 2);
|
|
421
|
+
ctx.arc(spec.width / 2, spec.height / 2, radius, 0, Math.PI * 2);
|
|
422
|
+
}
|
|
423
|
+
} else if (decoration.pattern === "burst") {
|
|
424
|
+
for (let angle = 0;angle < Math.PI * 2; angle += Math.PI / Math.max(8, Math.round(size / 3))) {
|
|
425
|
+
ctx.moveTo(spec.width / 2, spec.height / 2);
|
|
426
|
+
ctx.lineTo(spec.width / 2 + Math.cos(angle) * spec.width, spec.height / 2 + Math.sin(angle) * spec.height);
|
|
183
427
|
}
|
|
184
428
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
ctx.
|
|
189
|
-
|
|
190
|
-
} else if (decoration.pattern === "burst") {
|
|
191
|
-
for (let angle = 0;angle < Math.PI * 2; angle += Math.PI / Math.max(8, Math.round(size / 3))) {
|
|
192
|
-
ctx.moveTo(spec.width / 2, spec.height / 2);
|
|
193
|
-
ctx.lineTo(spec.width / 2 + Math.cos(angle) * spec.width, spec.height / 2 + Math.sin(angle) * spec.height);
|
|
194
|
-
}
|
|
429
|
+
if (decoration.pattern === "dots")
|
|
430
|
+
ctx.fill();
|
|
431
|
+
else
|
|
432
|
+
ctx.stroke();
|
|
433
|
+
ctx.restore();
|
|
195
434
|
}
|
|
196
|
-
if (decoration.pattern === "dots")
|
|
197
|
-
ctx.fill();
|
|
198
|
-
else
|
|
199
|
-
ctx.stroke();
|
|
200
|
-
ctx.restore();
|
|
201
435
|
await drawCoverImage(decoration.image);
|
|
202
436
|
}
|
|
437
|
+
if (config.canvasStyle?.pattern)
|
|
438
|
+
drawCanvasPattern(ctx, spec, config.canvasStyle.pattern);
|
|
203
439
|
const { stageX, stageY } = computeStageDimensions(spec, algorithm.safeArea, algorithm.contentBottomGap);
|
|
204
440
|
const isCover = config.aspectRatio === "wechat-cover-2.35";
|
|
205
441
|
const showHeader = chrome?.showHeader ?? !isCover;
|
|
@@ -388,6 +624,7 @@ async function drawPageToContext(ctx, options) {
|
|
|
388
624
|
ctx.restore();
|
|
389
625
|
}
|
|
390
626
|
}
|
|
627
|
+
drawCanvasBorder(ctx, spec, config.canvasStyle?.border);
|
|
391
628
|
}
|
|
392
629
|
|
|
393
630
|
// src/core/data-dir.ts
|
|
@@ -456,4 +693,4 @@ async function renderTypesetResultToPngs(result) {
|
|
|
456
693
|
return outputs;
|
|
457
694
|
}
|
|
458
695
|
|
|
459
|
-
export { getCanvasSpec, computeStageDimensions, resolveCanvasFontFamily, resolveCanvasFont, drawPageToContext, resolveDataDir, renderPageToCanvas, renderPageToPng, renderTypesetResultToPngs };
|
|
696
|
+
export { getCanvasSpec, computeStageDimensions, DEFAULT_PATTERN, DEFAULT_BORDER, validateCanvasStyle, resolveCanvasFontFamily, resolveCanvasFont, drawPageToContext, resolveDataDir, renderPageToCanvas, renderPageToPng, renderTypesetResultToPngs };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_BORDER,
|
|
3
|
+
DEFAULT_PATTERN,
|
|
2
4
|
computeStageDimensions,
|
|
3
5
|
getCanvasSpec,
|
|
4
6
|
renderPageToPng,
|
|
5
7
|
renderTypesetResultToPngs,
|
|
6
|
-
resolveDataDir
|
|
7
|
-
|
|
8
|
+
resolveDataDir,
|
|
9
|
+
validateCanvasStyle
|
|
10
|
+
} from "./index-bf1zs3mv.js";
|
|
8
11
|
import {
|
|
9
12
|
buildLineTextFromRange,
|
|
10
13
|
getLineTextCache,
|
|
@@ -2487,6 +2490,7 @@ function runTypeset(params) {
|
|
|
2487
2490
|
throw new RangeError("Invalid page constraints: maxPages must be 0 or at least minPages");
|
|
2488
2491
|
}
|
|
2489
2492
|
validateTemplateAssets(images);
|
|
2493
|
+
validateCanvasStyle(config.canvasStyle);
|
|
2490
2494
|
if (images.some((image) => !image.repeatEveryPage && image.pageIndex != null && (!Number.isInteger(image.pageIndex) || image.pageIndex < 1 || image.pageIndex > 50))) {
|
|
2491
2495
|
throw new RangeError("Image pageIndex must be an integer between 1 and 50");
|
|
2492
2496
|
}
|
|
@@ -2614,6 +2618,57 @@ function runTypeset(params) {
|
|
|
2614
2618
|
// src/server/routes/templates.ts
|
|
2615
2619
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2616
2620
|
import { dirname, join } from "path";
|
|
2621
|
+
|
|
2622
|
+
// src/core/canvas-presets.ts
|
|
2623
|
+
var designs = [
|
|
2624
|
+
{ id: "sage", name: "浅草信笺 · 径向渐变 / 圆点", fill: { type: "radial", position: { x: 15, y: 10 }, stops: [{ color: "#f8f6e8", offset: 0 }, { color: "#d0e5d7", offset: 100 }] }, pattern: "dots", ink: "#243c32", accent: "#47785c", texture: "#47785c", border: "solid", size: 48, opacity: 0.16, font: "Songti SC" },
|
|
2625
|
+
{ id: "sunset", name: "落日来信 · 线性渐变 / 波浪", fill: { type: "linear", angle: 70, stops: [{ color: "#fff2d8", offset: 0 }, { color: "#f1c2a2", offset: 55 }, { color: "#df9b91", offset: 100 }] }, pattern: "waves", ink: "#663b32", accent: "#ab503d", texture: "#a55f4c", border: "gradient", size: 88, opacity: 0.12, font: "Kaiti SC" },
|
|
2626
|
+
{ id: "blueprint", name: "工程蓝图 · 深蓝渐变 / 网格", fill: { type: "linear", angle: 115, stops: [{ color: "#142f48", offset: 0 }, { color: "#071923", offset: 100 }] }, pattern: "grid", ink: "#e6f1ec", accent: "#a1d7c6", texture: "#91c3d0", border: "corners", size: 56, opacity: 0.12, font: "PingFang SC" },
|
|
2627
|
+
{ id: "eclipse", name: "月蚀光场 · 径向渐变 / 圆环", fill: { type: "radial", position: { x: 85, y: 12 }, stops: [{ color: "#53747a", offset: 0 }, { color: "#182e38", offset: 45 }, { color: "#0e1d27", offset: 100 }] }, pattern: "rings", ink: "#f1eddf", accent: "#ddba80", texture: "#e3c899", border: "gradient", size: 92, opacity: 0.1, font: "Songti SC" },
|
|
2628
|
+
{ id: "paper", name: "旧纸书页 · 纸张颗粒 / 双线框", fill: { type: "solid", color: "#f2ead8" }, pattern: "grain", ink: "#403b30", accent: "#8a503b", texture: "#6b533e", border: "double", size: 24, opacity: 0.16, font: "Songti SC" },
|
|
2629
|
+
{ id: "rose", name: "玫瑰邮笺 · 柔粉渐变 / 斜线", fill: { type: "linear", angle: 120, stops: [{ color: "#fff6ef", offset: 0 }, { color: "#efd6d4", offset: 100 }] }, pattern: "diagonal", ink: "#583f45", accent: "#9f5f68", texture: "#ae777a", border: "dashed", size: 64, opacity: 0.1, font: "Kaiti SC" },
|
|
2630
|
+
{ id: "checker", name: "黄油棋盘 · 奶油底色 / 棋盘格", fill: { type: "solid", color: "#faf2d9" }, pattern: "checker", ink: "#3c4937", accent: "#66784c", texture: "#b5bb89", border: "solid", size: 104, opacity: 0.1, font: "PingFang SC" },
|
|
2631
|
+
{ id: "burst", name: "日光放射 · 暖黄渐变 / 放射线", fill: { type: "radial", position: { x: 80, y: 20 }, stops: [{ color: "#fff7d1", offset: 0 }, { color: "#e8c982", offset: 100 }] }, pattern: "burst", ink: "#58452b", accent: "#976333", texture: "#aa7739", border: "corners", size: 100, opacity: 0.12, font: "PingFang SC" },
|
|
2632
|
+
{ id: "ruled", name: "雾蓝手账 · 浅蓝渐变 / 横线", fill: { type: "linear", angle: 90, stops: [{ color: "#f0f5f4", offset: 0 }, { color: "#d3e3e8", offset: 100 }] }, pattern: "lines", ink: "#304d5b", accent: "#4b7484", texture: "#7294a3", border: "double", size: 64, opacity: 0.14, font: "Kaiti SC" }
|
|
2633
|
+
];
|
|
2634
|
+
var CANVAS_PRESETS = designs.map((design) => {
|
|
2635
|
+
const base = getDefaultConfig("longform-3-4");
|
|
2636
|
+
return {
|
|
2637
|
+
...base,
|
|
2638
|
+
id: `tpl_canvas_${design.id}`,
|
|
2639
|
+
name: design.name,
|
|
2640
|
+
slot: "newspic_content",
|
|
2641
|
+
images: [],
|
|
2642
|
+
algorithm: {
|
|
2643
|
+
...base.algorithm,
|
|
2644
|
+
baseFontSize: 42,
|
|
2645
|
+
scaleRatio: 1.3,
|
|
2646
|
+
targetFillRatio: 0.83,
|
|
2647
|
+
safeArea: { top: 126, right: 108, bottom: 110, left: 108 }
|
|
2648
|
+
},
|
|
2649
|
+
typography: { headingFont: design.font, bodyFont: design.font },
|
|
2650
|
+
theme: {
|
|
2651
|
+
...base.theme,
|
|
2652
|
+
bgColor: design.fill.type === "solid" ? design.fill.color : design.fill.stops[0].color,
|
|
2653
|
+
textColor: design.ink,
|
|
2654
|
+
headingColor: design.ink,
|
|
2655
|
+
accentColor: design.accent,
|
|
2656
|
+
quoteColor: design.accent,
|
|
2657
|
+
captionColor: design.accent,
|
|
2658
|
+
watermarkColor: design.accent
|
|
2659
|
+
},
|
|
2660
|
+
chrome: { ...base.chrome, showHeader: false, showFooter: false },
|
|
2661
|
+
footerText: "",
|
|
2662
|
+
watermarkText: "",
|
|
2663
|
+
canvasStyle: {
|
|
2664
|
+
fill: design.fill,
|
|
2665
|
+
pattern: { ...DEFAULT_PATTERN, pattern: design.pattern, color: design.texture, size: design.size, opacity: design.opacity },
|
|
2666
|
+
border: { ...DEFAULT_BORDER, style: design.border, color: design.accent, endColor: design.texture, width: 2, inset: 38, radius: design.id === "paper" ? 0 : 22, opacity: 0.65 }
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
// src/server/routes/templates.ts
|
|
2617
2672
|
var TEMPLATES_DIR = join(resolveDataDir(), "templates");
|
|
2618
2673
|
function ensureTemplatesDir() {
|
|
2619
2674
|
if (!existsSync(TEMPLATES_DIR)) {
|
|
@@ -2628,6 +2683,17 @@ function ensureTemplatesDir() {
|
|
|
2628
2683
|
writeFileSync(seedMarker, "1");
|
|
2629
2684
|
} catch {}
|
|
2630
2685
|
}
|
|
2686
|
+
const canvasMarker = join(TEMPLATES_DIR, ".canvas-presets-v1");
|
|
2687
|
+
if (!existsSync(canvasMarker)) {
|
|
2688
|
+
try {
|
|
2689
|
+
for (const preset of CANVAS_PRESETS) {
|
|
2690
|
+
const path = join(TEMPLATES_DIR, `${preset.id}.json`);
|
|
2691
|
+
if (!existsSync(path))
|
|
2692
|
+
writeFileSync(path, JSON.stringify(preset, null, 2), "utf-8");
|
|
2693
|
+
}
|
|
2694
|
+
writeFileSync(canvasMarker, "1");
|
|
2695
|
+
} catch {}
|
|
2696
|
+
}
|
|
2631
2697
|
}
|
|
2632
2698
|
var BUILTIN_PRESETS = [
|
|
2633
2699
|
{
|
|
@@ -2937,7 +3003,8 @@ var BUILTIN_PRESETS = [
|
|
|
2937
3003
|
showFooterText: true,
|
|
2938
3004
|
showFillRatio: false
|
|
2939
3005
|
}
|
|
2940
|
-
}
|
|
3006
|
+
},
|
|
3007
|
+
...CANVAS_PRESETS
|
|
2941
3008
|
];
|
|
2942
3009
|
function seedDefaultTemplates() {
|
|
2943
3010
|
if (!existsSync(TEMPLATES_DIR))
|
|
@@ -3161,6 +3228,7 @@ templatesRouter.post("/", async (c) => {
|
|
|
3161
3228
|
}
|
|
3162
3229
|
try {
|
|
3163
3230
|
validateTemplateAssets(body.images);
|
|
3231
|
+
validateCanvasStyle(body.canvasStyle);
|
|
3164
3232
|
} catch (error) {
|
|
3165
3233
|
return c.json({ ok: false, error: error instanceof Error ? error.message : "无效的模板素材" }, 400);
|
|
3166
3234
|
}
|
|
@@ -103,8 +103,8 @@ async function renderRemoteBatch(studioUrl, body, outputDir, route, kind) {
|
|
|
103
103
|
}
|
|
104
104
|
async function renderLocalBatch(body, outputDir, route, kind) {
|
|
105
105
|
const [{ resolveTypesetResult }, { renderTypesetResultToPngs }] = await Promise.all([
|
|
106
|
-
import("./render-
|
|
107
|
-
import("./canvas-renderer-
|
|
106
|
+
import("./render-3043w6s6.js"),
|
|
107
|
+
import("./canvas-renderer-4jg80agt.js")
|
|
108
108
|
]);
|
|
109
109
|
const { result } = resolveTypesetResult(body);
|
|
110
110
|
const pngs = await renderTypesetResultToPngs(result);
|
|
@@ -131,7 +131,7 @@ async function renderLocalBatch(body, outputDir, route, kind) {
|
|
|
131
131
|
}
|
|
132
132
|
var typesetStudioAdapter = {
|
|
133
133
|
name: "typeset-studio",
|
|
134
|
-
version: "0.2.
|
|
134
|
+
version: "0.2.6",
|
|
135
135
|
async doctor() {
|
|
136
136
|
const checks = [];
|
|
137
137
|
const REMOTE_STUDIO_URL = remoteStudioUrl();
|
package/dist/lib/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
plugin,
|
|
3
3
|
typesetStudioAdapter
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-p8ttqbs6.js";
|
|
5
5
|
import {
|
|
6
6
|
applyTemplateImages,
|
|
7
7
|
getDefaultConfig,
|
|
8
8
|
resolveTypesetResult,
|
|
9
9
|
runTypeset,
|
|
10
10
|
validateTemplateAssets
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-hegsbxc0.js";
|
|
12
12
|
import {
|
|
13
13
|
drawPageToContext,
|
|
14
14
|
renderPageToCanvas,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
renderTypesetResultToPngs,
|
|
17
17
|
resolveCanvasFont,
|
|
18
18
|
resolveCanvasFontFamily
|
|
19
|
-
} from "./index-
|
|
19
|
+
} from "./index-bf1zs3mv.js";
|
|
20
20
|
import {
|
|
21
21
|
readFontFamilyName,
|
|
22
22
|
registerServerFonts,
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akairos/typeset-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Algorithmic typesetting studio & zzhub-pipeline image renderer plugin",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/k7airos/typeset-studio.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://github.com/
|
|
12
|
+
"homepage": "https://github.com/k7airos/typeset-studio#readme",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "./dist/lib/index.js",
|
package/src/adapter.ts
CHANGED
|
@@ -266,7 +266,7 @@ async function renderLocalBatch(
|
|
|
266
266
|
|
|
267
267
|
export const typesetStudioAdapter: ImageRenderPlugin = {
|
|
268
268
|
name: "typeset-studio",
|
|
269
|
-
version: "0.2.
|
|
269
|
+
version: "0.2.6",
|
|
270
270
|
|
|
271
271
|
async doctor(): Promise<PipelinePluginDoctorCheck[]> {
|
|
272
272
|
const checks: PipelinePluginDoctorCheck[] = [];
|