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,338 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
if (!global.customElements || global.customElements.get("animate-it-embed")) return;
|
|
5
|
+
|
|
6
|
+
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
|
|
7
|
+
|
|
8
|
+
class AnimateItEmbed extends HTMLElement {
|
|
9
|
+
connectedCallback() {
|
|
10
|
+
if (this.connected) return;
|
|
11
|
+
this.connected = true;
|
|
12
|
+
this.manifest = JSON.parse(this.querySelector("[data-animate-it-embed-manifest]").textContent);
|
|
13
|
+
this.options = this.manifest.options;
|
|
14
|
+
this.viewport = this.querySelector("[data-animate-it-embed-viewport]");
|
|
15
|
+
this.frame = this.querySelector("[data-animate-it-embed-frame]");
|
|
16
|
+
this.control = this.querySelector("[data-animate-it-embed-control]");
|
|
17
|
+
this.chapterControls = Array.from(this.querySelectorAll("[data-animate-it-chapter]"));
|
|
18
|
+
this.reducedMotion = global.matchMedia("(prefers-reduced-motion: reduce)");
|
|
19
|
+
this.userPaused = false;
|
|
20
|
+
this.userStarted = this.options.autoplay;
|
|
21
|
+
this.visibleRatio = 0;
|
|
22
|
+
this.playerReady = false;
|
|
23
|
+
this.currentChapter = null;
|
|
24
|
+
this.playing = false;
|
|
25
|
+
this.boundMessage = this.receiveMessage.bind(this);
|
|
26
|
+
this.boundVisibility = this.syncPlayback.bind(this);
|
|
27
|
+
this.boundVariantChange = this.variantChanged.bind(this);
|
|
28
|
+
this.boundReducedMotion = this.reducedMotionChanged.bind(this);
|
|
29
|
+
this.boundControl = this.toggle.bind(this);
|
|
30
|
+
this.boundResize = this.scaleFrame.bind(this);
|
|
31
|
+
global.addEventListener("message", this.boundMessage);
|
|
32
|
+
document.addEventListener("visibilitychange", this.boundVisibility);
|
|
33
|
+
this.control.addEventListener("click", this.boundControl);
|
|
34
|
+
this.reducedMotion.addEventListener("change", this.boundReducedMotion);
|
|
35
|
+
this.chapterControls.forEach((control) => {
|
|
36
|
+
control.addEventListener("click", () => this.seekChapter(control.dataset.animateItChapter));
|
|
37
|
+
});
|
|
38
|
+
if ("ResizeObserver" in global) {
|
|
39
|
+
this.resizeObserver = new global.ResizeObserver(this.boundResize);
|
|
40
|
+
this.resizeObserver.observe(this.viewport);
|
|
41
|
+
} else {
|
|
42
|
+
global.addEventListener("resize", this.boundResize);
|
|
43
|
+
}
|
|
44
|
+
this.setupVariants();
|
|
45
|
+
this.setupVisibility();
|
|
46
|
+
this.applyReducedMotion();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disconnectedCallback() {
|
|
50
|
+
global.removeEventListener("message", this.boundMessage);
|
|
51
|
+
document.removeEventListener("visibilitychange", this.boundVisibility);
|
|
52
|
+
this.control && this.control.removeEventListener("click", this.boundControl);
|
|
53
|
+
this.reducedMotion && this.reducedMotion.removeEventListener("change", this.boundReducedMotion);
|
|
54
|
+
this.resizeObserver && this.resizeObserver.disconnect();
|
|
55
|
+
global.removeEventListener("resize", this.boundResize);
|
|
56
|
+
this.intersectionObserver && this.intersectionObserver.disconnect();
|
|
57
|
+
(this.variantQueries || []).forEach((entry) => entry.query.removeEventListener("change", this.boundVariantChange));
|
|
58
|
+
this.cancelLoad();
|
|
59
|
+
this.clearReadyTimer();
|
|
60
|
+
this.removePlayer();
|
|
61
|
+
this.connected = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setupVariants() {
|
|
65
|
+
this.variantQueries = this.manifest.variants.filter((variant) => variant.media).map((variant) => ({
|
|
66
|
+
variant: variant,
|
|
67
|
+
query: global.matchMedia(variant.media)
|
|
68
|
+
}));
|
|
69
|
+
this.variantQueries.forEach((entry) => entry.query.addEventListener("change", this.boundVariantChange));
|
|
70
|
+
this.activateVariant(this.selectedVariant());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
selectedVariant() {
|
|
74
|
+
const matched = this.variantQueries.find((entry) => entry.query.matches);
|
|
75
|
+
return matched ? matched.variant : this.manifest.variants[0];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
variantChanged() {
|
|
79
|
+
const variant = this.selectedVariant();
|
|
80
|
+
if (this.variant && variant.source === this.variant.source) return;
|
|
81
|
+
this.activateVariant(variant);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
activateVariant(variant) {
|
|
85
|
+
const resumeChapter = this.currentChapter;
|
|
86
|
+
this.cancelLoad();
|
|
87
|
+
this.clearReadyTimer();
|
|
88
|
+
this.removePlayer();
|
|
89
|
+
this.variant = variant;
|
|
90
|
+
this.resumeChapter = resumeChapter;
|
|
91
|
+
this.playerReady = false;
|
|
92
|
+
this.dataset.playerReady = "false";
|
|
93
|
+
const composition = variant.composition;
|
|
94
|
+
this.style.setProperty("--animate-it-crossfade-duration", `${this.options.crossfadeDuration}ms`);
|
|
95
|
+
this.viewport.style.aspectRatio = `${composition.width} / ${composition.height}`;
|
|
96
|
+
this.scaleFrame();
|
|
97
|
+
this.updateChapters(0, null);
|
|
98
|
+
if (!this.reducedMotion.matches && this.visibleRatio >= this.options.loadWhenVisible) this.scheduleLoad();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setupVisibility() {
|
|
102
|
+
if (!global.IntersectionObserver) {
|
|
103
|
+
this.visibleRatio = 1;
|
|
104
|
+
if (!this.reducedMotion.matches) this.scheduleLoad();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const thresholds = Array.from(new Set([0, this.options.loadWhenVisible, this.options.playWhenVisible, 1])).sort();
|
|
108
|
+
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
109
|
+
const entry = entries[entries.length - 1];
|
|
110
|
+
this.visibleRatio = entry && entry.isIntersecting && entry.boundingClientRect.height > 0 ?
|
|
111
|
+
entry.intersectionRect.height / entry.boundingClientRect.height : 0;
|
|
112
|
+
if (this.visibleRatio >= this.options.loadWhenVisible && !this.iframe && !this.reducedMotion.matches) this.scheduleLoad();
|
|
113
|
+
this.syncPlayback();
|
|
114
|
+
}, { threshold: thresholds });
|
|
115
|
+
this.intersectionObserver.observe(this.viewport);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
applyReducedMotion() {
|
|
119
|
+
this.dataset.reducedMotion = this.reducedMotion.matches ? "true" : "false";
|
|
120
|
+
if (this.reducedMotion.matches) {
|
|
121
|
+
this.cancelLoad();
|
|
122
|
+
this.removePlayer();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
reducedMotionChanged() {
|
|
127
|
+
this.applyReducedMotion();
|
|
128
|
+
if (!this.reducedMotion.matches && this.visibleRatio >= this.options.loadWhenVisible) this.scheduleLoad();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
scheduleLoad() {
|
|
132
|
+
if (this.iframe || this.loadHandle) return;
|
|
133
|
+
const load = () => {
|
|
134
|
+
this.loadHandle = null;
|
|
135
|
+
if (!this.reducedMotion.matches && this.visibleRatio >= this.options.loadWhenVisible) this.mountPlayer();
|
|
136
|
+
};
|
|
137
|
+
if ("requestIdleCallback" in global) this.loadHandle = global.requestIdleCallback(load, { timeout: 800 });
|
|
138
|
+
else this.loadHandle = global.setTimeout(load, 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
cancelLoad() {
|
|
142
|
+
if (!this.loadHandle) return;
|
|
143
|
+
if ("cancelIdleCallback" in global) global.cancelIdleCallback(this.loadHandle);
|
|
144
|
+
else global.clearTimeout(this.loadHandle);
|
|
145
|
+
this.loadHandle = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
mountPlayer() {
|
|
149
|
+
if (this.iframe) return;
|
|
150
|
+
const composition = this.variant.composition;
|
|
151
|
+
const separator = this.variant.source.includes("?") ? "&" : "?";
|
|
152
|
+
const iframe = document.createElement("iframe");
|
|
153
|
+
iframe.src = `${this.variant.source}${separator}embedded=1&host_navigation=${this.chapterControls.length ? "1" : "0"}`;
|
|
154
|
+
iframe.title = this.manifest.title;
|
|
155
|
+
iframe.loading = "eager";
|
|
156
|
+
iframe.tabIndex = -1;
|
|
157
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
158
|
+
iframe.setAttribute("allow", "autoplay");
|
|
159
|
+
iframe.width = composition.width;
|
|
160
|
+
iframe.height = composition.height;
|
|
161
|
+
iframe.addEventListener("load", () => {
|
|
162
|
+
if (iframe !== this.iframe) return;
|
|
163
|
+
if (!iframe.contentDocument || !iframe.contentDocument.querySelector("[data-animate-it-tracks]")) {
|
|
164
|
+
this.fail(new Error("AnimateIt player returned an invalid document"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.readyTimer = global.setTimeout(() => this.degradedReady(), this.options.readyTimeout);
|
|
168
|
+
}, { once: true });
|
|
169
|
+
iframe.addEventListener("error", () => this.fail(new Error("AnimateIt player failed to load")), { once: true });
|
|
170
|
+
this.iframe = iframe;
|
|
171
|
+
this.frame.replaceChildren(iframe);
|
|
172
|
+
this.scaleFrame();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
removePlayer() {
|
|
176
|
+
if (this.iframe) {
|
|
177
|
+
this.send("pause");
|
|
178
|
+
this.iframe.remove();
|
|
179
|
+
}
|
|
180
|
+
this.iframe = null;
|
|
181
|
+
this.playerReady = false;
|
|
182
|
+
this.playing = false;
|
|
183
|
+
if (this.control) this.control.hidden = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
scaleFrame() {
|
|
187
|
+
if (!this.variant || !this.viewport) return;
|
|
188
|
+
const composition = this.variant.composition;
|
|
189
|
+
const scale = this.viewport.clientWidth / composition.width;
|
|
190
|
+
this.frame.style.width = `${composition.width}px`;
|
|
191
|
+
this.frame.style.height = `${composition.height}px`;
|
|
192
|
+
this.frame.style.transform = `scale(${scale})`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
receiveMessage(event) {
|
|
196
|
+
if (!this.iframe || event.source !== this.iframe.contentWindow || event.origin !== global.location.origin) return;
|
|
197
|
+
const message = event.data || {};
|
|
198
|
+
if (message.namespace !== "animate-it" || !message.event) return;
|
|
199
|
+
const detail = message.detail || {};
|
|
200
|
+
if (message.event === "ready") this.ready(detail);
|
|
201
|
+
else if (message.event === "framechange") this.frameChanged(detail);
|
|
202
|
+
else if (message.event === "chapterchange") this.chapterChanged(detail);
|
|
203
|
+
else if (message.event === "play") this.setPlaying(true);
|
|
204
|
+
else if (message.event === "pause" || message.event === "ended") this.setPlaying(false);
|
|
205
|
+
else if (message.event === "error") this.fail(new Error(detail.message || "AnimateIt player error"));
|
|
206
|
+
this.dispatchEvent(new CustomEvent(`animateit:${message.event}`, { detail }));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
ready(detail) {
|
|
210
|
+
this.clearReadyTimer();
|
|
211
|
+
this.playerReady = true;
|
|
212
|
+
this.dataset.playerReady = "true";
|
|
213
|
+
this.dataset.playerReadiness = "complete";
|
|
214
|
+
this.control.hidden = false;
|
|
215
|
+
if (this.resumeChapter) this.send("seekChapter", { chapter: this.resumeChapter });
|
|
216
|
+
this.updateChapters(detail.frame || 0, detail.chapter);
|
|
217
|
+
this.syncPlayback();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
degradedReady() {
|
|
221
|
+
if (!this.iframe || this.playerReady) return;
|
|
222
|
+
this.playerReady = true;
|
|
223
|
+
this.dataset.playerReady = "true";
|
|
224
|
+
this.dataset.playerReadiness = "degraded";
|
|
225
|
+
this.control.hidden = false;
|
|
226
|
+
this.syncPlayback();
|
|
227
|
+
this.dispatchEvent(new CustomEvent("animateit:degradedready"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fail(error) {
|
|
231
|
+
this.clearReadyTimer();
|
|
232
|
+
this.dataset.playerReady = "false";
|
|
233
|
+
this.dataset.playerError = "true";
|
|
234
|
+
this.playerReady = false;
|
|
235
|
+
this.control.hidden = true;
|
|
236
|
+
this.dispatchEvent(new CustomEvent("animateit:error", { detail: { message: error.message } }));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
clearReadyTimer() {
|
|
240
|
+
if (this.readyTimer) global.clearTimeout(this.readyTimer);
|
|
241
|
+
this.readyTimer = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
frameChanged(detail) {
|
|
245
|
+
this.lastFrame = Number(detail.frame) || 0;
|
|
246
|
+
this.updateChapters(this.lastFrame, detail.chapter);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
chapterChanged(detail) {
|
|
250
|
+
this.currentChapter = detail.chapter || null;
|
|
251
|
+
this.updateChapters(Number(detail.frame) || 0, this.currentChapter);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
updateChapters(frame, namedChapter) {
|
|
255
|
+
const chapters = this.variant.composition.chapters;
|
|
256
|
+
let currentIndex = -1;
|
|
257
|
+
for (let index = chapters.length - 1; index >= 0; index -= 1) {
|
|
258
|
+
if (frame >= chapters[index].startFrame) { currentIndex = index; break; }
|
|
259
|
+
}
|
|
260
|
+
if (namedChapter) currentIndex = chapters.findIndex((chapter) => chapter.name === namedChapter);
|
|
261
|
+
const current = currentIndex >= 0 ? chapters[currentIndex] : null;
|
|
262
|
+
this.currentChapter = current && current.name;
|
|
263
|
+
const progress = current ? (current.durationFrames === 1 ? 1 :
|
|
264
|
+
clamp((frame - current.startFrame) / (current.durationFrames - 1), 0, 1)) : 0;
|
|
265
|
+
this.chapterControls.forEach((control) => {
|
|
266
|
+
const index = chapters.findIndex((chapter) => chapter.name === control.dataset.animateItChapter);
|
|
267
|
+
const state = currentIndex < 0 || index > currentIndex ? "upcoming" : (index < currentIndex ? "completed" : "current");
|
|
268
|
+
const position = currentIndex < 0 ? "hidden" :
|
|
269
|
+
(index === currentIndex ? "current" : (index === currentIndex - 1 ? "previous" : (index === currentIndex + 1 ? "next" : "hidden")));
|
|
270
|
+
control.dataset.chapterState = state;
|
|
271
|
+
control.dataset.chapterPosition = position;
|
|
272
|
+
control.style.setProperty("--animate-it-chapter-progress", String(state === "completed" ? 1 : (state === "current" ? progress : 0)));
|
|
273
|
+
control.style.setProperty("--animate-it-chapter-active", state === "current" ? "1" : "0");
|
|
274
|
+
control.style.setProperty("--animate-it-chapter-complete", state === "completed" ? "1" : "0");
|
|
275
|
+
if (state === "current") control.setAttribute("aria-current", "step");
|
|
276
|
+
else control.removeAttribute("aria-current");
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
seekChapter(name) {
|
|
281
|
+
if (!this.playerReady) { this.resumeChapter = name; return; }
|
|
282
|
+
this.send("seekChapter", { chapter: name });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
seek(frame) {
|
|
286
|
+
this.send("seek", { frame: Number(frame) || 0 });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
play() {
|
|
290
|
+
this.userStarted = true;
|
|
291
|
+
this.userPaused = false;
|
|
292
|
+
this.send("play");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
pause() {
|
|
296
|
+
this.userPaused = true;
|
|
297
|
+
this.send("pause");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
playingState() {
|
|
301
|
+
return this.playing;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
currentFrame() {
|
|
305
|
+
return this.lastFrame || 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
toggle() {
|
|
309
|
+
if (!this.playerReady) return;
|
|
310
|
+
this.userStarted = true;
|
|
311
|
+
this.userPaused = this.playing;
|
|
312
|
+
this.send(this.playing ? "pause" : "play");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
syncPlayback() {
|
|
316
|
+
if (!this.playerReady) return;
|
|
317
|
+
const inViewport = this.visibleRatio >= this.options.playWhenVisible;
|
|
318
|
+
const shouldPlay = this.userStarted && !this.userPaused && !document.hidden &&
|
|
319
|
+
(inViewport || !this.options.pauseOffscreen);
|
|
320
|
+
if (shouldPlay && !this.playing) this.send("play");
|
|
321
|
+
else if (!shouldPlay && this.playing) this.send("pause");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
send(command, detail) {
|
|
325
|
+
if (!this.iframe || !this.iframe.contentWindow) return;
|
|
326
|
+
this.iframe.contentWindow.postMessage(Object.assign({ namespace: "animate-it", command }, detail || {}), global.location.origin);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
setPlaying(playing) {
|
|
330
|
+
this.playing = playing;
|
|
331
|
+
this.control.textContent = playing ? "Pause" : "Play";
|
|
332
|
+
this.control.setAttribute("aria-label", playing ? "Pause animation" : "Play animation");
|
|
333
|
+
this.control.setAttribute("aria-pressed", playing ? "true" : "false");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
global.customElements.define("animate-it-embed", AnimateItEmbed);
|
|
338
|
+
})(typeof window !== "undefined" ? window : globalThis);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module AnimateIt
|
|
2
|
+
module EmbedStyles
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def chapter_source
|
|
6
|
+
@chapter_source ||= <<~CSS.freeze
|
|
7
|
+
.animate-it-chapters {
|
|
8
|
+
--animate-it-progress-color: #28d2bc;
|
|
9
|
+
--animate-it-progress-width: 2.5px;
|
|
10
|
+
--animate-it-active-glow: 0 0 10px 2px rgb(40 210 188 / 42%);
|
|
11
|
+
--animate-it-chapter-color: #414b57;
|
|
12
|
+
--animate-it-chapter-background: #fff;
|
|
13
|
+
--animate-it-chapter-shadow: 0 4px 12px rgb(39 47 57 / 8%);
|
|
14
|
+
--animate-it-chapter-font: 700 14px/1 system-ui, sans-serif;
|
|
15
|
+
--animate-it-chapter-gap: 14px;
|
|
16
|
+
--animate-it-chapter-height: 44px;
|
|
17
|
+
--animate-it-carousel-distance: 108%;
|
|
18
|
+
--animate-it-carousel-scale: .84;
|
|
19
|
+
--animate-it-carousel-opacity: .68;
|
|
20
|
+
--animate-it-carousel-duration: 180ms;
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: repeat(var(--animate-it-chapter-count, 4), minmax(0, 1fr));
|
|
23
|
+
gap: var(--animate-it-chapter-gap);
|
|
24
|
+
}
|
|
25
|
+
.animate-it-chapter--pills {
|
|
26
|
+
--animate-it-chapter-progress: 0;
|
|
27
|
+
--animate-it-chapter-active: 0;
|
|
28
|
+
--animate-it-chapter-complete: 0;
|
|
29
|
+
position: relative;
|
|
30
|
+
min-width: 0;
|
|
31
|
+
min-height: 44px;
|
|
32
|
+
height: var(--animate-it-chapter-height);
|
|
33
|
+
padding: 2px;
|
|
34
|
+
border: 0;
|
|
35
|
+
border-radius: 999px;
|
|
36
|
+
appearance: none;
|
|
37
|
+
color: var(--animate-it-chapter-color);
|
|
38
|
+
background: var(--animate-it-chapter-background);
|
|
39
|
+
box-shadow: var(--animate-it-chapter-shadow);
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
}
|
|
42
|
+
.animate-it-chapter--pills[data-chapter-state="current"] { box-shadow: var(--animate-it-active-glow); }
|
|
43
|
+
.animate-it-chapter--pills:focus-visible { outline: 3px solid var(--animate-it-progress-color); outline-offset: 3px; }
|
|
44
|
+
.animate-it-chapter__progress { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
|
|
45
|
+
.animate-it-chapter__progress rect {
|
|
46
|
+
fill: none;
|
|
47
|
+
stroke: var(--animate-it-progress-color);
|
|
48
|
+
stroke-width: var(--animate-it-progress-width);
|
|
49
|
+
stroke-dasharray: var(--animate-it-chapter-progress) 1;
|
|
50
|
+
stroke-opacity: clamp(0, calc(var(--animate-it-chapter-progress) * 1000), 1);
|
|
51
|
+
stroke-linecap: round;
|
|
52
|
+
vector-effect: non-scaling-stroke;
|
|
53
|
+
}
|
|
54
|
+
.animate-it-chapter__label {
|
|
55
|
+
position: relative;
|
|
56
|
+
display: flex;
|
|
57
|
+
height: 100%;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
opacity: calc(.58 + (var(--animate-it-chapter-active) * .42));
|
|
61
|
+
font: var(--animate-it-chapter-font);
|
|
62
|
+
}
|
|
63
|
+
@media (max-width: 767px) {
|
|
64
|
+
.animate-it-chapters--mobile-carousel { position: relative; display: block; height: var(--animate-it-chapter-height); overflow: clip; }
|
|
65
|
+
.animate-it-chapters--mobile-carousel .animate-it-chapter--pills {
|
|
66
|
+
position: absolute;
|
|
67
|
+
left: 50%;
|
|
68
|
+
width: min(31.25%, 122px);
|
|
69
|
+
transition: transform var(--animate-it-carousel-duration) ease, opacity var(--animate-it-carousel-duration) ease;
|
|
70
|
+
}
|
|
71
|
+
.animate-it-chapters--mobile-carousel .animate-it-chapter--pills[data-chapter-position="previous"] { transform: translateX(calc(-50% - var(--animate-it-carousel-distance))) scale(var(--animate-it-carousel-scale)); opacity: var(--animate-it-carousel-opacity); }
|
|
72
|
+
.animate-it-chapters--mobile-carousel .animate-it-chapter--pills[data-chapter-position="current"] { transform: translateX(-50%); opacity: 1; }
|
|
73
|
+
.animate-it-chapters--mobile-carousel .animate-it-chapter--pills[data-chapter-position="next"] { transform: translateX(calc(-50% + var(--animate-it-carousel-distance))) scale(var(--animate-it-carousel-scale)); opacity: var(--animate-it-carousel-opacity); }
|
|
74
|
+
.animate-it-chapters--mobile-carousel .animate-it-chapter--pills[data-chapter-position="hidden"] { visibility: hidden; pointer-events: none; opacity: 0; }
|
|
75
|
+
}
|
|
76
|
+
@media (prefers-reduced-motion: reduce) {
|
|
77
|
+
.animate-it-chapter--pills { transition: none !important; }
|
|
78
|
+
}
|
|
79
|
+
CSS
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def source
|
|
83
|
+
@source ||= <<~CSS.freeze
|
|
84
|
+
#{chapter_source}
|
|
85
|
+
animate-it-embed {
|
|
86
|
+
--animate-it-crossfade-duration: 120ms;
|
|
87
|
+
position: relative;
|
|
88
|
+
display: block;
|
|
89
|
+
width: 100%;
|
|
90
|
+
background: transparent;
|
|
91
|
+
}
|
|
92
|
+
.animate-it-embed__navigation { margin-bottom: 14px; }
|
|
93
|
+
.animate-it-embed__viewport { position: relative; width: 100%; background: transparent; }
|
|
94
|
+
.animate-it-embed__poster, .animate-it-embed__shell { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
95
|
+
.animate-it-embed__poster { z-index: 2; margin: 0; opacity: 1; transition: opacity var(--animate-it-crossfade-duration) ease; pointer-events: none; }
|
|
96
|
+
.animate-it-embed__poster img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
97
|
+
.animate-it-embed__shell { z-index: 1; overflow: hidden; opacity: 0; transition: opacity var(--animate-it-crossfade-duration) ease; background: transparent; }
|
|
98
|
+
.animate-it-embed__frame { position: absolute; top: 0; left: 0; transform-origin: top left; background: transparent; }
|
|
99
|
+
.animate-it-embed__frame iframe { display: block; border: 0; background: transparent; pointer-events: none; }
|
|
100
|
+
animate-it-embed[data-player-ready="true"] .animate-it-embed__poster { opacity: 0; }
|
|
101
|
+
animate-it-embed[data-player-ready="true"] .animate-it-embed__shell { opacity: 1; }
|
|
102
|
+
.animate-it-embed__control {
|
|
103
|
+
position: absolute;
|
|
104
|
+
z-index: 4;
|
|
105
|
+
right: 12px;
|
|
106
|
+
bottom: 12px;
|
|
107
|
+
width: 44px;
|
|
108
|
+
height: 44px;
|
|
109
|
+
padding: 0;
|
|
110
|
+
border: 0;
|
|
111
|
+
border-radius: 999px;
|
|
112
|
+
color: #fff;
|
|
113
|
+
background: rgb(18 24 29 / 84%);
|
|
114
|
+
font: 700 13px/1 system-ui, sans-serif;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
.animate-it-embed__control:focus-visible { outline: 3px solid var(--animate-it-progress-color, #28d2bc); outline-offset: 3px; }
|
|
118
|
+
animate-it-embed[data-reduced-motion="true"] .animate-it-embed__navigation,
|
|
119
|
+
animate-it-embed[data-reduced-motion="true"] .animate-it-embed__control { display: none; }
|
|
120
|
+
@media (prefers-reduced-motion: reduce) {
|
|
121
|
+
.animate-it-embed__poster, .animate-it-embed__shell { transition: none; }
|
|
122
|
+
}
|
|
123
|
+
CSS
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/animate_it/engine.rb
CHANGED
|
@@ -27,5 +27,12 @@ module AnimateIt
|
|
|
27
27
|
AnimateIt.reload_compositions!
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
initializer "animate_it.embed_helper" do
|
|
32
|
+
ActiveSupport.on_load(:action_view) do
|
|
33
|
+
include AnimateIt::EmbedHelper
|
|
34
|
+
include AnimateIt::ChapterNavigationHelper
|
|
35
|
+
end
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
38
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module AnimateIt
|
|
2
|
+
class PlayerManifest
|
|
3
|
+
VERSION = 1
|
|
4
|
+
|
|
5
|
+
def initialize(composition)
|
|
6
|
+
@composition = composition
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def as_json(*)
|
|
10
|
+
@composition.chapters.validate!
|
|
11
|
+
{
|
|
12
|
+
"version" => VERSION,
|
|
13
|
+
"id" => @composition.id,
|
|
14
|
+
"width" => @composition.width,
|
|
15
|
+
"height" => @composition.height,
|
|
16
|
+
"fps" => @composition.fps,
|
|
17
|
+
"duration" => @composition.duration_in_frames,
|
|
18
|
+
"chapters" => @composition.chapters.as_json,
|
|
19
|
+
"playback" => @composition.public_player_options.transform_keys { |key| camelize(key) }
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def camelize(key)
|
|
26
|
+
key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|