mulmocast 2.5.0 → 2.6.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.
@@ -372,14 +372,16 @@ export declare const mulmoMermaidMediaSchema: z.ZodObject<{
372
372
  opacity: z.ZodOptional<z.ZodNumber>;
373
373
  }, z.core.$strip>]>>>;
374
374
  }, z.core.$strict>;
375
+ export declare const swipeElementSchema: z.ZodType;
375
376
  export declare const htmlTailwindAnimationSchema: z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
376
377
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
377
378
  movie: z.ZodOptional<z.ZodBoolean>;
378
379
  }, z.core.$strip>]>;
379
380
  export declare const mulmoHtmlTailwindMediaSchema: z.ZodObject<{
380
381
  type: z.ZodLiteral<"html_tailwind">;
381
- html: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
382
+ html: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
382
383
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
384
+ elements: z.ZodOptional<z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
383
385
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
384
386
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
385
387
  movie: z.ZodOptional<z.ZodBoolean>;
@@ -551,8 +553,9 @@ export declare const mulmoImageAssetSchema: z.ZodUnion<readonly [z.ZodObject<{
551
553
  }, z.core.$strip>]>>>;
552
554
  }, z.core.$strict>, z.ZodObject<{
553
555
  type: z.ZodLiteral<"html_tailwind">;
554
- html: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
556
+ html: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
555
557
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
558
+ elements: z.ZodOptional<z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
556
559
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
557
560
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
558
561
  movie: z.ZodOptional<z.ZodBoolean>;
@@ -3621,8 +3624,9 @@ export declare const mulmoBeatSchema: z.ZodObject<{
3621
3624
  }, z.core.$strip>]>>>;
3622
3625
  }, z.core.$strict>, z.ZodObject<{
3623
3626
  type: z.ZodLiteral<"html_tailwind">;
3624
- html: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
3627
+ html: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
3625
3628
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
3629
+ elements: z.ZodOptional<z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
3626
3630
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
3627
3631
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
3628
3632
  movie: z.ZodOptional<z.ZodBoolean>;
@@ -7428,8 +7432,9 @@ export declare const mulmoScriptSchema: z.ZodObject<{
7428
7432
  }, z.core.$strip>]>>>;
7429
7433
  }, z.core.$strict>, z.ZodObject<{
7430
7434
  type: z.ZodLiteral<"html_tailwind">;
7431
- html: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
7435
+ html: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
7432
7436
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
7437
+ elements: z.ZodOptional<z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
7433
7438
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
7434
7439
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
7435
7440
  movie: z.ZodOptional<z.ZodBoolean>;
@@ -10850,8 +10855,9 @@ export declare const mulmoStudioSchema: z.ZodObject<{
10850
10855
  }, z.core.$strip>]>>>;
10851
10856
  }, z.core.$strict>, z.ZodObject<{
10852
10857
  type: z.ZodLiteral<"html_tailwind">;
10853
- html: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
10858
+ html: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
10854
10859
  script: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
10860
+ elements: z.ZodOptional<z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
10855
10861
  animation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
10856
10862
  fps: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
10857
10863
  movie: z.ZodOptional<z.ZodBoolean>;
@@ -194,6 +194,72 @@ export const mulmoMermaidMediaSchema = z
194
194
  backgroundImage: backgroundImageSchema,
195
195
  })
196
196
  .strict();
