Source: display/Canvas.js

//------------------------------------------------------------------------------
// Constructor scope
//------------------------------------------------------------------------------

/**
 * Creates a new (Rune) Canvas.
 *
 * @constructor
 *
 * @param {number} [width] The width of the object, specified in pixels.
 * @param {number} [height] The height of the object, specified in pixels.
 *
 * @class
 * @classdesc
 * 
 * The Canvas class is a pixel buffer for 32-bit raster graphics and contains 
 * methods for manipulating the pixels included in the buffer. The class is 
 * used to represent all display objects and their current graphic state, ie. 
 * all display objects have their own pixel buffer (Canvas).
 */
rune.display.Canvas = function(width, height) {
	
	//--------------------------------------------------------------------------
	// Protected properties
	//--------------------------------------------------------------------------
	
	/**
	 * The HTMLCanvasElement that represents the pixel buffer.
	 *
	 * @type {Element}
	 * @protected
	 * @ignore
	 */
	this.m_canvas = null;
	
	/**
	 * The CanvasRenderingContext2D used to draw the pixel buffer.
	 *
	 * @type {Object}
	 * @protected
	 * @ignore
	 */
	this.m_context = null;
	
	/**
	 * The height of the pixel buffer.
	 *
	 * @type {number}
	 * @protected
	 * @ignore
	 */
	this.m_height = height || 16;
	
	/**
	 * The width of the pixel buffer.
	 *
	 * @type {number}
	 * @protected
	 * @ignore
	 */
	this.m_width = width || 16;
	
	//--------------------------------------------------------------------------
	// Constructor call
	//--------------------------------------------------------------------------

	/**
	 * Invokes secondary class constructor.
	 */
	this.m_construct();
};

//------------------------------------------------------------------------------
// Public prototype getter and setter methods
//------------------------------------------------------------------------------

/**
 * The HTMLCanvasElement used to draw the pixel buffer.
 *
 * @member {Object} context
 * @memberof rune.display.Canvas
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.Canvas.prototype, "context", {
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	get : function() {
		return this.m_context;
	}
});

/**
 * The HTMLCanvasElement that represents the pixel buffer.
 *
 * @member {Element} element
 * @memberof rune.display.Canvas
 * @instance
 * @readonly
 */
Object.defineProperty(rune.display.Canvas.prototype, "element", {
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	get : function() {
		return this.m_canvas;
	}
});

/**
 * The height of the pixel buffer.
 *
 * @member {number} height
 * @memberof rune.display.Canvas
 * @instance
 */
Object.defineProperty(rune.display.Canvas.prototype, "height", {
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	get : function() {
		return this.m_canvas.height;
	},
	
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	set : function(value) {
		this.m_canvas.height = value;
	}
});

/**
 * Controls whether or not the canvas is smoothed when scaled. If true, the 
 * canvas is smoothed when scaled. If false, the canvas is not smoothed when 
 * scaled.
 *
 * @member {boolean} smoothing
 * @memberof rune.display.Canvas
 * @instance
 * @default false
 */
Object.defineProperty(rune.display.Canvas.prototype, "smoothing", {
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	get : function() {
		return (this.m_canvas.style.imageRendering == "pixelated") ? false : true;
	},
	
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	set : function(value) {
		if (value == true) {
			this.m_canvas.style.imageRendering = "auto";
			this.m_context.imageSmoothingEnabled = true;
		} else {
			this.m_canvas.style.imageRendering = "pixelated";
			this.m_context.imageSmoothingEnabled = false;
		}
	}
});

/**
 * The width of the pixel buffer.
 *
 * @member {number} width
 * @memberof rune.display.Canvas
 * @instance
 */
Object.defineProperty(rune.display.Canvas.prototype, "width", {
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	get : function() {
		return this.m_canvas.width;
	},
	
	/**
	 * @this rune.display.Canvas
	 * @ignore
	 */
	set : function(value) {
		this.m_canvas.width = value;
	}
});

//------------------------------------------------------------------------------
// Public prototype methods (API)
//------------------------------------------------------------------------------

