gsap
Version:
GSAP is a framework-agnostic JavaScript animation library that turns developers into animation superheroes. Build high-performance animations that work in **every** major browser. Animate CSS, SVG, canvas, React, Vue, WebGL, colors, strings, motion paths,
439 lines (424 loc) • 21.7 kB
JavaScript
/*!
* Observer 3.13.0
* https://gsap.com
*
* @license Copyright 2008-2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license
* @author: Jack Doyle, jack@greensock.com
*/
/* eslint-disable */
let gsap, _coreInitted, _clamp, _win, _doc, _docEl, _body, _isTouch, _pointerType, ScrollTrigger, _root, _normalizer, _eventTypes, _context,
_getGSAP = () => gsap || (typeof(window) !== "undefined" && (gsap = window.gsap) && gsap.registerPlugin && gsap),
_startup = 1,
_observers = [],
_scrollers = [],
_proxies = [],
_getTime = Date.now,
_bridge = (name, value) => value,
_integrate = () => {
let core = ScrollTrigger.core,
data = core.bridge || {},
scrollers = core._scrollers,
proxies = core._proxies;
scrollers.push(..._scrollers);
proxies.push(..._proxies);
_scrollers = scrollers;
_proxies = proxies;
_bridge = (name, value) => data[name](value);
},
_getProxyProp = (element, property) => ~_proxies.indexOf(element) && _proxies[_proxies.indexOf(element) + 1][property],
_isViewport = el => !!~_root.indexOf(el),
_addListener = (element, type, func, passive, capture) => element.addEventListener(type, func, {passive: passive !== false, capture: !!capture}),
_removeListener = (element, type, func, capture) => element.removeEventListener(type, func, !!capture),
_scrollLeft = "scrollLeft",
_scrollTop = "scrollTop",
_onScroll = () => (_normalizer && _normalizer.isPressed) || _scrollers.cache++,
_scrollCacheFunc = (f, doNotCache) => {
let cachingFunc = value => { // since reading the scrollTop/scrollLeft/pageOffsetY/pageOffsetX can trigger a layout, this function allows us to cache the value so it only gets read fresh after a "scroll" event fires (or while we're refreshing because that can lengthen the page and alter the scroll position). when "soft" is true, that means don't actually set the scroll, but cache the new value instead (useful in ScrollSmoother)
if (value || value === 0) {
_startup && (_win.history.scrollRestoration = "manual"); // otherwise the new position will get overwritten by the browser onload.
let isNormalizing = _normalizer && _normalizer.isPressed;
value = cachingFunc.v = Math.round(value) || (_normalizer && _normalizer.iOS ? 1 : 0); //TODO: iOS Bug: if you allow it to go to 0, Safari can start to report super strange (wildly inaccurate) touch positions!
f(value);
cachingFunc.cacheID = _scrollers.cache;
isNormalizing && _bridge("ss", value); // set scroll (notify ScrollTrigger so it can dispatch a "scrollStart" event if necessary
} else if (doNotCache || _scrollers.cache !== cachingFunc.cacheID || _bridge("ref")) {
cachingFunc.cacheID = _scrollers.cache;
cachingFunc.v = f();
}
return cachingFunc.v + cachingFunc.offset;
};
cachingFunc.offset = 0;
return f && cachingFunc;
},
_horizontal = {s: _scrollLeft, p: "left", p2: "Left", os: "right", os2: "Right", d: "width", d2: "Width", a: "x", sc: _scrollCacheFunc(function(value) { return arguments.length ? _win.scrollTo(value, _vertical.sc()) : _win.pageXOffset || _doc[_scrollLeft] || _docEl[_scrollLeft] || _body[_scrollLeft] || 0})},
_vertical = {s: _scrollTop, p: "top", p2: "Top", os: "bottom", os2: "Bottom", d: "height", d2: "Height", a: "y", op: _horizontal, sc: _scrollCacheFunc(function(value) { return arguments.length ? _win.scrollTo(_horizontal.sc(), value) : _win.pageYOffset || _doc[_scrollTop] || _docEl[_scrollTop] || _body[_scrollTop] || 0})},
_getTarget = (t, self) => ((self && self._ctx && self._ctx.selector) || gsap.utils.toArray)(t)[0] || (typeof(t) === "string" && gsap.config().nullTargetWarn !== false ? console.warn("Element not found:", t) : null),
_isWithin = (element, list) => { // check if the element is in the list or is a descendant of an element in the list.
let i = list.length;
while (i--) {
if (list[i] === element || list[i].contains(element)) {
return true;
}
}
return false;
},
_getScrollFunc = (element, {s, sc}) => { // we store the scroller functions in an alternating sequenced Array like [element, verticalScrollFunc, horizontalScrollFunc, ...] so that we can minimize memory, maximize performance, and we also record the last position as a ".rec" property in order to revert to that after refreshing to ensure things don't shift around.
_isViewport(element) && (element = _doc.scrollingElement || _docEl);
let i = _scrollers.indexOf(element),
offset = sc === _vertical.sc ? 1 : 2;
!~i && (i = _scrollers.push(element) - 1);
_scrollers[i + offset] || _addListener(element, "scroll", _onScroll); // clear the cache when a scroll occurs
let prev = _scrollers[i + offset],
func = prev || (_scrollers[i + offset] = _scrollCacheFunc(_getProxyProp(element, s), true) || (_isViewport(element) ? sc : _scrollCacheFunc(function(value) { return arguments.length ? (element[s] = value) : element[s]; })));
func.target = element;
prev || (func.smooth = gsap.getProperty(element, "scrollBehavior") === "smooth"); // only set it the first time (don't reset every time a scrollFunc is requested because perhaps it happens during a refresh() when it's disabled in ScrollTrigger.
return func;
},
_getVelocityProp = (value, minTimeRefresh, useDelta) => {
let v1 = value,
v2 = value,
t1 = _getTime(),
t2 = t1,
min = minTimeRefresh || 50,
dropToZeroTime = Math.max(500, min * 3),
update = (value, force) => {
let t = _getTime();
if (force || t - t1 > min) {
v2 = v1;
v1 = value;
t2 = t1;
t1 = t;
} else if (useDelta) {
v1 += value;
} else { // not totally necessary, but makes it a bit more accurate by adjusting the v1 value according to the new slope. This way we're not just ignoring the incoming data. Removing for now because it doesn't seem to make much practical difference and it's probably not worth the kb.
v1 = v2 + (value - v2) / (t - t2) * (t1 - t2);
}
},
reset = () => { v2 = v1 = useDelta ? 0 : v1; t2 = t1 = 0; },
getVelocity = latestValue => {
let tOld = t2,
vOld = v2,
t = _getTime();
(latestValue || latestValue === 0) && latestValue !== v1 && update(latestValue);
return (t1 === t2 || t - t2 > dropToZeroTime) ? 0 : (v1 + (useDelta ? vOld : -vOld)) / ((useDelta ? t : t1) - tOld) * 1000;
};
return {update, reset, getVelocity};
},
_getEvent = (e, preventDefault) => {
preventDefault && !e._gsapAllow && e.preventDefault();
return e.changedTouches ? e.changedTouches[0] : e;
},
_getAbsoluteMax = a => {
let max = Math.max(...a),
min = Math.min(...a);
return Math.abs(max) >= Math.abs(min) ? max : min;
},
_setScrollTrigger = () => {
ScrollTrigger = gsap.core.globals().ScrollTrigger;
ScrollTrigger && ScrollTrigger.core && _integrate();
},
_initCore = core => {
gsap = core || _getGSAP();
if (!_coreInitted && gsap && typeof(document) !== "undefined" && document.body) {
_win = window;
_doc = document;
_docEl = _doc.documentElement;
_body = _doc.body;
_root = [_win, _doc, _docEl, _body];
_clamp = gsap.utils.clamp;
_context = gsap.core.context || function() {};
_pointerType = "onpointerenter" in _body ? "pointer" : "mouse";
// isTouch is 0 if no touch, 1 if ONLY touch, and 2 if it can accommodate touch but also other types like mouse/pointer.
_isTouch = Observer.isTouch = _win.matchMedia && _win.matchMedia("(hover: none), (pointer: coarse)").matches ? 1 : ("ontouchstart" in _win || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) ? 2 : 0;
_eventTypes = Observer.eventTypes = ("ontouchstart" in _docEl ? "touchstart,touchmove,touchcancel,touchend" : !("onpointerdown" in _docEl) ? "mousedown,mousemove,mouseup,mouseup" : "pointerdown,pointermove,pointercancel,pointerup").split(",");
setTimeout(() => _startup = 0, 500);
_setScrollTrigger();
_coreInitted = 1;
}
return _coreInitted;
};
_horizontal.op = _vertical;
_scrollers.cache = 0;
export class Observer {
constructor(vars) {
this.init(vars);
}
init(vars) {
_coreInitted || _initCore(gsap) || console.warn("Please gsap.registerPlugin(Observer)");
ScrollTrigger || _setScrollTrigger();
let {tolerance, dragMinimum, type, target, lineHeight, debounce, preventDefault, onStop, onStopDelay, ignore, wheelSpeed, event, onDragStart, onDragEnd, onDrag, onPress, onRelease, onRight, onLeft, onUp, onDown, onChangeX, onChangeY, onChange, onToggleX, onToggleY, onHover, onHoverEnd, onMove, ignoreCheck, isNormalizer, onGestureStart, onGestureEnd, onWheel, onEnable, onDisable, onClick, scrollSpeed, capture, allowClicks, lockAxis, onLockAxis} = vars;
this.target = target = _getTarget(target) || _docEl;
this.vars = vars;
ignore && (ignore = gsap.utils.toArray(ignore));
tolerance = tolerance || 1e-9;
dragMinimum = dragMinimum || 0;
wheelSpeed = wheelSpeed || 1;
scrollSpeed = scrollSpeed || 1;
type = type || "wheel,touch,pointer";
debounce = debounce !== false;
lineHeight || (lineHeight = parseFloat(_win.getComputedStyle(_body).lineHeight) || 22); // note: browser may report "normal", so default to 22.
let id, onStopDelayedCall, dragged, moved, wheeled, locked, axis,
self = this,
prevDeltaX = 0,
prevDeltaY = 0,
passive = vars.passive || (!preventDefault && vars.passive !== false),
scrollFuncX = _getScrollFunc(target, _horizontal),
scrollFuncY = _getScrollFunc(target, _vertical),
scrollX = scrollFuncX(),
scrollY = scrollFuncY(),
limitToTouch = ~type.indexOf("touch") && !~type.indexOf("pointer") && _eventTypes[0] === "pointerdown", // for devices that accommodate mouse events and touch events, we need to distinguish.
isViewport = _isViewport(target),
ownerDoc = target.ownerDocument || _doc,
deltaX = [0, 0, 0], // wheel, scroll, pointer/touch
deltaY = [0, 0, 0],
onClickTime = 0,
clickCapture = () => onClickTime = _getTime(),
_ignoreCheck = (e, isPointerOrTouch) => (self.event = e) && (ignore && _isWithin(e.target, ignore)) || (isPointerOrTouch && limitToTouch && e.pointerType !== "touch") || (ignoreCheck && ignoreCheck(e, isPointerOrTouch)),
onStopFunc = () => {
self._vx.reset();
self._vy.reset();
onStopDelayedCall.pause();
onStop && onStop(self);
},
update = () => {
let dx = self.deltaX = _getAbsoluteMax(deltaX),
dy = self.deltaY = _getAbsoluteMax(deltaY),
changedX = Math.abs(dx) >= tolerance,
changedY = Math.abs(dy) >= tolerance;
onChange && (changedX || changedY) && onChange(self, dx, dy, deltaX, deltaY); // in ScrollTrigger.normalizeScroll(), we need to know if it was touch/pointer so we need access to the deltaX/deltaY Arrays before we clear them out.
if (changedX) {
onRight && self.deltaX > 0 && onRight(self);
onLeft && self.deltaX < 0 && onLeft(self);
onChangeX && onChangeX(self);
onToggleX && ((self.deltaX < 0) !== (prevDeltaX < 0)) && onToggleX(self);
prevDeltaX = self.deltaX;
deltaX[0] = deltaX[1] = deltaX[2] = 0
}
if (changedY) {
onDown && self.deltaY > 0 && onDown(self);
onUp && self.deltaY < 0 && onUp(self);
onChangeY && onChangeY(self);
onToggleY && ((self.deltaY < 0) !== (prevDeltaY < 0)) && onToggleY(self);
prevDeltaY = self.deltaY;
deltaY[0] = deltaY[1] = deltaY[2] = 0
}
if (moved || dragged) {
onMove && onMove(self);
if (dragged) {
onDragStart && dragged === 1 && onDragStart(self);
onDrag && onDrag(self);
dragged = 0;
}
moved = false;
}
locked && !(locked = false) && onLockAxis && onLockAxis(self);
if (wheeled) {
onWheel(self);
wheeled = false;
}
id = 0;
},
onDelta = (x, y, index) => {
deltaX[index] += x;
deltaY[index] += y;
self._vx.update(x);
self._vy.update(y);
debounce ? id || (id = requestAnimationFrame(update)) : update();
},
onTouchOrPointerDelta = (x, y) => {
if (lockAxis && !axis) {
self.axis = axis = Math.abs(x) > Math.abs(y) ? "x" : "y";
locked = true;
}
if (axis !== "y") {
deltaX[2] += x;
self._vx.update(x, true); // update the velocity as frequently as possible instead of in the debounced function so that very quick touch-scrolls (flicks) feel natural. If it's the mouse/touch/pointer, force it so that we get snappy/accurate momentum scroll.
}
if (axis !== "x") {
deltaY[2] += y;
self._vy.update(y, true);
}
debounce ? id || (id = requestAnimationFrame(update)) : update();
},
_onDrag = e => {
if (_ignoreCheck(e, 1)) {return;}
e = _getEvent(e, preventDefault);
let x = e.clientX,
y = e.clientY,
dx = x - self.x,
dy = y - self.y,
isDragging = self.isDragging;
self.x = x;
self.y = y;
if (isDragging || ((dx || dy) && (Math.abs(self.startX - x) >= dragMinimum || Math.abs(self.startY - y) >= dragMinimum))) {
dragged = isDragging ? 2 : 1; // dragged: 0 = not dragging, 1 = first drag, 2 = normal drag
isDragging || (self.isDragging = true);
onTouchOrPointerDelta(dx, dy);
}
},
_onPress = self.onPress = e => {
if (_ignoreCheck(e, 1) || (e && e.button)) {return;}
self.axis = axis = null;
onStopDelayedCall.pause();
self.isPressed = true;
e = _getEvent(e); // note: may need to preventDefault(?) Won't side-scroll on iOS Safari if we do, though.
prevDeltaX = prevDeltaY = 0;
self.startX = self.x = e.clientX;
self.startY = self.y = e.clientY;
self._vx.reset(); // otherwise the t2 may be stale if the user touches and flicks super fast and releases in less than 2 requestAnimationFrame ticks, causing velocity to be 0.
self._vy.reset();
_addListener(isNormalizer ? target : ownerDoc, _eventTypes[1], _onDrag, passive, true);
self.deltaX = self.deltaY = 0;
onPress && onPress(self);
},
_onRelease = self.onRelease = e => {
if (_ignoreCheck(e, 1)) {return;}
_removeListener(isNormalizer ? target : ownerDoc, _eventTypes[1], _onDrag, true);
let isTrackingDrag = !isNaN(self.y - self.startY),
wasDragging = self.isDragging,
isDragNotClick = wasDragging && (Math.abs(self.x - self.startX) > 3 || Math.abs(self.y - self.startY) > 3), // some touch devices need some wiggle room in terms of sensing clicks - the finger may move a few pixels.
eventData = _getEvent(e);
if (!isDragNotClick && isTrackingDrag) {
self._vx.reset();
self._vy.reset();
//if (preventDefault && allowClicks && self.isPressed) { // check isPressed because in a rare edge case, the inputObserver in ScrollTrigger may stopPropagation() on the press/drag, so the onRelease may get fired without the onPress/onDrag ever getting called, thus it could trigger a click to occur on a link after scroll-dragging it.
if (preventDefault && allowClicks) {
gsap.delayedCall(0.08, () => { // some browsers (like Firefox) won't trust script-generated clicks, so if the user tries to click on a video to play it, for example, it simply won't work. Since a regular "click" event will most likely be generated anyway (one that has its isTrusted flag set to true), we must slightly delay our script-generated click so that the "real"/trusted one is prioritized. Remember, when there are duplicate events in quick succession, we suppress all but the first one. Some browsers don't even trigger the "real" one at all, so our synthetic one is a safety valve that ensures that no matter what, a click event does get dispatched.
if (_getTime() - onClickTime > 300 && !e.defaultPrevented) {
if (e.target.click) { //some browsers (like mobile Safari) don't properly trigger the click event
e.target.click();
} else if (ownerDoc.createEvent) {
let syntheticEvent = ownerDoc.createEvent("MouseEvents");
syntheticEvent.initMouseEvent("click", true, true, _win, 1, eventData.screenX, eventData.screenY, eventData.clientX, eventData.clientY, false, false, false, false, 0, null);
e.target.dispatchEvent(syntheticEvent);
}
}
});
}
}
self.isDragging = self.isGesturing = self.isPressed = false;
onStop && wasDragging && !isNormalizer && onStopDelayedCall.restart(true);
dragged && update(); // in case debouncing, we don't want onDrag to fire AFTER onDragEnd().
onDragEnd && wasDragging && onDragEnd(self);
onRelease && onRelease(self, isDragNotClick);
},
_onGestureStart = e => e.touches && e.touches.length > 1 && (self.isGesturing = true) && onGestureStart(e, self.isDragging),
_onGestureEnd = () => (self.isGesturing = false) || onGestureEnd(self),
onScroll = e => {
if (_ignoreCheck(e)) {return;}
let x = scrollFuncX(),
y = scrollFuncY();
onDelta((x - scrollX) * scrollSpeed, (y - scrollY) * scrollSpeed, 1);
scrollX = x;
scrollY = y;
onStop && onStopDelayedCall.restart(true);
},
_onWheel = e => {
if (_ignoreCheck(e)) {return;}
e = _getEvent(e, preventDefault);
onWheel && (wheeled = true);
let multiplier = (e.deltaMode === 1 ? lineHeight : e.deltaMode === 2 ? _win.innerHeight : 1) * wheelSpeed;
onDelta(e.deltaX * multiplier, e.deltaY * multiplier, 0);
onStop && !isNormalizer && onStopDelayedCall.restart(true);
},
_onMove = e => {
if (_ignoreCheck(e)) {return;}
let x = e.clientX,
y = e.clientY,
dx = x - self.x,
dy = y - self.y;
self.x = x;
self.y = y;
moved = true;
onStop && onStopDelayedCall.restart(true);
(dx || dy) && onTouchOrPointerDelta(dx, dy);
},
_onHover = e => {self.event = e; onHover(self);},
_onHoverEnd = e => {self.event = e; onHoverEnd(self);},
_onClick = e => _ignoreCheck(e) || (_getEvent(e, preventDefault) && onClick(self));
onStopDelayedCall = self._dc = gsap.delayedCall(onStopDelay || 0.25, onStopFunc).pause();
self.deltaX = self.deltaY = 0;
self._vx = _getVelocityProp(0, 50, true);
self._vy = _getVelocityProp(0, 50, true);
self.scrollX = scrollFuncX;
self.scrollY = scrollFuncY;
self.isDragging = self.isGesturing = self.isPressed = false;
_context(this);
self.enable = e => {
if (!self.isEnabled) {
_addListener(isViewport ? ownerDoc : target, "scroll", _onScroll);
type.indexOf("scroll") >= 0 && _addListener(isViewport ? ownerDoc : target, "scroll", onScroll, passive, capture);
type.indexOf("wheel") >= 0 && _addListener(target, "wheel", _onWheel, passive, capture);
if ((type.indexOf("touch") >= 0 && _isTouch) || type.indexOf("pointer") >= 0) {
_addListener(target, _eventTypes[0], _onPress, passive, capture);
_addListener(ownerDoc, _eventTypes[2], _onRelease);
_addListener(ownerDoc, _eventTypes[3], _onRelease);
allowClicks && _addListener(target, "click", clickCapture, true, true);
onClick && _addListener(target, "click", _onClick);
onGestureStart && _addListener(ownerDoc, "gesturestart", _onGestureStart);
onGestureEnd && _addListener(ownerDoc, "gestureend", _onGestureEnd);
onHover && _addListener(target, _pointerType + "enter", _onHover);
onHoverEnd && _addListener(target, _pointerType + "leave", _onHoverEnd);
onMove && _addListener(target, _pointerType + "move", _onMove);
}
self.isEnabled = true;
self.isDragging = self.isGesturing = self.isPressed = moved = dragged = false;
self._vx.reset();
self._vy.reset();
scrollX = scrollFuncX();
scrollY = scrollFuncY();
e && e.type && _onPress(e);
onEnable && onEnable(self);
}
return self;
};
self.disable = () => {
if (self.isEnabled) {
// only remove the _onScroll listener if there aren't any others that rely on the functionality.
_observers.filter(o => o !== self && _isViewport(o.target)).length || _removeListener(isViewport ? ownerDoc : target, "scroll", _onScroll);
if (self.isPressed) {
self._vx.reset();
self._vy.reset();
_removeListener(isNormalizer ? target : ownerDoc, _eventTypes[1], _onDrag, true);
}
_removeListener(isViewport ? ownerDoc : target, "scroll", onScroll, capture);
_removeListener(target, "wheel", _onWheel, capture);
_removeListener(target, _eventTypes[0], _onPress, capture);
_removeListener(ownerDoc, _eventTypes[2], _onRelease);
_removeListener(ownerDoc, _eventTypes[3], _onRelease);
_removeListener(target, "click", clickCapture, true);
_removeListener(target, "click", _onClick);
_removeListener(ownerDoc, "gesturestart", _onGestureStart);
_removeListener(ownerDoc, "gestureend", _onGestureEnd);
_removeListener(target, _pointerType + "enter", _onHover);
_removeListener(target, _pointerType + "leave", _onHoverEnd);
_removeListener(target, _pointerType + "move", _onMove);
self.isEnabled = self.isPressed = self.isDragging = false;
onDisable && onDisable(self);
}
};
self.kill = self.revert = () => {
self.disable();
let i = _observers.indexOf(self);
i >= 0 && _observers.splice(i, 1);
_normalizer === self && (_normalizer = 0);
}
_observers.push(self);
isNormalizer && _isViewport(target) && (_normalizer = self);
self.enable(event);
}
get velocityX() {
return this._vx.getVelocity();
}
get velocityY() {
return this._vy.getVelocity();
}
}
Observer.version = "3.13.0";
Observer.create = vars => new Observer(vars);
Observer.register = _initCore;
Observer.getAll = () => _observers.slice();
Observer.getById = id => _observers.filter(o => o.vars.id === id)[0];
_getGSAP() && gsap.registerPlugin(Observer);
export { Observer as default, _isViewport, _scrollers, _getScrollFunc, _getProxyProp, _proxies, _getVelocityProp, _vertical, _horizontal, _getTarget };