mulmocast 2.4.9 → 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.
@@ -1,10 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import nodePath from "node:path";
3
3
  import { MulmoBeatMethods } from "../../methods/mulmo_beat.js";
4
- import { getHTMLFile } from "../file.js";
4
+ 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,11 +87,14 @@ 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,
95
+ animation_runtime: getJSFile("animation_runtime"),
96
+ data_attribute_registration: getJSFile("data_attribute_registration"),
97
+ auto_render: getJSFile("auto_render"),
75
98
  user_script: buildUserScript(script),
76
99
  totalFrames: String(totalFrames),
77
100
  fps: String(fps),
@@ -103,9 +126,9 @@ const processHtmlTailwindStatic = async (params) => {
103
126
  const { beat, imagePath, canvasSize, context } = params;
104
127
  if (!beat.image || beat.image.type !== imageType)
105
128
  return;
106
- const html = joinHtml(beat.image.html);
129
+ const imageData = beat.image;
130
+ const { html, script } = resolveHtmlAndScript(imageData);
107
131
  const template = getHTMLFile("tailwind");
108
- const script = "script" in beat.image ? beat.image.script : undefined;
109
132
  const rawHtmlData = interpolate(template, {
110
133
  html_body: html,
111
134
  user_script: buildUserScript(script),
@@ -126,7 +149,11 @@ const dumpHtml = async (params) => {
126
149
  const { beat } = params;
127
150
  if (!beat.image || beat.image.type !== imageType)
128
151
  return;
129
- 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 ?? "");
130
157
  };
131
158
  export const process = processHtmlTailwind;
132
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.4.9",
3
+ "version": "2.6.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -24,7 +24,8 @@
24
24
  }
25
25
  },
26
26
  "resolutions": {
27
- "minimatch": "^10.2.4"
27
+ "minimatch": "^10.2.4",
28
+ "yauzl": "^3.2.1"
28
29
  },
