├── .gitignore ├── LICENSE ├── README.md ├── dist ├── magnet.min.js └── magnet.min.js.map ├── docs └── resources │ ├── magnetjs-example.gif │ └── magnetjs-title.gif ├── example └── html │ ├── index.html │ └── load_magnet.js ├── gulpfile.js ├── package.json └── src ├── Events.js ├── Generator.js ├── Magnet.js ├── Mixins.js ├── Socket.js ├── Theme.js ├── View.js └── component ├── Component.js ├── Letter.README.md ├── Letter.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | .idea 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Magnet JS](docs/resources/magnetjs-title.gif) 2 | 3 | # MagnetJS 4 | 5 | > An open source javascript library that allows you to create interactive fridge magnets. 6 | 7 | [![npm version](http://img.shields.io/npm/v/magnetjs.svg?style=flat)](https://npmjs.org/package/magnetjs "View this project on npm") 8 | [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) 9 | [![npm downloads](https://img.shields.io/npm/dt/magnetjs.svg)](https://npmjs.org/package/magnetjs "View this project on npm") 10 | 11 | __Live Example:__ [www.fridgeee.com/example](http://www.fridgeee.com/example) 12 | 13 | __Live Example (with dark theme):__ [www.fridgeee.com/codetheme](http://www.fridgeee.com/codetheme) 14 | 15 | ## Overview 16 | 17 | MagnetJS is an open source javascript library for creating and interacting with virtual fridge magnets. 18 | 19 | The library can be used in local mode _(local movement of magnets)_, or using the [fridgeee.com](http://www.fridgeee.com) api (anybody can move the magnets in real-time). 20 | 21 | It is designed to be modular and include other components in the future (for example post-it notes). 22 | 23 | See the [Quick Start](#quick-start) section to get started. 24 | 25 | ## Quick Start 26 | 27 | To get started using MagnetJS, include the library. 28 | 29 | ```html 30 | 31 | ``` 32 | 33 | Add a target to your website (this is where you board will be built). 34 | 35 | ```html 36 |
37 | ``` 38 | 39 | Create your board (`target:` is the 'id' of your target element) 40 | 41 | ```javascript 42 | const board = new Magnet({ target: 'root' }); 43 | ``` 44 | 45 | After the dom has loaded, mount the board. 46 | 47 | ```javascript 48 | window.onload = () => { 49 | board.mount(); 50 | }; 51 | ``` 52 | 53 | And Vualá!! 54 | 55 | ![MagnetJS Example](docs/resources/magnetjs-example.gif) 56 | 57 | ## Making changes 58 | 59 | To make changes to the library and test it out. 60 | 61 | ```console 62 | npm install 63 | ``` 64 | 65 | And run the example app: 66 | 67 | ```console 68 | npm start 69 | ``` 70 | 71 | The files that load the board are in `example/html/.`. 72 | 73 | ## Methods 74 | 75 | ### mount() 76 | 77 | Mounts the board to the target (set in config). This must only be called once the DOM has loaded. 78 | 79 | ## Config 80 | 81 | All values apart from the target are optional, however if you include a theme or item config they must be complete. 82 | 83 | ### id 84 | 85 | The `id:` value is __not__ the target element for your board. This is used to identify the board when using online mode (see [Online Mode](#Online-Mode)). The board can also be viewed by visiting www.fridgeee.com/{id here}. 86 | 87 | ### target 88 | 89 | The `target:` value mounts the board to the DOM element with that id. In the example this is `"root"`. 90 | 91 | ### width 92 | 93 | In pixels, the width of the board (not target element). 94 | 95 | ### height 96 | 97 | In pixels, the height of the board (not target element). 98 | 99 | ### items 100 | 101 | This contains the config for items on the board, they can come in one of two formats. 102 | 103 | #### items -> Array 104 | 105 | Array of items (for example, a letter item config as described [here](src/component/Letter.README.md)) 106 | 107 | #### items -> Object 108 | 109 | Items config, this is the information that is required to generate items. 110 | 111 | ##### spawn 112 | 113 | The number of components to generate for the board. 114 | 115 | ##### options 116 | 117 | An object containing options for the items. These are defined in the component config (for example [here](src/component/Letter.README.md)). 118 | 119 | ### theme 120 | 121 | An array of styles to be used as a theme for the board and its content. 122 | 123 | If the structure of this config seems a little odd it is because it must also mimic the database structure for the use of the [online-fridge.com](http://www.online-fridge.com) api. 124 | 125 | Each style is a javascript object with the following structure: 126 | 127 | #### code 128 | 129 | This value should be one of the following: 130 | 131 | * `background` - This means to set the style to the background of the board, this can be an rgb value for example. 132 | 133 | * `color` - This is to create a specific theme color that can be uniquely defined per item (see item 'color' values in the Letter component [README](src/component/Letter.README.md)) 134 | 135 | * Component name, e.g `letter` - This means a style to set for a specific item 136 | 137 | #### id 138 | 139 | This value should always be 1 unless it the style object has `code: 'color'`. If it has a 'color' code then it should increment per value and be used as a unique identifier for each color. 140 | 141 | #### key 142 | 143 | This value must be one of the following: 144 | 145 | * The key in css ['key / value pairs'](https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax) but with camelCase instead of css hyphen-case. 146 | 147 | * `rgb`, which would convert into an rgb color value. This is then handled differently depending on the `code` value of the style object. For example, the `background` code will set this value as the background. 148 | 149 | #### value 150 | 151 | This will the one of the following: 152 | 153 | * The value in css ['key / value pairs'](https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax). This value can also perform calculations on the color value of its item. To do this you should use the following syntax: 154 | 155 | ``` 156 | key: "textShadow", 157 | value : "0 10px 0 rgb({[r]-50},{[g]-50},{[b]-50})" 158 | ``` 159 | 160 | This is the same as an [es6 template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) but without the `$` and assuming that the rbg values are storage as variable with the name `r`, `g` and `b`. This allows you also use variations of the rgb color values. 161 | 162 | ### Example config 163 | 164 | ```javascript 165 | var config = { 166 | id: 'test', // ID of the board (if using sockets) 167 | target: 'root', // ID of the element to build the board onto 168 | width: 1000, // Width of the board (px) 169 | height: 1000, // Height of the board (px) 170 | items: { 171 | spawn: 100, // Spawn 100 items to the board 172 | options: { 173 | text: ['Toast','is','Food','for','my','belly'], // Text combinations to create the items with. 174 | color: [1,2,3,4,5], // The themes color unique identifiers. 175 | type: 'Letter' // The item type 176 | } 177 | }, 178 | theme: [ 179 | {code: "background", id: 1, key: "rgb", value: "50,50,50"}, // Set the board background to rgb(50,50,50) 180 | {code: "color", id: 1, key: "rgb", value: "132,159,187"}, // First theme color 181 | {code: "color", id: 2, key: "rgb", value: "179,159,161"}, // Second theme color 182 | {code: "color", id: 3, key: "rgb", value: "110,201,151"}, // Third theme color 183 | {code: "color", id: 4, key: "rgb", value: "183,101,191"}, // Fourth theme color 184 | {code: "color", id: 5, key: "rgb", value: "255,132,107"}, // Fifth theme color 185 | {code: "letter", id: 1, key: "fontFamily", value: "'Baloo Bhaina', cursive"}, // Font family for `letter` component 186 | // Text shadow for the `letter` component that uses the theme color to build a shade. 187 | { 188 | code: "letter", 189 | id: 1, 190 | key: "textShadow", 191 | value : "0 1px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 2px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 3px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 4px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 5px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 6px 6px 20px rgba(0,0,0,.1)" 192 | } 193 | ] 194 | }; 195 | ``` 196 | 197 | ## Online Mode 198 | 199 | To enable the use of the [fridgeee.com api](http://www.fridgeee.com) you must: 200 | 201 | Load socket.io from online-fridge.com 202 | 203 | ```html 204 | 205 | ``` 206 | 207 | Include the id of the board you want to access in the config. 208 | 209 | ```javascript 210 | const board = new Magnet({ 211 | target: 'root', 212 | id: 'example' 213 | }); 214 | ``` 215 | 216 | ## Compatibility 217 | 218 | Browser support could stretch back further but will need further testing to confirm. 219 | 220 | | Device | Browser | Version | 221 | |---------|---------|-------------| 222 | | Mobile | Safari | IOS 11.4.0+ | 223 | | Mobile | Chrome | 67.0.x+ | 224 | | Desktop | Chrome | 67.0.x+ | 225 | | Desktop | Safari | 11.1+ | 226 | | Desktop | Firefox | 40.0.2+ | 227 | 228 | ## FAQ 229 | 230 | ### Why did you make this? 231 | 232 | For fun.... mostly. This was also my project for familiarising myself with sockets and javascript modules. 233 | 234 | ### Why did you not choose canvas? 235 | 236 | The decision for not using canvas was based on the idea that anybody can add to their own components. As DOM is more common I decided to not use canvas. 237 | 238 | ## What I would like to change 239 | 240 | - There is some ambiguity between how classes are accessed, I would like to re-evaluate the class structure with [SOLID](https://deviq.com/solid/) design. 241 | 242 | - I would like to look at better alternative ways to load css, the current method of dynamically adding stylesheets to the webpage is probably not the bet option. 243 | 244 | - Move the config initialisation out of the socket class, this is more of a TODO as a result from originally not designing it to work offline. 245 | 246 | - Make this compatible for modern javascript libraries like ReactJS and VueJS. I started this project early on in my career as a developer and was not fully aware of these technologies and their importance at the time. 247 | 248 | ## Release History 249 | 250 | * 0.1.0 251 | * The first proper release. Includes all core functionality for magnets and socket.io implementation. 252 | 253 | ## Meta 254 | 255 | __MrVann__ - [https://github.com/MrVann](https://github.com/MrVann) 256 | 257 | Distributed under the MIT license. See [``LICENSE``](LICENSE) for more information. 258 | 259 | ## Contributing 260 | 261 | 1. Fork it () 262 | 2. Create your feature branch (`git checkout -b feature/fooBar`) 263 | 3. Commit your changes (`git commit -am 'Add some fooBar'`) 264 | 4. Push to the branch (`git push origin feature/fooBar`) 265 | 5. Create a new Pull Request 266 | -------------------------------------------------------------------------------- /dist/magnet.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var _get=function e(t,n,i){null===t&&(t=Function.prototype);var r=Object.getOwnPropertyDescriptor(t,n);if(void 0===r){var o=Object.getPrototypeOf(t);return null===o?void 0:e(o,n,i)}if("value"in r)return r.value;var s=r.get;return void 0!==s?s.call(i):void 0},_createClass=function(){function i(e,t){for(var n=0;n>16),l((65280&r)>>8),l(255&r);return 2===o?l(255&(r=c(e.charAt(t))<<2|c(e.charAt(t+1))>>4)):1===o&&(l((r=c(e.charAt(t))<<10|c(e.charAt(t+1))<<4|c(e.charAt(t+2))>>2)>>8&255),l(255&r)),s},e.fromByteArray=function(e){var t,n,i,r,o=e.length%3,s="";function a(e){return"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(e)}for(t=0,i=e.length-o;t>18&63)+a(r>>12&63)+a(r>>6&63)+a(63&r);switch(o){case 1:s+=a((n=e[e.length-1])>>2),s+=a(n<<4&63),s+="==";break;case 2:s+=a((n=(e[e.length-2]<<8)+e[e.length-1])>>10),s+=a(n>>4&63),s+=a(n<<2&63),s+="="}return s}}(void 0===l?this.base64js={}:l)}).call(this,e("pBGvAp"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},e("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/../node_modules/base64-js/lib/b64.js","/../node_modules/base64-js/lib")},{buffer:2,pBGvAp:4}],2:[function(P,e,D){(function(e,t,m,n,i,r,o,s,a){var u=P("base64-js"),l=P("ieee754");function m(e,t,n){if(!(this instanceof m))return new m(e,t,n);var i,r,o,s,a,u=void 0===e?"undefined":_typeof(e);if("base64"===t&&"string"===u)for(e=(i=e).trim?i.trim():i.replace(/^\s+|\s+$/g,"");e.length%4!=0;)e+="=";if("number"===u)r=x(e);else if("string"===u)r=m.byteLength(e,t);else{if("object"!==u)throw new Error("First argument needs to be a number, array or string.");r=x(e.length)}if(m._useTypedArrays?o=m._augment(new Uint8Array(r)):((o=this).length=r,o._isBuffer=!0),m._useTypedArrays&&"number"==typeof e.byteLength)o._set(e);else if(A(a=e)||m.isBuffer(a)||a&&"object"===(void 0===a?"undefined":_typeof(a))&&"number"==typeof a.length)for(s=0;s>>0)):(t+1>>0),r}function d(e,t,n,i){if(i||(G("boolean"==typeof n,"missing or invalid endian"),G(null!=t,"missing offset"),G(t+1>>8*(i?s:1-s)}function w(e,t,n,i,r){r||(G(null!=t,"missing value"),G("boolean"==typeof i,"missing or invalid endian"),G(null!=n,"missing offset"),G(n+3>>8*(i?s:3-s)&255}function b(e,t,n,i,r){r||(G(null!=t,"missing value"),G("boolean"==typeof i,"missing or invalid endian"),G(null!=n,"missing offset"),G(n+1>8,i=t%256,r.push(i),r.push(n);return r}(e),s,a,u);break;default:throw new Error("Unknown encoding")}return o},m.prototype.toString=function(e,t,n){var i,r,o,s,a=this;if(e=String(e||"utf8").toLowerCase(),t=Number(t)||0,(n=void 0!==n?Number(n):n=a.length)===t)return"";switch(e){case"hex":i=function(e,t,n){var i=e.length;(!t||t<0)&&(t=0);(!n||n<0||ithis.length&&(i=this.length),e.length-t=this.length))return this[e]},m.prototype.readUInt16LE=function(e,t){return c(this,e,!0,t)},m.prototype.readUInt16BE=function(e,t){return c(this,e,!1,t)},m.prototype.readUInt32LE=function(e,t){return h(this,e,!0,t)},m.prototype.readUInt32BE=function(e,t){return h(this,e,!1,t)},m.prototype.readInt8=function(e,t){if(t||(G(null!=e,"missing offset"),G(e=this.length))return 128&this[e]?-1*(255-this[e]+1):this[e]},m.prototype.readInt16LE=function(e,t){return d(this,e,!0,t)},m.prototype.readInt16BE=function(e,t){return d(this,e,!1,t)},m.prototype.readInt32LE=function(e,t){return p(this,e,!0,t)},m.prototype.readInt32BE=function(e,t){return p(this,e,!1,t)},m.prototype.readFloatLE=function(e,t){return y(this,e,!0,t)},m.prototype.readFloatBE=function(e,t){return y(this,e,!1,t)},m.prototype.readDoubleLE=function(e,t){return _(this,e,!0,t)},m.prototype.readDoubleBE=function(e,t){return _(this,e,!1,t)},m.prototype.writeUInt8=function(e,t,n){n||(G(null!=e,"missing value"),G(null!=t,"missing offset"),G(t=this.length||(this[t]=e)},m.prototype.writeUInt16LE=function(e,t,n){v(this,e,t,!0,n)},m.prototype.writeUInt16BE=function(e,t,n){v(this,e,t,!1,n)},m.prototype.writeUInt32LE=function(e,t,n){w(this,e,t,!0,n)},m.prototype.writeUInt32BE=function(e,t,n){w(this,e,t,!1,n)},m.prototype.writeInt8=function(e,t,n){n||(G(null!=e,"missing value"),G(null!=t,"missing offset"),G(t=this.length||(0<=e?this.writeUInt8(e,t,n):this.writeUInt8(255+e+1,t,n))},m.prototype.writeInt16LE=function(e,t,n){b(this,e,t,!0,n)},m.prototype.writeInt16BE=function(e,t,n){b(this,e,t,!1,n)},m.prototype.writeInt32LE=function(e,t,n){E(this,e,t,!0,n)},m.prototype.writeInt32BE=function(e,t,n){E(this,e,t,!1,n)},m.prototype.writeFloatLE=function(e,t,n){k(this,e,t,!0,n)},m.prototype.writeFloatBE=function(e,t,n){k(this,e,t,!1,n)},m.prototype.writeDoubleLE=function(e,t,n){I(this,e,t,!0,n)},m.prototype.writeDoubleBE=function(e,t,n){I(this,e,t,!1,n)},m.prototype.fill=function(e,t,n){if(e||(e=0),t||(t=0),n||(n=this.length),"string"==typeof e&&(e=e.charCodeAt(0)),G("number"==typeof e&&!isNaN(e),"value is not a number"),G(t<=n,"end < start"),n!==t&&0!==this.length){G(0<=t&&t"},m.prototype.toArrayBuffer=function(){if("undefined"!=typeof Uint8Array){if(m._useTypedArrays)return new m(this).buffer;for(var e=new Uint8Array(this.length),t=0,n=e.length;t=t.length||r>=e.length);r++)t[r+n]=e[r];return r}function j(e){try{return decodeURIComponent(e)}catch(e){return String.fromCharCode(65533)}}function U(e,t){G("number"==typeof e,"cannot write a non-number as a number"),G(0<=e,"specified a negative value for writing an unsigned value"),G(e<=t,"value is larger than maximum value for type"),G(Math.floor(e)===e,"value has a fractional component")}function V(e,t,n){G("number"==typeof e,"cannot write a non-number as a number"),G(e<=t,"value larger than maximum allowed value"),G(n<=e,"value smaller than minimum allowed value"),G(Math.floor(e)===e,"value has a fractional component")}function N(e,t,n){G("number"==typeof e,"cannot write a non-number as a number"),G(e<=t,"value larger than maximum allowed value"),G(n<=e,"value smaller than minimum allowed value")}function G(e,t){if(!e)throw new Error(t||"Failed assertion")}m._augment=function(e){return e._isBuffer=!0,e._get=e.get,e._set=e.set,e.get=B.get,e.set=B.set,e.write=B.write,e.toString=B.toString,e.toLocaleString=B.toString,e.toJSON=B.toJSON,e.copy=B.copy,e.slice=B.slice,e.readUInt8=B.readUInt8,e.readUInt16LE=B.readUInt16LE,e.readUInt16BE=B.readUInt16BE,e.readUInt32LE=B.readUInt32LE,e.readUInt32BE=B.readUInt32BE,e.readInt8=B.readInt8,e.readInt16LE=B.readInt16LE,e.readInt16BE=B.readInt16BE,e.readInt32LE=B.readInt32LE,e.readInt32BE=B.readInt32BE,e.readFloatLE=B.readFloatLE,e.readFloatBE=B.readFloatBE,e.readDoubleLE=B.readDoubleLE,e.readDoubleBE=B.readDoubleBE,e.writeUInt8=B.writeUInt8,e.writeUInt16LE=B.writeUInt16LE,e.writeUInt16BE=B.writeUInt16BE,e.writeUInt32LE=B.writeUInt32LE,e.writeUInt32BE=B.writeUInt32BE,e.writeInt8=B.writeInt8,e.writeInt16LE=B.writeInt16LE,e.writeInt16BE=B.writeInt16BE,e.writeInt32LE=B.writeInt32LE,e.writeInt32BE=B.writeInt32BE,e.writeFloatLE=B.writeFloatLE,e.writeFloatBE=B.writeFloatBE,e.writeDoubleLE=B.writeDoubleLE,e.writeDoubleBE=B.writeDoubleBE,e.fill=B.fill,e.inspect=B.inspect,e.toArrayBuffer=B.toArrayBuffer,e}}).call(this,P("pBGvAp"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},P("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/../node_modules/buffer/index.js","/../node_modules/buffer")},{"base64-js":1,buffer:2,ieee754:3,pBGvAp:4}],3:[function(e,t,l){(function(e,t,n,i,r,o,s,a,u){l.read=function(e,t,n,i,r){var o,s,a=8*r-i-1,u=(1<>1,f=-7,c=n?r-1:0,h=n?-1:1,d=e[t+c];for(c+=h,o=d&(1<<-f)-1,d>>=-f,f+=a;0>=-f,f+=i;0>1,h=23===r?Math.pow(2,-24)-Math.pow(2,-77):0,d=i?0:o-1,p=i?1:-1,y=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=f):(s=Math.floor(Math.log(t)/Math.LN2),t*(u=Math.pow(2,-s))<1&&(s--,u*=2),2<=(t+=1<=s+c?h/u:h*Math.pow(2,1-c))*u&&(s++,u/=2),f<=s+c?(a=0,s=f):1<=s+c?(a=(t*u-1)*Math.pow(2,r),s+=c):(a=t*Math.pow(2,c-1)*Math.pow(2,r),s=0));8<=r;e[n+d]=255&a,d+=p,a/=256,r-=8);for(s=s<this._parent.View._width/2&&(document.getElementById(this._focus).style.left=this._parent.View._width/2),parseInt(window.getComputedStyle(t,null).getPropertyValue("top").split("px")[0])>this._parent.View._height/2&&(document.getElementById(this._focus).style.top=this._parent.View._height/2),parseInt(window.getComputedStyle(t,null).getPropertyValue("right").split("px")[0])>this._parent.View._width/2&&(document.getElementById(this._focus).style.left=-this._parent.View._width/2+this._parent.View._wrapperX),parseInt(window.getComputedStyle(t,null).getPropertyValue("bottom").split("px")[0])>this._parent.View._height/2&&(document.getElementById(this._focus).style.top=-this._parent.View._height/2+this._parent.View._wrapperY)),this._lastX=e.x,this._lastY=e.y}},{key:"stopDrag",value:function(e){var t=this._startX+(e.x-this._offsetX),n=this._startY+(e.y-this._offsetY),i=this._parent.View.convertXY(t,n,"component");if(this._component)this._parent.View.getItem(this._focus).setLocalPosition(i.x,i.y);else{document.getElementById(this._focus).style.left.split("px")[0],document.getElementById(this._focus).style.top.split("px")[0];var r=this._parent.View.getPosition();window.location.hash=r.x+","+r.y}this._focus=!1}},{key:"mouseLeave",value:function(){document.getElementById(this._focus).style.left=this._startX,document.getElementById(this._focus).style.top=this._startY,this._focus=!1}}]),t}()}).call(this,e("pBGvAp"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},e("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/Events.js","/")},{buffer:2,pBGvAp:4}],6:[function(e,l,t){(function(e,t,n,i,r,o,s,a,u){l.exports=function(){function t(e){return _classCallCheck(this,t),this._board=this.generateBoard({id:e.id||!1,name:e.name||!1,desc:e.desc||!1,width:e.width||!1,height:e.height||!1}),this._items=this.generateItems(e.items),this._theme=this.generateTheme(e.theme),{board:this._board,items:this._items,theme:this._theme,exists:!0}}return _createClass(t,[{key:"_getDefaultConfig",value:function(){return{target:"root",id:"default",width:1e3,height:1e3,items:{spawn:500,options:{text:["A","B","C","D","E","F"],color:[1,2,3,4,5],type:"Letter"}},theme:[{code:"background",id:1,key:"rgb",value:"255,255,255"},{code:"color",id:1,key:"rgb",value:"32,59,87"},{code:"color",id:2,key:"rgb",value:"79,159,161"},{code:"color",id:3,key:"rgb",value:"0,201,151"},{code:"color",id:4,key:"rgb",value:"183,101,191"},{code:"color",id:5,key:"rgb",value:"255,132,107"},{code:"letter",id:1,key:"fontFamily",value:"'Baloo Bhaina', cursive"},{code:"letter",id:1,key:"textShadow",value:"0 1px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 2px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 3px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 4px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 5px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 6px 6px 20px rgba(0,0,0,.1)"}]}}},{key:"generateBoard",value:function(e){return Object.assign(e,{name:e.name?e.name:"Empty",desc:e.desc?e.desc:"No Description",width:e.width?e.width:2e3,height:e.height?e.height:2e3})}},{key:"generateItems",value:function(f){var c=this;return f||(f=this._getDefaultConfig().items),"array"==typeof f?f:[].concat(_toConsumableArray(Array(f.spawn||this._getDefaultConfig().items.spawn))).map(function(e,t){var n=c.generateLocation(),i={},r=!0,o=!1,s=void 0;try{for(var a,u=Object.keys(f&&f.options)[Symbol.iterator]();!(r=(a=u.next()).done);r=!0){var l=a.value;Array.isArray(f.options[l])?i[l]=c.pickRandomFromArray(f.options[l]):i[l]=f.options[l]}}catch(e){o=!0,s=e}finally{try{!r&&u.return&&u.return()}finally{if(o)throw s}}return Object.assign({id:t},n,i)})}},{key:"generateTheme",value:function(e){return e||this._getDefaultConfig().theme}},{key:"pickRandomFromArray",value:function(e){return e[Math.floor(Math.random()*e.length)]}},{key:"generateLocation",value:function(){return{x:Math.floor(Math.random()*(this._board.width+1))-this._board.width/2,y:Math.floor(Math.random()*(this._board.height+1))-this._board.height/2}}}]),t}()}).call(this,e("pBGvAp"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},e("buffer").Buffer,arguments[3],arguments[4],arguments[5],arguments[6],"/Generator.js","/")},{buffer:2,pBGvAp:4}],7:[function(require,module,exports){(function(process,global,Buffer,__argument0,__argument1,__argument2,__argument3,__filename,__dirname){module.exports=function(){function Mixins(){_classCallCheck(this,Mixins)}return _createClass(Mixins,[{key:"applyStyle",value:function(e,t,n){var i=void 0!==t.length?t:Object.entries(t),r=!0,o=!1,s=void 0;try{for(var a,u=i[Symbol.iterator]();!(r=(a=u.next()).done);r=!0)t=a.value,e.style[t[0]]=this.applyTemplate(t[1],n)}catch(e){o=!0,s=e}finally{try{!r&&u.return&&u.return()}finally{if(o)throw s}}return e}},{key:"applyTemplate",value:function applyTemplate(value,params){if(-1!=value.indexOf("[")&&(value=value.replace(/\[r\]/g,params.r).replace(/\[g\]/g,params.g).replace(/\[b\]/g,params.b)),-1!=value.indexOf("{")){var regex=value.match(/\{([^}]+)\}/g),_iteratorNormalCompletion3=!0,_didIteratorError3=!1,_iteratorError3=void 0;try{for(var _iterator3=regex[Symbol.iterator](),_step3;!(_iteratorNormalCompletion3=(_step3=_iterator3.next()).done);_iteratorNormalCompletion3=!0){var match=_step3.value;value=value.replace(match,eval(match))}}catch(e){_didIteratorError3=!0,_iteratorError3=e}finally{try{!_iteratorNormalCompletion3&&_iterator3.return&&_iterator3.return()}finally{if(_didIteratorError3)throw _iteratorError3}}}return value}},{key:"convertToCSSString",value:function(e){var t=Object.entries(e),n=[],i=!0,r=!1,o=void 0;try{for(var s,a=t[Symbol.iterator]();!(i=(s=a.next()).done);i=!0){for(var u=s.value,l=0;l\n ';var i=document.createElement("button");i.innerHTML="Create",n.id="message",i.addEventListener("click",function(){document.getElementsByClassName("message").outerHTML="",document.getElementById(e._parent._id+"_board").removeChild(document.getElementById("message")),t._parent.Socket._socket.emit("start",{id:t._parent._id})}),n.appendChild(i),document.getElementById(this._parent._id+"_board").appendChild(n),this.fadeIn()}},{key:"setEvents",value:function(){var t=this;window.onresize=function(e){t.setWrapperSize(),t.loadPosition()}}},{key:"setSize",value:function(e,t){this._width=e,this._height=t;var n=document.getElementById(this._parent._id+"_board");n.style.width=this._width+"px",n.style.height=this._height+"px",n.style.margin="-"+this._height/2+"px -"+this._width/2+"px"}},{key:"setPosition",value:function(e,t){this._x=e,this._y=t,this.loadPosition()}},{key:"getPosition",value:function(){var e=document.getElementById(this._parent._id+"_board");return{x:Math.floor(parseInt(e.style.left.split("px")[0])-this._wrapperX/2),y:Math.floor(parseInt(e.style.top.split("px")[0])-this._wrapperY/2)}}},{key:"setWrapperSize",value:function(){document.getElementById(this._parent._id+"_wrapper");this._wrapperX=document.getElementById(this._parent._root).offsetWidth,this._wrapperY=document.getElementById(this._parent._root).offsetHeight}},{key:"loadPosition",value:function(){var e=document.getElementById(this._parent._id+"_board");e.style.top=parseInt(this._wrapperY/2)+parseInt(this._y)+"px",e.style.left=parseInt(this._wrapperX/2)+parseInt(this._x)+"px"}},{key:"setTheme",value:function(){this._parent.Theme.applyBackground(document.getElementById(this._parent._id+"_board"))}},{key:"setItem",value:function(e,t){this._items.set(e,t)}},{key:"getItem",value:function(e){return this._items.get(parseInt(e))}},{key:"convertXY",value:function(e,t,n){if("board"==n){var i=document.getElementById(this._parent._id+"_board");return{x:e+parseInt(i.style.width.split("px")[0])/2,y:t+parseInt(i.style.height.split("px")[0])/2}}var r=document.getElementById(this._parent._id+"_board");return{x:e-parseInt(r.style.width.split("px")[0])/2,y:t-parseInt(r.style.height.split("px")[0])/2}}},{key:"updateStats",value:function(e){document.getElementById("members").innerHTML=e.members+" Online"}},{key:"hideLoad",value:function(){document.getElementById("spinner").style.display="none"}},{key:"fadeIn",value:function(){document.getElementsByClassName("wrapper")[0].style.opacity=1}},{key:"fadeInStats",value:function(){document.getElementsByClassName("statsToggle")[0].style.opacity=1}},{key:"renderItems",value:function(){this.hideLoad();for(var e=[].concat(_toConsumableArray(this._items.values())),t=0;t 2 | 3 | MagnetJS 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /example/html/load_magnet.js: -------------------------------------------------------------------------------- 1 | 2 | var config = { 3 | id: 'test', // ID of the board (if using sockets) 4 | target: 'root', // ID of the element to build the board onto 5 | width: 1000, // Width of the board (px) 6 | height: 1000, // Height of the board (px) 7 | items: { 8 | spawn: 100, // Spawn 100 items to the board 9 | options: { 10 | text: ['Toast','is','Food','for','my','belly'], // Text combinations to create the items with. 11 | color: [1,2,3,4,5], // The themes color unique identifiers. 12 | type: 'Letter' // The item type 13 | } 14 | }, 15 | theme: [ 16 | {code: "background", id: 1, key: "rgb", value: "50,50,50"}, // Set the board background to rgb(50,50,50) 17 | {code: "color", id: 1, key: "rgb", value: "132,159,187"}, // First theme color 18 | {code: "color", id: 2, key: "rgb", value: "179,159,161"}, // Second theme color 19 | {code: "color", id: 3, key: "rgb", value: "110,201,151"}, // Third theme color 20 | {code: "color", id: 4, key: "rgb", value: "183,101,191"}, // Fourth theme color 21 | {code: "color", id: 5, key: "rgb", value: "255,132,107"}, // Fifth theme color 22 | {code: "letter", id: 1, key: "fontFamily", value: "'Baloo Bhaina', cursive"}, // Font family for `letter` component 23 | // Text shadow for the `letter` component that uses the theme color to build a shade. 24 | { 25 | code: "letter", 26 | id: 1, 27 | key: "textShadow", 28 | value : "0 1px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 2px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 3px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 4px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 5px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 6px 6px 20px rgba(0,0,0,.1)" 29 | } 30 | ] 31 | }; 32 | 33 | const board = new Magnet({ target: 'root' }); 34 | 35 | window.onload = function () { 36 | board.mount(); 37 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | const concat = require('gulp-concat'); 4 | const uglify = require('gulp-uglify'); 5 | const webserver = require('gulp-webserver'); 6 | const watch = require('gulp-watch'); 7 | const browserify = require('gulp-browserify'); 8 | const sourcemaps = require('gulp-sourcemaps'); 9 | 10 | gulp.task('build', () => { 11 | gulp.src('src/Magnet.js') 12 | .pipe(browserify({ 13 | insertGlobals : true, 14 | debug : true 15 | })) 16 | .pipe(sourcemaps.init({loadMaps: true})) 17 | .pipe(concat('magnet.min.js')) 18 | .pipe(babel({ 19 | presets: ["env", "stage-2", "stage-0"] 20 | })) 21 | .pipe(uglify()) 22 | .pipe(sourcemaps.write('./')) 23 | .pipe(gulp.dest('dist')); 24 | }); 25 | 26 | gulp.task('server', () => { 27 | return gulp.src(['example/html','dist']) 28 | .pipe(webserver({ 29 | fallback: 'index.html', 30 | path: '/', 31 | livereload: true, 32 | directoryListing: false, 33 | open: true 34 | })); 35 | }) 36 | 37 | gulp.task('watch', () => { 38 | return watch('src/**/*', function () { 39 | gulp.src('src/Magnet.js') 40 | .pipe(browserify({ 41 | insertGlobals : true, 42 | debug : true 43 | })) 44 | .pipe(sourcemaps.init({loadMaps: true})) 45 | .pipe(concat('magnet.min.js')) 46 | .pipe(babel({ 47 | presets: ["env", "stage-2", "stage-0"] 48 | })) 49 | .pipe(uglify()) 50 | .pipe(sourcemaps.write('./')) 51 | .pipe(gulp.dest('dist')); 52 | }); 53 | }); 54 | 55 | gulp.task('dev', ['build','watch','server']) 56 | 57 | gulp.task('default', ['build'] ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magnetjs", 3 | "version": "0.1.0", 4 | "description": "Library for creating your own online magnets board.", 5 | "main": "example/index.js", 6 | "scripts": { 7 | "start": "gulp dev", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/MrVann/MagnetJS.git" 13 | }, 14 | "keywords": [ 15 | "magnetjs", 16 | "magnet", 17 | "fridge", 18 | "fridge-magnet", 19 | "library" 20 | ], 21 | "author": "David Vann", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/MrVann/MagnetJS/issues" 25 | }, 26 | "homepage": "https://github.com/MrVann/MagnetJS#readme", 27 | "devDependencies": { 28 | "babel-core": "^6.26.3", 29 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 30 | "babel-preset-env": "^1.7.0", 31 | "babel-preset-stage-0": "^6.24.1", 32 | "babel-preset-stage-2": "^6.24.1", 33 | "gulp": "^3.9.1", 34 | "gulp-babel": "^7.0.1", 35 | "gulp-browserify": "^0.5.1", 36 | "gulp-concat": "^2.6.1", 37 | "gulp-sourcemaps": "^2.6.4", 38 | "gulp-uglify": "^3.0.0", 39 | "gulp-watch": "^5.0.0", 40 | "gulp-webserver": "^0.9.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Events.js: -------------------------------------------------------------------------------- 1 | module.exports = class Events{ 2 | constructor(parent){ 3 | this._parent = parent; 4 | this._startX = 0; 5 | this._startY = 0; 6 | this._focus = false; 7 | this._component = false; 8 | this.initEvents(); 9 | } 10 | 11 | initEvents(){ 12 | const board = document.getElementById(`${this._parent._id}_board`); 13 | const me = this; 14 | board.addEventListener("mousedown", (e) => { 15 | const id = e.target.id; 16 | const component = (e.target.getAttribute('type') == 'component'); 17 | me.startDrag({ 18 | id, 19 | component, 20 | x: e.x, 21 | y: e.y 22 | }); 23 | }); 24 | board.addEventListener("mouseup", (e) => { 25 | this.stopDrag({ 26 | x: e.x, 27 | y: e.y 28 | }); 29 | }); 30 | board.addEventListener("mousemove", (e) => { 31 | if(this._focus) this.moveItem({ 32 | x: e.x, 33 | y: e.y 34 | }) 35 | }); 36 | board.addEventListener("mouseleave", (e) => { 37 | if(this._focus) this.mouseLeave() 38 | }); 39 | board.addEventListener("touchstart", (e) => { 40 | const id = e.changedTouches[0].target.id; 41 | const component = (e.changedTouches[0].target.getAttribute('type') == 'component'); 42 | me.startDrag({ 43 | id, 44 | component, 45 | x: e.changedTouches[0].clientX, 46 | y: e.changedTouches[0].clientY 47 | }); 48 | }); 49 | board.addEventListener("touchmove", (e) => { 50 | if(this._focus) this.moveItem({ 51 | x: e.changedTouches[0].clientX, 52 | y: e.changedTouches[0].clientY 53 | }) 54 | }); 55 | board.addEventListener("touchend", (e) => { 56 | this.stopDrag({ 57 | x: e.changedTouches[0].clientX, 58 | y: e.changedTouches[0].clientY 59 | }); 60 | board.addEventListener("touchcancel", (e) => { 61 | if(this._focus) this.mouseLeave() 62 | }); 63 | }); 64 | } 65 | 66 | startDrag(conf){ 67 | const item = document.getElementById(conf.id); 68 | this._offsetX = conf.x; 69 | this._offsetY = conf.y; 70 | this._startX = parseInt(item.style.left.split('px')[0]); 71 | this._startY = parseInt(item.style.top.split('px')[0]); 72 | this._focus = conf.id; 73 | this._component = conf.component; 74 | } 75 | 76 | moveItem(conf){ 77 | const board = document.getElementById(`${this._parent._id}_board`); 78 | const left = parseInt(window.getComputedStyle(board,null).getPropertyValue("left").split('px')[0]); 79 | const right = parseInt(window.getComputedStyle(board,null).getPropertyValue("right").split('px')[0]); 80 | const top = parseInt(window.getComputedStyle(board,null).getPropertyValue("top").split('px')[0]); 81 | const bottom = parseInt(window.getComputedStyle(board,null).getPropertyValue("bottom").split('px')[0]); 82 | const xDir = this._lastX - conf.x; 83 | const yDir = this._lastY - conf.y; 84 | 85 | if(((left < (this._parent.View._width / 2) || xDir > 0 ) && (right < ((this._parent.View._width / 2))) || xDir < 0 ) || this._component) document.getElementById(this._focus).style.left = this._startX + (conf.x - this._offsetX); 86 | if(((top < (this._parent.View._width / 2) || yDir > 0 ) && (bottom < ((this._parent.View._width / 2))) || yDir < 0 ) || this._component) document.getElementById(this._focus).style.top = this._startY + (conf.y - this._offsetY); 87 | 88 | 89 | if(!this._component) { 90 | if (parseInt(window.getComputedStyle(board, null).getPropertyValue("left").split('px')[0]) > (this._parent.View._width / 2)) { 91 | document.getElementById(this._focus).style.left = (this._parent.View._width / 2) 92 | } 93 | 94 | if (parseInt(window.getComputedStyle(board, null).getPropertyValue("top").split('px')[0]) > (this._parent.View._height / 2)) { 95 | document.getElementById(this._focus).style.top = (this._parent.View._height / 2) 96 | } 97 | 98 | if ((parseInt(window.getComputedStyle(board, null).getPropertyValue("right").split('px')[0]) > (this._parent.View._width / 2))) { 99 | document.getElementById(this._focus).style.left = -((this._parent.View._width / 2)) + this._parent.View._wrapperX 100 | } 101 | 102 | if ((parseInt(window.getComputedStyle(board, null).getPropertyValue("bottom").split('px')[0]) > (this._parent.View._height / 2))) { 103 | document.getElementById(this._focus).style.top = -((this._parent.View._height / 2)) + this._parent.View._wrapperY 104 | } 105 | } 106 | 107 | this._lastX = conf.x; 108 | this._lastY = conf.y; 109 | } 110 | 111 | stopDrag(conf){ 112 | const x = this._startX + (conf.x - this._offsetX); 113 | const y = this._startY + (conf.y - this._offsetY); 114 | const newXY = this._parent.View.convertXY(x, y, 'component'); 115 | if(this._component) this._parent.View.getItem(this._focus).setLocalPosition(newXY.x, newXY.y); 116 | else{ 117 | const left = document.getElementById(this._focus).style.left.split('px')[0]; 118 | const top = document.getElementById(this._focus).style.top.split('px')[0]; 119 | const boardXY = this._parent.View.getPosition(); 120 | window.location.hash = boardXY.x+','+boardXY.y; 121 | } 122 | this._focus = false; 123 | } 124 | 125 | mouseLeave(){ 126 | document.getElementById(this._focus).style.left = this._startX; 127 | document.getElementById(this._focus).style.top = this._startY; 128 | this._focus = false; 129 | // $('#'+this._focus).animate({ 130 | // left: this._startX, 131 | // top: this._startY 132 | // }, 200); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Generator.js: -------------------------------------------------------------------------------- 1 | module.exports = class Generator { 2 | constructor(props){ 3 | this._board = this.generateBoard({ 4 | id: props.id || false, 5 | name: props.name || false, 6 | desc: props.desc || false, 7 | width: props.width || false, 8 | height: props.height || false 9 | }); 10 | this._items = this.generateItems(props.items); 11 | this._theme = this.generateTheme(props.theme); 12 | 13 | return { 14 | board: this._board, 15 | items: this._items, 16 | theme: this._theme, 17 | exists: true // Required to align with the sockets feature (handles a scenario where the board doesn't yet exist) 18 | } 19 | } 20 | 21 | _getDefaultConfig(){ 22 | return { 23 | target: 'root', 24 | id: 'default', 25 | width: 1000, 26 | height: 1000, 27 | items: { 28 | spawn: 500, 29 | options: { 30 | text: ['A','B','C','D','E','F'], 31 | color: [1,2,3,4,5], 32 | type: 'Letter' 33 | } 34 | }, 35 | theme: [ 36 | {code: "background", id: 1, key: "rgb", value: "255,255,255"}, 37 | {code: "color", id: 1, key: "rgb", value: "32,59,87"}, 38 | {code: "color", id: 2, key: "rgb", value: "79,159,161"}, 39 | {code: "color", id: 3, key: "rgb", value: "0,201,151"}, 40 | {code: "color", id: 4, key: "rgb", value: "183,101,191"}, 41 | {code: "color", id: 5, key: "rgb", value: "255,132,107"}, 42 | {code: "letter", id: 1, key: "fontFamily", value: "'Baloo Bhaina', cursive"}, 43 | { 44 | code: "letter", 45 | id: 1, 46 | key: "textShadow", 47 | value : "0 1px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 2px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 3px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 4px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 0 5px 0 rgb({[r]-50},{[g]-50},{[b]-50}), 6px 6px 20px rgba(0,0,0,.1)" 48 | } 49 | ] 50 | }; 51 | } 52 | 53 | 54 | generateBoard(board){ 55 | return Object.assign( 56 | board, 57 | { 58 | name: board.name ? board.name : "Empty", 59 | desc: board.desc ? board.desc : "No Description", 60 | width: board.width ? board.width : 2000, 61 | height: board.height ? board.height : 2000 62 | }) 63 | } 64 | 65 | generateItems(items) { 66 | 67 | if(!items){ 68 | items = this._getDefaultConfig().items; 69 | } 70 | 71 | if (typeof items == 'array') { 72 | return items; 73 | } 74 | 75 | return [...Array(items.spawn || this._getDefaultConfig().items.spawn)].map((item, index) => { 76 | 77 | const location = this.generateLocation(); 78 | 79 | const extraValues = {}; 80 | 81 | for (const key of Object.keys(items && items.options)) { 82 | if (Array.isArray(items.options[key])){ 83 | extraValues[key] = this.pickRandomFromArray(items.options[key]); 84 | } 85 | else { 86 | extraValues[key] = items.options[key]; 87 | } 88 | } 89 | 90 | return Object.assign( 91 | { 92 | id: index 93 | }, 94 | location, 95 | extraValues); 96 | 97 | }) 98 | 99 | } 100 | 101 | generateTheme(theme){ 102 | if(!theme) return this._getDefaultConfig().theme; 103 | return theme; 104 | } 105 | 106 | pickRandomFromArray(array){ 107 | return array[Math.floor(Math.random() * array.length)]; 108 | } 109 | 110 | generateLocation(){ 111 | return { 112 | x: (Math.floor(Math.random() * (this._board.width + 1))) - (this._board.width / 2), 113 | y: (Math.floor(Math.random() * (this._board.height + 1))) - (this._board.height / 2) 114 | } 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /src/Magnet.js: -------------------------------------------------------------------------------- 1 | const component = require('./component/index.js') 2 | const View = require('./View.js') 3 | const Theme = require('./Theme.js') 4 | const Events = require('./Events.js') 5 | const Socket = require('./Socket.js') 6 | const Mixins = require('./Mixins.js') 7 | const Generator = require('./Generator') 8 | 9 | module.exports = window.Magnet; 10 | 11 | window.Magnet = class Magnet { 12 | constructor(config){ 13 | this.initConfig(config); 14 | // this.initClasses(); 15 | } 16 | 17 | /** 18 | * Mount the board 19 | */ 20 | mount(){ 21 | console.log('Magnet started'); 22 | this.initClasses(); 23 | } 24 | 25 | /** 26 | * Initialise Config 27 | * @param target 28 | * @param id 29 | */ 30 | initConfig(config, id){ 31 | this._root = config.target; 32 | this._id = config.id ? config.id : 'local'; 33 | this._name = ''; 34 | this._description = ''; 35 | this._config = new Generator(config) 36 | } 37 | 38 | /** 39 | * Initialise Classes 40 | */ 41 | initClasses(){ 42 | this.Mixins = new Mixins(this); 43 | this.component = component; 44 | this.View = new View(this); 45 | this.Socket = new Socket(this); 46 | this.Events = new Events(this); 47 | } 48 | 49 | /** 50 | * Return root dom 51 | */ 52 | getRoot(){ 53 | return document.getElementById(this._root); 54 | } 55 | 56 | /** 57 | * Get Board data 58 | */ 59 | getBoard(data){ 60 | this.Socket._socket.emit('load'); 61 | } 62 | 63 | /** 64 | * Set Board from Data 65 | */ 66 | setBoard(data){ 67 | this.setTheme(data.theme); 68 | this.setDetails(data.board); 69 | this.setItems(data.items); 70 | this.View.fadeIn(); 71 | this.View.fadeInStats(); 72 | } 73 | 74 | /** 75 | * Set board theme 76 | */ 77 | setTheme(theme){ 78 | this.Theme = new Theme(theme, this); 79 | this.View.setTheme(); 80 | } 81 | 82 | /** 83 | * Set board details 84 | */ 85 | setDetails(details){ 86 | this._name = details.name; 87 | this._description = details.desc; 88 | this.View.setSize(details.width, details.height); 89 | this.View.setPosition(window.location.hash.split(',')[0].split('#')[1] || 0,window.location.hash.split(',')[1] || 0); 90 | } 91 | 92 | /** 93 | * Set board items 94 | */ 95 | setItems(items){ 96 | for(const item of items){ 97 | this.View.setItem(item.id, new this.component[item.type](item, this)); 98 | } 99 | this.View.renderItems(); 100 | } 101 | } -------------------------------------------------------------------------------- /src/Mixins.js: -------------------------------------------------------------------------------- 1 | module.exports = class Mixins{ 2 | constructor(){ 3 | } 4 | 5 | /** 6 | * Apply styles to dom object 7 | */ 8 | applyStyle(dom, style, params){ 9 | const arrayStyle = (typeof style.length != 'undefined') ? style : Object.entries(style); 10 | for(style of arrayStyle){ 11 | dom.style[style[0]] = this.applyTemplate(style[1], params); 12 | } 13 | return dom; 14 | } 15 | 16 | /** 17 | * Apply template information 18 | */ 19 | applyTemplate(value, params){ 20 | if(value.indexOf('[') != -1) value = value.replace(/\[r\]/g, params.r).replace(/\[g\]/g, params.g).replace(/\[b\]/g, params.b); 21 | if(value.indexOf('{') !=- 1) { 22 | const regex = value.match(/\{([^}]+)\}/g); 23 | for (const match of regex) { 24 | value = value.replace(match, eval(match)); 25 | } 26 | } 27 | return value; 28 | } 29 | 30 | /** 31 | * Convert css object to string 32 | */ 33 | convertToCSSString(cssObject){ 34 | const entries = Object.entries(cssObject); 35 | const cssArray = []; 36 | for(const entry of entries){ 37 | for(let i = 0; i < entry[0].length; i++){ 38 | if(entry[0].charAt(i) === entry[0].charAt(i).toUpperCase()){ 39 | entry[0] = entry[0].slice(0,i)+'-'+entry[0].slice(i, entry[0].length).toLowerCase(); 40 | } 41 | } 42 | cssArray.push(entry[0]+': '+entry[1]); 43 | } 44 | return cssArray.join('; '); 45 | } 46 | 47 | makeAnimation(item, value, to, duration){ 48 | const start = item.style[value].split('px')[0]; 49 | const change = to - start; 50 | const increment = 10; 51 | let currentTime = 0; 52 | const easeInOutQuad = function(t, b, c, d) { 53 | t /= d/2; 54 | if (t < 1) return c/2*t*t + b; 55 | t--; 56 | return -c/2 * (t*(t-2) - 1) + b; 57 | }; 58 | 59 | var animateScroll = function(){ 60 | currentTime += increment; 61 | var val = easeInOutQuad(currentTime, start, change, duration); 62 | // console.log(value, (parseFloat(start) + parseFloat(val)), val, duration, currentTime); 63 | item.style[value] = currentTime == duration ? to : (parseFloat(start) + parseFloat(val)); 64 | if(currentTime < duration) { 65 | setTimeout(animateScroll, increment); 66 | } 67 | }; 68 | animateScroll(); 69 | 70 | } 71 | } -------------------------------------------------------------------------------- /src/Socket.js: -------------------------------------------------------------------------------- 1 | const Generator = require('./Generator'); 2 | 3 | module.exports = class Socket{ 4 | constructor(parent){ 5 | this._parent = parent; 6 | this.initSocket(); 7 | } 8 | 9 | /** 10 | * Init socket events 11 | */ 12 | initSocket(){ 13 | const me = this; 14 | this._socketConnected = false; 15 | if(typeof io == 'undefined'){ 16 | console.info('Socket.io not enabled, generating static board'); 17 | this._socketEnabled = false; 18 | this._socket = { 19 | emit: () => { 20 | return false; 21 | } 22 | } 23 | const generatedBoard = this._parent._config; 24 | me.onLoad(generatedBoard); 25 | } 26 | else{ 27 | this._socketEnabled = true; 28 | this._socket = io.connect('ws://www.online-fridge.com'); 29 | this._socket.on('connect', () => { 30 | me.onSocketConnected(); 31 | }); 32 | this._socket.on('disconnect', () => { 33 | me.onSocketDisconnected(); 34 | }); 35 | this._socket.on('error', (err) => { 36 | me.onSocketError(err); 37 | }); 38 | this._socket.on('authenticate', (data) => { 39 | me.onAuthenticate(data); 40 | }) 41 | this._socket.on('load', (data) => { 42 | me.onLoad(data); 43 | }) 44 | this._socket.on('move', (data) => { 45 | me.onMove(data); 46 | }) 47 | this._socket.on('add', (data) => { 48 | me.onAdd(data); 49 | }) 50 | this._socket.on('noExist', (data) => { 51 | me._parent.View.askCreate(); 52 | }) 53 | this._socket.on('stats', (data) => { 54 | me._parent.View.updateStats(data); 55 | }) 56 | } 57 | } 58 | 59 | /** 60 | * On Authenticate 61 | */ 62 | onAuthenticate(data){ 63 | this._parent.getBoard(data); 64 | } 65 | 66 | /** 67 | * On Load 68 | */ 69 | onLoad(data){ 70 | this._parent.setBoard(data) 71 | } 72 | 73 | /** 74 | * On Move 75 | */ 76 | onMove(data){ 77 | for(const move of data){ 78 | if(move.time) this._parent.View.getItem(move.id).setRemotePosition(move.x, move.y, move.time) 79 | else this._parent.View.getItem(move.id).setRemotePosition(move.x, move.y) 80 | } 81 | } 82 | 83 | /** 84 | * On Item Add 85 | */ 86 | onAdd(data){ 87 | this._parent.View.addItem(data); 88 | } 89 | 90 | /** 91 | * On Socket Connect 92 | */ 93 | onSocketConnected(){ 94 | this._socketConnected = true; 95 | console.log(`${this._parent._id.split('#')[0]}: Connected to socket`); 96 | this.authenticate(); 97 | } 98 | 99 | /** 100 | * Authenticate Board 101 | */ 102 | authenticate(){ 103 | console.log('authenticating'); 104 | this._socket.emit('authenticate', {id: this._parent._id}); 105 | } 106 | 107 | /** 108 | * On Socket Disconnected 109 | */ 110 | onSocketDisconnected(){ 111 | this._socketConnected = false; 112 | console.log(`${this._parent._id}: Socket disconnected`); 113 | } 114 | 115 | /** 116 | * On Socket Error 117 | */ 118 | onSocketError(err){ 119 | this._socketConnected = false; 120 | console.log(error); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Theme.js: -------------------------------------------------------------------------------- 1 | module.exports = class Theme{ 2 | 3 | /** 4 | * Initiate theme with config 5 | * @param theme 6 | */ 7 | constructor(theme, parent){ 8 | this._parent = parent; 9 | this._items = new Map(); 10 | this._colors = new Map(); 11 | this.setTheme(theme); 12 | } 13 | 14 | /** 15 | * Set theme state 16 | */ 17 | setTheme(theme){ 18 | for(const item of theme){ 19 | switch(item.key){ 20 | case 'rgb':{ 21 | this._colors.set((item.code == 'color') ? item.id : item.code, { 22 | r: item.value.split(',')[0], 23 | g: item.value.split(',')[1], 24 | b: item.value.split(',')[2] 25 | }) 26 | break; 27 | } 28 | default:{ 29 | if(!this._items.has(item.code)) { 30 | this._items.set(item.code, theme.filter(other => (other.code == item.code)).map( item => ([item.key, item.value]))); 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Apply theme to dom object 39 | */ 40 | applyTheme(dom, code, params){ 41 | //debugger; 42 | if(this._items.has(code)){ 43 | // debugger; 44 | return this._parent.Mixins.applyStyle(dom, this.getItem(code), params); 45 | } 46 | return dom; 47 | } 48 | 49 | /** 50 | * Apply background to dom object 51 | */ 52 | applyBackground(dom){ 53 | dom.style.background = this.getColorCSS('background'); 54 | return dom; 55 | } 56 | 57 | /** 58 | * Get theme item array 59 | */ 60 | getItem(code){ 61 | //console.log('getting: ', this._items.get(code)); 62 | return this._items.get(code); 63 | } 64 | 65 | /** 66 | * Get color rgb values as object 67 | */ 68 | getColor(key){ 69 | return this._colors.get(key) 70 | } 71 | 72 | /** 73 | * Get color as rgb string (CSS) 74 | */ 75 | getColorCSS(key){ 76 | const color = this._colors.get(key); 77 | return `rgb(${color.r},${color.g},${color.b})`; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/View.js: -------------------------------------------------------------------------------- 1 | module.exports = class View{ 2 | constructor(me){ 3 | this._parent = me; 4 | this._items = new Map(); 5 | this._classNames = new Set(); 6 | this.initCSS(); 7 | this.buildDOM(); 8 | this.setWrapperSize(); 9 | this.setEvents(); 10 | } 11 | 12 | /** 13 | * Initialise CSS Objects 14 | */ 15 | initCSS(){ 16 | this._wrapperCSS = { 17 | width: '100%', 18 | height: '100%', 19 | overflow: 'hidden', 20 | position: 'relative' 21 | } 22 | 23 | this._boardCSS = { 24 | width: '100%', 25 | height: '100%', 26 | position: 'absolute', 27 | background: 'white' 28 | } 29 | } 30 | 31 | /** 32 | * Build DOM 33 | */ 34 | buildDOM(){ 35 | const root = document.getElementById(this._parent._root); 36 | const wrapper = document.createElement("div"); 37 | wrapper.id = `${this._parent._id}_wrapper`; 38 | wrapper.className = "wrapper"; 39 | this._parent.Mixins.applyStyle(wrapper, this._wrapperCSS); 40 | const board = document.createElement("div"); 41 | board.id = `${this._parent._id}_board`; 42 | this._parent.Mixins.applyStyle(board, this._boardCSS); 43 | const stats = document.createElement('div'); 44 | stats.id = 'members'; 45 | stats.className = 'statsToggle'; 46 | const spinner = document.createElement('div'); 47 | const dot1 = document.createElement('div'); 48 | const dot2 = document.createElement('div'); 49 | spinner.className = 'spinner'; 50 | spinner.id = 'spinner'; 51 | dot1.className = 'dot1'; 52 | dot2.className = 'dot2'; 53 | spinner.appendChild(dot1); 54 | spinner.appendChild(dot2); 55 | wrapper.appendChild(board); 56 | root.innerHTML = ''; 57 | root.appendChild(wrapper); 58 | root.appendChild(stats); 59 | root.appendChild(spinner); 60 | } 61 | 62 | /** 63 | * Add Item 64 | */ 65 | addItem(item){ 66 | this.setItem(item.id, new this._parent.component[item.type](item, this._parent)); 67 | this.renderItems(); 68 | } 69 | 70 | /** 71 | * Ask To Create Board (If Board doesn't exist) 72 | */ 73 | askCreate(){ 74 | const me = this; 75 | const message = document.createElement("div"); 76 | message.className = "message"; 77 | message.innerHTML = ` 78 | Fridge "${this._parent._id}" is available!
79 | ` 80 | const button = document.createElement("button"); 81 | button.innerHTML = 'Create'; 82 | message.id = 'message'; 83 | button.addEventListener('click', () => { 84 | document.getElementsByClassName('message').outerHTML = ''; 85 | document.getElementById(`${this._parent._id}_board`).removeChild(document.getElementById('message')); 86 | me._parent.Socket._socket.emit('start', {id: me._parent._id}); 87 | }); 88 | 89 | message.appendChild(button); 90 | 91 | document.getElementById(`${this._parent._id}_board`).appendChild(message); 92 | this.fadeIn(); 93 | } 94 | 95 | /** 96 | * Set View Events (Resize window etc) 97 | */ 98 | setEvents(){ 99 | const me = this; 100 | window.onresize = function(event) { 101 | me.setWrapperSize(); 102 | me.loadPosition(); 103 | }; 104 | } 105 | 106 | /** 107 | * Set Board size 108 | */ 109 | setSize(width, height){ 110 | this._width = width; 111 | this._height = height; 112 | const board = document.getElementById(`${this._parent._id}_board`); 113 | board.style.width = this._width+'px'; 114 | board.style.height = this._height+'px'; 115 | board.style.margin = `-${this._height / 2}px -${this._width / 2}px`; 116 | } 117 | 118 | /** 119 | * Set board Position 120 | */ 121 | setPosition(x, y){ 122 | this._x = x; 123 | this._y = y; 124 | this.loadPosition(); 125 | } 126 | 127 | /** 128 | * Get board position 129 | */ 130 | getPosition(){ 131 | const board = document.getElementById(`${this._parent._id}_board`); 132 | return { 133 | x: Math.floor((parseInt(board.style.left.split('px')[0]) - (this._wrapperX / 2))), 134 | y: Math.floor(parseInt(board.style.top.split('px')[0]) - (this._wrapperY / 2)) 135 | } 136 | } 137 | 138 | setWrapperSize(){ 139 | const wrapper = document.getElementById(`${this._parent._id}_wrapper`); 140 | this._wrapperX = document.getElementById(this._parent._root).offsetWidth; 141 | this._wrapperY = document.getElementById(this._parent._root).offsetHeight; 142 | } 143 | 144 | loadPosition(){ 145 | const board = document.getElementById(`${this._parent._id}_board`); 146 | board.style.top = `${parseInt((this._wrapperY / 2)) + parseInt(this._y)}px`; 147 | board.style.left = `${parseInt((this._wrapperX / 2)) + parseInt(this._x)}px`; 148 | } 149 | 150 | setTheme(){ 151 | this._parent.Theme.applyBackground(document.getElementById(`${this._parent._id}_board`)); 152 | } 153 | 154 | setItem(id, object){ 155 | this._items.set(id, object) 156 | } 157 | 158 | getItem(id){ 159 | return this._items.get(parseInt(id)); 160 | } 161 | 162 | convertXY(x, y, target){ 163 | if(target == 'board') { 164 | const board = document.getElementById(`${this._parent._id}_board`); 165 | return { 166 | x: x + (parseInt(board.style.width.split('px')[0]) / 2), 167 | y: y + (parseInt(board.style.height.split('px')[0]) / 2), 168 | } 169 | } 170 | else{ 171 | const board = document.getElementById(`${this._parent._id}_board`); 172 | return { 173 | x: x - (parseInt(board.style.width.split('px')[0]) / 2), 174 | y: y - (parseInt(board.style.height.split('px')[0]) / 2), 175 | } 176 | } 177 | } 178 | 179 | updateStats(stats){ 180 | document.getElementById('members').innerHTML = stats.members+' Online'; 181 | } 182 | 183 | hideLoad(){ 184 | document.getElementById('spinner').style.display = 'none'; 185 | } 186 | 187 | /** 188 | * Fade the board into view 189 | */ 190 | fadeIn(){ 191 | document.getElementsByClassName('wrapper')[0].style.opacity = 1; 192 | } 193 | 194 | fadeInStats(){ 195 | document.getElementsByClassName('statsToggle')[0].style.opacity = 1; 196 | } 197 | 198 | renderItems(){ 199 | this.hideLoad(); 200 | for(const item of [...this._items.values()]){ 201 | if(!document.getElementById(item.getItem().id)) { 202 | const board = document.getElementById(`${this._parent._id}_board`); 203 | const itemDiv = item.getItem(); 204 | board.appendChild(itemDiv); 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * Create a css class from an object 211 | */ 212 | addCSSClass(className, classDetails){ 213 | const style = document.createElement('style'); 214 | style.type = 'text/css'; 215 | style.innerHTML = `.${className} { ${this._parent.Mixins.convertToCSSString(classDetails)} }`; 216 | document.getElementsByTagName('head')[0].appendChild(style); 217 | this._classNames.add(className); 218 | } 219 | 220 | /** 221 | * Has the class been dynamicaly loaded? 222 | */ 223 | hasClass(className){ 224 | return this._classNames.has(className); 225 | } 226 | } -------------------------------------------------------------------------------- /src/component/Component.js: -------------------------------------------------------------------------------- 1 | const Mixins = require('../Mixins'); 2 | 3 | module.exports = class Component { 4 | constructor(config, parent){ 5 | this._parent = parent; 6 | this._id = config.id; 7 | this._x = config.x; 8 | this._y = config.y; 9 | this._type = 'component'; 10 | } 11 | 12 | /** 13 | * Initialise default values (css class defaults) 14 | */ 15 | init(){ 16 | this._defaultClass = { 17 | position: 'absolute', 18 | transform: 'translate(-50%, -50%)', 19 | cursor: 'pointer', 20 | padding: '0px', 21 | userSelect: 'none' 22 | } 23 | } 24 | 25 | /** 26 | * Return item as dom object 27 | */ 28 | getItem(){ 29 | const r = this._parent.Theme.getColor(this._color).r; 30 | const g = this._parent.Theme.getColor(this._color).g; 31 | const b = this._parent.Theme.getColor(this._color).b; 32 | const itemDiv = document.createElement("div"); 33 | const xy = this._parent.View.convertXY(this._x, this._y, 'board'); 34 | itemDiv.id = this._id; 35 | itemDiv.className = this._type; 36 | itemDiv.innerHTML = this._text; 37 | itemDiv.style.top = xy.y; 38 | itemDiv.style.left = xy.x; 39 | itemDiv.setAttribute('type', 'component'); 40 | this._parent.Mixins.applyStyle(itemDiv, this._defaultTheme, {r,g,b}); 41 | this._parent.Theme.applyTheme(itemDiv, 'letter', {r,g,b}); 42 | return itemDiv; 43 | } 44 | 45 | /** 46 | * Set the position of the item as and emit to socket (as if it was set by the current user) 47 | */ 48 | setLocalPosition(x, y){ 49 | this._x = x; 50 | this._y = y; 51 | console.log(`${this._type} ${this._id}: x(${Math.floor(x)}) y(${Math.floor(y)})`); 52 | this._parent.Socket._socket.emit('move', { 53 | id: this._id, 54 | x: this._x, 55 | y: this._y 56 | }) 57 | } 58 | 59 | /** 60 | * Set the position of the item and create pulse animation (as if it was set by a different user) 61 | */ 62 | setRemotePosition(x, y, time){ 63 | this._x = x; 64 | this._y = y; 65 | this.animateSetPosition(time); 66 | if(typeof this.moveAnimation === 'function') this.moveAnimation(); 67 | } 68 | 69 | /** 70 | * Animate the movement of the item 71 | */ 72 | animateSetPosition(time){ 73 | const xy = this._parent.View.convertXY(this._x, this._y, 'board'); 74 | const item = document.getElementById(this._id); 75 | 76 | this._parent.Mixins.makeAnimation(item, 'left', xy.x, time || 200); 77 | this._parent.Mixins.makeAnimation(item, 'top', xy.y, time || 200); 78 | 79 | // $('#'+this._id).animate({ 80 | // left: xy.x, 81 | // top: xy.y 82 | // }, time || 200); 83 | } 84 | } -------------------------------------------------------------------------------- /src/component/Letter.README.md: -------------------------------------------------------------------------------- 1 | Component: Letter 2 | ================= 3 | 4 | ``` 5 | Component type: 'letter' 6 | ``` 7 | 8 | ## Description 9 | 10 | The letter component displays a letter (or word) on the board. This is one of / the main component for magnetJS for the current time. 11 | 12 | ## Config (Array) 13 | 14 | ### text 15 | 16 | This defines the text to be displayed on the board 17 | 18 | ### color 19 | 20 | The theme id for the item (see Themes) 21 | 22 | ### x 23 | 24 | The x location of the item on the board (`0` for center). 25 | 26 | ### y 27 | 28 | The y location of the item on the board (`0` for center). 29 | 30 | ## Config (Spawn) 31 | 32 | ### text 33 | 34 | An array of the possible text to display on the board. 35 | 36 | ### color 37 | 38 | An array of potential theme ids for the items (see Themes) 39 | 40 | ### type 41 | 42 | The component type (in this case 'letter') -------------------------------------------------------------------------------- /src/component/Letter.js: -------------------------------------------------------------------------------- 1 | const Component = require('./Component.js') 2 | 3 | module.exports = class Letter extends Component{ 4 | constructor(config, parent){ 5 | super(config, parent); 6 | this._text = config.text; 7 | this._color = config.color; 8 | this._type = 'letter'; 9 | if(!this._parent.View.hasClass(this._type)) this.init(); 10 | this.initCustom(); 11 | } 12 | 13 | init(){ 14 | /** 15 | * Add parent default class information 16 | */ 17 | super.init(); 18 | /** 19 | * Insert custom default class information 20 | */ 21 | this._defaultClass = Object.assign({ 22 | fontStyle: 'normal', 23 | fontSize: '40px', 24 | lineHeight: '40px', 25 | color: 'rgb([r],[g],[b])', 26 | height: '40px', 27 | outline: '1px solid transparent' 28 | },this._defaultClass); 29 | /** 30 | * Dynamically insert class 31 | */ 32 | this._parent.View.addCSSClass(this._type, this._defaultClass); 33 | } 34 | 35 | initCustom(){ 36 | this._defaultTheme = { 37 | color: 'rgb([r],[g],[b])' 38 | }; 39 | } 40 | 41 | moveAnimation(){ 42 | const pulse = document.createElement('div'); 43 | const me = this; 44 | pulse.id = 'pulse_'+this._id; 45 | pulse.className = 'pulse'; 46 | pulse.style.borderColor = `rgb(${this._parent.Theme.getColor(this._color)})`; 47 | document.getElementById(this._id).appendChild(pulse); 48 | setTimeout(() => { document.getElementById(me._id).removeChild(pulse); },10000); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/component/index.js: -------------------------------------------------------------------------------- 1 | const Letter = require('./Letter.js') 2 | 3 | module.exports = { 4 | Letter 5 | } --------------------------------------------------------------------------------