├── .gitignore ├── .jshintrc ├── LICENSE.md ├── README.md ├── demos ├── assets │ ├── castle-brick.png │ ├── debugcanvas.js │ ├── demo.gif │ ├── ghost.png │ ├── github.png │ ├── mark-github-black-128.png │ ├── mark-github-white-128.png │ ├── plant.png │ ├── shroom.png │ ├── style.css │ └── toad.png └── index.html ├── dist └── index.js ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "asi": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2017` `Djordje Ungar` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub stars](https://img.shields.io/github/stars/ArtBIT/gravity-cursor.svg)](https://github.com/ArtBIT/gravity-cursor) [![GitHub license](https://img.shields.io/github/license/ArtBIT/gravity-cursor.svg)](https://github.com/ArtBIT/gravity-cursor) [![GitHub issues](https://img.shields.io/github/issues/ArtBIT/gravity-cursor.svg)](https://github.com/ArtBIT/gravity-cursor/issues) 2 | 3 | # Cursor Gravity 4 | 5 | This is a small experiment that hijacks the user cursor and makes it attract to or repel from certain elements on the page. 6 | 7 | Try the live demo [here](https://artbit.github.io/gravity-cursor/demos/). 8 | 9 | [![gravity-cursor](demos/assets/demo.gif)](http://github.com/artbit/gravity-cursor/) 10 | 11 | ## How it works? 12 | It makes the user cursor invisible using a simple `cursor: none;` CSS rule, and replaces it with a simple image element, which is moved around the screen to imitate original cursor, but making it react to attractors and deflectors on the page. 13 | 14 | ## Usage 15 | ```js 16 | GravityCursor 17 | .attract(document.querySelector('a.attractor')) 18 | .repel(document.querySelector('a.deflector')) 19 | .start(); 20 | 21 | document.querySelector('a.stop').addEventListener('click', function() { 22 | GravityCursor.stop(); 23 | }); 24 | ``` 25 | 26 | This will replace the real cursor with the fake one and activate the 'repel' and 'attract' behavior on the selected DOM elements. 27 | 28 | ## Local Build 29 | ``` 30 | git clone https://github.com/ArtBIT/gravity-cursor.git 31 | cd gravity-cursor 32 | npm install 33 | npm start 34 | ``` 35 | 36 | ## License 37 | 38 | MIT 39 | 40 | 41 | ## Credits 42 | 43 | Inspired by [javierbyte/control-user-cursor](https://github.com/javierbyte/control-user-cursor) 44 | -------------------------------------------------------------------------------- /demos/assets/castle-brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/castle-brick.png -------------------------------------------------------------------------------- /demos/assets/debugcanvas.js: -------------------------------------------------------------------------------- 1 | window.DebugCanvas = (function() { 2 | var canvas = document.createElement('canvas'); 3 | canvas.style.position = 'fixed'; 4 | canvas.style.pointerEvents = 'none'; 5 | canvas.width = canvas.style.width = window.innerWidth; 6 | canvas.height = canvas.style.height = window.innerHeight 7 | document.body.appendChild(canvas); 8 | var ctx = canvas.getContext('2d'); 9 | var applyContextOptions = function(options) { 10 | options = options || {}; 11 | ctx.lineWidth = options.lineWidth || 1; 12 | ctx.strokeStyle = options.strokeStyle || '#F00'; 13 | if (options.lineDash) { 14 | ctx.setLineDash(options.lineDash); 15 | } 16 | if (options.opacity) { 17 | ctx.globalAlpha = options.opacity; 18 | } 19 | }; 20 | var api = { 21 | show: function() { 22 | canvas.style.display = 'block'; 23 | }, 24 | hide: function() { 25 | canvas.style.display = 'none'; 26 | }, 27 | clear: function() { 28 | ctx.clearRect(0, 0, canvas.width, canvas.height); 29 | }, 30 | draw: { 31 | circle: function(x, y, radius, options) { 32 | options = options || {}; 33 | ctx.beginPath(); 34 | ctx.arc(x, y, radius, 0, 2 * Math.PI, false); 35 | ctx.closePath(); 36 | api.draw.stroke(options); 37 | }, 38 | rect: function(x, y, width, height, options) { 39 | ctx.beginPath(); 40 | ctx.rect(x, y, width, height); 41 | ctx.closePath(); 42 | api.draw.stroke(options); 43 | }, 44 | line: function(x, y, x1, y1, options) { 45 | ctx.beginPath(); 46 | ctx.moveTo(x, y); 47 | ctx.lineTo(x1, y1); 48 | ctx.closePath(); 49 | api.draw.stroke(options); 50 | }, 51 | moveTo: function(x, y) { 52 | ctx.moveTo(x, y); 53 | }, 54 | stroke: function(options) { 55 | ctx.save(); 56 | applyContextOptions(options); 57 | ctx.stroke(); 58 | ctx.restore(); 59 | }, 60 | image: function(img, x, y, w, h, dx, dy, dw, dh, options) { 61 | ctx.save(); 62 | switch (arguments.length) { 63 | case 2: 64 | applyContextOptions(x); 65 | ctx.drawImage(img, 0, 0); 66 | break; 67 | case 1: 68 | ctx.drawImage(img, 0, 0); 69 | break; 70 | case 4: 71 | case 3: 72 | applyContextOptions(w); 73 | ctx.drawImage(img, x, y); 74 | break; 75 | case 6: 76 | case 5: 77 | applyContextOptions(dx); 78 | ctx.drawImage(img, x, y, w, h); 79 | break; 80 | default: 81 | applyContextOptions(options); 82 | ctx.drawImage(img, x, y, w, h, dx, dy, dw, dh); 83 | } 84 | ctx.restore(); 85 | } 86 | } 87 | }; 88 | api.clear(); 89 | return api; 90 | })(); 91 | -------------------------------------------------------------------------------- /demos/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/demo.gif -------------------------------------------------------------------------------- /demos/assets/ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/ghost.png -------------------------------------------------------------------------------- /demos/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/github.png -------------------------------------------------------------------------------- /demos/assets/mark-github-black-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/mark-github-black-128.png -------------------------------------------------------------------------------- /demos/assets/mark-github-white-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/mark-github-white-128.png -------------------------------------------------------------------------------- /demos/assets/plant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/plant.png -------------------------------------------------------------------------------- /demos/assets/shroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/shroom.png -------------------------------------------------------------------------------- /demos/assets/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | min-height: 100vh; 10 | height: auto; 11 | width: 100vw; 12 | font-family: Helvetica, Arial, sans-serif; 13 | font-size: 15px; 14 | margin: 0; 15 | padding: 0; 16 | color: #ddd; 17 | background-color: #111; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | overflow: hidden; 21 | } 22 | 23 | a { 24 | text-decoration: none; 25 | border-color: transparent; 26 | color: #ddd; 27 | outline: none; 28 | padding: 0 0 0.15em; 29 | } 30 | 31 | a.active, 32 | a:hover, 33 | a:focus { 34 | color: #000; 35 | } 36 | 37 | .hidden { 38 | position: absolute; 39 | overflow: hidden; 40 | width: 0; 41 | height: 0; 42 | pointer-events: none; 43 | } 44 | 45 | header { 46 | padding: 1em; 47 | position: absolute; 48 | width: 100%; 49 | } 50 | 51 | header .row { 52 | display: flex; 53 | flex-direction: row; 54 | flex-wrap: wrap; 55 | align-items: center; 56 | width: 100%; 57 | } 58 | 59 | header .title { 60 | font-size: 1.85em; 61 | font-weight: normal; 62 | margin: 0; 63 | padding: 0; 64 | } 65 | 66 | header .github-link { 67 | height: 1em; 68 | width: 1em; 69 | display: inline-block; 70 | background-image: url(mark-github-white-128.png); 71 | background-repeat: no-repeat; 72 | background-size: cover; 73 | text-decoration: none; 74 | border: none; 75 | position: relative; 76 | top: 5px; 77 | padding: 2px; 78 | } 79 | 80 | header a { 81 | border-bottom: 2px solid; 82 | } 83 | 84 | header .tagline { 85 | margin: 1em 0 0.5em; 86 | width: 100%; 87 | } 88 | 89 | header .description { 90 | margin: 0 0 1em 0; 91 | font-weight: bold; 92 | width: 100%; 93 | } 94 | 95 | .demos { 96 | margin: 0 0 0 auto; 97 | } 98 | 99 | .demo { 100 | display: inline-block; 101 | margin: 0 1em 0.5em 0; 102 | padding: 0 0 0.25em; 103 | } 104 | 105 | .not-clickable { 106 | pointer-events: none; 107 | cursor: normal; 108 | } 109 | 110 | @media screen and (max-width: 60em) { 111 | .header { 112 | flex-direction: column; 113 | align-items: flex-start; 114 | font-size: 0.85em; 115 | } 116 | .demos { 117 | width: 100%; 118 | margin: 1em 0 0; 119 | } 120 | } 121 | 122 | /* THEME */ 123 | 124 | /* COLOR */ 125 | body, 126 | .demos, 127 | a, a:visited { 128 | color: #DDD; 129 | } 130 | 131 | /* HOVER COLOR */ 132 | a.active, 133 | a:hover, 134 | a:focus { 135 | color: #fff; 136 | } 137 | 138 | /* BACKGROUND */ 139 | -------------------------------------------------------------------------------- /demos/assets/toad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBIT/gravity-cursor/8ba0a2236fee5f5ff37ae81936e41ef4e72e9386/demos/assets/toad.png -------------------------------------------------------------------------------- /demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Cursor Gravity Demo 12 | 13 | 14 | 15 | 16 | 17 | 68 | 69 | 70 |
71 |

