Source: render/2d/kekule.render.canvasRenderer.js

/**
 * @fileoverview
 * 2D renderer using HTML5 canvas.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.structures.js
 * requires /render/kekule.render.base.js
 * requires /render/kekule.render.baseTextRender.js
 * requires /render/2d/kekule.render.def2DRenderer.js
 */

"use strict";

/**
 * Render bridge class of HTML5 Canvas.
 * @class
 */
Kekule.Render.CanvasRendererBridge = Class.create(
/** @lends Kekule.Render.CanvasRendererBridge# */
{
	/** @private */
	CLASS_NAME: 'Kekule.Render.CanvasRendererBridge',
	/** @private */
	DEF_DOUBLE_BUFFERED: false,
	/** @pirvate */
	SHADOW_CANVAS_FIELD: '__$shadow_canvas__',
	/** @private */
	SHADOW_CONTEXT_FIELD: '__$shadow_context__',

	/**
	 * Returns shadow canvas context for double buffered drawing.
	 * @param {Object} context
	 * @returns {Object}
	 * @private
	 */
	getShadowContext: function(context)
	{
		return context[this.SHADOW_CONTEXT_FIELD];
	},
	/** @private */
	getShadowCanvas: function(context)
	{
		return context[this.SHADOW_CANVAS_FIELD];
	},
	/**
	 * Returns a context that need to be drawn immediately.
	 * In normal state, this function just returns the canvas itself.
	 * In double buffered mode, this function will returns the shadow canvas context.
	 * @param {Object} context
	 * @returns {Object}
	 */
	getOperatingContext: function(context)
	{
		return this.getShadowContext(context) || context;
	},

	/**
	 * Create a context element for drawing.
	 * @param {Element} parentElem
	 * @param {Int} width Width of context, in px.
	 * @param {Int} height Height of context, in px.
	 * @param {Bool} doubleBuffered Whether use double buffer to make smooth drawing.
	 * @returns {Object} Context used for drawing.
	 */
	createContext: function(parentElem, width, height, id, doubleBuffered)
	{

		if (doubleBuffered === undefined)
			doubleBuffered = this.DEF_DOUBLE_BUFFERED;

		var doc = parentElem.ownerDocument;
		var canvas = document.createElement('canvas');
		if (id)
			canvas.id = id;
		/*
		if (width)
		{
			canvas.setAttribute('width', width);
			canvas.style.width = width + 'px';
		}
		if (height)
		{
			canvas.setAttribute('height', height);
			canvas.style.height = height + 'px';
		}
		*/
		parentElem.appendChild(canvas);

		var ctx = canvas.getContext('2d');
		if (doubleBuffered)
		{
			var shadowCanvas = document.createElement('canvas');
			shadowCanvas.style.position = 'absolute';
			shadowCanvas.style.display = 'none';
			parentElem.appendChild(shadowCanvas);
			ctx[this.SHADOW_CANVAS_FIELD] = shadowCanvas;
			ctx[this.SHADOW_CONTEXT_FIELD] = shadowCanvas.getContext('2d');
		}

		this.setContextDimension(ctx, width, height);

		return ctx;
	},

	/**
	 * Destroy context created.
	 * @param {Object} context
	 */
	releaseContext: function(context)
	{
		var canvas = context.canvas;
		var shadowCanvas = this.getShadowCanvas(context);
		var parent = canvas.parentNode;
		if (parent)
		{
			parent.removeChild(canvas);
			if (shadowCanvas)
				parent.removeChild(shadowCanvas);
		}
	},

	/**
	 * Get width and height of context.
	 * @param {Object} context
	 * @returns {Hash} {width, height}
	 */
	getContextDimension: function(context)
	{
		return {'width': context.canvas.width, 'height': context.canvas.height};
	},

	/**
	 * Set new width and height of context.
	 * Note in canvas, the content should be redrawn after resizing.
	 * @param {Object} context
	 * @param {Int} width
	 * @param {Int} height
	 */
	setContextDimension: function(context, width, height)
	{
		var shadowCanvas = this.getShadowCanvas(context);
		if (width)
		{
			//context.setAttribute('width', width);
			context.canvas.width = width;
			context.canvas.style.width = width + 'px';
			if (shadowCanvas)
			{
				shadowCanvas.width = width;
				shadowCanvas.style.width = width + 'px';
			}
		}
		if (height)
		{
			//context.setAttribute('height', height);
			context.canvas.height = height;
			context.canvas.style.height = height + 'px';
			if (shadowCanvas)
			{
				shadowCanvas.height = height;
				shadowCanvas.style.height = height + 'px';
			}
		}
		this.clearContext(context);
	},

	/**
	 * Get context related element.
	 * @param {Object} context
	 */
	getContextElem: function(context)
	{
		return context? context.canvas: null;
	},

	/**
	 * Set the view box of context. This method will also change context dimension to w/h if param changeDimension is not false.
	 * @param {Object} context
	 * @param {Int} x Top left x coord.
	 * @param {Int} y Top left y coord.
	 * @param {Int} w Width.
	 * @param {Int} h Height.
	 * @param {Bool} changeDimension
	 */
	setContextViewBox: function(context, x, y, w, h, changeDimension)
	{

	},

	/**
	 * Clear the whole context.
	 * @param {Object} context
	 */
	clearContext: function(context)
	{
		var elem = context.canvas;
		elem.width = elem.width;
		var shadowCanvas = this.getShadowCanvas(context);
		if (shadowCanvas)
			shadowCanvas.width = shadowCanvas.width;

		var clearColor = context.__$clearColor__;
		if (clearColor)
		{
			context.save();
			/*
			var oldFillColor = context.fillStyle;
			var oldStrokeColor = context.strokeStyle;
			console.log(oldFillColor, oldStrokeColor);
			*/
			try
			{
				var dim = this.getContextDimension(context);
				this.drawRect(context, {x: 0, y: 0}, {x: dim.width, y: dim.height}, {'fillColor': clearColor, 'strokeColor': clearColor});
			}
			finally
			{
				context.restore();
				/*
				context.fillStyle = oldFillColor;
				context.strokeStyle = oldStrokeColor;
				*/
			}
		}
	},
	/** @private */
	setClearColor: function(context, color)
	{
		if (context)
		{
			//console.log('set clear color', color);
			context.__$clearColor__ = color;
		}
	},

	renderContext: function(context)
	{
		var shadowCanvas = this.getShadowCanvas(context);
		if (shadowCanvas)  // double buffering
		{
			//console.log('double buffered');
			context.drawImage(shadowCanvas, 0, 0);
		}
	},

	/**
	 * Transform a context based coord to screen based one (usually in pixel).
	 * @param {Object} context
	 * @param {Hash} coord
	 * @return {Hash}
	 */
	transformContextCoordToScreen: function(context, coord)
	{
		return coord;
	},
	/**
	 * Transform a screen based coord to context based one.
	 * @param {Object} context
	 * @param {Hash} coord
	 * @return {Hash}
	 */
	transformScreenCoordToContext: function(context, coord)
	{
		return coord;
	},

	// methods to draw graphics
	/**
	 * Indicate whether this bridge and context can change glyph content or position after drawing it.
	 * Raphael is a typical environment of this type while canvas should returns false.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canModifyGraphic: function(context)
	{
		return false;
	},

	_drawDashLine: function(context, coord1, coord2, dashLen)
	{
		if (dashLen == undefined) dashLen = 5;
		/*
		if (context.setLineDash || (context.mozDash !== undefined))
		{
			if (context.mozDash !== undefined)
			{
				context.mozDash = [5, 10];
				context.moveTo(coord1.x, coord1.y);
				context.lineTo(coord2.x, coord2.y);
				context.mozDash = [];
			}
			else if (context.setLineDash)
			{
				console.log(dashLen);
				//context.setLineDash([dashLen]);
				context.setLineDash([5, 10]);
				context.moveTo(coord1.x, coord1.y);
				context.lineTo(coord2.x, coord2.y);
				context.setLineDash([]);
			}
		}
		*/
		/*
		if (context.setLineDash && (typeof(context.lineDashOffset) == "number"))
		{
			context.setLineDash([dashLen]);
			context.moveTo(coord1.x, coord1.y);
			context.lineTo(coord2.x, coord2.y);
			context.setLineDash([]);
		}
		else
		*/
		{
	    context.moveTo(coord1.x, coord1.y);

	    var dX = coord2.x - coord1.x;
	    var dY = coord2.y - coord1.y;
	    var dashes = Math.floor(Math.sqrt(dX * dX + dY * dY) / dashLen);
	    var dashX = dX / dashes;
	    var dashY = dY / dashes;

	    var q = 0;
	    var x1 = coord1.x;
	    var y1 = coord1.y;

	    while (q++ < dashes) {
	        x1 += dashX;
	        y1 += dashY;
	        context[q % 2 == 0 ? 'moveTo' : 'lineTo'](x1, y1);
	    }
	    context[q % 2 == 0 ? 'moveTo' : 'lineTo'](coord2.x2, coord2.y);
		}
	},

	drawLine: function(ctx, coord1, coord2, options)
	{
		var context = this.getOperatingContext(ctx);

		context.beginPath();
		this.setDrawStyle(context, options);
		if (options.strokeDash && (!this.isLineDashSupported(context)))
		{
			this._drawDashLine(context, coord1, coord2, options.strokeDash);
		}
		else
		{
			context.moveTo(coord1.x, coord1.y);
			context.lineTo(coord2.x, coord2.y);
		}
		this.doneDraw(context, options);
	},
	drawTriangle: function(ctx, coord1, coord2, coord3, options)
	{
		var context = this.getOperatingContext(ctx);

		context.beginPath();
		this.setDrawStyle(context, options);
		context.moveTo(coord1.x, coord1.y);
		context.lineTo(coord2.x, coord2.y);
		context.lineTo(coord3.x, coord3.y);
		context.closePath();
		this.doneDraw(context, options);
		/*
		var path = Kekule.Render.DrawPathUtils.makePath(
			'M', [coord1.x, coord1.y],
			'L', [coord2.x, coord2.y],
			'L', [coord3.x, coord3.y],
			'Z'
		);
		this.drawPath(context, path, options);
		*/
	},
	drawRect: function(ctx, coord1, coord2, options)
	{
		var context = this.getOperatingContext(ctx);

		context.beginPath();
		this.setDrawStyle(context, options);
		context.rect(coord1.x, coord1.y, coord2.x - coord1.x, coord2.y - coord1.y);
		this.doneDraw(context, options);
	},
	drawRoundRect: function(ctx, coord1, coord2, cornerRadius, options)
	{
		// TODO: temp, draw a normal rect than a round rect
		return this.drawRect(ctx, coord1, coord2, options);
	},
	drawCircle: function(ctx, baseCoord, radius, options)
	{
		var context = this.getOperatingContext(ctx);
		context.beginPath();
		this.setDrawStyle(context, options);
		context.arc(baseCoord.x, baseCoord.y, radius, 0, 2 * Math.PI, false);
		this.doneDraw(context, options);
	},
	drawArc: function(ctx, centerCoord, radius, startAngle, endAngle, anticlockwise, options)
	{
		var context = this.getOperatingContext(ctx);
		context.beginPath();
		this.setDrawStyle(context, options);
		context.arc(centerCoord.x, centerCoord.y, radius, startAngle, endAngle, anticlockwise);
		this.doneDraw(context, options);
	},

	_PATH_CMDS: ['M', 'Z', 'L', 'C', 'Q'],
	_PATH_METHODS: ['moveTo', 'closePath', 'lineTo', 'bezierCurveTo', 'quadraticCurveTo'],
	drawPath: function(ctx, path, options)
	{
		var context = this.getOperatingContext(ctx);

		if (path.length <= 0)
			return;

		context.beginPath();
		this.setDrawStyle(context, options);

		var pathArgs;
		for (var i = 0, l = path.length; i < l; ++i)
		{
			pathArgs = [];
			var item = path[i];
			var c = item.method.toUpperCase();
			var index = this._PATH_CMDS.indexOf(c);
			var method;
			// TODO: currently S, T and A are not implemented
			if (index >= 0)
			{
				var method = this._PATH_METHODS[index];
				pathArgs = item.params;
				context[method].apply(context, pathArgs);
			}
		}
		this.doneDraw(context, options);
	},
	drawImage: function(ctx, src, baseCoord, size, options, callback, ctxGetter)
	{
		var context = this.getOperatingContext(ctx);
		// first load image into current document
		var contextElem = this.getContextElem(context);
		var doc = contextElem.ownerDocument;
		var imgElem = doc.createElement('img');
		imgElem.src = src;
		var self = this;
		imgElem.onload = function(){
			try
			{
				self.setDrawStyle(context, options);
				// draw after image is loaded
				// since actual drawing context may change after image is loaded,
				// here we must use a getter function to retrieve the real context used
				var actualCtx = ctx;
				if (ctxGetter)
					actualCtx = ctxGetter();
				actualCtx = self.getOperatingContext(actualCtx);
				actualCtx.drawImage(imgElem, baseCoord.x, baseCoord.y, size.x, size.y);
				if (callback)
					callback(true);
				self.doneDraw(context, options);
			}
			catch(e)
			{
				if (callback)
					callback(false);
				throw e;
			}
		};
		imgElem.src = src;
	},
	drawImageElem: function(ctx, imgElem, baseCoord, size, options)
	{
		var context = this.getOperatingContext(ctx);
		this.setDrawStyle(context, options);
		context.drawImage(imgElem, baseCoord.x, baseCoord.y, size.x, size.y);
		this.doneDraw(context, options);
	},

	/** @private */
	isLineDashSupported: function(context)
	{
		// using cached value
		if (Kekule.ObjUtils.isUnset(this.isLineDashSupported._cachedValue))
		{
			this.isLineDashSupported._cachedValue = (context.setLineDash && (typeof(context.lineDashOffset) == "number"));
		}
		return this.isLineDashSupported._cachedValue;
	},

	setDrawStyle: function(context, options)
	{
		if (Kekule.ObjUtils.notUnset(options.strokeWidth))
		{
			//if (context.lineWidth !== options.strokeWidth)
			context.lineWidth = options.strokeWidth;
		}
		else  // default
		{
			//if (context.lineWidth !== 1)
			context.lineWidth = 1;
		}
		if (options.strokeColor /* && context.strokeStyle !== options.strokeColor */)
			context.strokeStyle = options.strokeColor;

		// line cap and line join, has default value
		context.lineCap = options.lineCap || 'butt';
		context.lineJoin = options.lineJoin || 'miter';

		//console.log('draw style', options, context.strokeStyle);

		if (this.isLineDashSupported(context))
		{
			if (options.strokeDash)
			{
				var dashStyle = (options.strokeDash === true)? [5]:
					Kekule.ArrayUtils.isArray(options.strokeDash)? options.strokeDash: [options.strokeDash];
				context.setLineDash(dashStyle)
			}
			else
				context.setLineDash([]);
		}
		if (options.fillColor)
			context.fillStyle = options.fillColor;
		else
			context.fillStyle = 'transparent';
		if (Kekule.ObjUtils.notUnset(options.opacity))
			context.globalAlpha = options.opacity;
		else  // default
			context.globalAlpha = 1;
	},
	doneDraw: function(context, options)
	{
		context.stroke();
		if (options.fillColor)  // has fill
			context.fill();
	},

	// Methods to draw text
	/** @private */
	getCanvasFontStyle: function(drawOptions)
	{
		var result = '';
		if (drawOptions.fontStyle)
			result += drawOptions.fontStyle + ' ';
		if (drawOptions.fontWeight)
			result += drawOptions.fontWeight + ' ';
		if (drawOptions.fontSize)
		{
			if (typeof(drawOptions.fontSize) === 'string')  // already contains unit
				result += drawOptions.fontSize + ' ';
			else  // integer value
				result += drawOptions.fontSize + 'px ';
		}
		if (drawOptions.fontFamily)
			result += drawOptions.fontFamily + ' ';
		return result;
	},

	/**
	 * Draw a plain text on context.
	 * @param {Object} context
	 * @param {Object} coord The top left coord to draw text.
	 * @param {Object} text
	 * @param {Object} options Draw options, may contain the following fields:
	 * 	 {fontSize, fontFamily}
	 * @returns {Object} Null or element drawn on context.
	 */
	drawText: function(ctx, coord, text, options)
	{
		//console.log('draw text', text);
		var context = this.getOperatingContext(ctx);

		var oldFontStyle = context.font;
		var oldFillStyle = context.fillStyle;
		//context.save();

		var fontStyle = this.getCanvasFontStyle(options);

		this.setDrawStyle(context, options);

		context.font = fontStyle;
		if (options.color)
			context.fillStyle = options.color;
		context.textBaseline = 'top';
		context.fillText(text, coord.x, coord.y);

		context.fontStyle = oldFontStyle;
		context.fillStyle = oldFillStyle;

		//console.log('CANVAS DRAW TEXT', text, coord);
		//context.restore();
	},

	/**
	 * Indicate whether this bridge and context can measure text dimension before drawing it.
	 * HTML Canvas is a typical environment of this type.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canMeasureText: function(context)
	{
		return true;
	},
	/**
	 * Mearsure the width and height of text on context before drawing it.
	 * @param {Object} context
	 * @param {Object} text
	 * @param {Object} options
	 * @returns {Hash} An object with width and height fields.
	 */
	measureText: function(context, text, options)
	{
		var fontStyle = this.getCanvasFontStyle(options);
		context.font = fontStyle;
		var m = context.measureText(text);
		var fontSize = options.fontSize;
		if (typeof(fontSize) === 'string')  // with unit, e.g. 10px
			fontSize = Kekule.StyleUtils.analysisUnitsValue(fontSize).value;
		var result = {'width': m.width, 'height': fontSize};  // height can not be got from Canvas API, use font size instead
		return result;
	},

	/**
	 * Indicate whether this bridge and context can measure text dimension before drawing it.
	 * Raphael is a typical environment of this type.
	 * Such a bridge must also has the ability to modify text pos after drawn.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canMeasureDrawnText: function(context)
	{
		return false;
	},

	/**
	 * Indicate whether this bridge and context can change text content or position after drawing it.
	 * Raphael is a typical environment of this type.
	 * @param {Object} context
	 * @returns {Bool}
	 */
	canModifyText: function(context)
	{
		return false;
	},

	// group management is inavailable to canvas
	/** @ignore */
	createGroup: function(context)
	{

	},
	/** @ignore */
	addToGroup: function(elem, group)
	{

	},
	/** @ignore */
	removeFromGroup: function(elem, group)
	{

	},

	// methods about draw other glyph

	// export
	/** @ignore */
	exportToDataUri: function(context, dataType, options)
	{
		var elem = this.getContextElem(context);
		return elem? elem.toDataURL(dataType, options): null;
	}
});

/**
 * Check if current environment supports HTML canvas.
 * @returns {Bool}
 */
Kekule.Render.CanvasRendererBridge.isSupported = function()
{
	var result = false;
	if (document && document.createElement)
	{
		result = !!document.createElement('canvas').getContext;
	}
	return result;
};

//Kekule.ClassUtils.makeSingleton(Kekule.Render.CanvasRendererBridge);

Kekule.Render.DrawBridge2DMananger.register(Kekule.Render.CanvasRendererBridge, 20);