/**
 * Appends the HTMLCanvasElement to the specified DOMElement.
 *
 * @param {HTMLElement} element DOMElement to append to.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.attach = function(element) {
	if (this.m_canvas != null) {
		if (element instanceof HTMLElement) {
			element.appendChild(this.m_canvas);
		}
	}
};

/**
 * Clears the pixel buffer, ie. the value of each pixel is set to 0x00000000.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.clear = function() {
	this.m_context.clearRect(
		0,
		0,
		this.m_canvas.width,
		this.m_canvas.height
	);
};

/**
 * Removes the HTMLCanvasElement from the current DOMElement, ie. his parent.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.detach = function() {
	if (this.m_canvas != null) {
		if (this.m_canvas.parentNode != null) {
			this.m_canvas.parentNode.removeChild(this.m_canvas);
		}
	}
};

/**
 * Frees memory that is used to store the pixel buffer object.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.dispose = function() {
	this.m_disposeCanvas();
	this.m_disposeContext();
};

/**
 * creates a circular arc centered at (x, y) with a radius of r. The path 
 * starts at sa, ends at ea, and travels in the direction given 
 * by counterclockwise (defaulting to clockwise).
 *
 * @param {number} x The horizontal coordinate of the arc's center.
 * @param {number} y The vertical coordinate of the arc's center.
 * @param {number} r The arc's radius. Must be positive.
 * @param {number} sa The angle at which the arc starts in radians, measured from the positive x-axis.
 * @param {number} ea The angle at which the arc ends in radians, measured from the positive x-axis.
 * @param {number} c The color of the line.
 * @param {number} s The thickness (size) of the line.
 * @param {number} [a] An optional boolean value. If true, draws the arc counter-clockwise between the start and end angles. The default is false (clockwise).
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawArc = function(x, y, r, sa, ea, c, s, a) {
	this.m_context.save();
	this.m_context.lineWidth = s;
	this.m_context.strokeStyle = c;
	this.m_context.beginPath();
	this.m_context.arc(x, y, r, sa, ea, a);
	this.m_context.stroke();
	this.m_context.restore();
};

/**
 * Fills a rectangular area of pixels with a specified ARGB color.
 *
 * @param {string} c Fill color.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawFill = function(c) {
	this.m_context.fillStyle = c;
	this.m_context.fillRect(
		0,
		0,
		this.m_canvas.width,
		this.m_canvas.height
	);
};

/**
 * Draws a 32-bit image to the current pixel buffer.
 *
 * @param {HTMLImageElement} img An element to draw into the context. The specification permits any canvas image source (CanvasImageSource), specifically, a CSSImageValue, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement, an HTMLCanvasElement, an ImageBitmap, or an OffscreenCanvas.
 * @param {number} ox The x-axis coordinate in the destination canvas at which to place the top-left corner of the source image.
 * @param {number} oy The y-axis coordinate in the destination canvas at which to place the top-left corner of the source image.
 * @param {number} ow The width to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in width when drawn.
 * @param {number} oh The height to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in height when drawn.
 * @param {number} [cx] The x-axis coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
 * @param {number} [cy] The y-axis coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
 * @param {number} [cw] The width of the sub-rectangle of the source image to draw into the destination context. If not specified, the entire rectangle from the coordinates specified by ox and oy to the bottom-right corner of the image is used.
 * @param {number} [ch] The height of the sub-rectangle of the source image to draw into the destination context.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawImage = function(img, ox, oy, ow, oh, cx, cy, cw, ch) {
	this.m_context.save();
	this.m_context.drawImage(
		img,
		cx || 0,
		cy || 0,
		cw || ow,
		ch || oh,
		ox,
		oy,
		ow,
		oh
	);
	
	this.m_context.restore();
};

/**
 * Draws a picture according to a repeating (tile) pattern.
 *
 * @param {HTMLImageElement} img An element to draw into the context.
 * @param {number} x The x-axis coordinate in the destination canvas at which to place the top-left corner of the source image.
 * @param {number} y The y-axis coordinate in the destination canvas at which to place the top-left corner of the source image.
 * @param {number} w The width to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in width when drawn.
 * @param {number} h The height to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not scaled in height when drawn.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawImageFill = function(img, x, y, w, h) {
	this.m_context.save();
	this.m_context.fillStyle = this.m_context.createPattern(img, "repeat");
	this.m_context.translate(-x, -y);
	this.m_context.fillRect(x, y, w, h);
	this.m_context.restore();
};

/**
 * Draws a straight line between two points.
 *
 * @param {number} x1 the x-coordinate of the first point.
 * @param {number} y1 the y-coordinate of the first point.
 * @param {number} x2 the x-coordinate of the second point.
 * @param {number} y2 the y-coordinate of the second point.
 * @param {string} c The color of the line.
 * @param {number} s The thickness (size) of the line.
 * @param {number} a The transparency of the line.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawLine = function(x1, y1, x2, y2, c, s, a) {
	this.m_context.save();
	this.m_context.globalAlpha = a;
	this.m_context.beginPath();
	this.m_context.translate(0.5, 0.5);
	this.m_context.strokeStyle = c;
	this.m_context.lineWidth = s;
	this.m_context.moveTo(x1, y1);
	this.m_context.lineTo(x2, y2);
	this.m_context.stroke();
	this.m_context.restore();
};

/**
 * Draws a rectangle.
 *
 * @param {number} x The x-position of the rectangle.
 * @param {number} y The y-position of the rectangle.
 * @param {number} w The width of the rectangle.
 * @param {number} h The height of the rectangle.
 * @param {string} c The color of the rectangle.
 * @param {number} s The thickness (size) of the lines of the rectangle.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawRect = function(x, y, w, h, c, s) {
	this.m_context.save();
	this.m_context.translate(0.5, 0.5);
	this.m_context.strokeStyle = c;
	this.m_context.lineWidth = s;
	this.m_context.strokeRect(
		x,
		y,
		w,
		h
	);
	
	this.m_context.restore();
};

/**
 * Draws a filled rectangle.
 *
 * @param {number} x The x-position of the rectangle.
 * @param {number} y The y-position of the rectangle.
 * @param {number} w The width of the rectangle.
 * @param {number} h The height of the rectangle.
 * @param {string} c The color of the rectangle.
 * @param {number} a Transparency of the color.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.drawRectFill = function(x, y, w, h, c, a) {
	this.m_context.save();
	this.m_context.globalAlpha = a;
	this.m_context.fillStyle = c;
	this.m_context.fillRect(
		x,
		y,
		w,
		h
	);
	
	this.m_context.restore();
};

/**
 * Determines whether the object specified in the rect parameter intersects 
 * with this canvas object.
 *
 * @param {rune.geom.Rectangle} rect The Rectangle object to compare against.
 *
 * @return {boolean} A value of true if the specified object intersects with this Rectangle object; otherwise false.
 */
