vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
224 lines (173 loc) • 5.78 kB
text/typescript
import { clamp } from '@/utils';
import { Snap, SnapSlide } from '../..';
import { parallaxAttributes, parallaxGroups, parallaxTypes } from './constants';
import { parallaxAttrPrefix } from './globals';
import { ISnapSlideParallaxItem, ISnapSlideParallaxType } from './types';
export class SnapSlideParallax {
private _observer: MutationObserver;
private _types: ISnapSlideParallaxType[] = [];
private _items: ISnapSlideParallaxItem[] = [];
private _debounceInit: NodeJS.Timeout | null = null;
constructor(
private _snap: Snap,
private _slide: SnapSlide,
private _element: HTMLElement,
) {
this._initDebounce();
this._observer = new MutationObserver((mutations) => {
mutations.forEach(({ attributeName }) => {
if (attributeName && parallaxAttributes.includes(attributeName)) {
this._initDebounce();
}
});
});
this._observer.observe(this._element, { attributes: true });
}
/** Initialize parallax with debounce */
private _initDebounce() {
if (this._debounceInit) {
clearTimeout(this._debounceInit);
}
this._debounceInit = setTimeout(() => this._init(), 16);
}
/** Initialize parallax */
private _init() {
this._updateItems();
this.render();
}
/** Update parallax items */
private _updateItems() {
const element = this._element;
const defaultScope = this._getScope(
this._element,
`${parallaxAttrPrefix}scope`,
[-1, 1],
);
this._types = parallaxTypes.filter(({ n }) => element.hasAttribute(n));
this._items = this._types.map(
({ n, p, u: defaultUnit, isAbs: isAbsProp, modifier }) => {
const group = parallaxGroups.find(
// eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
({ types }) => types.find((type) => type.n === n)!!,
);
const scopeAttr = `${n}-scope`;
const scope = element.hasAttribute(scopeAttr)
? this._getScope(element, scopeAttr, [-1, 1])
: defaultScope;
const attrValue = this._getAttr(element, n);
const unit = attrValue.replace(/[-\d.]+/g, '') || defaultUnit;
const target = this._getFloatAttr(element, n, 0);
const offset = this._getFloatAttr(element, `${n}-offset`, 0);
const min = this._getFloatAttr(element, `${n}-min`, -Infinity);
const max = this._getFloatAttr(element, `${n}-max`, Infinity);
const influenceAttr = `${n}-influence`;
const influence = element.hasAttribute(influenceAttr)
? this._getFloatAttr(element, `${n}-influence`, 1)
: 0;
const directionalAttr = `${n}-directional`;
const isDirectional = element.hasAttribute(directionalAttr);
const absAttr = `${n}-abs`;
const isAbs = isAbsProp || element.hasAttribute(absAttr);
return {
n,
p,
u: unit,
group: group?.name ?? '',
modifier,
scope,
progress: 0,
target,
value: 0,
offset,
min,
max,
influence,
isDirectional,
isAbs,
};
},
);
}
/** Get parallax attribute */
private _getAttr(element: HTMLElement, name: string) {
return element.getAttribute(name) ?? '';
}
/** Get parallax float attribute */
private _getFloatAttr(
element: HTMLElement,
name: string,
defaultValue: number,
) {
const float = parseFloat(this._getAttr(element, name));
return Number.isNaN(float) ? defaultValue : float;
}
/** Get parallax scope */
private _getScope(
element: HTMLElement,
name: string,
defaultValue: number[],
) {
const attrValue = this._getAttr(element, name);
const attrStringValue = attrValue.trim().toLowerCase();
if (attrStringValue === 'none') {
return [-Infinity, Infinity];
}
if (attrStringValue === 'const') {
return [1, 1];
}
const cleanValue = attrValue.replace(/[\s\\[\]]+/g, '');
const minMax = cleanValue.split(',');
const minRaw = parseFloat(minMax[0]);
const maxRaw = parseFloat(minMax[1]);
const min = Number.isNaN(minRaw) ? defaultValue[0] : minRaw;
const max = Number.isNaN(maxRaw) ? defaultValue[1] : maxRaw;
return [min, max];
}
/** Render parallax */
public render() {
const {
_snap: snap,
_element: element,
_items: items,
_slide: slide,
} = this;
const globalProgress = slide.progress;
// Calculate parallax values
items.forEach((item) => {
let progress = clamp(globalProgress, ...item.scope);
if (Math.abs(item.influence) > 0) {
progress *= Math.abs(snap.influence) * item.influence;
}
if (item.isDirectional) {
progress = Math.abs(progress) * Math.sign(snap.influence);
}
if (item.isAbs) {
progress = Math.abs(progress);
}
item.progress = progress;
item.value = item.offset + progress * item.target;
if (item.modifier) {
item.value = item.modifier(item.value);
}
item.value = clamp(item.value, item.min, item.max);
});
parallaxGroups.forEach(({ name: groupName }) => {
const groupItems = items.filter((item) => item.group === groupName);
const styles = groupItems.map(({ value, p, u }) => {
if (groupName === 'opacity') {
return `${value}`;
}
return `${p}(${value}${u})`;
});
const styleString = styles.join(' ');
element.style[groupName as any] = styleString;
});
}
/** Destroy parallax */
public destroy() {
this._observer.disconnect();
if (this._debounceInit) {
clearTimeout(this._debounceInit);
}
}
}