mulmocast 2.21.0 → 2.22.1
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/lib/actions/images.js +2 -1
- package/lib/data/animationRuntime.d.ts +5 -0
- package/lib/data/animationRuntime.js +639 -0
- package/lib/methods/mulmo_beat.d.ts +2 -7
- package/lib/methods/mulmo_beat.js +1 -12
- package/lib/methods/mulmo_beat_node.d.ts +2 -8
- package/lib/utils/animation_config.d.ts +15 -0
- package/lib/utils/animation_config.js +17 -0
- package/lib/utils/audio_probe_source.d.ts +5 -0
- package/lib/utils/audio_probe_source.js +16 -0
- package/lib/utils/beat_html/chart.js +2 -1
- package/lib/utils/beat_html/decoration.d.ts +16 -0
- package/lib/utils/beat_html/decoration.js +55 -0
- package/lib/utils/beat_html/html_tailwind.d.ts +6 -6
- package/lib/utils/beat_html/html_tailwind.js +13 -7
- package/lib/utils/beat_html/index.js +3 -2
- package/lib/utils/beat_html/mermaid.js +2 -1
- package/lib/utils/beat_html/slide.d.ts +7 -2
- package/lib/utils/beat_html/slide.js +7 -22
- package/lib/utils/beat_html/type.d.ts +43 -1
- package/lib/utils/html_render.js +1 -1
- package/lib/utils/image_plugins/bg_image_css.d.ts +19 -0
- package/lib/utils/image_plugins/bg_image_css.js +75 -0
- package/lib/utils/image_plugins/bg_image_util.d.ts +0 -6
- package/lib/utils/image_plugins/bg_image_util.js +7 -69
- package/lib/utils/image_plugins/html_tailwind.js +2 -39
- package/lib/utils/image_plugins/html_tailwind_script.d.ts +41 -0
- package/lib/utils/image_plugins/html_tailwind_script.js +111 -0
- package/lib/utils/image_plugins/slide.js +2 -5
- package/lib/utils/image_plugins/slide_branding.d.ts +7 -0
- package/lib/utils/image_plugins/slide_branding.js +10 -0
- package/package.json +1 -1
|
@@ -5,8 +5,8 @@ import { getHTMLFile, getJSFile } from "../file.js";
|
|
|
5
5
|
import { renderHTMLToImage, interpolate, renderHTMLToFrames, renderHTMLToVideo, renderHTMLToFinalFrame } from "../html_render.js";
|
|
6
6
|
import { framesToVideo } from "../ffmpeg_utils.js";
|
|
7
7
|
import { parrotingImagePath } from "./utils.js";
|
|
8
|
-
import { swipeElementsToHtml, swipeElementsToScript } from "../swipe_to_html.js";
|
|
9
8
|
import { htmlTailwindMarkup } from "./html_tailwind_markup.js";
|
|
9
|
+
import { FINAL_FRAME_TOTAL, animationFps, buildUserScript, resolveHtmlAndScript } from "./html_tailwind_script.js";
|
|
10
10
|
import { MulmoStudioContextMethods } from "../../methods/mulmo_studio_context.js";
|
|
11
11
|
export const imageType = "html_tailwind";
|
|
12
12
|
/**
|
|
@@ -53,22 +53,6 @@ export const resolveRelativeImagePaths = (html, baseDirPath) => {
|
|
|
53
53
|
return `${prefix}${quote}file://${absolutePath}${quote}`;
|
|
54
54
|
});
|
|
55
55
|
};
|
|
56
|
-
const DEFAULT_ANIMATION_FPS = 30;
|
|
57
|
-
/** Join html field into a single string (handles both string and string[]) */
|
|
58
|
-
const joinHtml = (html) => {
|
|
59
|
-
return Array.isArray(html) ? html.join("\n") : html;
|
|
60
|
-
};
|
|
61
|
-
const buildUserScript = (script) => {
|
|
62
|
-
if (!script)
|
|
63
|
-
return "";
|
|
64
|
-
const code = Array.isArray(script) ? script.join("\n") : script;
|
|
65
|
-
// If user script contains ESM import/export, emit module script so imports work.
|
|
66
|
-
const isModule = code.split("\n").some((line) => {
|
|
67
|
-
const trimmed = line.trimStart();
|
|
68
|
-
return trimmed.startsWith("import ") || trimmed.startsWith("export ");
|
|
69
|
-
});
|
|
70
|
-
return isModule ? `<script type="module">\n${code}\n</script>` : `<script>\n${code}\n</script>`;
|
|
71
|
-
};
|
|
72
56
|
/**
|
|
73
57
|
* Resolve relative modelUrl assignment in user scripts to file:// absolute paths.
|
|
74
58
|
* e.g. const modelUrl = "models/a.glb" -> const modelUrl = "file:///abs/models/a.glb"
|
|
@@ -105,25 +89,6 @@ export const resolveRelativeModelPathsInScript = (html, baseDirPath) => {
|
|
|
105
89
|
})
|
|
106
90
|
.join("\n");
|
|
107
91
|
};
|
|
108
|
-
/**
|
|
109
|
-
* Resolve HTML and script from beat image data.
|
|
110
|
-
* If `elements` (Swipe-style) is provided, convert to HTML + script.
|
|
111
|
-
* Otherwise, use raw `html` and `script` fields.
|
|
112
|
-
*/
|
|
113
|
-
const resolveHtmlAndScript = (imageData) => {
|
|
114
|
-
if (imageData.elements && Array.isArray(imageData.elements) && imageData.elements.length > 0) {
|
|
115
|
-
const html = swipeElementsToHtml(imageData.elements);
|
|
116
|
-
const generatedScript = swipeElementsToScript(imageData.elements);
|
|
117
|
-
// Merge with user-provided script if any
|
|
118
|
-
const userScript = imageData.script ? joinHtml(imageData.script) : "";
|
|
119
|
-
const combinedScript = [generatedScript, userScript].filter(Boolean).join("\n");
|
|
120
|
-
return { html, script: combinedScript || undefined };
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
html: joinHtml(imageData.html ?? ""),
|
|
124
|
-
script: imageData.script,
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
92
|
const getAnimationConfig = (params) => {
|
|
128
93
|
const { beat } = params;
|
|
129
94
|
if (!beat.image || beat.image.type !== imageType)
|
|
@@ -131,12 +96,10 @@ const getAnimationConfig = (params) => {
|
|
|
131
96
|
const animation = beat.image.animation;
|
|
132
97
|
if (!MulmoBeatMethods.isAnimationEnabled(animation))
|
|
133
98
|
return null;
|
|
134
|
-
const fps =
|
|
99
|
+
const fps = animationFps(animation);
|
|
135
100
|
const movie = MulmoBeatMethods.isMovieMode(animation);
|
|
136
101
|
return { fps, movie };
|
|
137
102
|
};
|
|
138
|
-
/** Large frame count to ensure all animations reach their end state when exact duration is unknown */
|
|
139
|
-
const FINAL_FRAME_TOTAL = 9000;
|
|
140
103
|
/**
|
|
141
104
|
* Build the animated HTML string from beat data and template.
|
|
142
105
|
*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SwipeElement } from "../swipe_to_html.js";
|
|
2
|
+
/** How long the final frame waits for `<video>` elements to be playable, as the PNG render does. */
|
|
3
|
+
export declare const VIDEO_LOAD_TIMEOUT_MS = 15000;
|
|
4
|
+
/** `tailwind_animated.html`'s rules: CSS animations and transitions stay still, so frames come from `render` alone. */
|
|
5
|
+
export declare const ANIMATED_DOCUMENT_CSS = "*, *::before, *::after {\n animation-play-state: paused !important;\n transition: none !important;\n}";
|
|
6
|
+
/** Large frame count to ensure all animations reach their end state when exact duration is unknown */
|
|
7
|
+
export declare const FINAL_FRAME_TOTAL = 9000;
|
|
8
|
+
/** Frames per second of an enabled `animation` config. */
|
|
9
|
+
export declare const animationFps: (animation: unknown) => number;
|
|
10
|
+
export declare const buildUserScript: (script: string | string[] | undefined) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve HTML and script from beat image data.
|
|
13
|
+
* If `elements` (Swipe-style) is provided, convert to HTML + script.
|
|
14
|
+
* Otherwise, use raw `html` and `script` fields.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveHtmlAndScript: (imageData: {
|
|
17
|
+
html?: string | string[];
|
|
18
|
+
script?: string | string[];
|
|
19
|
+
elements?: SwipeElement[];
|
|
20
|
+
}) => {
|
|
21
|
+
html: string;
|
|
22
|
+
script: string | string[] | undefined;
|
|
23
|
+
};
|
|
24
|
+
export type HtmlTailwindScriptSource = {
|
|
25
|
+
script?: string | string[];
|
|
26
|
+
elements?: unknown;
|
|
27
|
+
animation?: unknown;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The scripts that bring an html_tailwind beat to the state its PNG shows, as `<script>` elements
|
|
31
|
+
* for the end of the beat's own document; undefined when it runs none.
|
|
32
|
+
*
|
|
33
|
+
* Without `animation` that is the author script and swipe driver, as `tailwind.html` runs them.
|
|
34
|
+
* With it, the runtime, frame state and registration `tailwind_animated.html` loads, then the
|
|
35
|
+
* final frame — what `renderHTMLToFinalFrame` captures. The final frame is drawn on `load`, after
|
|
36
|
+
* a module author script (deferred) has defined its `render`, as Puppeteer calls it after load;
|
|
37
|
+
* scripts a host adds to an already-loaded document draw it once `render` exists (bounded wait).
|
|
38
|
+
*/
|
|
39
|
+
export declare const htmlTailwindDocumentScript: (image: HtmlTailwindScriptSource) => string | undefined;
|
|
40
|
+
/** `documentCss` for an html_tailwind beat: the animated template's pause rules, when it animates. */
|
|
41
|
+
export declare const htmlTailwindDocumentCss: (image: HtmlTailwindScriptSource) => string | undefined;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { isAnimationEnabled, isAnimationObject } from "../animation_config.js";
|
|
2
|
+
import { isSwipeElements, swipeElementsToHtml, swipeElementsToScript } from "../swipe_to_html.js";
|
|
3
|
+
import { animationRuntime } from "../../data/animationRuntime.js";
|
|
4
|
+
/**
|
|
5
|
+
* The scripts an html_tailwind beat runs. Pure — no Node, no filesystem — because the PNG render
|
|
6
|
+
* and the browser fragment path run the same author script, swipe driver and animation runtime,
|
|
7
|
+
* and must assemble them the same way.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_ANIMATION_FPS = 30;
|
|
10
|
+
/** How long the final frame waits for `<video>` elements to be playable, as the PNG render does. */
|
|
11
|
+
export const VIDEO_LOAD_TIMEOUT_MS = 15000;
|
|
12
|
+
/** `tailwind_animated.html`'s rules: CSS animations and transitions stay still, so frames come from `render` alone. */
|
|
13
|
+
export const ANIMATED_DOCUMENT_CSS = `*, *::before, *::after {
|
|
14
|
+
animation-play-state: paused !important;
|
|
15
|
+
transition: none !important;
|
|
16
|
+
}`;
|
|
17
|
+
// How long scripts added to an already-loaded document wait for a module author script to define `render`.
|
|
18
|
+
const LATE_RENDER_WAIT_MS = 5000;
|
|
19
|
+
const LATE_RENDER_POLL_MS = 50;
|
|
20
|
+
/** Large frame count to ensure all animations reach their end state when exact duration is unknown */
|
|
21
|
+
export const FINAL_FRAME_TOTAL = 9000;
|
|
22
|
+
/** Frames per second of an enabled `animation` config. */
|
|
23
|
+
export const animationFps = (animation) => (isAnimationObject(animation) ? (animation.fps ?? DEFAULT_ANIMATION_FPS) : DEFAULT_ANIMATION_FPS);
|
|
24
|
+
/** Join html field into a single string (handles both string and string[]) */
|
|
25
|
+
const joinHtml = (html) => {
|
|
26
|
+
return Array.isArray(html) ? html.join("\n") : html;
|
|
27
|
+
};
|
|
28
|
+
// A `</script` anywhere in the code ends the element early, even inside a string; `<\/script` is the same JS.
|
|
29
|
+
const neutralizeScriptTerminator = (code) => code.replace(/<\/script/gi, (terminator) => `<\\${terminator.slice(1)}`);
|
|
30
|
+
export const buildUserScript = (script) => {
|
|
31
|
+
if (!script)
|
|
32
|
+
return "";
|
|
33
|
+
const code = neutralizeScriptTerminator(Array.isArray(script) ? script.join("\n") : script);
|
|
34
|
+
// If user script contains ESM import/export, emit module script so imports work.
|
|
35
|
+
const isModule = code.split("\n").some((line) => {
|
|
36
|
+
const trimmed = line.trimStart();
|
|
37
|
+
return trimmed.startsWith("import ") || trimmed.startsWith("export ");
|
|
38
|
+
});
|
|
39
|
+
return isModule ? `<script type="module">\n${code}\n</script>` : `<script>\n${code}\n</script>`;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Resolve HTML and script from beat image data.
|
|
43
|
+
* If `elements` (Swipe-style) is provided, convert to HTML + script.
|
|
44
|
+
* Otherwise, use raw `html` and `script` fields.
|
|
45
|
+
*/
|
|
46
|
+
export const resolveHtmlAndScript = (imageData) => {
|
|
47
|
+
if (imageData.elements && Array.isArray(imageData.elements) && imageData.elements.length > 0) {
|
|
48
|
+
const html = swipeElementsToHtml(imageData.elements);
|
|
49
|
+
const generatedScript = swipeElementsToScript(imageData.elements);
|
|
50
|
+
// Merge with user-provided script if any
|
|
51
|
+
const userScript = imageData.script ? joinHtml(imageData.script) : "";
|
|
52
|
+
const combinedScript = [generatedScript, userScript].filter(Boolean).join("\n");
|
|
53
|
+
return { html, script: combinedScript || undefined };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
html: joinHtml(imageData.html ?? ""),
|
|
57
|
+
script: imageData.script,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const inlineScript = (code) => `<script>\n${neutralizeScriptTerminator(code)}\n</script>`;
|
|
61
|
+
/**
|
|
62
|
+
* The scripts that bring an html_tailwind beat to the state its PNG shows, as `<script>` elements
|
|
63
|
+
* for the end of the beat's own document; undefined when it runs none.
|
|
64
|
+
*
|
|
65
|
+
* Without `animation` that is the author script and swipe driver, as `tailwind.html` runs them.
|
|
66
|
+
* With it, the runtime, frame state and registration `tailwind_animated.html` loads, then the
|
|
67
|
+
* final frame — what `renderHTMLToFinalFrame` captures. The final frame is drawn on `load`, after
|
|
68
|
+
* a module author script (deferred) has defined its `render`, as Puppeteer calls it after load;
|
|
69
|
+
* scripts a host adds to an already-loaded document draw it once `render` exists (bounded wait).
|
|
70
|
+
*/
|
|
71
|
+
export const htmlTailwindDocumentScript = (image) => {
|
|
72
|
+
// The schema leaves `elements` unknown[]; the markup reads it through the same guard.
|
|
73
|
+
const elements = isSwipeElements(image.elements) ? image.elements : undefined;
|
|
74
|
+
const userScript = buildUserScript(resolveHtmlAndScript({ script: image.script, ...(elements ? { elements } : {}) }).script);
|
|
75
|
+
if (!isAnimationEnabled(image.animation))
|
|
76
|
+
return userScript || undefined;
|
|
77
|
+
const frameState = `window.__MULMO = { frame: 0, totalFrames: ${FINAL_FRAME_TOTAL}, fps: ${animationFps(image.animation)} };`;
|
|
78
|
+
// On `load` when the scripts are part of the document as it loads. Added to a loaded document, a module author
|
|
79
|
+
// script (one with imports above all) may run later, so wait for its `render`, bounded, before drawing. Either
|
|
80
|
+
// way the videos get to be playable first, as `waitForVideosReady` lets them before the PNG's final frame.
|
|
81
|
+
const finalFrame = [
|
|
82
|
+
"(function () {",
|
|
83
|
+
" var whenVideosReady = function (done) {",
|
|
84
|
+
' var pending = Array.prototype.slice.call(document.querySelectorAll("video")).filter(function (video) { return video.readyState < 3; });',
|
|
85
|
+
" if (pending.length === 0) return done();",
|
|
86
|
+
" var remaining = pending.length, finished = false;",
|
|
87
|
+
" var finish = function () { if (!finished) { finished = true; done(); } };",
|
|
88
|
+
` setTimeout(finish, ${VIDEO_LOAD_TIMEOUT_MS});`,
|
|
89
|
+
' pending.forEach(function (video) { video.addEventListener("canplaythrough", function () { remaining -= 1; if (remaining <= 0) finish(); }, { once: true }); });',
|
|
90
|
+
" };",
|
|
91
|
+
" var drawFinal = function () { whenVideosReady(function () { Promise.resolve(window.renderFinal()).catch(console.error); }); };",
|
|
92
|
+
" var whenRenderReady = function (startedAt) {",
|
|
93
|
+
` if (typeof window.render === "function" || Date.now() - startedAt > ${LATE_RENDER_WAIT_MS}) drawFinal();`,
|
|
94
|
+
` else setTimeout(function () { whenRenderReady(startedAt); }, ${LATE_RENDER_POLL_MS});`,
|
|
95
|
+
" };",
|
|
96
|
+
' if (document.readyState === "complete") whenRenderReady(Date.now());',
|
|
97
|
+
' else window.addEventListener("load", drawFinal);',
|
|
98
|
+
"})();",
|
|
99
|
+
].join("\n");
|
|
100
|
+
return [
|
|
101
|
+
inlineScript(`${animationRuntime.animation_runtime}\n${frameState}`),
|
|
102
|
+
userScript,
|
|
103
|
+
inlineScript(animationRuntime.data_attribute_registration),
|
|
104
|
+
inlineScript(animationRuntime.auto_render),
|
|
105
|
+
inlineScript(finalFrame),
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join("\n");
|
|
109
|
+
};
|
|
110
|
+
/** `documentCss` for an html_tailwind beat: the animated template's pause rules, when it animates. */
|
|
111
|
+
export const htmlTailwindDocumentCss = (image) => isAnimationEnabled(image.animation) ? ANIMATED_DOCUMENT_CSS : undefined;
|
|
@@ -8,6 +8,7 @@ import { MulmoMediaSourceMethods } from "../../methods/mulmo_media_source.js";
|
|
|
8
8
|
import { MulmoPresentationStyleMethods } from "../../methods/mulmo_presentation_style.js";
|
|
9
9
|
import { imageAction, imageFileTarget, unknownMediaType } from "../error_cause.js";
|
|
10
10
|
import { MulmoStudioContextMethods } from "../../methods/mulmo_studio_context.js";
|
|
11
|
+
import { pickBranding } from "./slide_branding.js";
|
|
11
12
|
export const imageType = "slide";
|
|
12
13
|
const slideImageRefError = (refKey) => ({
|
|
13
14
|
type: unknownMediaType,
|
|
@@ -124,11 +125,7 @@ const resolveBranding = (params) => {
|
|
|
124
125
|
const { beat, context } = params;
|
|
125
126
|
if (!beat.image || beat.image.type !== imageType)
|
|
126
127
|
return undefined;
|
|
127
|
-
|
|
128
|
-
// null means explicitly disabled
|
|
129
|
-
if (beatBranding === null)
|
|
130
|
-
return undefined;
|
|
131
|
-
return beatBranding ?? context.presentationStyle.slideParams?.branding;
|
|
128
|
+
return pickBranding(beat.image.branding, context.presentationStyle.slideParams?.branding);
|
|
132
129
|
};
|
|
133
130
|
/**
|
|
134
131
|
* Convert SlideBranding to ResolvedBranding (all sources → data URLs).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SlideBranding } from "@mulmocast/deck";
|
|
2
|
+
/**
|
|
3
|
+
* Which branding a slide beat gets. Pure — the PNG render and the browser fragment path pick it
|
|
4
|
+
* the same way: the beat's own wins, a beat-level `null` switches the deck's off, and otherwise
|
|
5
|
+
* the deck's `slideParams.branding` applies.
|
|
6
|
+
*/
|
|
7
|
+
export declare const pickBranding: (beatBranding: SlideBranding | null | undefined, deckBranding: SlideBranding | undefined) => SlideBranding | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which branding a slide beat gets. Pure — the PNG render and the browser fragment path pick it
|
|
3
|
+
* the same way: the beat's own wins, a beat-level `null` switches the deck's off, and otherwise
|
|
4
|
+
* the deck's `slideParams.branding` applies.
|
|
5
|
+
*/
|
|
6
|
+
export const pickBranding = (beatBranding, deckBranding) => {
|
|
7
|
+
if (beatBranding === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
return beatBranding ?? deckBranding;
|
|
10
|
+
};
|