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.
- package/lib/types/schema.d.ts +11 -5
- package/lib/types/schema.js +71 -1
- package/lib/utils/context.d.ts +4 -2
- package/lib/utils/image_plugins/html_tailwind.js +29 -5
- package/lib/utils/swipe_to_html.d.ts +55 -0
- package/lib/utils/swipe_to_html.js +240 -0
- package/package.json +1 -1
- package/scripts/test/macoro_anime_proto.json +120 -0
- package/scripts/test/macoro_swipe_proto.json +104 -0
- package/scripts/test/macoro_swipe_rich.json +820 -0
package/lib/types/schema.d.ts
CHANGED
|
@@ -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>;
|
package/lib/types/schema.js
CHANGED
|
@@ -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."),
|
package/lib/utils/context.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
@@ -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
|
+
}
|