@sliiu/present-themes 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/builtin-ids.js +19 -0
- package/dist/cjs/decorations.js +303 -0
- package/dist/cjs/fonts/inject.js +160 -0
- package/dist/cjs/gallery-order.js +19 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/qa/color.js +98 -0
- package/dist/cjs/template-masters.js +882 -0
- package/dist/cjs/theme-commands.js +290 -0
- package/dist/cjs/themes.js +742 -0
- package/dist/esm/builtin-ids.js +16 -0
- package/dist/esm/decorations.js +300 -0
- package/dist/esm/fonts/inject.js +154 -0
- package/dist/esm/gallery-order.js +16 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/qa/color.js +88 -0
- package/dist/esm/template-masters.js +874 -0
- package/dist/esm/theme-commands.js +285 -0
- package/dist/esm/themes.js +736 -0
- package/dist/types/builtin-ids.d.ts +3 -0
- package/dist/types/decorations.d.ts +18 -0
- package/dist/types/fonts/inject.d.ts +27 -0
- package/dist/types/gallery-order.d.ts +3 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/qa/color.d.ts +27 -0
- package/dist/types/template-masters.d.ts +18 -0
- package/dist/types/theme-commands.d.ts +41 -0
- package/dist/types/themes.d.ts +276 -0
- package/package.json +27 -4
- package/README.md +0 -3
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApplyThemeCmd = exports.ApplyCustomThemeCmd = void 0;
|
|
4
|
+
const core_1 = require("@sliiu/present-core");
|
|
5
|
+
const template_masters_1 = require("./template-masters");
|
|
6
|
+
const themes_1 = require("./themes");
|
|
7
|
+
/** 示例封面 shape 的 id 前缀 —— 换主题时按前缀识别并整组重建。 */
|
|
8
|
+
const SAMPLE_COVER_ID_PREFIX = "sample_cover_";
|
|
9
|
+
/** ThemeDef.fonts 是 CSS font-stack,run.fontFamily 要单一 family 名。 */
|
|
10
|
+
function firstFamily(stack) {
|
|
11
|
+
const m = stack.trim().match(/^"([^"]+)"|^'([^']+)'|^([^,]+)/);
|
|
12
|
+
return (m?.[1] ?? m?.[2] ?? m?.[3] ?? stack).trim();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 按主题排印生成一组「主标题 + 副标题」示例,几何对齐 generate-template 的封面版式。
|
|
16
|
+
* 让用户在空白 deck 上套主题后能立刻看到该主题的标题字体/字号/艺术字效果。
|
|
17
|
+
*/
|
|
18
|
+
function buildSampleCoverShapes(theme, size, text) {
|
|
19
|
+
const W = size.width, H = size.height;
|
|
20
|
+
const mx = W * 0.08;
|
|
21
|
+
const wa = theme.titleWordArt;
|
|
22
|
+
const align = theme.layout === "centered" ? "middle" : "start";
|
|
23
|
+
const titleY = H * 0.36;
|
|
24
|
+
const titleRun = {
|
|
25
|
+
text: text.title,
|
|
26
|
+
fontFamily: firstFamily(theme.fonts.title),
|
|
27
|
+
fontSize: theme.fontSizes.coverTitle,
|
|
28
|
+
color: wa?.color ?? theme.palette.text,
|
|
29
|
+
fontWeight: wa?.weight ?? 700,
|
|
30
|
+
};
|
|
31
|
+
if (wa?.letterSpacing !== undefined)
|
|
32
|
+
titleRun.letterSpacing = wa.letterSpacing;
|
|
33
|
+
if (wa?.caps)
|
|
34
|
+
titleRun.caps = "all";
|
|
35
|
+
if (wa?.outlineColor && wa.outlineWidth) {
|
|
36
|
+
titleRun.textOutline = { paint: { kind: "solid", color: wa.outlineColor }, width: wa.outlineWidth };
|
|
37
|
+
}
|
|
38
|
+
const base = (id, name, bbox) => ({
|
|
39
|
+
id: `${SAMPLE_COVER_ID_PREFIX}${id}`,
|
|
40
|
+
kind: "text",
|
|
41
|
+
name,
|
|
42
|
+
bbox,
|
|
43
|
+
visible: true,
|
|
44
|
+
locked: false,
|
|
45
|
+
opacity: 1,
|
|
46
|
+
transform: (0, core_1.identityTransform)(),
|
|
47
|
+
});
|
|
48
|
+
const title = {
|
|
49
|
+
...base("title", "Cover Title", {
|
|
50
|
+
x: mx,
|
|
51
|
+
y: titleY,
|
|
52
|
+
w: W - mx * 2,
|
|
53
|
+
h: theme.fontSizes.coverTitle * 1.4,
|
|
54
|
+
}),
|
|
55
|
+
paragraphs: [{ runs: [titleRun], ...(align === "middle" ? { align } : {}) }],
|
|
56
|
+
};
|
|
57
|
+
if (wa?.shadow) {
|
|
58
|
+
title.shadow = {
|
|
59
|
+
kind: "outer",
|
|
60
|
+
blur: wa.shadow.blur,
|
|
61
|
+
dx: wa.shadow.dx,
|
|
62
|
+
dy: wa.shadow.dy,
|
|
63
|
+
color: wa.shadow.color,
|
|
64
|
+
alpha: wa.shadow.alpha,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const subtitle = {
|
|
68
|
+
...base("subtitle", "Cover Subtitle", {
|
|
69
|
+
x: mx,
|
|
70
|
+
y: titleY + theme.fontSizes.coverTitle * 1.5,
|
|
71
|
+
w: W - mx * 2,
|
|
72
|
+
h: theme.fontSizes.coverSubtitle * 1.6,
|
|
73
|
+
}),
|
|
74
|
+
paragraphs: [
|
|
75
|
+
{
|
|
76
|
+
runs: [
|
|
77
|
+
{
|
|
78
|
+
text: text.subtitle,
|
|
79
|
+
fontFamily: firstFamily(theme.fonts.body),
|
|
80
|
+
fontSize: theme.fontSizes.coverSubtitle,
|
|
81
|
+
color: theme.palette.textMuted,
|
|
82
|
+
fontWeight: 400,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
...(align === "middle" ? { align } : {}),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
return [title, subtitle];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 首页只有装饰(或只有上一次注入的示例封面)时,铺一组主题化的示例主副标题。
|
|
93
|
+
* 一旦用户在首页写过任何真实内容就完全不介入 —— 换主题绝不能吃掉用户的东西。
|
|
94
|
+
*/
|
|
95
|
+
function refreshSampleCover(slide, theme, size, text) {
|
|
96
|
+
const layer = slide.layers[0];
|
|
97
|
+
if (!layer)
|
|
98
|
+
return;
|
|
99
|
+
const authored = layer.shapes.filter((sh) => !sh.id.startsWith("dec_") && !sh.id.startsWith(SAMPLE_COVER_ID_PREFIX));
|
|
100
|
+
if (authored.length > 0)
|
|
101
|
+
return;
|
|
102
|
+
const kept = layer.shapes.filter((sh) => !sh.id.startsWith(SAMPLE_COVER_ID_PREFIX));
|
|
103
|
+
layer.shapes = [...kept, ...buildSampleCoverShapes(theme, size, text)];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 渲染优先画 `run.fill` 的纯色,`run.color` 只在没有填充时才用。
|
|
107
|
+
* 导入的正文几乎都带着 solid fill,只改 color 时画面上的字色不会变。
|
|
108
|
+
*/
|
|
109
|
+
function syncTextColorsForTheme(pres, palette) {
|
|
110
|
+
const textColor = palette.text;
|
|
111
|
+
const snaps = [];
|
|
112
|
+
const paintRun = (run) => {
|
|
113
|
+
snaps.push({ run, color: run.color, fill: run.fill });
|
|
114
|
+
run.color = textColor;
|
|
115
|
+
if (run.fill?.kind === "solid") {
|
|
116
|
+
run.fill = { ...run.fill, color: textColor };
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const walk = (shapes) => {
|
|
120
|
+
for (const s of shapes) {
|
|
121
|
+
if (s.kind === "text") {
|
|
122
|
+
for (const para of s.paragraphs) {
|
|
123
|
+
for (const run of para.runs)
|
|
124
|
+
paintRun(run);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (s.kind === "group") {
|
|
128
|
+
walk(s.children);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
for (const slide of pres.slides) {
|
|
133
|
+
for (const layer of slide.layers)
|
|
134
|
+
walk(layer.shapes);
|
|
135
|
+
}
|
|
136
|
+
return snaps;
|
|
137
|
+
}
|
|
138
|
+
function restoreTextColors(snaps) {
|
|
139
|
+
if (!snaps)
|
|
140
|
+
return;
|
|
141
|
+
for (const snap of snaps) {
|
|
142
|
+
if (snap.color === undefined)
|
|
143
|
+
delete snap.run.color;
|
|
144
|
+
else
|
|
145
|
+
snap.run.color = snap.color;
|
|
146
|
+
if (snap.fill === undefined)
|
|
147
|
+
delete snap.run.fill;
|
|
148
|
+
else
|
|
149
|
+
snap.run.fill = snap.fill;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
class ApplyCustomThemeCmd {
|
|
153
|
+
theme;
|
|
154
|
+
opts;
|
|
155
|
+
label = "Apply custom theme";
|
|
156
|
+
prev;
|
|
157
|
+
prevMasters;
|
|
158
|
+
prevThemeMeta;
|
|
159
|
+
prevTextColors;
|
|
160
|
+
constructor(theme, opts = {}) {
|
|
161
|
+
this.theme = theme;
|
|
162
|
+
this.opts = opts;
|
|
163
|
+
}
|
|
164
|
+
apply(pres) {
|
|
165
|
+
const theme = this.theme;
|
|
166
|
+
const preserveImg = this.opts.preserveImageBackgrounds !== false;
|
|
167
|
+
const size = pres.size ?? { width: 1280, height: 720 };
|
|
168
|
+
const baseId = `applied:${theme.id}`;
|
|
169
|
+
this.prevMasters = pres.masters
|
|
170
|
+
? (typeof structuredClone === "function"
|
|
171
|
+
? structuredClone(pres.masters)
|
|
172
|
+
: JSON.parse(JSON.stringify(pres.masters)))
|
|
173
|
+
: undefined;
|
|
174
|
+
this.prev = pres.slides.map((s) => ({
|
|
175
|
+
slide: s,
|
|
176
|
+
bg: s.background,
|
|
177
|
+
bgRef: s.bgRef,
|
|
178
|
+
shapes: s.layers[0] ? [...s.layers[0].shapes] : [],
|
|
179
|
+
layoutRef: s.layoutRef ? { ...s.layoutRef } : undefined,
|
|
180
|
+
}));
|
|
181
|
+
this.prevThemeMeta = pres.theme ? { ...pres.theme } : undefined;
|
|
182
|
+
const { masters, coreTheme } = (0, template_masters_1.buildMastersFromThemeDef)(theme, size, baseId);
|
|
183
|
+
pres.masters = masters;
|
|
184
|
+
for (const [idx, slide] of pres.slides.entries()) {
|
|
185
|
+
(0, template_masters_1.stripSlideThemeDecorations)(slide);
|
|
186
|
+
const layoutType = (0, template_masters_1.resolveLayoutTypeForSlide)(slide, idx, this.prevMasters);
|
|
187
|
+
const ref = (0, template_masters_1.findLayoutRefByType)(masters, layoutType) ??
|
|
188
|
+
(0, template_masters_1.findLayoutRefByType)(masters, idx === 0 ? "title" : "obj");
|
|
189
|
+
if (ref)
|
|
190
|
+
slide.layoutRef = ref;
|
|
191
|
+
const skip = preserveImg && slide.background?.kind === "image";
|
|
192
|
+
if (!skip)
|
|
193
|
+
(0, core_1.assignSlideBackground)(slide, undefined);
|
|
194
|
+
}
|
|
195
|
+
pres.theme = {
|
|
196
|
+
...coreTheme,
|
|
197
|
+
colors: {
|
|
198
|
+
...coreTheme.colors,
|
|
199
|
+
bg1: theme.palette.bg,
|
|
200
|
+
bg2: theme.palette.bgAlt,
|
|
201
|
+
tx1: theme.palette.text,
|
|
202
|
+
tx2: theme.palette.textMuted,
|
|
203
|
+
accent1: theme.palette.accent1,
|
|
204
|
+
accent2: theme.palette.accent2,
|
|
205
|
+
accent3: theme.palette.accent3,
|
|
206
|
+
accent4: theme.palette.accent4,
|
|
207
|
+
},
|
|
208
|
+
fonts: { major: theme.fonts.title, minor: theme.fonts.body },
|
|
209
|
+
};
|
|
210
|
+
this.prevTextColors = syncTextColorsForTheme(pres, theme.palette);
|
|
211
|
+
// 必须排在 syncTextColorsForTheme 之后:那一步会把所有 run 刷成 palette.text,
|
|
212
|
+
// 先注入的话副标题的弱化色和标题艺术字色都会被抹平。
|
|
213
|
+
const first = pres.slides[0];
|
|
214
|
+
if (this.opts.sampleCover && first) {
|
|
215
|
+
refreshSampleCover(first, theme, size, this.opts.sampleCover);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
revert(pres) {
|
|
219
|
+
if (!this.prev)
|
|
220
|
+
return;
|
|
221
|
+
for (const { slide, bg, bgRef, shapes, layoutRef } of this.prev) {
|
|
222
|
+
slide.background = bg;
|
|
223
|
+
if (bgRef)
|
|
224
|
+
slide.bgRef = bgRef;
|
|
225
|
+
else
|
|
226
|
+
delete slide.bgRef;
|
|
227
|
+
slide.layoutRef = layoutRef;
|
|
228
|
+
const layer = slide.layers[0];
|
|
229
|
+
if (layer)
|
|
230
|
+
layer.shapes = shapes;
|
|
231
|
+
}
|
|
232
|
+
pres.masters = this.prevMasters;
|
|
233
|
+
pres.theme = this.prevThemeMeta;
|
|
234
|
+
restoreTextColors(this.prevTextColors);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
exports.ApplyCustomThemeCmd = ApplyCustomThemeCmd;
|
|
238
|
+
class ApplyThemeCmd {
|
|
239
|
+
themeId;
|
|
240
|
+
opts;
|
|
241
|
+
label = "Apply theme";
|
|
242
|
+
prev;
|
|
243
|
+
prevThemeMeta;
|
|
244
|
+
prevTextColors;
|
|
245
|
+
constructor(themeId, opts = {}) {
|
|
246
|
+
this.themeId = themeId;
|
|
247
|
+
this.opts = opts;
|
|
248
|
+
}
|
|
249
|
+
apply(pres) {
|
|
250
|
+
const theme = (0, themes_1.getTheme)(this.themeId);
|
|
251
|
+
const preserveImg = this.opts.preserveImageBackgrounds !== false;
|
|
252
|
+
this.prev = pres.slides.map((s) => ({ slide: s, bg: s.background, bgRef: s.bgRef }));
|
|
253
|
+
this.prevThemeMeta = pres.theme ? { ...pres.theme } : undefined;
|
|
254
|
+
for (const slide of pres.slides) {
|
|
255
|
+
if (preserveImg && slide.background?.kind === "image")
|
|
256
|
+
continue;
|
|
257
|
+
(0, core_1.assignSlideBackground)(slide, { kind: "solid", color: theme.palette.bg });
|
|
258
|
+
}
|
|
259
|
+
pres.theme = {
|
|
260
|
+
id: theme.id,
|
|
261
|
+
name: theme.name,
|
|
262
|
+
colors: {
|
|
263
|
+
bg1: theme.palette.bg,
|
|
264
|
+
bg2: theme.palette.bgAlt,
|
|
265
|
+
tx1: theme.palette.text,
|
|
266
|
+
tx2: theme.palette.textMuted,
|
|
267
|
+
accent1: theme.palette.accent1,
|
|
268
|
+
accent2: theme.palette.accent2,
|
|
269
|
+
accent3: theme.palette.accent3,
|
|
270
|
+
accent4: theme.palette.accent4,
|
|
271
|
+
},
|
|
272
|
+
fonts: { major: theme.fonts.title, minor: theme.fonts.body },
|
|
273
|
+
};
|
|
274
|
+
this.prevTextColors = syncTextColorsForTheme(pres, theme.palette);
|
|
275
|
+
}
|
|
276
|
+
revert(pres) {
|
|
277
|
+
if (!this.prev)
|
|
278
|
+
return;
|
|
279
|
+
for (const { slide, bg, bgRef } of this.prev) {
|
|
280
|
+
slide.background = bg;
|
|
281
|
+
if (bgRef)
|
|
282
|
+
slide.bgRef = bgRef;
|
|
283
|
+
else
|
|
284
|
+
delete slide.bgRef;
|
|
285
|
+
}
|
|
286
|
+
pres.theme = this.prevThemeMeta;
|
|
287
|
+
restoreTextColors(this.prevTextColors);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
exports.ApplyThemeCmd = ApplyThemeCmd;
|