├── .bowerrc ├── .gitignore ├── .travis.yml ├── FrontEnd.dia ├── FrontEnd.svg ├── Gruntfile.js ├── README.md ├── bower.json ├── client ├── app.js ├── assets │ └── images │ │ ├── ai │ │ ├── ai-cursors │ │ │ ├── albus-cursor.ai │ │ │ ├── arrow.ai │ │ │ ├── circle.ai │ │ │ ├── copy.ai │ │ │ ├── eraser.ai │ │ │ ├── magnify.ai │ │ │ ├── move.ai │ │ │ ├── pan.ai │ │ │ ├── rectangle.ai │ │ │ ├── strokeColor.ai │ │ │ └── text.ai │ │ ├── ai-favicon │ │ │ ├── favicon.png │ │ │ └── logo.ai │ │ ├── ai-icons │ │ │ ├── arrow.ai │ │ │ ├── circle.ai │ │ │ ├── color.ai │ │ │ ├── copy.ai │ │ │ ├── cursor-triangle.ai │ │ │ ├── draw.ai │ │ │ ├── eraser.ai │ │ │ ├── fill.ai │ │ │ ├── line.ai │ │ │ ├── magnify.ai │ │ │ ├── move.ai │ │ │ ├── pan.ai │ │ │ ├── path.ai │ │ │ ├── rectangle.ai │ │ │ ├── stroke.ai │ │ │ ├── text.ai │ │ │ ├── thickness.ai │ │ │ └── tool.ai │ │ └── ai-logo │ │ │ ├── Untitled-1.ai │ │ │ └── logo.ai │ │ ├── albus-screenshot-new.png │ │ ├── arrow.png │ │ ├── circle.png │ │ ├── color.png │ │ ├── color.svg │ │ ├── copy.png │ │ ├── cursors │ │ ├── arrow.png │ │ ├── circle.png │ │ ├── copy.png │ │ ├── eraser.png │ │ ├── fill.ai │ │ ├── fill.png │ │ ├── line.png │ │ ├── magnify.png │ │ ├── move.png │ │ ├── pan.png │ │ ├── path.png │ │ ├── rectangle.png │ │ ├── stroke.png │ │ ├── strokeColor.png │ │ └── text.png │ │ ├── draw.png │ │ ├── eraser.png │ │ ├── favicon.png │ │ ├── favicons │ │ ├── android-chrome-144x144.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-36x36.png │ │ ├── android-chrome-48x48.png │ │ ├── android-chrome-72x72.png │ │ ├── android-chrome-96x96.png │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── apple-touch-icon.png │ │ ├── browserconfig.xml │ │ ├── favicon-16x16.png │ │ ├── favicon-194x194.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ │ ├── fill.png │ │ ├── frontend-dataflow.png │ │ ├── line.png │ │ ├── logo.png │ │ ├── magnify.png │ │ ├── move.png │ │ ├── pan.png │ │ ├── path.png │ │ ├── rectangle.png │ │ ├── stroke.png │ │ ├── text.png │ │ ├── thickness.png │ │ └── tool.png ├── directives │ ├── board.js │ └── toolbar.js ├── dist │ └── style.min.css ├── index.html ├── js │ ├── raphael-min.js │ ├── raphael.js │ ├── resize-handler.js │ ├── resize-handler.min.js │ ├── socket.io.js │ └── socket.io.min.js ├── services │ ├── auth.js │ ├── board-data.js │ ├── broadcast.js │ ├── event-handler.js │ ├── input-handler.js │ ├── leap.js │ ├── receive.js │ ├── shape-builder.js │ ├── shape-editor.js │ ├── shape-manipulation.js │ ├── snap.js │ ├── sockets.js │ ├── token.js │ ├── visualizer.js │ └── zoom.js ├── styles │ └── style.css └── views │ ├── board.html │ ├── layers.html │ └── toolbar.html ├── deploy ├── package.json ├── server ├── board.js ├── db │ └── config.js ├── favicon.ico ├── rooms.js ├── server.js ├── sockets.js └── utils │ └── util.js └── test └── test.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "client/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # Bower 5 | bower_components 6 | client/lib 7 | 8 | sandbox.js 9 | dump.rdb 10 | /ssh 11 | .elasticbeanstalk 12 | Dockerfile 13 | 14 | *.log 15 | 16 | whiteboard.zip 17 | deploy 18 | 19 | # min 20 | client/dist/* 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | -------------------------------------------------------------------------------- /FrontEnd.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/FrontEnd.dia -------------------------------------------------------------------------------- /FrontEnd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Board 16 | Service 17 | 18 | 19 | 20 | 21 | 22 | Snaps 23 | 24 | 25 | 26 | 27 | 28 | 29 | paper obj 30 | 31 | 32 | 33 | 34 | 35 | 36 | canvas 37 | properties 38 | 39 | 40 | 41 | 42 | 43 | 44 | color 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | toolbar 56 | directive 57 | 58 | 59 | 60 | Ctrl 61 | 62 | 63 | 64 | 65 | 66 | 67 | Input Handler 68 | Service 69 | 70 | 71 | 72 | 73 | 74 | controller 75 | 76 | 77 | 78 | 79 | 80 | 81 | Mouse 82 | 83 | 84 | 85 | 86 | 87 | 88 | Key 89 | 90 | 91 | 92 | 93 | 94 | 95 | Touch 96 | 97 | 98 | 99 | 100 | 101 | 102 | link 103 | 104 | 105 | 106 | 107 | 108 | 109 | Board 110 | Directive 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Event Handler 119 | Service 120 | 121 | 122 | 123 | 124 | 125 | 126 | Socket Receiver 127 | Service 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | Server 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | Snap 217 | 218 | 219 | 220 | 221 | 222 | 223 | Create 224 | Shape 225 | 226 | 227 | 228 | 229 | 230 | 231 | Zoom 232 | 233 | 234 | 235 | 236 | 237 | 238 | ..... 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 4 | 5 | var pkg = grunt.file.readJSON('package.json'); 6 | 7 | grunt.initConfig({ 8 | pkg: pkg, 9 | 10 | concat: { 11 | options: { 12 | separator: ';' 13 | }, 14 | dist: { 15 | src: ['client/app.js', 'client/directives/*.js', 'client/services/*.js'], 16 | // the location of the resulting JS file 17 | dest: 'client/dist/whiteboard.js' 18 | } 19 | }, 20 | 21 | uglify: { 22 | options: { 23 | mangle: false, 24 | compress: true 25 | }, 26 | target: { 27 | files: { 28 | 'client/dist/whiteboard.min.js': ['client/dist/whiteboard.js'] 29 | } 30 | } 31 | }, 32 | 33 | cssmin: { 34 | options: { 35 | shorthandCompacting: false, 36 | roundingPrecision: -1 37 | }, 38 | target: { 39 | files: { 40 | 'client/dist/style.min.css': ['client/style/style.css'] 41 | } 42 | } 43 | }, 44 | 45 | watch: { 46 | scripts: { 47 | files: ['client/app.js', 'client/directives/*.js', 'client/services/*.js'], 48 | tasks: ['concat', 'uglify'] 49 | }, 50 | css: { 51 | files: ['client/styles/style.css'], 52 | tasks: ['cssmin'] 53 | } 54 | } 55 | 56 | }); 57 | 58 | grunt.registerTask('release', 'Concats, Minifies', [ 59 | 'concat', 60 | 'uglify' 61 | ]); 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Albus 2 | 3 | > Albus is a real-time collaborative whiteboard. Key features include: 4 | 5 | - suggested snapping points to shape corners and midpoints using k-d trees; 6 | - automatic path smoothing for freehand drawing; 7 | - path autofill upon closing; 8 | - infinite board panning and zooming; 9 | - shape moving, copying, and erasing; 10 | - color and fill customization; and 11 | - responsive marching menu 12 | 13 | 14 | 15 | 16 | Data flows onto the board in the following path: 17 | 18 | ![alt tag](https://raw.githubusercontent.com/QuixoticScientist/whiteboard/master/client/assets/images/frontend-dataflow.png) 19 | 20 | ## Team 21 | 22 | - __Product Owner__: Haley Bash 23 | - __Scrum Master__: Lorenzo De Nobili 24 | - __Development Team Members__: Christian Everett, Lorenzo De Nobili, Rory Sametz, Haley Bash 25 | 26 | ## Table of Contents 27 | 28 | 1. [Usage](#Usage) 29 | 1. [Technologies Used](#technologies-used) 30 | 1. [Requirements](#requirements) 31 | 1. [Development](#development) 32 | 1. [Installing Dependencies](#installing-dependencies) 33 | 1. [Tasks](#tasks) 34 | 1. [Contributing](#contributing) 35 | 36 | ## Usage 37 | 38 | Visit the page, currently hosted on [albus.io](http://albus.io) 39 | 40 | ## Technologies Used 41 | 42 | - [AngularJS](http://angularjs.org) 43 | - [Node](https://nodejs.org/) 44 | - [Express](http://expressjs.com/) 45 | - [Redis](http://redis.io/) 46 | - [Raphael](http://raphaeljs.com) 47 | - [Socket.io](http://socket.io/) 48 | - [Heroku Deployment](https://www.heroku.com/) 49 | 50 | ## Requirements 51 | 52 | - [Node 0.10.x](https://nodejs.org/en/download/) 53 | - [Redis](http://redis.io/download) 54 | 55 | ## Development Process 56 | 57 | ### Step 0: Fork and clone the repository from GitHub 58 | 59 | ### Step 1: Installing Dependencies 60 | 61 | Run the following in the command line, from within the repository: 62 | 63 | ```sh 64 | bower install 65 | npm install 66 | ``` 67 | 68 | ### Step 2: Running Locally 69 | 70 | Run the Redis database from the command line, in one tab: 71 | ```sh 72 | redis-server 73 | ``` 74 | 75 | Run the server in the other tab using node: 76 | 77 | ```sh 78 | npm run server 79 | ``` 80 | 81 | ### Step 3: Making Local Changes 82 | 83 | Each time a change is made, run the following to update the minified files: 84 | 85 | ```sh 86 | grunt release 87 | ``` 88 | 89 | ### Step 4: Issuing a Pull Request 90 | 91 | Feel free to add contributions by issuing a pull request to the dev branch of this repo. 92 | 93 | ### Visiting the server 94 | 95 | While node is running, visit the locally running server at [127.0.0.1:3000](127.0.0.1:3000) 96 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whiteboard", 3 | "description": "whiteboard", 4 | "main": "server/server.js", 5 | "authors": [ 6 | "Christian Everett " 7 | ], 8 | "license": "ISC", 9 | "homepage": "https://github.com/nahash411/whiteboard", 10 | "moduleType": [ 11 | "es6", 12 | "node" 13 | ], 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "client/lib", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "angular-route": "~1.4.8", 24 | "angular-socket-io": "~0.7.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard', [ 2 | 'btford.socket-io', 3 | 'whiteboard.services.receive', 4 | 'whiteboard.services.broadcast', 5 | 'whiteboard.services.shapebuilder', 6 | 'whiteboard.services.shapeeditor', 7 | 'whiteboard.services.shapemanipulation', 8 | 'whiteboard.services.snap', 9 | 'whiteboard.services.auth', 10 | 'whiteboard.services.token', 11 | 'whiteboard.services.sockets', 12 | 'whiteboard.services.boarddata', 13 | 'whiteboard.services.eventhandler', 14 | 'whiteboard.services.inputhandler', 15 | 'whiteboard.services.zoom', 16 | 'whiteboard.services.leapMotion', 17 | 'whiteboard.services.visualizer', 18 | 'ngRoute' 19 | ]) 20 | .config(['$routeProvider', '$locationProvider', '$httpProvider', 21 | function($routeProvider, $locationProvider, $httpProvider) { 22 | $routeProvider 23 | .when('/', { 24 | resolve: { 25 | 'something': function (Sockets, Auth, $location) { 26 | var roomId = Auth.generateRandomId(5); 27 | Sockets.emit('roomId', {roomId: roomId}); 28 | $location.path('/' + roomId); 29 | } 30 | } 31 | }) 32 | .when('/:id', { 33 | templateUrl: 'views/board.html', 34 | resolve: { 35 | 'somethingElse': function (Sockets, $location) { 36 | Sockets.emit('roomId', {roomId: $location.path().slice(1)}); 37 | } 38 | }, 39 | authenticate: true 40 | }); 41 | 42 | $locationProvider.html5Mode({ 43 | enabled: true, 44 | requireBase: false 45 | }); 46 | }]); 47 | // 48 | -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/albus-cursor.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/albus-cursor.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/arrow.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/arrow.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/circle.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/circle.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/copy.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/copy.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/eraser.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/eraser.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/magnify.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/magnify.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/move.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/move.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/pan.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/pan.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/rectangle.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/rectangle.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/strokeColor.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/strokeColor.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-cursors/text.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-cursors/text.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-favicon/favicon.png -------------------------------------------------------------------------------- /client/assets/images/ai/ai-favicon/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-favicon/logo.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/arrow.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/arrow.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/circle.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/circle.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/color.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/color.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/copy.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/copy.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/cursor-triangle.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/cursor-triangle.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/draw.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/draw.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/eraser.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/eraser.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/fill.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/fill.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/line.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/line.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/magnify.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/magnify.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/move.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/move.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/pan.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/pan.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/path.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/path.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/rectangle.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/rectangle.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/stroke.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/stroke.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/text.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/text.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/thickness.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/thickness.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-icons/tool.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-icons/tool.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-logo/Untitled-1.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-logo/Untitled-1.ai -------------------------------------------------------------------------------- /client/assets/images/ai/ai-logo/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/ai/ai-logo/logo.ai -------------------------------------------------------------------------------- /client/assets/images/albus-screenshot-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/albus-screenshot-new.png -------------------------------------------------------------------------------- /client/assets/images/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/arrow.png -------------------------------------------------------------------------------- /client/assets/images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/circle.png -------------------------------------------------------------------------------- /client/assets/images/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/color.png -------------------------------------------------------------------------------- /client/assets/images/color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 89 | 90 | color 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /client/assets/images/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/copy.png -------------------------------------------------------------------------------- /client/assets/images/cursors/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/arrow.png -------------------------------------------------------------------------------- /client/assets/images/cursors/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/circle.png -------------------------------------------------------------------------------- /client/assets/images/cursors/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/copy.png -------------------------------------------------------------------------------- /client/assets/images/cursors/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/eraser.png -------------------------------------------------------------------------------- /client/assets/images/cursors/fill.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/fill.ai -------------------------------------------------------------------------------- /client/assets/images/cursors/fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/fill.png -------------------------------------------------------------------------------- /client/assets/images/cursors/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/line.png -------------------------------------------------------------------------------- /client/assets/images/cursors/magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/magnify.png -------------------------------------------------------------------------------- /client/assets/images/cursors/move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/move.png -------------------------------------------------------------------------------- /client/assets/images/cursors/pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/pan.png -------------------------------------------------------------------------------- /client/assets/images/cursors/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/path.png -------------------------------------------------------------------------------- /client/assets/images/cursors/rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/rectangle.png -------------------------------------------------------------------------------- /client/assets/images/cursors/stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/stroke.png -------------------------------------------------------------------------------- /client/assets/images/cursors/strokeColor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/strokeColor.png -------------------------------------------------------------------------------- /client/assets/images/cursors/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/cursors/text.png -------------------------------------------------------------------------------- /client/assets/images/draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/draw.png -------------------------------------------------------------------------------- /client/assets/images/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/eraser.png -------------------------------------------------------------------------------- /client/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicon.png -------------------------------------------------------------------------------- /client/assets/images/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /client/assets/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/assets/images/favicons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/android-chrome-36x36.png -------------------------------------------------------------------------------- /client/assets/images/favicons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/android-chrome-48x48.png -------------------------------------------------------------------------------- /client/assets/images/favicons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/android-chrome-72x72.png -------------------------------------------------------------------------------- /client/assets/images/favicons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/android-chrome-96x96.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /client/assets/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /client/assets/images/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #da532c 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/assets/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /client/assets/images/favicons/favicon-194x194.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/favicon-194x194.png -------------------------------------------------------------------------------- /client/assets/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /client/assets/images/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /client/assets/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/favicon.ico -------------------------------------------------------------------------------- /client/assets/images/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Albus", 3 | "icons": [ 4 | { 5 | "src": "\/android-chrome-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": 0.75 9 | }, 10 | { 11 | "src": "\/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": 1 15 | }, 16 | { 17 | "src": "\/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": 1.5 21 | }, 22 | { 23 | "src": "\/android-chrome-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": 2 27 | }, 28 | { 29 | "src": "\/android-chrome-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": 3 33 | }, 34 | { 35 | "src": "\/android-chrome-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": 4 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /client/assets/images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /client/assets/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /client/assets/images/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /client/assets/images/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /client/assets/images/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /client/assets/images/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 25 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /client/assets/images/fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/fill.png -------------------------------------------------------------------------------- /client/assets/images/frontend-dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/frontend-dataflow.png -------------------------------------------------------------------------------- /client/assets/images/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/line.png -------------------------------------------------------------------------------- /client/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/logo.png -------------------------------------------------------------------------------- /client/assets/images/magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/magnify.png -------------------------------------------------------------------------------- /client/assets/images/move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/move.png -------------------------------------------------------------------------------- /client/assets/images/pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/pan.png -------------------------------------------------------------------------------- /client/assets/images/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/path.png -------------------------------------------------------------------------------- /client/assets/images/rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/rectangle.png -------------------------------------------------------------------------------- /client/assets/images/stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/stroke.png -------------------------------------------------------------------------------- /client/assets/images/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/text.png -------------------------------------------------------------------------------- /client/assets/images/thickness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/thickness.png -------------------------------------------------------------------------------- /client/assets/images/tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/assets/images/tool.png -------------------------------------------------------------------------------- /client/directives/board.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard') 2 | .directive('wbBoard', ['BoardData', 'Broadcast', 'Receive', 'LeapMotion', function (BoardData) { 3 | return { 4 | restrict: 'A', 5 | require: ['wbBoard'], 6 | replace: true, 7 | template: 8 | '
' + 9 | '
' + 10 | '
' + 11 | '
', 12 | controller: function (InputHandler) { 13 | this.handleEvent = function (ev) { 14 | InputHandler[ev.type](ev); 15 | } 16 | }, 17 | link: function (scope, element, attrs, ctrls) { 18 | var boardCtrl = ctrls[0]; 19 | BoardData.createBoard(element); 20 | BoardData.getCanvas().bind('mousedown mouseup mousemove dblclick', boardCtrl.handleEvent); 21 | 22 | $('body').on('keypress', function (ev) { 23 | boardCtrl.handleEvent(ev); 24 | }); 25 | 26 | scope.$on('setCursorClass', function (evt, msg) { 27 | // console.log('A') 28 | // var oldTool = BoardData.getCurrentTool(); 29 | var svg = BoardData.getCanvas(); 30 | 31 | // svg.addClass('A'); 32 | svg.attr("class", msg.tool); 33 | // console.log('> ', svg.attr("class").split(' ')); 34 | }); 35 | 36 | } 37 | } 38 | }]); 39 | -------------------------------------------------------------------------------- /client/directives/toolbar.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard') 2 | .directive('wbToolbar', ['BoardData', 'Zoom', function (BoardData, Zoom) { 3 | return { 4 | restrict: 'A', 5 | replace: true, 6 | templateUrl: 'views/toolbar.html', 7 | require: ['^wbBoard', 'wbToolbar'], 8 | // scope: { 9 | // wbToolSelect: '@', 10 | // wbZoomScale: '@', 11 | // wbColorSelect: '@' 12 | // }, 13 | controller: function ($scope) { 14 | 15 | var fill = [ 16 | '#e74c3c', 17 | '#e67e22', 18 | '#f1c40f', 19 | '#1abc9c', 20 | '#2ecc71', 21 | '#3498db', 22 | '#9b59b6', 23 | '#34495e', 24 | '#95a5a6', 25 | '#ecf0f1', 26 | ]; 27 | 28 | var stroke = [ 29 | '#c0392b', 30 | '#d35400', 31 | '#f39c12', 32 | '#16a085', 33 | '#27ae60', 34 | '#2980b9', 35 | '#8e44ad', 36 | '#2c3e50', 37 | '#7f8c8d', 38 | '#bdc3c7', 39 | ]; 40 | 41 | var thickness = [ 42 | '10', 43 | '9', 44 | '8', 45 | '7', 46 | '6', 47 | '5', 48 | '4', 49 | '3', 50 | '2', 51 | '1' 52 | ]; 53 | 54 | // $scope.colorIconSVG = 'data:image/svg+xml;utf8,'; 55 | 56 | $scope.menuStructure = [ 57 | ['Draw', ['Path', 'Line', 'Arrow', 'Rectangle', 'Circle', 'Text']], 58 | ['Tool', ['Magnify', 'Eraser', 'Pan', 'Move', 'Copy']], 59 | ['Color', [['Fill', fill], ['Stroke', stroke], ['Thickness', thickness]]] 60 | ]; 61 | 62 | 63 | 64 | $scope.$on('toggleAllSubmenu', function (ev, msg) { 65 | if (msg.action === 'hide') { 66 | // console.log('wbToolbar: closing all submenus') 67 | $scope.$broadcast('toggleSubmenu', msg) 68 | } 69 | }); 70 | 71 | $scope.$on('resetBackgrounds', function (ev, msg) { 72 | 73 | if (Array.isArray(msg.target)) { 74 | msg.target.forEach(function (target) { 75 | $scope.$broadcast('resetTargetBackground', {target: target}); 76 | }) 77 | } else { 78 | $scope.$broadcast('resetTargetBackground', msg); 79 | } 80 | }) 81 | 82 | }, 83 | link: function (scope, element, attrs, ctrls) { 84 | 85 | // var $colorIcon = element.find('.icon-color'); 86 | 87 | 88 | scope.$on('activateMenu', function (event, action) { 89 | // console.log(event, options); 90 | if (action === 'show') { 91 | element.addClass('show'); 92 | scope.$broadcast('toggleMouseEv', action); 93 | } else { 94 | // scope.$broadcast('test', 'hide'); 95 | element.removeClass('show'); 96 | scope.$broadcast('toggleMouseEv', action); 97 | } 98 | }); 99 | 100 | // scope.$on('changeIconColors', function (event, action) { 101 | // console.log(scope.colorIconSVG) 102 | // console.log(action) 103 | // if (action.type === 'fill') { 104 | // // $colorIcon.find('small-circle').attr({'class': 'small-circle fill-' + action.color.substr(1)}) 105 | // $colorIcon.css({'background-image': 'url(' + scope.colorIconSVG + ')'}); 106 | // } else { 107 | // $colorIcon.css({'background-image': 'url(' + scope.colorIconSVG + ')'}); 108 | // // $colorIcon.find('outer-circle').attr({'class': 'outer-circle stroke-' + action.color.substr(1)}) 109 | // } 110 | // }); 111 | 112 | } 113 | }; 114 | }]) 115 | .directive('wbMenuOpener', function () { 116 | return { 117 | restrict: 'C', 118 | replace: false, 119 | require: 'wbMenuOpener', 120 | scope: false, 121 | controller: function ($scope) { 122 | 123 | this.menuHandler = function (attr) { 124 | $scope.$emit('activateMenu', attr); 125 | } 126 | 127 | }, 128 | link: function (scope, element, attrs, ctrl) { 129 | 130 | element.bind('mouseover mouseleave', function (ev) { 131 | if (ev.buttons === 0 && ev.type === 'mouseover' && (angular.element(ev.relatedTarget).is('svg') || angular.element(ev.relatedTarget)[0].raphael)) { 132 | // console.log(angular.element(ev.relatedTarget).is('svg')) 133 | // console.log('add class show'); 134 | // console.log(ev.buttons) 135 | ctrl.menuHandler('show'); 136 | // element.addClass('show'); 137 | } else { 138 | // console.log('remove class show'); 139 | 140 | // ctrl.menuHandler('hide'); 141 | 142 | } 143 | 144 | }); 145 | } 146 | }; 147 | }) 148 | .directive('wbSubmenuOpener', function () { 149 | return { 150 | restrict: 'C', 151 | replace: false, 152 | require: 'wbSubmenuOpener', 153 | controller: function ($scope) { 154 | 155 | this.submenuOpener = function (action) { 156 | //if (action.level === 2) { 157 | this.submenuCloser({action: 'hide', level: action.level}); 158 | //} 159 | $scope.$broadcast('toggleSubmenu', action); 160 | } 161 | 162 | this.submenuCloser = function (action) { 163 | // console.log('close?') 164 | $scope.$emit('toggleAllSubmenu', action); 165 | } 166 | 167 | }, 168 | link: function (scope, element, attrs, submenuOpenerCtrl) { 169 | 170 | var bindMouseEv = function () { 171 | element.bind('mouseover mouseleave', function (ev) { 172 | // console.log(ev, attrs.wbLevel) 173 | if (ev.type === 'mouseover' && attrs.wbLevel === '2') { 174 | // console.log('Should open submenu', ev); 175 | submenuOpenerCtrl.submenuOpener({action: 'show', level: '2'}); 176 | } else if (ev.type === 'mouseover' && attrs.wbLevel === '3') { 177 | // console.log('Should open the color palette!') 178 | // console.log('Should open third level') 179 | submenuOpenerCtrl.submenuOpener({action: 'show', level: '3'}); 180 | } else if (ev.type === 'mouseleave' && (angular.element(ev.toElement).hasClass('lvl1') || angular.element(ev.toElement).hasClass('level-one'))) { 181 | // console.log('Should close submenu'); 182 | submenuOpenerCtrl.submenuCloser({action: 'hide', level: '2'}); 183 | } else if (ev.type === 'mouseleave' && (angular.element(ev.toElement).hasClass('level-three') || angular.element(ev.toElement).hasClass('lvl2'))) { 184 | // console.log('close level three') 185 | submenuOpenerCtrl.submenuCloser({action: 'hide', level: '3'}); 186 | } else if (ev.type === 'mouseleave' && angular.element(ev.toElement).hasClass('wb-submenu-opener')) { 187 | // console.log('Here is where i broke D:'); 188 | // console.log(ev) 189 | submenuOpenerCtrl.submenuCloser({action: 'hide', level: attrs.wbLevel}); 190 | } 191 | }); 192 | }; 193 | 194 | var unbindMouseEv = function () { 195 | // console.log('EVENTS BOUND: ', jQuery._data(element, 'events')); 196 | element.unbind('mouseover mouseleave'); 197 | submenuOpenerCtrl.submenuCloser({action: 'hide', level: 'all'}); 198 | } 199 | 200 | scope.$on('toggleMouseEv', function (event, action) { 201 | // console.log('ACTION: ', action) 202 | if (action === 'show') { 203 | element.addClass('show'); 204 | bindMouseEv(); 205 | } else { 206 | element.removeClass('show'); 207 | unbindMouseEv(); 208 | } 209 | }) 210 | 211 | } 212 | }; 213 | }) 214 | .directive('wbSubmenu', function () { 215 | return { 216 | restrict: 'C', 217 | replace: false, 218 | controller: function () { 219 | 220 | }, 221 | link: function (scope, element, attrs, ctrl) { 222 | 223 | if (attrs.wbLevel === 3) { 224 | // console.log('Sono qui?') 225 | } else { 226 | scope.$on('toggleSubmenu', function (event, msg) { 227 | // console.log(msg, attrs.wbLevel); 228 | if (msg.action === 'show') { 229 | if (msg.level === attrs.wbLevel) { 230 | element.addClass('show'); 231 | } 232 | } else { 233 | if (msg.level === attrs.wbLevel) { 234 | // console.log('DIE BASTARD') 235 | element.removeClass('show'); 236 | } else if (msg.level === 'all') { 237 | element.removeClass('show'); 238 | } 239 | } 240 | }); 241 | } 242 | } 243 | }; 244 | }) 245 | .directive('wbSubmenuItems', function () { 246 | return { 247 | restrict: 'C', 248 | replace: false, 249 | require: 'wbSubmenuItems', 250 | controller: function ($scope, BoardData) { 251 | 252 | $scope.setAttributeTool = function (toolName) { 253 | if (typeof toolName === 'string') { 254 | return toolName.toLowerCase(); 255 | } 256 | return toolName[0]; 257 | } 258 | 259 | this.setTool = function (toolName) { 260 | BoardData.setCurrentToolName(toolName); 261 | } 262 | 263 | this.setColors = function (type, color) { 264 | if (type === 'fill') { 265 | BoardData.setColors(color, null); 266 | } else { 267 | BoardData.setColors(null, color); 268 | } 269 | } 270 | 271 | this.setThickness = function (thickness) { 272 | BoardData.setStrokeWidth(thickness); 273 | } 274 | 275 | }, 276 | link: function (scope, element, attrs, submenuItemsCtrl) { 277 | 278 | var updateIconColors = function (type, color) { 279 | scope.$emit('changeIconColors', {type: type, color: color}); 280 | }; 281 | 282 | element.bind('mouseover', function (ev) { 283 | //ev.stopPropagation(); 284 | // console.log(attrs.wbTool) 285 | }) 286 | 287 | element.bind('mouseleave', function (ev) { 288 | ev.stopPropagation(); 289 | // console.log(angular.element(ev.currentTarget).hasClass('level-two-items')); 290 | // console.log('!!!!!!!!!!!!!!!!!', attrs.wbTool, ev); 291 | // if (angular.element(ev.currentTarget).hasClass('level-two-items')) { return; } 292 | if (attrs.wbColor && (angular.element(ev.relatedTarget).is('svg') || angular.element(ev.relatedTarget)[0].raphael)) { 293 | // console.log('A') 294 | submenuItemsCtrl.setColors(attrs.wbColorType, attrs.wbColor); 295 | // updateIconColors(attrs.wbColorType, attrs.wbColor); 296 | scope.$emit('activateMenu', 'hide'); 297 | } else if (attrs.wbThickness && (angular.element(ev.relatedTarget).is('svg') || angular.element(ev.relatedTarget)[0].raphael)) { 298 | // console.log('SET THICKNESS') 299 | submenuItemsCtrl.setThickness(attrs.wbThickness); 300 | scope.$emit('activateMenu', 'hide'); 301 | } else if (attrs.wbTool && (angular.element(ev.relatedTarget).is('svg') || angular.element(ev.relatedTarget)[0].raphael)) { 302 | // console.log('b') 303 | scope.$emit('setCursorClass', {tool: attrs.wbTool}); 304 | submenuItemsCtrl.setTool(attrs.wbTool); 305 | scope.$emit('activateMenu', 'hide'); 306 | } else if (angular.element(ev.relatedTarget).hasClass('menu') || angular.element(ev.relatedTarget).hasClass('icon')) { 307 | // console.log(ev) 308 | scope.$emit('toggleAllSubmenu', {action: 'hide', level: '3'}); 309 | } 310 | // console.log(angular.element(ev.relatedTarget).is('svg')) 311 | }) 312 | } 313 | }; 314 | }) 315 | .directive('wbMenuOverHandler', function () { 316 | return { 317 | restrict: 'A', 318 | replace: false, 319 | require: 'wbMenuOverHandler', 320 | controllerAs: 'menuOver', 321 | controller: function ($scope) { 322 | var elemWidth; 323 | // var elemLeftOffset; 324 | 325 | // this.storeElemLeftOffset = function (leftOffset) { 326 | // elemLeftOffset = leftOffset; 327 | // }; 328 | 329 | // this.getElemLeftOffset = function () { 330 | // return elemLeftOffset; 331 | // }; 332 | 333 | this.storeElemWidth = function (width) { 334 | elemWidth = width; 335 | }; 336 | 337 | this.getElemWidth = function () { 338 | return elemWidth; 339 | }; 340 | 341 | this.calcBg = function (mouseX, leftOffset) { 342 | var width = this.getElemWidth(); 343 | 344 | //100 : elemWidth = x : mouseX 345 | var bgSizes = {}; 346 | bgSizes.overed = (mouseX - leftOffset) * 100 / this.getElemWidth(); 347 | // bgSizes.free = 100 - bgSizes.overed; 348 | // bgSizes.free = 0; 349 | 350 | return bgSizes; 351 | }; 352 | 353 | this.hexToRGBA = function (hex, opacity) { 354 | opacity = opacity || 90; 355 | 356 | hex = hex.replace('#',''); 357 | r = parseInt(hex.substring(0,2), 16); 358 | g = parseInt(hex.substring(2,4), 16); 359 | b = parseInt(hex.substring(4,6), 16); 360 | 361 | result = 'rgba('+r+','+g+','+b+','+opacity/100+')'; 362 | return result; 363 | } 364 | 365 | }, 366 | link: function (scope, element, attrs, ctrl) { 367 | // {background: linear-gradient(90deg, rgba(53,53,53,0.99) {{overed}}%, rgba(53,53,53,0.89) free%)} 368 | 369 | if (ctrl.getElemWidth() === undefined) { 370 | ctrl.storeElemWidth(element.width()) 371 | } 372 | 373 | // console.log(element.offset()) 374 | // ctrl.storeElemWidth(element.offset().left); 375 | 376 | var setBg = function (el, sizes) { 377 | // console.log(sizes.overed, el.offset().left) 378 | el.css({'background': 'linear-gradient(90deg, rgba(177,102,24,0.96) ' + (sizes.overed) + '%, rgba(53,53,53,0.93) 0%)'}) 379 | } 380 | 381 | var setColorBg = function (el, color, sizes) { 382 | // console.log(sizes.overed, el.offset().left) 383 | var rgbaOver = ctrl.hexToRGBA(color, 100); 384 | var rgbaFree = ctrl.hexToRGBA(color, 90); 385 | el.css({'background': 'linear-gradient(90deg, ' + rgbaOver + ' ' + (sizes.overed) + '%, ' + rgbaFree + ' 0%)'}) 386 | } 387 | 388 | element.bind('mouseover', function (ev) { 389 | ev.stopPropagation(); 390 | // console.log(ev); 391 | // console.log(ev.currentTarget) 392 | if (angular.element(ev.currentTarget).hasClass('level-two-items')) { 393 | var $levelOne = angular.element(ev.currentTarget).parents('.level-one') 394 | // console.log($levelOne) 395 | scope.$emit('resetBackgrounds', {target: 'level-two-items'}); 396 | setBg($levelOne, {overed: 100}); 397 | } else if (angular.element(ev.currentTarget).hasClass('level-one')) { 398 | scope.$emit('resetBackgrounds', {target: 'level-two-items'}); 399 | } else if (angular.element(ev.currentTarget).hasClass('color-palette')) { 400 | // console.log('A') 401 | var $levelTwo = angular.element(ev.currentTarget).parents('.level-two-items') 402 | setBg($levelTwo, {overed: 100}); 403 | scope.$emit('resetBackgrounds', {target: 'level-three-items'}); 404 | } else if (angular.element(ev.currentTarget).hasClass('thickness')) { 405 | // console.log('A') 406 | var $levelTwo = angular.element(ev.currentTarget).parents('.level-two-items'); 407 | // console.log($levelTwo) 408 | setBg($levelTwo, {overed: 100}); 409 | scope.$emit('resetBackgrounds', {target: 'level-three-items'}); 410 | } 411 | 412 | 413 | }); 414 | 415 | element.bind('mousemove', function (ev) { 416 | ev.stopPropagation(); 417 | 418 | var $el = angular.element(ev.currentTarget); 419 | // console.log('ev'); 420 | if ($el.hasClass('level-one') || $el.hasClass('level-two-items') || $el.hasClass('thickness')) { 421 | // console.log('over level one'); 422 | var bgSizes = ctrl.calcBg(ev.clientX, $el.offset().left); 423 | setBg($el, bgSizes); 424 | 425 | } else if ($el.hasClass('color-palette')) { 426 | var bgSizes = ctrl.calcBg(ev.clientX, $el.offset().left); 427 | setColorBg($el, scope.color, bgSizes); 428 | 429 | } 430 | }); 431 | 432 | element.bind('mouseleave', function (ev) { 433 | 434 | var $elTarget = angular.element(ev.currentTarget); 435 | var $elToElement = angular.element(ev.toElement); 436 | 437 | // console.log($elTarget) 438 | if ($elTarget.hasClass('level-two-items')) { 439 | // console.log(ev) 440 | if ($elToElement.is('svg') || angular.element(ev.relatedTarget)[0].raphael) { 441 | // console.log(1) 442 | scope.$emit('resetBackgrounds', {target: 'all'}); 443 | } else if ($elToElement.hasClass('wb-submenu-opener')) { 444 | // console.log(2) 445 | scope.$emit('resetBackgrounds', {target: 'all'}); 446 | } else { 447 | // console.log(3) 448 | scope.$emit('resetBackgrounds', {target: 'level-two-items'}); 449 | } 450 | } else if ($elTarget.hasClass('level-one')) { 451 | // console.log('reset!') 452 | scope.$emit('resetBackgrounds', {target: 'level-one'}); 453 | } else if ($elTarget.hasClass('color-palette') || $elTarget.hasClass('thickness')) { 454 | if ($elToElement.is('svg') || angular.element(ev.relatedTarget)[0].raphael) { 455 | scope.$emit('resetBackgrounds', {target: 'all'}); 456 | } else if ($elToElement.hasClass('wb-submenu-opener') || $elToElement.hasClass('level-three-items')) { 457 | // scope.$emit('resetBackgrounds', {target: 'all'}); 458 | scope.$emit('resetBackgrounds', {target: ['color-palette', 'thickness']}); 459 | } 460 | } 461 | }) 462 | 463 | scope.$on('resetTargetBackground', function (ev, msg) { 464 | // console.log('should reset color', element) 465 | if (msg.target === 'all') { 466 | // console.log('- ', element) 467 | element.hasClass('color-palette') ? setColorBg(element, scope.color, {overed: 0}) : setBg(element, {overed: 0}); 468 | } else if (element.hasClass('color-palette') && (msg.target === 'level-three-items' || msg.target === 'color-palette')) { 469 | // console.log(scope.color) 470 | setColorBg(element, scope.color, {overed: 0}); 471 | } else if (element.hasClass('thickness') && (msg.target === 'level-three-items' || msg.target === 'thickness')) { 472 | setBg(element, {overed: 0}); 473 | } else if (element.hasClass(msg.target)) { 474 | // console.log('here', msg.target) 475 | setBg(element, {overed: 0}); 476 | } 477 | 478 | }) 479 | 480 | } 481 | }; 482 | }) 483 | -------------------------------------------------------------------------------- /client/dist/style.min.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/client/dist/style.min.css -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Albus 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

31 | 32 |
33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /client/js/resize-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/pesla/ResizeSensor 3 | * @author Peter Slagter 4 | * @license MIT 5 | * @description Based on https://github.com/marcj/css-element-queries 6 | * @preserve 7 | */ 8 | 9 | /** 10 | * @returns {ResizeSensor} 11 | */ 12 | var ResizeSensor = (function () { 13 | 'use strict'; 14 | 15 | /** ----- Feature tests and polyfills ----- */ 16 | 17 | /** {array} */ 18 | var unsuitableElements = ['IMG', 'COL', 'TR', 'THEAD', 'TFOOT']; 19 | /** {boolean} */ 20 | var supportsAttachEvent = ('attachEvent' in document); 21 | 22 | if (!supportsAttachEvent) { 23 | var browserSupportsCSSAnimations = isCSSAnimationSupported(); 24 | var animationPropertiesForBrowser = (browserSupportsCSSAnimations) ? getAnimationPropertiesForBrowser() : {}; 25 | insertResizeSensorStyles(); 26 | 27 | if (!('requestAnimationFrame' in window) || !('cancelAnimationFrame' in window)) { 28 | polyfillRAF(); 29 | } 30 | } 31 | 32 | /** ----- ResizeSensor ----- */ 33 | 34 | /** 35 | * @param {HTMLElement} targetElement 36 | * @param {Function} callback 37 | * @constructor 38 | */ 39 | var ResizeSensor = function (targetElement, callback) { 40 | if (isUnsuitableElement(targetElement)) { 41 | console && console.error("Given element isn't suitable to act as a resize sensor. Try wrapping it with one that is. Unsuitable elements are:", unsuitableElements); 42 | return; 43 | } 44 | 45 | /** @var {HTMLElement} */ 46 | this.targetElement = targetElement; 47 | /** @var {Function} */ 48 | this.callback = callback; 49 | /** @var {{width: int, height: int}} */ 50 | this.dimensions = { 51 | width: 0, 52 | height: 0 53 | }; 54 | 55 | if (supportsAttachEvent) { 56 | this.boundOnResizeHandler = this.onElementResize.bind(this); 57 | this.targetElement.attachEvent('onresize', this.boundOnResizeHandler); 58 | return; 59 | } 60 | 61 | /** @var {{container: HTMLElement, expand: HTMLElement, expandChild: HTMLElement, contract: HTMLElement}} */ 62 | this.triggerElements = {}; 63 | /** @var {int} */ 64 | this.resizeRAF = 0; 65 | 66 | this.setup(); 67 | }; 68 | 69 | ResizeSensor.prototype.setup = function () { 70 | // Make sure the target element is "positioned" 71 | forcePositionedBox(this.targetElement); 72 | 73 | // Create and append resize trigger elements 74 | this.insertResizeTriggerElements(); 75 | 76 | // Start listening to events 77 | this.boundScrollListener = this.handleElementScroll.bind(this); 78 | this.targetElement.addEventListener('scroll', this.boundScrollListener, true); 79 | 80 | if (browserSupportsCSSAnimations) { 81 | this.boundAnimationStartListener = this.resetTriggersOnAnimationStart.bind(this); 82 | this.triggerElements.container.addEventListener(animationPropertiesForBrowser.animationStartEvent, this.boundAnimationStartListener); 83 | } 84 | 85 | // Initial value reset of all triggers 86 | this.resetTriggers(); 87 | }; 88 | 89 | ResizeSensor.prototype.insertResizeTriggerElements = function () { 90 | var resizeTrigger = document.createElement('div'); 91 | var expandTrigger = document.createElement('div'); 92 | var expandTriggerChild = document.createElement('div'); 93 | var contractTrigger = document.createElement('div'); 94 | 95 | resizeTrigger.className = 'ResizeSensor ResizeSensor__resizeTriggers'; 96 | expandTrigger.className = 'ResizeSensor__expandTrigger'; 97 | contractTrigger.className = 'ResizeSensor__contractTrigger'; 98 | 99 | expandTrigger.appendChild(expandTriggerChild); 100 | resizeTrigger.appendChild(expandTrigger); 101 | resizeTrigger.appendChild(contractTrigger); 102 | 103 | this.triggerElements.container = resizeTrigger; 104 | this.triggerElements.expand = expandTrigger; 105 | this.triggerElements.expandChild = expandTriggerChild; 106 | this.triggerElements.contract = contractTrigger; 107 | 108 | this.targetElement.appendChild(resizeTrigger); 109 | }; 110 | 111 | ResizeSensor.prototype.onElementResize = function () { 112 | var currentDimensions = this.getDimensions(); 113 | 114 | if (this.isResized(currentDimensions)) { 115 | this.dimensions.width = currentDimensions.width; 116 | this.dimensions.height = currentDimensions.height; 117 | this.elementResized(); 118 | } 119 | }; 120 | 121 | ResizeSensor.prototype.handleElementScroll = function () { 122 | var _this = this; 123 | 124 | this.resetTriggers(); 125 | 126 | if (this.resizeRAF) { 127 | window.cancelAnimationFrame(this.resizeRAF); 128 | } 129 | 130 | this.resizeRAF = window.requestAnimationFrame(function () { 131 | var currentDimensions = _this.getDimensions(); 132 | if (_this.isResized(currentDimensions)) { 133 | _this.dimensions.width = currentDimensions.width; 134 | _this.dimensions.height = currentDimensions.height; 135 | _this.elementResized(); 136 | } 137 | }); 138 | }; 139 | 140 | /** 141 | * @param {{width: number, height: number}} currentDimensions 142 | * @returns {boolean} 143 | */ 144 | ResizeSensor.prototype.isResized = function (currentDimensions) { 145 | return (currentDimensions.width !== this.dimensions.width || currentDimensions.height !== this.dimensions.height) 146 | }; 147 | 148 | /** 149 | * @returns {{width: number, height: number}} 150 | */ 151 | ResizeSensor.prototype.getDimensions = function () { 152 | return { 153 | width: this.targetElement.offsetWidth, 154 | height: this.targetElement.offsetHeight 155 | }; 156 | }; 157 | 158 | /** 159 | * @param {Event} event 160 | */ 161 | ResizeSensor.prototype.resetTriggersOnAnimationStart = function (event) { 162 | if (event.animationName === animationPropertiesForBrowser.animationName) { 163 | this.resetTriggers(); 164 | } 165 | }; 166 | 167 | ResizeSensor.prototype.resetTriggers = function () { 168 | this.triggerElements.contract.scrollLeft = this.triggerElements.contract.scrollWidth; 169 | this.triggerElements.contract.scrollTop = this.triggerElements.contract.scrollHeight; 170 | this.triggerElements.expandChild.style.width = this.triggerElements.expand.offsetWidth + 1 + 'px'; 171 | this.triggerElements.expandChild.style.height = this.triggerElements.expand.offsetHeight + 1 + 'px'; 172 | this.triggerElements.expand.scrollLeft = this.triggerElements.expand.scrollWidth; 173 | this.triggerElements.expand.scrollTop = this.triggerElements.expand.scrollHeight; 174 | }; 175 | 176 | ResizeSensor.prototype.elementResized = function () { 177 | this.callback(this.dimensions); 178 | }; 179 | 180 | ResizeSensor.prototype.destroy = function () { 181 | this.removeEventListeners(); 182 | this.targetElement.removeChild(this.triggerElements.container); 183 | delete this.boundAnimationStartListener; 184 | delete this.boundScrollListener; 185 | delete this.callback; 186 | delete this.targetElement; 187 | }; 188 | 189 | ResizeSensor.prototype.removeEventListeners = function () { 190 | if (supportsAttachEvent) { 191 | this.targetElement.detachEvent('onresize', this.boundOnResizeHandler); 192 | } 193 | 194 | this.triggerElements.container.removeEventListener(animationPropertiesForBrowser.animationStartEvent, this.boundAnimationStartListener); 195 | this.targetElement.removeEventListener('scroll', this.boundScrollListener, true); 196 | }; 197 | 198 | /** ----- Various helper functions ----- */ 199 | 200 | /** 201 | * An element is said to be positioned if its 'position' property has a value other than 'static' 202 | * @see http://www.w3.org/TR/CSS2/visuren.html#propdef-position 203 | * @param {HTMLElement} element 204 | */ 205 | function forcePositionedBox (element) { 206 | var position = getStyle(element, 'position'); 207 | 208 | if (position === 'static') { 209 | element.style.position = 'relative'; 210 | } 211 | } 212 | 213 | /** 214 | * @returns {boolean} 215 | */ 216 | function isCSSAnimationSupported () { 217 | var testElement = document.createElement('div'); 218 | var isAnimationSupported = ('animationName' in testElement.style); 219 | 220 | if (isAnimationSupported) { 221 | return true; 222 | } 223 | 224 | var browserPrefixes = 'Webkit Moz O ms'.split(' '); 225 | var i = 0; 226 | var l = browserPrefixes.length; 227 | 228 | for (; i < l; i++) { 229 | if ((browserPrefixes[i] + 'AnimationName') in testElement.style) { 230 | return true; 231 | } 232 | } 233 | 234 | return false; 235 | } 236 | 237 | /** 238 | * @param {HTMLElement} targetElement 239 | * @returns {boolean} 240 | */ 241 | function isUnsuitableElement (targetElement) { 242 | var tagName = targetElement.tagName.toUpperCase(); 243 | return (unsuitableElements.indexOf(tagName) > -1); 244 | } 245 | 246 | /** 247 | * Determines which style convention (properties) to follow 248 | * @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Using_CSS_animations/Detecting_CSS_animation_support 249 | * @returns {{keyframesRule: string, styleDeclaration: string, animationStartEvent: string, animationName: string}} 250 | */ 251 | function getAnimationPropertiesForBrowser () { 252 | var testElement = document.createElement('div'); 253 | var supportsUnprefixedAnimationProperties = ('animationName' in testElement.style); 254 | 255 | // Unprefixed animation properties 256 | var animationStartEvent = 'animationstart'; 257 | var animationName = 'resizeanim'; 258 | 259 | if (supportsUnprefixedAnimationProperties) { 260 | return { 261 | keyframesRule: '@keyframes ' + animationName + ' {from { opacity: 0; } to { opacity: 0; }}', 262 | styleDeclaration: 'animation: 1ms ' + animationName + ';', 263 | animationStartEvent: animationStartEvent, 264 | animationName: animationName 265 | }; 266 | } 267 | 268 | // Browser specific animation properties 269 | var keyframePrefix = ''; 270 | var browserPrefixes = 'Webkit Moz O ms'.split(' '); 271 | var startEvents = 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split(' '); 272 | 273 | var i; 274 | var l = browserPrefixes.length; 275 | 276 | for (i = 0; i < l ; i++) { 277 | if ((browserPrefixes[i] + 'AnimationName') in testElement.style) { 278 | keyframePrefix = '-' + browserPrefixes[i].toLowerCase() + '-'; 279 | animationStartEvent = startEvents[i]; 280 | break; 281 | } 282 | } 283 | 284 | return { 285 | keyframesRule: '@' + keyframePrefix + 'keyframes ' + animationName + ' {from { opacity: 0; } to { opacity: 0; }}', 286 | styleDeclaration: keyframePrefix + 'animation: 1ms ' + animationName + ';', 287 | animationStartEvent: animationStartEvent, 288 | animationName: animationName 289 | }; 290 | } 291 | 292 | /** 293 | * Provides requestAnimationFrame in a cross browser way 294 | * @see https://gist.github.com/mrdoob/838785 295 | */ 296 | function polyfillRAF () { 297 | if (!window.requestAnimationFrame) { 298 | window.requestAnimationFrame = (function () { 299 | return window.webkitRequestAnimationFrame || 300 | window.mozRequestAnimationFrame || 301 | window.oRequestAnimationFrame || 302 | window.msRequestAnimationFrame || 303 | function (callback) { 304 | window.setTimeout(callback, 1000 / 60); 305 | }; 306 | })(); 307 | } 308 | 309 | if (!window.cancelAnimationFrame) { 310 | window.cancelAnimationFrame = (function () { 311 | return window.webkitCancelAnimationFrame || 312 | window.mozCancelAnimationFrame || 313 | window.oCancelAnimationFrame || 314 | window.msCancelAnimationFrame || 315 | window.clearTimeout; 316 | })(); 317 | } 318 | } 319 | 320 | /** 321 | * Adds a style block that contains CSS essential for detecting resize events 322 | */ 323 | function insertResizeSensorStyles () { 324 | var css = [ 325 | (animationPropertiesForBrowser.keyframesRule) ? animationPropertiesForBrowser.keyframesRule : '', 326 | '.ResizeSensor__resizeTriggers { ' + ((animationPropertiesForBrowser.styleDeclaration) ? animationPropertiesForBrowser.styleDeclaration : '') + ' visibility: hidden; opacity: 0; }', 327 | '.ResizeSensor__resizeTriggers, .ResizeSensor__resizeTriggers > div, .ResizeSensor__contractTrigger:before { content: \' \'; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; } .ResizeSensor__resizeTriggers > div { background: #eee; overflow: auto; } .ResizeSensor__contractTrigger:before { width: 200%; height: 200%; }' 328 | ]; 329 | 330 | css = css.join(' '); 331 | 332 | var headElem = document.head || document.getElementsByTagName('head')[0]; 333 | 334 | var styleElem = document.createElement('style'); 335 | styleElem.type = 'text/css'; 336 | 337 | if (styleElem.styleSheet) { 338 | styleElem.styleSheet.cssText = css; 339 | } else { 340 | styleElem.appendChild(document.createTextNode(css)); 341 | } 342 | 343 | headElem.appendChild(styleElem); 344 | } 345 | 346 | /** 347 | * 348 | * @param {HTMLElement} element 349 | * @param {string} property 350 | * @returns {null|string} 351 | */ 352 | function getStyle (element, property) { 353 | var value = null; 354 | 355 | if (element.currentStyle) { 356 | value = element.currentStyle[property]; 357 | } else if (window.getComputedStyle) { 358 | value = document.defaultView.getComputedStyle(element, null).getPropertyValue(property); 359 | } 360 | 361 | return value; 362 | } 363 | 364 | return ResizeSensor; 365 | })(); 366 | 367 | var ResizeSensorApi = (function (ResizeSensor) { 368 | //'use strict'; 369 | 370 | /** {{}} Map of all resize sensors (id => ResizeSensor) */ 371 | var allResizeSensors = {}; 372 | 373 | /** 374 | * @constructor 375 | */ 376 | var ResizeSensorApi = function () {}; 377 | 378 | /** 379 | * @param {HTMLElement} targetElement 380 | * @param {Function} callback 381 | * @returns {ResizeSensor} 382 | */ 383 | ResizeSensorApi.prototype.create = function (targetElement, callback) { 384 | var sensorId = this.getSensorId(targetElement); 385 | 386 | if (allResizeSensors[sensorId]) { 387 | return allResizeSensors[sensorId]; 388 | } 389 | 390 | var Instance = new ResizeSensor(targetElement, callback); 391 | allResizeSensors[sensorId] = Instance; 392 | return Instance; 393 | }; 394 | 395 | /** 396 | * @param {HTMLElement} targetElement 397 | */ 398 | ResizeSensorApi.prototype.destroy = function (targetElement) { 399 | var sensorId = this.getSensorId(targetElement); 400 | 401 | /** @var ResizeSensor */ 402 | var Sensor = allResizeSensors[sensorId]; 403 | 404 | if (!Sensor) { 405 | console && console.error("Can't destroy ResizeSensor (404 not found).", targetElement); 406 | } 407 | 408 | Sensor.destroy(); 409 | delete allResizeSensors[sensorId]; 410 | }; 411 | 412 | /** 413 | * @param {HTMLElement} targetElement 414 | * @returns {string} 415 | */ 416 | ResizeSensorApi.prototype.getSensorId = function (targetElement) { 417 | return targetElement.id; 418 | }; 419 | 420 | return new ResizeSensorApi(); 421 | })(ResizeSensor); -------------------------------------------------------------------------------- /client/js/resize-handler.min.js: -------------------------------------------------------------------------------- 1 | var ResizeSensor=function(){"use strict";function e(e){var t=o(e,"position");"static"===t&&(e.style.position="relative")}function t(){var e=document.createElement("div"),t="animationName"in e.style;if(t)return!0;for(var i="Webkit Moz O ms".split(" "),n=0,r=i.length;r>n;n++)if(i[n]+"AnimationName"in e.style)return!0;return!1}function i(e){var t=e.tagName.toUpperCase();return a.indexOf(t)>-1}function n(){var e=document.createElement("div"),t="animationName"in e.style,i="animationstart",n="resizeanim";if(t)return{keyframesRule:"@keyframes "+n+" {from { opacity: 0; } to { opacity: 0; }}",styleDeclaration:"animation: 1ms "+n+";",animationStartEvent:i,animationName:n};var r,s="",o="Webkit Moz O ms".split(" "),a="webkitAnimationStart animationstart oAnimationStart MSAnimationStart".split(" "),l=o.length;for(r=0;l>r;r++)if(o[r]+"AnimationName"in e.style){s="-"+o[r].toLowerCase()+"-",i=a[r];break}return{keyframesRule:"@"+s+"keyframes "+n+" {from { opacity: 0; } to { opacity: 0; }}",styleDeclaration:s+"animation: 1ms "+n+";",animationStartEvent:i,animationName:n}}function r(){window.requestAnimationFrame||(window.requestAnimationFrame=function(){return window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||window.clearTimeout}())}function s(){var e=[d.keyframesRule?d.keyframesRule:"",".ResizeSensor__resizeTriggers { "+(d.styleDeclaration?d.styleDeclaration:"")+" visibility: hidden; opacity: 0; }",".ResizeSensor__resizeTriggers, .ResizeSensor__resizeTriggers > div, .ResizeSensor__contractTrigger:before { content: ' '; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; } .ResizeSensor__resizeTriggers > div { background: #eee; overflow: auto; } .ResizeSensor__contractTrigger:before { width: 200%; height: 200%; }"];e=e.join(" ");var t=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css",i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e)),t.appendChild(i)}function o(e,t){var i=null;return e.currentStyle?i=e.currentStyle[t]:window.getComputedStyle&&(i=document.defaultView.getComputedStyle(e,null).getPropertyValue(t)),i}var a=["IMG","COL","TR","THEAD","TFOOT"],l="attachEvent"in document;if(!l){var m=t(),d=m?n():{};s(),"requestAnimationFrame"in window&&"cancelAnimationFrame"in window||r()}var h=function(e,t){return i(e)?void(console&&console.error("Given element isn't suitable to act as a resize sensor. Try wrapping it with one that is. Unsuitable elements are:",a)):(this.targetElement=e,this.callback=t,this.dimensions={width:0,height:0},l?(this.boundOnResizeHandler=this.onElementResize.bind(this),void this.targetElement.attachEvent("onresize",this.boundOnResizeHandler)):(this.triggerElements={},this.resizeRAF=0,void this.setup()))};return h.prototype.setup=function(){e(this.targetElement),this.insertResizeTriggerElements(),this.boundScrollListener=this.handleElementScroll.bind(this),this.targetElement.addEventListener("scroll",this.boundScrollListener,!0),m&&(this.boundAnimationStartListener=this.resetTriggersOnAnimationStart.bind(this),this.triggerElements.container.addEventListener(d.animationStartEvent,this.boundAnimationStartListener)),this.resetTriggers()},h.prototype.insertResizeTriggerElements=function(){var e=document.createElement("div"),t=document.createElement("div"),i=document.createElement("div"),n=document.createElement("div");e.className="ResizeSensor ResizeSensor__resizeTriggers",t.className="ResizeSensor__expandTrigger",n.className="ResizeSensor__contractTrigger",t.appendChild(i),e.appendChild(t),e.appendChild(n),this.triggerElements.container=e,this.triggerElements.expand=t,this.triggerElements.expandChild=i,this.triggerElements.contract=n,this.targetElement.appendChild(e)},h.prototype.onElementResize=function(){var e=this.getDimensions();this.isResized(e)&&(this.dimensions.width=e.width,this.dimensions.height=e.height,this.elementResized())},h.prototype.handleElementScroll=function(){var e=this;this.resetTriggers(),this.resizeRAF&&window.cancelAnimationFrame(this.resizeRAF),this.resizeRAF=window.requestAnimationFrame(function(){var t=e.getDimensions();e.isResized(t)&&(e.dimensions.width=t.width,e.dimensions.height=t.height,e.elementResized())})},h.prototype.isResized=function(e){return e.width!==this.dimensions.width||e.height!==this.dimensions.height},h.prototype.getDimensions=function(){return{width:this.targetElement.offsetWidth,height:this.targetElement.offsetHeight}},h.prototype.resetTriggersOnAnimationStart=function(e){e.animationName===d.animationName&&this.resetTriggers()},h.prototype.resetTriggers=function(){this.triggerElements.contract.scrollLeft=this.triggerElements.contract.scrollWidth,this.triggerElements.contract.scrollTop=this.triggerElements.contract.scrollHeight,this.triggerElements.expandChild.style.width=this.triggerElements.expand.offsetWidth+1+"px",this.triggerElements.expandChild.style.height=this.triggerElements.expand.offsetHeight+1+"px",this.triggerElements.expand.scrollLeft=this.triggerElements.expand.scrollWidth,this.triggerElements.expand.scrollTop=this.triggerElements.expand.scrollHeight},h.prototype.elementResized=function(){this.callback(this.dimensions)},h.prototype.destroy=function(){this.removeEventListeners(),this.targetElement.removeChild(this.triggerElements.container),delete this.boundAnimationStartListener,delete this.boundScrollListener,delete this.callback,delete this.targetElement},h.prototype.removeEventListeners=function(){l&&this.targetElement.detachEvent("onresize",this.boundOnResizeHandler),this.triggerElements.container.removeEventListener(d.animationStartEvent,this.boundAnimationStartListener),this.targetElement.removeEventListener("scroll",this.boundScrollListener,!0)},h}(),ResizeSensorApi=function(e){var t={},i=function(){};return i.prototype.create=function(i,n){var r=this.getSensorId(i);if(t[r])return t[r];var s=new e(i,n);return t[r]=s,s},i.prototype.destroy=function(e){var i=this.getSensorId(e),n=t[i];n||console&&console.error("Can't destroy ResizeSensor (404 not found).",e),n.destroy(),delete t[i]},i.prototype.getSensorId=function(e){return e.id},new i}(ResizeSensor); 2 | -------------------------------------------------------------------------------- /client/services/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.auth', []) 2 | .factory('Auth', function ($http, $window) { 3 | 4 | var generateRandomId = function (length) { 5 | var id = ""; 6 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 7 | 8 | for (var i = 0; i < length; i++) { 9 | id += chars.charAt(Math.floor(Math.random() * chars.length)); 10 | } 11 | 12 | return id; 13 | }; 14 | 15 | return { 16 | generateRandomId: generateRandomId 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /client/services/board-data.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.boarddata', []) 2 | .factory('BoardData', function () { 3 | //svgWidth/Height are the width and height of the DOM element 4 | var svgWidth = 1500; //sizeX 5 | var svgHeight = 1000; //sizeY 6 | //offsetX/Y measure the top-left point of the viewbox 7 | var offsetX = 0; 8 | var offsetY = 0; 9 | //scalingFactor is the level of zooming relative to the start 10 | var scalingFactor = 1; 11 | 12 | var board; 13 | var $canvas; 14 | //canvasMarginX/Y are the left and top margin of the SVG in the browser 15 | var canvasMarginX; //canvasX 16 | var canvasMarginY; //canvasY 17 | //viewBoxWidth/Height are needed for zooming 18 | var viewBoxWidth;// = svgWidth; 19 | var viewBoxHeight;// = svgHeight; 20 | var cursor; 21 | var shapeStorage = {}; 22 | var currentShape; 23 | var currentShapeId; 24 | var editorShape; 25 | var socketId; 26 | 27 | var tool = { 28 | name: 'path', 29 | 'stroke-width': 3, 30 | colors: { 31 | fill: 'transparent', 32 | stroke: '#000000' 33 | } 34 | }; 35 | 36 | function createBoard (element) { 37 | 38 | ResizeSensorApi.create(document.getElementsByClassName('app-container')[0], handleWindowResize); 39 | 40 | board = Raphael(element[0]); 41 | board.setViewBox(0, 0, svgWidth, svgHeight, true); 42 | board.canvas.setAttribute('preserveAspectRatio', 'none'); 43 | 44 | $canvas = element.find('svg'); 45 | canvasMarginX = $canvas.position().left; 46 | canvasMarginY = $canvas.position().top; 47 | } 48 | 49 | function handleWindowResize (newPageSize) { 50 | svgWidth = newPageSize.width; 51 | svgHeight = newPageSize.height; 52 | 53 | viewBoxWidth = svgWidth * scalingFactor; 54 | viewBoxHeight = svgHeight * scalingFactor; 55 | var offset = getOffset(); 56 | board.setViewBox(offset.x, offset.y, viewBoxWidth, viewBoxHeight, true); 57 | } 58 | 59 | function getBoard () { 60 | return board; 61 | } 62 | 63 | function getCursor () { 64 | return cursor; 65 | } 66 | 67 | function setCursor () { 68 | cursor = board.circle(window.innerWidth / 2, window.innerHeight / 2, 5); 69 | return cursor; 70 | } 71 | 72 | function moveCursor (screenPosition) { 73 | cursor.attr({ 74 | cx: Math.floor(screenPosition[0]), 75 | cy: Math.floor(screenPosition[1]) 76 | }) 77 | } 78 | 79 | function setEditorShape (shape) { 80 | editorShape = shape; 81 | } 82 | 83 | function unsetEditorShape () { 84 | editorShape = null; 85 | } 86 | 87 | function getEditorShape () { 88 | return editorShape; 89 | } 90 | 91 | function getViewBoxDims () { 92 | return { 93 | width: viewBoxWidth, 94 | height: viewBoxHeight 95 | }; 96 | } 97 | 98 | function setViewBoxDims (newViewBoxDims) { 99 | viewBoxWidth = newViewBoxDims.width; 100 | viewBoxHeight = newViewBoxDims.height; 101 | } 102 | 103 | function getOriginalDims () { 104 | return { 105 | width: svgWidth, 106 | height: svgHeight 107 | }; 108 | } 109 | 110 | function getCanvasMargin () { 111 | return { 112 | x: canvasMarginX, 113 | y: canvasMarginY 114 | }; 115 | } 116 | 117 | function getScalingFactor () { 118 | return scalingFactor; 119 | } 120 | 121 | function getOffset () { 122 | return { 123 | x: offsetX, 124 | y: offsetY 125 | } 126 | } 127 | 128 | function setOffset (newOffset) { 129 | offsetX = newOffset.x; 130 | offsetY = newOffset.y; 131 | } 132 | 133 | function getCanvas () { 134 | return $canvas; 135 | } 136 | 137 | function setSocketId (id) { 138 | socketId = id; 139 | } 140 | 141 | function getSocketId () { 142 | return socketId; 143 | } 144 | 145 | function pushToStorage (id, socketId, shape) { 146 | if (!shapeStorage[socketId]) { 147 | shapeStorage[socketId] = {}; 148 | } 149 | shapeStorage[socketId][id] = shape; 150 | } 151 | 152 | function getShapeById (id, socketId) { 153 | return shapeStorage[socketId][id]; 154 | } 155 | 156 | function getCurrentShape () { 157 | return currentShape; 158 | } 159 | 160 | function setCurrentShape (id) { 161 | currentShape = shapeStorage[socketId][id]; 162 | } 163 | 164 | function unsetCurrentShape () { 165 | currentShape = null; 166 | } 167 | 168 | function getCurrentShapeId () { 169 | return currentShapeId; 170 | } 171 | 172 | function generateShapeId () { 173 | currentShapeId = Raphael._oid; 174 | return currentShapeId; 175 | } 176 | 177 | function getCurrentTool () { 178 | return tool; 179 | } 180 | 181 | function setCurrentToolName (name) { 182 | tool.name = name; 183 | } 184 | 185 | function setColors (fill, stroke) { 186 | fill = fill || tool.colors.fill; 187 | stroke = stroke || tool.colors.stroke; 188 | 189 | tool.colors.fill = fill; 190 | tool.colors.stroke = stroke; 191 | } 192 | 193 | function setZoomScale (scale) { 194 | scalingFactor = 1 / scale; 195 | }; 196 | 197 | function getZoomScale () { 198 | return scalingFactor; 199 | } 200 | 201 | function getShapeStorage () { 202 | return shapeStorage; 203 | } 204 | 205 | function setStrokeWidth (width) { 206 | tool['stroke-width'] = width; 207 | } 208 | 209 | function getStrokeWidth () { 210 | return tool['stroke-width']; 211 | } 212 | 213 | return { 214 | getShapeStorage: getShapeStorage, 215 | getCursor: getCursor, 216 | setCursor: setCursor, 217 | moveCursor: moveCursor, 218 | createBoard: createBoard, 219 | getCurrentShape: getCurrentShape, 220 | getShapeById: getShapeById, 221 | getCurrentTool: getCurrentTool, 222 | generateShapeId: generateShapeId, 223 | getCurrentShapeId: getCurrentShapeId, 224 | setColors: setColors, 225 | setZoomScale: setZoomScale, 226 | getZoomScale: getZoomScale, 227 | getCanvas: getCanvas, 228 | setSocketId: setSocketId, 229 | getSocketId: getSocketId, 230 | setCurrentToolName: setCurrentToolName, 231 | getBoard: getBoard, 232 | getScalingFactor: getScalingFactor, 233 | getOffset: getOffset, 234 | getCanvasMargin: getCanvasMargin, 235 | pushToStorage: pushToStorage, 236 | setCurrentShape: setCurrentShape, 237 | unsetCurrentShape: unsetCurrentShape, 238 | getViewBoxDims: getViewBoxDims, 239 | setViewBoxDims: setViewBoxDims, 240 | setOffset: setOffset, 241 | getOriginalDims: getOriginalDims, 242 | setEditorShape: setEditorShape, 243 | unsetEditorShape: unsetEditorShape, 244 | getEditorShape: getEditorShape, 245 | setStrokeWidth: setStrokeWidth, 246 | getStrokeWidth: getStrokeWidth 247 | } 248 | }); 249 | -------------------------------------------------------------------------------- /client/services/broadcast.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.broadcast', []) 2 | .factory('Broadcast', function (Sockets) { 3 | 4 | var socketUserId; 5 | 6 | var getSocketId = function () { 7 | return socketUserId; 8 | }; 9 | 10 | var saveSocketId = function (id) { 11 | socketUserId = id; 12 | }; 13 | 14 | Sockets.emit('idRequest'); 15 | 16 | var newShape = function (myid, socketId, tool, initX, initY) { 17 | Sockets.emit('newShape', { 18 | myid: myid, 19 | socketId: socketId, 20 | tool: tool, 21 | initX: initX, 22 | initY: initY 23 | }); 24 | }; 25 | 26 | var editShape = function (myid, socketId, currentTool, mouseX, mouseY) { 27 | var data = {}; 28 | data.mouseX = mouseX; 29 | data.mouseY = mouseY; 30 | data.myid = myid; 31 | data.socketId = socketId; 32 | data.tool = currentTool; 33 | Sockets.emit('editShape', data); 34 | }; 35 | 36 | var finishPath = function (myid, currentTool, pathDProps) { 37 | Sockets.emit('pathCompleted', { 38 | myid: myid, 39 | tool: currentTool, 40 | pathDProps: pathDProps 41 | }); 42 | }; 43 | 44 | var finishCopiedPath = function (myid, currentTool, pathDProps) { 45 | Sockets.emit('copiedPathCompleted', { 46 | myid: myid, 47 | tool: currentTool, 48 | pathDProps: pathDProps 49 | }); 50 | }; 51 | 52 | var finishShape = function (myid, currentTool) { 53 | Sockets.emit('shapeCompleted', { 54 | myid: myid, 55 | tool: currentTool 56 | }); 57 | }; 58 | 59 | var deleteShape = function (myid, socketId) { 60 | Sockets.emit('deleteShape', { 61 | myid: myid, 62 | socketId: socketId 63 | }) 64 | }; 65 | 66 | var moveShape = function (shape, x, y) { 67 | var type = shape.type; 68 | Sockets.emit('moveShape', { 69 | myid: shape.myid, 70 | socketId: shape.socketId, 71 | x: x, 72 | y: y, 73 | attr: shape.attr(), 74 | pathDProps: shape.pathDProps 75 | }); 76 | }; 77 | 78 | var finishMovingShape = function (shape) { 79 | Sockets.emit('finishMovingShape', { 80 | myid: shape.myid, 81 | socketId: shape.socketId, 82 | attr: shape.attr() 83 | }) 84 | }; 85 | 86 | return { 87 | getSocketId: getSocketId, 88 | saveSocketId: saveSocketId, 89 | newShape: newShape, 90 | editShape: editShape, 91 | finishPath: finishPath, 92 | finishCopiedPath: finishCopiedPath, 93 | finishShape: finishShape, 94 | deleteShape: deleteShape, 95 | finishMovingShape: finishMovingShape, 96 | moveShape: moveShape 97 | }; 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /client/services/event-handler.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.eventhandler', []) 2 | .factory('EventHandler', ['BoardData', 'ShapeBuilder', 'ShapeEditor', 'ShapeManipulation', 'Snap', function (BoardData, ShapeBuilder, ShapeEditor, ShapeManipulation, Snap) { 3 | 4 | function setSocketId (socketId) { 5 | BoardData.setSocketId(socketId); 6 | }; 7 | 8 | function createShape (id, socketId, tool, x, y) { 9 | ShapeBuilder.newShape(id, socketId, tool, x, y); 10 | } 11 | 12 | function editShape (id, socketId, tool, x, y) { 13 | ShapeEditor.editShape(id, socketId, tool, x, y); 14 | } 15 | 16 | function finishShape (id, socketId, tool) { 17 | ShapeEditor.finishShape(id, socketId, tool); 18 | } 19 | 20 | function finishCopiedPath (id, socketId, tool, pathDProps) { 21 | ShapeEditor.finishCopiedPath(id, socketId, tool, pathDProps); 22 | } 23 | 24 | function deleteShape (id, socketId) { 25 | ShapeEditor.deleteShape(id, socketId); 26 | } 27 | 28 | function moveShape (shape, x, y) { 29 | ShapeManipulation.moveShape(shape.myid, shape.socketId, x, y); 30 | } 31 | 32 | function finishMovingShape (id, socketId) { 33 | ShapeManipulation.finishMovingShape(id, socketId); 34 | } 35 | 36 | function drawExistingPath (shape) { 37 | ShapeBuilder.drawExistingPath(shape); 38 | var currentShape = BoardData.getShapeById(shape.myid, shape.socketId); 39 | ShapeManipulation.pathSmoother(currentShape); 40 | } 41 | 42 | function cursor (screenPosition) { 43 | var cursor = BoardData.getCursor() || BoardData.setCursor(); 44 | BoardData.moveCursor(screenPosition); 45 | } 46 | 47 | function grabShape (screenPosition) { 48 | var x = Math.floor(screenPosition[0]); 49 | var y = Math.floor(screenPosition[1]); 50 | 51 | var currentEditorShape; 52 | 53 | currentEditorShape = BoardData.getEditorShape(); 54 | 55 | if (!currentEditorShape) { 56 | var shape = BoardData.getBoard().getElementByPoint(x, y); 57 | if (shape) { 58 | BoardData.setEditorShape(shape); 59 | currentEditorShape = BoardData.getEditorShape(); 60 | } 61 | } else { 62 | moveShape(currentEditorShape, x, y); 63 | } 64 | } 65 | 66 | return { 67 | cursor: cursor, 68 | setSocketId: setSocketId, 69 | createShape: createShape, 70 | editShape: editShape, 71 | finishShape: finishShape, 72 | finishCopiedPath: finishCopiedPath, 73 | deleteShape: deleteShape, 74 | moveShape: moveShape, 75 | finishMovingShape: finishMovingShape, 76 | drawExistingPath: drawExistingPath, 77 | grabShape: grabShape 78 | }; 79 | }]); 80 | -------------------------------------------------------------------------------- /client/services/input-handler.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.inputhandler', []) 2 | .factory('InputHandler', ['BoardData', 'Snap', 'EventHandler', 'Broadcast', 'Visualizer', 'Zoom', function (BoardData, Snap, EventHandler, Broadcast, Visualizer, Zoom) { 3 | var toggleAttrs = {}; 4 | function toggle (attr) { 5 | if (!toggleAttrs[attr]) { 6 | toggleAttrs[attr] = true; 7 | } else { 8 | toggleAttrs[attr] = false; 9 | } 10 | } 11 | function isToggled (attr) { 12 | return toggleAttrs[attr]; 13 | } 14 | 15 | function getClosestElementByArea (ev) { 16 | var paper = BoardData.getBoard(); 17 | var width = height = 5; 18 | var mouseXY = getMouseXY(ev); 19 | var bbox = { 20 | x: mouseXY.x - width / 2, 21 | y: mouseXY.y - height / 2, 22 | x2: mouseXY.x + width / 2, 23 | y2: mouseXY.y + height / 2, 24 | width: width, 25 | height: height 26 | }; 27 | var bboxCenter = {x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2}; 28 | // var set = this.set(); 29 | var closest = null; 30 | var closestDist; 31 | paper.forEach(function (el) { 32 | var elBBox = el.getBBox(); 33 | if (!(el.type === 'set') && Raphael.isBBoxIntersect(elBBox, bbox)) { 34 | var elBBoxCenter = {x: elBBox.x + elBBox.width / 2, y: elBBox.y + elBBox.height / 2}; 35 | var dist = Math.sqrt(Math.pow(elBBoxCenter.x - bboxCenter.x, 2) + Math.pow(elBBoxCenter.y - bboxCenter.y, 2)); 36 | if (!closestDist || dist < closestDist) { 37 | closest = el; 38 | closestDist = dist; 39 | } 40 | } 41 | }); 42 | return closest; 43 | }; 44 | 45 | var actions = {}; 46 | 47 | var lastEv; 48 | actions.eraser = { 49 | mouseDown: function (ev) { 50 | }, 51 | mouseHold: function (ev) { 52 | if (lastEv) { 53 | var mousePathString = 'M' + lastEv.clientX + ',' + lastEv.clientY + 'L' + ev.clientX + ',' + ev.clientY; 54 | BoardData.getBoard().forEach(function (shape) { 55 | if (shape.type === 'path') { 56 | if (Raphael.pathIntersection(mousePathString, shape.attr('path')).length) { 57 | Broadcast.deleteShape(shape.myid, shape.socketId); 58 | EventHandler.deleteShape(shape.myid, shape.socketId); 59 | } 60 | } 61 | }); 62 | } 63 | var shape = BoardData.getBoard().getElementByPoint(ev.clientX, ev.clientY); 64 | if (shape) { 65 | Broadcast.deleteShape(shape.myid, shape.socketId); 66 | EventHandler.deleteShape(shape.myid, shape.socketId); 67 | } 68 | lastEv = ev; 69 | }, 70 | mouseUp: function (ev) { 71 | lastEv = null; 72 | }, 73 | mouseOver: function (ev) { 74 | } 75 | }; 76 | 77 | actions.pan = { 78 | mouseDown: function (ev) { 79 | }, 80 | mouseHold: function (ev) { 81 | Zoom.pan(ev); 82 | }, 83 | mouseUp: function (ev) { 84 | Zoom.resetPan(); 85 | }, 86 | mouseOver: function (ev) { 87 | } 88 | }; 89 | 90 | actions.move = { 91 | mouseDown: function (ev) { 92 | Visualizer.clearSelection(); 93 | var target = getClosestElementByArea(ev); 94 | 95 | if (target) { 96 | BoardData.setEditorShape(target); 97 | } else { 98 | toggle('move'); 99 | } 100 | }, 101 | mouseHold: function (ev) { 102 | var currentEditorShape = BoardData.getEditorShape(); 103 | var mouseXY = getMouseXY(ev); 104 | 105 | Visualizer.clearSelection(); 106 | EventHandler.moveShape(currentEditorShape, mouseXY.x, mouseXY.y); 107 | Broadcast.moveShape(currentEditorShape, mouseXY.x, mouseXY.y); 108 | }, 109 | mouseUp: function (ev) { 110 | var editorShape = BoardData.getEditorShape(); 111 | var currentTool = BoardData.getCurrentTool(); 112 | 113 | Broadcast.finishMovingShape(editorShape); 114 | EventHandler.finishMovingShape(editorShape.myid, editorShape.socketId); 115 | BoardData.unsetEditorShape(); 116 | }, 117 | mouseOver: function (ev) { 118 | Visualizer.clearSelection(); 119 | var selection = getClosestElementByArea(ev); 120 | Visualizer.visualizeSelection(selection); 121 | } 122 | }; 123 | 124 | actions.copy = { 125 | mouseDown: function (ev) { 126 | Visualizer.clearSelection(); 127 | var shape = getClosestElementByArea(ev); 128 | var socketId = BoardData.getSocketId(); 129 | 130 | var newId = BoardData.generateShapeId(); 131 | 132 | var newInitX = shape.initX + 10; 133 | var newInitY = shape.initY + 10; 134 | var newMouseX = shape.mouseX + 10 || newInitX; 135 | var newMouseY = shape.mouseY + 10 || newInitY; 136 | 137 | EventHandler.createShape(newId, socketId, shape.tool, newInitX, newInitY); 138 | Broadcast.newShape(newId, socketId, shape.tool, newInitX, newInitY); 139 | if (shape.tool.name === 'path') { 140 | BoardData.setCurrentShape(newId); 141 | 142 | var currentShape = BoardData.getCurrentShape(); 143 | var parsedPathArray = Raphael.parsePathString(shape.pathDProps); 144 | 145 | var temp = parsedPathArray.map(function (coordinate) { 146 | return coordinate.map(function (element) { 147 | return typeof element === 'number' ? element + 10 : element; 148 | }); 149 | }); 150 | 151 | var stringifiedPath = temp.map(function (coordinate) { 152 | return coordinate[0] + coordinate[1] + ',' + coordinate[2]; 153 | }).join(''); 154 | 155 | currentShape.pathDProps = stringifiedPath; 156 | } 157 | 158 | if (!!shape.tool.text) { 159 | EventHandler.editShape(newId, socketId, shape.tool, newInitX, newInitY); 160 | } else { 161 | EventHandler.editShape(newId, socketId, shape.tool, newMouseX, newMouseY); 162 | } 163 | 164 | Broadcast.editShape(newId, socketId, shape.tool, newMouseX, newMouseY); 165 | 166 | EventHandler.finishShape(newId, socketId, shape.tool); 167 | shape.tool.name === 'path' ? Broadcast.finishCopiedPath(newId, shape.tool, currentShape.pathDProps) : Broadcast.finishShape(newId, shape.tool); 168 | }, 169 | mouseOver: function (ev) { 170 | Visualizer.clearSelection(); 171 | var selection = getClosestElementByArea(ev); 172 | Visualizer.visualizeSelection(selection); 173 | } 174 | }; 175 | 176 | var defaultText = 'Start Typing...' 177 | actions.text = { 178 | mouseDown: function (ev) { 179 | var id = BoardData.generateShapeId(); 180 | var mouseXY = getMouseXY(ev); 181 | var socketId = BoardData.getSocketId(); 182 | var currentTool = BoardData.getCurrentTool(); 183 | currentTool.text = defaultText; 184 | 185 | EventHandler.createShape(id, socketId, currentTool, mouseXY.x, mouseXY.y); 186 | BoardData.setCurrentShape(id); 187 | Broadcast.newShape(id, socketId, currentTool, mouseXY.x, mouseXY.y); 188 | var currentShape = BoardData.getCurrentShape(); 189 | 190 | document.onkeypress = function (ev) { 191 | BoardData.setEditorShape(currentShape); 192 | var editorShape = BoardData.getEditorShape(); 193 | if (editorShape.attr('text') === defaultText) { 194 | editorShape.attr('text', ''); 195 | currentTool.text = ''; 196 | } 197 | 198 | if (ev.keyCode === 13) { 199 | // enter key to complete text insertion process 200 | editorShape.tool = currentTool; 201 | editorShape.tool.colors.fill = editorShape.trueColors.fill; 202 | editorShape.tool.colors.stroke = editorShape.trueColors.stroke; 203 | editorShape.attr({ 204 | 'fill': editorShape.trueColors.fill, 205 | 'stroke': editorShape.trueColors.stroke 206 | }) 207 | EventHandler.finishShape(id, socketId, editorShape.tool); 208 | Broadcast.finishShape(id, editorShape.tool); 209 | editorShape = null; 210 | document.onkeydown = document.onkeypress = function () {}; 211 | } else { 212 | // typing text 213 | editorShape.attr('text', editorShape.attr('text') + String.fromCharCode(ev.keyCode)); 214 | currentTool.text = editorShape.attr('text'); 215 | EventHandler.editShape(id, socketId, currentTool, editorShape.initX, editorShape.initY); 216 | Broadcast.editShape(id, socketId, currentTool, editorShape.initX, editorShape.initY); 217 | } 218 | } 219 | 220 | document.onkeydown = function (ev) { 221 | BoardData.setEditorShape(currentShape); 222 | var editorShape = BoardData.getEditorShape(); 223 | if (ev.which === 8) { 224 | ev.preventDefault(); 225 | if (editorShape) { 226 | editorShape.attr('text', editorShape.attr('text').slice(0, editorShape.attr('text').length - 1)); 227 | currentTool.text = editorShape.attr('text'); 228 | EventHandler.editShape(id, socketId, currentTool, editorShape.initX, editorShape.initY); 229 | Broadcast.editShape(id, socketId, currentTool, editorShape.initX, editorShape.initY); 230 | } 231 | } 232 | } 233 | 234 | }, 235 | mouseHold: function (ev) { 236 | }, 237 | mouseUp: function (ev) { 238 | }, 239 | mouseOver: function (ev) { 240 | } 241 | }; 242 | 243 | actions.shape = { 244 | mouseDown: function (ev) { 245 | var socketId = BoardData.getSocketId(); 246 | var currentTool = BoardData.getCurrentTool(); 247 | var mouseXY = getMouseXY(ev); 248 | var coords = Snap.snapToPoints(mouseXY.x, mouseXY.y); 249 | var id = BoardData.generateShapeId(); 250 | 251 | if (currentTool.name !== 'text' && currentTool.text) { 252 | delete currentTool.text; 253 | } 254 | 255 | EventHandler.createShape(id, socketId, currentTool, coords[0], coords[1]); 256 | BoardData.setCurrentShape(id); 257 | Broadcast.newShape(id, socketId, currentTool, coords[0], coords[1]); 258 | }, 259 | mouseHold: function (ev) { 260 | var id = BoardData.getCurrentShapeId(); 261 | var socketId = BoardData.getSocketId(); 262 | var currentTool = BoardData.getCurrentTool(); 263 | var mouseXY = getMouseXY(ev); 264 | var coords = Snap.snapToPoints(mouseXY.x, mouseXY.y); 265 | 266 | Broadcast.editShape(id, socketId, currentTool, coords[0], coords[1]); 267 | EventHandler.editShape(id, socketId, currentTool, coords[0], coords[1]); 268 | }, 269 | mouseUp: function (ev) { 270 | var id = BoardData.getCurrentShapeId(); 271 | var socketId = BoardData.getSocketId(); 272 | var currentTool = BoardData.getCurrentTool(); 273 | var shape = BoardData.getCurrentShape(); 274 | 275 | var currentToolCopy = {}; 276 | currentToolCopy.name = currentTool.name; 277 | currentToolCopy['stroke-width'] = currentTool['stroke-width']; 278 | currentToolCopy.colors = {}; 279 | currentToolCopy.colors.fill = currentTool.colors.fill; 280 | currentToolCopy.colors.stroke = currentTool.colors.stroke; 281 | 282 | shape.tool = currentToolCopy; 283 | 284 | EventHandler.finishShape(id, socketId, currentToolCopy); 285 | BoardData.unsetCurrentShape(); 286 | Visualizer.clearSnaps(); 287 | 288 | if (currentTool.name === 'path') { 289 | Broadcast.finishPath(id, currentTool, shape.pathDProps); 290 | } else { 291 | Broadcast.finishShape(id, currentTool); 292 | } 293 | }, 294 | mouseOver: function (ev) { 295 | var mouseXY = getMouseXY(ev); 296 | Snap.snapToPoints(mouseXY.x, mouseXY.y); 297 | } 298 | }; 299 | 300 | actions.magnify = { 301 | mouseDown: function (ev) { 302 | }, 303 | mouseHold: function (ev) { 304 | var mouseXY = getMouseXY(ev); 305 | 306 | Zoom.zoom(ev, mouseXY); 307 | }, 308 | mouseUp: function (ev) { 309 | Zoom.resetZoom(); 310 | }, 311 | mouseOver: function (ev) { 312 | } 313 | }; 314 | 315 | actions.noTool = { 316 | mouseDown: function (ev) { 317 | }, 318 | mouseHold: function (ev) { 319 | }, 320 | mouseUp: function (ev) { 321 | }, 322 | mouseOver: function (ev) { 323 | } 324 | }; 325 | 326 | function getMouseXY (ev) { 327 | var canvasMarginXY = BoardData.getCanvasMargin(); 328 | var scalingFactor = BoardData.getScalingFactor(); 329 | var offsetXY = BoardData.getOffset(); 330 | return { 331 | x: (ev.clientX - canvasMarginXY.x) * scalingFactor + offsetXY.x, 332 | y: (ev.clientY - canvasMarginXY.y) * scalingFactor + offsetXY.y 333 | }; 334 | } 335 | 336 | var shapeTools = ['line','circle','path','rectangle','arrow']; 337 | function parseToolName (toolName) { 338 | for (var i = 0; i < shapeTools.length; i++) { 339 | if (toolName === shapeTools[i]) { 340 | toolName = 'shape'; 341 | } 342 | } 343 | if (!toolName) { 344 | toolName = 'noName'; 345 | } 346 | return toolName; 347 | } 348 | 349 | function mouseDown (ev) { 350 | var toolName = parseToolName(BoardData.getCurrentTool().name); 351 | 352 | toggle(toolName); 353 | actions[toolName].mouseDown(ev); 354 | } 355 | 356 | function mouseMove (ev) { 357 | var toolName = parseToolName(BoardData.getCurrentTool().name); 358 | 359 | if (isToggled(toolName)) { 360 | actions[toolName].mouseHold(ev); 361 | } else { 362 | actions[toolName].mouseOver(ev); 363 | } 364 | } 365 | 366 | function mouseUp (ev) { 367 | var toolName = parseToolName(BoardData.getCurrentTool().name); 368 | 369 | if (isToggled(toolName)) { 370 | toggle(toolName); 371 | actions[toolName].mouseUp(ev); 372 | } 373 | } 374 | 375 | function keyPress (ev) { 376 | var toolName = parseToolName(BoardData.getCurrentTool().name); 377 | 378 | if (toolName !== 'text') { 379 | // keycode value for lowercase m 380 | if (ev.keyCode === 109) { 381 | console.log('m has been typed'); 382 | } 383 | } 384 | } 385 | 386 | return { 387 | mousedown: mouseDown, 388 | mousemove: mouseMove, 389 | mouseup: mouseUp, 390 | keypress: keyPress 391 | }; 392 | }]); 393 | -------------------------------------------------------------------------------- /client/services/leap.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.leapMotion', []) 2 | .factory('LeapMotion', ['EventHandler', function (EventHandler) { 3 | 4 | // var controller = new Leap.Controller({enableGestures: true}) 5 | // .use('screenPosition', {scale: 0.25}) 6 | // .connect() 7 | // .on('frame', function(frame){ 8 | 9 | // // if (frame.valid && frame.gestures.length > 0) { 10 | // // frame.gestures.forEach(function(gesture){ 11 | // // switch (gesture.type){ 12 | // // case "circle": 13 | // // console.log("Circle Gesture"); 14 | // // break; 15 | // // case "keyTap": 16 | // // console.log("Key Tap Gesture"); 17 | // // break; 18 | // // case "screenTap": 19 | // // console.log("Screen Tap Gesture"); 20 | // // break; 21 | // // case "swipe": 22 | // // console.log("Swipe Gesture"); 23 | // // break; 24 | // // } 25 | // // }); 26 | // // } 27 | 28 | // frame.hands.forEach(function (hand, index) { 29 | // console.log(hand.indexFinger.touchZone); 30 | // EventHandler.cursor(hand.indexFinger.screenPosition()); 31 | // if (hand.indexFinger.extended) { 32 | // // 33 | // } 34 | // if (hand.grabStrength === 1) { 35 | // // console.log('grabStrength === 1') 36 | // EventHandler.grabShape(hand.screenPosition()); 37 | // //console.log(hand.pinchStrength) 38 | // } 39 | 40 | // if (hand.pinchStrength === 1) { 41 | // // console.log('pinchStrength === 1'); 42 | // } 43 | // }); 44 | // }) 45 | 46 | return {}; 47 | }]); 48 | -------------------------------------------------------------------------------- /client/services/receive.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.receive', []) 2 | .factory('Receive', function (Sockets, EventHandler) { 3 | Sockets.on('showExisting', function (data) { 4 | for (socketId in data) { 5 | if (Object.keys(data[socketId]).length) { 6 | for (id in data[socketId]) { 7 | var thisShape = data[socketId][id]; 8 | if (thisShape.tool.name === 'path') { 9 | EventHandler.drawExistingPath(thisShape); 10 | } else if (thisShape.initX && thisShape.initY) { 11 | EventHandler.createShape(id, socketId, thisShape.tool, thisShape.initX, thisShape.initY); 12 | if (thisShape.tool.name !== 'text') { 13 | EventHandler.editShape(id, socketId, thisShape.tool, thisShape.mouseX, thisShape.mouseY); 14 | } 15 | EventHandler.finishShape(thisShape.myid, thisShape.socketId, thisShape.tool); 16 | } 17 | } 18 | } 19 | } 20 | }); 21 | 22 | Sockets.on('heartbeat', function () { 23 | Sockets.emit('heartbeat'); 24 | }) 25 | 26 | Sockets.on('socketId', function (data) { 27 | EventHandler.setSocketId(data.socketId); 28 | }); 29 | 30 | Sockets.on('shapeEdited', function (data) { 31 | EventHandler.editShape(data.myid, data.socketId, data.tool, data.mouseX, data.mouseY); 32 | }); 33 | 34 | Sockets.on('shapeCompleted', function (data) { 35 | EventHandler.finishShape(data.myid, data.socketId, data.tool); 36 | }); 37 | 38 | Sockets.on('copiedPathCompleted', function (data) { 39 | EventHandler.finishCopiedPath(data.myid, data.socketId, data.tool, data.pathDProps); 40 | }); 41 | 42 | Sockets.on('shapeCreated', function (data) { 43 | EventHandler.createShape(data.myid, data.socketId, data.tool, data.initX, data.initY); 44 | }); 45 | 46 | Sockets.on('shapeMoved', function (data) { 47 | EventHandler.moveShape(data, data.x, data.y); 48 | }); 49 | 50 | Sockets.on('shapeFinishedMoving', function (data) { 51 | EventHandler.finishMovingShape(data.myid, data.socketId); 52 | }); 53 | 54 | Sockets.on('shapeDeleted', function (data) { 55 | EventHandler.deleteShape(data.myid, data.socketId); 56 | }); 57 | 58 | return {}; 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /client/services/shape-builder.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.shapebuilder', []) 2 | .factory('ShapeBuilder', ['BoardData', 'Snap', function (BoardData, Snap) { 3 | 4 | function setColor (shape, colors) { 5 | if (shape.type === 'path') { 6 | shape.attr('stroke', colors.stroke); 7 | } else if (shape.type === 'text' && shape.attr('text') === 'Start Typing...') { 8 | shape.attr('stroke', '#b3b3b3'); 9 | shape.attr('fill', '#b3b3b3'); 10 | shape.trueColors = { 11 | stroke: colors.stroke, 12 | fill: colors.fill 13 | }; 14 | } else { 15 | shape.attr('stroke', colors.stroke); 16 | shape.attr('fill', colors.fill); 17 | } 18 | } 19 | 20 | function setWidth (shape, width) { 21 | shape.attr('stroke-width', width); 22 | } 23 | 24 | function drawExistingPath (shape) { 25 | newShape(shape.myid, shape.socketId, shape.tool, shape.initX, shape.initY); 26 | var existingPath = BoardData.getShapeById(shape.myid, shape.socketId); 27 | existingPath.customSetPathD(shape.pathDProps); 28 | existingPath.pathDProps = shape.pathDProps; 29 | existingPath.attr('fill', existingPath.tool.colors.fill); 30 | BoardData.pushToStorage(shape.myid, shape.socketId, existingPath); 31 | } 32 | 33 | function newShape (id, socketId, tool, x, y) { 34 | var shapeConstructors = { 35 | 'circle': function (x, y) { 36 | return BoardData.getBoard().circle(x, y, 0); 37 | }, 38 | 'line': function (x, y) { 39 | return BoardData.getBoard().path("M" + String(x) + "," + String(y)) 40 | .attr({ 41 | 'stroke-linecap': 'round' 42 | }); 43 | }, 44 | 'path': function (x, y) { 45 | var path = BoardData.getBoard().path("M" + String(x) + "," + String(y)) 46 | .attr({ 47 | 'stroke-linecap': 'round' 48 | }); 49 | path.pathDProps = ''; 50 | return path; 51 | }, 52 | 'rectangle': function (x,y) { 53 | return BoardData.getBoard().rect(x, y, 0, 0); 54 | }, 55 | 'text': function (x, y, text) { 56 | return BoardData.getBoard().text(x, y, text) 57 | .attr({ 58 | 'font-size': 18, 59 | 'font-family': "San Francisco" 60 | }); 61 | }, 62 | 'arrow': function (x, y) { 63 | var arrow = BoardData.getBoard().path("M" + String(x) + ',' + String(y)); 64 | arrow.attr('arrow-end', 'classic-wide-long'); 65 | return arrow; 66 | } 67 | }; 68 | var shape = !!tool.text ? shapeConstructors['text'](x, y, tool.text) : shapeConstructors[tool.name](x, y); 69 | shape.initX = x; 70 | shape.initY = y; 71 | shape.tool = tool; 72 | setColor(shape, tool.colors); 73 | shape.myid = id; 74 | shape.socketId = socketId; 75 | if (tool.name === 'path') Snap.createSnaps(shape); 76 | if (tool.name !== 'text') setWidth(shape, tool['stroke-width']); 77 | BoardData.pushToStorage(id, socketId, shape); 78 | }; 79 | 80 | return { 81 | newShape: newShape, 82 | drawExistingPath: drawExistingPath 83 | }; 84 | 85 | }]); 86 | -------------------------------------------------------------------------------- /client/services/shape-editor.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.shapeeditor', []) 2 | .factory('ShapeEditor', ['BoardData', 'Snap', 'ShapeManipulation', function (BoardData, Snap, ShapeManipulation) { 3 | 4 | var changeCircle = function (shape, x, y) { 5 | var deltaX = x - shape.initX; 6 | var deltaY = y - shape.initY; 7 | var newRadius = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); 8 | shape.attr('r', newRadius); 9 | }; 10 | 11 | var changeLine = function (shape, x, y) { 12 | //"M10,20L30,40" 13 | var deltaX = x - shape.initX; 14 | var deltaY = y - shape.initY; 15 | 16 | if (Math.sqrt(Math.pow(deltaX,2) + Math.pow(deltaY,2)) > 20) { 17 | if (Math.abs(deltaY) < 5) { 18 | y = shape.initY; 19 | } else if (Math.abs(deltaX) < 5) { 20 | x = shape.initX; 21 | } 22 | } 23 | 24 | var linePathOrigin = "M" + String(shape.initX) + "," + String(shape.initY); 25 | var linePathEnd = "L" + String(x) + "," + String(y); 26 | shape.attr('path', linePathOrigin + linePathEnd); 27 | }; 28 | 29 | var changePath = function (shape, x, y) { 30 | //"M10,20L30,40" 31 | 32 | shape.pathDProps += shape.pathDProps === '' ? 'M' + shape.initX + ',' + shape.initY + 'L' + x + ',' + y : 'L' + x + ',' + y; 33 | //this custom function is in raphael 34 | shape.customSetPathD(shape.pathDProps); 35 | }; 36 | 37 | var changeRectangle = function (shape, x, y) { 38 | var left, top; 39 | 40 | if (x < shape.initX && y < shape.initY) { 41 | left = x; 42 | top = y; 43 | width = shape.initX - left; 44 | height = shape.initY - top; 45 | } else if (x < shape.initX) { 46 | left = x; 47 | top = shape.initY; 48 | width = shape.initX - left; 49 | height = y - shape.initY; 50 | } else if (y < shape.initY) { 51 | left = shape.attr('x'); 52 | top = y; 53 | width = x - shape.initX; 54 | height = shape.initY - top; 55 | } else { 56 | left = shape.attr('x'); 57 | top = shape.attr('y'); 58 | width = x - shape.initX; 59 | height = y - shape.initY; 60 | } 61 | 62 | shape.attr({ 63 | x: left, 64 | y: top, 65 | width: width, 66 | height: height 67 | }); 68 | } 69 | 70 | var changeText = function (shape, x, y, tool) { 71 | shape.attr({ 72 | x: x, 73 | y: y, 74 | text: tool.text, 75 | 'stroke-width': 1 76 | }); 77 | }; 78 | 79 | function editShape (id, socketId, tool, x, y) { 80 | var shapeHandlers = { 81 | 'circle': changeCircle, 82 | 'path': changePath, 83 | 'line': changeLine, 84 | 'arrow': changeLine, 85 | 'rectangle': changeRectangle, 86 | 'text': changeText 87 | }; 88 | var shape = BoardData.getShapeById(id, socketId); 89 | 90 | if (tool.name !== 'text') { 91 | shape.mouseX = x; 92 | shape.mouseY = y; 93 | } 94 | 95 | // optional tool argument for text change 96 | !!tool.text ? shapeHandlers['text'](shape, x, y, tool) : shapeHandlers[tool.name](shape, x, y); 97 | }; 98 | 99 | function finishShape (id, socketId, tool) { 100 | var shape = BoardData.getShapeById(id, socketId); 101 | 102 | if (shape.type === 'text') { 103 | if (shape.attr('text') === 'Start Typing...') { 104 | shape.attr('text', ''); 105 | } else { 106 | shape.attr({ 107 | text: tool.text, 108 | stroke: tool.colors.stroke 109 | }); 110 | } 111 | } 112 | 113 | if (shape.pathDProps !== undefined) { 114 | var path = shape.pathDProps; 115 | var lastPoint = path.slice(path.lastIndexOf('L') + 1).split(',').map(Number); 116 | if (lastPoint[0] === shape.initX && lastPoint[1] === shape.initY) { 117 | shape.pathDProps = path + 'Z'; 118 | shape.attr('fill', shape.tool.colors.fill ? shape.tool.colors.fill : (shape.tool.colors.fill = tool.colors.fill)); 119 | } 120 | ShapeManipulation.pathSmoother(shape); 121 | } 122 | 123 | Snap.createSnaps(shape); 124 | }; 125 | 126 | function finishCopiedPath (id, socketId, tool, pathDProps) { 127 | var shape = BoardData.getShapeById(id, socketId); 128 | shape.pathDProps = pathDProps; 129 | shape.attr('path', shape.pathDProps); 130 | if ((shape.myid || shape.myid === 0) && tool.name === 'path') { 131 | ShapeManipulation.pathSmoother(shape); 132 | } 133 | } 134 | 135 | function deleteShape (id, socketId) { 136 | var shape = BoardData.getShapeById(id, socketId); 137 | 138 | Snap.deleteSnaps(shape); 139 | shape.remove(); 140 | }; 141 | 142 | return { 143 | editShape: editShape, 144 | finishShape: finishShape, 145 | finishCopiedPath: finishCopiedPath, 146 | deleteShape: deleteShape 147 | }; 148 | 149 | }]); 150 | -------------------------------------------------------------------------------- /client/services/shape-manipulation.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.shapemanipulation', []) 2 | .factory('ShapeManipulation', ['BoardData', 'ShapeBuilder', 'Snap', function (BoardData, ShapeBuilder, Snap) { 3 | 4 | var pathSmoother = function (pathElement) { 5 | var path = pathElement.attr('path'); 6 | path = path.length > 1 ? path : Raphael.parsePathString(pathElement.pathDProps); 7 | if (!path) return; 8 | var interval = 5; 9 | 10 | var newPath = path.reduce(function (newPathString, currentPoint, index, path) { 11 | if (!(index % interval) || index === (path.length - 1)) { 12 | return newPathString += currentPoint[1] + ',' + currentPoint[2] + ' '; 13 | } else { 14 | return newPathString; 15 | } 16 | }, path[0][0] + path[0][1] + ',' + path[0][2] + ' ' + "R"); 17 | 18 | if (path[path.length - 1] === 'Z') { 19 | newPath += 'Z'; 20 | } 21 | pathElement.attr('path', newPath); 22 | }; 23 | 24 | var grabPoint; 25 | var origin; 26 | function moveCircle (shape, x, y) { 27 | var deltaX = x - grabPoint.x; 28 | var deltaY = y - grabPoint.y; 29 | shape.attr({ 30 | cx: origin.cx + deltaX, 31 | cy: origin.cy + deltaY 32 | }); 33 | } 34 | 35 | function moveRectangle (shape, x, y) { 36 | var deltaX = x - grabPoint.x; 37 | var deltaY = y - grabPoint.y; 38 | shape.attr({ 39 | x: origin.x + deltaX, 40 | y: origin.y + deltaY 41 | }); 42 | } 43 | 44 | function movePath (shape, x, y) { 45 | var deltaX = x - grabPoint.x; 46 | var deltaY = y - grabPoint.y; 47 | 48 | var pathArr = shape.attr('path'); 49 | for (var seg in pathArr) { 50 | pathArr[seg][1] = origin.path[seg][1] + deltaX; 51 | pathArr[seg][2] = origin.path[seg][2] + deltaY; 52 | if (pathArr[seg].length > 3) { 53 | pathArr[seg][3] = origin.path[seg][3] + deltaX; 54 | pathArr[seg][4] = origin.path[seg][4] + deltaY; 55 | pathArr[seg][5] = origin.path[seg][5] + deltaX; 56 | pathArr[seg][6] = origin.path[seg][6] + deltaY; 57 | shape.pathDProps = origin.pathDProps.split('L').map(function (subpath, index) { 58 | var xy = subpath.split(','); 59 | var x; 60 | if (index === 0) { 61 | x = 'M' + (+xy[0].slice(1) + deltaX); 62 | } else { 63 | x = +xy[0] + deltaX; 64 | } 65 | var y = +xy[1] + deltaY; 66 | return x + ',' + y; 67 | }).join('L'); 68 | } 69 | } 70 | 71 | shape.attr('path',pathArr); 72 | } 73 | 74 | function moveText (shape, x, y) { 75 | var deltaX = x - grabPoint.x; 76 | var deltaY = y - grabPoint.y; 77 | shape.attr({ 78 | x: origin.x + deltaX, 79 | y: origin.y + deltaY 80 | }); 81 | } 82 | 83 | function moveShape (id, socketId, x, y) { 84 | var shapeHandlers = { 85 | 'circle': moveCircle, 86 | 'path': movePath, 87 | 'line': movePath, 88 | 'rect': moveRectangle, 89 | 'text': moveText 90 | }; 91 | var shape = BoardData.getShapeById(id, socketId).toFront(); 92 | if (!grabPoint) { 93 | grabPoint = {x: x, y: y}; 94 | origin = shape.attr(); 95 | origin.pathDProps = shape.pathDProps; 96 | } 97 | shapeHandlers[shape.type](shape, x, y); 98 | } 99 | 100 | function finishMovingShape (id, socketId) { 101 | grabPoint = null; 102 | origin = null; 103 | 104 | var shape = BoardData.getShapeById(id, socketId); 105 | Snap.createSnaps(shape); 106 | } 107 | 108 | return { 109 | pathSmoother: pathSmoother, 110 | moveShape: moveShape, 111 | finishMovingShape: finishMovingShape 112 | }; 113 | 114 | }]); 115 | -------------------------------------------------------------------------------- /client/services/snap.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.snap', []) 2 | .factory('Snap', ['BoardData', 'Visualizer', function (BoardData, Visualizer) { 3 | var endSnapTree; 4 | function Point (x, y) { 5 | this.x = x; 6 | this.y = y; 7 | } 8 | 9 | function Node (val, left, right) { 10 | this.val = val; 11 | this.left = left || null; 12 | this.right = right || null; 13 | } 14 | 15 | function Rectangle (x0, y0, x1, y1) { 16 | this.left = x0; 17 | this.bottom = y0; 18 | this.right = x1; 19 | this.top = y1; 20 | } 21 | 22 | function KDTree (points, depth) { 23 | var split, sortedPoints; 24 | points = points || generatePoints(10); 25 | depth = depth || 0; 26 | if (points.length <= 1) { 27 | return points[0]; 28 | } 29 | else { 30 | var mid = Math.ceil(points.length / 2); 31 | if ((depth % 2) === 0) { 32 | sortedPoints = points.slice().sort(function (a,b) { 33 | return a.x - b.x; 34 | }); 35 | split = sortedPoints[mid].x; 36 | } else { 37 | sortedPoints = points.slice().sort(function (a,b) { 38 | return a.y - b.y; 39 | }); 40 | split = sortedPoints[mid].y; 41 | } 42 | var left = new KDTree(sortedPoints.slice(0, mid), depth + 1); 43 | var right = new KDTree(sortedPoints.slice(mid), depth + 1); 44 | return new Node(split, left, right); 45 | } 46 | } 47 | 48 | function reportSubtree (node) { 49 | if (node instanceof Node) { 50 | var returnArr = []; 51 | return returnArr.concat(reportSubtree(node.left), reportSubtree(node.right)); 52 | } else { 53 | return node; 54 | } 55 | } 56 | 57 | function pointIsInRange (point, range) { 58 | return point.x >= range.left && point.x <= range.right && point.y >= range.bottom && point.y <= range.top; 59 | } 60 | 61 | function regionIntersection (r1, r2) { 62 | var left = Math.max(r1.left, r2.left); 63 | var bottom = Math.max(r1.bottom, r2.bottom); 64 | var right = Math.min(r1.right, r2.right); 65 | var top = Math.min(r1.top, r2.top); 66 | if (right < left || top < bottom) { 67 | return null; 68 | } else { 69 | return new Rectangle(left, bottom, right, top); 70 | } 71 | } 72 | 73 | function regionContainedInRange (region, range) { 74 | return region.left > range.left && region.bottom > range.bottom && region.right < range.right && region.top < range.top; 75 | } 76 | 77 | function searchKDTree (node, range, nodeRange, depth) { 78 | depth = depth || 0; 79 | nodeRange = nodeRange || new Rectangle(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); 80 | // base case if node is a leaf 81 | if (typeof node.x === 'number' && typeof node.y === 'number') { 82 | if (pointIsInRange(node, range)) { 83 | return node; 84 | } 85 | } else { 86 | var leftRange = new Rectangle(nodeRange.left, nodeRange.bottom, nodeRange.right, nodeRange.top); 87 | var rightRange = new Rectangle(nodeRange.left, nodeRange.bottom, nodeRange.right, nodeRange.top); 88 | if ((depth % 2) === 0) { 89 | // split on x 90 | leftRange.right = node.val; 91 | rightRange.left = node.val; 92 | } else { 93 | // split on y 94 | leftRange.top = node.val; 95 | rightRange.bottom = node.val; 96 | } 97 | var returnArr = []; 98 | var subtreeNodes; 99 | // check if left region is fully contained in range 100 | if (regionContainedInRange(leftRange, range)) { 101 | subtreeNodes = reportSubtree(node.left); 102 | if (subtreeNodes) returnArr = returnArr.concat(subtreeNodes); 103 | // else check if left region intersects range 104 | } else if (regionIntersection(leftRange, range)) { 105 | subtreeNodes = searchKDTree(node.left, range, leftRange, depth + 1); 106 | if (subtreeNodes) returnArr = returnArr.concat(subtreeNodes); 107 | } 108 | // check if right region is fully contained in range 109 | if (regionContainedInRange(rightRange, range)) { 110 | subtreeNodes = reportSubtree(node.right); 111 | if (subtreeNodes) returnArr = returnArr.concat(subtreeNodes); 112 | // else check if right region intersects range 113 | } else if (regionIntersection(rightRange, range)) { 114 | subtreeNodes = searchKDTree(node.right, range, rightRange, depth + 1); 115 | if (subtreeNodes) returnArr = returnArr.concat(subtreeNodes); 116 | } 117 | return returnArr; 118 | } 119 | } 120 | 121 | var findSnaps = function (shape) { 122 | var newSnaps = []; 123 | if (shape.type === 'rect') { 124 | var x = shape.attr('x'); 125 | var y = shape.attr('y'); 126 | var width = shape.attr('width'); 127 | var height = shape.attr('height'); 128 | var cornerSnaps = [ 129 | new Point(x, y), 130 | new Point(x + width, y), 131 | new Point(x, y + height), 132 | new Point(x + width, y + height) 133 | ]; 134 | var cardinalSnaps = [ 135 | new Point(x + width / 2, y), 136 | new Point(x, y + height / 2), 137 | new Point(x + width, y + height / 2), 138 | new Point(x + width / 2, y + height), 139 | ]; 140 | cornerSnaps.forEach(function (snap) { 141 | newSnaps.push(snap); 142 | }.bind(this)); 143 | cardinalSnaps.forEach(function (snap) { 144 | newSnaps.push(snap); 145 | }.bind(this)); 146 | } else if (shape.type === 'path') { 147 | var path = shape.attr('path'); 148 | if (path.length <= 1) { 149 | startPoint = new Point(path[0][1], path[0][2]); 150 | newSnaps.push(startPoint); 151 | } else if (path.length === 2) { 152 | startPoint = new Point(path[0][1], path[0][2]); 153 | endPoint = new Point(path[1][1], path[1][2]); 154 | midPoint = new Point(startPoint.x + (endPoint.x - startPoint.x) / 2, startPoint.y + (endPoint.y - startPoint.y) / 2); 155 | newSnaps.push(startPoint, midPoint, endPoint); 156 | } else { 157 | startPoint = new Point(path[0][1], path[0][2]); 158 | if (path[path.length - 1][0] === 'Z') { 159 | endPoint = new Point(path[path.length - 2][1], path[path.length - 2][2]); 160 | } else { 161 | endPoint = new Point(path[path.length - 1][1], path[path.length - 1][2]); 162 | } 163 | newSnaps.push(startPoint, endPoint); 164 | } 165 | } else if (shape.type === 'circle') { 166 | var cx = shape.attr('cx'); 167 | var cy = shape.attr('cy'); 168 | var r = shape.attr('r'); 169 | var centerSnap = new Point(cx, cy); 170 | cardinalSnaps = [ 171 | new Point(cx + r, cy), 172 | new Point(cx - r, cy), 173 | new Point(cx, cy + r), 174 | new Point(cx, cy - r) 175 | ]; 176 | newSnaps.push(centerSnap); 177 | cardinalSnaps.forEach(function (snap) { 178 | newSnaps.push(snap); 179 | }); 180 | } 181 | return newSnaps; 182 | } 183 | 184 | var createSnaps = function (shape) { 185 | // Visualizer.clearSnaps(); 186 | this.endSnaps[shape.myid] = findSnaps(shape); 187 | recreateKDTree(this.endSnaps); 188 | }; 189 | 190 | var deleteSnaps = function (shape) { 191 | this.endSnaps[shape.myid] = null; 192 | recreateKDTree(this.endSnaps); 193 | } 194 | 195 | var recreateKDTree = function (snaps) { 196 | var flatSnaps = []; 197 | for (var key in snaps) { 198 | if (snaps[key] !== null) { 199 | flatSnaps = flatSnaps.concat(snaps[key]); 200 | } 201 | } 202 | endSnapTree = new KDTree(flatSnaps); 203 | } 204 | 205 | function objectKeysAreEmpty (object) { 206 | for (var key in object) { 207 | if (Object.keys(object[key]).length !== 0) { 208 | return false; 209 | } 210 | } 211 | return true; 212 | } 213 | 214 | var snapToPoints = function (x, y, tolerance) { 215 | var scale = BoardData.getZoomScale(); 216 | if (!this.snapsEnabled || !endSnapTree || !endSnapTree.val) return [x, y]; 217 | if (!tolerance) tolerance = this.tolerance; 218 | tolerance *= scale; 219 | var buffer = 50 * scale; 220 | var searchBox = new Rectangle(x - (tolerance + buffer), y - (tolerance + buffer), x + (tolerance + buffer), y + (tolerance + buffer)); 221 | var localTree = searchKDTree(endSnapTree, searchBox); 222 | for (var i = 0; i < localTree.length; i++) { 223 | var pointX = localTree[i].x; 224 | var pointY = localTree[i].y; 225 | var dist = Math.sqrt(Math.pow(x - pointX, 2) + Math.pow(y - pointY, 2)); 226 | if (dist < tolerance && (!closest || dist < closestDist)) { 227 | var closest = localTree[i]; 228 | var closestDist = dist; 229 | } 230 | } 231 | Visualizer.visualizeSnaps(localTree, closest); 232 | if (closest) { 233 | return [closest.x, closest.y]; 234 | } else { 235 | return [x,y]; 236 | } 237 | }; 238 | 239 | return { 240 | endSnaps: {}, 241 | snapsEnabled: true, 242 | tolerance: 7, 243 | createSnaps: createSnaps, 244 | deleteSnaps: deleteSnaps, 245 | snapToPoints: snapToPoints 246 | }; 247 | 248 | }]); 249 | -------------------------------------------------------------------------------- /client/services/sockets.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.sockets', []) 2 | .factory('Sockets', function (socketFactory) { 3 | var myIoSocket = io.connect(); 4 | 5 | mySocket = socketFactory({ 6 | ioSocket: myIoSocket 7 | }); 8 | 9 | return mySocket; 10 | }); 11 | -------------------------------------------------------------------------------- /client/services/token.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.token', []) 2 | .factory('AttachTokens', function ($window) { 3 | var attach = { 4 | request: function (object) { 5 | var jwt = $window.localStorage.getItem('token'); 6 | if (jwt) { 7 | object.headers['x-access-token'] = jwt; 8 | } 9 | object.headers['Allow-Control-Allow-Origin'] = '*'; 10 | 11 | return object; 12 | } 13 | }; 14 | return attach; 15 | }) 16 | -------------------------------------------------------------------------------- /client/services/visualizer.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.visualizer', []) 2 | .factory('Visualizer', ['BoardData', function (BoardData) { 3 | var selectionGlow; 4 | var selected; 5 | function visualizeSelection (selection) { 6 | var board = BoardData.getBoard(); 7 | var scale = BoardData.getZoomScale() 8 | if (!selection || !(selection === selected)) { 9 | if (selectionGlow) { 10 | selectionGlow.remove(); 11 | selectionGlow.clear(); 12 | selected = null; 13 | } 14 | } 15 | if (selection && (!selectionGlow || selectionGlow.items.length === 0)) { 16 | selected = selection; 17 | selectionGlow = selection.glow({ 18 | 'color': 'blue', 19 | 'width': 10 * scale 20 | }); 21 | } 22 | } 23 | 24 | function clearSelection () { 25 | if (selectionGlow) { 26 | selectionGlow.remove(); 27 | selectionGlow.clear(); 28 | selected = null; 29 | } 30 | } 31 | 32 | var displayedSnaps; 33 | function visualizeSnaps (snaps, closest) { 34 | var board = BoardData.getBoard(); 35 | var scale = BoardData.getZoomScale(); 36 | if (!displayedSnaps) { 37 | displayedSnaps = BoardData.getBoard().set(); 38 | } else { 39 | displayedSnaps.remove(); 40 | displayedSnaps.clear(); 41 | } 42 | for (var snap in snaps) { 43 | if (snaps[snap] === closest) { 44 | displayedSnaps.push(board.circle(snaps[snap].x, snaps[snap].y, 5 * scale).attr({'stroke': 'red', 'stroke-width': 1 * scale})); 45 | } else { 46 | displayedSnaps.push(board.circle(snaps[snap].x, snaps[snap].y, 3.5 * scale).attr({'stroke': 'green', 'stroke-width': 1 * scale})); 47 | } 48 | } 49 | } 50 | 51 | function clearSnaps () { 52 | if (displayedSnaps) { 53 | displayedSnaps.remove(); 54 | displayedSnaps.clear(); 55 | } 56 | } 57 | 58 | return { 59 | visualizeSelection: visualizeSelection, 60 | visualizeSnaps: visualizeSnaps, 61 | clearSelection: clearSelection, 62 | clearSnaps: clearSnaps 63 | } 64 | }]); 65 | -------------------------------------------------------------------------------- /client/services/zoom.js: -------------------------------------------------------------------------------- 1 | angular.module('whiteboard.services.zoom', []) 2 | .factory('Zoom', ['BoardData', function (BoardData) { 3 | var last; 4 | function zoom (ev, mouseXY) { 5 | var board = BoardData.getBoard(); 6 | var scalingFactor = BoardData.getZoomScale(); 7 | var offset = BoardData.getOffset(); 8 | var originalDims = BoardData.getOriginalDims(); 9 | var currentDims = BoardData.getViewBoxDims(); 10 | 11 | if (mouseXY) { 12 | if (last) { 13 | var up; 14 | if (ev.clientY > last) { 15 | up = 1.05; 16 | } else if (ev.clientY < last) { 17 | up = 0.95; 18 | } else { 19 | up = 1; 20 | } 21 | scalingFactor = scalingFactor * up; 22 | BoardData.setZoomScale(1 / scalingFactor); 23 | } 24 | last = ev.clientY; 25 | } 26 | 27 | var newViewBoxDims = { 28 | width: originalDims.width * scalingFactor, 29 | height: originalDims.height * scalingFactor 30 | }; 31 | BoardData.setViewBoxDims(newViewBoxDims); 32 | 33 | if (mouseXY) { 34 | var newOffset = { 35 | x: offset.x, 36 | y: offset.y 37 | }; 38 | } else { 39 | var newOffset = { 40 | x: offset.x + currentDims.width / 2 - newViewBoxDims.width / 2, 41 | y: offset.y + currentDims.height / 2 - newViewBoxDims.height / 2 42 | }; 43 | } 44 | BoardData.setOffset(newOffset); 45 | 46 | board.setViewBox(newOffset.x, newOffset.y, newViewBoxDims.width, newViewBoxDims.height); 47 | }; 48 | 49 | function resetZoom () { 50 | last = null; 51 | } 52 | 53 | var startPanCoords; 54 | var startPanOffset; 55 | var newOffset; 56 | function pan (ev) { 57 | var board = BoardData.getBoard(); 58 | var scalingFactor = BoardData.getScalingFactor(); 59 | var offset = BoardData.getOffset(); 60 | var currentDims = BoardData.getViewBoxDims(); 61 | var canvasMargin = BoardData.getCanvasMargin(); 62 | 63 | var mousePosition = { 64 | x: (ev.clientX - canvasMargin.x) * scalingFactor + offset.x, 65 | y: (ev.clientY - canvasMargin.y) * scalingFactor + offset.y 66 | }; 67 | 68 | if (!startPanCoords) { 69 | startPanCoords = mousePosition; 70 | startPanOffset = offset; 71 | } else { 72 | newOffset = { 73 | x: startPanOffset.x + (startPanCoords.x - mousePosition.x), 74 | y: startPanOffset.y + (startPanCoords.y - mousePosition.y) 75 | }; 76 | 77 | board.setViewBox(newOffset.x, newOffset.y, currentDims.width, currentDims.height); 78 | } 79 | } 80 | 81 | function resetPan () { 82 | startPanCoords = startPanOffset = null; 83 | BoardData.setOffset(newOffset); 84 | } 85 | 86 | return { 87 | zoom: zoom, 88 | resetZoom: resetZoom, 89 | pan: pan, 90 | resetPan: resetPan 91 | } 92 | }]); 93 | -------------------------------------------------------------------------------- /client/styles/style.css: -------------------------------------------------------------------------------- 1 | body, 2 | html{ 3 | margin : 0; 4 | padding : 0; 5 | overflow : hidden; 6 | height:100%; 7 | width:100%; 8 | font-family: San Francisco; 9 | font-weight: 200; 10 | } 11 | 12 | h1.title { 13 | position: absolute; 14 | background-image: url('../assets/images/logo.png'); 15 | background-size: cover; 16 | background-repeat: no-repeat; 17 | right: 20px; 18 | width: 100px; 19 | height: 36px; 20 | } 21 | 22 | .app-container { 23 | height: 100%; 24 | } 25 | 26 | #board-container { 27 | height: 100%; 28 | width: 100%; 29 | } 30 | 31 | svg { 32 | height: 100%; 33 | width: 100%; 34 | position: absolute; 35 | cursor: url('../assets/images/cursors/path.png'), auto; 36 | } 37 | 38 | svg.path { 39 | cursor: url('../assets/images/cursors/path.png'), auto; 40 | } 41 | 42 | svg.line { 43 | cursor: url('../assets/images/cursors/line.png'), auto; 44 | } 45 | 46 | svg.arrow { 47 | cursor: url('../assets/images/cursors/arrow.png'), auto; 48 | } 49 | 50 | svg.rectangle { 51 | cursor: url('../assets/images/cursors/rectangle.png'), auto; 52 | } 53 | 54 | svg.circle { 55 | cursor: url('../assets/images/cursors/circle.png'), auto; 56 | } 57 | 58 | svg.text { 59 | cursor: url('../assets/images/cursors/text.png'), auto; 60 | } 61 | 62 | svg.magnify { 63 | cursor: url('../assets/images/cursors/magnify.png'), auto; 64 | } 65 | 66 | svg.eraser { 67 | cursor: url('../assets/images/cursors/eraser.png'), auto; 68 | } 69 | 70 | svg.pan { 71 | cursor: url('../assets/images/cursors/pan.png'), auto; 72 | } 73 | 74 | svg.move { 75 | cursor: url('../assets/images/cursors/move.png'), auto; 76 | } 77 | 78 | svg.copy { 79 | cursor: url('../assets/images/cursors/copy.png'), auto; 80 | } 81 | 82 | svg.fill { 83 | cursor: url('../assets/images/cursors/fill.png'), auto; 84 | } 85 | 86 | svg.strokeColor { 87 | cursor: url('../assets/images/cursors/strokeColor.png'), auto; 88 | } 89 | /* 90 | * 91 | * TOOLBAR 92 | * 93 | */ 94 | 95 | .toolbar { 96 | position: absolute; 97 | top: 0px; 98 | left: -150px; 99 | height: 100%; 100 | width: 200px; 101 | color: white; 102 | } 103 | 104 | .toolbar.show { 105 | left: 0; 106 | } 107 | 108 | .toolbar .menu { 109 | /*width: 100%;*/ 110 | width: 95.8%; 111 | /*background-color: red;*/ 112 | /*background-color: rgba(10, 10, 10, 0.7); 113 | background-color: rgba(31,67,134,0.90);*/ 114 | /*background-color: rgba(62,62,62,0.97);*/ 115 | background: rgba(53,53,53,0.99); 116 | /*padding-top: 40%;*/ 117 | /*display: table;*/ 118 | margin: 8px 8px; 119 | border-radius: 3px; 120 | } 121 | 122 | /*.menu.level-one.icon.icon-draw { 123 | background: url('../assets/images/draw.png') no-repeat 49% / 90px, linear-gradient(90deg, rgba(53,53,53,0.99) 50%, rgba(53,53,53,0.89) 50%); 124 | }*/ 125 | 126 | .toolbar .menu.level-one { 127 | /*background: linear-gradient(90deg, rgba(53,53,53,0.999) 0%, rgba(53,53,53,0.93) 0%);*/ 128 | background: linear-gradient(90deg, rgba(177,102,24,0.96) 0%, rgba(53,53,53,0.93) 0%); 129 | 130 | } 131 | 132 | .menu-text { 133 | position: absolute; 134 | right: 10px; 135 | /*margin-top: 90%;*/ 136 | height: 55.5%; 137 | } 138 | 139 | .icon-draw .menu-text { 140 | right: 9px; 141 | } 142 | .icon-tool .menu-text { 143 | right: 11px; 144 | } 145 | .icon-color .menu-text { 146 | right: 8px; 147 | } 148 | 149 | .show .menu-text { 150 | width: 96%; 151 | left: 8px; 152 | height: 66%; 153 | /*margin-top: 110%;*/ 154 | } 155 | 156 | .valign-text { 157 | /*display: table-cell; 158 | vertical-align: middle;*/ 159 | text-align: center; 160 | display: flex; 161 | justify-content: center; /* align horizontal */ 162 | align-items: flex-end; /* align vertical */ 163 | } 164 | 165 | /*.level-two-items .valign-text { 166 | margin-top: 120px; 167 | }*/ 168 | 169 | /*.level-three .valign-text { 170 | line-height: 13; 171 | }*/ 172 | 173 | .menu .wb-submenu-opener { 174 | width: 40%; 175 | height: 102.4%; 176 | margin: 0; 177 | float: right; 178 | position: relative; 179 | display: none; 180 | background-color: transparent; 181 | } 182 | 183 | .menu .wb-submenu-opener.show { 184 | display: block !important; 185 | } 186 | 187 | .toolbar .menu.level-two { 188 | width: 100%; 189 | /*height: 100%;*/ 190 | height: 97.5%; 191 | position: absolute; 192 | margin-left: 200px; 193 | /*background-color: orange;*/ 194 | /*background-color: rgba(10, 10, 10, 0.8);*/ 195 | background-color: rgba(33, 33, 33, 0); 196 | top: 0px; 197 | left: -400px; 198 | } 199 | 200 | .toolbar .tool.menu.level-two, 201 | .toolbar .draw.menu.level-two { 202 | height: 95.1%; 203 | } 204 | 205 | .toolbar .menu.level-two.show { 206 | left: 0; 207 | } 208 | 209 | .toolbar .menu.level-two.draw { 210 | height: 94.3%; 211 | } 212 | 213 | .toolbar .menu.level-two.color { 214 | height: 96.8%; 215 | } 216 | 217 | .level-two-items { 218 | height: 100%; 219 | z-index: 1; 220 | /*margin: 0px 6px;*/ 221 | margin: 0px 0 0 6px; 222 | /*background-color: rgba(53,53,53,0.99);*/ 223 | /*background: linear-gradient(90deg, rgba(53,53,53,0.999) 0%, rgba(53,53,53,0.93) 0%);*/ 224 | background: linear-gradient(90deg, rgba(177,102,24,0.96) 0%, rgba(53,53,53,0.93) 0%); 225 | border-radius: 3px; 226 | } 227 | 228 | .level-two-items:not(:last-child) { 229 | margin-bottom: 8px; 230 | } 231 | 232 | .toolbar .menu.level-three { 233 | width: 100%; 234 | height: 100%; 235 | position: absolute; 236 | margin-left: 200px; 237 | margin-right: 0px; 238 | background-color: transparent; 239 | top: 0px; 240 | left: -600px; 241 | } 242 | 243 | .level-three.level-three-container { 244 | height: 100%; 245 | width: 100%; 246 | } 247 | 248 | .toolbar .level-three.icon { 249 | width: 100%; 250 | } 251 | 252 | .toolbar .menu.level-three.show { 253 | left: 0; 254 | } 255 | 256 | .level-three-items { 257 | height: 100%; 258 | position: relative; 259 | z-index: 1; 260 | top: -8px; 261 | padding: 0 0px 8px 8px; 262 | } 263 | 264 | .level-three-items .thickness { 265 | border-radius: 3px; 266 | /*opacity: 0.9;*/ 267 | width: 100%; 268 | height: 100%; 269 | background: linear-gradient(90deg, rgba(177,102,24,0.96) 0%, rgba(53,53,53,0.93) 0%); 270 | } 271 | 272 | .color-palette { 273 | border-radius: 3px; 274 | /*opacity: 0.9;*/ 275 | width: 100%; 276 | height: 100%; 277 | } 278 | /*.level-three-items:after { 279 | content: ''; 280 | width: 100%; 281 | height: 100%; 282 | margin: 8px 8px; 283 | } 284 | */ 285 | 286 | 287 | /* 288 | * 289 | * MENU STROKE SIZES 290 | * 291 | */ 292 | 293 | .thickness.stroke10:after { 294 | width: 165px; 295 | top: 5px; 296 | border-bottom: 10px solid; 297 | 298 | content: ''; 299 | height: 50%; 300 | position: relative; 301 | display: block; 302 | margin: auto; 303 | box-sizing: border-box; 304 | } 305 | 306 | .thickness.stroke9:after { 307 | width: 150px; 308 | top: 4.5px; 309 | border-bottom: 9px solid; 310 | 311 | content: ''; 312 | height: 50%; 313 | position: relative; 314 | display: block; 315 | margin: auto; 316 | box-sizing: border-box; 317 | } 318 | 319 | .thickness.stroke8:after { 320 | width: 135px; 321 | top: 4px; 322 | border-bottom: 8px solid; 323 | 324 | content: ''; 325 | height: 50%; 326 | position: relative; 327 | display: block; 328 | margin: auto; 329 | box-sizing: border-box; 330 | } 331 | 332 | .thickness.stroke7:after { 333 | width: 120px; 334 | top: 3.5px; 335 | border-bottom: 7px solid; 336 | 337 | content: ''; 338 | height: 50%; 339 | position: relative; 340 | display: block; 341 | margin: auto; 342 | box-sizing: border-box; 343 | } 344 | 345 | .thickness.stroke6:after { 346 | width: 105px; 347 | top: 3px; 348 | border-bottom: 6px solid; 349 | 350 | content: ''; 351 | height: 50%; 352 | position: relative; 353 | display: block; 354 | margin: auto; 355 | box-sizing: border-box; 356 | } 357 | 358 | .thickness.stroke5:after { 359 | width: 90px; 360 | top: 2.5px; 361 | border-bottom: 5px solid; 362 | 363 | content: ''; 364 | height: 50%; 365 | position: relative; 366 | display: block; 367 | margin: auto; 368 | box-sizing: border-box; 369 | } 370 | 371 | .thickness.stroke4:after { 372 | width: 75px; 373 | top: 2px; 374 | border-bottom: 4px solid; 375 | 376 | content: ''; 377 | height: 50%; 378 | position: relative; 379 | display: block; 380 | margin: auto; 381 | box-sizing: border-box; 382 | } 383 | 384 | .thickness.stroke3:after { 385 | width: 50px; 386 | top: 1.5px; 387 | border-bottom: 3px solid; 388 | 389 | content: ''; 390 | height: 50%; 391 | position: relative; 392 | display: block; 393 | margin: auto; 394 | box-sizing: border-box; 395 | } 396 | 397 | .thickness.stroke2:after { 398 | width: 35px; 399 | top: 1px; 400 | border-bottom: 2px solid; 401 | 402 | content: ''; 403 | height: 50%; 404 | position: relative; 405 | display: block; 406 | margin: auto; 407 | box-sizing: border-box; 408 | } 409 | 410 | .thickness.stroke1:after { 411 | width: 20px; 412 | /*top: 0px;*/ 413 | border-bottom: 1px solid; 414 | 415 | content: ''; 416 | height: 50%; 417 | position: relative; 418 | display: block; 419 | margin: auto; 420 | box-sizing: border-box; 421 | } 422 | /* 423 | * 424 | * ICONS 425 | * 426 | */ 427 | 428 | .level-one .icon { 429 | background-repeat: no-repeat; 430 | background-size: 30px; 431 | /*background-position: 94%;*/ 432 | background-position: 94% 45%; 433 | width: 100%; 434 | height: 100%; 435 | position: absolute; 436 | width: 95.8%; 437 | /*height: 33%;*/ 438 | } 439 | 440 | .icon, 441 | .show .level-one .icon{ 442 | background-repeat: no-repeat; 443 | /*background-size: 90px;*/ 444 | /*position: relative;*/ 445 | background-size: 65px; 446 | background-position: 50%; 447 | } 448 | 449 | .level-two .icon { 450 | position: relative; 451 | } 452 | 453 | .level-three .icon { 454 | position: absolute; 455 | } 456 | 457 | /* Icons level one */ 458 | .icon-draw { 459 | background-image: url('../assets/images/draw.png'); 460 | } 461 | 462 | .icon-tool { 463 | background-image: url('../assets/images/tool.png'); 464 | } 465 | 466 | .icon-color { 467 | background-image: url('../assets/images/color.png'); 468 | } 469 | 470 | /* Icons level two/one */ 471 | .level-two .icon.icon-path { 472 | background-image: url('../assets/images/path.png'); 473 | } 474 | 475 | .icon-line { 476 | background-image: url('../assets/images/line.png'); 477 | } 478 | 479 | .level-two .icon.icon-arrow { 480 | background-image: url('../assets/images/arrow.png'); 481 | background-size: 55px; 482 | } 483 | 484 | .icon-rectangle { 485 | background-image: url('../assets/images/rectangle.png'); 486 | } 487 | 488 | .icon-circle { 489 | background-image: url('../assets/images/circle.png'); 490 | } 491 | 492 | .level-two .icon.icon-text { 493 | background-image: url('../assets/images/text.png'); 494 | background-size: 55px; 495 | } 496 | 497 | /* Icons level two/one */ 498 | .level-two .icon.icon-magnify { 499 | background-image: url('../assets/images/magnify.png'); 500 | background-size: 50px; 501 | } 502 | 503 | .icon-eraser { 504 | background-image: url('../assets/images/eraser.png'); 505 | } 506 | 507 | .icon-pan { 508 | background-image: url('../assets/images/pan.png'); 509 | } 510 | 511 | .icon-move { 512 | background-image: url('../assets/images/move.png'); 513 | } 514 | 515 | .icon-copy { 516 | background-image: url('../assets/images/copy.png'); 517 | } 518 | 519 | /* Icons level two/three */ 520 | .icon-stroke { 521 | background-image: url('../assets/images/stroke.png'); 522 | } 523 | 524 | .icon-fill { 525 | background-image: url('../assets/images/fill.png'); 526 | } 527 | 528 | .icon-thickness { 529 | background-image: url('../assets/images/thickness.png'); 530 | } 531 | 532 | /* 533 | * 534 | * FONTS 535 | * 536 | */ 537 | 538 | /** Thin */ 539 | @font-face { 540 | font-family: "San Francisco"; 541 | font-weight: 200; 542 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-thin-webfont.woff"); 543 | } 544 | 545 | /* 546 | /** Regular 547 | @font-face { 548 | font-family: "San Francisco"; 549 | font-weight: 400; 550 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-regular-webfont.woff"); 551 | } 552 | */ 553 | 554 | /* 555 | * 556 | * MEDIA QUERIES 557 | * 558 | */ 559 | 560 | @media (max-height: 950px) { 561 | 562 | .toolbar { 563 | height: 99%; 564 | } 565 | 566 | .toolbar .draw.menu.level-two { 567 | height: 94%; 568 | } 569 | 570 | .toolbar .menu.level-three { 571 | height: 99.9%; 572 | } 573 | 574 | } 575 | 576 | @media (max-height: 900px) { 577 | 578 | .toolbar { 579 | height: 99%; 580 | } 581 | 582 | /*.toolbar .menu.level-two { 583 | height: 97.7%; 584 | }*/ 585 | 586 | .toolbar .menu.level-two.tool { 587 | height: 94.9%; 588 | } 589 | 590 | .toolbar .menu.level-two.draw { 591 | height: 94.1%; 592 | } 593 | 594 | .toolbar .menu.level-three { 595 | height: 99.3%; 596 | } 597 | 598 | 599 | } 600 | 601 | @media (max-height: 850px) { 602 | 603 | /* .toolbar .menu.level-two.tool { 604 | height: 94.9%; 605 | }*/ 606 | 607 | .toolbar .menu.level-two.draw { 608 | height: 93.9%; 609 | } 610 | 611 | .toolbar .menu.level-two.color { 612 | height: 96.7%; 613 | } 614 | 615 | .toolbar .menu.level-three { 616 | height: 99%; 617 | } 618 | 619 | 620 | } 621 | 622 | @media (max-height: 800px) { 623 | 624 | .toolbar { 625 | height: 99%; 626 | } 627 | 628 | .show .menu-text { 629 | margin-top: 10%; 630 | } 631 | 632 | .valign-text { 633 | line-height: 0; 634 | } 635 | 636 | /* .level-three .valign-text { 637 | line-height: 9; 638 | }*/ 639 | 640 | .toolbar .menu.level-two { 641 | height: 97.7%; 642 | } 643 | 644 | .toolbar .menu.level-two.tool { 645 | height: 94.7%; 646 | } 647 | 648 | .toolbar .menu.level-two.draw { 649 | height: 93.7%; 650 | } 651 | 652 | .toolbar .menu.level-three { 653 | height: 98.5%; 654 | } 655 | 656 | } 657 | 658 | @media (max-height: 750px) { 659 | 660 | .toolbar .menu.level-two.draw { 661 | height: 93.5%; 662 | } 663 | 664 | .toolbar .menu.level-three { 665 | height: 97.9%; 666 | } 667 | 668 | } 669 | 670 | @media (max-height: 700px) { 671 | 672 | .toolbar { 673 | height: 99%; 674 | } 675 | 676 | /* .toolbar .menu.level-two { 677 | height: 97.7%; 678 | } 679 | */ 680 | .toolbar .menu.level-two.tool { 681 | height: 94.4%; 682 | } 683 | 684 | .toolbar .menu.level-two.draw { 685 | height: 93.4%; 686 | } 687 | 688 | .show .level-two-items .icon { 689 | background-size: 50px; 690 | } 691 | 692 | .show .level-two-items .icon.icon-fill, 693 | .show .level-two-items .icon.icon-stroke, 694 | .show .level-two-items .icon.icon-thickness { 695 | /*background-size: 45px;*/ 696 | background-size: 65px; 697 | background-position: 50% 50%; 698 | } 699 | 700 | .level-two-items .icon.icon-magnify { 701 | background-size: 40px; 702 | } 703 | 704 | .level-two-items .icon.icon-arrow { 705 | background-size: 40px; 706 | } 707 | 708 | .level-two-items .icon.icon-text { 709 | background-size: 40px; 710 | } 711 | 712 | .toolbar .menu.level-three { 713 | height: 97.2%; 714 | } 715 | 716 | } 717 | 718 | @media (max-height: 660px) { 719 | 720 | .toolbar .menu.level-two.draw { 721 | height: 93.1%; 722 | } 723 | 724 | /*.show .menu-text { 725 | margin-top: 85%; 726 | }*/ 727 | 728 | .valign-text { 729 | line-height: 0; 730 | } 731 | 732 | /* .level-three .valign-text { 733 | line-height: 9; 734 | }*/ 735 | 736 | .toolbar .menu.level-two { 737 | height: 97.9%; 738 | } 739 | 740 | .show .level-two-items .icon { 741 | /*background-size: 45px;*/ 742 | background-position: 50% 40%; 743 | } 744 | 745 | 746 | 747 | .level-two-items .icon.icon-magnify { 748 | background-size: 35px; 749 | } 750 | 751 | .toolbar .menu.level-three { 752 | height: 96.7%; 753 | } 754 | 755 | } 756 | 757 | @media (max-height: 650px) { 758 | 759 | .toolbar .menu.level-three { 760 | height: 96.6%; 761 | } 762 | 763 | } 764 | 765 | @media (max-height: 600px) { 766 | 767 | .toolbar { 768 | height: 98%; 769 | } 770 | 771 | .menu-text { 772 | /*margin-top: 70%;*/ 773 | } 774 | 775 | .toolbar .menu.level-two { 776 | height: 98.1%; 777 | } 778 | 779 | .toolbar .menu.level-two.tool { 780 | height: 94%; 781 | } 782 | 783 | .toolbar .menu.level-two.draw { 784 | height: 92.7%; 785 | } 786 | 787 | .toolbar .menu.level-three { 788 | height: 95.4%; 789 | } 790 | 791 | .level-two-items.icon { 792 | background-size: 50px; 793 | } 794 | 795 | .level-two .icon { 796 | position: relative; 797 | background-size: 65px; 798 | background-position: 50% 50%; 799 | } 800 | 801 | .show .level-two-items .icon.icon-fill, 802 | .show .level-two-items .icon.icon-stroke, 803 | .show .level-two-items .icon.icon-thickness { 804 | position: absolute; 805 | /*background-size: 65px;*/ 806 | } 807 | 808 | .level-two-items.icon.icon-magnify { 809 | background-size: 35px; 810 | } 811 | 812 | .level-two-items.icon.icon-arrow { 813 | background-size: 45px; 814 | } 815 | 816 | } 817 | 818 | @media (max-height: 550px) { 819 | 820 | .toolbar .menu.level-two.tool { 821 | height: 93.7%; 822 | } 823 | 824 | .toolbar .menu.level-two.draw { 825 | height: 92.2%; 826 | } 827 | 828 | .toolbar .menu.level-three { 829 | height: 94.5%; 830 | } 831 | 832 | .show .menu-text { 833 | margin-top: 15%; 834 | } 835 | 836 | .show .level-two-items .icon { 837 | background-size: 40px; 838 | background-position: 50% 35%; 839 | } 840 | 841 | .level-two-items .icon.icon-magnify { 842 | background-size: 35px; 843 | } 844 | 845 | .level-two-items .icon.icon-arrow { 846 | background-size: 35px; 847 | } 848 | 849 | .level-two-items .icon.icon-text { 850 | background-size: 35px; 851 | } 852 | 853 | } 854 | 855 | @media (max-height: 500px) { 856 | 857 | .toolbar { 858 | height: 97%; 859 | } 860 | 861 | /* .show .menu-text { 862 | margin-top: 15%; 863 | }*/ 864 | 865 | .toolbar .menu.level-two.tool { 866 | height: 93.4%; 867 | } 868 | 869 | .toolbar .menu.level-two.draw { 870 | height: 91.8%; 871 | } 872 | 873 | .toolbar .menu.level-two { 874 | height: 98.3%; 875 | } 876 | 877 | .toolbar .menu.level-three { 878 | height: 92.9%; 879 | } 880 | 881 | .level-two-items.icon { 882 | background-size: 40px; 883 | } 884 | 885 | .show .level-two-items .icon { 886 | background-size: 38px; 887 | background-position: 50% 30%; 888 | } 889 | 890 | .level-two-items .icon.icon-magnify { 891 | background-size: 30px; 892 | } 893 | 894 | .level-two-items .icon.icon-arrow { 895 | background-size: 30px; 896 | } 897 | 898 | .level-two-items .icon.icon-text { 899 | background-size: 30px; 900 | } 901 | 902 | } 903 | -------------------------------------------------------------------------------- /client/views/board.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | -------------------------------------------------------------------------------- /client/views/layers.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{socket}} 4 |
5 |
6 | -------------------------------------------------------------------------------- /client/views/toolbar.html: -------------------------------------------------------------------------------- 1 |
2 | 30 |
-------------------------------------------------------------------------------- /deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #find . -type f \( -name "*.js" ! -name "*-min*" \) -exec awk '1' {} + > compiled.js 4 | 5 | zip_file="whiteboard.zip" # / (root directory) 6 | if [ -e "$zip_file" ] 7 | then 8 | rm $zip_file 9 | fi 10 | 11 | #git pull --rebase upstream dev 12 | grunt release 13 | zip -r whiteboard.zip . 14 | scp -i ~/.ssh/qs2keys.pem whiteboard.zip ec2-user@52.35.39.88: 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whiteboard", 3 | "version": "1.0.0", 4 | "description": "whiteboard", 5 | "main": "server/server.js", 6 | "jshintConfig": { 7 | "bitwise": true, 8 | "camelcase": true, 9 | "curly": false, 10 | "expr": true, 11 | "eqeqeq": true, 12 | "immed": true, 13 | "indent": 4, 14 | "latedef": "nofunct", 15 | "newcap": true, 16 | "undef": false, 17 | "unused": false, 18 | "strict": false, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "maxparams": 4, 22 | "maxdepth": 2, 23 | "maxcomplexity": 6, 24 | "node": true, 25 | "browser": true, 26 | "jquery": true, 27 | "globals": { 28 | "moment": true, 29 | "before": true, 30 | "describe": true, 31 | "expect": true, 32 | "it": true 33 | } 34 | }, 35 | "dependencies": { 36 | "body-parser": "^1.14.1", 37 | "compression": "^1.6.0", 38 | "express": "^4.13.3", 39 | "kerberos": "0.0.17", 40 | "redis": "^2.4.2", 41 | "socket.io": "^1.3.7", 42 | "underscore": "^1.8.3" 43 | }, 44 | "devDependencies": { 45 | "grunt": "^0.4.5", 46 | "grunt-contrib-concat": "", 47 | "grunt-contrib-cssmin": "^0.14.0", 48 | "grunt-contrib-uglify": "", 49 | "grunt-contrib-watch": "", 50 | "matchdep": "" 51 | }, 52 | "scripts": { 53 | "start": "node server/server.js", 54 | "server": "node-inspector & nodemon --debug ./server/server.js", 55 | "test": "mocha test/test.js" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/nahash411/whiteboard.git" 60 | }, 61 | "author": "", 62 | "license": "ISC", 63 | "bugs": { 64 | "url": "https://github.com/nahash411/whiteboard/issues" 65 | }, 66 | "homepage": "https://github.com/nahash411/whiteboard#readme" 67 | } 68 | -------------------------------------------------------------------------------- /server/board.js: -------------------------------------------------------------------------------- 1 | var Board = function (sizeX, sizeY) { 2 | this.sizeX = sizeX; 3 | this.sizeY = sizeY; 4 | this.init(); 5 | }; 6 | 7 | Board.prototype.init = function () { 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /server/db/config.js: -------------------------------------------------------------------------------- 1 | var redis = require("redis"); 2 | var client = redis.createClient(); 3 | 4 | client.on("error", function (err) { 5 | console.log("Error " + err); 6 | }); 7 | 8 | module.exports = client; 9 | -------------------------------------------------------------------------------- /server/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuixoticScientist/whiteboard/eb1ab2874d15c4cc22b0716a976e5388b9013eec/server/favicon.ico -------------------------------------------------------------------------------- /server/rooms.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils/util'); 2 | var client = require('./db/config'); 3 | var _ = require('underscore'); 4 | 5 | var rooms = {}; 6 | 7 | var roomsManager = { 8 | 9 | getRoom: function (roomId) { 10 | return rooms[roomId]; 11 | }, 12 | 13 | addMember: function (socket, roomId) { 14 | 15 | // ensure there isn't double counting of roomIds in client side ('/roomId' and 'roomId' emit separately) 16 | if (roomId[0] === '/') { 17 | roomId = roomId.slice(1); 18 | } 19 | 20 | socket.room = roomId; 21 | socket.join(roomId); 22 | 23 | if (!rooms[roomId]) { 24 | rooms[roomId] = {}; 25 | } 26 | 27 | client.get(roomId, function (err, reply) { 28 | if (reply) { 29 | storedRoom = JSON.parse(reply); 30 | _.extend(rooms[roomId], storedRoom); 31 | } else { 32 | client.set(roomId, JSON.stringify({})); 33 | rooms[roomId] = {}; 34 | } 35 | 36 | if (!rooms[roomId]) { 37 | rooms[roomId] = {}; 38 | } 39 | 40 | // add member to room based on socket id 41 | // console.log(rooms[roomId]); 42 | var socketId = socket.id; 43 | rooms[roomId][socketId] = {}; 44 | socket.emit('showExisting', rooms[roomId]); 45 | //console.log(rooms[roomId]); 46 | 47 | var count = 0; 48 | for (var member in rooms[roomId]) { 49 | count++; 50 | } 51 | // console.log('Current room ' + roomId + ' has ' + count + ' members'); 52 | }); 53 | }, 54 | 55 | addShape: function (shape, socket) { 56 | rooms[socket.room][shape.socketId][shape.myid] = shape; 57 | }, 58 | 59 | editShape: function (shape, socket) { 60 | rooms[socket.room][shape.socketId][shape.myid]['mouseX'] = shape.mouseX; 61 | rooms[socket.room][shape.socketId][shape.myid]['mouseY'] = shape.mouseY; 62 | }, 63 | 64 | moveShape: function (shape, socket) { 65 | var storedShape = rooms[socket.room][shape.socketId][shape.myid]; 66 | if (shape.attr.r) { 67 | storedShape.initX = shape.attr.cx; 68 | storedShape.initY = shape.attr.cy; 69 | storedShape.mouseX = shape.attr.cx + shape.attr.r; 70 | storedShape.mouseY = shape.attr.cy; 71 | } else if (shape.attr.width) { 72 | storedShape.initX = shape.attr.x; 73 | storedShape.initY = shape.attr.y; 74 | storedShape.mouseX = shape.attr.x + shape.attr.width; 75 | storedShape.mouseY = shape.attr.y + shape.attr.height; 76 | } else if (shape.attr.text) { 77 | storedShape.initX = shape.attr.x; 78 | storedShape.initY = shape.attr.y; 79 | } else { 80 | if (shape.pathDProps) { 81 | storedShape.pathDProps = shape.pathDProps; 82 | } else { 83 | var path = shape.attr.path; 84 | storedShape.initX = path[0][1]; 85 | storedShape.initY = path[0][2]; 86 | storedShape.mouseX = path[1][1]; 87 | storedShape.mouseY = path[1][2]; 88 | } 89 | } 90 | }, 91 | 92 | completePath: function (shape, socket) { 93 | rooms[socket.room][socket.id][shape.myid]['pathDProps'] = shape.pathDProps; 94 | client.set(socket.room, JSON.stringify(rooms[socket.room])); 95 | }, 96 | 97 | completeShape: function (shape, socket) { 98 | if (shape.tool && shape.tool.text) { 99 | rooms[socket.room][socket.id][shape.myid]['tool'] = shape.tool; 100 | } 101 | client.set(socket.room, JSON.stringify(rooms[socket.room])); 102 | }, 103 | 104 | deleteShape: function (shape, socket) { 105 | delete rooms[socket.room][shape.socketId][shape.myid]; 106 | client.set(socket.room, JSON.stringify(rooms[socket.room])); 107 | } 108 | 109 | } 110 | 111 | module.exports = roomsManager; 112 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var http = require('http'); 4 | var bodyParser = require('body-parser'); 5 | var util = require('./utils/util'); 6 | var rooms = require('./rooms'); 7 | var client = require('./db/config'); 8 | var fs = require('fs'); 9 | var compression = require('compression'); 10 | 11 | app.use(compression()); 12 | app.use(express.static(__dirname + '/../client')); 13 | app.use(express.static(__dirname + '/lib')); 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({ extended: true })); 16 | 17 | var port = process.env.PORT || '3000'; 18 | app.set('port', port); 19 | 20 | var server = http.createServer(app); 21 | var io = require('./sockets')(server); 22 | 23 | app.get('/:id', function (req, res) { 24 | res.sendfile('./client/index.html'); 25 | }); 26 | 27 | app.get('/:id/screenShot', function (req, res) { 28 | webshot('localhost:3000/' + req.params.id, req.params.id + '.png', function(err) { 29 | res.sendfile(req.params.id + '.png'); 30 | }); 31 | }) 32 | 33 | var start = function () { 34 | server.listen(port); 35 | }; 36 | 37 | var end = function () { 38 | server.close(); 39 | }; 40 | 41 | start(); 42 | 43 | exports.start = start; 44 | exports.end = end; 45 | exports.app = app; 46 | -------------------------------------------------------------------------------- /server/sockets.js: -------------------------------------------------------------------------------- 1 | var socketio = require('socket.io'); 2 | var rooms = require('./rooms'); 3 | var client = require('./db/config'); 4 | var _ = require('underscore'); 5 | 6 | module.exports = function(server) { 7 | 8 | var room = {}; 9 | var board = {}; 10 | 11 | var io = socketio.listen(server); 12 | 13 | io.on('connection', function (socket) { 14 | 15 | setInterval(function() { 16 | socket.emit('heartbeat'); 17 | }, 5000); 18 | 19 | socket.on('heartbeat', function () { 20 | }) 21 | 22 | socket.on('idRequest', function () { 23 | socket.emit('socketId', {socketId: socket.id}); 24 | }); 25 | 26 | socket.on('roomId', function (data) { 27 | rooms.addMember(socket, data.roomId); 28 | }); 29 | 30 | socket.on('newShape', function (data) { 31 | socket.to(this.room).emit('shapeCreated', data); 32 | rooms.addShape(data, socket); 33 | }); 34 | 35 | socket.on('editShape', function (data) { 36 | socket.to(this.room).emit('shapeEdited', data); 37 | if (data.tool.name !== 'text') { 38 | rooms.editShape(data, socket); 39 | } 40 | }); 41 | 42 | socket.on('shapeCompleted', function (data) { 43 | socket.to(this.room).emit('shapeCompleted', { 44 | socketId: socket.id, 45 | myid: data.myid, 46 | tool: data.tool 47 | }); 48 | rooms.completeShape(data, socket); 49 | }); 50 | 51 | socket.on('pathCompleted', function (data) { 52 | socket.to(this.room).emit('shapeCompleted', { 53 | socketId: socket.id, 54 | myid: data.myid, 55 | tool: data.tool 56 | }); 57 | rooms.completePath(data, socket); 58 | }); 59 | 60 | socket.on('copiedPathCompleted', function (data) { 61 | socket.to(this.room).emit('copiedPathCompleted', { 62 | socketId: socket.id, 63 | myid: data.myid, 64 | tool: data.tool, 65 | pathDProps: data.pathDProps 66 | }); 67 | rooms.completePath(data, socket); 68 | }) 69 | 70 | socket.on('moveShape', function (data) { 71 | rooms.moveShape(data, socket); 72 | socket.to(this.room).emit('shapeMoved', data); 73 | }); 74 | 75 | socket.on('finishMovingShape', function (data) { 76 | rooms.completeShape(data, socket); 77 | socket.to(this.room).emit('shapeFinishedMoving', data); 78 | }); 79 | 80 | socket.on('deleteShape', function (data) { 81 | rooms.deleteShape(data, socket); 82 | socket.to(this.room).emit('shapeDeleted', {myid: data.myid, socketId: data.socketId}); 83 | }); 84 | 85 | socket.on('disconnect', function () { 86 | }); 87 | 88 | }); 89 | 90 | return io; 91 | 92 | }; 93 | -------------------------------------------------------------------------------- /server/utils/util.js: -------------------------------------------------------------------------------- 1 | var generateRandomId = function (length) { 2 | var id = ""; 3 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 4 | 5 | for (var i = 0; i < length; i++) { 6 | id += chars.charAt(Math.floor(Math.random() * chars.length)); 7 | } 8 | 9 | return id; 10 | }; 11 | 12 | module.exports = { 13 | generateRandomId: generateRandomId 14 | }; 15 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | 5 | var db = require('./../server/db/config'); 6 | var Board = require('./../server/db/models/board'); 7 | 8 | var request = require('supertest'); 9 | var server = require('./../server/server'); 10 | var serverUrl = 'http://localhost:3000'; 11 | 12 | describe('HTTP', function () { 13 | before(function () { 14 | server.start(); 15 | }); 16 | 17 | after(function () { 18 | server.end(); 19 | }); 20 | 21 | describe('GET /:id', function () { 22 | 23 | it('should get /:id', function (done) { 24 | var id = ''; 25 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 26 | var length = 5; 27 | for (var i = 0; i < length; i++) { 28 | id += chars.charAt(Math.floor(Math.random() * chars.length)); 29 | } 30 | 31 | request(serverUrl) 32 | .get('/:' + id) 33 | .expect(200, done); 34 | }); 35 | }); 36 | 37 | }); 38 | // 39 | --------------------------------------------------------------------------------