UNPKG

mouse-follower-cube

Version:

A powerful javascript library to create amazing and smooth effects for the mouse cursor on your website.

630 lines (597 loc) 20.3 kB
/*! * Cuberto Mouse Follower * https://cuberto.com/ * * @version 1.1.2 * @author Cuberto, Artem Dordzhiev (Draft) */ var MouseFollower = /*#__PURE__*/function () { /** * Create cursor instance. * * @param {MouseFollowerOptions} [options] Cursor options. */ function MouseFollower(options) { if (options === void 0) { options = {}; } /** @type {MouseFollowerOptions} **/ this.options = Object.assign({}, { el: null, container: document.body, className: "mf-cursor", innerClassName: "mf-cursor-inner", textClassName: "mf-cursor-text", mediaClassName: "mf-cursor-media", mediaBoxClassName: "mf-cursor-media-box", iconSvgClassName: "mf-svgsprite", iconSvgNamePrefix: "-", iconSvgSrc: "", dataAttr: "cursor", hiddenState: "-hidden", textState: "-text", iconState: "-icon", activeState: "-active", mediaState: "-media", stateDetection: { "-pointer": "a,button" }, visible: true, visibleOnState: false, speed: 0.55, ease: "expo.out", overwrite: true, skewing: 0, skewingText: 2, skewingIcon: 2, skewingMedia: 2, skewingDelta: 0.001, skewingDeltaMax: 0.15, stickDelta: 0.15, showTimeout: 0, hideOnLeave: true, hideTimeout: 300, hideMediaTimeout: 300, initialPos: [-window.innerWidth, -window.innerHeight] }, options); if (this.options.visible && options.stateDetection == null) this.options.stateDetection["-hidden"] = "iframe"; this.gsap = MouseFollower.gsap || window.gsap; this.el = typeof this.options.el === "string" ? document.querySelector(this.options.el) : this.options.el; this.container = typeof this.options.container === "string" ? document.querySelector(this.options.container) : this.options.container; this.skewing = this.options.skewing; this.pos = { x: this.options.initialPos[0], y: this.options.initialPos[1] }; this.vel = { x: 0, y: 0 }; this.event = {}; this.events = []; this.init(); } /** * Init cursor. */ /** * @typedef {Object} MouseFollowerOptions * @property {string|HTMLElement|null} [el] Existed cursor element. * @property {string|HTMLElement|null} [container] Cursor container. * @property {string} [className] Cursor root element class name. * @property {string} [innerClassName] Inner element class name. * @property {string} [textClassName] Text element class name. * @property {string} [mediaClassName] Media element class name. * @property {string} [mediaBoxClassName] Media inner element class name. * @property {string} [iconSvgClassName] SVG sprite class name. * @property {string} [iconSvgNamePrefix] SVG sprite icon class name prefix. * @property {string} [iconSvgSrc] SVG sprite source. * @property {string|null} [dataAttr] Name of data attribute for changing cursor state directly in HTML. * @property {string} [hiddenState] Hidden state name. * @property {string} [textState] Text state name. * @property {string} [iconState] Icon state name. * @property {string|null} [activeState] Active (mousedown) state name. Set false to disable. * @property {string} [mediaState] Media (image/video) state name. * @property {Object} [stateDetection] State detection rules. * @property {boolean} [visible] Is cursor visible by default. * @property {boolean} [visibleOnState] Automatically show/hide cursor when state added. * @property {number} [speed] Cursor movement speed. * @property {string} [ease] Timing function of cursor movement. * @property {boolean} [overwrite] Overwrite or remain cursor position when `mousemove` event happens. * @property {number} [skewing] Default skewing factor. * @property {number} [skewingText] Skewing effect factor in a text state. * @property {number} [skewingIcon] Skewing effect factor in a icon state. * @property {number} [skewingMedia] Skewing effect factor in a media (image/video) state. * @property {number} [skewingDelta] Skewing effect base delta. * @property {number} [skewingDeltaMax] Skew effect max delta. * @property {number} [stickDelta] Stick effect delta. * @property {number} [showTimeout] Delay before show. * @property {boolean} [hideOnLeave] Hide the cursor when mouse leave container. * @property {number} [hideTimeout] Delay before hiding. It should be equal to the CSS hide animation time. * @property {number[]} [initialPos] Array (x, y) of initial cursor position. */ /** * Register GSAP animation library. * * @param {gsap} gsap GSAP library. */ MouseFollower.registerGSAP = function registerGSAP(gsap) { MouseFollower.gsap = gsap; }; var _proto = MouseFollower.prototype; _proto.init = function init() { if (!this.el) this.create(); this.createSetter(); this.bind(); this.render(true); this.ticker = this.render.bind(this, false); this.gsap.ticker.add(this.ticker); } /** * Create cursor DOM element and append to container. */; _proto.create = function create() { this.el = document.createElement("div"); this.el.className = this.options.className; this.el.classList.add(this.options.hiddenState); this.inner = document.createElement("div"); this.inner.className = this.options.innerClassName; this.text = document.createElement("div"); this.text.className = this.options.textClassName; this.media = document.createElement("div"); this.media.className = this.options.mediaClassName; this.mediaBox = document.createElement("div"); this.mediaBox.className = this.options.mediaBoxClassName; this.media.appendChild(this.mediaBox); this.inner.appendChild(this.media); this.inner.appendChild(this.text); this.el.appendChild(this.inner); this.container.appendChild(this.el); } /** * Create GSAP setters. */; _proto.createSetter = function createSetter() { this.setter = { x: this.gsap.quickSetter(this.el, "x", "px"), y: this.gsap.quickSetter(this.el, "y", "px"), // rotation: this.gsap.quickSetter(this.el, "rotation", "deg"), scaleX: this.gsap.quickSetter(this.el, "scaleX"), scaleY: this.gsap.quickSetter(this.el, "scaleY"), wc: this.gsap.quickSetter(this.el, "willChange"), inner: { // rotation: this.gsap.quickSetter(this.inner, "rotation", "deg"), } }; } /** * Create and attach events. */; _proto.bind = function bind() { var _this = this; this.event.mouseleave = function () { return _this.hide(); }; this.event.mouseenter = function () { return _this.show(); }; this.event.mousedown = function () { return _this.addState(_this.options.activeState); }; this.event.mouseup = function () { return _this.removeState(_this.options.activeState); }; this.event.mousemoveOnce = function () { return _this.show(); }; this.event.mousemove = function (e) { _this.gsap.to(_this.pos, { x: _this.stick ? _this.stick.x - (_this.stick.x - e.clientX) * _this.options.stickDelta : e.clientX, y: _this.stick ? _this.stick.y - (_this.stick.y - e.clientY) * _this.options.stickDelta : e.clientY, overwrite: _this.options.overwrite, ease: _this.options.ease, duration: _this.visible ? _this.options.speed : 0, onUpdate: function onUpdate() { return _this.vel = { x: e.clientX - _this.pos.x, y: e.clientY - _this.pos.y }; } }); }; this.event.mouseover = function (e) { for (var target = e.target; target && target !== _this.container; target = target.parentNode) { if (e.relatedTarget && target.contains(e.relatedTarget)) break; for (var state in _this.options.stateDetection) { if (target.matches(_this.options.stateDetection[state])) _this.addState(state); } if (_this.options.dataAttr) { var params = _this.getFromDataset(target); if (params.state) _this.addState(params.state); if (params.text) _this.setText(params.text); if (params.icon) _this.setIcon(params.icon); if (params.img) _this.setImg(params.img); if (params.video) _this.setVideo(params.video); if (typeof params.show !== "undefined") _this.show(); if (typeof params.stick !== "undefined") _this.setStick(params.stick || target); } } }; this.event.mouseout = function (e) { for (var target = e.target; target && target !== _this.container; target = target.parentNode) { if (e.relatedTarget && target.contains(e.relatedTarget)) break; for (var state in _this.options.stateDetection) { if (target.matches(_this.options.stateDetection[state])) _this.removeState(state); } if (_this.options.dataAttr) { var params = _this.getFromDataset(target); if (params.state) _this.removeState(params.state); if (params.text) _this.removeText(); if (params.icon) _this.removeIcon(); if (params.img) _this.removeImg(); if (params.video) _this.removeVideo(); if (typeof params.show !== "undefined") _this.hide(); if (typeof params.stick !== "undefined") _this.removeStick(); } } }; if (this.options.hideOnLeave) { this.container.addEventListener("mouseleave", this.event.mouseleave, { passive: true }); } if (this.options.visible) { this.container.addEventListener("mouseenter", this.event.mouseenter, { passive: true }); } if (this.options.activeState) { this.container.addEventListener("mousedown", this.event.mousedown, { passive: true }); this.container.addEventListener("mouseup", this.event.mouseup, { passive: true }); } this.container.addEventListener("mousemove", this.event.mousemove, { passive: true }); if (this.options.visible) { this.container.addEventListener("mousemove", this.event.mousemoveOnce, { passive: true, once: true }); } if (this.options.stateDetection || this.options.dataAttr) { this.container.addEventListener("mouseover", this.event.mouseover, { passive: true }); this.container.addEventListener("mouseout", this.event.mouseout, { passive: true }); } } /** * Render the cursor in a new position. * * @param {boolean} [force=false] Force render. */; _proto.render = function render(force) { if (force !== true && (this.vel.y === 0 || this.vel.x === 0)) { this.setter.wc("auto"); return; } this.trigger("render"); this.setter.wc("transform"); this.setter.x(this.pos.x); this.setter.y(this.pos.y); if (this.skewing) { var distance = Math.sqrt(Math.pow(this.vel.x, 2) + Math.pow(this.vel.y, 2)); var scale = Math.min(distance * this.options.skewingDelta, this.options.skewingDeltaMax) * this.skewing; // this.setter.rotation(angle); this.setter.scaleX(1 + scale); this.setter.scaleY(1 - scale); // this.setter.inner.rotation(-angle); } } /** * Show cursor. */; _proto.show = function show() { var _this2 = this; this.trigger("show"); clearInterval(this.visibleInt); this.visibleInt = setTimeout(function () { _this2.el.classList.remove(_this2.options.hiddenState); _this2.visible = true; _this2.render(true); }, this.options.showTimeout); } /** * Hide cursor. */; _proto.hide = function hide() { var _this3 = this; this.trigger("hide"); clearInterval(this.visibleInt); this.el.classList.add(this.options.hiddenState); this.visibleInt = setTimeout(function () { return _this3.visible = false; }, this.options.hideTimeout); } /** * Toggle cursor. * * @param {boolean} [force] Force state. */; _proto.toggle = function toggle(force) { if (force === true || force !== false && !this.visible) { this.show(); } else { this.hide(); } } /** * Add state/states to the cursor. * * @param {string} state State name. */; _proto.addState = function addState(state) { var _this$el$classList; this.trigger("addState", state); if (state === this.options.hiddenState) return this.hide(); (_this$el$classList = this.el.classList).add.apply(_this$el$classList, state.split(" ")); if (this.options.visibleOnState) this.show(); } /** * Remove state/states from cursor. * * @param {string} state State name. */; _proto.removeState = function removeState(state) { var _this$el$classList2; this.trigger("removeState", state); if (state === this.options.hiddenState) return this.show(); (_this$el$classList2 = this.el.classList).remove.apply(_this$el$classList2, state.split(" ")); if (this.options.visibleOnState && this.el.className === this.options.className) this.hide(); } /** * Toggle cursor state. * * @param {string} state State name. * @param {boolean} [force] Force state. */; _proto.toggleState = function toggleState(state, force) { if (force === true || force !== false && !this.el.classList.contains(state)) { this.addState(state); } else { this.removeState(state); } } /** * Set factor of skewing effect. * * @param {number} value Skewing factor. */; _proto.setSkewing = function setSkewing(value) { this.gsap.to(this, { skewing: value }); } /** * Reverts skewing factor to default. */; _proto.removeSkewing = function removeSkewing() { this.gsap.to(this, { skewing: this.options.skewing }); } /** * Stick cursor to the element. * * @param {string|HTMLElement} element Element or selector. */; _proto.setStick = function setStick(element) { var el = typeof element === "string" ? document.querySelector(element) : element; var rect = el.getBoundingClientRect(); this.stick = { y: rect.top + rect.height / 2, x: rect.left + rect.width / 2 }; } /** * Unstick cursor from the element. */; _proto.removeStick = function removeStick() { this.stick = false; } /** * Transform cursor to text mode with a given string. * * @param {string} text Text. */; _proto.setText = function setText(text) { this.text.innerHTML = text; this.addState(this.options.textState); this.setSkewing(this.options.skewingText); } /** * Reverts cursor from text mode. */; _proto.removeText = function removeText() { this.removeState(this.options.textState); this.removeSkewing(); } /** * Transform cursor to svg icon mode. * * @param {string} name Icon identifier. * @param {string} [style=""] Additional SVG styles. */; _proto.setIcon = function setIcon(name, style) { if (style === void 0) { style = ""; } this.text.innerHTML = "<svg class='" + this.options.iconSvgClassName + " " + this.options.iconSvgNamePrefix + name + "'" + (" style='" + style + "'><use xlink:href='" + this.options.iconSvgSrc + "#" + name + "'></use></svg>"); this.addState(this.options.iconState); this.setSkewing(this.options.skewingIcon); } /** * Reverts cursor from icon mode. */; _proto.removeIcon = function removeIcon() { this.removeState(this.options.iconState); this.removeSkewing(); } /** * Transform cursor to media mode with a given element. * * @param {HTMLElement} element Element. */; _proto.setMedia = function setMedia(element) { var _this4 = this; clearTimeout(this.mediaInt); if (element) { this.mediaBox.innerHTML = ""; this.mediaBox.appendChild(element); } this.mediaInt = setTimeout(function () { return _this4.addState(_this4.options.mediaState); }, 20); this.setSkewing(this.options.skewingMedia); } /** * Revert cursor from media mode. */; _proto.removeMedia = function removeMedia() { var _this5 = this; clearTimeout(this.mediaInt); this.removeState(this.options.mediaState); this.mediaInt = setTimeout(function () { return _this5.mediaBox.innerHTML = ""; }, this.options.hideMediaTimeout); this.removeSkewing(); } /** * Transform cursor to image mode. * * @param {string} url Image url. */; _proto.setImg = function setImg(url) { if (!this.mediaImg) this.mediaImg = new Image(); if (this.mediaImg.src !== url) this.mediaImg.src = url; this.setMedia(this.mediaImg); } /** * Reverts cursor from image mode. */; _proto.removeImg = function removeImg() { this.removeMedia(); } /** * Transform cursor to video mode. * * @param {string} url Video url. */; _proto.setVideo = function setVideo(url) { if (!this.mediaVideo) { this.mediaVideo = document.createElement("video"); this.mediaVideo.muted = true; this.mediaVideo.loop = true; this.mediaVideo.autoplay = true; } if (this.mediaVideo.src !== url) { this.mediaVideo.src = url; this.mediaVideo.load(); } this.mediaVideo.play(); this.setMedia(this.mediaVideo); } /** * Reverts cursor from video mode. */; _proto.removeVideo = function removeVideo() { if (this.mediaVideo && this.mediaVideo.readyState > 2) this.mediaVideo.pause(); this.removeMedia(); } /** * Attach an event handler function. * * @param {string} event Event name. * @param {function} callback Callback. */; _proto.on = function on(event, callback) { if (!(this.events[event] instanceof Array)) this.off(event); this.events[event].push(callback); } /** * Remove an event handler. * * @param {string} event Event name. * @param {function} [callback] Callback. */; _proto.off = function off(event, callback) { if (callback) { this.events[event] = this.events[event].filter(function (f) { return f !== callback; }); } else { this.events[event] = []; } } /** * Execute all handlers for the given event type. * * @param {string} event Event name. * @param params Extra parameters. */; _proto.trigger = function trigger(event) { var _arguments = arguments, _this6 = this; if (!this.events[event]) return; this.events[event].forEach(function (f) { return f.call.apply(f, [_this6, _this6].concat([].slice.call(_arguments, 1))); }); } /** * Get cursor options from data attribute of a given element. * * @param {HTMLElement} element Element. * @return {Object} Options. */; _proto.getFromDataset = function getFromDataset(element) { var dataset = element.dataset; return { state: dataset[this.options.dataAttr], show: dataset[this.options.dataAttr + "Show"], text: dataset[this.options.dataAttr + "Text"], icon: dataset[this.options.dataAttr + "Icon"], img: dataset[this.options.dataAttr + "Img"], video: dataset[this.options.dataAttr + "Video"], stick: dataset[this.options.dataAttr + "Stick"] }; } /** * Destroy cursor instance. */; _proto.destroy = function destroy() { this.trigger("destroy"); this.gsap.ticker.remove(this.ticker); this.container.removeEventListener("mouseleave", this.event.mouseleave); this.container.removeEventListener("mouseenter", this.event.mouseenter); this.container.removeEventListener("mousedown", this.event.mousedown); this.container.removeEventListener("mouseup", this.event.mouseup); this.container.removeEventListener("mousemove", this.event.mousemove); this.container.removeEventListener("mousemove", this.event.mousemoveOnce); this.container.removeEventListener("mouseover", this.event.mouseover); this.container.removeEventListener("mouseout", this.event.mouseout); if (this.el) { this.container.removeChild(this.el); this.el = null; this.mediaImg = null; this.mediaVideo = null; } }; return MouseFollower; }(); export { MouseFollower as default };