├── .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)
3 | [](#sponsors)
4 |
5 | A simple sketchpad project. ([Live Demo](http://yiom.github.io/sketchpad/))
6 |
7 | [](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 |
--------------------------------------------------------------------------------