├── .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 | [](https://github.com/ArtBIT/gravity-cursor) [](https://github.com/ArtBIT/gravity-cursor) [](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 | [](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 |
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 |
--------------------------------------------------------------------------------