├── .babelrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── lib └── sketchpad.js ├── package.json ├── scripts └── sketchpad.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary files 2 | *.swp 3 | *~ 4 | 5 | # others 6 | .DS_Store 7 | node_modules 8 | *.log 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 YIOM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sketchpad 2 | [![Backers on Open Collective](https://opencollective.com/sketchpad/backers/badge.svg)](#backers) 3 | [![Sponsors on Open Collective](https://opencollective.com/sketchpad/sponsors/badge.svg)](#sponsors) 4 | 5 | A simple sketchpad project. ([Live Demo](http://yiom.github.io/sketchpad/)) 6 | 7 | [![NPM](https://nodei.co/npm/sketchpad.png?downloads=true)](https://nodei.co/npm/sketchpad/) 8 | 9 | ## Authors 10 | - [Nihey Takizawa](https://github.com/nihey) 11 | - [Jean Lucas](https://github.com/jeanleonino) 12 | 13 | ## Installation 14 | To install Sketchpad via [Bower](https://github.com/bower/bower): 15 | ``` 16 | $ bower install sketchpad --save 17 | ``` 18 | or use npm: 19 | ``` 20 | npm install sketchpad 21 | ``` 22 | 23 | ## Usage 24 | 25 | Having a canvas on the DOM: 26 | ```html 27 | 28 | ``` 29 | You should simply configure it by instantiating the Sketchpad: 30 | ```js 31 | var sketchpad = new Sketchpad({ 32 | element: '#sketchpad', 33 | width: 400, 34 | height: 400, 35 | }); 36 | ``` 37 | After that, the API provides a variety of functionalities: 38 | ```js 39 | // undo 40 | sketchpad.undo(); 41 | 42 | // redo 43 | sketchpad.redo(); 44 | 45 | // Change color 46 | sketchpad.color = '#FF0000'; 47 | 48 | // Change stroke size 49 | sketchpad.penSize = 10; 50 | 51 | // Playback each sketchpad stroke (10 ms is the time between each line piece) 52 | sketchpad.animate(10); 53 | ``` 54 | 55 | For more documentation about the project, visit: TBA 56 | 57 | ## Contributing 58 | 59 | * Fork this repository. 60 | * Install with `npm install` 61 | * Send a PR 62 | 63 | 64 | ### Contributors 65 | 66 | This project exists thanks to all the people who contribute. 67 | 68 | 69 | 70 | ## Backers 71 | 72 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/sketchpad#backer)] 73 | 74 | 75 | 76 | 77 | ## Sponsors 78 | 79 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/sketchpad#sponsor)] 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /lib/sketchpad.js: -------------------------------------------------------------------------------- 1 | class Sketchpad { 2 | constructor(options) { 3 | // Support both old api (element) and new (canvas) 4 | options.canvas = options.canvas || options.element; 5 | if (!options.canvas) { 6 | console.error('[SKETCHPAD]: Please provide an element/canvas:'); 7 | return; 8 | } 9 | 10 | if (typeof options.canvas === 'string') { 11 | options.canvas = document.querySelector(options.canvas); 12 | } 13 | 14 | this.canvas = options.canvas; 15 | 16 | // Try to extract 'width', 'height', 'color', 'penSize' and 'readOnly' 17 | // from the options or the DOM element. 18 | ['width', 'height', 'color', 'penSize', 'readOnly'].forEach(function(attr) { 19 | this[attr] = options[attr] || this.canvas.getAttribute('data-' + attr); 20 | }, this); 21 | 22 | // Setting default values 23 | this.width = this.width || 0; 24 | this.height = this.height || 0; 25 | 26 | this.color = this.color || '#000'; 27 | this.penSize = this.penSize || 5; 28 | 29 | this.readOnly = this.readOnly || false; 30 | 31 | // Sketchpad History settings 32 | this.strokes = options.strokes || []; 33 | 34 | this.undoHistory = options.undoHistory || []; 35 | 36 | // Enforce context for Moving Callbacks 37 | this.onMouseMove = this.onMouseMove.bind(this); 38 | 39 | // Setup Internal Events 40 | this.events = {}; 41 | this.events['mousemove'] = []; 42 | this.internalEvents = ['MouseDown', 'MouseUp', 'MouseOut']; 43 | this.internalEvents.forEach(function(name) { 44 | let lower = name.toLowerCase(); 45 | this.events[lower] = []; 46 | 47 | // Enforce context for Internal Event Functions 48 | this['on' + name] = this['on' + name].bind(this); 49 | 50 | // Add DOM Event Listeners 51 | this.canvas.addEventListener(lower, (...args) => this.trigger(lower, args)); 52 | }, this); 53 | 54 | this.reset(); 55 | } 56 | 57 | /* 58 | * Private API 59 | */ 60 | 61 | _position(event) { 62 | return { 63 | x: event.pageX - this.canvas.offsetLeft, 64 | y: event.pageY - this.canvas.offsetTop, 65 | }; 66 | } 67 | 68 | _stroke(stroke) { 69 | if (stroke.type === 'clear') { 70 | return this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 71 | } 72 | 73 | stroke.lines.forEach(function(line) { 74 | this._line(line.start, line.end, stroke.color, stroke.size); 75 | }, this); 76 | } 77 | 78 | _draw(start, end, color, size) { 79 | this._line(start, end, color, size, 'source-over'); 80 | } 81 | 82 | _erase(start, end, color, size) { 83 | this._line(start, end, color, size, 'destination-out'); 84 | } 85 | 86 | _line(start, end, color, size, compositeOperation) { 87 | this.context.save(); 88 | this.context.lineJoin = 'round'; 89 | this.context.lineCap = 'round'; 90 | this.context.strokeStyle = color; 91 | this.context.lineWidth = size; 92 | this.context.globalCompositeOperation = compositeOperation; 93 | this.context.beginPath(); 94 | this.context.moveTo(start.x, start.y); 95 | this.context.lineTo(end.x, end.y); 96 | this.context.closePath(); 97 | this.context.stroke(); 98 | this.context.restore(); 99 | } 100 | 101 | /* 102 | * Events/Callback 103 | */ 104 | 105 | onMouseDown(event) { 106 | this._sketching = true; 107 | this._lastPosition = this._position(event); 108 | this._currentStroke = { 109 | color: this.color, 110 | size: this.penSize, 111 | lines: [], 112 | }; 113 | 114 | this.canvas.addEventListener('mousemove', this.onMouseMove); 115 | } 116 | 117 | onMouseUp(event) { 118 | if (this._sketching) { 119 | this.strokes.push(this._currentStroke); 120 | this._sketching = false; 121 | } 122 | 123 | this.canvas.removeEventListener('mousemove', this.onMouseMove); 124 | } 125 | 126 | onMouseOut(event) { 127 | this.onMouseUp(event); 128 | } 129 | 130 | onMouseMove(event) { 131 | let currentPosition = this._position(event); 132 | this._draw(this._lastPosition, currentPosition, this.color, this.penSize); 133 | this._currentStroke.lines.push({ 134 | start: this._lastPosition, 135 | end: currentPosition, 136 | }); 137 | this._lastPosition = currentPosition; 138 | 139 | this.trigger('mousemove', [event]); 140 | } 141 | 142 | /* 143 | * Public API 144 | */ 145 | 146 | toObject() { 147 | return { 148 | width: this.canvas.width, 149 | height: this.canvas.height, 150 | strokes: this.strokes, 151 | undoHistory: this.undoHistory, 152 | }; 153 | } 154 | 155 | toJSON() { 156 | return JSON.stringify(this.toObject()); 157 | } 158 | 159 | redo() { 160 | var stroke = this.undoHistory.pop(); 161 | if (stroke) { 162 | this.strokes.push(stroke); 163 | this._stroke(stroke); 164 | } 165 | } 166 | 167 | undo() { 168 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 169 | var stroke = this.strokes.pop(); 170 | this.redraw(); 171 | 172 | if (stroke) { 173 | this.undoHistory.push(stroke); 174 | } 175 | } 176 | 177 | clear() { 178 | this.strokes.push({ 179 | type: 'clear', 180 | }); 181 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 182 | } 183 | 184 | redraw() { 185 | this.strokes.forEach(function(stroke) { 186 | this._stroke(stroke); 187 | }, this); 188 | } 189 | 190 | reset() { 191 | // Setup canvas 192 | this.canvas.width = this.width; 193 | this.canvas.height = this.height; 194 | this.context = this.canvas.getContext('2d'); 195 | 196 | // Redraw image 197 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 198 | this.redraw(); 199 | 200 | // Remove all event listeners, this way readOnly option will be respected 201 | // on the reset 202 | this.internalEvents.forEach(name => this.off(name.toLowerCase())); 203 | 204 | if (this.readOnly) { 205 | return; 206 | } 207 | 208 | // Re-Attach all event listeners 209 | this.internalEvents.forEach(name => this.on(name.toLowerCase(), this['on' + name])); 210 | } 211 | 212 | cancelAnimation() { 213 | this.animateIds = this.animateIds || []; 214 | this.animateIds.forEach(function(id) { 215 | clearTimeout(id); 216 | }); 217 | this.animateIds = []; 218 | } 219 | 220 | animate(interval=10, loop=false, loopInterval=0) { 221 | let delay = interval; 222 | 223 | this.cancelAnimation(); 224 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 225 | 226 | this.strokes.forEach(stroke => { 227 | if (stroke.type === 'clear') { 228 | delay += interval; 229 | return this.animateIds.push(setTimeout(() => { 230 | this.context.clearRect(0, 0, this.canvas.width, 231 | this.canvas.height); 232 | }, delay)); 233 | } 234 | 235 | stroke.lines.forEach(line => { 236 | delay += interval; 237 | this.animateIds.push(setTimeout(() => { 238 | this._draw(line.start, line.end, stroke.color, stroke.size); 239 | }, delay)); 240 | }); 241 | }); 242 | 243 | if (loop) { 244 | this.animateIds.push(setTimeout(() => { 245 | this.animate(interval=10, loop, loopInterval); 246 | }, delay + interval + loopInterval)); 247 | } 248 | 249 | this.animateIds(setTimeout(() => { 250 | this.trigger('animation-end', [interval, loop, loopInterval]); 251 | }, delay + interval)); 252 | } 253 | 254 | /* 255 | * Event System 256 | */ 257 | 258 | /* Attach an event callback 259 | * 260 | * @param {String} action Which action will have a callback attached 261 | * @param {Function} callback What will be executed when this event happen 262 | */ 263 | on(action, callback) { 264 | // Tell the user if the action he has input was invalid 265 | if (this.events[action] === undefined) { 266 | return console.error(`Sketchpad: No such action '${action}'`); 267 | } 268 | 269 | this.events[action].push(callback); 270 | } 271 | 272 | /* Detach an event callback 273 | * 274 | * @param {String} action Which action will have event(s) detached 275 | * @param {Function} callback Which function will be detached. If none is 276 | * provided, all callbacks are detached 277 | */ 278 | off(action, callback) { 279 | if (callback) { 280 | // If a callback has been specified delete it specifically 281 | var index = this.events[action].indexOf(callback); 282 | (index !== -1) && this.events[action].splice(index, 1); 283 | return index !== -1; 284 | } 285 | 286 | // Else just erase all callbacks 287 | this.events[action] = []; 288 | } 289 | 290 | /* Trigger an event 291 | * 292 | * @param {String} action Which event will be triggered 293 | * @param {Array} args Which arguments will be provided to the callbacks 294 | */ 295 | trigger(action, args=[]) { 296 | // Fire all events with the given callback 297 | this.events[action].forEach(function(callback) { 298 | callback(...args); 299 | }); 300 | } 301 | } 302 | 303 | window.Sketchpad = Sketchpad; 304 | module.exports = Sketchpad; 305 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amaplex-software/sketchpad", 3 | "version": "0.1.0", 4 | "description": "A simple sketchpad.", 5 | "public": true, 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/yiom/sketchpad.git" 9 | }, 10 | "scripts": { 11 | "build": "webpack && webpack -p --output-filename sketchpad.min.js", 12 | "watch": "webpack -d --watch", 13 | "test": "eslint .", 14 | "postinstall": "opencollective postinstall" 15 | }, 16 | "keywords": [ 17 | "sketchpad", 18 | "javascript" 19 | ], 20 | "author": "Yiom", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/yiom/sketchpad/issues" 24 | }, 25 | "devDependencies": { 26 | "babel": "^6.5.2", 27 | "babel-core": "^6.7.7", 28 | "babel-loader": "^6.2.4", 29 | "babel-preset-es2015": "^6.6.0", 30 | "babel-preset-stage-0": "^6.5.0", 31 | "webpack": "^1.13.0" 32 | }, 33 | "dependencies": { 34 | "opencollective": "^1.0.3" 35 | }, 36 | "collective": { 37 | "type": "opencollective", 38 | "url": "https://opencollective.com/sketchpad" 39 | } 40 | } -------------------------------------------------------------------------------- /scripts/sketchpad.js: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2014-2016 YIOM 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | function Sketchpad(config) { 24 | // Enforces the context for all functions 25 | for (var key in this.constructor.prototype) { 26 | this[key] = this[key].bind(this); 27 | } 28 | 29 | // Warn the user if no DOM element was selected 30 | if (!config.hasOwnProperty('element')) { 31 | console.error('SKETCHPAD ERROR: No element selected'); 32 | return; 33 | } 34 | 35 | if (typeof(config.element) === 'string') { 36 | this.element = $(config.element); 37 | } 38 | else { 39 | this.element = config.element; 40 | } 41 | 42 | // Width can be defined on the HTML or programatically 43 | this._width = config.width || this.element.attr('data-width') || 0; 44 | this._height = config.height || this.element.attr('data-height') || 0; 45 | 46 | // Pen attributes 47 | this.color = config.color || this.element.attr('data-color') || '#000000'; 48 | this.penSize = config.penSize || this.element.attr('data-penSize') || 5; 49 | 50 | // ReadOnly sketchpads may not be modified 51 | this.readOnly = config.readOnly || 52 | this.element.attr('data-readOnly') || 53 | false; 54 | if (!this.readOnly) { 55 | this.element.css({cursor: 'crosshair'}); 56 | } 57 | 58 | // Stroke control variables 59 | this.strokes = config.strokes || []; 60 | this._currentStroke = { 61 | color: null, 62 | size: null, 63 | lines: [], 64 | }; 65 | 66 | // Undo History 67 | this.undoHistory = config.undoHistory || []; 68 | 69 | // Animation function calls 70 | this.animateIds = []; 71 | 72 | // Set sketching state 73 | this._sketching = false; 74 | 75 | // Setup canvas sketching listeners 76 | this.reset(); 77 | } 78 | 79 | // 80 | // Private API 81 | // 82 | 83 | Sketchpad.prototype._cursorPosition = function(event) { 84 | return { 85 | x: event.pageX - $(this.canvas).offset().left, 86 | y: event.pageY - $(this.canvas).offset().top, 87 | }; 88 | }; 89 | 90 | Sketchpad.prototype._draw = function(start, end, color, size) { 91 | this._stroke(start, end, color, size, 'source-over'); 92 | }; 93 | 94 | Sketchpad.prototype._erase = function(start, end, color, size) { 95 | this._stroke(start, end, color, size, 'destination-out'); 96 | }; 97 | 98 | Sketchpad.prototype._stroke = function(start, end, color, size, compositeOperation) { 99 | this.context.save(); 100 | this.context.lineJoin = 'round'; 101 | this.context.lineCap = 'round'; 102 | this.context.strokeStyle = color; 103 | this.context.lineWidth = size; 104 | this.context.globalCompositeOperation = compositeOperation; 105 | this.context.beginPath(); 106 | this.context.moveTo(start.x, start.y); 107 | this.context.lineTo(end.x, end.y); 108 | this.context.closePath(); 109 | this.context.stroke(); 110 | 111 | this.context.restore(); 112 | }; 113 | 114 | // 115 | // Callback Handlers 116 | // 117 | 118 | Sketchpad.prototype._mouseDown = function(event) { 119 | this._lastPosition = this._cursorPosition(event); 120 | this._currentStroke.color = this.color; 121 | this._currentStroke.size = this.penSize; 122 | this._currentStroke.lines = []; 123 | this._sketching = true; 124 | this.canvas.addEventListener('mousemove', this._mouseMove); 125 | }; 126 | 127 | Sketchpad.prototype._mouseUp = function(event) { 128 | if (this._sketching) { 129 | 130 | // Check that the current stroke is not empty 131 | if (this._currentStroke.lines.length > 0) { 132 | this.strokes.push($.extend(true, {}, this._currentStroke)); 133 | } 134 | 135 | this._sketching = false; 136 | } 137 | this.canvas.removeEventListener('mousemove', this._mouseMove); 138 | }; 139 | 140 | Sketchpad.prototype._mouseMove = function(event) { 141 | var currentPosition = this._cursorPosition(event); 142 | 143 | this._draw(this._lastPosition, currentPosition, this.color, this.penSize); 144 | this._currentStroke.lines.push({ 145 | start: $.extend(true, {}, this._lastPosition), 146 | end: $.extend(true, {}, currentPosition), 147 | }); 148 | 149 | this._lastPosition = currentPosition; 150 | }; 151 | 152 | Sketchpad.prototype._touchStart = function(event) { 153 | event.preventDefault(); 154 | if (this._sketching) { 155 | return; 156 | } 157 | this._lastPosition = this._cursorPosition(event.changedTouches[0]); 158 | this._currentStroke.color = this.color; 159 | this._currentStroke.size = this.penSize; 160 | this._currentStroke.lines = []; 161 | this._sketching = true; 162 | this.canvas.addEventListener('touchmove', this._touchMove, false); 163 | }; 164 | 165 | Sketchpad.prototype._touchEnd = function(event) { 166 | event.preventDefault(); 167 | if (this._sketching) { 168 | this.strokes.push($.extend(true, {}, this._currentStroke)); 169 | this._sketching = false; 170 | } 171 | this.canvas.removeEventListener('touchmove', this._touchMove); 172 | }; 173 | 174 | Sketchpad.prototype._touchCancel = function(event) { 175 | event.preventDefault(); 176 | if (this._sketching) { 177 | this.strokes.push($.extend(true, {}, this._currentStroke)); 178 | this._sketching = false; 179 | } 180 | this.canvas.removeEventListener('touchmove', this._touchMove); 181 | }; 182 | 183 | Sketchpad.prototype._touchLeave = function(event) { 184 | event.preventDefault(); 185 | if (this._sketching) { 186 | this.strokes.push($.extend(true, {}, this._currentStroke)); 187 | this._sketching = false; 188 | } 189 | this.canvas.removeEventListener('touchmove', this._touchMove); 190 | }; 191 | 192 | Sketchpad.prototype._touchMove = function(event) { 193 | event.preventDefault(); 194 | var currentPosition = this._cursorPosition(event.changedTouches[0]); 195 | 196 | this._draw(this._lastPosition, currentPosition, this.color, this.penSize); 197 | this._currentStroke.lines.push({ 198 | start: $.extend(true, {}, this._lastPosition), 199 | end: $.extend(true, {}, currentPosition), 200 | }); 201 | 202 | this._lastPosition = currentPosition; 203 | }; 204 | 205 | // 206 | // Public API 207 | // 208 | 209 | Sketchpad.prototype.reset = function() { 210 | // Set attributes 211 | this.canvas = this.element[0]; 212 | this.canvas.width = this._width; 213 | this.canvas.height = this._height; 214 | this.context = this.canvas.getContext('2d'); 215 | 216 | // Setup event listeners 217 | this.redraw(this.strokes); 218 | 219 | if (this.readOnly) { 220 | return; 221 | } 222 | 223 | // Mouse 224 | this.canvas.addEventListener('mousedown', this._mouseDown); 225 | this.canvas.addEventListener('mouseout', this._mouseUp); 226 | this.canvas.addEventListener('mouseup', this._mouseUp); 227 | 228 | // Touch 229 | this.canvas.addEventListener('touchstart', this._touchStart); 230 | this.canvas.addEventListener('touchend', this._touchEnd); 231 | this.canvas.addEventListener('touchcancel', this._touchCancel); 232 | this.canvas.addEventListener('touchleave', this._touchLeave); 233 | }; 234 | 235 | Sketchpad.prototype.drawStroke = function(stroke) { 236 | for (var j = 0; j < stroke.lines.length; j++) { 237 | var line = stroke.lines[j]; 238 | this._draw(line.start, line.end, stroke.color, stroke.size); 239 | } 240 | }; 241 | 242 | Sketchpad.prototype.redraw = function(strokes) { 243 | for (var i = 0; i < strokes.length; i++) { 244 | this.drawStroke(strokes[i]); 245 | } 246 | }; 247 | 248 | Sketchpad.prototype.toObject = function() { 249 | return { 250 | width: this.canvas.width, 251 | height: this.canvas.height, 252 | strokes: this.strokes, 253 | undoHistory: this.undoHistory, 254 | }; 255 | }; 256 | 257 | Sketchpad.prototype.toJSON = function() { 258 | return JSON.stringify(this.toObject()); 259 | }; 260 | 261 | Sketchpad.prototype.animate = function(ms, loop, loopDelay) { 262 | this.clear(); 263 | var delay = ms; 264 | var callback = null; 265 | for (var i = 0; i < this.strokes.length; i++) { 266 | var stroke = this.strokes[i]; 267 | for (var j = 0; j < stroke.lines.length; j++) { 268 | var line = stroke.lines[j]; 269 | callback = this._draw.bind(this, line.start, line.end, 270 | stroke.color, stroke.size); 271 | this.animateIds.push(setTimeout(callback, delay)); 272 | delay += ms; 273 | } 274 | } 275 | if (loop) { 276 | loopDelay = loopDelay || 0; 277 | callback = this.animate.bind(this, ms, loop, loopDelay); 278 | this.animateIds.push(setTimeout(callback, delay + loopDelay)); 279 | } 280 | }; 281 | 282 | Sketchpad.prototype.cancelAnimation = function() { 283 | for (var i = 0; i < this.animateIds.length; i++) { 284 | clearTimeout(this.animateIds[i]); 285 | } 286 | }; 287 | 288 | Sketchpad.prototype.clear = function() { 289 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 290 | }; 291 | 292 | Sketchpad.prototype.undo = function() { 293 | this.clear(); 294 | var stroke = this.strokes.pop(); 295 | if (stroke) { 296 | this.undoHistory.push(stroke); 297 | this.redraw(this.strokes); 298 | } 299 | }; 300 | 301 | Sketchpad.prototype.redo = function() { 302 | var stroke = this.undoHistory.pop(); 303 | if (stroke) { 304 | this.strokes.push(stroke); 305 | this.drawStroke(stroke); 306 | } 307 | }; 308 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(path.join(__dirname, 'lib', 'sketchpad.js')), 5 | output: { 6 | path: path.resolve(path.join(__dirname, '.', 'dist')), 7 | library: 'module', 8 | libraryTarget: 'umd', 9 | filename: 'sketchpad.js', 10 | }, 11 | module: { 12 | loaders: [{test: /\.js?$/, exclude: /(node_modules|bower_components)/, loader: 'babel'}], 13 | }, 14 | }; 15 | --------------------------------------------------------------------------------