animate_it 0.3.2 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -1
- data/README.md +212 -20
- data/app/controllers/animate_it/embed_assets_controller.rb +25 -0
- data/app/controllers/animate_it/frames_controller.rb +10 -0
- data/app/controllers/animate_it/public_players_controller.rb +73 -0
- data/app/controllers/animate_it/renders_controller.rb +3 -17
- data/app/controllers/animate_it/studio_controller.rb +1 -0
- data/app/jobs/animate_it/render_job.rb +2 -0
- data/app/views/animate_it/frames/filmstrip.html.haml +37 -6
- data/app/views/animate_it/frames/player.html.haml +92 -0
- data/app/views/animate_it/studio/_preview_pane.html.haml +1 -1
- data/app/views/animate_it/studio/_props_pane.html.haml +3 -2
- data/app/views/animate_it/studio/_studio_script.html.haml +71 -48
- data/app/views/animate_it/studio/show.html.haml +10 -3
- data/config/routes.rb +10 -0
- data/lib/animate_it/asset_manifest.rb +92 -0
- data/lib/animate_it/asset_renderer.rb +39 -7
- data/lib/animate_it/chapter_navigation.rb +160 -0
- data/lib/animate_it/chapters.rb +95 -0
- data/lib/animate_it/composition.rb +115 -9
- data/lib/animate_it/embed_helper.rb +214 -0
- data/lib/animate_it/embed_runtime/embed.js +338 -0
- data/lib/animate_it/embed_runtime.rb +13 -0
- data/lib/animate_it/embed_styles.rb +126 -0
- data/lib/animate_it/engine.rb +7 -0
- data/lib/animate_it/player_manifest.rb +29 -0
- data/lib/animate_it/runtime/runtime.js +518 -0
- data/lib/animate_it/runtime.rb +11 -0
- data/lib/animate_it/scene.rb +57 -3
- data/lib/animate_it/text_effects.rb +104 -0
- data/lib/animate_it/track_document_schema.rb +71 -0
- data/lib/animate_it/tracks/document.rb +100 -0
- data/lib/animate_it/tracks/layer.rb +13 -0
- data/lib/animate_it/tracks/recorder.rb +149 -0
- data/lib/animate_it/verification.rb +187 -0
- data/lib/animate_it/version.rb +1 -1
- data/lib/animate_it/video_renderer.rb +89 -26
- data/lib/animate_it/view_helpers.rb +11 -1
- data/lib/animate_it.rb +17 -0
- data/lib/tasks/animate_it_tasks.rake +133 -0
- metadata +25 -6
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// AnimateIt client runtime: plays a recorded track document against a
|
|
2
|
+
// once-rendered scene DOM.
|
|
3
|
+
(function (global) {
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
var Easing = {
|
|
7
|
+
linear: function (p) { return p; },
|
|
8
|
+
ease_in: function (p) { return p * p; },
|
|
9
|
+
ease_out: function (p) { return 1 - (1 - p) * (1 - p); },
|
|
10
|
+
ease_in_out: function (p) {
|
|
11
|
+
if (p < 0.5) return 2 * p * p;
|
|
12
|
+
var q = -2 * p + 2;
|
|
13
|
+
return 1 - (q * q) / 2;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function round4(value) {
|
|
18
|
+
var rounded = Math.round(Math.abs(value) * 1e4) / 1e4;
|
|
19
|
+
return value < 0 ? -rounded : rounded;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatComputed(value) {
|
|
23
|
+
return Number.isInteger(value) ? value.toFixed(1) : String(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function interpolate(input, frames, values, easingName) {
|
|
27
|
+
var n = frames.length;
|
|
28
|
+
if (input < frames[0]) return { raw: values[0] };
|
|
29
|
+
if (input > frames[n - 1]) return { raw: values[n - 1] };
|
|
30
|
+
|
|
31
|
+
var left = 0;
|
|
32
|
+
if (input > frames[0]) {
|
|
33
|
+
left = -1;
|
|
34
|
+
for (var i = 0; i < n - 1; i += 1) {
|
|
35
|
+
if (input >= frames[i] && input <= frames[i + 1]) { left = i; break; }
|
|
36
|
+
}
|
|
37
|
+
if (left === -1) left = n - 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var start = frames[left];
|
|
41
|
+
var end = frames[left + 1];
|
|
42
|
+
if (end === start) return { raw: values[left] };
|
|
43
|
+
|
|
44
|
+
var progress = (input - start) / (end - start);
|
|
45
|
+
var eased = (Easing[easingName] || Easing.ease_out)(progress);
|
|
46
|
+
return { computed: values[left] + (values[left + 1] - values[left]) * eased };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function compileTrack(track) {
|
|
50
|
+
if (track.t === "rle") {
|
|
51
|
+
var expanded = [];
|
|
52
|
+
for (var i = 0; i < track.r.length; i += 1) {
|
|
53
|
+
var value = track.r[i][0];
|
|
54
|
+
for (var j = 0; j < track.r[i][1]; j += 1) expanded.push(value);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
valueAt: function (frame) {
|
|
58
|
+
if (!expanded.length) return "";
|
|
59
|
+
return expanded[Math.min(frame, expanded.length - 1)];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var frames = track.k.map(function (pair) { return pair[0]; });
|
|
65
|
+
var values = track.k.map(function (pair) { return pair[1]; });
|
|
66
|
+
var unit = track.u || "";
|
|
67
|
+
var easing = track.e || "ease_out";
|
|
68
|
+
return {
|
|
69
|
+
valueAt: function (frame) {
|
|
70
|
+
var result = interpolate(frame, frames, values, easing);
|
|
71
|
+
if ("raw" in result) return String(result.raw) + unit;
|
|
72
|
+
return formatComputed(round4(result.computed)) + unit;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clamp(value, min, max) {
|
|
78
|
+
return Math.max(min, Math.min(max, value));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createChapterState(manifest, root) {
|
|
82
|
+
var chapters = (manifest && manifest.chapters) || [];
|
|
83
|
+
var elements = chapters.length ? Array.prototype.slice.call(root.querySelectorAll("[data-animate-it-chapter]")) : [];
|
|
84
|
+
var lastName = null;
|
|
85
|
+
|
|
86
|
+
function setElementState(el, chapter, index, currentIndex, progress) {
|
|
87
|
+
var state = currentIndex < 0 || index > currentIndex ? "upcoming" : (index < currentIndex ? "completed" : "current");
|
|
88
|
+
var position = currentIndex < 0 ? "hidden" :
|
|
89
|
+
(index === currentIndex ? "current" : (index === currentIndex - 1 ? "previous" : (index === currentIndex + 1 ? "next" : "hidden")));
|
|
90
|
+
var chapterProgress = state === "completed" ? 1 : (state === "current" ? progress : 0);
|
|
91
|
+
el.dataset.chapterState = state;
|
|
92
|
+
el.dataset.chapterPosition = position;
|
|
93
|
+
el.style.setProperty("--animate-it-chapter-progress", String(chapterProgress));
|
|
94
|
+
el.style.setProperty("--animate-it-chapter-active", state === "current" ? "1" : "0");
|
|
95
|
+
el.style.setProperty("--animate-it-chapter-complete", state === "completed" ? "1" : "0");
|
|
96
|
+
if (el.tagName === "BUTTON") {
|
|
97
|
+
if (state === "current") el.setAttribute("aria-current", "step");
|
|
98
|
+
else el.removeAttribute("aria-current");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function update(frame) {
|
|
103
|
+
var currentIndex = -1;
|
|
104
|
+
for (var i = chapters.length - 1; i >= 0; i -= 1) {
|
|
105
|
+
if (frame >= chapters[i].startFrame) { currentIndex = i; break; }
|
|
106
|
+
}
|
|
107
|
+
var current = currentIndex >= 0 ? chapters[currentIndex] : null;
|
|
108
|
+
var progress = current ? (current.durationFrames === 1 ? 1 :
|
|
109
|
+
clamp((frame - current.startFrame) / (current.durationFrames - 1), 0, 1)) : 0;
|
|
110
|
+
elements.forEach(function (el) {
|
|
111
|
+
var index = chapters.findIndex(function (chapter) { return chapter.name === el.dataset.animateItChapter; });
|
|
112
|
+
if (index >= 0) setElementState(el, chapters[index], index, currentIndex, progress);
|
|
113
|
+
});
|
|
114
|
+
var changed = (current && current.name) !== lastName;
|
|
115
|
+
lastName = current && current.name;
|
|
116
|
+
return { chapter: current, index: currentIndex, progress: progress, changed: changed };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { update: update, chapters: chapters };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Pause native CSS/Web Animations and seek them to deterministic frame
|
|
123
|
+
// time. Layers use scene-local time so delayed scenes start at zero.
|
|
124
|
+
var animationCache = typeof WeakMap === "undefined" ? null : new WeakMap();
|
|
125
|
+
|
|
126
|
+
function seekAnimations(root, frame, fps) {
|
|
127
|
+
if (!root || typeof root.getAnimations !== "function") return [];
|
|
128
|
+
|
|
129
|
+
var frameRate = Number(fps);
|
|
130
|
+
if (!(frameRate > 0)) return [];
|
|
131
|
+
|
|
132
|
+
var currentTime = (frame / frameRate) * 1000;
|
|
133
|
+
var animations = Array.prototype.slice.call(root.getAnimations({ subtree: true }));
|
|
134
|
+
var cached = animationCache && animationCache.get(root);
|
|
135
|
+
if (cached) {
|
|
136
|
+
cached.forEach(function (animation) {
|
|
137
|
+
if (animation.playState !== "idle" && animations.indexOf(animation) === -1) animations.push(animation);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
animations.forEach(function (animation) {
|
|
141
|
+
animation.pause();
|
|
142
|
+
animation.currentTime = currentTime;
|
|
143
|
+
});
|
|
144
|
+
if (animationCache) animationCache.set(root, animations);
|
|
145
|
+
return animations;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createPlayer(doc, root, options) {
|
|
149
|
+
var settings = options || {};
|
|
150
|
+
var duration = doc.duration;
|
|
151
|
+
var varBindings = [];
|
|
152
|
+
var group;
|
|
153
|
+
var name;
|
|
154
|
+
|
|
155
|
+
for (group in doc.groups || {}) {
|
|
156
|
+
var groupSelector = (doc.groupSelectors || {})[group] || '[data-animate-vars="' + group + '"]';
|
|
157
|
+
var els = root.querySelectorAll(groupSelector);
|
|
158
|
+
if (!els.length) continue;
|
|
159
|
+
for (name in doc.groups[group]) {
|
|
160
|
+
varBindings.push({
|
|
161
|
+
els: els,
|
|
162
|
+
name: name,
|
|
163
|
+
track: compileTrack(doc.groups[group][name]),
|
|
164
|
+
last: null,
|
|
165
|
+
initialized: false
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var textBindings = [];
|
|
171
|
+
for (name in doc.texts || {}) {
|
|
172
|
+
var textSelector = (doc.textSelectors || {})[name] || '[data-animate-text="' + name + '"]';
|
|
173
|
+
var textEls = root.querySelectorAll(textSelector);
|
|
174
|
+
if (!textEls.length) continue;
|
|
175
|
+
textBindings.push({ els: textEls, track: compileTrack(doc.texts[name]), last: null });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var layers = (doc.layers || []).map(function (layer) {
|
|
179
|
+
return {
|
|
180
|
+
els: root.querySelectorAll(layer.sel),
|
|
181
|
+
from: layer.from,
|
|
182
|
+
to: layer.to,
|
|
183
|
+
origin: Number(layer.origin) || 0,
|
|
184
|
+
active: null
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
var current = -1;
|
|
189
|
+
var chapterState = createChapterState(settings.manifest, root);
|
|
190
|
+
var currentChapter = null;
|
|
191
|
+
|
|
192
|
+
function setFrame(n) {
|
|
193
|
+
var frame = Math.max(0, Math.min(duration - 1, Math.round(Number(n) || 0)));
|
|
194
|
+
var frameChanged = frame !== current;
|
|
195
|
+
current = frame;
|
|
196
|
+
|
|
197
|
+
layers.forEach(function (layer) {
|
|
198
|
+
var active = frame >= layer.from && frame < layer.to;
|
|
199
|
+
if (active === layer.active) return;
|
|
200
|
+
layer.active = active;
|
|
201
|
+
layer.els.forEach(function (el) { el.classList.toggle("is-active", active); });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
varBindings.forEach(function (binding) {
|
|
205
|
+
var value = binding.track.valueAt(frame);
|
|
206
|
+
if (binding.initialized && value === binding.last) return;
|
|
207
|
+
binding.initialized = true;
|
|
208
|
+
binding.last = value;
|
|
209
|
+
binding.els.forEach(function (el) {
|
|
210
|
+
if (value === null) el.style.removeProperty(binding.name);
|
|
211
|
+
else el.style.setProperty(binding.name, value);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
textBindings.forEach(function (binding) {
|
|
216
|
+
var value = binding.track.valueAt(frame);
|
|
217
|
+
if (value === binding.last) return;
|
|
218
|
+
binding.last = value;
|
|
219
|
+
binding.els.forEach(function (el) { el.textContent = value; });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (layers.length) {
|
|
223
|
+
layers.forEach(function (layer) {
|
|
224
|
+
if (!layer.active) return;
|
|
225
|
+
layer.els.forEach(function (el) {
|
|
226
|
+
seekAnimations(el, frame - layer.origin, doc.fps);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
seekAnimations(root, frame, doc.fps);
|
|
231
|
+
}
|
|
232
|
+
currentChapter = chapterState.update(frame);
|
|
233
|
+
if (frameChanged && typeof settings.onFrame === "function") settings.onFrame(frame, currentChapter);
|
|
234
|
+
return frame;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
duration: duration,
|
|
239
|
+
fps: doc.fps,
|
|
240
|
+
setFrame: setFrame,
|
|
241
|
+
currentFrame: function () { return current; },
|
|
242
|
+
currentChapter: function () { return currentChapter; }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Owns wall-clock playback for production embeds. Audio starts only from
|
|
247
|
+
// this transport, so a browser-blocked autoplay attempt falls back to the
|
|
248
|
+
// visible Play control instead of silently advancing the picture.
|
|
249
|
+
function createTransport(player, audios, options) {
|
|
250
|
+
var settings = options || {};
|
|
251
|
+
var shouldLoop = settings.loop !== false;
|
|
252
|
+
var button = settings.button || null;
|
|
253
|
+
var duration = player.duration;
|
|
254
|
+
var fps = player.fps;
|
|
255
|
+
var frame = player.currentFrame() < 0 ? 0 : player.currentFrame();
|
|
256
|
+
var animationFrame = null;
|
|
257
|
+
var startedAt = 0;
|
|
258
|
+
var startedFrame = frame;
|
|
259
|
+
var emit = typeof settings.onEvent === "function" ? settings.onEvent : function () {};
|
|
260
|
+
|
|
261
|
+
audios.forEach(function (el) {
|
|
262
|
+
var gain = Number(el.dataset.gain);
|
|
263
|
+
if (Number.isFinite(gain)) el.volume = Math.max(0, Math.min(1, gain));
|
|
264
|
+
el.loop = el.dataset.loop === "true";
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
function audioWindow(el) {
|
|
268
|
+
var start = Number(el.dataset.fromFrame) || 0;
|
|
269
|
+
var rawLength = Number(el.dataset.durationFrames);
|
|
270
|
+
return { start: start, length: rawLength > 0 ? rawLength : duration - start };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function audioTime(el, localTime) {
|
|
274
|
+
if (el.dataset.loop !== "true" || !Number.isFinite(el.duration) || el.duration <= 0) return localTime;
|
|
275
|
+
return localTime % el.duration;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function updateButton(playing) {
|
|
279
|
+
if (!button) return;
|
|
280
|
+
button.textContent = playing ? "Pause" : "Play";
|
|
281
|
+
button.setAttribute("aria-pressed", playing ? "true" : "false");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function prepareAudio(el, currentFrame, shouldPlay) {
|
|
285
|
+
var window = audioWindow(el);
|
|
286
|
+
var within = currentFrame >= window.start && currentFrame < window.start + window.length;
|
|
287
|
+
if (!within) {
|
|
288
|
+
if (!el.paused) el.pause();
|
|
289
|
+
return Promise.resolve();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
var begin = function () {
|
|
293
|
+
var time = audioTime(el, (currentFrame - window.start) / fps);
|
|
294
|
+
if (!shouldPlay || el.paused) el.currentTime = time;
|
|
295
|
+
if (!shouldPlay) {
|
|
296
|
+
if (!el.paused) el.pause();
|
|
297
|
+
return Promise.resolve();
|
|
298
|
+
}
|
|
299
|
+
if (!el.paused) return Promise.resolve();
|
|
300
|
+
return Promise.resolve(el.play());
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (el.readyState >= 1) return begin();
|
|
304
|
+
return new Promise(function (resolve, reject) {
|
|
305
|
+
el.addEventListener("loadedmetadata", function () {
|
|
306
|
+
begin().then(resolve, reject);
|
|
307
|
+
}, { once: true });
|
|
308
|
+
el.addEventListener("error", function () {
|
|
309
|
+
reject(new Error("AnimateIt audio failed to load"));
|
|
310
|
+
}, { once: true });
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function syncAudio(currentFrame, shouldPlay) {
|
|
315
|
+
return Promise.all(audios.map(function (el) {
|
|
316
|
+
return prepareAudio(el, currentFrame, shouldPlay);
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function stopAudio() {
|
|
321
|
+
audios.forEach(function (el) { if (!el.paused) el.pause(); });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function pause() {
|
|
325
|
+
var wasPlaying = animationFrame !== null;
|
|
326
|
+
if (animationFrame !== null) global.cancelAnimationFrame(animationFrame);
|
|
327
|
+
animationFrame = null;
|
|
328
|
+
syncAudio(frame, false);
|
|
329
|
+
updateButton(false);
|
|
330
|
+
if (wasPlaying) emit("pause", { frame: frame });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function seek(nextFrame) {
|
|
334
|
+
frame = player.setFrame(nextFrame);
|
|
335
|
+
if (animationFrame !== null) {
|
|
336
|
+
stopAudio();
|
|
337
|
+
startedAt = global.performance.now();
|
|
338
|
+
startedFrame = frame;
|
|
339
|
+
syncAudio(frame, true).catch(pause);
|
|
340
|
+
} else {
|
|
341
|
+
syncAudio(frame, false);
|
|
342
|
+
}
|
|
343
|
+
return frame;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function tick(now) {
|
|
347
|
+
if (animationFrame === null) return;
|
|
348
|
+
var next = startedFrame + Math.floor(((now - startedAt) / 1000) * fps);
|
|
349
|
+
if (next >= duration) {
|
|
350
|
+
if (!shouldLoop) {
|
|
351
|
+
frame = player.setFrame(duration - 1);
|
|
352
|
+
emit("ended", { frame: frame });
|
|
353
|
+
pause();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
frame = player.setFrame(0);
|
|
357
|
+
stopAudio();
|
|
358
|
+
startedAt = now;
|
|
359
|
+
startedFrame = 0;
|
|
360
|
+
syncAudio(frame, true).catch(pause);
|
|
361
|
+
} else if (next !== frame) {
|
|
362
|
+
frame = player.setFrame(next);
|
|
363
|
+
syncAudio(frame, true).catch(pause);
|
|
364
|
+
}
|
|
365
|
+
if (animationFrame !== null) animationFrame = global.requestAnimationFrame(tick);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function play() {
|
|
369
|
+
if (animationFrame !== null) return Promise.resolve();
|
|
370
|
+
if (frame >= duration - 1) frame = player.setFrame(0);
|
|
371
|
+
startedAt = global.performance.now();
|
|
372
|
+
startedFrame = frame;
|
|
373
|
+
updateButton(true);
|
|
374
|
+
animationFrame = global.requestAnimationFrame(tick);
|
|
375
|
+
emit("play", { frame: frame });
|
|
376
|
+
return syncAudio(frame, true).catch(function (error) {
|
|
377
|
+
pause();
|
|
378
|
+
emit("error", { message: error && error.message ? error.message : "AnimateIt playback failed" });
|
|
379
|
+
throw error;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function toggle() {
|
|
384
|
+
return animationFrame === null ? play() : (pause(), Promise.resolve());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (button) button.addEventListener("click", function () { toggle().catch(function () {}); });
|
|
388
|
+
updateButton(false);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
play: play,
|
|
392
|
+
pause: pause,
|
|
393
|
+
toggle: toggle,
|
|
394
|
+
seek: seek,
|
|
395
|
+
playing: function () { return animationFrame !== null; },
|
|
396
|
+
currentFrame: function () { return frame; }
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function waitForReady(root) {
|
|
401
|
+
var fonts = root.fonts && root.fonts.ready ? root.fonts.ready.catch(function () {}) : Promise.resolve();
|
|
402
|
+
var images = Array.prototype.slice.call(root.images || []).filter(function (image) {
|
|
403
|
+
if (image.hidden) return false;
|
|
404
|
+
if (typeof image.getBoundingClientRect !== "function") return true;
|
|
405
|
+
var rect = image.getBoundingClientRect();
|
|
406
|
+
return rect.width > 0 && rect.height > 0;
|
|
407
|
+
}).map(function (image) {
|
|
408
|
+
if (image.complete) {
|
|
409
|
+
if (image.naturalWidth === 0) return Promise.reject(new Error("AnimateIt visible image failed to load"));
|
|
410
|
+
return typeof image.decode === "function" ? image.decode() : Promise.resolve();
|
|
411
|
+
}
|
|
412
|
+
return new Promise(function (resolve, reject) {
|
|
413
|
+
image.addEventListener("load", resolve, { once: true });
|
|
414
|
+
image.addEventListener("error", function () {
|
|
415
|
+
reject(new Error("AnimateIt visible image failed to load"));
|
|
416
|
+
}, { once: true });
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
return Promise.all([fonts, Promise.all(images)]).then(function () {
|
|
420
|
+
return new Promise(function (resolve) {
|
|
421
|
+
global.requestAnimationFrame(function () { global.requestAnimationFrame(resolve); });
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function boot() {
|
|
427
|
+
var script = document.querySelector("script[data-animate-it-tracks]");
|
|
428
|
+
if (!script) return;
|
|
429
|
+
var manifestScript = document.querySelector("script[data-animate-it-manifest]");
|
|
430
|
+
var manifest = manifestScript ? JSON.parse(manifestScript.textContent) : { chapters: [] };
|
|
431
|
+
var currentChapterName = null;
|
|
432
|
+
function emit(name, detail) {
|
|
433
|
+
var payload = Object.assign({ frame: player ? player.currentFrame() : 0 }, detail || {});
|
|
434
|
+
if (typeof global.CustomEvent === "function") global.dispatchEvent(new CustomEvent("animateit:" + name, { detail: payload }));
|
|
435
|
+
if (global.parent && global.parent !== global && global.location) {
|
|
436
|
+
global.parent.postMessage({ namespace: "animate-it", event: name, detail: payload }, global.location.origin);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
var player = createPlayer(JSON.parse(script.textContent), document, {
|
|
440
|
+
manifest: manifest,
|
|
441
|
+
onFrame: function (frame, chapterState) {
|
|
442
|
+
var chapter = chapterState.chapter;
|
|
443
|
+
emit("framechange", { frame: frame, chapter: chapter && chapter.name, progress: chapterState.progress });
|
|
444
|
+
if (chapterState.changed) {
|
|
445
|
+
currentChapterName = chapter && chapter.name;
|
|
446
|
+
emit("chapterchange", { frame: frame, chapter: currentChapterName, progress: chapterState.progress });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
player.setFrame(0);
|
|
451
|
+
global.__animateIt = { totalFrames: player.duration, setFrame: player.setFrame };
|
|
452
|
+
global.AnimateItRuntime = player;
|
|
453
|
+
if (script.dataset.animateItTransport === "true") {
|
|
454
|
+
var transport = createTransport(
|
|
455
|
+
player,
|
|
456
|
+
Array.prototype.slice.call(document.querySelectorAll("audio[data-from-frame]")),
|
|
457
|
+
{
|
|
458
|
+
loop: script.dataset.animateItLoop === "true",
|
|
459
|
+
button: document.querySelector("[data-animate-it-play]"),
|
|
460
|
+
onEvent: emit
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
transport.seekChapter = function (name) {
|
|
464
|
+
var chapter = (manifest.chapters || []).find(function (item) { return item.name === String(name); });
|
|
465
|
+
if (!chapter) throw new Error("Unknown AnimateIt chapter: " + name);
|
|
466
|
+
return transport.seek(chapter.startFrame);
|
|
467
|
+
};
|
|
468
|
+
global.AnimateItTransport = transport;
|
|
469
|
+
global.AnimateItPlayer = transport;
|
|
470
|
+
global.addEventListener("message", function (event) {
|
|
471
|
+
if (event.source !== global.parent || !global.location || event.origin !== global.location.origin) return;
|
|
472
|
+
var data = event.data || {};
|
|
473
|
+
if (data.namespace !== "animate-it" || data.command === undefined) return;
|
|
474
|
+
try {
|
|
475
|
+
if (data.command === "play") transport.play().catch(function () {});
|
|
476
|
+
else if (data.command === "pause") transport.pause();
|
|
477
|
+
else if (data.command === "toggle") transport.toggle().catch(function () {});
|
|
478
|
+
else if (data.command === "seek") transport.seek(data.frame);
|
|
479
|
+
else if (data.command === "seekChapter") transport.seekChapter(data.chapter);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
emit("error", { message: error.message });
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
if (script.dataset.animateItAutoplay === "true") transport.play().catch(function () {});
|
|
485
|
+
}
|
|
486
|
+
waitForReady(document)
|
|
487
|
+
.then(function () {
|
|
488
|
+
document.documentElement.dataset.animateItReady = "1";
|
|
489
|
+
emit("ready", { manifest: manifest, chapter: currentChapterName });
|
|
490
|
+
})
|
|
491
|
+
.catch(function (error) {
|
|
492
|
+
emit("error", { message: error && error.message ? error.message : "AnimateIt player failed readiness" });
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
var api = {
|
|
497
|
+
Easing: Easing,
|
|
498
|
+
round4: round4,
|
|
499
|
+
formatComputed: formatComputed,
|
|
500
|
+
interpolate: interpolate,
|
|
501
|
+
compileTrack: compileTrack,
|
|
502
|
+
createChapterState: createChapterState,
|
|
503
|
+
seekAnimations: seekAnimations,
|
|
504
|
+
createPlayer: createPlayer,
|
|
505
|
+
createTransport: createTransport
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
509
|
+
module.exports = api;
|
|
510
|
+
} else {
|
|
511
|
+
global.AnimateItRuntimeApi = api;
|
|
512
|
+
if (document.readyState === "loading") {
|
|
513
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
514
|
+
} else {
|
|
515
|
+
boot();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
})(typeof window !== "undefined" ? window : globalThis);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module AnimateIt
|
|
2
|
+
# The client-side playback runtime, inlined into the player page via
|
|
3
|
+
# `javascript_tag` (no asset-pipeline coupling).
|
|
4
|
+
module Runtime
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def source
|
|
8
|
+
@source ||= File.read(File.expand_path("runtime/runtime.js", __dir__)).freeze
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/animate_it/scene.rb
CHANGED
|
@@ -2,6 +2,7 @@ module AnimateIt
|
|
|
2
2
|
class Scene
|
|
3
3
|
include AnimationHelpers
|
|
4
4
|
include ViewHelpers
|
|
5
|
+
include TextEffects
|
|
5
6
|
|
|
6
7
|
attr_reader :context, :props, :view_context
|
|
7
8
|
|
|
@@ -49,6 +50,29 @@ module AnimateIt
|
|
|
49
50
|
@animations ||= AnimationSet.new
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# Register pure per-frame CSS-variable math for client recording.
|
|
54
|
+
def track_vars(name = :root, &block)
|
|
55
|
+
raise ArgumentError, "track_vars requires a block" unless block
|
|
56
|
+
|
|
57
|
+
own_var_groups[name.to_sym] = block
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def var_groups
|
|
61
|
+
inherited = superclass.respond_to?(:var_groups) ? superclass.var_groups : {}
|
|
62
|
+
inherited.merge(own_var_groups)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def text_track(key, &block)
|
|
66
|
+
raise ArgumentError, "text_track requires a block" unless block
|
|
67
|
+
|
|
68
|
+
own_text_tracks[key.to_sym] = block
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def text_tracks
|
|
72
|
+
inherited = superclass.respond_to?(:text_tracks) ? superclass.text_tracks : {}
|
|
73
|
+
inherited.merge(own_text_tracks)
|
|
74
|
+
end
|
|
75
|
+
|
|
52
76
|
# Composition class this scene belongs to. Set by the engine when the
|
|
53
77
|
# scene is mounted via `composition.scene MyScene` / single-scene
|
|
54
78
|
# auto-mount; lets animation property procs reach `Composition.beats`.
|
|
@@ -112,6 +136,14 @@ module AnimateIt
|
|
|
112
136
|
@disable_fragment_caching == true
|
|
113
137
|
end
|
|
114
138
|
|
|
139
|
+
def own_var_groups
|
|
140
|
+
@own_var_groups ||= {}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def own_text_tracks
|
|
144
|
+
@own_text_tracks ||= {}
|
|
145
|
+
end
|
|
146
|
+
|
|
115
147
|
# Class-side mirror of ViewHelpers#stub_methods so class-method
|
|
116
148
|
# fixture builders (which run before any Scene instance exists)
|
|
117
149
|
# can also use the helper to define singleton-method stubs on
|
|
@@ -165,7 +197,7 @@ module AnimateIt
|
|
|
165
197
|
# programmatic (no sidecar template) or has multiple acts.
|
|
166
198
|
def body
|
|
167
199
|
tag.div(class: self.class.canvas_class, style: canvas_style) do
|
|
168
|
-
absolute_fill(style: animation_var_style) do
|
|
200
|
+
absolute_fill(style: animation_var_style, **animate_wrapper_attributes) do
|
|
169
201
|
parts = []
|
|
170
202
|
parts << generated_animation_styles if self.class.animations.elements.any?
|
|
171
203
|
parts << render_scene_template(self.class.template)
|
|
@@ -178,7 +210,7 @@ module AnimateIt
|
|
|
178
210
|
# get the same wrapper + animation-var div around custom content.
|
|
179
211
|
def with_canvas(&block)
|
|
180
212
|
tag.div(class: self.class.canvas_class, style: canvas_style) do
|
|
181
|
-
absolute_fill(style: animation_var_style) do
|
|
213
|
+
absolute_fill(style: animation_var_style, **animate_wrapper_attributes) do
|
|
182
214
|
parts = []
|
|
183
215
|
parts << generated_animation_styles if self.class.animations.elements.any?
|
|
184
216
|
parts << view_context.capture(&block) if block
|
|
@@ -189,10 +221,26 @@ module AnimateIt
|
|
|
189
221
|
|
|
190
222
|
# CSS var bag computed from this frame's animations. Available inside
|
|
191
223
|
# custom render methods that want extra inline styles.
|
|
192
|
-
def animation_vars(**extras)
|
|
224
|
+
def animation_vars(group = nil, **extras)
|
|
225
|
+
return Style.vars(**evaluate_var_group(group), **extras) if group
|
|
226
|
+
|
|
193
227
|
Style.vars(**self.class.animations.vars_for(self), **extras)
|
|
194
228
|
end
|
|
195
229
|
|
|
230
|
+
def evaluate_var_group(name)
|
|
231
|
+
block = self.class.var_groups[name.to_sym]
|
|
232
|
+
raise Error, "Unknown track_vars group :#{name} on #{self.class}" unless block
|
|
233
|
+
|
|
234
|
+
instance_exec(&block)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def evaluate_text_track(key)
|
|
238
|
+
block = self.class.text_tracks[key.to_sym]
|
|
239
|
+
raise Error, "Unknown text_track :#{key} on #{self.class}" unless block
|
|
240
|
+
|
|
241
|
+
instance_exec(&block)
|
|
242
|
+
end
|
|
243
|
+
|
|
196
244
|
delegate :composition_class, to: :class
|
|
197
245
|
|
|
198
246
|
private
|
|
@@ -209,6 +257,12 @@ module AnimateIt
|
|
|
209
257
|
view_context.capture(...)
|
|
210
258
|
end
|
|
211
259
|
|
|
260
|
+
def animate_wrapper_attributes
|
|
261
|
+
return {} unless self.class.animations.elements.any?
|
|
262
|
+
|
|
263
|
+
{ data: { animate_vars: Tracks::Recorder::ANIMATE_GROUP } }
|
|
264
|
+
end
|
|
265
|
+
|
|
212
266
|
def generated_animation_styles
|
|
213
267
|
view_context.tag.style(self.class.animations.css_rules.html_safe)
|
|
214
268
|
end
|