(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);