29
30
  "bin": {
30
31
  "mulmo": "lib/cli/bin.js",
@@ -56,7 +57,7 @@
56
57
  "pdf": "npx tsx ./src/cli/bin.ts pdf",
57
58
  "test": "rm -f scratchpad/test*.* && npx tsx ./src/audio.ts scripts/test/test.json && npx tsx ./src/images.ts scripts/test/test.json && npx tsx ./src/movie.ts scripts/test/test.json",
58
59
  "ci_test": "cross-env NODE_ENV=test tsx --test --experimental-test-coverage ./test/*/test_*.ts",
59
- "lint": "eslint src test",
60
+ "lint": "eslint src test assets/html/js",
60
61
  "build": "tsc",
61
62
  "build_test": "tsc && git checkout -- lib/*",
62
63
  "cli": "npx tsx ./src/cli/bin.ts",
@@ -68,7 +69,7 @@
68
69
  "story_to_script": "npx tsx ./src/cli/bin.ts tool story_to_script",
69
70
  "whisper": "npx tsx ./src/cli/bin.ts tool whisper",
70
71
  "latest": "yarn upgrade-interactive --latest",
71
- "format": "prettier --write '{src,scripts,assets/templates,assets/styles,draft,ideason,scripts_mag2,proto,test,batch,graphai,output,docs/scripts}/**/*.{ts,json,yaml}'",
72
+ "format": "prettier --write '{src,scripts,assets/templates,assets/styles,assets/html/js,draft,ideason,scripts_mag2,proto,test,batch,graphai,output,docs/scripts}/**/*.{ts,js,json,yaml}'",
72
73
  "deep_research": "npx tsx ./src/tools/deep_research.ts",
73
74
  "template": "npx tsx batch/template2tsobject.ts && yarn run format",
74
75
  "mcp_server": "npx tsx ./src/mcp/server.ts",
@@ -109,9 +110,9 @@
109
110
  "graphai": "^2.0.16",
110
111
  "jsdom": "^28.1.0",
111
112
  "marked": "^17.0.4",
112
- "mulmocast-vision": "^1.0.8",
113
+ "mulmocast-vision": "^1.0.9",
113
114
  "ora": "^9.3.0",
114
- "puppeteer": "^24.39.0",
115
+ "puppeteer": "^24.39.1",
115
116
  "replicate": "^1.4.0",
116
117
  "yaml": "^2.8.2",
117
118
  "yargs": "^18.0.0",
@@ -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
+ }
@@ -0,0 +1,104 @@
1
+ {
2
+ "$mulmocast": { "version": "1.1" },
3
+ "title": "マコロ Swipeアニメ プロトタイプ",
4
+ "description": "Swipe風宣言的elementsによるアニメーションテスト",
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
+ "duration": 6,
20
+ "image": {
21
+ "type": "html_tailwind",
22
+ "elements": [
23
+ {
24
+ "id": "bg",
25
+ "w": "100%",
26
+ "h": "100%",
27
+ "bc": "linear-gradient(135deg, #fce4ec 0%, #f8bbd0 30%, #e1bee7 60%, #bbdefb 100%)"
28
+ },
29
+ {
30
+ "id": "star1",
31
+ "text": "✦",
32
+ "fontSize": "36px",
33
+ "textColor": "gold",
34
+ "pos": ["15%", "15%"],
35
+ "opacity": 0.3,
36
+ "loop": { "style": "blink", "count": 0, "duration": 2 }
37
+ },
38
+ {
39
+ "id": "star2",
40
+ "text": "✦",
41
+ "fontSize": "28px",
42
+ "textColor": "gold",
43
+ "pos": ["80%", "20%"],
44
+ "opacity": 0.3,
45
+ "loop": { "style": "blink", "count": 0, "duration": 3 }
46
+ },
47
+ {
48
+ "id": "star3",
49
+ "text": "✦",
50
+ "fontSize": "22px",
51
+ "textColor": "gold",
52
+ "pos": ["60%", "10%"],
53
+ "opacity": 0.3,
54
+ "loop": { "style": "blink", "count": 0, "duration": 1.5 }
55
+ },
56
+ {
57
+ "id": "macoro",
58
+ "img": "https://raw.githubusercontent.com/receptron/mulmocast-media/main/characters/macoro.png",
59
+ "pos": ["50%", "65%"],
60
+ "w": 500,
61
+ "h": 300,
62
+ "imgFit": "contain",
63
+ "opacity": 0,
64
+ "shadow": { "color": "black", "offset": [0, 10], "opacity": 0.2, "radius": 20 },
65
+ "to": {
66
+ "opacity": 1,
67
+ "translate": [0, -20],
68
+ "timing": [0, 0.15]
69
+ },
70
+ "loop": { "style": "wiggle", "delta": 3, "count": 0, "duration": 0.5 }
71
+ },
72
+ {
73
+ "id": "bubble",
74
+ "pos": ["72%", "18%"],
75
+ "w": 350,
76
+ "h": 120,
77
+ "bc": "white",
78
+ "cornerRadius": 24,
79
+ "opacity": 0,
80
+ "shadow": { "color": "black", "offset": [0, 4], "opacity": 0.1, "radius": 10 },
81
+ "to": {
82
+ "opacity": 1,
83
+ "scale": 1,
84
+ "timing": [0.1, 0.2]
85
+ },
86
+ "elements": [
87
+ {
88
+ "id": "bubble-text",
89
+ "x": 24,
90
+ "y": 20,
91
+ "w": 300,
92
+ "text": "こんにちは!ぼくマコロだよ!今日はみんなに会えてうれしいな!",
93
+ "fontSize": "20px",
94
+ "fontWeight": "bold",
95
+ "textColor": "#555"
96
+ }
97
+ ]
98
+ }
99
+ ],
100
+ "animation": { "fps": 24 }
101
+ }
102
+ }
103
+ ]
104
+ }