@open-aippt/core 1.13.2
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/LICENSE +21 -0
- package/README.md +98 -0
- package/bin.js +2 -0
- package/dist/build-DxTqmvsO.js +17 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +86 -0
- package/dist/config-CjzqjrEA.js +4280 -0
- package/dist/config-DIC-yVPp.d.ts +23 -0
- package/dist/design-cpzS8aud.js +35 -0
- package/dist/dev-BYuTeJbA.js +20 -0
- package/dist/format-BCeKbTOM.js +1605 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +467 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +3 -0
- package/dist/preview-DlQvnJPq.js +18 -0
- package/dist/sync-BPZ0m27m.js +139 -0
- package/dist/sync-EsYusbbL.js +3 -0
- package/dist/types-CHmFPIG_.d.ts +430 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +4 -0
- package/env.d.ts +59 -0
- package/package.json +103 -0
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +91 -0
- package/skills/create-theme/SKILL.md +250 -0
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +625 -0
- package/src/app/app.tsx +47 -0
- package/src/app/components/asset-view.tsx +966 -0
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +243 -0
- package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
- package/src/app/components/inspector/comment-widget.tsx +93 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +387 -0
- package/src/app/components/inspector/inspector-panel.tsx +1115 -0
- package/src/app/components/inspector/inspector-provider.tsx +1218 -0
- package/src/app/components/inspector/save-bar.tsx +48 -0
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/notes-drawer.tsx +120 -0
- package/src/app/components/overview-grid.tsx +363 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +80 -0
- package/src/app/components/panel/save-card.tsx +142 -0
- package/src/app/components/pdf-progress-toast.tsx +32 -0
- package/src/app/components/player.tsx +466 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +315 -0
- package/src/app/components/present/help-overlay.tsx +57 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +39 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +46 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +66 -0
- package/src/app/components/present/use-touch-swipe.ts +66 -0
- package/src/app/components/shared-element.tsx +48 -0
- package/src/app/components/sidebar/folder-item.tsx +258 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
- package/src/app/components/sidebar/sidebar.tsx +284 -0
- package/src/app/components/slide-canvas.tsx +102 -0
- package/src/app/components/slide-transition-layer.tsx +844 -0
- package/src/app/components/style-panel/design-provider.tsx +148 -0
- package/src/app/components/style-panel/style-panel.tsx +349 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +59 -0
- package/src/app/components/themes/theme-detail.tsx +305 -0
- package/src/app/components/themes/themes-gallery.tsx +149 -0
- package/src/app/components/thumbnail-rail.tsx +805 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +99 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/components/ui/dialog.tsx +157 -0
- package/src/app/components/ui/dropdown-menu.tsx +245 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/popover.tsx +75 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +196 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +58 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/assets.ts +242 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/export-html.ts +326 -0
- package/src/app/lib/export-pdf.ts +298 -0
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +85 -0
- package/src/app/lib/inspector/use-comments.ts +74 -0
- package/src/app/lib/inspector/use-editor.ts +73 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/print-ready.test.ts +32 -0
- package/src/app/lib/print-ready.ts +51 -0
- package/src/app/lib/sdk.test.ts +13 -0
- package/src/app/lib/sdk.ts +37 -0
- package/src/app/lib/slides.ts +26 -0
- package/src/app/lib/step-context.tsx +261 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/transition.ts +30 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-click-page-navigation.ts +60 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/lib/use-wheel-page-navigation.ts +99 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +213 -0
- package/src/app/routes/home.tsx +807 -0
- package/src/app/routes/presenter.tsx +418 -0
- package/src/app/routes/slide.tsx +1108 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +429 -0
- package/src/app/virtual.d.ts +51 -0
- package/src/locale/en.ts +416 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +422 -0
- package/src/locale/types.ts +443 -0
- package/src/locale/zh-cn.ts +414 -0
- package/src/locale/zh-tw.ts +414 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
3
|
+
import { designToCssVars } from './design';
|
|
4
|
+
import { SlidePageProvider } from './page-context';
|
|
5
|
+
import { isFrameAnimationSettled, waitForDataWaitfor, waitForFonts } from './print-ready';
|
|
6
|
+
import type { SlideModule } from './sdk';
|
|
7
|
+
|
|
8
|
+
const PRINT_ROOT_ID = 'os-print-root';
|
|
9
|
+
const PRINT_STYLE_ID = 'os-print-style';
|
|
10
|
+
|
|
11
|
+
const PRINT_STYLES = `
|
|
12
|
+
@page { size: 1920px 1080px; margin: 0; }
|
|
13
|
+
|
|
14
|
+
@media screen {
|
|
15
|
+
#${PRINT_ROOT_ID} {
|
|
16
|
+
position: fixed !important;
|
|
17
|
+
left: -99999px !important;
|
|
18
|
+
top: 0 !important;
|
|
19
|
+
pointer-events: none !important;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media print {
|
|
24
|
+
html, body {
|
|
25
|
+
margin: 0 !important;
|
|
26
|
+
padding: 0 !important;
|
|
27
|
+
background: #fff !important;
|
|
28
|
+
}
|
|
29
|
+
body > *:not(#${PRINT_ROOT_ID}) { display: none !important; }
|
|
30
|
+
#${PRINT_ROOT_ID} {
|
|
31
|
+
position: static !important;
|
|
32
|
+
left: 0 !important;
|
|
33
|
+
top: 0 !important;
|
|
34
|
+
pointer-events: auto !important;
|
|
35
|
+
display: block !important;
|
|
36
|
+
background: #fff !important;
|
|
37
|
+
}
|
|
38
|
+
#${PRINT_ROOT_ID} .os-print-frame {
|
|
39
|
+
width: 1920px !important;
|
|
40
|
+
height: 1080px !important;
|
|
41
|
+
background: #fff;
|
|
42
|
+
color: #000;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
page-break-after: always;
|
|
45
|
+
break-after: page;
|
|
46
|
+
-webkit-print-color-adjust: exact;
|
|
47
|
+
print-color-adjust: exact;
|
|
48
|
+
}
|
|
49
|
+
#${PRINT_ROOT_ID} .os-print-frame:last-child {
|
|
50
|
+
page-break-after: auto;
|
|
51
|
+
break-after: auto;
|
|
52
|
+
}
|
|
53
|
+
/* Supersample: Chrome rasterizes filtered/composited layers (e.g. filter:
|
|
54
|
+
blur, mix-blend-mode) at the layer's CSS-pixel size, so a blurred
|
|
55
|
+
gradient on a 1920×1080 page bakes in at ~1× DPI and bands when the PDF
|
|
56
|
+
is viewed scaled up. zoom:2 doubles the layer raster size; scale(0.5)
|
|
57
|
+
composites it back to 1920×1080. Vector content (text, plain CSS
|
|
58
|
+
gradients, SVG) stays vector through both transforms. */
|
|
59
|
+
#${PRINT_ROOT_ID} .os-print-supersample {
|
|
60
|
+
width: 1920px !important;
|
|
61
|
+
height: 1080px !important;
|
|
62
|
+
zoom: 2;
|
|
63
|
+
transform: scale(0.5);
|
|
64
|
+
transform-origin: top left;
|
|
65
|
+
}
|
|
66
|
+
/* Chromium serializes box-shadow and CSS gradients as PDF transparency
|
|
67
|
+
groups / soft masks. macOS Preview re-composites those on every page
|
|
68
|
+
turn, causing 0.5–2s per-page lag. Strip them in the print container
|
|
69
|
+
only — gradients on pseudo-elements via CSS (DOM walk can't reach them),
|
|
70
|
+
inline-style gradients via neutralizeGradientBackgrounds() below. */
|
|
71
|
+
#${PRINT_ROOT_ID} *,
|
|
72
|
+
#${PRINT_ROOT_ID} *::before,
|
|
73
|
+
#${PRINT_ROOT_ID} *::after {
|
|
74
|
+
box-shadow: none !important;
|
|
75
|
+
}
|
|
76
|
+
#${PRINT_ROOT_ID} *::before,
|
|
77
|
+
#${PRINT_ROOT_ID} *::after {
|
|
78
|
+
background-image: none !important;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
export function isSafari(): boolean {
|
|
84
|
+
if (typeof navigator === 'undefined') return false;
|
|
85
|
+
const ua = navigator.userAgent;
|
|
86
|
+
return /Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR|Firefox/.test(ua);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type PdfExportProgress = {
|
|
90
|
+
phase: 'processing' | 'printing' | 'done';
|
|
91
|
+
/** Number of pages whose intro animations have finished (0..total). */
|
|
92
|
+
current: number;
|
|
93
|
+
total: number;
|
|
94
|
+
/** 0–99 while processing, 99 during printing, 100 when done. */
|
|
95
|
+
percent: number;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const ANIMATION_TIMEOUT_MS = 15_000;
|
|
99
|
+
const POLL_INTERVAL_MS = 100;
|
|
100
|
+
|
|
101
|
+
export async function exportSlideAsPdf(
|
|
102
|
+
slide: SlideModule,
|
|
103
|
+
slideId: string,
|
|
104
|
+
onProgress?: (progress: PdfExportProgress) => void,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const pages = slide.default ?? [];
|
|
107
|
+
if (pages.length === 0) return;
|
|
108
|
+
|
|
109
|
+
const total = pages.length;
|
|
110
|
+
|
|
111
|
+
const style = document.createElement('style');
|
|
112
|
+
style.id = PRINT_STYLE_ID;
|
|
113
|
+
style.textContent = PRINT_STYLES;
|
|
114
|
+
document.head.appendChild(style);
|
|
115
|
+
|
|
116
|
+
const root = document.createElement('div');
|
|
117
|
+
root.id = PRINT_ROOT_ID;
|
|
118
|
+
root.setAttribute('aria-hidden', 'true');
|
|
119
|
+
document.body.appendChild(root);
|
|
120
|
+
|
|
121
|
+
onProgress?.({ phase: 'processing', current: 0, total, percent: 0 });
|
|
122
|
+
|
|
123
|
+
const designVars = slide.design ? designToCssVars(slide.design) : null;
|
|
124
|
+
|
|
125
|
+
const reactRoots: Root[] = [];
|
|
126
|
+
const frames: HTMLElement[] = [];
|
|
127
|
+
for (let i = 0; i < pages.length; i++) {
|
|
128
|
+
const Page = pages[i];
|
|
129
|
+
if (!Page) continue;
|
|
130
|
+
const host = document.createElement('div');
|
|
131
|
+
host.className = 'os-print-frame';
|
|
132
|
+
host.setAttribute('data-osd-canvas', '');
|
|
133
|
+
host.style.width = '1920px';
|
|
134
|
+
host.style.height = '1080px';
|
|
135
|
+
if (designVars) {
|
|
136
|
+
for (const [k, v] of Object.entries(designVars)) host.style.setProperty(k, v);
|
|
137
|
+
}
|
|
138
|
+
const inner = document.createElement('div');
|
|
139
|
+
inner.className = 'os-print-supersample';
|
|
140
|
+
inner.style.width = '1920px';
|
|
141
|
+
inner.style.height = '1080px';
|
|
142
|
+
host.appendChild(inner);
|
|
143
|
+
root.appendChild(host);
|
|
144
|
+
frames.push(host);
|
|
145
|
+
const r = createRoot(inner);
|
|
146
|
+
r.render(
|
|
147
|
+
createElement(SlidePageProvider, { index: i, total: pages.length }, createElement(Page)),
|
|
148
|
+
);
|
|
149
|
+
reactRoots.push(r);
|
|
150
|
+
}
|
|
151
|
+
// Yield once so React commits all pages and CSS animations actually start
|
|
152
|
+
// (queued via Web Animations API on the first paint after mount).
|
|
153
|
+
await nextPaint();
|
|
154
|
+
|
|
155
|
+
const previousTitle = document.title;
|
|
156
|
+
document.title = slide.meta?.title ?? slideId;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await waitForFonts();
|
|
160
|
+
|
|
161
|
+
// Poll per-page animation completion. The bar tracks how many pages have
|
|
162
|
+
// settled, which matches "page X of N is being processed" mental model.
|
|
163
|
+
const deadline = performance.now() + ANIMATION_TIMEOUT_MS;
|
|
164
|
+
while (performance.now() < deadline) {
|
|
165
|
+
const settled = frames.reduce((n, frame) => (isFrameAnimationSettled(frame) ? n + 1 : n), 0);
|
|
166
|
+
onProgress?.({
|
|
167
|
+
phase: 'processing',
|
|
168
|
+
current: settled,
|
|
169
|
+
total,
|
|
170
|
+
percent: Math.min(99, (settled / total) * 99),
|
|
171
|
+
});
|
|
172
|
+
if (settled === total) break;
|
|
173
|
+
await sleep(POLL_INTERVAL_MS);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await waitForDataWaitfor(root);
|
|
177
|
+
neutralizeGradientBackgrounds(root);
|
|
178
|
+
await sleep(100); // flush layout
|
|
179
|
+
|
|
180
|
+
onProgress?.({ phase: 'printing', current: total, total, percent: 99 });
|
|
181
|
+
const printDone = waitForAfterPrint();
|
|
182
|
+
window.print();
|
|
183
|
+
await printDone;
|
|
184
|
+
} finally {
|
|
185
|
+
onProgress?.({ phase: 'done', current: total, total, percent: 100 });
|
|
186
|
+
document.title = previousTitle;
|
|
187
|
+
for (const r of reactRoots) r.unmount();
|
|
188
|
+
root.remove();
|
|
189
|
+
style.remove();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Strip inline-style gradients from background-image so Chromium does not
|
|
194
|
+
// emit them as PDF soft masks. url(...) backgrounds are preserved.
|
|
195
|
+
function neutralizeGradientBackgrounds(root: HTMLElement): void {
|
|
196
|
+
const elements = root.querySelectorAll<HTMLElement>('*');
|
|
197
|
+
for (const el of elements) {
|
|
198
|
+
const styles = getComputedStyle(el);
|
|
199
|
+
const bg = styles.backgroundImage;
|
|
200
|
+
if (!bg?.includes('gradient(')) continue;
|
|
201
|
+
|
|
202
|
+
const result = removeGradientBackgroundLayers(bg);
|
|
203
|
+
const size = styles.backgroundSize;
|
|
204
|
+
const position = styles.backgroundPosition;
|
|
205
|
+
const repeat = styles.backgroundRepeat;
|
|
206
|
+
|
|
207
|
+
el.style.backgroundImage = result.backgroundImage;
|
|
208
|
+
if (result.keptIndices.length === 0 || result.keptIndices.length === result.layerCount)
|
|
209
|
+
continue;
|
|
210
|
+
|
|
211
|
+
el.style.backgroundSize = reindexBackgroundLayerValues(size, result.keptIndices);
|
|
212
|
+
el.style.backgroundPosition = reindexBackgroundLayerValues(position, result.keptIndices);
|
|
213
|
+
el.style.backgroundRepeat = reindexBackgroundLayerValues(repeat, result.keptIndices);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function removeGradientBackgroundLayers(backgroundImage: string): {
|
|
218
|
+
backgroundImage: string;
|
|
219
|
+
keptIndices: number[];
|
|
220
|
+
layerCount: number;
|
|
221
|
+
} {
|
|
222
|
+
const layers = splitBackgroundImageLayers(backgroundImage);
|
|
223
|
+
const keptLayers: string[] = [];
|
|
224
|
+
const keptIndices: number[] = [];
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < layers.length; i++) {
|
|
227
|
+
const layer = layers[i];
|
|
228
|
+
if (!layer) continue;
|
|
229
|
+
const value = layer.trim();
|
|
230
|
+
if (value.startsWith('url(') && !value.includes('gradient(')) {
|
|
231
|
+
keptLayers.push(value);
|
|
232
|
+
keptIndices.push(i);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
backgroundImage: keptLayers.length > 0 ? keptLayers.join(', ') : 'none',
|
|
238
|
+
keptIndices,
|
|
239
|
+
layerCount: layers.length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function reindexBackgroundLayerValues(value: string, keptIndices: number[]): string {
|
|
244
|
+
const layers = splitBackgroundImageLayers(value);
|
|
245
|
+
if (layers.length === 0) return value;
|
|
246
|
+
|
|
247
|
+
return keptIndices.map((index) => layers[index % layers.length]).join(', ');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function splitBackgroundImageLayers(backgroundImage: string): string[] {
|
|
251
|
+
const layers: string[] = [];
|
|
252
|
+
let depth = 0;
|
|
253
|
+
let layerStart = 0;
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < backgroundImage.length; i++) {
|
|
256
|
+
const char = backgroundImage[i];
|
|
257
|
+
if (char === '(') depth++;
|
|
258
|
+
if (char === ')') depth = Math.max(0, depth - 1);
|
|
259
|
+
if (char === ',' && depth === 0) {
|
|
260
|
+
layers.push(backgroundImage.slice(layerStart, i).trim());
|
|
261
|
+
layerStart = i + 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
layers.push(backgroundImage.slice(layerStart).trim());
|
|
266
|
+
return layers;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sleep(ms: number): Promise<void> {
|
|
270
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function nextPaint(): Promise<void> {
|
|
274
|
+
// rAF in real tabs; setTimeout fallback for hidden/throttled headless tabs.
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
let settled = false;
|
|
277
|
+
const settle = () => {
|
|
278
|
+
if (settled) return;
|
|
279
|
+
settled = true;
|
|
280
|
+
resolve();
|
|
281
|
+
};
|
|
282
|
+
requestAnimationFrame(settle);
|
|
283
|
+
setTimeout(settle, 50);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function waitForAfterPrint(timeoutMs = 60_000): Promise<void> {
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
const cleanup = () => {
|
|
290
|
+
window.removeEventListener('afterprint', onAfter);
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
resolve();
|
|
293
|
+
};
|
|
294
|
+
const onAfter = () => cleanup();
|
|
295
|
+
const timer = setTimeout(cleanup, timeoutMs);
|
|
296
|
+
window.addEventListener('afterprint', onAfter, { once: true });
|
|
297
|
+
});
|
|
298
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
3
|
+
import { designToCssVars } from './design';
|
|
4
|
+
import { SlidePageProvider } from './page-context';
|
|
5
|
+
import { isFrameAnimationSettled, waitForDataWaitfor, waitForFonts } from './print-ready';
|
|
6
|
+
import type { SlideModule } from './sdk';
|
|
7
|
+
|
|
8
|
+
const SLIDE_W = 1920;
|
|
9
|
+
const SLIDE_H = 1080;
|
|
10
|
+
// 16:9 widescreen in English Metric Units (914400 EMU per inch → 13.333in × 7.5in).
|
|
11
|
+
const EMU_W = 12192000;
|
|
12
|
+
const EMU_H = 6858000;
|
|
13
|
+
const CAPTURE_PIXEL_RATIO = 2;
|
|
14
|
+
|
|
15
|
+
const ANIMATION_TIMEOUT_MS = 15_000;
|
|
16
|
+
const POLL_INTERVAL_MS = 100;
|
|
17
|
+
|
|
18
|
+
const CAPTURE_CLASS = 'os-pptx-capture';
|
|
19
|
+
const CAPTURE_STYLE_ID = 'os-pptx-capture-style';
|
|
20
|
+
// Properties intro animations drive from a hidden start state to a visible end
|
|
21
|
+
// state. We read them back once settled and pin them inline so the capture clone
|
|
22
|
+
// can't re-run the keyframes from their invisible 0% frame (see freezeForCapture).
|
|
23
|
+
const FROZEN_PROPS = ['opacity', 'transform', 'filter', 'clip-path'] as const;
|
|
24
|
+
|
|
25
|
+
export type PptxExportProgress = {
|
|
26
|
+
phase: 'processing' | 'generating' | 'done';
|
|
27
|
+
/** Number of pages captured so far (0..total). */
|
|
28
|
+
current: number;
|
|
29
|
+
total: number;
|
|
30
|
+
/** 0–95 while capturing, 98 while assembling, 100 when done. */
|
|
31
|
+
percent: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function exportSlideAsImagePptx(
|
|
35
|
+
slide: SlideModule,
|
|
36
|
+
slideId: string,
|
|
37
|
+
onProgress?: (progress: PptxExportProgress) => void,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const pages = slide.default ?? [];
|
|
40
|
+
if (pages.length === 0) return;
|
|
41
|
+
|
|
42
|
+
const total = pages.length;
|
|
43
|
+
onProgress?.({ phase: 'processing', current: 0, total, percent: 0 });
|
|
44
|
+
|
|
45
|
+
const container = document.createElement('div');
|
|
46
|
+
container.className = CAPTURE_CLASS;
|
|
47
|
+
container.setAttribute('aria-hidden', 'true');
|
|
48
|
+
Object.assign(container.style, {
|
|
49
|
+
position: 'fixed',
|
|
50
|
+
left: '-99999px',
|
|
51
|
+
top: '0',
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
});
|
|
54
|
+
document.body.appendChild(container);
|
|
55
|
+
|
|
56
|
+
// html-to-image clones each frame and copies its computed style — including the
|
|
57
|
+
// intro animation — into the clone, which then re-runs the keyframes from their
|
|
58
|
+
// hidden 0% frame in the rasterised SVG. Fast-forward every animation to its end
|
|
59
|
+
// frame in the live DOM (a large negative delay lands past a 1ms duration, so
|
|
60
|
+
// even pseudo-elements paint their final state on the first frame).
|
|
61
|
+
const captureStyle = document.createElement('style');
|
|
62
|
+
captureStyle.id = CAPTURE_STYLE_ID;
|
|
63
|
+
captureStyle.textContent = `.${CAPTURE_CLASS} *, .${CAPTURE_CLASS} *::before, .${CAPTURE_CLASS} *::after {
|
|
64
|
+
animation-delay: -1s !important;
|
|
65
|
+
animation-duration: 1ms !important;
|
|
66
|
+
animation-iteration-count: 1 !important;
|
|
67
|
+
animation-fill-mode: forwards !important;
|
|
68
|
+
transition: none !important;
|
|
69
|
+
}`;
|
|
70
|
+
document.head.appendChild(captureStyle);
|
|
71
|
+
|
|
72
|
+
const designVars = slide.design ? designToCssVars(slide.design) : null;
|
|
73
|
+
|
|
74
|
+
const reactRoots: Root[] = [];
|
|
75
|
+
const frames: HTMLElement[] = [];
|
|
76
|
+
for (let i = 0; i < pages.length; i++) {
|
|
77
|
+
const Page = pages[i];
|
|
78
|
+
if (!Page) continue;
|
|
79
|
+
const host = document.createElement('div');
|
|
80
|
+
host.setAttribute('data-osd-canvas', '');
|
|
81
|
+
host.style.width = `${SLIDE_W}px`;
|
|
82
|
+
host.style.height = `${SLIDE_H}px`;
|
|
83
|
+
host.style.overflow = 'hidden';
|
|
84
|
+
host.style.background = '#fff';
|
|
85
|
+
if (designVars) {
|
|
86
|
+
for (const [k, v] of Object.entries(designVars)) host.style.setProperty(k, v);
|
|
87
|
+
}
|
|
88
|
+
container.appendChild(host);
|
|
89
|
+
frames.push(host);
|
|
90
|
+
const r = createRoot(host);
|
|
91
|
+
r.render(
|
|
92
|
+
createElement(SlidePageProvider, { index: i, total: pages.length }, createElement(Page)),
|
|
93
|
+
);
|
|
94
|
+
reactRoots.push(r);
|
|
95
|
+
}
|
|
96
|
+
// Yield once so React commits all pages and intro animations actually start.
|
|
97
|
+
await nextPaint();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await waitForFonts();
|
|
101
|
+
|
|
102
|
+
const deadline = performance.now() + ANIMATION_TIMEOUT_MS;
|
|
103
|
+
while (performance.now() < deadline) {
|
|
104
|
+
const settled = frames.every((frame) => isFrameAnimationSettled(frame));
|
|
105
|
+
if (settled) break;
|
|
106
|
+
await sleep(POLL_INTERVAL_MS);
|
|
107
|
+
}
|
|
108
|
+
await waitForDataWaitfor(container);
|
|
109
|
+
|
|
110
|
+
const { toBlob } = await import('html-to-image');
|
|
111
|
+
const images: Uint8Array[] = [];
|
|
112
|
+
for (let i = 0; i < frames.length; i++) {
|
|
113
|
+
freezeForCapture(frames[i]);
|
|
114
|
+
const blob = await toBlob(frames[i], {
|
|
115
|
+
width: SLIDE_W,
|
|
116
|
+
height: SLIDE_H,
|
|
117
|
+
pixelRatio: CAPTURE_PIXEL_RATIO,
|
|
118
|
+
backgroundColor: '#ffffff',
|
|
119
|
+
cacheBust: true,
|
|
120
|
+
});
|
|
121
|
+
if (!blob) throw new Error(`failed to capture page ${i + 1}`);
|
|
122
|
+
images.push(new Uint8Array(await blob.arrayBuffer()));
|
|
123
|
+
onProgress?.({
|
|
124
|
+
phase: 'processing',
|
|
125
|
+
current: i + 1,
|
|
126
|
+
total,
|
|
127
|
+
percent: Math.min(95, ((i + 1) / total) * 95),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onProgress?.({ phase: 'generating', current: total, total, percent: 98 });
|
|
132
|
+
const pptx = await buildImagePptx(images);
|
|
133
|
+
downloadBlob(
|
|
134
|
+
new Blob([pptx as BlobPart], {
|
|
135
|
+
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
136
|
+
}),
|
|
137
|
+
`${slideId}.pptx`,
|
|
138
|
+
);
|
|
139
|
+
} finally {
|
|
140
|
+
onProgress?.({ phase: 'done', current: total, total, percent: 100 });
|
|
141
|
+
for (const r of reactRoots) r.unmount();
|
|
142
|
+
container.remove();
|
|
143
|
+
captureStyle.remove();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Pin each element's settled visual state inline and remove its animation so the
|
|
148
|
+
// clone html-to-image rasterises renders the final frame instead of replaying the
|
|
149
|
+
// (initially invisible) keyframes. Pseudo-elements are handled by CAPTURE_STYLE_ID.
|
|
150
|
+
function freezeForCapture(root: HTMLElement): void {
|
|
151
|
+
for (const el of root.querySelectorAll<HTMLElement>('*')) {
|
|
152
|
+
const cs = getComputedStyle(el);
|
|
153
|
+
for (const prop of FROZEN_PROPS) {
|
|
154
|
+
el.style.setProperty(prop, cs.getPropertyValue(prop), 'important');
|
|
155
|
+
}
|
|
156
|
+
el.style.setProperty('animation', 'none', 'important');
|
|
157
|
+
el.style.setProperty('transition', 'none', 'important');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const XML_DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
|
|
162
|
+
const REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
163
|
+
const OD_REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
|
|
164
|
+
|
|
165
|
+
async function buildImagePptx(images: Uint8Array[]): Promise<Uint8Array> {
|
|
166
|
+
const { zipSync, strToU8 } = await import('fflate');
|
|
167
|
+
const n = images.length;
|
|
168
|
+
const files: Record<string, Uint8Array> = {};
|
|
169
|
+
|
|
170
|
+
files['[Content_Types].xml'] = strToU8(contentTypesXml(n));
|
|
171
|
+
files['_rels/.rels'] = strToU8(rootRelsXml());
|
|
172
|
+
files['ppt/presentation.xml'] = strToU8(presentationXml(n));
|
|
173
|
+
files['ppt/_rels/presentation.xml.rels'] = strToU8(presentationRelsXml(n));
|
|
174
|
+
files['ppt/presProps.xml'] = strToU8(presPropsXml());
|
|
175
|
+
files['ppt/theme/theme1.xml'] = strToU8(themeXml());
|
|
176
|
+
files['ppt/slideMasters/slideMaster1.xml'] = strToU8(slideMasterXml());
|
|
177
|
+
files['ppt/slideMasters/_rels/slideMaster1.xml.rels'] = strToU8(slideMasterRelsXml());
|
|
178
|
+
files['ppt/slideLayouts/slideLayout1.xml'] = strToU8(slideLayoutXml());
|
|
179
|
+
files['ppt/slideLayouts/_rels/slideLayout1.xml.rels'] = strToU8(slideLayoutRelsXml());
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
const idx = i + 1;
|
|
183
|
+
files[`ppt/slides/slide${idx}.xml`] = strToU8(slideXml());
|
|
184
|
+
files[`ppt/slides/_rels/slide${idx}.xml.rels`] = strToU8(slideRelsXml(idx));
|
|
185
|
+
files[`ppt/media/image${idx}.png`] = images[i];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return zipSync(files);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function contentTypesXml(n: number): string {
|
|
192
|
+
const slideOverrides = Array.from(
|
|
193
|
+
{ length: n },
|
|
194
|
+
(_, i) =>
|
|
195
|
+
`<Override PartName="/ppt/slides/slide${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`,
|
|
196
|
+
).join('');
|
|
197
|
+
return `${XML_DECL}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Default Extension="png" ContentType="image/png"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/><Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/><Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/><Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>${slideOverrides}</Types>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function rootRelsXml(): string {
|
|
201
|
+
return `${XML_DECL}<Relationships xmlns="${REL_NS}"><Relationship Id="rId1" Type="${OD_REL}/officeDocument" Target="ppt/presentation.xml"/></Relationships>`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function presentationXml(n: number): string {
|
|
205
|
+
const sldIds = Array.from(
|
|
206
|
+
{ length: n },
|
|
207
|
+
(_, i) => `<p:sldId id="${256 + i}" r:id="rId${i + 3}"/>`,
|
|
208
|
+
).join('');
|
|
209
|
+
return `${XML_DECL}<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${OD_REL}" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst><p:sldIdLst>${sldIds}</p:sldIdLst><p:sldSz cx="${EMU_W}" cy="${EMU_H}"/><p:notesSz cx="6858000" cy="9144000"/></p:presentation>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function presentationRelsXml(n: number): string {
|
|
213
|
+
const rels = [
|
|
214
|
+
`<Relationship Id="rId1" Type="${OD_REL}/slideMaster" Target="slideMasters/slideMaster1.xml"/>`,
|
|
215
|
+
`<Relationship Id="rId2" Type="${OD_REL}/presProps" Target="presProps.xml"/>`,
|
|
216
|
+
];
|
|
217
|
+
for (let i = 0; i < n; i++) {
|
|
218
|
+
rels.push(
|
|
219
|
+
`<Relationship Id="rId${i + 3}" Type="${OD_REL}/slide" Target="slides/slide${i + 1}.xml"/>`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return `${XML_DECL}<Relationships xmlns="${REL_NS}">${rels.join('')}</Relationships>`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function presPropsXml(): string {
|
|
226
|
+
return `${XML_DECL}<p:presentationPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${OD_REL}" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"/>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function slideMasterXml(): string {
|
|
230
|
+
return `${XML_DECL}<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${OD_REL}" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/><p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst></p:sldMaster>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function slideMasterRelsXml(): string {
|
|
234
|
+
return `${XML_DECL}<Relationships xmlns="${REL_NS}"><Relationship Id="rId1" Type="${OD_REL}/slideLayout" Target="../slideLayouts/slideLayout1.xml"/><Relationship Id="rId2" Type="${OD_REL}/theme" Target="../theme/theme1.xml"/></Relationships>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function slideLayoutXml(): string {
|
|
238
|
+
return `${XML_DECL}<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${OD_REL}" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1"><p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function slideLayoutRelsXml(): string {
|
|
242
|
+
return `${XML_DECL}<Relationships xmlns="${REL_NS}"><Relationship Id="rId1" Type="${OD_REL}/slideMaster" Target="../slideMasters/slideMaster1.xml"/></Relationships>`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function slideXml(): string {
|
|
246
|
+
return `${XML_DECL}<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${OD_REL}" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:pic><p:nvPicPr><p:cNvPr id="2" name="Slide"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rId2"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${EMU_W}" cy="${EMU_H}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function slideRelsXml(idx: number): string {
|
|
250
|
+
return `${XML_DECL}<Relationships xmlns="${REL_NS}"><Relationship Id="rId1" Type="${OD_REL}/slideLayout" Target="../slideLayouts/slideLayout1.xml"/><Relationship Id="rId2" Type="${OD_REL}/image" Target="../media/image${idx}.png"/></Relationships>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function themeXml(): string {
|
|
254
|
+
return `${XML_DECL}<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="44546A"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:lumMod val="110000"/><a:satMod val="105000"/><a:tint val="67000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="103000"/><a:tint val="73000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="105000"/><a:satMod val="109000"/><a:tint val="81000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:satMod val="103000"/><a:lumMod val="102000"/><a:tint val="94000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:satMod val="110000"/><a:lumMod val="100000"/><a:shade val="100000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:lumMod val="99000"/><a:satMod val="120000"/><a:shade val="78000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"><a:tint val="95000"/><a:satMod val="170000"/></a:schemeClr></a:solidFill><a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="93000"/><a:satMod val="150000"/><a:shade val="98000"/><a:lumMod val="102000"/></a:schemeClr></a:gs><a:gs pos="50000"><a:schemeClr val="phClr"><a:tint val="98000"/><a:satMod val="130000"/><a:shade val="90000"/><a:lumMod val="103000"/></a:schemeClr></a:gs><a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="63000"/><a:satMod val="120000"/></a:schemeClr></a:gs></a:gsLst><a:lin ang="5400000" scaled="0"/></a:gradFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function sleep(ms: number): Promise<void> {
|
|
258
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function nextPaint(): Promise<void> {
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
let settled = false;
|
|
264
|
+
const settle = () => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
settled = true;
|
|
267
|
+
resolve();
|
|
268
|
+
};
|
|
269
|
+
requestAnimationFrame(settle);
|
|
270
|
+
setTimeout(settle, 50);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function downloadBlob(blob: Blob, filename: string): void {
|
|
275
|
+
const url = URL.createObjectURL(blob);
|
|
276
|
+
const a = document.createElement('a');
|
|
277
|
+
a.href = url;
|
|
278
|
+
a.download = filename;
|
|
279
|
+
a.rel = 'noopener';
|
|
280
|
+
document.body.appendChild(a);
|
|
281
|
+
a.click();
|
|
282
|
+
a.remove();
|
|
283
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
284
|
+
}
|