mirror of
https://github.com/micku7zu/vanilla-tilt.js
synced 2024-06-26 09:10:00 +02:00
360 lines
10 KiB
JavaScript
360 lines
10 KiB
JavaScript
/**
|
|
* Created by Șandor Sergiu (micku7zu) on 1/27/2017.
|
|
* Original idea: https://github.com/gijsroge/tilt.js
|
|
* MIT License.
|
|
* Version 1.4.1
|
|
*/
|
|
|
|
class VanillaTilt {
|
|
constructor(element, settings = {}) {
|
|
if (!(element instanceof Node)) {
|
|
throw ("Can't initialize VanillaTilt because " + element + " is not a Node.");
|
|
}
|
|
|
|
this.width = null;
|
|
this.height = null;
|
|
this.left = null;
|
|
this.top = null;
|
|
this.transitionTimeout = null;
|
|
this.updateCall = null;
|
|
|
|
this.updateBind = this.update.bind(this);
|
|
this.resetBind = this.reset.bind(this);
|
|
|
|
this.element = element;
|
|
this.settings = this.extendSettings(settings);
|
|
this.elementListener = this.getElementListener();
|
|
|
|
this.reverse = this.settings.reverse ? -1 : 1;
|
|
|
|
this.glare = this.isSettingTrue(this.settings.glare);
|
|
this.glarePrerender = this.isSettingTrue(this.settings['glare-prerender']);
|
|
|
|
if (this.glare) {
|
|
this.prepareGlare();
|
|
}
|
|
|
|
this.addEventListeners();
|
|
}
|
|
|
|
isSettingTrue(setting) {
|
|
return setting === "" || setting === true || setting === 1;
|
|
}
|
|
|
|
/**
|
|
* Method returns element what will be listen mouse events
|
|
* @return {Node}
|
|
*/
|
|
getElementListener() {
|
|
if (!this.settings || !this.settings.mouseEventElement) { return this.element; }
|
|
|
|
if (typeof this.settings.mouseEventElement === 'string') {
|
|
const mouseEventElement = document.querySelector(this.settings.mouseEventElement);
|
|
|
|
if (mouseEventElement) {
|
|
return mouseEventElement;
|
|
}
|
|
}
|
|
|
|
if (this.settings.mouseEventElement instanceof Node && this.settings.mouseEventElement) {
|
|
return this.settings.mouseEventElement;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method set listen methods for this.elementListener
|
|
* @return {Node}
|
|
*/
|
|
addEventListeners() {
|
|
this.onMouseEnterBind = this.onMouseEnter.bind(this);
|
|
this.onMouseMoveBind = this.onMouseMove.bind(this);
|
|
this.onMouseLeaveBind = this.onMouseLeave.bind(this);
|
|
this.onWindowResizeBind = this.onWindowResizeBind.bind(this);
|
|
|
|
this.elementListener.addEventListener('mouseenter', this.onMouseEnterBind);
|
|
this.elementListener.addEventListener('mousemove', this.onMouseMoveBind);
|
|
this.elementListener.addEventListener('mouseleave', this.onMouseLeaveBind);
|
|
|
|
if (this.glare) {
|
|
window.addEventListener('resize', this.onWindowResizeBind);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Method remove event listeners from current this.elementListener
|
|
*/
|
|
removeEventListeners() {
|
|
this.elementListener.removeEventListener('mouseenter', this.onMouseEnterBind);
|
|
this.elementListener.removeEventListener('mousemove', this.onMouseMoveBind);
|
|
this.elementListener.removeEventListener('mouseleave', this.onMouseLeaveBind);
|
|
|
|
if (this.glare) {
|
|
window.removeEventListener('resize', this.onWindowResizeBind);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
clearTimeout(this.transitionTimeout);
|
|
if (this.updateCall !== null) {
|
|
cancelAnimationFrame(this.updateCall);
|
|
}
|
|
|
|
this.reset();
|
|
|
|
this.removeEventListeners();
|
|
this.element.vanillaTilt = null;
|
|
delete this.element.vanillaTilt;
|
|
|
|
this.element = null;
|
|
}
|
|
|
|
onMouseEnter() {
|
|
this.updateElementPosition();
|
|
this.element.style.willChange = 'transform';
|
|
this.setTransition();
|
|
}
|
|
|
|
onMouseMove(event) {
|
|
if (this.updateCall !== null) {
|
|
cancelAnimationFrame(this.updateCall);
|
|
}
|
|
|
|
this.event = event;
|
|
this.updateCall = requestAnimationFrame(this.updateBind);
|
|
}
|
|
|
|
onMouseLeave() {
|
|
this.setTransition();
|
|
|
|
if (this.settings.reset) {
|
|
requestAnimationFrame(this.resetBind);
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.event = {
|
|
pageX: this.left + this.width / 2,
|
|
pageY: this.top + this.height / 2
|
|
};
|
|
|
|
if (this.element && this.element.style) {
|
|
this.element.style.transform = `perspective(${this.settings.perspective}px) ` +
|
|
`rotateX(0deg) ` +
|
|
`rotateY(0deg) ` +
|
|
`scale3d(1, 1, 1)`;
|
|
}
|
|
|
|
if (this.glare) {
|
|
this.glareElement.style.transform = 'rotate(180deg) translate(-50%, -50%)';
|
|
this.glareElement.style.opacity = '0';
|
|
}
|
|
}
|
|
|
|
getValues() {
|
|
let x = (this.event.clientX - this.left) / this.width;
|
|
let y = (this.event.clientY - this.top) / this.height;
|
|
|
|
x = Math.min(Math.max(x, 0), 1);
|
|
y = Math.min(Math.max(y, 0), 1);
|
|
|
|
let tiltX = (this.reverse * (this.settings.max / 2 - x * this.settings.max)).toFixed(2);
|
|
let tiltY = (this.reverse * (y * this.settings.max - this.settings.max / 2)).toFixed(2);
|
|
let angle = Math.atan2(this.event.clientX - (this.left + this.width / 2), -(this.event.clientY - (this.top + this.height / 2))) * (180 / Math.PI);
|
|
|
|
return {
|
|
tiltX: tiltX,
|
|
tiltY: tiltY,
|
|
percentageX: x * 100,
|
|
percentageY: y * 100,
|
|
angle: angle
|
|
};
|
|
}
|
|
|
|
updateElementPosition() {
|
|
let rect = this.element.getBoundingClientRect();
|
|
|
|
this.width = this.element.offsetWidth;
|
|
this.height = this.element.offsetHeight;
|
|
this.left = rect.left;
|
|
this.top = rect.top;
|
|
}
|
|
|
|
update() {
|
|
let values = this.getValues();
|
|
|
|
this.element.style.transform = "perspective(" + this.settings.perspective + "px) " +
|
|
"rotateX(" + (this.settings.axis === "x" ? 0 : values.tiltY) + "deg) " +
|
|
"rotateY(" + (this.settings.axis === "y" ? 0 : values.tiltX) + "deg) " +
|
|
"scale3d(" + this.settings.scale + ", " + this.settings.scale + ", " + this.settings.scale + ")";
|
|
|
|
if (this.glare) {
|
|
this.glareElement.style.transform = `rotate(${values.angle}deg) translate(-50%, -50%)`;
|
|
this.glareElement.style.opacity = `${values.percentageY * this.settings["max-glare"] / 100}`;
|
|
}
|
|
|
|
this.element.dispatchEvent(new CustomEvent("tiltChange", {
|
|
"detail": values
|
|
}));
|
|
|
|
this.updateCall = null;
|
|
}
|
|
|
|
/**
|
|
* Appends the glare element (if glarePrerender equals false)
|
|
* and sets the default style
|
|
*/
|
|
prepareGlare() {
|
|
// If option pre-render is enabled we assume all html/css is present for an optimal glare effect.
|
|
if (!this.glarePrerender) {
|
|
// Create glare element
|
|
const jsTiltGlare = document.createElement("div");
|
|
jsTiltGlare.classList.add("js-tilt-glare");
|
|
|
|
const jsTiltGlareInner = document.createElement("div");
|
|
jsTiltGlareInner.classList.add("js-tilt-glare-inner");
|
|
|
|
jsTiltGlare.appendChild(jsTiltGlareInner);
|
|
this.element.appendChild(jsTiltGlare);
|
|
}
|
|
|
|
this.glareElementWrapper = this.element.querySelector(".js-tilt-glare");
|
|
this.glareElement = this.element.querySelector(".js-tilt-glare-inner");
|
|
|
|
if (this.glarePrerender) {
|
|
return;
|
|
}
|
|
|
|
Object.assign(this.glareElementWrapper.style, {
|
|
"position": "absolute",
|
|
"top": "0",
|
|
"left": "0",
|
|
"width": "100%",
|
|
"height": "100%",
|
|
"overflow": "hidden"
|
|
});
|
|
|
|
Object.assign(this.glareElement.style, {
|
|
'position': 'absolute',
|
|
'top': '50%',
|
|
'left': '50%',
|
|
'pointer-events': 'none',
|
|
'background-image': `linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%)`,
|
|
'width': `${this.element.offsetWidth * 2}px`,
|
|
'height': `${this.element.offsetWidth * 2}px`,
|
|
'transform': 'rotate(180deg) translate(-50%, -50%)',
|
|
'transform-origin': '0% 0%',
|
|
'opacity': '0',
|
|
});
|
|
}
|
|
|
|
updateGlareSize() {
|
|
Object.assign(this.glareElement.style, {
|
|
'width': `${this.element.offsetWidth * 2}`,
|
|
'height': `${this.element.offsetWidth * 2}`,
|
|
});
|
|
}
|
|
|
|
onWindowResizeBind() {
|
|
this.updateGlareSize();
|
|
}
|
|
|
|
setTransition() {
|
|
clearTimeout(this.transitionTimeout);
|
|
this.element.style.transition = this.settings.speed + "ms " + this.settings.easing;
|
|
if (this.glare) this.glareElement.style.transition = `opacity ${this.settings.speed}ms ${this.settings.easing}`;
|
|
|
|
this.transitionTimeout = setTimeout(() => {
|
|
this.element.style.transition = "";
|
|
if (this.glare) {
|
|
this.glareElement.style.transition = "";
|
|
}
|
|
}, this.settings.speed);
|
|
|
|
}
|
|
|
|
/**
|
|
* Method return patched settings of instance
|
|
* @param {boolean} settings.reverse - reverse the tilt direction
|
|
* @param {number} settings.max - max tilt rotation (degrees)
|
|
* @param {number} settings.perspective - Transform perspective, the lower the more extreme the tilt gets
|
|
* @param {string} settings.easing - Easing on enter/exit
|
|
* @param {number} settings.scale - 2 = 200%, 1.5 = 150%, etc..
|
|
* @param {number} settings.speed - Speed of the enter/exit transition
|
|
* @param {boolean} settings.transition - Set a transition on enter/exit
|
|
* @param settings.axis - What axis should be disabled. Can be X or Y
|
|
* @param {boolean} settings.glare - What axis should be disabled. Can be X or Y
|
|
* @param {number} settings.max-glare - the maximum "glare" opacity (1 = 100%, 0.5 = 50%)
|
|
* @param {boolean} settings.glare-prerender - false = VanillaTilt creates the glare elements for you, otherwise
|
|
* @param {boolean} settings.reset - false = If the tilt effect has to be reset on exit
|
|
* @param {boolean} settings.mouseEventElement - String selector or link to HTML-element what will be listen mouse events
|
|
*/
|
|
extendSettings(settings) {
|
|
let defaultSettings = {
|
|
reverse: false,
|
|
max: 35,
|
|
perspective: 1000,
|
|
easing: 'cubic-bezier(.03,.98,.52,.99)',
|
|
scale: 1,
|
|
speed: 300,
|
|
transition: true,
|
|
axis: null,
|
|
glare: false,
|
|
'max-glare': 1,
|
|
"glare-prerender": false,
|
|
reset: true,
|
|
mouseEventElement: null,
|
|
};
|
|
|
|
let newSettings = {};
|
|
for (var property in defaultSettings) {
|
|
if (property in settings) {
|
|
newSettings[property] = settings[property];
|
|
} else if (this.element.hasAttribute("data-tilt-" + property)) {
|
|
let attribute = this.element.getAttribute("data-tilt-" + property);
|
|
try {
|
|
newSettings[property] = JSON.parse(attribute);
|
|
} catch (e) {
|
|
newSettings[property] = attribute;
|
|
}
|
|
|
|
} else {
|
|
newSettings[property] = defaultSettings[property];
|
|
}
|
|
}
|
|
|
|
return newSettings;
|
|
}
|
|
|
|
static init(elements, settings) {
|
|
if (elements instanceof Node) {
|
|
elements = [elements];
|
|
}
|
|
|
|
if (elements instanceof NodeList) {
|
|
elements = [].slice.call(elements);
|
|
}
|
|
|
|
if (!(elements instanceof Array)) {
|
|
return;
|
|
}
|
|
|
|
elements.forEach((element) => {
|
|
if (!("vanillaTilt" in element)) {
|
|
element.vanillaTilt = new VanillaTilt(element, settings);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (typeof document !== "undefined") {
|
|
/* expose the class to window */
|
|
window.VanillaTilt = VanillaTilt;
|
|
|
|
/**
|
|
* Auto load
|
|
*/
|
|
VanillaTilt.init(document.querySelectorAll("[data-tilt]"));
|
|
}
|
|
|
|
export default VanillaTilt;
|