197
+ // Swipe-inspired element animation schemas
198
+ const swipePositionValue = z.union([z.number(), z.string()]);
199
+ const swipeTransitionSchema = z
200
+ .object({
201
+ opacity: z.number().min(0).max(1).optional(),
202
+ rotate: z.number().optional(),
203
+ scale: z.union([z.number(), z.tuple([z.number(), z.number()])]).optional(),
204
+ translate: z.tuple([z.number(), z.number()]).optional(),
205
+ bc: z.string().optional(),
206
+ timing: z.tuple([z.number(), z.number()]).default([0, 1]).optional().describe("Animation timing [start, end] as ratio 0.0-1.0 of beat duration"),
207
+ })
208
+ .strict();
209
+ const swipeLoopSchema = z
210
+ .object({
211
+ style: z.enum(["vibrate", "blink", "wiggle", "spin", "shift", "bounce", "pulse"]),
212
+ count: z.number().optional().describe("Number of loop iterations. 0 = infinite"),
213
+ delta: z.number().optional().describe("Distance for vibrate / angle for wiggle"),
214
+ duration: z.number().optional().describe("Duration of one cycle in seconds"),
215
+ direction: z.enum(["n", "s", "e", "w"]).optional().describe("Direction for shift animation"),
216
+ clockwise: z.boolean().optional().describe("Spin direction. Default: true"),
217
+ })
218
+ .strict();
219
+ const swipeShadowSchema = z
220
+ .object({
221
+ color: z.string().optional().default("black"),
222
+ offset: z.tuple([z.number(), z.number()]).optional().default([1, 1]),
223
+ opacity: z.number().optional().default(0.5),
224
+ radius: z.number().optional().default(1),
225
+ })
226
+ .strict();
227
+ export const swipeElementSchema = z.lazy(() => z
228
+ .object({
229
+ id: z.string().optional(),
230
+ // Position & size
231
+ x: swipePositionValue.optional(),
232
+ y: swipePositionValue.optional(),
233
+ w: swipePositionValue.optional(),
234
+ h: swipePositionValue.optional(),
235
+ pos: z.tuple([swipePositionValue, swipePositionValue]).optional().describe("Position by anchor point [x, y]"),
236
+ // Visual
237
+ bc: z.string().optional().describe("Background color"),
238
+ opacity: z.number().min(0).max(1).optional(),
239
+ rotate: z.number().optional(),
240
+ scale: z.union([z.number(), z.tuple([z.number(), z.number()])]).optional(),
241
+ translate: z.tuple([z.number(), z.number()]).optional(),
242
+ cornerRadius: z.number().optional(),
243
+ borderWidth: z.number().optional(),
244
+ borderColor: z.string().optional(),
245
+ shadow: swipeShadowSchema.optional(),
246
+ clip: z.boolean().optional(),
247
+ // Content
248
+ text: z.string().optional(),
249
+ fontSize: z.union([z.number(), z.string()]).optional(),
250
+ fontWeight: z.string().optional(),
251
+ textColor: z.string().optional(),
252
+ textAlign: z.enum(["center", "left", "right"]).optional(),
253
+ lineHeight: z.union([z.number(), z.string()]).optional(),
254
+ img: z.string().optional().describe("Image URL or image:ref"),
255
+ imgFit: z.enum(["contain", "cover", "fill"]).optional().default("contain"),
256
+ // Animation
257
+ to: swipeTransitionSchema.optional().describe("Transition animation"),
258
+ loop: swipeLoopSchema.optional().describe("Loop animation"),
259
+ // Children
260
+ elements: z.array(z.lazy(() => swipeElementSchema)).optional(),
261
+ })
262
+ .strict());
197
263
  export const htmlTailwindAnimationSchema = z.union([
198
264
  z.literal(true),
199
265
  z.object({
@@ -204,8 +270,12 @@ export const htmlTailwindAnimationSchema = z.union([
204
270
  export const mulmoHtmlTailwindMediaSchema = z
205
271
  .object({
206
272
  type: z.literal("html_tailwind"),
207
- html: stringOrStringArray,
273
+ html: stringOrStringArray.optional(),
208
274
  script: stringOrStringArray.optional().describe("JavaScript code for the beat. Injected as a <script> tag after html. Use for render() function etc."),
275
+ elements: z
276
+ .array(swipeElementSchema)
277
+ .optional()
278
+ .describe("Swipe-style declarative animation elements. Converted to HTML + render() automatically. Use this OR html, not both."),
209
279
  animation: htmlTailwindAnimationSchema
210
280
  .optional()
211
281
  .describe("Enable frame-based animation (Remotion-style). true for defaults (30fps), or { fps: N } for custom frame rate."),
@@ -1725,8 +1725,9 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
1725
1725
  } | null | undefined;
1726
1726
  } | {
1727
1727
  type: "html_tailwind";
1728
- html: string | string[];
1728
+ html?: string | string[] | undefined;
1729
1729
  script?: string | string[] | undefined;
1730
+ elements?: unknown[] | undefined;
1730
1731
  animation?: true | {
1731
1732
  fps: number;
1732
1733
  movie?: boolean | undefined;
@@ -3824,8 +3825,9 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
3824
3825
  } | null | undefined;
3825
3826
  } | {
3826
3827
  type: "html_tailwind";
3827
- html: string | string[];
3828
+ html?: string | string[] | undefined;
3828
3829
  script?: string | string[] | undefined;
3830
+ elements?: unknown[] | undefined;
3829
3831
  animation?: true | {
3830
3832
  fps: number;
3831
3833
  movie?: boolean | undefined;
@@ -5,6 +5,7 @@ import { getHTMLFile, getJSFile } from "../file.js";
5
5
  import { renderHTMLToImage, interpolate, renderHTMLToFrames, renderHTMLToVideo } 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";
8
9
  export const imageType = "html_tailwind";
9
10
  /**
10
11
  * Resolve image:name references to file:// absolute paths using imageRefs.
@@ -40,6 +41,25 @@ const buildUserScript = (script) => {
40
41
  const code = Array.isArray(script) ? script.join("\n") : script;
41
42
  return `<script>\n${code}\n</script>`;
42
43
  };
44
+ /**
45
+ * Resolve HTML and script from beat image data.
46
+ * If `elements` (Swipe-style) is provided, convert to HTML + script.
47
+ * Otherwise, use raw `html` and `script` fields.
48
+ */
49
+ const resolveHtmlAndScript = (imageData) => {
50
+ if (imageData.elements && Array.isArray(imageData.elements) && imageData.elements.length > 0) {
51
+ const html = swipeElementsToHtml(imageData.elements);
52
+ const generatedScript = swipeElementsToScript(imageData.elements);
53
+ // Merge with user-provided script if any
54
+ const userScript = imageData.script ? joinHtml(imageData.script) : "";
55
+ const combinedScript = [generatedScript, userScript].filter(Boolean).join("\n");
56
+ return { html, script: combinedScript || undefined };
57
+ }
58
+ return {
59
+ html: joinHtml(imageData.html ?? ""),
60
+ script: imageData.script,
61
+ };
62
+ };
43
63
  const getAnimationConfig = (params) => {
44
64
  const { beat } = params;
45
65
  if (!beat.image || beat.image.type !== imageType)
@@ -67,9 +87,9 @@ const processHtmlTailwindAnimated = async (params) => {
67
87
  if (totalFrames <= 0) {
68
88
  throw new Error(`html_tailwind animation: totalFrames is ${totalFrames} (duration=${duration}, fps=${fps}). Increase duration or fps.`);
69
89
  }
70
- const html = joinHtml(beat.image.html);
90
+ const imageData = beat.image;
91
+ const { html, script } = resolveHtmlAndScript(imageData);
71
92
  const template = getHTMLFile("tailwind_animated");
72
- const script = "script" in beat.image ? beat.image.script : undefined;
73
93
  const rawHtmlData = interpolate(template, {
74
94
  html_body: html,
75
95
  animation_runtime: getJSFile("animation_runtime"),
@@ -106,9 +126,9 @@ const processHtmlTailwindStatic = async (params) => {
106
126
  const { beat, imagePath, canvasSize, context } = params;
107
127
  if (!beat.image || beat.image.type !== imageType)
108
128
  return;
109
- const html = joinHtml(beat.image.html);
129
+ const imageData = beat.image;
130
+ const { html, script } = resolveHtmlAndScript(imageData);
110
131
  const template = getHTMLFile("tailwind");
111
- const script = "script" in beat.image ? beat.image.script : undefined;
112
132
  const rawHtmlData = interpolate(template, {
113
133
  html_body: html,
114
134
  user_script: buildUserScript(script),
@@ -129,7 +149,11 @@ const dumpHtml = async (params) => {
129
149
  const { beat } = params;
130
150
  if (!beat.image || beat.image.type !== imageType)
131
151
  return;
132
- return joinHtml(beat.image.html);
152
+ const imageData = beat.image;
153
+ if (imageData.elements && Array.isArray(imageData.elements) && imageData.elements.length > 0) {
154
+ return swipeElementsToHtml(imageData.elements);
155
+ }
156
+ return joinHtml(imageData.html ?? "");
133
157
  };
134
158
  export const process = processHtmlTailwind;
135
159
  export const path = parrotingImagePath;
@@ -0,0 +1,55 @@
1
+ export interface SwipeTransition {
2
+ opacity?: number;
3
+ rotate?: number;
4
+ scale?: number | [number, number];
5
+ translate?: [number, number];
6
+ bc?: string;
7
+ timing?: [number, number];
8
+ }
9
+ export interface SwipeLoop {
10
+ style: "vibrate" | "blink" | "wiggle" | "spin" | "shift" | "bounce" | "pulse";
11
+ count?: number;
12
+ delta?: number;
13
+ duration?: number;
14
+ direction?: "n" | "s" | "e" | "w";
15
+ clockwise?: boolean;
16
+ }
17
+ export interface SwipeShadow {
18
+ color?: string;
19
+ offset?: [number, number];
20
+ opacity?: number;
21
+ radius?: number;
22
+ }
23
+ export interface SwipeElement {
24
+ id?: string;
25
+ x?: number | string;
26
+ y?: number | string;
27
+ w?: number | string;
28
+ h?: number | string;
29
+ pos?: [number | string, number | string];
30
+ bc?: string;
31
+ opacity?: number;
32
+ rotate?: number;
33
+ scale?: number | [number, number];
34
+ translate?: [number, number];
35
+ cornerRadius?: number;
36
+ borderWidth?: number;
37
+ borderColor?: string;
38
+ shadow?: SwipeShadow;
39
+ clip?: boolean;
40
+ text?: string;
41
+ fontSize?: number | string;
42
+ fontWeight?: string;
43
+ textColor?: string;
44
+ textAlign?: "center" | "left" | "right";
45
+ lineHeight?: number | string;
46
+ img?: string;
47
+ imgFit?: "contain" | "cover" | "fill";
48
+ to?: SwipeTransition;
49
+ loop?: SwipeLoop;
50
+ elements?: SwipeElement[];
51
+ }
52
+ /** Generate HTML from Swipe elements */
53
+ export declare const swipeElementsToHtml: (elements: SwipeElement[]) => string;
54
+ /** Generate render() script from Swipe element animations */
55
+ export declare const swipeElementsToScript: (elements: SwipeElement[]) => string;
@@ -0,0 +1,240 @@
1
+ const escapeHtml = (str) => {
2
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3
+ };
4
+ const toCssValue = (value) => {
5
+ return typeof value === "number" ? `${value}px` : value;
6
+ };
7
+ const buildElementStyle = (el) => {
8
+ const styles = ["position: absolute;"];
9
+ if (el.pos) {
10
+ styles.push(`left: ${toCssValue(el.pos[0])};`);
11
+ styles.push(`top: ${toCssValue(el.pos[1])};`);
12
+ }
13
+ else {
14
+ if (el.x !== undefined)
15
+ styles.push(`left: ${toCssValue(el.x)};`);
16
+ if (el.y !== undefined)
17
+ styles.push(`top: ${toCssValue(el.y)};`);
18
+ }
19
+ if (el.w !== undefined)
20
+ styles.push(`width: ${toCssValue(el.w)};`);
21
+ if (el.h !== undefined)
22
+ styles.push(`height: ${toCssValue(el.h)};`);
23
+ if (el.bc)
24
+ styles.push(`background: ${el.bc};`);
25
+ if (el.opacity !== undefined)
26
+ styles.push(`opacity: ${el.opacity};`);
27
+ if (el.cornerRadius !== undefined)
28
+ styles.push(`border-radius: ${el.cornerRadius}px;`);
29
+ if (el.borderWidth !== undefined)
30
+ styles.push(`border: ${el.borderWidth}px solid ${el.borderColor ?? "black"};`);
31
+ if (el.clip)
32
+ styles.push("overflow: hidden;");
33
+ if (el.shadow) {
34
+ const s = el.shadow;
35
+ const ox = s.offset?.[0] ?? 1;
36
+ const oy = s.offset?.[1] ?? 1;
37
+ const opacity = s.opacity ?? 0.5;
38
+ const radius = s.radius ?? 1;
39
+ styles.push(`filter: drop-shadow(${ox}px ${oy}px ${radius}px rgba(0,0,0,${opacity}));`);
40
+ }
41
+ const transforms = [];
42
+ if (el.pos)
43
+ transforms.push("translate(-50%, -50%)");
44
+ if (el.rotate)
45
+ transforms.push(`rotate(${el.rotate}deg)`);
46
+ if (el.scale !== undefined) {
47
+ const [sx, sy] = Array.isArray(el.scale) ? el.scale : [el.scale, el.scale];
48
+ transforms.push(`scale(${sx}, ${sy})`);
49
+ }
50
+ if (el.translate)
51
+ transforms.push(`translate(${el.translate[0]}px, ${el.translate[1]}px)`);
52
+ if (transforms.length > 0) {
53
+ styles.push(`transform: ${transforms.join(" ")};`);
54
+ }
55
+ return styles.join(" ");
56
+ };
57
+ const buildTextStyle = (el) => {
58
+ const styles = [];
59
+ if (el.fontSize)
60
+ styles.push(`font-size: ${toCssValue(el.fontSize)};`);
61
+ if (el.fontWeight)
62
+ styles.push(`font-weight: ${el.fontWeight};`);
63
+ if (el.textColor)
64
+ styles.push(`color: ${el.textColor};`);
65
+ if (el.textAlign)
66
+ styles.push(`text-align: ${el.textAlign};`);
67
+ if (el.lineHeight)
68
+ styles.push(`line-height: ${toCssValue(el.lineHeight)};`);
69
+ return styles.join(" ");
70
+ };
71
+ const elementToHtml = (el, index) => {
72
+ const id = el.id ?? `swipe_el_${index}`;
73
+ const style = buildElementStyle(el);
74
+ const textStyle = buildTextStyle(el);
75
+ const lines = [];
76
+ lines.push(`<div id="${escapeHtml(id)}" style="${escapeHtml(style)}">`);
77
+ if (el.img) {
78
+ const fit = el.imgFit ?? "contain";
79
+ lines.push(` <img src="${escapeHtml(el.img)}" style="width:100%; height:100%; object-fit:${fit};" />`);
80
+ }
81
+ if (el.text) {
82
+ lines.push(` <span style="${escapeHtml(textStyle)}">${escapeHtml(el.text)}</span>`);
83
+ }
84
+ if (el.elements) {
85
+ el.elements.forEach((child, childIdx) => {
86
+ lines.push(elementToHtml(child, index * 100 + childIdx));
87
+ });
88
+ }
89
+ lines.push("</div>");
90
+ return lines.join("\n");
91
+ };
92
+ /** Generate HTML from Swipe elements */
93
+ export const swipeElementsToHtml = (elements) => {
94
+ const html = elements.map((el, i) => elementToHtml(el, i)).join("\n");
95
+ return `<div style="position:relative; width:100%; height:100%; overflow:hidden;">\n${html}\n</div>`;
96
+ };
97
+ const collectAnimations = (elements, entries, indexBase = 0) => {
98
+ elements.forEach((el, i) => {
99
+ const id = el.id ?? `swipe_el_${indexBase + i}`;
100
+ if (el.to || el.loop) {
101
+ entries.push({ id, to: el.to, loop: el.loop });
102
+ }
103
+ if (el.elements) {
104
+ collectAnimations(el.elements, entries, (indexBase + i) * 100);
105
+ }
106
+ });
107
+ };
108
+ const generateTransitionCode = (id, to) => {
109
+ const timing = to.timing ?? [0, 1];
110
+ const props = [];
111
+ if (to.opacity !== undefined)
112
+ props.push(`opacity: [undefined, ${to.opacity}]`);
113
+ if (to.rotate !== undefined)
114
+ props.push(`rotate: [undefined, ${to.rotate}]`);
115
+ if (to.translate)
116
+ props.push(`translateX: [undefined, ${to.translate[0]}], translateY: [undefined, ${to.translate[1]}]`);
117
+ if (to.scale !== undefined) {
118
+ const [sx, sy] = Array.isArray(to.scale) ? to.scale : [to.scale, to.scale];
119
+ props.push(`scaleX: [undefined, ${sx}], scaleY: [undefined, ${sy}]`);
120
+ }
121
+ if (to.bc)
122
+ props.push(`backgroundColor: [undefined, '${to.bc}']`);
123
+ return `animation.animate('#${id}', { ${props.join(", ")} }, { start: ${timing[0]}, end: ${timing[1]}, easing: 'easeOut' });`;
124
+ };
125
+ const generateLoopCode = (id, loop) => {
126
+ const count = loop.count ?? 1;
127
+ const dur = loop.duration ?? 1;
128
+ const infinite = count === 0;
129
+ const base = { id, style: loop.style, duration: dur, count, infinite };
130
+ switch (loop.style) {
131
+ case "wiggle":
132
+ return `__swipe_loops.push(${JSON.stringify({ ...base, delta: loop.delta ?? 15 })});`;
133
+ case "vibrate":
134
+ return `__swipe_loops.push(${JSON.stringify({ ...base, delta: loop.delta ?? 10 })});`;
135
+ case "bounce":
136
+ return `__swipe_loops.push(${JSON.stringify({ ...base, delta: loop.delta ?? 20 })});`;
137
+ case "pulse":
138
+ return `__swipe_loops.push(${JSON.stringify({ ...base, delta: loop.delta ?? 0.1 })});`;
139
+ case "blink":
140
+ return `__swipe_loops.push(${JSON.stringify(base)});`;
141
+ case "spin":
142
+ return `__swipe_loops.push(${JSON.stringify({ ...base, clockwise: loop.clockwise !== false })});`;
143
+ case "shift":
144
+ return `__swipe_loops.push(${JSON.stringify({ ...base, direction: loop.direction ?? "s" })});`;
145
+ }
146
+ };
147
+ /** Generate render() script from Swipe element animations */
148
+ export const swipeElementsToScript = (elements) => {
149
+ const entries = [];
150
+ collectAnimations(elements, entries);
151
+ if (entries.length === 0)
152
+ return "";
153
+ const lines = [];
154
+ // Transition animations via MulmoAnimation
155
+ const hasTransitions = entries.some((e) => e.to);
156
+ if (hasTransitions) {
157
+ lines.push("const animation = new MulmoAnimation();");
158
+ entries.forEach((entry) => {
159
+ if (entry.to) {
160
+ lines.push(generateTransitionCode(entry.id, entry.to));
161
+ }
162
+ });
163
+ }
164
+ // Loop animations via custom render
165
+ const hasLoops = entries.some((e) => e.loop);
166
+ if (hasLoops) {
167
+ lines.push("const __swipe_loops = [];");
168
+ entries.forEach((entry) => {
169
+ if (entry.loop) {
170
+ lines.push(generateLoopCode(entry.id, entry.loop));
171
+ }
172
+ });
173
+ lines.push("");
174
+ lines.push(LOOP_PROCESSOR);
175
+ // Store base transforms on init
176
+ lines.push("");
177
+ lines.push("(function() {");
178
+ lines.push(" __swipe_loops.forEach(function(lp) {");
179
+ lines.push(" const el = document.getElementById(lp.id);");
180
+ lines.push(" if (el) el.dataset.baseTransform = el.style.transform || '';");
181
+ lines.push(" });");
182
+ lines.push("})();");
183
+ }
184
+ // Generate render function
185
+ if (hasTransitions && hasLoops) {
186
+ lines.push("");
187
+ lines.push("function render(frame, totalFrames, fps) {");
188
+ lines.push(" animation.update(frame, fps);");
189
+ lines.push(" __processLoops(frame / fps);");
190
+ lines.push("}");
191
+ }
192
+ else if (hasLoops) {
193
+ lines.push("");
194
+ lines.push("function render(frame, totalFrames, fps) {");
195
+ lines.push(" __processLoops(frame / fps);");
196
+ lines.push("}");
197
+ }
198
+ return lines.join("\n");
199
+ };
200
+ const LOOP_PROCESSOR = `function __processLoops(t) {
201
+ __swipe_loops.forEach(function(lp) {
202
+ var el = document.getElementById(lp.id);
203
+ if (!el) return;
204
+ var cycleT = lp.duration > 0 ? (t % lp.duration) / lp.duration : 0;
205
+ var totalCycles = lp.duration > 0 ? t / lp.duration : 0;
206
+ if (!lp.infinite && totalCycles >= lp.count) return;
207
+ var phase = cycleT * Math.PI * 2;
208
+ var base = el.dataset.baseTransform || '';
209
+ switch(lp.style) {
210
+ case 'wiggle':
211
+ el.style.transform = base + ' rotate(' + (Math.sin(phase) * lp.delta) + 'deg)';
212
+ break;
213
+ case 'vibrate':
214
+ el.style.transform = base + ' translateX(' + (Math.sin(phase) * lp.delta) + 'px)';
215
+ break;
216
+ case 'bounce':
217
+ el.style.transform = base + ' translateY(' + (-Math.abs(Math.sin(phase)) * lp.delta) + 'px)';
218
+ break;
219
+ case 'pulse':
220
+ var s = 1 + Math.sin(phase) * lp.delta;
221
+ el.style.transform = base + ' scale(' + s + ')';
222
+ break;
223
+ case 'blink':
224
+ el.style.opacity = 0.5 + Math.sin(phase) * 0.5;
225
+ break;
226
+ case 'spin': {
227
+ var deg = lp.clockwise ? cycleT * 360 : -cycleT * 360;
228
+ el.style.transform = base + ' rotate(' + deg + 'deg)';
229
+ break;
230
+ }
231
+ case 'shift': {
232
+ var dist = cycleT * 100;
233
+ var dx = lp.direction === 'e' ? dist : lp.direction === 'w' ? -dist : 0;
234
+ var dy = lp.direction === 's' ? dist : lp.direction === 'n' ? -dist : 0;
235
+ el.style.transform = base + ' translate(' + dx + '%, ' + dy + '%)';
236
+ break;
237
+ }
238
+ }
239
+ });
240
+ }`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -0,0 +1,120 @@
1
+ {
2
+ "$mulmocast": { "version": "1.1" },
3
+ "title": "マコロアニメ プロトタイプ",
4
+ "description": "MulmoCastリミテッドアニメーションのテスト",
5
+ "lang": "ja",
6
+ "speechParams": {
7
+ "provider": "openai",
8
+ "speakers": {
9
+ "macoro": {
10
+ "voiceId": "nova",
11
+ "displayName": { "ja": "マコロ" }
12
+ }
13
+ }
14
+ },
15
+ "beats": [
16
+ {
17
+ "speaker": "macoro",
18
+ "text": "こんにちは!ぼくマコロだよ!今日はみんなに会えてうれしいな!",
19
+ "image": {
20
+ "type": "html_tailwind",
21
+ "html": [
22
+ "<div class='h-full w-full relative overflow-hidden' style='background: linear-gradient(135deg, #fce4ec 0%, #f8bbd0 30%, #e1bee7 60%, #bbdefb 100%);'>",
23
+ "",
24
+ " <!-- 背景の浮遊パーティクル -->",
25
+ " <div id='p1' class='absolute w-4 h-4 rounded-full' style='background: rgba(255,255,255,0.6); left:10%; top:80%;'></div>",
26
+ " <div id='p2' class='absolute w-3 h-3 rounded-full' style='background: rgba(255,200,200,0.5); left:30%; top:85%;'></div>",
27
+ " <div id='p3' class='absolute w-5 h-5 rounded-full' style='background: rgba(200,200,255,0.5); left:70%; top:75%;'></div>",
28
+ " <div id='p4' class='absolute w-3 h-3 rounded-full' style='background: rgba(255,255,200,0.6); left:85%; top:90%;'></div>",
29
+ " <div id='p5' class='absolute w-4 h-4 rounded-full' style='background: rgba(200,255,200,0.5); left:50%; top:82%;'></div>",
30
+ "",
31
+ " <!-- 背景の星 -->",
32
+ " <div id='star1' class='absolute text-4xl' style='left:15%; top:15%;'>✦</div>",
33
+ " <div id='star2' class='absolute text-3xl' style='left:80%; top:20%;'>✦</div>",
34
+ " <div id='star3' class='absolute text-2xl' style='left:60%; top:10%;'>✦</div>",
35
+ "",
36
+ " <!-- マコロ本体 -->",
37
+ " <div id='macoro-container' class='absolute' style='bottom:5%; left:50%; transform:translateX(-50%);'>",
38
+ " <img id='macoro' src='https://raw.githubusercontent.com/receptron/mulmocast-media/main/characters/macoro.png' style='width:500px; filter:drop-shadow(0 10px 20px rgba(0,0,0,0.2));' />",
39
+ " </div>",
40
+ "",
41
+ " <!-- 吹き出し -->",
42
+ " <div id='bubble' class='absolute' style='top:8%; right:8%; opacity:0;'>",
43
+ " <div class='bg-white rounded-3xl px-8 py-5 shadow-lg relative' style='max-width:380px;'>",
44
+ " <p id='bubble-text' class='text-2xl font-bold text-gray-700'></p>",
45
+ " <div class='absolute -bottom-3 left-12 w-6 h-6 bg-white transform rotate-45'></div>",
46
+ " </div>",
47
+ " </div>",
48
+ "",
49
+ "</div>"
50
+ ],
51
+ "script": [
52
+ "const fullText = 'こんにちは!ぼくマコロだよ!\\n今日はみんなに会えてうれしいな!';",
53
+ "",
54
+ "function render(frame, totalFrames, fps) {",
55
+ " const t = frame / fps;",
56
+ " const macoro = document.getElementById('macoro-container');",
57
+ " const bubble = document.getElementById('bubble');",
58
+ " const bubbleText = document.getElementById('bubble-text');",
59
+ "",
60
+ " // --- マコロの動き ---",
61
+ " // 登場: 下からバウンスイン (0-1秒)",
62
+ " let macoroY = 0;",
63
+ " if (t < 1.0) {",
64
+ " const p = t / 1.0;",
65
+ " const bounce = Math.sin(p * Math.PI) * 30;",
66
+ " macoroY = (1 - p) * 200 - bounce;",
67
+ " }",
68
+ "",
69
+ " // 話している時のぴょこぴょこ (1秒以降)",
70
+ " if (t >= 1.0) {",
71
+ " const talkBounce = Math.sin(t * 8) * 8;",
72
+ " const talkTilt = Math.sin(t * 5) * 3;",
73
+ " macoroY = talkBounce;",
74
+ " macoro.style.transform = 'translateX(-50%) translateY(' + macoroY + 'px) rotate(' + talkTilt + 'deg)';",
75
+ " } else {",
76
+ " macoro.style.transform = 'translateX(-50%) translateY(' + macoroY + 'px)';",
77
+ " }",
78
+ "",
79
+ " // --- 吹き出し ---",
80
+ " if (t >= 0.8) {",
81
+ " const bubbleP = Math.min((t - 0.8) / 0.3, 1);",
82
+ " const scale = 0.5 + bubbleP * 0.5;",
83
+ " bubble.style.opacity = bubbleP;",
84
+ " bubble.style.transform = 'scale(' + scale + ')';",
85
+ "",
86
+ " // タイプライター効果",
87
+ " const textProgress = Math.min((t - 1.0) / 3.0, 1);",
88
+ " if (textProgress > 0) {",
89
+ " const charCount = Math.floor(textProgress * fullText.length);",
90
+ " bubbleText.innerHTML = fullText.substring(0, charCount).replace('\\n', '<br>');",
91
+ " }",
92
+ " }",
93
+ "",
94
+ " // --- 背景パーティクル ---",
95
+ " for (let i = 1; i <= 5; i++) {",
96
+ " const p = document.getElementById('p' + i);",
97
+ " const speed = 0.3 + i * 0.15;",
98
+ " const sway = Math.sin(t * (1 + i * 0.3) + i) * 20;",
99
+ " const y = ((1 - ((t * speed * 0.1) % 1)) * 110) - 10;",
100
+ " p.style.top = y + '%';",
101
+ " p.style.transform = 'translateX(' + sway + 'px)';",
102
+ " }",
103
+ "",
104
+ " // --- 背景の星キラキラ ---",
105
+ " for (let i = 1; i <= 3; i++) {",
106
+ " const star = document.getElementById('star' + i);",
107
+ " const twinkle = 0.3 + Math.sin(t * 3 + i * 2) * 0.7;",
108
+ " const starScale = 0.8 + Math.sin(t * 2 + i) * 0.3;",
109
+ " star.style.opacity = twinkle;",
110
+ " star.style.transform = 'scale(' + starScale + ')';",
111
+ " star.style.color = 'rgba(255,200,50,' + twinkle + ')';",
112
+ " }",
113
+ "}"
114
+ ],
115
+ "animation": { "fps": 24 }
116
+ },
117
+ "duration": 6
118
+ }
119
+ ]
120
+ }