diff --git a/lib/canvas.js b/lib/canvas.js index 8f309a3..bdc4cb3 100644 --- a/lib/canvas.js +++ b/lib/canvas.js @@ -10,11 +10,9 @@ */ var canvas = require('../build/default/canvas') - , colors = require('./colors') , Canvas = canvas.Canvas - , Context2d = canvas.CanvasRenderingContext2d - , CanvasGradient = canvas.CanvasGradient , cairoVersion = canvas.cairoVersion + , Context2d = require('./context2d') , PNGStream = require('./pngstream') , fs = require('fs'); @@ -37,36 +35,11 @@ exports.version = '0.0.1'; exports.cairoVersion = cairoVersion; /** - * Cache color string RGBA values. + * Expose constructors. */ -var cache = {}; - -/** - * Text baselines. - */ - -var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; - -/** - * Font RegExp helpers. - */ - -var weights = 'normal|bold|bolder|lighter|[1-9](?:00)' - , styles = 'normal|italic|oblique' - , units = 'px|pt|pc|in|cm|mm|%' - , string = '"([^"]+)"|[\\w-]+'; - -/** - * Font parser RegExp; - */ - -var fontre = new RegExp('^ *' - + '(?:(' + weights + ') *)?' - + '(?:(' + styles + ') *)?' - + '(\\d+)(' + units + ') *' - + '((?:' + string + ')( *, *(?:' + string + '))*)' - ); +exports.Context2d = Context2d; +exports.PNGStream = PNGStream; /** * Buffer extensions. @@ -75,128 +48,10 @@ var fontre = new RegExp('^ *' require('./buffer'); /** - * Return a function used to normalize an RGBA color `prop`. - * - * @param {String} prop - * @return {Function} - * @api public - */ - -function normalizedColor(prop) { - return function(){ - var rgba = this[prop]; - if (1 == rgba[3]) { - return '#' - + rgba[0].toString(16) - + rgba[1].toString(16) - + rgba[2].toString(16); - } else { - return 'rgba(' - + rgba[0] + ', ' - + rgba[1] + ', ' - + rgba[2] + ', ' - + rgba[3] + ')'; - } - } -} - -/** - * Parse font `str`. - * - * @param {String} str - * @return {Object} - * @api public + * Context2d implementation. */ -var parseFont = exports.parseFont = function(str){ - var font = {} - , captures = fontre.exec(str); - - // Invalid - if (!captures) return; - - // Populate font object - font.weight = captures[1] || 'normal'; - font.style = captures[2] || 'normal'; - font.size = parseInt(captures[3], 10); - font.unit = captures[4]; - font.family = captures[5]; - - return font; -}; - -/** - * Parse the given color `str`. - * - * Current supports: - * - * - #nnn - * - #nnnnnn - * - rgb(r,g,b) - * - rgba(r,g,b,a) - * - color - * - * Examples - * - * - #fff - * - #FFF - * - #FFFFFF - * - rgb(255,255,5) - * - rgba(255,255,5,.8) - * - rgba(255,255,5,0.8) - * - white - * - red - * - * @param {String} str - * @return {Array} - * @api public - */ - -var parseColor = exports.parseColor = function(str){ - if (cache[str]) return cache[str]; - str = colors[str] || String(str); - // RGBA - if (0 == str.indexOf('rgba')) { - var captures = /rgba\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *, *(\d+\.\d+|\.\d+|\d+) *\)/.exec(str); - if (!captures) return; - return cache[str] = [ - parseInt(captures[1], 10) - , parseInt(captures[2], 10) - , parseInt(captures[3], 10) - , parseFloat(captures[4], 10) - ]; - // RGB - } else if (0 == str.indexOf('rgb')) { - var captures = /rgb\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *\)/.exec(str); - if (!captures) return; - return cache[str] = [ - parseInt(captures[1], 10) - , parseInt(captures[2], 10) - , parseInt(captures[3], 10) - , 1 - ]; - // #RRGGBB - } else if ('#' == str[0] && str.length > 4) { - var captures = /#([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})/.exec(str); - if (!captures) return; - return cache[str] = [ - parseInt(captures[1], 16) - , parseInt(captures[2], 16) - , parseInt(captures[3], 16) - , 1 - ]; - // #RGB - } else if ('#' == str[0]) { - var captures = /#([a-fA-F\d])([a-fA-F\d])([a-fA-F\d])/.exec(str); - if (!captures) return; - return cache[str] = [ - parseInt(captures[1] + captures[1], 16) - , parseInt(captures[2] + captures[2], 16) - , parseInt(captures[3] + captures[3], 16) - , 1 - ]; - } -}; +require('./context2d'); /** * Inspect canvas. @@ -283,273 +138,3 @@ Canvas.prototype.toDataURL = function(type){ return 'data:' + type + ';base64,' + this.toBuffer().toString('base64'); }; - -/** - * Add `color` stop at the given `offset`. - * - * @param {Number} offset - * @param {String} color - * @api public - */ - -CanvasGradient.prototype.addColorStop = function(offset, color){ - var rgba; - if (rgba = parseColor(color)) { - this.addColorStopRGBA( - offset - , rgba[0] - , rgba[1] - , rgba[2] - , rgba[3]); - } -}; - -/** - * Create a linear gradient at the given point `(x0, y0)` and `(x1, y1)`. - * - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * @return {CanvasGradient} - * @api public - */ - -Context2d.prototype.createLinearGradient = function(x0, y0, x1, y1){ - return new CanvasGradient(x0, y0, x1, y1); -}; - -/** - * Create a radial gradient at the given point `(x0, y0)` and `(x1, y1)` - * and radius `r0` and `r1`. - * - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} r0 - * @param {Number} x1 - * @param {Number} y1 - * @param {Number} r1 - * @return {CanvasGradient} - * @api public - */ - -Context2d.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1){ - return new CanvasGradient(x0, y0, r0, x1, y1, r1); -}; - -/** - * Reset transform matrix to identity, then apply the given args. - * - * @param {...} - * @api public - */ - -Context2d.prototype.setTransform = function(){ - this.resetTransform(); - this.transform.apply(this, arguments); -}; - -/** - * Set the fill style with the given css color string. - * - * @see exports.parseColor() - * @api public - */ - -Context2d.prototype.__defineSetter__('fillStyle', function(val){ - if (val instanceof CanvasGradient) { - this.setFillPattern(val); - } else if ('string' == typeof val) { - var rgba; - if (rgba = parseColor(val)) { - this.lastFillStyle = rgba; - this.setFillRGBA( - rgba[0] - , rgba[1] - , rgba[2] - , rgba[3]); - } - } -}); - -/** - * Get the current fill style string. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('fillStyle', normalizedColor('lastFillStyle')); - -/** - * Set the stroke style with the given css color string. - * - * @see exports.parseColor() - * @api public - */ - -Context2d.prototype.__defineSetter__('strokeStyle', function(val){ - if (val instanceof CanvasGradient) { - this.setStrokePattern(val); - } else if ('string' == typeof val) { - var rgba; - if (rgba = parseColor(val)) { - this.lastStrokeStyle = rgba; - this.setStrokeRGBA( - rgba[0] - , rgba[1] - , rgba[2] - , rgba[3]); - } - } -}); - -/** - * Get the current stroke style string. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('strokeStyle', normalizedColor('lastStrokeStyle')); - -/** - * Set the shadow color with the given css color string. - * - * @see exports.parseColor() - * @api public - */ - -Context2d.prototype.__defineSetter__('shadowColor', function(val){ - if ('string' == typeof val) { - var rgba; - if (rgba = parseColor(val)) { - this.lastShadowColor = rgba; - this.setShadowRGBA( - rgba[0] - , rgba[1] - , rgba[2] - , rgba[3]); - } - } -}); - -/** - * Get the current shadow color string. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('shadowColor', normalizedColor('lastShadowColor')); - -/** - * Set font. - * - * @see exports.parseFont() - * @api public - */ - -Context2d.prototype.__defineSetter__('font', function(val){ - if ('string' == typeof val) { - var font; - if (font = cache[val] || parseFont(val)) { - this.lastFontString = val; - - // TODO: dpi - // TODO: remaining unit conversion - switch (font.unit) { - case 'pt': - font.size /= .75; - break; - case 'in': - font.size *= 96; - break; - case 'mm': - font.size *= 96.0 / 25.4; - break; - case 'cm': - font.size *= 96.0 / 2.54; - break; - } - - // Cache font object - cache[val] = font; - - // Set font - this.setFont( - font.weight - , font.style - , font.size - , font.unit - , font.family); - } - } -}); - -/** - * Get the current font. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('font', function(){ - return this.lastFontString || '10px sans-serif'; -}); - -/** - * Set text baseline. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('textBaseline', function(val){ - var n = baselines.indexOf(val); - if (~n) { - this.lastBaseline = val; - this.setTextBaseline(n); - } -}); - -/** - * Get the current baseline setting. - * - * @api public - */ - -Context2d.prototype.__defineGetter__('textAlign', function(){ - return this.lastBaseline || 'alphabetic'; -}); - -/** - * Set text alignment. - * - * @api public - */ - -Context2d.prototype.__defineSetter__('textAlign', function(val){ - switch (val) { - case 'center': - this.setTextAlignment(0); - this.lastTextAlignment = val; - break; - case 'left': - case 'start': - this.setTextAlignment(-1); - this.lastTextAlignment = val; - break; - case 'right': - case 'end': - this.setTextAlignment(1); - this.lastTextAlignment = val; - break; - } -}); - -/** - * Get the current font. - * - * @see exports.parseFont() - * @api public - */ - -Context2d.prototype.__defineGetter__('textAlign', function(){ - return this.lastTextAlignment || 'start'; -}); \ No newline at end of file diff --git a/lib/context2d.js b/lib/context2d.js new file mode 100644 index 0000000..066dc39 --- /dev/null +++ b/lib/context2d.js @@ -0,0 +1,447 @@ + +/*! + * Canvas - Context2d + * Copyright (c) 2010 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var canvas = require('../build/default/canvas') + , Context2d = canvas.CanvasRenderingContext2d + , CanvasGradient = canvas.CanvasGradient + , colors = require('./colors'); + +/** + * Export `Context2d` as the module. + */ + +var Context2d = exports = module.exports = Context2d; + +/** + * Cache color string RGBA values. + */ + +var cache = {}; + +/** + * Text baselines. + */ + +var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; + +/** + * Font RegExp helpers. + */ + +var weights = 'normal|bold|bolder|lighter|[1-9](?:00)' + , styles = 'normal|italic|oblique' + , units = 'px|pt|pc|in|cm|mm|%' + , string = '"([^"]+)"|[\\w-]+'; + +/** + * Font parser RegExp; + */ + +var fontre = new RegExp('^ *' + + '(?:(' + weights + ') *)?' + + '(?:(' + styles + ') *)?' + + '(\\d+)(' + units + ') *' + + '((?:' + string + ')( *, *(?:' + string + '))*)' + ); + +/** + * Return a function used to normalize an RGBA color `prop`. + * + * @param {String} prop + * @return {Function} + * @api private + */ + +function normalizedColor(prop) { + return function(){ + var rgba = this[prop]; + if (1 == rgba[3]) { + return '#' + + rgba[0].toString(16) + + rgba[1].toString(16) + + rgba[2].toString(16); + } else { + return 'rgba(' + + rgba[0] + ', ' + + rgba[1] + ', ' + + rgba[2] + ', ' + + rgba[3] + ')'; + } + } +} + +/** + * Parse font `str`. + * + * @param {String} str + * @return {Object} + * @api private + */ + +var parseFont = exports.parseFont = function(str){ + var font = {} + , captures = fontre.exec(str); + + // Invalid + if (!captures) return; + + // Populate font object + font.weight = captures[1] || 'normal'; + font.style = captures[2] || 'normal'; + font.size = parseInt(captures[3], 10); + font.unit = captures[4]; + font.family = captures[5]; + + return font; +}; + +/** + * Parse the given color `str`. + * + * Current supports: + * + * - #nnn + * - #nnnnnn + * - rgb(r,g,b) + * - rgba(r,g,b,a) + * - color + * + * Examples + * + * - #fff + * - #FFF + * - #FFFFFF + * - rgb(255,255,5) + * - rgba(255,255,5,.8) + * - rgba(255,255,5,0.8) + * - white + * - red + * + * @param {String} str + * @return {Array} + * @api private + */ + +var parseColor = exports.parseColor = function(str){ + if (cache[str]) return cache[str]; + str = colors[str] || String(str); + // RGBA + if (0 == str.indexOf('rgba')) { + var captures = /rgba\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *, *(\d+\.\d+|\.\d+|\d+) *\)/.exec(str); + if (!captures) return; + return cache[str] = [ + parseInt(captures[1], 10) + , parseInt(captures[2], 10) + , parseInt(captures[3], 10) + , parseFloat(captures[4], 10) + ]; + // RGB + } else if (0 == str.indexOf('rgb')) { + var captures = /rgb\((\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *\)/.exec(str); + if (!captures) return; + return cache[str] = [ + parseInt(captures[1], 10) + , parseInt(captures[2], 10) + , parseInt(captures[3], 10) + , 1 + ]; + // #RRGGBB + } else if ('#' == str[0] && str.length > 4) { + var captures = /#([a-fA-F\d]{2})([a-fA-F\d]{2})([a-fA-F\d]{2})/.exec(str); + if (!captures) return; + return cache[str] = [ + parseInt(captures[1], 16) + , parseInt(captures[2], 16) + , parseInt(captures[3], 16) + , 1 + ]; + // #RGB + } else if ('#' == str[0]) { + var captures = /#([a-fA-F\d])([a-fA-F\d])([a-fA-F\d])/.exec(str); + if (!captures) return; + return cache[str] = [ + parseInt(captures[1] + captures[1], 16) + , parseInt(captures[2] + captures[2], 16) + , parseInt(captures[3] + captures[3], 16) + , 1 + ]; + } +}; + +/** + * Add `color` stop at the given `offset`. + * + * @param {Number} offset + * @param {String} color + * @api public + */ + +CanvasGradient.prototype.addColorStop = function(offset, color){ + var rgba; + if (rgba = parseColor(color)) { + this.addColorStopRGBA( + offset + , rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); + } +}; + +/** + * Create a linear gradient at the given point `(x0, y0)` and `(x1, y1)`. + * + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * @return {CanvasGradient} + * @api public + */ + +Context2d.prototype.createLinearGradient = function(x0, y0, x1, y1){ + return new CanvasGradient(x0, y0, x1, y1); +}; + +/** + * Create a radial gradient at the given point `(x0, y0)` and `(x1, y1)` + * and radius `r0` and `r1`. + * + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} r0 + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} r1 + * @return {CanvasGradient} + * @api public + */ + +Context2d.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1){ + return new CanvasGradient(x0, y0, r0, x1, y1, r1); +}; + +/** + * Reset transform matrix to identity, then apply the given args. + * + * @param {...} + * @api public + */ + +Context2d.prototype.setTransform = function(){ + this.resetTransform(); + this.transform.apply(this, arguments); +}; + +/** + * Set the fill style with the given css color string. + * + * @see exports.parseColor() + * @api public + */ + +Context2d.prototype.__defineSetter__('fillStyle', function(val){ + if (val instanceof CanvasGradient) { + this.setFillPattern(val); + } else if ('string' == typeof val) { + var rgba; + if (rgba = parseColor(val)) { + this.lastFillStyle = rgba; + this.setFillRGBA( + rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); + } + } +}); + +/** + * Get the current fill style string. + * + * @api public + */ + +Context2d.prototype.__defineGetter__('fillStyle', normalizedColor('lastFillStyle')); + +/** + * Set the stroke style with the given css color string. + * + * @see exports.parseColor() + * @api public + */ + +Context2d.prototype.__defineSetter__('strokeStyle', function(val){ + if (val instanceof CanvasGradient) { + this.setStrokePattern(val); + } else if ('string' == typeof val) { + var rgba; + if (rgba = parseColor(val)) { + this.lastStrokeStyle = rgba; + this.setStrokeRGBA( + rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); + } + } +}); + +/** + * Get the current stroke style string. + * + * @api public + */ + +Context2d.prototype.__defineGetter__('strokeStyle', normalizedColor('lastStrokeStyle')); + +/** + * Set the shadow color with the given css color string. + * + * @see exports.parseColor() + * @api public + */ + +Context2d.prototype.__defineSetter__('shadowColor', function(val){ + if ('string' == typeof val) { + var rgba; + if (rgba = parseColor(val)) { + this.lastShadowColor = rgba; + this.setShadowRGBA( + rgba[0] + , rgba[1] + , rgba[2] + , rgba[3]); + } + } +}); + +/** + * Get the current shadow color string. + * + * @api public + */ + +Context2d.prototype.__defineGetter__('shadowColor', normalizedColor('lastShadowColor')); + +/** + * Set font. + * + * @see exports.parseFont() + * @api public + */ + +Context2d.prototype.__defineSetter__('font', function(val){ + if ('string' == typeof val) { + var font; + if (font = cache[val] || parseFont(val)) { + this.lastFontString = val; + + // TODO: dpi + // TODO: remaining unit conversion + switch (font.unit) { + case 'pt': + font.size /= .75; + break; + case 'in': + font.size *= 96; + break; + case 'mm': + font.size *= 96.0 / 25.4; + break; + case 'cm': + font.size *= 96.0 / 2.54; + break; + } + + // Cache font object + cache[val] = font; + + // Set font + this.setFont( + font.weight + , font.style + , font.size + , font.unit + , font.family); + } + } +}); + +/** + * Get the current font. + * + * @api public + */ + +Context2d.prototype.__defineGetter__('font', function(){ + return this.lastFontString || '10px sans-serif'; +}); + +/** + * Set text baseline. + * + * @api public + */ + +Context2d.prototype.__defineSetter__('textBaseline', function(val){ + var n = baselines.indexOf(val); + if (~n) { + this.lastBaseline = val; + this.setTextBaseline(n); + } +}); + +/** + * Get the current baseline setting. + * + * @api public + */ + +Context2d.prototype.__defineGetter__('textAlign', function(){ + return this.lastBaseline || 'alphabetic'; +}); + +/** + * Set text alignment. + * + * @api public + */ + +Context2d.prototype.__defineSetter__('textAlign', function(val){ + switch (val) { + case 'center': + this.setTextAlignment(0); + this.lastTextAlignment = val; + break; + case 'left': + case 'start': + this.setTextAlignment(-1); + this.lastTextAlignment = val; + break; + case 'right': + case 'end': + this.setTextAlignment(1); + this.lastTextAlignment = val; + break; + } +}); + +/** + * Get the current font. + * + * @see exports.parseFont() + * @api public + */ + +Context2d.prototype.__defineGetter__('textAlign', function(){ + return this.lastTextAlignment || 'start'; +}); \ No newline at end of file diff --git a/test/canvas.test.js b/test/canvas.test.js index eee25f9..67bdb46 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -5,6 +5,8 @@ var Canvas = require('canvas') , assert = require('assert') + , parseColor = Canvas.Context2d.parseColor + , parseFont = Canvas.Context2d.parseFont , sys = require('sys') , fs = require('fs'); @@ -18,34 +20,34 @@ module.exports = { }, 'test .parseColor()': function(assert){ - assert.equal(null, Canvas.parseColor()); - assert.equal(null, Canvas.parseColor('')); + assert.equal(null, parseColor()); + assert.equal(null, parseColor('')); // rgb() - assert.eql([255,165,0,1], Canvas.parseColor('rgb(255,165,0)')); - assert.eql([255,165,0,1], Canvas.parseColor('rgb(255, 165, 0)')); - assert.eql([255,165,0,1], Canvas.parseColor('rgb(255 , 165 , 0)')); - assert.equal(null, Canvas.parseColor('rgb()')); + assert.eql([255,165,0,1], parseColor('rgb(255,165,0)')); + assert.eql([255,165,0,1], parseColor('rgb(255, 165, 0)')); + assert.eql([255,165,0,1], parseColor('rgb(255 , 165 , 0)')); + assert.equal(null, parseColor('rgb()')); // rgba() - assert.eql([255,165,0,1], Canvas.parseColor('rgba(255,165,0,1)')); - assert.eql([255,165,0,1], Canvas.parseColor('rgba(255,165,0,1)')); - assert.eql([255,165,0,.6], Canvas.parseColor('rgba(255,165,0,0.6)')); - assert.eql([255,165,0,.6], Canvas.parseColor('rgba(255,165, 0, 0.6)')); - assert.eql([255,165,0,.6], Canvas.parseColor('rgba(255,165 , 0 ,.6)')); - assert.equal(null, Canvas.parseColor('rgba(2554,165 , 0 ,.6)')); - assert.equal(null, Canvas.parseColor('rgba()')); + assert.eql([255,165,0,1], parseColor('rgba(255,165,0,1)')); + assert.eql([255,165,0,1], parseColor('rgba(255,165,0,1)')); + assert.eql([255,165,0,.6], parseColor('rgba(255,165,0,0.6)')); + assert.eql([255,165,0,.6], parseColor('rgba(255,165, 0, 0.6)')); + assert.eql([255,165,0,.6], parseColor('rgba(255,165 , 0 ,.6)')); + assert.equal(null, parseColor('rgba(2554,165 , 0 ,.6)')); + assert.equal(null, parseColor('rgba()')); // hex - assert.eql([165,89,89,1], Canvas.parseColor('#A55959')); - assert.eql([255,255,255,1], Canvas.parseColor('#FFFFFF')); - assert.eql([255,255,255,1], Canvas.parseColor('#ffffff')); - assert.eql([255,255,255,1], Canvas.parseColor('#FFF')); - assert.eql([255,255,255,1], Canvas.parseColor('#fff')); + assert.eql([165,89,89,1], parseColor('#A55959')); + assert.eql([255,255,255,1], parseColor('#FFFFFF')); + assert.eql([255,255,255,1], parseColor('#ffffff')); + assert.eql([255,255,255,1], parseColor('#FFF')); + assert.eql([255,255,255,1], parseColor('#fff')); // name - assert.eql([255,255,255,1], Canvas.parseColor('white')); - assert.eql([0,0,0,1], Canvas.parseColor('black')); + assert.eql([255,255,255,1], parseColor('white')); + assert.eql([0,0,0,1], parseColor('black')); }, 'test .parseFont()': function(assert){ @@ -93,7 +95,7 @@ module.exports = { for (var i = 0, len = tests.length; i < len; ++i) { var str = tests[i++] , obj = tests[i] - , got = Canvas.parseFont(str); + , got = parseFont(str); if (!obj.style) obj.style = 'normal'; if (!obj.weight) obj.weight = 'normal'; assert.eql(obj, got, ''