/*!
* jSketch 1.1 | Luis A. Leiva | MIT license
* A simple JavaScript library for drawing facilities on HTML5 canvas.
*/
/**
* A simple JavaScript library for drawing facilities on HTML5 canvas.
* This class is mostly a wrapper for the HTML5 canvas API with some syntactic sugar,
* such as function chainability and old-school AS3-like notation.
* @name jSketch
* @class
* @version 1.1
* @author Luis A. Leiva
* @license MIT license
* @example
* var canvas1 = document.getElementById('foo');
* var canvas2 = document.getElementById('bar');
* // Instantiate once, reuse everywhere.
* var brush = new jSketch(canvas1).lineStyle('red').moveTo(50,50).lineTo(10,10).stroke();
* // Actually, `.moveTo(50,50).lineTo(10,10)` can be just `.line(50,50, 10,10)`.
* // Switching between contexts removes the need of having to reinstantiate the jSketch class.
* brush.setContext(canvas2).beginFill('#5F7').fillCircle(30,30,8).endFill();
*/
;(function(window) {
/**
* @constructor
* @param {object|strig} elem - DOM element or selector.
* @param {object} [options] - Configuration (default: {@link Sketchable#defaults}).
*/
function jSketch(elem, options) {
if (!elem) throw new Error('Sketchable requires a DOM element or selector.');
if (typeof elem === 'string') elem = document.querySelector(elem);
// Set drawing context first.
this.setContext(elem);
// Scene defaults.
this.stageWidth = elem.offsetWidth;
this.stageHeight = elem.offsetHeight;
// Make room for storing some data such as line type, colors, etc.
this.data = options;
// Save abstract calls to low-level canvas methods,
// this way we can reproduce the drawing in different renderers.
this.callStack = [];
// Set drawing defaults.
// All methods are chainable.
return this.setDefaults();
};
/**
* jSketch methods (publicly extensible).
* @ignore
*/
jSketch.prototype = {
/**
* Allows to change the drawing context at runtime.
* @param {object} elem - DOM element.
* @return {object} jSketch
* @memberof jSketch
*/
setContext: function(elem) {
if (!elem) throw new Error('No canvas element specified.');
// Save shortcuts: canvas (DOM elem) & graphics (2D canvas context).
this.canvas = elem;
this.context = elem.getContext('2d');
// Always allow chainability.
return this;
},
/**
* Sets drawing defaults:
* - fillStyle: Fill style color ('#F00').
* - strokeStyle: Stroke style color ('#F0F').
* - lineWidth: Line width (2).
* - lineCap: Line cap ('round').
* - lineJoin: Line join ('round').
* - miterLimit: Line miter (10). Works only if the lineJoin attribute is "miter".
* @return {object} jSketch
* @memberof jSketch
*/
setDefaults: function() {
var options = {
fillStyle: this.data.fillStyle || '#F00',
strokeStyle: this.data.strokeStyle || '#F0F',
lineWidth: this.data.lineWidth || 2,
lineCap: this.data.lineCap || 'round',
lineJoin: this.data.lineJoin || 'round',
miterLimit: this.data.miterLimit || 10,
};
// Save options as abstract calls at least once, so that we can recover them.
for (var opt in options) {
var val = options[opt];
this.callStack.push({ property: opt, value: val });
}
return this.saveGraphics(options).restoreGraphics();
},
/**
* Sets the dimensions of canvas.
* @param {number} width - New canvas width.
* @param {number} height - New canvas width.
* @return {object} jSketch
* @memberof jSketch
*/
size: function(width, height) {
// Allow grabbing values from CSS such as "100px".
var w = parseFloat(width);
var h = parseFloat(height);
this.stageWidth = w;
this.stageHeight = h;
this.canvas.width = w;
this.canvas.height = h;
// On resizing we lose drawing options, so restore them.
this.restoreGraphics();
return this;
},
/**
* Sets the background color of canvas.
* @param {string} color - An HTML color.
* @return {object} jSketch
* @memberof jSketch
*/
background: function(color) {
var args = [0, 0, this.stageWidth, this.stageHeight];
// Process canvas.
this.beginFill(color);
this.context.fillRect.apply(this.context, args);
this.endFill();
// Save abstract call.
this.callStack.push({ property: 'fillStyle', value: color });
this.callStack.push({ method: 'fillRect', args: args });
return this;
},
/**
* Shortcut for setting the size + background color.
* @param {number} width - New canvas width.
* @param {number} height - New canvas width.
* @param {string} bgcolor - An HTML color.
* @return {object} jSketch
* @memberof jSketch
*/
stage: function(width, height, bgcolor) {
this.size(width, height).background(bgcolor);
return this;
},
/**
* Sets the fill color.
* @param {string} color - An HTML color.
* @return {object} jSketch
* @memberof jSketch
*/
beginFill: function(color) {
this.saveGraphics({ fillStyle: color });
this.context.fillStyle = color;
this.callStack.push({ property: 'fillStyle', value: color });
return this;
},
/**
* Recovers the fill color that was set before `beginFill()`.
* @return {object} jSketch
* @memberof jSketch
*/
endFill: function() {
this.restoreGraphics();
return this;
},
/**
* Sets the line style.
* @param {string} color - An HTML color.
* @param {number} thickness - Line thickness.
* @param {string} capStyle - Style of line cap.
* @param {string} joinStyle - Style of line join.
* @param {string} miter - Style of line miter. Only works if capStyle is "miter".
* @return {object} jSketch
* @memberof jSketch
*/
lineStyle: function(color, thickness, capStyle, joinStyle, miter) {
var options = {
strokeStyle: color,
lineWidth: thickness,
lineCap: capStyle,
lineJoin: joinStyle,
miterLimit: miter,
};
return this.saveGraphics(options).restoreGraphics();
},
/**
* Move brush to a coordinate in canvas.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @return {object} jSketch
* @memberof jSketch
*/
moveTo: function(x, y) {
var args = [].slice.call(arguments);
this.context.moveTo.apply(this.context, args);
this.callStack.push({ method: 'moveTo', args: args });
return this;
},
/**
* Draws line to given coordinate.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @return {object} jSketch
* @memberof jSketch
*/
lineTo: function(x, y) {
var args = [].slice.call(arguments);
this.context.lineTo.apply(this.context, args);
this.callStack.push({ method: 'lineTo', args: args });
return this;
},
/**
* Draws line from point 1 to point 2.
* @param {number} x1 - Horizontal coordinate of point 1.
* @param {number} y1 - Vertical coordinate of point 1.
* @param {number} x2 - Horizontal coordinate of point 2.
* @param {number} y2 - Vertical coordinate of point 2.
* @return {object} jSketch
* @memberof jSketch
*/
line: function(x1, y1, x2, y2) {
this.moveTo(x1, y1);
this.lineTo(x2, y2);
return this;
},
/**
* Draws curve to given coordinate.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} cpx - Horizontal coordinate of control point.
* @param {number} cpy - Vertical coordinate of control point.
* @return {object} jSketch
* @memberof jSketch
*/
curveTo: function(x, y, cpx, cpy) {
// XXX: The native canvas API uses a different arg order.
var args = [cpx, cpy, x, y];
this.context.quadraticCurveTo.apply(this.context, args);
this.callStack.push({ method: 'quadraticCurveTo', args: args });
return this;
},
/**
* Draws curve from coordinate 1 to coordinate 2.
* @param {number} x1 - Horizontal coordinate of point 1.
* @param {number} y1 - Vertical coordinate of point 1.
* @param {number} x2 - Horizontal coordinate of point 2.
* @param {number} y2 - Vertical coordinate of point 2.
* @param {number} cpx - Horizontal coordinate of control point.
* @param {number} cpy - Vertical coordinate of control point.
* @return {object} jSketch
* @memberof jSketch
*/
curve: function(x1, y1, x2, y2, cpx, cpy) {
this.moveTo(x1, y1);
this.curveTo(x2, y2, cpx, cpy);
return this;
},
/**
* Strokes a given path.
* @return {object} jSketch
* @memberof jSketch
*/
stroke: function() {
this.context.stroke();
this.callStack.push({ method: 'stroke' });
return this;
},
/**
* Draws a stroke-only rectangle.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} width - Rectangle width.
* @param {number} height - Rectangle height.
* @return {object} jSketch
* @memberof jSketch
*/
strokeRect: function(x, y, width, height) {
var args = [].slice.call(arguments);
this.context.beginPath();
this.context.strokeRect.apply(this.context, args);
this.context.closePath();
this.callStack.push({ method: 'strokeRect', args: args });
return this;
},
/**
* Draws a filled rectangle.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} width - Rectangle width.
* @param {number} height - Rectangle height.
* @return {object} jSketch
* @memberof jSketch
*/
fillRect: function(x, y, width, height) {
var args = [].slice.call(arguments);
this.context.beginPath();
this.context.fillRect.apply(this.context, args);
this.context.closePath();
this.callStack.push({ method: 'fillRect', args: args });
return this;
},
/**
* Draws a filled+stroked rectangle.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} width - Rectangle width.
* @param {number} height - Rectangle height.
* @return {object} jSketch
* @memberof jSketch
*/
rect: function(x, y, width, height) {
var args = [].slice.call(arguments);
this.fillRect.apply(this, args);
this.strokeRect.apply(this, args);
return this;
},
/**
* Draws a stroke-only circle.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} radius - Circle radius.
* @return {object} jSketch
* @memberof jSketch
*/
strokeCircle: function(x, y, radius) {
var args = [x, y, radius, 0, 2*Math.PI, false];
this.context.beginPath();
this.context.arc.apply(this.context, args);
this.context.stroke();
this.context.closePath();
this.callStack.push({ method: 'strokeCircle', args: args });
return this;
},
/**
* Draws a filled circle.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} radius - Circle radius.
* @return {object} jSketch
* @memberof jSketch
*/
fillCircle: function(x, y, radius) {
var args = [x, y, radius, 0, 2*Math.PI, false];
this.context.beginPath();
this.context.arc.apply(this.context, args);
this.context.fill();
this.context.closePath();
this.callStack.push({ method: 'fillCircle', args: args });
return this;
},
/**
* Draws a filled+stroked circle.
* @param {number} x - Horizontal coordinate.
* @param {number} y - Vertical coordinate.
* @param {number} radius - Circle radius.
* @return {object} jSketch
* @memberof jSketch
*/
circle: function(x, y, radius) {
var args = [].slice.call(arguments);
this.fillCircle.apply(this, args);
this.strokeCircle.apply(this, args);
return this;
},
/**
* Experimental.
* @ignore
*/
radialCircle: function(x, y, radius, glowSize, colors) {
if (typeof glowSize === 'undefined' || glowSize < 0) glowSize = 1;
var g = this.context.createRadialGradient(x, y, radius, x, y, glowSize);
if (!colors || colors.constructor.name.toLowerCase() !== 'array') {
colors = [this.context.fillStyle, 'white'];
}
for (var s = 0; s < colors.length; s++) {
var color = colors[s];
g.addColorStop(s, color);
}
this.beginFill(g).fillCircle(x, y, radius).endFill();
return this;
},
/**
* A path is started.
* @return {object} jSketch
* @memberof jSketch
*/
beginPath: function() {
this.context.beginPath();
this.callStack.push({ method: 'beginPath' });
return this;
},
/**
* A path is finished.
* @return {object} jSketch
* @memberof jSketch
*/
closePath: function() {
this.context.closePath();
this.callStack.push({ method: 'closePath' });
return this;
},
/**
* Sets brush to eraser mode.
* @return {object} jSketch
* @memberof jSketch
*/
eraser: function() {
this.context.globalCompositeOperation = 'destination-out';
this.callStack.push({ property: 'comp-op', value: 'dst_out' });
return this;
},
/**
* Sets brush to pencil mode.
* @return {object} jSketch
* @memberof jSketch
*/
pencil: function() {
this.context.globalCompositeOperation = 'source-over';
this.callStack.push({ property: 'comp-op', value: 'src_over' });
return this;
},
/**
* Clears stage.
* @return {object} jSketch
* @memberof jSketch
*/
clear: function() {
var args = [0, 0, this.stageWidth, this.stageHeight];
// Note: using 'this.canvas.width = this.canvas.width' resets _all_ styles, so better use clearRect.
this.context.clearRect.apply(this.context, args);
this.callStack.push({ method: 'clear' });
return this;
},
/**
* Saves a snapshot of all styles and transformations.
* @return {object} jSketch
* @memberof jSketch
*/
save: function() {
this.context.save();
this.callStack.push({ method: 'save' });
return this;
},
/**
* Restores previous drawing state.
* @return {object} jSketch
* @memberof jSketch
*/
restore: function() {
this.context.restore();
this.callStack.push({ method: 'restore' });
return this;
},
/**
* Saves given drawing settings.
* @param {object} [options] - Graphics options.
* @return {object} jSketch
* @memberof jSketch
*/
saveGraphics: function(options) {
for (var opt in options) {
var val = options[opt];
if (val && val !== this.data[opt]) {
this.data[opt] = val;
// Save only the options that have changed as abstract calls.
this.callStack.push({ property: opt, value: val });
}
}
return this;
},
/**
* Restores given drawing settings.
* @return {object} jSketch
* @memberof jSketch
*/
restoreGraphics: function() {
for (var opt in this.data) {
this.context[opt] = this.data[opt];
}
return this;
},
/**
* Draws an image.
* @param {string} src - Image source path.
* @param {number} [x] - Horizontal coordinate.
* @param {number} [y] - Vertical coordinate.
* @return {object} jSketch
* @memberof jSketch
*/
drawImage: function(src, x, y) {
if (typeof x === 'undefined') x = 0;
if (typeof y === 'undefined') y = 0;
var self = this, img = new Image();
img.src = src;
img.onload = function() {
self.context.drawImage(img, x, y);
self.callStack.push({ method: 'drawImage', args: [img, x, y] });
self.callStack.push({ method: 'removeAsync' });
};
img.onerror = function() {
self.callStack.push({ method: 'removeAsync' });
};
self.callStack.push({ method: 'addAsync' });
return this;
},
};
// Expose.
window.jSketch = jSketch;
})(this);