rune.display.Canvas.prototype.intersects = function(rect) {
	return rune.geom.Rectangle.intersects(
		0,
		0,
		this.m_canvas.width,
		this.m_canvas.height,
		rect.x,
		rect.y,
		rect['width'],
		rect['height']
	);
};

/**
 * Renders a Display Object according to its visual properties. The rendering 
 * process includes rendering of any sub-objects, etc..
 *
 * @param {rune.display.DisplayObject} obj The object to be rendered.
 * @param {number} [offsetX] Render offset in x direction.
 * @param {number} [offsetY] Render offset in y direction.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.renderDisplayObject = function(obj, offsetX, offsetY) {
	if (obj['hidden'] == false) {
		var frame = obj.getRenderFrame();
			frame.x -= offsetX || 0;
			frame.y -= offsetY || 0;
			
		if (this.intersects(frame)) {
			obj.render();
			
			var fx = (obj['flippedX']) ? -1 : 1;
			var fy = (obj['flippedY']) ? -1 : 1;
			var tx = (frame.x + obj['pivotX']);
			var ty = (frame.y + obj['pivotY']);
			
			this.m_context.save();
			this.m_context.translate(tx, ty);
			this.m_context.scale(fx, fy);
			this.m_context.rotate(obj['rotation'] * rune.util.Math.DEG_TO_RAD);
			this.m_context.translate(-tx, -ty);
			this.m_context.globalAlpha = obj['alpha'];
			this.m_context.drawImage(
				obj['canvas']['element'],
				frame['clipping']['x'],
				frame['clipping']['y'],
				frame['clipping']['width'],
				frame['clipping']['height'],
				frame['x'],
				frame['y'],
				frame['width'],
				frame['height']
			);
			
			this.m_context.restore();
		}	
	}
};

/**
 * Renders a Path object as a series of connected lines.
 *
 * @param {rune.util.Path} path Path to render.
 * @param {number} [offsetX] Displacement in x direction.
 * @param {number} [offsetY] Displacement in y direction.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.renderPath = function(path, offsetX, offsetY) {
	offsetX = offsetX || 0;
	offsetY = offsetY || 0;
	
	var a = null;
	var b = null;
	
	for (var i = 0; i < path['length']; i++) {
		a = path.getAt(i + 0);
		b = path.getAt(i + 1);
		
		if (a && b) {
			this.drawLine(
				a['x'] - offsetX,
				a['y'] - offsetY,
				b['x'] - offsetX,
				b['y'] - offsetY,
				path.color,
				path.thickness,
				path.alpha
			);
		}
	}
};

/**
 * Renders Tiles from a Tilemap.
 *
 * @param {rune.tilemap.Tilemap} map The map to be rendered.
 * @param {rune.geom.Rectangle} rect Which part of the map to render.
 * @param {number} buffer Index to the buffer (layer) to be rendered.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.renderTiles = function(map, rect, buffer) {
	var ti = (buffer == 0) ? map.getBackBufferInRect(rect) : map.getFrontBufferInRect(rect);
	var wt = map['widthInTiles'];
	var ht = map['heightInTiles'];
	var tw = map['tileWidth'];
	var th = map['tileHeight'];
	var ox = rect['x'] % tw;
	var oy = rect['y'] % th;
	var tv = 0;
	var tp = null;
	
	if (ox < 0) {
		ox = tw + ox;
	}
	
	if (oy < 0) {
		oy = th + oy;
	}
	
	var ua = Math.floor(rect['x'] / tw);
	var ub = Math.floor(rect['y'] / th);
	
	for (var i = 0; i < ti.length; i++) {
		var tb = (buffer == 0) ? 'back' : 'front';
		tv = map[tb].getTileValueAt(ti[i]);
		if (tv > 0) {
			tp = map.getTileTextureRectOf(tv);
			this.m_context.drawImage(
				map['texture'],
				tp.x,
				tp.y,
				tw,
				th,
				(((ti[i] % wt) - ua) * tw) - ox,
				((Math.floor(ti[i] / wt) - ub) * th) - oy,
				tw,
				th
			);
		}
	}
};

/**
 * Resizes the canvas object to specified dimensions.
 *
 * @param {number} width The new width of the canvas.
 * @param {number} height The new height of the canvas.
 *
 * @return {undefined}
 */
