mulmocast 2.20.0 → 2.22.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.
Files changed (36) hide show
  1. package/assets/schemas/mulmo_script.json +169 -0
  2. package/lib/data/animationRuntime.d.ts +5 -0
  3. package/lib/data/animationRuntime.js +639 -0
  4. package/lib/methods/mulmo_beat.d.ts +2 -7
  5. package/lib/methods/mulmo_beat.js +1 -12
  6. package/lib/methods/mulmo_beat_node.d.ts +2 -8
  7. package/lib/types/schema.d.ts +264 -0
  8. package/lib/utils/animation_config.d.ts +15 -0
  9. package/lib/utils/animation_config.js +17 -0
  10. package/lib/utils/beat_html/chart.d.ts +7 -11
  11. package/lib/utils/beat_html/chart.js +8 -18
  12. package/lib/utils/beat_html/decoration.d.ts +16 -0
  13. package/lib/utils/beat_html/decoration.js +55 -0
  14. package/lib/utils/beat_html/html_tailwind.d.ts +6 -6
  15. package/lib/utils/beat_html/html_tailwind.js +13 -7
  16. package/lib/utils/beat_html/index.js +5 -4
  17. package/lib/utils/beat_html/mermaid.d.ts +3 -6
  18. package/lib/utils/beat_html/mermaid.js +5 -10
  19. package/lib/utils/beat_html/slide.d.ts +13 -6
  20. package/lib/utils/beat_html/slide.js +8 -21
  21. package/lib/utils/beat_html/type.d.ts +47 -4
  22. package/lib/utils/context.d.ts +62 -0
  23. package/lib/utils/html_render.js +1 -1
  24. package/lib/utils/image_plugins/bg_image_css.d.ts +19 -0
  25. package/lib/utils/image_plugins/bg_image_css.js +75 -0
  26. package/lib/utils/image_plugins/bg_image_util.d.ts +0 -6
  27. package/lib/utils/image_plugins/bg_image_util.js +7 -69
  28. package/lib/utils/image_plugins/chart_html.d.ts +0 -10
  29. package/lib/utils/image_plugins/chart_html.js +5 -31
  30. package/lib/utils/image_plugins/html_tailwind.js +2 -39
  31. package/lib/utils/image_plugins/html_tailwind_script.d.ts +41 -0
  32. package/lib/utils/image_plugins/html_tailwind_script.js +111 -0
  33. package/lib/utils/image_plugins/slide.js +2 -5
  34. package/lib/utils/image_plugins/slide_branding.d.ts +7 -0
  35. package/lib/utils/image_plugins/slide_branding.js +10 -0
  36. package/package.json +2 -2
@@ -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
- const beatBranding = beat.image.branding;
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.20.0",
3
+ "version": "2.22.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -114,7 +114,7 @@
114
114
  "@modelcontextprotocol/sdk": "^1.32.1",
115
115
  "@modernized/fluent-ffmpeg": "^1.0.3",
116
116
  "@mozilla/readability": "^0.6.0",
117
- "@mulmocast/deck": "^2.0.2",
117
+ "@mulmocast/deck": "^2.1.0",
118
118
  "@tavily/core": "^0.7.14",
119
119
  "archiver": "^8.0.0",
120
120
  "clipboardy": "^5.3.2",