281 lines
7.5 KiB
JavaScript
281 lines
7.5 KiB
JavaScript
(function (window) {
|
|
"use strict";
|
|
|
|
function extend(a, b) {
|
|
for (var key in b) {
|
|
if (b.hasOwnProperty(key)) {
|
|
a[key] = b[key];
|
|
}
|
|
}
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* TiltFx obj.
|
|
*/
|
|
function TiltFx(el, options) {
|
|
this.DOM = {};
|
|
this.DOM.el = el;
|
|
this.options = extend({}, this.options);
|
|
extend(this.options, options);
|
|
this._init();
|
|
}
|
|
|
|
TiltFx.prototype.options = {
|
|
movement: {
|
|
wrapper: {
|
|
translation: { x: 0, y: 0, z: 0 },
|
|
rotation: { x: -5, y: 5, z: 0 },
|
|
reverseAnimation: {
|
|
duration: 1200,
|
|
easing: "easeOutElastic",
|
|
elasticity: 600,
|
|
},
|
|
},
|
|
shine: {
|
|
translation: { x: 50, y: 50, z: 0 },
|
|
reverseAnimation: {
|
|
duration: 1200,
|
|
easing: "easeOutElastic",
|
|
elasticity: 600,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Init.
|
|
*/
|
|
TiltFx.prototype._init = function () {
|
|
this.DOM.animatable = {};
|
|
this.DOM.animatable.wrapper = this.DOM.el.querySelector(".card__figure");
|
|
this.DOM.animatable.shine = this.DOM.el.querySelector(
|
|
".card__deco--shine > div"
|
|
);
|
|
this._gyroActive = false;
|
|
this._initEvents();
|
|
this._initGyro();
|
|
};
|
|
|
|
/**
|
|
* Init/Bind events.
|
|
*/
|
|
TiltFx.prototype._initEvents = function () {
|
|
var self = this;
|
|
|
|
this.mouseenterFn = function () {
|
|
for (var key in self.DOM.animatable) {
|
|
anime.remove(self.DOM.animatable[key]);
|
|
}
|
|
};
|
|
|
|
this.mousemoveFn = function (ev) {
|
|
if (self._gyroActive) return;
|
|
requestAnimationFrame(function () {
|
|
var bounds = self.DOM.el.getBoundingClientRect();
|
|
self._applyTilt(
|
|
ev.clientX - bounds.left,
|
|
ev.clientY - bounds.top,
|
|
bounds.width,
|
|
bounds.height
|
|
);
|
|
});
|
|
};
|
|
|
|
this.mouseleaveFn = function () {
|
|
requestAnimationFrame(function () {
|
|
self._animateReset();
|
|
});
|
|
};
|
|
|
|
this.debounceLeaveFn = function (e) {
|
|
e.stopPropagation();
|
|
clearTimeout(self.timeout);
|
|
self.timeout = setTimeout(function () {
|
|
self._animateReset();
|
|
}, 25);
|
|
};
|
|
|
|
// Touch handlers
|
|
this.touchstartFn = function () {
|
|
if (self._gyroActive) return;
|
|
for (var key in self.DOM.animatable) {
|
|
anime.remove(self.DOM.animatable[key]);
|
|
}
|
|
};
|
|
|
|
this.touchmoveFn = function (ev) {
|
|
if (self._gyroActive) return;
|
|
var touch = ev.touches[0];
|
|
if (!touch) return;
|
|
requestAnimationFrame(function () {
|
|
var bounds = self.DOM.el.getBoundingClientRect();
|
|
self._applyTilt(
|
|
touch.clientX - bounds.left,
|
|
touch.clientY - bounds.top,
|
|
bounds.width,
|
|
bounds.height
|
|
);
|
|
});
|
|
};
|
|
|
|
this.DOM.el.addEventListener("mousemove", this.mousemoveFn);
|
|
this.DOM.el.addEventListener("mouseleave", this.mouseleaveFn);
|
|
this.DOM.el.addEventListener("mouseenter", this.mouseenterFn);
|
|
this.DOM.el.addEventListener("touchstart", this.touchstartFn, { passive: true });
|
|
this.DOM.el.addEventListener("touchmove", this.touchmoveFn, { passive: true });
|
|
this.DOM.el.addEventListener("touchend", this.debounceLeaveFn);
|
|
};
|
|
|
|
/**
|
|
* Apply tilt from a relative position within the card.
|
|
*/
|
|
TiltFx.prototype._applyTilt = function (relX, relY, width, height) {
|
|
for (var key in this.DOM.animatable) {
|
|
if (
|
|
this.DOM.animatable[key] == undefined ||
|
|
this.options.movement[key] == undefined
|
|
) {
|
|
continue;
|
|
}
|
|
var t =
|
|
this.options.movement[key].translation || { x: 0, y: 0, z: 0 },
|
|
r =
|
|
this.options.movement[key].rotation || { x: 0, y: 0, z: 0 },
|
|
setRange = function (obj) {
|
|
for (var k in obj) {
|
|
if (obj[k] == undefined) {
|
|
obj[k] = [0, 0];
|
|
} else if (typeof obj[k] === "number") {
|
|
obj[k] = [-1 * obj[k], obj[k]];
|
|
}
|
|
}
|
|
};
|
|
|
|
setRange(t);
|
|
setRange(r);
|
|
|
|
var transforms = {
|
|
translation: {
|
|
x: ((t.x[1] - t.x[0]) / width) * relX + t.x[0],
|
|
y: ((t.y[1] - t.y[0]) / height) * relY + t.y[0],
|
|
z: ((t.z[1] - t.z[0]) / height) * relY + t.z[0],
|
|
},
|
|
rotation: {
|
|
x: ((r.x[1] - r.x[0]) / height) * relY + r.x[0],
|
|
y: ((r.y[1] - r.y[0]) / width) * relX + r.y[0],
|
|
z: ((r.z[1] - r.z[0]) / width) * relX + r.z[0],
|
|
},
|
|
};
|
|
|
|
this.DOM.animatable[key].style.transform =
|
|
"translateX(" +
|
|
transforms.translation.x +
|
|
"px) translateY(" +
|
|
transforms.translation.y +
|
|
"px) translateZ(" +
|
|
transforms.translation.z +
|
|
"px) rotateX(" +
|
|
transforms.rotation.x +
|
|
"deg) rotateY(" +
|
|
transforms.rotation.y +
|
|
"deg) rotateZ(" +
|
|
transforms.rotation.z +
|
|
"deg)";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Animate back to resting position.
|
|
*/
|
|
TiltFx.prototype._animateReset = function () {
|
|
for (var key in this.DOM.animatable) {
|
|
if (this.options.movement[key] == undefined) {
|
|
continue;
|
|
}
|
|
anime({
|
|
targets: this.DOM.animatable[key],
|
|
duration:
|
|
this.options.movement[key].reverseAnimation != undefined
|
|
? this.options.movement[key].reverseAnimation.duration || 0
|
|
: 1,
|
|
easing:
|
|
this.options.movement[key].reverseAnimation != undefined
|
|
? this.options.movement[key].reverseAnimation.easing || "linear"
|
|
: "linear",
|
|
elasticity:
|
|
this.options.movement[key].reverseAnimation != undefined
|
|
? this.options.movement[key].reverseAnimation.elasticity || null
|
|
: null,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
scaleZ: 1,
|
|
translateX: 0,
|
|
translateY: 0,
|
|
translateZ: 0,
|
|
rotateX: 0,
|
|
rotateY: 0,
|
|
rotateZ: 0,
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Init gyroscope / device orientation tilt.
|
|
* On iOS 13+ this requires a user gesture to request permission.
|
|
*/
|
|
TiltFx.prototype._initGyro = function () {
|
|
var self = this;
|
|
|
|
if (!window.DeviceOrientationEvent) return;
|
|
|
|
// iOS 13+ requires explicit permission request — skip those devices
|
|
if (typeof DeviceOrientationEvent.requestPermission === "function") return;
|
|
|
|
// On Android and other devices, test if events actually fire
|
|
var testHandler = function (e) {
|
|
if (e.gamma !== null && e.beta !== null) {
|
|
self._bindGyro();
|
|
}
|
|
window.removeEventListener("deviceorientation", testHandler);
|
|
};
|
|
window.addEventListener("deviceorientation", testHandler);
|
|
};
|
|
|
|
TiltFx.prototype._bindGyro = function () {
|
|
var self = this;
|
|
this._gyroActive = true;
|
|
|
|
// Stop any in-progress touch/mouse animations
|
|
for (var key in this.DOM.animatable) {
|
|
anime.remove(this.DOM.animatable[key]);
|
|
}
|
|
|
|
window.addEventListener("deviceorientation", function (e) {
|
|
requestAnimationFrame(function () {
|
|
// beta: front-back tilt (-180 to 180), gamma: left-right tilt (-90 to 90)
|
|
var beta = e.beta || 0;
|
|
var gamma = e.gamma || 0;
|
|
|
|
// Clamp to reasonable range
|
|
beta = Math.max(-30, Math.min(30, beta));
|
|
gamma = Math.max(-30, Math.min(30, gamma));
|
|
|
|
// Normalize to 0-1 range (centered at 0 degrees = 0.5)
|
|
var normX = (gamma + 30) / 60;
|
|
var normY = (beta + 30) / 60;
|
|
|
|
var bounds = self.DOM.el.getBoundingClientRect();
|
|
self._applyTilt(
|
|
normX * bounds.width,
|
|
normY * bounds.height,
|
|
bounds.width,
|
|
bounds.height
|
|
);
|
|
});
|
|
});
|
|
};
|
|
|
|
window.TiltFx = TiltFx;
|
|
})(window);
|