rune.display.Canvas.prototype.resize = function(width, height) {
	this.m_canvas.width = width;
	this.m_canvas.height = height;
};

//------------------------------------------------------------------------------
// Protected prototype methods
//------------------------------------------------------------------------------

/**
 * The class constructor.
 *
 * @return {undefined}
 * @protected
 * @ignore
 */
rune.display.Canvas.prototype.m_construct = function() {
	this.m_constructCanvas();
	this.m_constructContext();
};

/**
 * Creates the HTMLCanvasElement that represents the pixel buffer.
 *
 * @throws {Error} If a canvas already exists.
 *
 * @return {undefined}
 * @protected
 * @ignore
 */
rune.display.Canvas.prototype.m_constructCanvas = function() {
	this.m_disposeCanvas();
	if (this.m_canvas === null) {
		this.m_canvas = document.createElement('canvas');
		this.m_canvas.width  = this.m_width;
		this.m_canvas.height = this.m_height;
		this.m_canvas.style.imageRendering = "pixelated";
	} else throw new Error();
};

/**
 * Creates the CanvasRenderingContext2D used to draw the pixel buffer.
 *
 * @throws {Error} If a context already exists.
 *
 * @return {undefined}
 * @protected
 * @ignore
 */
rune.display.Canvas.prototype.m_constructContext = function() {
	this.m_disposeContext();
	if (this.m_context == null && this.m_canvas instanceof HTMLCanvasElement ) {
		this.m_context = this.m_canvas.getContext("2d", {
			willReadFrequently: true
		});
		this.m_context.imageSmoothingEnabled = false;
		this.m_context.imageSmoothingQuality = "low";
	} else throw new Error();
};

/**
 * Deletes current context.
 *
 * @return {undefined}
 * @protected
 * @ignore
 */
rune.display.Canvas.prototype.m_disposeContext = function() {
	if (this.m_context != null) {
		this.m_context  = null;
	};
};

/**
 * Deletes current canvas.
 *
 * @return {undefined}
 * @protected
 * @ignore
 */
rune.display.Canvas.prototype.m_disposeCanvas = function() {
	if (this.m_canvas instanceof HTMLCanvasElement) {
		if (this.m_canvas.parentNode != null) {
			this.m_canvas.parentNode.removeChild(this.m_canvas);
		}
		
		this.m_canvas = null;
	}
};