You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
585 lines
11 KiB
585 lines
11 KiB
|
|
/*!
|
|
* Canvas
|
|
* Copyright (c) 2010 LearnBoost <tj@learnboost.com>
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var canvas = require('../build/default/canvas')
|
|
, colors = require('./colors')
|
|
, Canvas = canvas.Canvas
|
|
, Context2d = canvas.CanvasRenderingContext2d
|
|
, CanvasGradient = canvas.CanvasGradient
|
|
, cairoVersion = canvas.cairoVersion
|
|
, PNGStream = require('./pngstream')
|
|
, fs = require('fs');
|
|
|
|
/**
|
|
* Export `Canvas` as the module.
|
|
*/
|
|
|
|
var Canvas = exports = module.exports = Canvas;
|
|
|
|
/**
|
|
* Library version.
|
|
*/
|
|
|
|
exports.version = '0.0.1';
|
|
|
|
/**
|
|
* Cairo version.
|
|
*/
|
|
|
|
exports.cairoVersion = cairoVersion;
|
|
|
|
/**
|
|
* 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 + '))*)'
|
|
);
|
|
|
|
/**
|
|
* Buffer extensions.
|
|
*/
|
|
|
|
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
|
|
*/
|
|
|
|
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
|
|
];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Inspect canvas.
|
|
*
|
|
* @return {String}
|
|
* @api public
|
|
*/
|
|
|
|
Canvas.prototype.inspect = function(){
|
|
return '[Canvas ' + this.width + ' ' + this.height + ']';
|
|
};
|
|
|
|
/**
|
|
* Get a context object.
|
|
*
|
|
* @param {String} contextId
|
|
* @return {Context2d}
|
|
* @api public
|
|
*/
|
|
|
|
Canvas.prototype.getContext = function(contextId){
|
|
if ('2d' == contextId) {
|
|
var ctx = new Context2d(this);
|
|
this.context = ctx;
|
|
ctx.canvas = this;
|
|
return ctx;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create a `PNGStream` for `this` canvas.
|
|
*
|
|
* @return {PNGStream}
|
|
* @api public
|
|
*/
|
|
|
|
Canvas.prototype.createPNGStream = function(){
|
|
return new PNGStream(this);
|
|
};
|
|
|
|
/**
|
|
* Create a synchronous `PNGStream` for `this` canvas.
|
|
*
|
|
* @return {PNGStream}
|
|
* @api public
|
|
*/
|
|
|
|
Canvas.prototype.createSyncPNGStream = function(){
|
|
return new PNGStream(this, true);
|
|
};
|
|
|
|
/**
|
|
* Return a `Buffer` instance consisting of the PNG image data.
|
|
*
|
|
* @return {Buffer}
|
|
* @api public
|
|
*/
|
|
|
|
Canvas.prototype.toBuffer = function(){
|
|
var buf;
|
|
this.streamPNGSync(function(err, chunk, len){
|
|
if (err) throw err;
|
|
if (len) {
|
|
buf = buf
|
|
? buf.concat(chunk)
|
|
: chunk;
|
|
}
|
|
});
|
|
return buf;
|
|
};
|
|
|
|
/**
|
|
* Return a data url.
|
|
*
|
|
* @param {String} type
|
|
* @return {String}
|
|
* @api public
|
|
*/
|
|
|
|
Canvas.prototype.toDataURL = function(type){
|
|
// TODO: jpeg / svg / pdf :)
|
|
type = type || 'image/png';
|
|
if ('image/png' != type) throw new Error('currently only image/png is supported');
|
|
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);
|
|
};
|
|
|
|
/**
|
|
* Stroke `text` at (`x`, `y`).
|
|
*
|
|
* @param {String} text
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @api public
|
|
*/
|
|
|
|
Context2d.prototype.strokeText = function(text, x, y){
|
|
this.beginPath();
|
|
this.setTextPath(text, x, y);
|
|
return this.stroke();
|
|
};
|
|
|
|
/**
|
|
* Fill `text` at (`x`, `y`).
|
|
*
|
|
* @param {String} text
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @api public
|
|
*/
|
|
|
|
Context2d.prototype.fillText = function(text, x, y){
|
|
this.beginPath();
|
|
this.setTextPath(text, x, y);
|
|
return this.fill();
|
|
};
|
|
|
|
/**
|
|
* 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';
|
|
});
|