@adoratorio/hades
Version:
A smooth scrollbar based on Hermes, scroll down 'till hell
253 lines • 9 kB
JavaScript
import Aion from '@adoratorio/aion';
import Hermes from '@adoratorio/hermes';
import { DIRECTION, } from "./declarations";
import Easings from "./easing";
class Hades {
static EASING = Easings;
static DIRECTION = DIRECTION;
_amount = { x: 0, y: 0 };
_temp = { x: 0, y: 0 };
options;
engine;
manager;
scrollHandler;
frameHandler;
timeline;
prevDirection = { x: Hades.DIRECTION.INITIAL, y: Hades.DIRECTION.INITIAL };
prevAmount = { x: 0, y: 0 };
automaticScrolling = false;
imediateScrolling = false;
aionId = `hades-frame-${performance.now()}`;
plugins = [];
internalId = 0;
amount = { x: 0, y: 0 };
velocity = { x: 0, y: 0 };
running = false;
constructor(options) {
const defaults = {
root: document.body,
easing: {
mode: Easings.LINEAR,
duration: 1000,
},
autoplay: true,
aion: null,
globalMultiplier: 1,
touchMultiplier: 1.5,
smoothDirectionChange: false,
threshold: {
x: 0,
y: 3,
},
invert: false,
precision: 4,
};
this.options = { ...defaults, ...options };
this.timeline = {
start: 0,
duration: this.options.easing.duration,
initial: { x: 0, y: 0 },
final: { x: 0, y: 0 },
current: { x: 0, y: 0 },
};
this.scrollHandler = (event) => this.scroll(event);
this.frameHandler = (delta) => this.frame(delta);
this.manager = new Hermes({
mode: Hermes.MODE.VIRTUAL,
root: this.options.root,
touchMultiplier: this.options.touchMultiplier,
passive: false,
});
if (this.options.autoplay)
this.play();
if (this.options.aion === null || typeof this.options.aion === 'undefined') {
this.engine = new Aion({});
}
else {
this.engine = this.options.aion;
}
this.engine.add(this.frameHandler, this.aionId);
this.engine.start();
}
frame(delta) {
this.plugins.forEach((plugin) => plugin.preFrame && plugin.preFrame(this));
this.timeline.final.x = this._amount.x;
this.timeline.final.y = this._amount.y;
delta = Math.min(Math.max(delta, 0), this.options.easing.duration);
let time = delta / this.timeline.duration;
if (this.imediateScrolling) {
time = 1;
this.imediateScrolling = false;
}
time = this.options.easing.mode(time);
this.timeline.current.x = this.timeline.initial.x + (time * (this.timeline.final.x - this.timeline.initial.x));
this.timeline.current.y = this.timeline.initial.y + (time * (this.timeline.final.y - this.timeline.initial.y));
const current = {
x: this.timeline.current.x,
y: this.timeline.current.y,
};
this.amount = current;
this.velocity = {
x: (current.x - this.prevAmount.x) / delta,
y: (current.y - this.prevAmount.y) / delta,
};
this.prevAmount = this.amount;
this.velocity.x = parseFloat(this.velocity.x.toFixed(this.options.precision));
this.velocity.y = parseFloat(this.velocity.y.toFixed(this.options.precision));
const currentXDirection = this.velocity.x === 0 ? (Hades.DIRECTION.INITIAL) : (this.velocity.x > 0 ? Hades.DIRECTION.DOWN : Hades.DIRECTION.UP);
const currentYDirection = this.velocity.y === 0 ? (Hades.DIRECTION.INITIAL) : (this.velocity.y > 0 ? Hades.DIRECTION.DOWN : Hades.DIRECTION.UP);
if (!this.options.smoothDirectionChange && !this.automaticScrolling) {
if (currentXDirection !== this.prevDirection.x)
this._amount.x = this.amount.x;
if (currentYDirection !== this.prevDirection.y)
this._amount.y = this.amount.y;
}
this.prevDirection.x = currentXDirection;
this.prevDirection.y = currentYDirection;
this.timeline.initial = this.timeline.current;
this.plugins.forEach((plugin) => plugin.render && plugin.render(this));
}
scroll(event) {
let prevent = false;
this.plugins.forEach((plugin) => { if (plugin.wheel)
prevent = plugin.wheel(this, event); });
if (prevent)
return;
if (!this.running)
return;
if (Math.abs(event.delta.x) < this.options.threshold.x)
event.delta.x = 0;
if (Math.abs(event.delta.y) < this.options.threshold.y)
event.delta.y = 0;
if (this.automaticScrolling) {
this.timeline.duration = this.options.easing.duration;
this.amount = this.prevAmount;
this.automaticScrolling = false;
}
this.plugins.forEach((plugin) => plugin.preScroll && plugin.preScroll(this, event));
event.delta.x = event.delta.x * this.options.globalMultiplier;
event.delta.y = event.delta.y * this.options.globalMultiplier;
this._temp.x = this._amount.x + (!this.options.invert ? event.delta.x : event.delta.y);
this._temp.y = this._amount.y + (!this.options.invert ? event.delta.y : event.delta.x);
this.plugins.forEach((plugin) => plugin.scroll && plugin.scroll(this, event));
this._amount.x = this._temp.x;
this._amount.y = this._temp.y;
}
scrollTo(position, duration, prevent = false) {
if (duration > 0) {
this.automaticScrolling = true;
this.timeline.duration = duration;
}
else {
this.imediateScrolling = true;
}
if (!this.options.smoothDirectionChange) {
this._amount.x = this.amount.x;
this._amount.y = this.amount.y;
}
if (typeof position.x !== 'undefined')
this._amount.x = position.x;
if (typeof position.y !== 'undefined')
this._amount.y = position.y;
if (!prevent) {
this.plugins.forEach((plugin) => plugin.scrollTo && plugin.scrollTo(this, position, duration));
}
}
registerPlugin(plugin, id) {
let i = null;
if (typeof id === 'undefined') {
i = `hades-plugin-${this.internalId}`;
this.internalId += 1;
}
else {
i = id;
}
this.register(plugin, i);
return i;
}
unregisterPlugin(id) {
const foundIndex = this.plugins.findIndex((p) => p.id === id);
if (foundIndex === -1)
return false;
const found = this.plugins[foundIndex];
if (typeof found?.destroy === 'function')
found.destroy();
this.plugins.splice(foundIndex, 1);
return true;
}
registerPlugins(plugins, ids) {
const is = [];
plugins.forEach((plugin, index) => {
is.push(this.registerPlugin(plugin, ids[index]));
});
return is;
}
getPlugin(name) {
return this.plugins.find(plugin => plugin.name === name);
}
getRenderer() {
const valid = ['VirtualRender', 'LenisRender', 'NativeRender'];
return this.plugins.find((plugin => valid.includes(plugin.name)));
}
play() {
this.running = true;
this.manager.on(this.scrollHandler);
}
pause() {
this.running = false;
this.manager.off();
}
destroy() {
this.plugins.forEach((plugin) => plugin.destroy && plugin.destroy());
this.manager.destroy();
this.engine.remove(this.aionId);
delete this.manager;
delete this.engine;
}
get direction() {
return this.prevDirection;
}
get root() {
return this.options.root;
}
get internalAmount() {
return this._amount;
}
get internalTemp() {
return this._temp;
}
get still() {
return this.direction.y === DIRECTION.INITIAL && this.direction.x === DIRECTION.INITIAL;
}
get easing() {
return this.options.easing;
}
set easing(easing) {
this.options.easing = easing;
}
set touchMultiplier(touchMultiplier) {
this.options.touchMultiplier = touchMultiplier;
}
set smoothDirectionChange(smoothDirectionChange) {
this.options.smoothDirectionChange = smoothDirectionChange;
}
set invert(invert) {
this.options.invert = invert;
}
set internalAmount(values) {
this._amount.x = values.x;
this._amount.y = values.y;
}
set internalTemp(values) {
this._temp.x = values.x;
this._temp.y = values.y;
}
register(plugin, id) {
if (typeof plugin.register === 'function')
plugin.register(this);
plugin.id = id;
this.plugins.push(plugin);
}
}
export default Hades;
//# sourceMappingURL=index.js.map