|
@@ -82,6 +82,9 @@
|
|
|
requestAnimationFrame(this.tick);
|
|
requestAnimationFrame(this.tick);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ remove(anim) {
|
|
|
|
|
+ this.animations = this.animations.filter(a => a !== anim);
|
|
|
|
|
+ }
|
|
|
tick(t) {
|
|
tick(t) {
|
|
|
const now = t;
|
|
const now = t;
|
|
|
this.animations = this.animations.filter(anim => {
|
|
this.animations = this.animations.filter(anim => {
|
|
@@ -140,6 +143,9 @@
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Create animations but don't play if autoplay is false
|
|
|
|
|
+ const animations = [];
|
|
|
|
|
+
|
|
|
const promises = elements.map(el => {
|
|
const promises = elements.map(el => {
|
|
|
// 1. WAAPI Animation
|
|
// 1. WAAPI Animation
|
|
|
let waapiAnim = null;
|
|
let waapiAnim = null;
|
|
@@ -232,14 +238,21 @@
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const animation = el.animate(finalFrames, opts);
|
|
const animation = el.animate(finalFrames, opts);
|
|
|
|
|
+ if (!autoplay) animation.pause();
|
|
|
waapiAnim = animation;
|
|
waapiAnim = animation;
|
|
|
waapiPromise = animation.finished;
|
|
waapiPromise = animation.finished;
|
|
|
|
|
+ animations.push(waapiAnim);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 2. JS Animation (Fallback / Attributes)
|
|
// 2. JS Animation (Fallback / Attributes)
|
|
|
let jsPromise = Promise.resolve();
|
|
let jsPromise = Promise.resolve();
|
|
|
if (Object.keys(jsProps).length > 0) {
|
|
if (Object.keys(jsProps).length > 0) {
|
|
|
jsPromise = new Promise(resolve => {
|
|
jsPromise = new Promise(resolve => {
|
|
|
|
|
+ // For JS engine to support manual seek/scroll, we need to structure it differently.
|
|
|
|
|
+ // But for this lite version, we focus on WAAPI for scroll linking.
|
|
|
|
|
+ // JS fallback will just run normally if autoplay.
|
|
|
|
|
+ if (!autoplay) return; // JS engine simplified doesn't support pause/seek easily yet
|
|
|
|
|
+
|
|
|
const startTime = performance.now() + delay;
|
|
const startTime = performance.now() + delay;
|
|
|
const initialValues = {};
|
|
const initialValues = {};
|
|
|
|
|
|
|
@@ -248,8 +261,7 @@
|
|
|
if (k === 'scrollTop' || k === 'scrollLeft') {
|
|
if (k === 'scrollTop' || k === 'scrollLeft') {
|
|
|
initialValues[k] = el[k];
|
|
initialValues[k] = el[k];
|
|
|
} else if (k in el.style) {
|
|
} else if (k in el.style) {
|
|
|
- // For style props not in WAAPI whitelist (like strokeDashoffset)
|
|
|
|
|
- initialValues[k] = parseFloat(el.style[k]) || 0; // simplistic
|
|
|
|
|
|
|
+ initialValues[k] = parseFloat(el.style[k]) || 0;
|
|
|
} else if (isSVG(el) && el.hasAttribute(k)) {
|
|
} else if (isSVG(el) && el.hasAttribute(k)) {
|
|
|
initialValues[k] = parseFloat(el.getAttribute(k)) || 0;
|
|
initialValues[k] = parseFloat(el.getAttribute(k)) || 0;
|
|
|
} else {
|
|
} else {
|
|
@@ -273,9 +285,8 @@
|
|
|
if (k === 'scrollTop' || k === 'scrollLeft') {
|
|
if (k === 'scrollTop' || k === 'scrollLeft') {
|
|
|
el[k] = val;
|
|
el[k] = val;
|
|
|
} else if (k in el.style) {
|
|
} else if (k in el.style) {
|
|
|
- el.style[k] = val; // Set as style
|
|
|
|
|
|
|
+ el.style[k] = val;
|
|
|
} else if (isSVG(el)) {
|
|
} else if (isSVG(el)) {
|
|
|
- // Kebab-case conversion for setAttribute
|
|
|
|
|
const attr = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
const attr = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
|
el.setAttribute(attr, val);
|
|
el.setAttribute(attr, val);
|
|
|
} else {
|
|
} else {
|
|
@@ -299,6 +310,9 @@
|
|
|
|
|
|
|
|
const masterPromise = Promise.all(promises);
|
|
const masterPromise = Promise.all(promises);
|
|
|
masterPromise.then = (fn) => Promise.all(promises).then(fn);
|
|
masterPromise.then = (fn) => Promise.all(promises).then(fn);
|
|
|
|
|
+
|
|
|
|
|
+ // Attach animations to promise for external control (like scroll)
|
|
|
|
|
+ masterPromise.animations = animations;
|
|
|
return masterPromise;
|
|
return masterPromise;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -311,7 +325,6 @@
|
|
|
el.style.strokeDasharray = len;
|
|
el.style.strokeDasharray = len;
|
|
|
el.style.strokeDashoffset = len;
|
|
el.style.strokeDashoffset = len;
|
|
|
|
|
|
|
|
- // Animate offset to 0
|
|
|
|
|
animate(el, {
|
|
animate(el, {
|
|
|
strokeDashoffset: [len, 0],
|
|
strokeDashoffset: [len, 0],
|
|
|
...params
|
|
...params
|
|
@@ -333,6 +346,61 @@
|
|
|
|
|
|
|
|
elements.forEach(el => observer.observe(el));
|
|
elements.forEach(el => observer.observe(el));
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // --- Scroll Linked ---
|
|
|
|
|
+ function scroll(animationPromise, options = {}) {
|
|
|
|
|
+ // options: container (default window), range [start, end] (default viewport logic)
|
|
|
|
|
+ const container = options.container || window;
|
|
|
|
|
+ const target = options.target || document.body; // Element to track for progress
|
|
|
|
|
+ // If passing an animation promise, we control its WAAPI animations
|
|
|
|
|
+
|
|
|
|
|
+ const anims = animationPromise.animations || [];
|
|
|
|
|
+ if (!anims.length) return;
|
|
|
|
|
+
|
|
|
|
|
+ const updateScroll = () => {
|
|
|
|
|
+ let progress = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (container === window) {
|
|
|
|
|
+ const scrollY = window.scrollY;
|
|
|
|
|
+ const winH = window.innerHeight;
|
|
|
|
|
+ const docH = document.body.scrollHeight;
|
|
|
|
|
+
|
|
|
|
|
+ // Simple progress: how far down the page (0 to 1)
|
|
|
|
|
+ // Or element based?
|
|
|
|
|
+ // Motion One defaults to element entering view.
|
|
|
|
|
+
|
|
|
|
|
+ if (options.target) {
|
|
|
|
|
+ const rect = options.target.getBoundingClientRect();
|
|
|
|
|
+ const start = winH;
|
|
|
|
|
+ const end = -rect.height;
|
|
|
|
|
+ // progress 0 when rect.top == start (just entering)
|
|
|
|
|
+ // progress 1 when rect.top == end (just left)
|
|
|
|
|
+
|
|
|
|
|
+ const totalDistance = start - end;
|
|
|
|
|
+ const currentDistance = start - rect.top;
|
|
|
|
|
+ progress = currentDistance / totalDistance;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Whole page scroll
|
|
|
|
|
+ progress = scrollY / (docH - winH);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Clamp
|
|
|
|
|
+ progress = Math.max(0, Math.min(1, progress));
|
|
|
|
|
+
|
|
|
|
|
+ anims.forEach(anim => {
|
|
|
|
|
+ if (anim.effect) {
|
|
|
|
|
+ const dur = anim.effect.getComputedTiming().duration;
|
|
|
|
|
+ anim.currentTime = dur * progress;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ window.addEventListener('scroll', updateScroll);
|
|
|
|
|
+ updateScroll(); // Initial
|
|
|
|
|
+
|
|
|
|
|
+ return () => window.removeEventListener('scroll', updateScroll);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// --- Timeline ---
|
|
// --- Timeline ---
|
|
|
function timeline(defaults = {}) {
|
|
function timeline(defaults = {}) {
|
|
@@ -348,9 +416,6 @@
|
|
|
else if (typeof offset === 'number') start = offset;
|
|
else if (typeof offset === 'number') start = offset;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Use setTimeout to defer EXECUTION until the exact start time
|
|
|
|
|
- // This ensures that "implicit from" values are read correctly from the DOM
|
|
|
|
|
- // at the moment the animation is supposed to start.
|
|
|
|
|
if (start <= 0) {
|
|
if (start <= 0) {
|
|
|
animate(targets, animParams);
|
|
animate(targets, animParams);
|
|
|
} else {
|
|
} else {
|
|
@@ -372,7 +437,8 @@
|
|
|
timeline,
|
|
timeline,
|
|
|
svgDraw,
|
|
svgDraw,
|
|
|
inViewAnimate,
|
|
inViewAnimate,
|
|
|
- spring
|
|
|
|
|
|
|
+ spring,
|
|
|
|
|
+ scroll
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
})));
|
|
})));
|