kuliso
Version:
Tiny library for performant pointer-driven or gyroscope-driven effects
184 lines (155 loc) • 5.22 kB
JavaScript
import { getRect, clamp } from './utilities.js';
import { addScrollendListener } from './scrollend.js';
/**
* Return new progress for {x, y} for the farthest-side formula ("cover").
*
* @param {Object} target
* @param {number} target.left
* @param {number} target.top
* @param {number} target.width
* @param {number} target.height
* @param {Object} root
* @param {number} root.width
* @param {number} root.height
* @param {{x: number, y: number}} scrollPosition
* @returns {{x: (x: number) => number, y: (y: number) => number}}
*/
function centerToTargetFactory (target, root, scrollPosition) {
// we store reference to the arguments and do all calculation on the fly
// so that target dims, scroll position, and root dims are always up-to-date
return {
x (x1) {
const layerCenterX = target.left - scrollPosition.x + target.width / 2;
const isXStartFarthest = layerCenterX >= root.width / 2;
const xDuration = (isXStartFarthest ? layerCenterX : root.width - layerCenterX) * 2;
const x0 = isXStartFarthest ? 0 : layerCenterX - xDuration / 2;
return (x1 - x0) / xDuration;
},
y (y1) {
const layerCenterY = target.top - scrollPosition.y + target.height / 2;
const isYStartFarthest = layerCenterY >= root.height / 2;
const yDuration = (isYStartFarthest ? layerCenterY : root.height - layerCenterY) * 2;
const y0 = isYStartFarthest ? 0 : layerCenterY - yDuration / 2;
return (y1 - y0) / yDuration;
}
};
}
/**
* Updates scroll position on scrollend.
* Used when root is entire viewport and centeredOnTarget=true.
*/
function scrollendCallback (tick, lastProgress) {
this.x = window.scrollX;
this.y = window.scrollY;
requestAnimationFrame(() => tick && tick(lastProgress));
}
/**
* Update root rect when root is entire viewport.
*
* @param {PointerConfig} config
*/
function windowResize (config) {
config.rect.width = window.document.documentElement.clientWidth;
config.rect.height = window.document.documentElement.clientHeight;
}
/**
* Observe and update root rect when root is an element.
*
* @param {PointerConfig} config
* @returns {ResizeObserver}
*/
function observeRootResize (config) {
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
config.rect.width = entry.borderBoxSize[0].inlineSize;
config.rect.height = entry.borderBoxSize[0].blockSize;
});
});
observer.observe(config.root, { box: 'border-box' });
return observer;
}
/**
* Initialize and return a pointer controller.
*
* @private
* @param {PointerConfig} config
* @return {{tick: function, destroy: function}}
*/
export function getController (config) {
let hasCenteredToTarget = false;
let lastProgress = {x: config.rect.width / 2, y: config.rect.height / 2, vx: 0, vy: 0};
let tick, resizeObserver, windowResizeHandler, scrollendHandler, removeScrollendListener;
const scrollPosition = {x: 0, y: 0};
/*
* Prepare scenes data.
*/
config.scenes.forEach((scene) => {
if (scene.target && scene.centeredToTarget) {
scene.transform = centerToTargetFactory(getRect(scene.target), config.rect, scrollPosition);
hasCenteredToTarget = true;
}
if (config.root) {
resizeObserver = observeRootResize(config);
}
else {
windowResizeHandler = windowResize.bind(null, config);
window.addEventListener('resize', windowResizeHandler);
}
});
/**
* Updates progress in all scene effects.
*
* @private
* @param {Object} progress
* @param {number} progress.x
* @param {number} progress.y
* @param {number} progress.vx
* @param {number} progress.vy
*/
tick = function (progress) {
for (let scene of config.scenes) {
if (!scene.disabled) {
// get scene's progress
const normalizedX = scene.transform?.x(progress.x) || progress.x / config.rect.width;
const normalizedY = scene.transform?.y(progress.y) || progress.y / config.rect.height;
const x = +clamp(0, 1, normalizedX).toPrecision(4);
const y = +clamp(0, 1, normalizedY).toPrecision(4);
const velocity = {x: progress.vx, y: progress.vy};
if (config.allowActiveEvent) {
progress.active = (normalizedX <= 1 && normalizedY <= 1 && normalizedX >= 0 && normalizedY >= 0);
}
// run effect
scene.effect(scene, {x, y}, velocity, progress.active);
}
}
Object.assign(lastProgress, progress);
}
if (hasCenteredToTarget) {
scrollendHandler = scrollendCallback.bind(scrollPosition, tick, lastProgress)
removeScrollendListener = addScrollendListener(document, scrollendHandler);
}
/**
* Removes all side effects and deletes all objects.
*/
function destroy () {
config.scenes.forEach(scene => scene.destroy?.());
removeScrollendListener?.();
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
else {
window.removeEventListener('resize', windowResizeHandler);
windowResizeHandler = null;
}
tick = null;
lastProgress = null;
}
/**
* Mouse controller.
*/
return {
tick,
destroy
};
}