72 | 73 | Cursor Gravity Demo 74 |

75 |

76 | Hijack the mouse cursor and make react to attractor and deflector elements on the page. 77 |

78 | 79 |
80 |
81 | 82 | 83 | 352 | 353 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | window.GravityCursor = function () { 2 | const VIRTUAL_CURSOR_CLASSNAME = 'virtual-cursor'; 3 | const VIRTUAL_CURSOR_ZINDEX = 10000; 4 | let showDebugInfo = false; 5 | let debugLevel = 0; 6 | 7 | let cursor; 8 | let forces = []; 9 | let body = document.getElementsByTagName('body')[0]; 10 | let html = document.getElementsByTagName('html')[0]; 11 | 12 | const CursorAssets = { 13 | mac_retina: { 14 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAsCAMAAABFXPg2AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB7FBMVEUAAAAAAAAAAAD9/f0AAAAAAAD5+fkAAAAAAAD29vYAAAAAAAAAAAD09PQAAAAAAAAAAAAAAAAAAAAAAAAAAAD09PQAAADx8fHz8/Pw8PDv7+/////q6urv7+/p6emZmZmbm5udnZ2goKClpaWurq68vLzMzMze3t79/f1DQ0MAAAAAAAAAAAAAAAAAAAAAAAAAAAD9/f3S0tIAAAAAAAAAAAAAAAAAAAAAAAD6+vqdnZ3+/v74+PgJCQkAAAAAAAD6+vqAgIAAAADn5+fLy8v4+Ph0dHQAAACkpKT09PT6+vp3d3cGBgb29vb///+mpqb4+Ph+fn4AAAAAAAAAAADPz8/t7e36+vp5eXkAAABJSUn9/f38/Pw5OTmSkpIAAAAAAADk5OTc3NySkpL19fXw8PD29vbS0tJVVVXBwcH////p6emjo6MGBgYcHBz29vbd3d1tbW0AAAAAAAD///94eHj+/v4PDw9oaGgAAABubm53d3eBgYGHh4dlZWXf398UFBROTk4RERH29vbS0tLOzs6dnZ0bGxs4ODgxMTHb29ujo6PHx8cvLy/4+PgVFRVdXV3q6uoGBgbg4OACAgKGhoZPT08eHh78/Py6urqwsLAlJSVGRkaPj48EBARYWFjv7+8jIyN+fn7m5ub2NuO1AAAAdHRSTlMAAQKJAwWLBAmNBwgOjgoSCxYGDBeIGH9+dHD9Y2HHa2loZmNeV1BG+EEvLCooJB4N94gfHBsZFRDwcv3fHBQP7Fo3wVTqTy5otelNKt/8KONJJh0hhorgOyIx9egSMRoRqF5LtsvdfiFv/sJpLCTbo00pI5smug4AAAABYktHRACIBR1IAAAACXBIWXMAABYlAAAWJQFJUiTwAAACEklEQVQ4y33U92OTQBQH8ByCh4QSArGOOurWaGO0DcGVqHVvYxxx7739NoqzarVaZ511r3/UIwWEAL4f+OVzD97d414ikSCEJGKDEG4Ex8UuILyAkZSPc04QgVFSnPNJGUBLnFMlhf84VdNATx0tWqRTPQNcv1HH6Eineitji/mYKHc4zl2OcY+tm8DYkP9j61aE+zjK/Wz7OC3QnwDbPj7gQQ55Ezd7M1u3Ax5i647fwxzwCGbeNsHxKPZ5kO/eGw647nHvffZ48BButE2kPu7te2SnP8akye1Tpk6bPmPmLDvd4f4+4Anjp5g9Jzt3Xkdufjpv/53D3D+ABXhmpz/HwlxnV8EomqqX/WIAixYvefmK8WssXVYyy7KuSJRzvj2I5StWdqOH8ZtBrJJFTRMo71WO1Wuya9fh7Tvm77E+L3DEubcN3rCxI5fbtBkfGH8c2rJV4vzXYNv2yo5Wo7oTn+ziPmOXQr2OqaXde2qFlGwae/fVvzD+iv0HNJd50Tx4KHNYkZRU5xF8s6zvP4ZwVORdllTTlEXKa3njGH7++v2HVXpcdd/OUU0U2RYJr5SqJxqnffLUaV1wmHA8ZVtk67Ry4Qxw9tz5C9Wi6rI9mBpbJFQtXrx0+Ur2as0wk+HLyEllo1apXOtKm4oQnlWEKmbGYL3Qk0LUJGNDSpfzrBV85CAkHBVYK2JnIHHL9OIv2U7sgZRDHOgAAAAASUVORK5CYII=', 15 | height: 22, 16 | width: 15 17 | }, 18 | mac: { 19 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAVCAMAAABBhy+7AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAA+VBMVEUAAAAAAAD7+/sAAAD09PQAAAAAAAD09PTz8/PS0tJqampubm5+fn6np6cAAAD19fXw8PD39/cJCQkAAAAAAAAAAAAAAAD19fWzs7OgoKDX19cAAAD+/v6ysrIAAAAICAj29vb09PQAAAAAAADFxcUAAAAAAADR0dH8/Py2trYAAAAAAAAAAABRUVH29vb39/fJycni4uLp6emjo6MHBwcAAAAAAAAAAAD////T09MUFBTS0tIAAAAXFxfb29sbGxseHh4aGhrr6+vv7+/39/c5OTni4uIKCgqurq5BQUFmZmZDQ0PQ0NDW1tY9PT1tbW2np6cgICCioqIFyGmYAAAAOHRSTlMAAooIjg4QiH6dTUpBMQT92N4bFBILA/t4aU0B+3AgIt6zBQ1lGwqE/CMGCQcp9txCpsJpJQ8iGe73bXgAAAABYktHRACIBR1IAAAACXBIWXMAAAsSAAALEgHS3X78AAAArUlEQVQY003P1xKCMBRFUaKCvSuWWFFsYG9Rr1jA3v3/jzGMwOS8rac9h0MuxDFzEw9rnixYC2S5YiwQYE0Ja82xSdhoXsSQ2ocYOrZo26Zlk1vY7XXdINSUh+MJzv5AMBSOUF6isSvc4olkKi1SZrK5O8AjLyKEOaFQLJUrT3hVJTNUq8sNofmGT6uNKTu8gtVu72v0B2YXi5hDqeFoPJk6txR5NuclbBOpyh8/ou0enbQh1QcAAAAASUVORK5CYII=', 20 | width: 15, 21 | height: 22 22 | }, 23 | other: { 24 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAXCAYAAADtNKTnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAjnRFWHRDb21tZW50AGNIUk0gY2h1bmtsZW4gMzIgaWdub3JlZA1BU0NJSTogLi56JS4uLi4uLi4lLi4uLi4ubV8uLi5sLi48Li4uLlgNSEVYOiAwMDAwN0EyNTAwMDA4MDgzMDAwMEY0MjUwMDAwODREMTAwMDA2RDVGMDAwMEU4NkMwMDAwM0M4QjAwMDAxQjU4GN773QAAAgFJREFUeJyUlEtLAlEUx0cd7YFagUkvxKIgiB4USEREUNAqWgvtatEnaNVnaVWrNrVoEURFT6JCxBDDIpKScGG5EATR2/9MZ6ZrjjJe+MHMmXt+c+65c0cRQihEuVy2lUolO0HXetwKil31valN3SFFUVygFTQDJ7CBhiSHYA45ncAHvI2INMlH+lNAcgRWkDMI+kCbVZEmwYVg0SlYRc7of5EliSS6AGtIHLMqqpBIoiuwblVUJZFE12Yis+03leBbEanUu2VRlYQExWJRFAoF8fzyWlMkL61CIgvy+bzI5XIiHk/UFdH2GxIzQTabFZlMRkQiUTPR3wdJknqCdDqN/qTE7e2dwNwrJM2AEdDFx8SuSWTB01PSEODZjcQ1uGQJVRMAHuDQJLLA1ezL7ezuaRVw4pbb27OEyfNgGkyAIdAN3IZEEnyDDcQepSXEMHEBzIJxEOSD6uGT/7scFnyBMIJziJ3s7x+KZDJJkmOwyRJaRi8LnJqAd+cIyVmwiNthMIVYGEQTiYQ4OzsnUZTk9KyiF3y69aHy3tPWDThcPX1IPCBBLBYjyT1XEuJ+dHBOxaCymvgN7WikG4kB8AC2wTLik9zUfn5hlUQXqQyValNdfjugb8HPDQ3ydavRD31N8rpMBklb+O1eFlDM+OOZ/2RqV+mQK9Dn/wAAAP//AwCGMlcrSCX+UAAAAABJRU5ErkJggg==', 25 | width: 17, 26 | height: 23 27 | } 28 | }; 29 | 30 | function onNextFrame(callback) { 31 | window.requestAnimationFrame(() => { 32 | return callback(); 33 | }); 34 | } 35 | 36 | function VirtualCursor() { 37 | let type = window.navigator.platform.indexOf('Mac') > -1 ? 'mac' : 'win'; 38 | if (window.devicePixelRatio > 1) { 39 | type += '_retina'; 40 | } 41 | let config = CursorAssets[type] || CursorAssets.other; 42 | let node = document.createElement('img'); 43 | 44 | node.style.width = config.width + 'px'; 45 | node.style.height = config.height + 'px'; 46 | node.className = VIRTUAL_CURSOR_CLASSNAME; 47 | node.src = config.src; 48 | body.appendChild(node); 49 | 50 | this.image = node; 51 | var visible = false; 52 | 53 | this.show = () => { 54 | visible = true; 55 | }; 56 | 57 | this.hide = () => { 58 | visible = false; 59 | node.style.visibility = "hidden"; 60 | }; 61 | 62 | this.activate = () => { 63 | html.classList.add(VIRTUAL_CURSOR_CLASSNAME); 64 | }; 65 | 66 | this.deactivate = () => { 67 | onNextFrame(() => { 68 | html.classList.remove(VIRTUAL_CURSOR_CLASSNAME); 69 | }); 70 | }; 71 | 72 | this.moveTo = (x, y) => { 73 | if (visible && node.style.visibility == "hidden") { 74 | node.style.visibility = "visible"; 75 | } 76 | node.style.transform = 'translate(' + x + 'px, ' + y + 'px)'; 77 | }; 78 | 79 | this.isVisible = () => { 80 | return node.style.display !== "none"; 81 | }; 82 | } 83 | 84 | function constrain(value, min, max) { 85 | if (min !== undefined) { 86 | if (value < min) { 87 | return min; 88 | } 89 | } 90 | if (max !== undefined) { 91 | if (value > max) { 92 | return max; 93 | } 94 | } 95 | return value; 96 | } 97 | 98 | function dispatchEvent(element, eventType, detail) { 99 | var event = new CustomEvent(eventType, { detail: detail }); 100 | element.dispatchEvent(event); 101 | } 102 | 103 | function calculateForces(position, forces) { 104 | let force = { 105 | x: 0, 106 | y: 0 107 | }; 108 | if (showDebugInfo) DebugCanvas.clear(); 109 | 110 | forces.forEach(obj => { 111 | // Calculating object rectangle is more expensive, but simpler since we do not have to keep track 112 | // of object's position and window resizing. 113 | 114 | const rect = obj.node.getBoundingClientRect(); 115 | const rx = rect.left + rect.width / 2; 116 | const ry = rect.top + rect.height / 2; 117 | const dx = position.x - rx; 118 | const dy = position.y - ry; 119 | const d = Math.sqrt(dx * dx + dy * dy); 120 | 121 | const minRadius = obj.radius; 122 | const maxRadius = obj.radius << 1; 123 | let fx, fy, strength; 124 | 125 | if (d <= maxRadius) { 126 | var radius = constrain(d, minRadius) - minRadius; 127 | var angle = Math.atan2(dy, dx); 128 | if (obj.direction == 1) { 129 | // REPEL 130 | strength = Math.pow(radius / minRadius, 2); 131 | radius *= strength; 132 | radius += minRadius - d; 133 | force.x += fx = Math.cos(angle) * radius; 134 | force.y += fy = Math.sin(angle) * radius; 135 | if (showDebugInfo) { 136 | if (debugLevel == 1) DebugCanvas.draw.line(rx, ry, rx + fx, ry + fy, { strokeStyle: '#F00', lineWidth: 3 }); 137 | DebugCanvas.draw.circle(rx, ry, minRadius, { strokeStyle: '#F00', lineWidth: 1, lineDash: [2, 2] }); 138 | } 139 | dispatchEvent(obj.node, 'repel', { strength: strength, distance: d }); 140 | } else if (obj.direction == -1) { 141 | // ATTRACT 142 | strength = Math.pow(radius / minRadius, 2); 143 | radius = strength * d; 144 | force.x += fx = Math.cos(angle) * (radius - d); 145 | force.y += fy = Math.sin(angle) * (radius - d); 146 | if (showDebugInfo) { 147 | if (debugLevel == 1) DebugCanvas.draw.line(rx, ry, rx + fx, ry + fy, { strokeStyle: '#0F0', lineWidth: 3 }); 148 | DebugCanvas.draw.circle(rx, ry, minRadius, { strokeStyle: '#0F0', lineWidth: 1, lineDash: [2, 2] }); 149 | } 150 | dispatchEvent(obj.node, 'attract', { strength: 1 - strength, distance: d }); 151 | } 152 | } 153 | }); 154 | if (showDebugInfo) DebugCanvas.draw.image(cursor.image, position.x, position.y, { opacity: 0.5 }); 155 | return force; 156 | } 157 | 158 | function createCursorStyles() { 159 | var style = document.createElement('style'); 160 | style.type = 'text/css'; 161 | style.innerHTML = ` 162 | .${VIRTUAL_CURSOR_CLASSNAME}, 163 | .${VIRTUAL_CURSOR_CLASSNAME} * { 164 | cursor: none; 165 | -moz-user-select: none; 166 | user-select: none; 167 | -webkit-user-select: none; 168 | } 169 | img.${VIRTUAL_CURSOR_CLASSNAME} { 170 | position: fixed; 171 | display: none; 172 | pointer-events: none; 173 | z-index: ${VIRTUAL_CURSOR_ZINDEX}; 174 | } 175 | .${VIRTUAL_CURSOR_CLASSNAME} img.${VIRTUAL_CURSOR_CLASSNAME} { 176 | display: inline-block; 177 | } 178 | `; 179 | body.appendChild(style); 180 | } 181 | 182 | function bindEvents() { 183 | 184 | function onMouseOut(e) { 185 | // If relatedTarget is null, that means the mouse has left the page 186 | if (!e.relatedTarget) { 187 | cursor.deactivate(); 188 | } 189 | } 190 | 191 | function onBlur() { 192 | onNextFrame(() => { 193 | cursor.deactivate(); 194 | }); 195 | } 196 | 197 | function onFocus() { 198 | if (!forces.length) { 199 | return; 200 | } 201 | onNextFrame(() => { 202 | cursor.activate(); 203 | }); 204 | } 205 | 206 | function onMouseMove(evt) { 207 | if (!forces.length) { 208 | return; 209 | } 210 | cursor.activate(); 211 | let mouse = { 212 | x: evt.clientX, 213 | y: evt.clientY 214 | }; 215 | 216 | let force = calculateForces(mouse, forces); 217 | cursor.moveTo(mouse.x + force.x, mouse.y + force.y); 218 | } 219 | 220 | document.addEventListener('mouseout', onMouseOut); 221 | document.addEventListener('mousemove', onMouseMove); 222 | window.addEventListener('focus', onFocus); 223 | window.addEventListener('blur', onBlur); 224 | } 225 | 226 | function addForceElement(element, radius, direction) { 227 | forces.push({ 228 | node: element, 229 | radius: radius || 100, 230 | direction: direction 231 | }); 232 | } 233 | 234 | function attract(element, radius) { 235 | addForceElement(element, radius, -1); 236 | return this; 237 | } 238 | 239 | function repel(element, radius) { 240 | addForceElement(element, radius, 1); 241 | return this; 242 | } 243 | 244 | function start() { 245 | show(); 246 | return this; 247 | } 248 | 249 | function stop(element) { 250 | if (element) { 251 | forces = forces.filter(item => item.node !== element); 252 | } else { 253 | forces = []; 254 | } 255 | if (!forces.length) { 256 | cursor.hide(); 257 | cursor.deactivate(); 258 | } 259 | return this; 260 | } 261 | 262 | function debug(enable, level) { 263 | showDebugInfo = enable; 264 | debugLevel = level || 0; 265 | if (DebugCanvas) DebugCanvas.clear(); 266 | return this; 267 | } 268 | 269 | function show() { 270 | if (forces.length) { 271 | cursor.show(); 272 | cursor.activate(); 273 | } 274 | return this; 275 | } 276 | 277 | function hide() { 278 | cursor.hide(); 279 | return this; 280 | } 281 | 282 | function init() { 283 | forces = []; 284 | cursor = new VirtualCursor(); 285 | 286 | createCursorStyles(); 287 | bindEvents(); 288 | 289 | const controller = {}; 290 | controller.debug = debug; 291 | controller.repel = repel.bind(controller); 292 | controller.attract = attract.bind(controller); 293 | controller.stop = stop.bind(controller); 294 | controller.start = start.bind(controller); 295 | controller.show = show.bind(controller); 296 | controller.hide = hide.bind(controller); 297 | return controller; 298 | } 299 | 300 | return init(); 301 | }(); 302 | 303 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gravity-cursor", 3 | "version": "1.0.0", 4 | "description": "Hijack user cursor and make it react to attractive/deflective forces on the page.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "babel src/index.js > dist/index.js", 8 | "serve": "http-server -p 9090 .", 9 | "demo": "open http://localhost:9090/demos/", 10 | "start": "npm run build && npm run demo && npm run serve -s" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ArtBIT/gravity-cursor.git" 15 | }, 16 | "keywords": [ 17 | "mouse", 18 | "cursor", 19 | "hijack", 20 | "repel", 21 | "attract", 22 | "deflect" 23 | ], 24 | "author": "Djordje Ungar", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/ArtBIT/gravity-cursor/issues" 28 | }, 29 | "homepage": "https://github.com/ArtBIT/gravity-cursor#readme", 30 | "devDependencies": { 31 | "babel-cli": "^6.24.1", 32 | "http-server": "^0.9.0", 33 | "open": "0.0.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | window.GravityCursor = (function() { 2 | const VIRTUAL_CURSOR_CLASSNAME = 'virtual-cursor'; 3 | const VIRTUAL_CURSOR_ZINDEX = 10000; 4 | let showDebugInfo = false; 5 | let debugLevel = 0; 6 | 7 | let cursor; 8 | let forces = []; 9 | let body = document.getElementsByTagName('body')[0]; 10 | let html = document.getElementsByTagName('html')[0]; 11 | 12 | const CursorAssets = { 13 | mac_retina: { 14 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAsCAMAAABFXPg2AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB7FBMVEUAAAAAAAAAAAD9/f0AAAAAAAD5+fkAAAAAAAD29vYAAAAAAAAAAAD09PQAAAAAAAAAAAAAAAAAAAAAAAAAAAD09PQAAADx8fHz8/Pw8PDv7+/////q6urv7+/p6emZmZmbm5udnZ2goKClpaWurq68vLzMzMze3t79/f1DQ0MAAAAAAAAAAAAAAAAAAAAAAAAAAAD9/f3S0tIAAAAAAAAAAAAAAAAAAAAAAAD6+vqdnZ3+/v74+PgJCQkAAAAAAAD6+vqAgIAAAADn5+fLy8v4+Ph0dHQAAACkpKT09PT6+vp3d3cGBgb29vb///+mpqb4+Ph+fn4AAAAAAAAAAADPz8/t7e36+vp5eXkAAABJSUn9/f38/Pw5OTmSkpIAAAAAAADk5OTc3NySkpL19fXw8PD29vbS0tJVVVXBwcH////p6emjo6MGBgYcHBz29vbd3d1tbW0AAAAAAAD///94eHj+/v4PDw9oaGgAAABubm53d3eBgYGHh4dlZWXf398UFBROTk4RERH29vbS0tLOzs6dnZ0bGxs4ODgxMTHb29ujo6PHx8cvLy/4+PgVFRVdXV3q6uoGBgbg4OACAgKGhoZPT08eHh78/Py6urqwsLAlJSVGRkaPj48EBARYWFjv7+8jIyN+fn7m5ub2NuO1AAAAdHRSTlMAAQKJAwWLBAmNBwgOjgoSCxYGDBeIGH9+dHD9Y2HHa2loZmNeV1BG+EEvLCooJB4N94gfHBsZFRDwcv3fHBQP7Fo3wVTqTy5otelNKt/8KONJJh0hhorgOyIx9egSMRoRqF5LtsvdfiFv/sJpLCTbo00pI5smug4AAAABYktHRACIBR1IAAAACXBIWXMAABYlAAAWJQFJUiTwAAACEklEQVQ4y33U92OTQBQH8ByCh4QSArGOOurWaGO0DcGVqHVvYxxx7739NoqzarVaZ511r3/UIwWEAL4f+OVzD97d414ikSCEJGKDEG4Ex8UuILyAkZSPc04QgVFSnPNJGUBLnFMlhf84VdNATx0tWqRTPQNcv1HH6Eineitji/mYKHc4zl2OcY+tm8DYkP9j61aE+zjK/Wz7OC3QnwDbPj7gQQ55Ezd7M1u3Ax5i647fwxzwCGbeNsHxKPZ5kO/eGw647nHvffZ48BButE2kPu7te2SnP8akye1Tpk6bPmPmLDvd4f4+4Anjp5g9Jzt3Xkdufjpv/53D3D+ABXhmpz/HwlxnV8EomqqX/WIAixYvefmK8WssXVYyy7KuSJRzvj2I5StWdqOH8ZtBrJJFTRMo71WO1Wuya9fh7Tvm77E+L3DEubcN3rCxI5fbtBkfGH8c2rJV4vzXYNv2yo5Wo7oTn+ziPmOXQr2OqaXde2qFlGwae/fVvzD+iv0HNJd50Tx4KHNYkZRU5xF8s6zvP4ZwVORdllTTlEXKa3njGH7++v2HVXpcdd/OUU0U2RYJr5SqJxqnffLUaV1wmHA8ZVtk67Ry4Qxw9tz5C9Wi6rI9mBpbJFQtXrx0+Ur2as0wk+HLyEllo1apXOtKm4oQnlWEKmbGYL3Qk0LUJGNDSpfzrBV85CAkHBVYK2JnIHHL9OIv2U7sgZRDHOgAAAAASUVORK5CYII=', 15 | height: 22, 16 | width: 15 17 | }, 18 | mac: { 19 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAVCAMAAABBhy+7AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAA+VBMVEUAAAAAAAD7+/sAAAD09PQAAAAAAAD09PTz8/PS0tJqampubm5+fn6np6cAAAD19fXw8PD39/cJCQkAAAAAAAAAAAAAAAD19fWzs7OgoKDX19cAAAD+/v6ysrIAAAAICAj29vb09PQAAAAAAADFxcUAAAAAAADR0dH8/Py2trYAAAAAAAAAAABRUVH29vb39/fJycni4uLp6emjo6MHBwcAAAAAAAAAAAD////T09MUFBTS0tIAAAAXFxfb29sbGxseHh4aGhrr6+vv7+/39/c5OTni4uIKCgqurq5BQUFmZmZDQ0PQ0NDW1tY9PT1tbW2np6cgICCioqIFyGmYAAAAOHRSTlMAAooIjg4QiH6dTUpBMQT92N4bFBILA/t4aU0B+3AgIt6zBQ1lGwqE/CMGCQcp9txCpsJpJQ8iGe73bXgAAAABYktHRACIBR1IAAAACXBIWXMAAAsSAAALEgHS3X78AAAArUlEQVQY003P1xKCMBRFUaKCvSuWWFFsYG9Rr1jA3v3/jzGMwOS8rac9h0MuxDFzEw9rnixYC2S5YiwQYE0Ja82xSdhoXsSQ2ocYOrZo26Zlk1vY7XXdINSUh+MJzv5AMBSOUF6isSvc4olkKi1SZrK5O8AjLyKEOaFQLJUrT3hVJTNUq8sNofmGT6uNKTu8gtVu72v0B2YXi5hDqeFoPJk6txR5NuclbBOpyh8/ou0enbQh1QcAAAAASUVORK5CYII=', 20 | width: 15, 21 | height: 22 22 | }, 23 | other: { 24 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAXCAYAAADtNKTnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAjnRFWHRDb21tZW50AGNIUk0gY2h1bmtsZW4gMzIgaWdub3JlZA1BU0NJSTogLi56JS4uLi4uLi4lLi4uLi4ubV8uLi5sLi48Li4uLlgNSEVYOiAwMDAwN0EyNTAwMDA4MDgzMDAwMEY0MjUwMDAwODREMTAwMDA2RDVGMDAwMEU4NkMwMDAwM0M4QjAwMDAxQjU4GN773QAAAgFJREFUeJyUlEtLAlEUx0cd7YFagUkvxKIgiB4USEREUNAqWgvtatEnaNVnaVWrNrVoEURFT6JCxBDDIpKScGG5EATR2/9MZ6ZrjjJe+MHMmXt+c+65c0cRQihEuVy2lUolO0HXetwKil31valN3SFFUVygFTQDJ7CBhiSHYA45ncAHvI2INMlH+lNAcgRWkDMI+kCbVZEmwYVg0SlYRc7of5EliSS6AGtIHLMqqpBIoiuwblVUJZFE12Yis+03leBbEanUu2VRlYQExWJRFAoF8fzyWlMkL61CIgvy+bzI5XIiHk/UFdH2GxIzQTabFZlMRkQiUTPR3wdJknqCdDqN/qTE7e2dwNwrJM2AEdDFx8SuSWTB01PSEODZjcQ1uGQJVRMAHuDQJLLA1ezL7ezuaRVw4pbb27OEyfNgGkyAIdAN3IZEEnyDDcQepSXEMHEBzIJxEOSD6uGT/7scFnyBMIJziJ3s7x+KZDJJkmOwyRJaRi8LnJqAd+cIyVmwiNthMIVYGEQTiYQ4OzsnUZTk9KyiF3y69aHy3tPWDThcPX1IPCBBLBYjyT1XEuJ+dHBOxaCymvgN7WikG4kB8AC2wTLik9zUfn5hlUQXqQyValNdfjugb8HPDQ3ydavRD31N8rpMBklb+O1eFlDM+OOZ/2RqV+mQK9Dn/wAAAP//AwCGMlcrSCX+UAAAAABJRU5ErkJggg==', 25 | width: 17, 26 | height: 23 27 | } 28 | }; 29 | 30 | 31 | function onNextFrame(callback) { 32 | window.requestAnimationFrame(() => { 33 | return callback(); 34 | }); 35 | } 36 | 37 | function VirtualCursor() { 38 | let type = window.navigator.platform.indexOf('Mac') > -1 ? 'mac' : 'win'; 39 | if (window.devicePixelRatio > 1) { 40 | type += '_retina'; 41 | } 42 | let config = CursorAssets[type] || CursorAssets.other; 43 | let node = document.createElement('img'); 44 | 45 | node.style.width = config.width + 'px' 46 | node.style.height = config.height + 'px'; 47 | node.className = VIRTUAL_CURSOR_CLASSNAME; 48 | node.src = config.src; 49 | body.appendChild(node); 50 | 51 | this.image = node; 52 | var visible = false; 53 | 54 | this.show = () => { 55 | visible = true; 56 | } 57 | 58 | this.hide = () => { 59 | visible = false; 60 | node.style.visibility = "hidden"; 61 | } 62 | 63 | this.activate = () => { 64 | html.classList.add(VIRTUAL_CURSOR_CLASSNAME); 65 | } 66 | 67 | this.deactivate = () => { 68 | onNextFrame(() => { 69 | html.classList.remove(VIRTUAL_CURSOR_CLASSNAME); 70 | }); 71 | } 72 | 73 | this.moveTo = (x, y) => { 74 | if (visible && node.style.visibility == "hidden") { 75 | node.style.visibility = "visible"; 76 | } 77 | node.style.transform = 'translate(' + x + 'px, ' + y + 'px)'; 78 | } 79 | 80 | this.isVisible = () => { 81 | return node.style.display !== "none"; 82 | } 83 | } 84 | 85 | function constrain(value, min, max) { 86 | if (min !== undefined) { 87 | if (value < min) { 88 | return min; 89 | } 90 | } 91 | if (max !== undefined) { 92 | if (value > max) { 93 | return max; 94 | } 95 | } 96 | return value; 97 | } 98 | 99 | function dispatchEvent(element, eventType, detail) { 100 | var event = new CustomEvent(eventType, {detail: detail}); 101 | element.dispatchEvent(event); 102 | } 103 | 104 | function calculateForces(position, forces) { 105 | let force = { 106 | x: 0, 107 | y: 0 108 | }; 109 | if (showDebugInfo) DebugCanvas.clear(); 110 | 111 | forces.forEach(obj => { 112 | // Calculating object rectangle is more expensive, but simpler since we do not have to keep track 113 | // of object's position and window resizing. 114 | 115 | const rect = obj.node.getBoundingClientRect(); 116 | const rx = rect.left + rect.width / 2; 117 | const ry = rect.top + rect.height/2; 118 | const dx = position.x - rx; 119 | const dy = position.y - ry; 120 | const d = Math.sqrt(dx * dx + dy * dy); 121 | 122 | const minRadius = obj.radius; 123 | const maxRadius = obj.radius << 1; 124 | let fx, fy, strength; 125 | 126 | if (d <= maxRadius) { 127 | var radius = constrain(d, minRadius) - minRadius; 128 | var angle = Math.atan2(dy, dx); 129 | if (obj.direction == 1) { 130 | // REPEL 131 | strength = Math.pow(radius / minRadius, 2) 132 | radius *= strength; 133 | radius += minRadius - d; 134 | force.x += fx = Math.cos(angle) * radius; 135 | force.y += fy = Math.sin(angle) * radius; 136 | if (showDebugInfo) { 137 | if (debugLevel == 1) DebugCanvas.draw.line(rx, ry, rx + fx, ry + fy, { strokeStyle: '#F00', lineWidth: 3 }); 138 | DebugCanvas.draw.circle(rx, ry, minRadius, { strokeStyle: '#F00', lineWidth: 1, lineDash: [2, 2] }); 139 | } 140 | dispatchEvent(obj.node, 'repel', {strength: strength, distance: d}); 141 | } else if (obj.direction == -1) { 142 | // ATTRACT 143 | strength = Math.pow(radius / minRadius, 2); 144 | radius = strength * d; 145 | force.x += fx = Math.cos(angle) * (radius - d); 146 | force.y += fy = Math.sin(angle) * (radius - d); 147 | if (showDebugInfo) { 148 | if (debugLevel == 1) DebugCanvas.draw.line(rx, ry, rx + fx, ry + fy, { strokeStyle: '#0F0', lineWidth: 3 }); 149 | DebugCanvas.draw.circle(rx, ry, minRadius, { strokeStyle: '#0F0', lineWidth: 1, lineDash: [2, 2] }); 150 | } 151 | dispatchEvent(obj.node, 'attract', {strength: 1-strength, distance: d}); 152 | } 153 | } 154 | }) 155 | if (showDebugInfo) DebugCanvas.draw.image(cursor.image, position.x, position.y, {opacity: 0.5}); 156 | return force; 157 | } 158 | 159 | function createCursorStyles() { 160 | var style = document.createElement('style'); 161 | style.type = 'text/css'; 162 | style.innerHTML = ` 163 | .${VIRTUAL_CURSOR_CLASSNAME}, 164 | .${VIRTUAL_CURSOR_CLASSNAME} * { 165 | cursor: none; 166 | -moz-user-select: none; 167 | user-select: none; 168 | -webkit-user-select: none; 169 | } 170 | img.${VIRTUAL_CURSOR_CLASSNAME} { 171 | position: fixed; 172 | display: none; 173 | pointer-events: none; 174 | z-index: ${VIRTUAL_CURSOR_ZINDEX}; 175 | } 176 | .${VIRTUAL_CURSOR_CLASSNAME} img.${VIRTUAL_CURSOR_CLASSNAME} { 177 | display: inline-block; 178 | } 179 | `; 180 | body.appendChild(style); 181 | } 182 | 183 | function bindEvents() { 184 | 185 | function onMouseOut(e) { 186 | // If relatedTarget is null, that means the mouse has left the page 187 | if (!e.relatedTarget) { 188 | cursor.deactivate(); 189 | } 190 | } 191 | 192 | function onBlur() { 193 | onNextFrame(() => { 194 | cursor.deactivate(); 195 | }); 196 | } 197 | 198 | function onFocus() { 199 | if (!forces.length) { 200 | return; 201 | } 202 | onNextFrame(() => { 203 | cursor.activate(); 204 | }); 205 | } 206 | 207 | function onMouseMove(evt) { 208 | if (!forces.length) { 209 | return; 210 | } 211 | cursor.activate(); 212 | let mouse = { 213 | x: evt.clientX, 214 | y: evt.clientY 215 | } 216 | 217 | let force = calculateForces(mouse, forces); 218 | cursor.moveTo(mouse.x + force.x, mouse.y + force.y); 219 | } 220 | 221 | document.addEventListener('mouseout', onMouseOut); 222 | document.addEventListener('mousemove', onMouseMove); 223 | window.addEventListener('focus', onFocus); 224 | window.addEventListener('blur', onBlur); 225 | } 226 | 227 | function addForceElement(element, radius, direction) { 228 | forces.push({ 229 | node: element, 230 | radius: radius || 100, 231 | direction: direction, 232 | }); 233 | } 234 | 235 | function attract(element, radius) { 236 | addForceElement(element, radius, -1); 237 | return this; 238 | } 239 | 240 | function repel(element, radius) { 241 | addForceElement(element, radius, 1); 242 | return this; 243 | } 244 | 245 | function start() { 246 | show(); 247 | return this; 248 | } 249 | 250 | function stop(element) { 251 | if (element) { 252 | forces = forces.filter(item => item.node !== element); 253 | } else { 254 | forces = []; 255 | } 256 | if (!forces.length) { 257 | cursor.hide(); 258 | cursor.deactivate(); 259 | } 260 | return this; 261 | } 262 | 263 | function debug(enable, level) { 264 | showDebugInfo = enable; 265 | debugLevel = level || 0; 266 | if (DebugCanvas) DebugCanvas.clear(); 267 | return this; 268 | } 269 | 270 | function show() { 271 | if (forces.length) { 272 | cursor.show(); 273 | cursor.activate(); 274 | } 275 | return this; 276 | } 277 | 278 | function hide() { 279 | cursor.hide(); 280 | return this; 281 | } 282 | 283 | function init() { 284 | forces = []; 285 | cursor = new VirtualCursor(); 286 | 287 | createCursorStyles(); 288 | bindEvents(); 289 | 290 | const controller = {}; 291 | controller.debug = debug; 292 | controller.repel = repel.bind(controller); 293 | controller.attract = attract.bind(controller); 294 | controller.stop = stop.bind(controller); 295 | controller.start = start.bind(controller); 296 | controller.show = show.bind(controller); 297 | controller.hide = hide.bind(controller); 298 | return controller; 299 | } 300 | 301 | return init(); 302 | })(); 303 | --------------------------------------------------------------------------------