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.
- package/assets/html/js/animation_runtime.js +371 -0
- package/assets/html/js/auto_render.js +64 -0
- package/assets/html/js/data_attribute_registration.js +178 -0
- package/assets/html/tailwind_animated.html +37 -290
- package/lib/actions/image_references.js +1 -1
- package/lib/types/schema.d.ts +47 -5
- package/lib/types/schema.js +72 -1
- package/lib/utils/context.d.ts +16 -2
- package/lib/utils/file.d.ts +1 -0
- package/lib/utils/file.js +4 -0
- package/lib/utils/image_plugins/html_tailwind.js +33 -6
- package/lib/utils/swipe_to_html.d.ts +55 -0
- package/lib/utils/swipe_to_html.js +240 -0
- package/package.json +7 -6
- package/scripts/test/images/qa_landscape.jpg +0 -0
- package/scripts/test/images/qa_portrait.png +0 -0
- 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/scripts/test/test_animated.json +592 -0
- package/scripts/test/test_data_animation.json +149 -0
- package/scripts/test/test_html_cover_pan_zoom_landscape_canvas.json +245 -0
- package/scripts/test/test_html_cover_pan_zoom_portrait_canvas.json +207 -0
- package/scripts/test/test_reference_canvas_size.json +83 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// === MulmoCast Animation Runtime ===
|
|
2
|
+
// Extracted from tailwind_animated.html for maintainability and testability.
|
|
3
|
+
// This file is loaded by the template at runtime via fs.readFileSync.
|
|
4
|
+
//
|
|
5
|
+
// NOTE: Top-level declarations use `var` intentionally — these files run in
|
|
6
|
+
// browser global scope AND are tested via Node.js `vm.runInContext`, where
|
|
7
|
+
// only `var` (not `const`/`let`) creates properties on the sandbox context.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Easing functions for non-linear interpolation.
|
|
11
|
+
*/
|
|
12
|
+
var Easing = {
|
|
13
|
+
linear: function (t) {
|
|
14
|
+
return t;
|
|
15
|
+
},
|
|
16
|
+
easeIn: function (t) {
|
|
17
|
+
return t * t;
|
|
18
|
+
},
|
|
19
|
+
easeOut: function (t) {
|
|
20
|
+
return 1 - (1 - t) * (1 - t);
|
|
21
|
+
},
|
|
22
|
+
easeInOut: function (t) {
|
|
23
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse a numeric value, returning fallback if not finite.
|
|
29
|
+
* Shared utility used by MulmoAnimation and data-attribute registration.
|
|
30
|
+
*
|
|
31
|
+
* @param {*} value - Value to parse
|
|
32
|
+
* @param {number} fallback - Default if value is not a finite number
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
var toFiniteNumber = function (value, fallback) {
|
|
36
|
+
const n = Number(value);
|
|
37
|
+
return Number.isFinite(n) ? n : fallback;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Interpolation with clamping and optional easing.
|
|
42
|
+
*
|
|
43
|
+
* @param {number} value - Current value (typically frame number)
|
|
44
|
+
* @param {Object} opts - { input: { inMin, inMax }, output: { outMin, outMax }, easing?: string | function }
|
|
45
|
+
* @returns {number} Interpolated and clamped value
|
|
46
|
+
*/
|
|
47
|
+
var interpolate = function (value, opts) {
|
|
48
|
+
const inMin = opts.input.inMin;
|
|
49
|
+
const inMax = opts.input.inMax;
|
|
50
|
+
const outMin = opts.output.outMin;
|
|
51
|
+
const outMax = opts.output.outMax;
|
|
52
|
+
if (inMax === inMin) {
|
|
53
|
+
return outMin;
|
|
54
|
+
}
|
|
55
|
+
const easing = !opts.easing ? Easing.linear : typeof opts.easing === "function" ? opts.easing : Easing[opts.easing] || Easing.linear;
|
|
56
|
+
const progress = Math.max(0, Math.min(1, (value - inMin) / (inMax - inMin)));
|
|
57
|
+
return outMin + easing(progress) * (outMax - outMin);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// === MulmoAnimation Helper Class ===
|
|
61
|
+
|
|
62
|
+
var TRANSFORM_PROPS = { translateX: "px", translateY: "px", scale: "", rotate: "deg", rotateX: "deg", rotateY: "deg", rotateZ: "deg" };
|
|
63
|
+
var SVG_PROPS = [
|
|
64
|
+
"r",
|
|
65
|
+
"cx",
|
|
66
|
+
"cy",
|
|
67
|
+
"x",
|
|
68
|
+
"y",
|
|
69
|
+
"x1",
|
|
70
|
+
"y1",
|
|
71
|
+
"x2",
|
|
72
|
+
"y2",
|
|
73
|
+
"rx",
|
|
74
|
+
"ry",
|
|
75
|
+
"width",
|
|
76
|
+
"height",
|
|
77
|
+
"stroke-width",
|
|
78
|
+
"stroke-dashoffset",
|
|
79
|
+
"stroke-dasharray",
|
|
80
|
+
"opacity",
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
var MulmoAnimation = function () {
|
|
84
|
+
this._entries = [];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register a property animation on a single element.
|
|
89
|
+
* @param {string} selector - CSS selector (e.g. '#title')
|
|
90
|
+
* @param {Object} props - { opacity: [0, 1], translateY: [30, 0], width: [0, 80, '%'] }
|
|
91
|
+
* @param {Object} opts - { start, end, easing } (start/end in seconds)
|
|
92
|
+
*/
|
|
93
|
+
MulmoAnimation.prototype.animate = function (selector, props, opts) {
|
|
94
|
+
this._entries.push({ kind: "animate", selector: selector, props: props, opts: opts || {} });
|
|
95
|
+
return this;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stagger animation across numbered elements.
|
|
100
|
+
* Selector must contain {i} placeholder (e.g. '#item{i}').
|
|
101
|
+
*/
|
|
102
|
+
MulmoAnimation.prototype.stagger = function (selector, count, props, opts) {
|
|
103
|
+
this._entries.push({ kind: "stagger", selector: selector, count: count, props: props, opts: opts || {} });
|
|
104
|
+
return this;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Typewriter effect — reveal text character by character. */
|
|
108
|
+
MulmoAnimation.prototype.typewriter = function (selector, text, opts) {
|
|
109
|
+
this._entries.push({ kind: "typewriter", selector: selector, text: text, opts: opts || {} });
|
|
110
|
+
return this;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Animated counter — interpolate a number and display with optional prefix/suffix. */
|
|
114
|
+
MulmoAnimation.prototype.counter = function (selector, range, opts) {
|
|
115
|
+
this._entries.push({ kind: "counter", selector: selector, range: range, opts: opts || {} });
|
|
116
|
+
return this;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Code reveal — show lines of code one by one. */
|
|
120
|
+
MulmoAnimation.prototype.codeReveal = function (selector, lines, opts) {
|
|
121
|
+
this._entries.push({ kind: "codeReveal", selector: selector, lines: lines, opts: opts || {} });
|
|
122
|
+
return this;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Blink — periodic show/hide toggle. */
|
|
126
|
+
MulmoAnimation.prototype.blink = function (selector, opts) {
|
|
127
|
+
this._entries.push({ kind: "blink", selector: selector, opts: opts || {} });
|
|
128
|
+
return this;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Cover zoom for image/video elements. */
|
|
132
|
+
MulmoAnimation.prototype.coverZoom = function (selector, opts) {
|
|
133
|
+
this._entries.push({ kind: "coverZoom", selector: selector, opts: opts || {} });
|
|
134
|
+
return this;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Cover pan for image/video elements with automatic black-border prevention. */
|
|
138
|
+
MulmoAnimation.prototype.coverPan = function (selector, opts) {
|
|
139
|
+
this._entries.push({ kind: "coverPan", selector: selector, opts: opts || {} });
|
|
140
|
+
return this;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Resolve easing name string or function to an easing function */
|
|
144
|
+
MulmoAnimation.prototype._resolveEasing = function (e) {
|
|
145
|
+
if (!e) return Easing.linear;
|
|
146
|
+
if (typeof e === "function") return e;
|
|
147
|
+
return Easing[e] || Easing.linear;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Convert value to finite number, otherwise return fallback */
|
|
151
|
+
MulmoAnimation.prototype._toFiniteNumber = function (value, fallback) {
|
|
152
|
+
return toFiniteNumber(value, fallback);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Apply props to element at a given progress (0-1) with easing */
|
|
156
|
+
MulmoAnimation.prototype._applyProps = function (el, props, progress, easingFn) {
|
|
157
|
+
if (!el) return;
|
|
158
|
+
const self = this;
|
|
159
|
+
const transforms = [];
|
|
160
|
+
Object.keys(props).forEach(function (prop) {
|
|
161
|
+
const spec = props[prop];
|
|
162
|
+
const from = self._toFiniteNumber(spec[0], 0);
|
|
163
|
+
const to = self._toFiniteNumber(spec[1], from);
|
|
164
|
+
const unit = spec.length > 2 ? spec[2] : null;
|
|
165
|
+
const val = from + easingFn(progress) * (to - from);
|
|
166
|
+
|
|
167
|
+
if (TRANSFORM_PROPS.hasOwnProperty(prop)) {
|
|
168
|
+
const tUnit = unit || TRANSFORM_PROPS[prop];
|
|
169
|
+
transforms.push(prop === "scale" ? "scale(" + val + ")" : prop + "(" + val + tUnit + ")");
|
|
170
|
+
} else if (el instanceof SVGElement && SVG_PROPS.indexOf(prop) !== -1) {
|
|
171
|
+
el.setAttribute(prop, val);
|
|
172
|
+
} else if (prop === "opacity") {
|
|
173
|
+
el.style.opacity = val;
|
|
174
|
+
} else {
|
|
175
|
+
const cssUnit = unit || "px";
|
|
176
|
+
el.style[prop] = val + cssUnit;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
if (transforms.length > 0) {
|
|
180
|
+
el.style.transform = transforms.join(" ");
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Resolve container for cover-based helpers */
|
|
185
|
+
MulmoAnimation.prototype._resolveContainer = function (el, selector) {
|
|
186
|
+
if (selector) return document.querySelector(selector) || (el ? el.parentElement : null);
|
|
187
|
+
return el ? el.parentElement : null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Ensure cover target is positioned in the same container used for sizing */
|
|
191
|
+
MulmoAnimation.prototype._prepareCoverContext = function (el, container) {
|
|
192
|
+
if (!el || !container) return null;
|
|
193
|
+
if (el.contains(container)) {
|
|
194
|
+
container = el.parentElement;
|
|
195
|
+
if (!container) return null;
|
|
196
|
+
}
|
|
197
|
+
const computed = window.getComputedStyle(container);
|
|
198
|
+
if (computed.position === "static") {
|
|
199
|
+
container.style.position = "relative";
|
|
200
|
+
}
|
|
201
|
+
if (el.parentElement !== container) {
|
|
202
|
+
try {
|
|
203
|
+
container.appendChild(el);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.warn("MulmoAnimation: failed to move cover element into container", e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return container;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** Calculate cover-scaled dimensions from intrinsic media size */
|
|
212
|
+
MulmoAnimation.prototype._coverSize = function (el, container, zoom) {
|
|
213
|
+
if (!el || !container) return null;
|
|
214
|
+
const intrinsicW = el.naturalWidth || el.videoWidth;
|
|
215
|
+
const intrinsicH = el.naturalHeight || el.videoHeight;
|
|
216
|
+
if (!intrinsicW || !intrinsicH) return null;
|
|
217
|
+
const ww = container.clientWidth || container.offsetWidth;
|
|
218
|
+
const wh = container.clientHeight || container.offsetHeight;
|
|
219
|
+
if (!ww || !wh) return null;
|
|
220
|
+
const cover = Math.max(ww / intrinsicW, wh / intrinsicH);
|
|
221
|
+
const s = cover * zoom;
|
|
222
|
+
return { ww: ww, wh: wh, iw: intrinsicW * s, ih: intrinsicH * s };
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/** Apply absolute-centered base style for cover helpers */
|
|
226
|
+
MulmoAnimation.prototype._applyCoverBaseStyle = function (el, iw, ih) {
|
|
227
|
+
el.style.position = "absolute";
|
|
228
|
+
el.style.top = "50%";
|
|
229
|
+
el.style.left = "50%";
|
|
230
|
+
el.style.transform = "translate(-50%,-50%)";
|
|
231
|
+
el.style.maxWidth = "none";
|
|
232
|
+
el.style.maxHeight = "none";
|
|
233
|
+
el.style.width = iw + "px";
|
|
234
|
+
el.style.height = ih + "px";
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update all registered animations for the given frame.
|
|
239
|
+
* @param {number} frame - current frame number
|
|
240
|
+
* @param {number} fps - frames per second
|
|
241
|
+
*/
|
|
242
|
+
MulmoAnimation.prototype.update = function (frame, fps) {
|
|
243
|
+
const self = this;
|
|
244
|
+
const autoEndFrame = Math.max(0, window.__MULMO.totalFrames - 1);
|
|
245
|
+
this._entries.forEach(function (entry) {
|
|
246
|
+
const opts = entry.opts;
|
|
247
|
+
const easingFn = self._resolveEasing(opts.easing);
|
|
248
|
+
|
|
249
|
+
if (entry.kind === "animate") {
|
|
250
|
+
const startFrame = (opts.start || 0) * fps;
|
|
251
|
+
const endFrame = opts.end === "auto" ? autoEndFrame : (opts.end || 0) * fps;
|
|
252
|
+
const progress = Math.max(0, Math.min(1, endFrame === startFrame ? 1 : (frame - startFrame) / (endFrame - startFrame)));
|
|
253
|
+
const el = document.querySelector(entry.selector);
|
|
254
|
+
self._applyProps(el, entry.props, progress, easingFn);
|
|
255
|
+
} else if (entry.kind === "stagger") {
|
|
256
|
+
const baseStart = (opts.start || 0) * fps;
|
|
257
|
+
const staggerDelay = (opts.stagger !== undefined ? opts.stagger : 0.2) * fps;
|
|
258
|
+
const dur = (opts.duration !== undefined ? opts.duration : 0.5) * fps;
|
|
259
|
+
for (let j = 0; j < entry.count; j++) {
|
|
260
|
+
const sel = entry.selector.replace(/\{i\}/g, j);
|
|
261
|
+
const sEl = document.querySelector(sel);
|
|
262
|
+
const sStart = baseStart + j * staggerDelay;
|
|
263
|
+
const sEnd = sStart + dur;
|
|
264
|
+
const sProgress = Math.max(0, Math.min(1, sEnd === sStart ? 1 : (frame - sStart) / (sEnd - sStart)));
|
|
265
|
+
self._applyProps(sEl, entry.props, sProgress, easingFn);
|
|
266
|
+
}
|
|
267
|
+
} else if (entry.kind === "typewriter") {
|
|
268
|
+
const twStart = (opts.start || 0) * fps;
|
|
269
|
+
const twEnd = opts.end === "auto" ? autoEndFrame : (opts.end || 0) * fps;
|
|
270
|
+
const twProgress = Math.max(0, Math.min(1, twEnd === twStart ? 1 : (frame - twStart) / (twEnd - twStart)));
|
|
271
|
+
const charCount = Math.floor(twProgress * entry.text.length);
|
|
272
|
+
const twEl = document.querySelector(entry.selector);
|
|
273
|
+
if (twEl) twEl.textContent = entry.text.substring(0, charCount);
|
|
274
|
+
} else if (entry.kind === "counter") {
|
|
275
|
+
const cStart = (opts.start || 0) * fps;
|
|
276
|
+
const cEnd = opts.end === "auto" ? autoEndFrame : (opts.end || 0) * fps;
|
|
277
|
+
const cProgress = Math.max(0, Math.min(1, cEnd === cStart ? 1 : (frame - cStart) / (cEnd - cStart)));
|
|
278
|
+
const cVal = entry.range[0] + easingFn(cProgress) * (entry.range[1] - entry.range[0]);
|
|
279
|
+
const decimals = opts.decimals || 0;
|
|
280
|
+
const display = (opts.prefix || "") + cVal.toFixed(decimals) + (opts.suffix || "");
|
|
281
|
+
const cEl = document.querySelector(entry.selector);
|
|
282
|
+
if (cEl) cEl.textContent = display;
|
|
283
|
+
} else if (entry.kind === "codeReveal") {
|
|
284
|
+
const crStart = (opts.start || 0) * fps;
|
|
285
|
+
const crEnd = opts.end === "auto" ? autoEndFrame : (opts.end || 0) * fps;
|
|
286
|
+
const crProgress = Math.max(0, Math.min(1, crEnd === crStart ? 1 : (frame - crStart) / (crEnd - crStart)));
|
|
287
|
+
const lineCount = Math.floor(crProgress * entry.lines.length);
|
|
288
|
+
const crEl = document.querySelector(entry.selector);
|
|
289
|
+
if (crEl) crEl.textContent = entry.lines.slice(0, lineCount).join("\n");
|
|
290
|
+
} else if (entry.kind === "blink") {
|
|
291
|
+
const interval_s = opts.interval || 0.5;
|
|
292
|
+
const blinkEl = document.querySelector(entry.selector);
|
|
293
|
+
if (blinkEl) {
|
|
294
|
+
const cycle = frame / fps / interval_s;
|
|
295
|
+
blinkEl.style.opacity = Math.floor(cycle) % 2 === 0 ? 1 : 0;
|
|
296
|
+
}
|
|
297
|
+
} else if (entry.kind === "coverZoom") {
|
|
298
|
+
const zEl = document.querySelector(entry.selector);
|
|
299
|
+
if (!zEl) return;
|
|
300
|
+
const zContainer = self._prepareCoverContext(zEl, self._resolveContainer(zEl, opts.containerSelector));
|
|
301
|
+
if (!zContainer) return;
|
|
302
|
+
const zStart = (opts.start || 0) * fps;
|
|
303
|
+
const zEnd = opts.end === "auto" ? autoEndFrame : (opts.end || 0) * fps;
|
|
304
|
+
const zProgress = Math.max(0, Math.min(1, zEnd === zStart ? 1 : (frame - zStart) / (zEnd - zStart)));
|
|
305
|
+
const zFrom = opts.zoomFrom === undefined ? (opts.from === undefined ? 1 : self._toFiniteNumber(opts.from, 1)) : self._toFiniteNumber(opts.zoomFrom, 1);
|
|
306
|
+
const safeZFrom = Math.max(1e-6, zFrom);
|
|
307
|
+
const zTo =
|
|
308
|
+
opts.zoomTo === undefined
|
|
309
|
+
? opts.to === undefined
|
|
310
|
+
? safeZFrom
|
|
311
|
+
: self._toFiniteNumber(opts.to, safeZFrom)
|
|
312
|
+
: self._toFiniteNumber(opts.zoomTo, safeZFrom);
|
|
313
|
+
const safeZTo = Math.max(1e-6, zTo);
|
|
314
|
+
const zCurrent = safeZFrom + easingFn(zProgress) * (safeZTo - safeZFrom);
|
|
315
|
+
const zSize = self._coverSize(zEl, zContainer, zCurrent);
|
|
316
|
+
if (!zSize) return;
|
|
317
|
+
self._applyCoverBaseStyle(zEl, zSize.iw, zSize.ih);
|
|
318
|
+
} else if (entry.kind === "coverPan") {
|
|
319
|
+
const pEl = document.querySelector(entry.selector);
|
|
320
|
+
if (!pEl) return;
|
|
321
|
+
const pContainer = self._prepareCoverContext(pEl, self._resolveContainer(pEl, opts.containerSelector));
|
|
322
|
+
if (!pContainer) return;
|
|
323
|
+
const pStart = (opts.start || 0) * fps;
|
|
324
|
+
const pEnd = opts.end === "auto" ? autoEndFrame : (opts.end || 0) * fps;
|
|
325
|
+
const pProgress = Math.max(0, Math.min(1, pEnd === pStart ? 1 : (frame - pStart) / (pEnd - pStart)));
|
|
326
|
+
const pAxis = opts.axis === "y" ? "y" : "x";
|
|
327
|
+
const pDirection = self._toFiniteNumber(opts.direction, 1) < 0 ? -1 : 1;
|
|
328
|
+
const pRequested = Math.abs(self._toFiniteNumber(opts.distance, 0));
|
|
329
|
+
const pZoom = Math.max(1e-6, self._toFiniteNumber(opts.zoom, 1));
|
|
330
|
+
const pSize = self._coverSize(pEl, pContainer, pZoom);
|
|
331
|
+
if (!pSize) return;
|
|
332
|
+
self._applyCoverBaseStyle(pEl, pSize.iw, pSize.ih);
|
|
333
|
+
|
|
334
|
+
const viewport = pAxis === "x" ? pSize.ww : pSize.wh;
|
|
335
|
+
const imageSize = pAxis === "x" ? pSize.iw : pSize.ih;
|
|
336
|
+
const maxDistancePercent = Math.max(0, ((imageSize - viewport) / 2 / viewport) * 100);
|
|
337
|
+
const minPos = 50 - maxDistancePercent;
|
|
338
|
+
const maxPos = 50 + maxDistancePercent;
|
|
339
|
+
const safeRange = maxPos - minPos;
|
|
340
|
+
const clampPercent = function (v) {
|
|
341
|
+
return Math.max(0, Math.min(100, v));
|
|
342
|
+
};
|
|
343
|
+
const mapToSafePos = function (v) {
|
|
344
|
+
return minPos + safeRange * (clampPercent(v) / 100);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
let panFrom, panTo;
|
|
348
|
+
if (opts.from !== undefined || opts.to !== undefined) {
|
|
349
|
+
const fromNorm = opts.from === undefined ? 50 : self._toFiniteNumber(opts.from, 50);
|
|
350
|
+
const toNorm = opts.to === undefined ? fromNorm : self._toFiniteNumber(opts.to, fromNorm);
|
|
351
|
+
panFrom = mapToSafePos(fromNorm);
|
|
352
|
+
panTo = mapToSafePos(toNorm);
|
|
353
|
+
} else {
|
|
354
|
+
const distancePercent = Math.min(pRequested, maxDistancePercent);
|
|
355
|
+
panFrom = 50;
|
|
356
|
+
panTo = panFrom + pDirection * distancePercent;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const clampedFrom = Math.max(minPos, Math.min(maxPos, panFrom));
|
|
360
|
+
const clampedTo = Math.max(minPos, Math.min(maxPos, panTo));
|
|
361
|
+
const current = clampedFrom + easingFn(pProgress) * (clampedTo - clampedFrom);
|
|
362
|
+
if (pAxis === "x") {
|
|
363
|
+
pEl.style.left = current + "%";
|
|
364
|
+
pEl.style.top = "50%";
|
|
365
|
+
} else {
|
|
366
|
+
pEl.style.top = current + "%";
|
|
367
|
+
pEl.style.left = "50%";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// === Auto-render and playAnimation ===
|
|
2
|
+
// Auto-render: if MulmoAnimation is used but render() is not defined, generate it.
|
|
3
|
+
// Check both local var (from user_script) and window.animation (from data-attribute auto-registration).
|
|
4
|
+
//
|
|
5
|
+
// NOTE: Top-level declarations use `var` intentionally — see animation_runtime.js header comment.
|
|
6
|
+
var _autoAnim =
|
|
7
|
+
typeof animation !== "undefined" && animation instanceof MulmoAnimation ? animation : window.animation instanceof MulmoAnimation ? window.animation : null;
|
|
8
|
+
if (typeof window.render !== "function" && typeof render === "function") {
|
|
9
|
+
window.render = render;
|
|
10
|
+
} else if (typeof window.render !== "function" && _autoAnim) {
|
|
11
|
+
window.render = function (frame, totalFrames, fps) {
|
|
12
|
+
_autoAnim.update(frame, fps);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initial render (frame 0)
|
|
17
|
+
if (typeof window.render === "function") {
|
|
18
|
+
try {
|
|
19
|
+
var _initResult = window.render(0, window.__MULMO.totalFrames, window.__MULMO.fps);
|
|
20
|
+
if (_initResult && typeof _initResult.then === "function") {
|
|
21
|
+
_initResult.catch(console.error);
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error("MulmoAnimation: initial render failed", e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Play animation in real-time using requestAnimationFrame.
|
|
30
|
+
* Returns a Promise that resolves when all frames have been rendered.
|
|
31
|
+
* Called by Puppeteer's page.evaluate() during screencast recording.
|
|
32
|
+
*/
|
|
33
|
+
window.playAnimation = function () {
|
|
34
|
+
return new Promise(function (resolve, reject) {
|
|
35
|
+
const mulmo = window.__MULMO;
|
|
36
|
+
const fps = mulmo.fps;
|
|
37
|
+
const totalFrames = mulmo.totalFrames;
|
|
38
|
+
const frameDuration = 1000 / fps;
|
|
39
|
+
let startTime = null;
|
|
40
|
+
|
|
41
|
+
async function tick(timestamp) {
|
|
42
|
+
if (startTime === null) startTime = timestamp;
|
|
43
|
+
const elapsed = timestamp - startTime;
|
|
44
|
+
const frame = Math.min(Math.floor(elapsed / frameDuration), totalFrames - 1);
|
|
45
|
+
|
|
46
|
+
mulmo.frame = frame;
|
|
47
|
+
if (typeof window.render === "function") {
|
|
48
|
+
await Promise.resolve(window.render(frame, totalFrames, fps));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (frame < totalFrames - 1) {
|
|
52
|
+
requestAnimationFrame(function (nextTimestamp) {
|
|
53
|
+
tick(nextTimestamp).catch(reject);
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
requestAnimationFrame(function (timestamp) {
|
|
61
|
+
tick(timestamp).catch(reject);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// === Data-attribute animation auto-registration ===
|
|
2
|
+
// Scan [data-animation] elements and register MulmoAnimation entries automatically.
|
|
3
|
+
// This lets users declare animations in HTML without writing JS.
|
|
4
|
+
//
|
|
5
|
+
// Supported data-animation values and their data-* params:
|
|
6
|
+
// animate — data-opacity, data-translate-x, data-translate-y, data-scale,
|
|
7
|
+
// data-rotate, data-rotate-x, data-rotate-y, data-rotate-z,
|
|
8
|
+
// data-width (use "80,%" for value+unit)
|
|
9
|
+
// stagger — same props as animate + data-count, data-stagger, data-duration
|
|
10
|
+
// counter — data-from, data-to, data-prefix, data-suffix, data-decimals
|
|
11
|
+
// typewriter — data-text
|
|
12
|
+
// codeReveal — data-lines (JSON array string)
|
|
13
|
+
// blink — data-interval
|
|
14
|
+
// coverZoom — data-zoom-from, data-zoom-to (or data-from, data-to)
|
|
15
|
+
// coverPan — data-axis, data-direction, data-distance, data-from, data-to, data-zoom
|
|
16
|
+
//
|
|
17
|
+
// Common opts: data-start, data-end, data-easing, data-container
|
|
18
|
+
(function () {
|
|
19
|
+
const els = document.querySelectorAll("[data-animation]");
|
|
20
|
+
if (els.length === 0) return;
|
|
21
|
+
|
|
22
|
+
// Reuse existing animation instance from user script, or create new one
|
|
23
|
+
const _anim = typeof animation !== "undefined" && animation instanceof MulmoAnimation ? animation : new MulmoAnimation();
|
|
24
|
+
|
|
25
|
+
function parseRange(v) {
|
|
26
|
+
// "0,1" → [0, 1] or "0,80,%" → [0, 80, '%']
|
|
27
|
+
if (!v) return null;
|
|
28
|
+
const parts = v.split(",");
|
|
29
|
+
const result = [];
|
|
30
|
+
parts.forEach(function (p) {
|
|
31
|
+
const trimmed = p.trim();
|
|
32
|
+
const n = Number(trimmed);
|
|
33
|
+
result.push(Number.isFinite(n) ? n : trimmed);
|
|
34
|
+
});
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function commonOpts(el) {
|
|
39
|
+
const opts = {};
|
|
40
|
+
const start = el.getAttribute("data-start");
|
|
41
|
+
const end = el.getAttribute("data-end");
|
|
42
|
+
const easing = el.getAttribute("data-easing");
|
|
43
|
+
const container = el.getAttribute("data-container");
|
|
44
|
+
if (start !== null) opts.start = toFiniteNumber(start, 0);
|
|
45
|
+
if (end !== null) opts.end = end === "auto" ? "auto" : toFiniteNumber(end, 0);
|
|
46
|
+
else opts.end = "auto";
|
|
47
|
+
if (easing) opts.easing = easing;
|
|
48
|
+
if (container) opts.containerSelector = container;
|
|
49
|
+
return opts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Animate prop names that map to data-* attributes
|
|
53
|
+
const ANIMATE_ATTRS = [
|
|
54
|
+
["data-opacity", "opacity"],
|
|
55
|
+
["data-translate-x", "translateX"],
|
|
56
|
+
["data-translate-y", "translateY"],
|
|
57
|
+
["data-scale", "scale"],
|
|
58
|
+
["data-rotate", "rotate"],
|
|
59
|
+
["data-rotate-x", "rotateX"],
|
|
60
|
+
["data-rotate-y", "rotateY"],
|
|
61
|
+
["data-rotate-z", "rotateZ"],
|
|
62
|
+
["data-width", "width"],
|
|
63
|
+
["data-height", "height"],
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function parseAnimateProps(el) {
|
|
67
|
+
const props = {};
|
|
68
|
+
ANIMATE_ATTRS.forEach(function (pair) {
|
|
69
|
+
const val = el.getAttribute(pair[0]);
|
|
70
|
+
if (val !== null) {
|
|
71
|
+
const range = parseRange(val);
|
|
72
|
+
if (range) props[pair[1]] = range;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return props;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate unique selector for element (add id if missing)
|
|
79
|
+
let autoIdCounter = 0;
|
|
80
|
+
function ensureSelector(el) {
|
|
81
|
+
if (el.id) return "#" + el.id;
|
|
82
|
+
const id = "__mulmo_da_" + autoIdCounter++;
|
|
83
|
+
el.id = id;
|
|
84
|
+
return "#" + id;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
els.forEach(function (el) {
|
|
88
|
+
const kind = el.getAttribute("data-animation");
|
|
89
|
+
const selector = ensureSelector(el);
|
|
90
|
+
const opts = commonOpts(el);
|
|
91
|
+
|
|
92
|
+
switch (kind) {
|
|
93
|
+
case "animate": {
|
|
94
|
+
const props = parseAnimateProps(el);
|
|
95
|
+
if (Object.keys(props).length > 0) {
|
|
96
|
+
_anim.animate(selector, props, opts);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "stagger": {
|
|
101
|
+
const sProps = parseAnimateProps(el);
|
|
102
|
+
const count = toFiniteNumber(el.getAttribute("data-count"), 0);
|
|
103
|
+
opts.stagger = toFiniteNumber(el.getAttribute("data-stagger"), 0.2);
|
|
104
|
+
opts.duration = toFiniteNumber(el.getAttribute("data-duration"), 0.5);
|
|
105
|
+
if (Object.keys(sProps).length > 0 && count > 0) {
|
|
106
|
+
_anim.stagger(selector, count, sProps, opts);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "counter": {
|
|
111
|
+
const from = toFiniteNumber(el.getAttribute("data-from"), 0);
|
|
112
|
+
const to = toFiniteNumber(el.getAttribute("data-to"), 0);
|
|
113
|
+
opts.prefix = el.getAttribute("data-prefix") || "";
|
|
114
|
+
opts.suffix = el.getAttribute("data-suffix") || "";
|
|
115
|
+
opts.decimals = toFiniteNumber(el.getAttribute("data-decimals"), 0);
|
|
116
|
+
_anim.counter(selector, [from, to], opts);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case "typewriter": {
|
|
120
|
+
const text = el.getAttribute("data-text") || el.textContent || "";
|
|
121
|
+
_anim.typewriter(selector, text, opts);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "codeReveal": {
|
|
125
|
+
const linesStr = el.getAttribute("data-lines");
|
|
126
|
+
let lines = [];
|
|
127
|
+
if (linesStr) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(linesStr);
|
|
130
|
+
lines = Array.isArray(parsed) ? parsed : [String(parsed)];
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn("MulmoAnimation: failed to parse data-lines", e);
|
|
133
|
+
lines = [linesStr];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
_anim.codeReveal(selector, lines, opts);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "blink": {
|
|
140
|
+
opts.interval = toFiniteNumber(el.getAttribute("data-interval"), 0.5);
|
|
141
|
+
_anim.blink(selector, opts);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "coverZoom": {
|
|
145
|
+
const zf = el.getAttribute("data-zoom-from");
|
|
146
|
+
const zt = el.getAttribute("data-zoom-to");
|
|
147
|
+
if (zf !== null) opts.zoomFrom = toFiniteNumber(zf, 1);
|
|
148
|
+
if (zt !== null) opts.zoomTo = toFiniteNumber(zt, 1);
|
|
149
|
+
if (zf === null && zt === null) {
|
|
150
|
+
const cf = el.getAttribute("data-from");
|
|
151
|
+
const ct = el.getAttribute("data-to");
|
|
152
|
+
if (cf !== null) opts.from = toFiniteNumber(cf, 1);
|
|
153
|
+
if (ct !== null) opts.to = toFiniteNumber(ct, 1);
|
|
154
|
+
}
|
|
155
|
+
_anim.coverZoom(selector, opts);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "coverPan": {
|
|
159
|
+
opts.axis = el.getAttribute("data-axis") || "x";
|
|
160
|
+
const dir = el.getAttribute("data-direction");
|
|
161
|
+
const dist = el.getAttribute("data-distance");
|
|
162
|
+
const pFrom = el.getAttribute("data-from");
|
|
163
|
+
const pTo = el.getAttribute("data-to");
|
|
164
|
+
const pZoom = el.getAttribute("data-zoom");
|
|
165
|
+
if (dir !== null) opts.direction = toFiniteNumber(dir, 1);
|
|
166
|
+
if (dist !== null) opts.distance = toFiniteNumber(dist, 0);
|
|
167
|
+
if (pFrom !== null) opts.from = toFiniteNumber(pFrom, 50);
|
|
168
|
+
if (pTo !== null) opts.to = toFiniteNumber(pTo, 50);
|
|
169
|
+
if (pZoom !== null) opts.zoom = toFiniteNumber(pZoom, 1);
|
|
170
|
+
_anim.coverPan(selector, opts);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Expose as global so auto-render picks it up
|
|
177
|
+
window.animation = _anim;
|
|
178
|
+
})();
|