/// /// /// /// /// declare module PixelDrawr { /** * A typed array of 8-bit unsigned integer values. The contents are initialized * to 0. If the requested number of bytes could not be allocated an exception is * raised. */ interface Uint8ClampedArray extends ArrayBufferView { [index: number]: number; /** * The size in bytes of each element in the array. */ BYTES_PER_ELEMENT: number; /** * The length of the array. */ length: number; /** * Gets the element at the specified index. * * @param {Number} index The index at which to get the element of the array. */ get(index: number): number; /** * Sets a value or an array of values. * * @param {Number} index The index of the location to set. * @param {Number} value The value to set. */ set(index: number, value: number): void; /** * Sets a value or an array of values. * * @param {Uint8ClampedArray} array A typed or untyped array of values * to set. * @param {Number} [offset] The index in the current array at which the * values are to be written. */ set(array: Uint8ClampedArray, offset?: number): void; /** * Sets a value or an array of values. * * @param {Number[]} array A typed or untyped array of values to set. * @param {Number} [offset] The index in the current array at which the * values are to be written. */ set(array: number[], offset?: number): void; /** * Gets a new Uint8ClampedArray view of the ArrayBuffer Object store for * this array, specifying the first and last members of the subarray. * * @param {Number} begin The index of the beginning of the array. * @param {Number} end The index of the end of the array. */ subarray(begin: number, end?: number): Uint8ClampedArray; } var Uint8ClampedArray: { prototype: Uint8ClampedArray; new (length: number): Uint8ClampedArray; new (array: Uint8ClampedArray): Uint8ClampedArray; new (array: number[]): Uint8ClampedArray; new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Uint8ClampedArray; BYTES_PER_ELEMENT: number; } export interface IScreenBoundaries { top: number; right: number; bottom: number; left: number; } export interface IThingCanvases { direction: string; multiple: boolean; middle?: IThingSubCanvas; top?: IThingSubCanvas; right?: IThingSubCanvas; bottom?: IThingSubCanvas; left?: IThingSubCanvas; topRight?: IThingSubCanvas; bottomRight?: IThingSubCanvas; bottomLeft?: IThingSubCanvas; topLeft?: IThingSubCanvas; } export interface IThingSubCanvas { "canvas": HTMLCanvasElement; "context": CanvasRenderingContext2D; } export interface IThing { /** * The sprite for this Thing to have drawn. */ sprite: Uint8ClampedArray | PixelRendr.ISpriteMultiple; /** * The canvas upon which the Thing's sprite is to be drawn. */ canvas: HTMLCanvasElement; /** * For Things with multiple sprites, the various sprite component canvases. */ canvases?: IThingCanvases; /** * The rendering context used to draw the Thing's sprite on its canvas. */ context: CanvasRenderingContext2D; /** * Whether this shouldn't be drawn (is completely hidden). */ hidden: boolean; /** * How transparent this is, in [0, 1]. */ opacity: number; /** * How many sprites this has (1 for regular, 0 or >1 for multiple). */ numSprites?: number; /** * Whether the Thing's sprite should repeat across large canvases. */ repeat?: boolean; /** * How much to expand the Thing's sprite size (by default, 1 for not at all). */ scale?: number; /** * Width in game pixels, equal to width * unitsize. */ unitwidth?: number; /** * Height in game pixels, equal to height * unitsize. */ unitheight?: number; /** * How many pixels wide the output sprite should be. */ spritewidth: number; /** * How many pixels high the output sprite should be. */ spriteheight: number; /** * Sprite width in real-life pixels, equal to spritewidth * scale. */ spritewidthpixels?: number; /** * Sprite height in real-life pixels, equal to spritewidth * scale. */ spriteheightpixels?: number; } export interface IPixelDrawrSettings { /** * The PixelRendr used for sprite lookups and generation. */ PixelRender: PixelRendr.IPixelRendr; /** * The bounds of the screen for bounds checking (typically a MapScreenr). */ MapScreener: IScreenBoundaries; /** * A Function to create a canvas of a given width and height. */ createCanvas: (width: number, height: number) => HTMLCanvasElement; /** * How much to scale canvases on creation (by default, 1 for not at all). */ unitsize?: number; /** * Whether refills should skip redrawing the background each time. */ noRefill?: boolean; /** * The maximum size of a SpriteMultiple to pre-render (by default, 0 for * never pre-rendering). */ spriteCacheCutoff?: number; /** * The names of groups to refill (only used if using Quadrant refilling). */ groupNames?: string[]; /** * How often to draw frames (by default, 1 for every time). */ framerateSkip?: number; /** * How to generat ekeys to retrieve sprites from the PixelRendr (by default, * Object.toString). */ generateObjectKey?: (thing: IThing) => string; /** * An arbitrarily small minimum opacity for a Thing to be considered not * completely transparent (by default, .007). */ epsilon?: number; /** * The attribute name for a Thing's width (by default, "width"). */ keyWidth?: string; /** * The attribute name for a Thing's height (by default, "height"). */ keyHeight?: string; /** * The attribute name for a Thing's top (by default, "top"). */ keyTop?: string; /** * The attribute name for a Thing's right (by default, "right"). */ keyRight?: string; /** * The attribute name for a Thing's bottom (by default, "bottom"). */ keyBottom?: string; /** * The attribute name for a Thing's left (by default, "left"). */ keyLeft?: string; /** * The attribute name for a Thing's horizontal offest (by default, ignored). */ keyOffsetX?: string; /** * The attribute name for a Thing's vertical offset(by default, ignored). */ keyOffsetY?: string; } export interface IPixelDrawr { getFramerateSkip(): number; getThingArray(): IThing[][]; getCanvas(): HTMLCanvasElement; getContext(): CanvasRenderingContext2D; getBackgroundCanvas(): HTMLCanvasElement; getBackgroundContext(): CanvasRenderingContext2D; getNoRefill(): boolean; getEpsilon(): number; setFramerateSkip(framerateSkip: number): void; setThingArrays(thingArrays: IThing[][]): void; setCanvas(canvas: HTMLCanvasElement): void; setNoRefill(noRefill: boolean): void; setEpsilon(epsilon: number): void; resetBackground(): void; setBackground(fillStyle: any): void; drawBackground(): void; setThingSprite(thing: IThing): void; refillThingCanvasSingle(thing: IThing): void; refillThingCanvasMultiple(thing: IThing): void; refillGlobalCanvas(): void; refillThingArray(array: IThing[]): void; refillQuadrantGroups(groups: QuadsKeepr.IQuadrantRow[]): void; refillQuadrants(quadrants: QuadsKeepr.IQuadrant[]): void; refillQuadrant(quadrant: QuadsKeepr.IQuadrant): void; drawThingOnContext(context: CanvasRenderingContext2D, thing: IThing): void; drawThingOnQuadrant(thing: IThing, quadrant: QuadsKeepr.IQuadrant): void; drawThingOnContextSingle( context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, thing: IThing, left: number, top: number): void; drawThingOnContextMultiple( context: CanvasRenderingContext2D, canvases: IThingCanvases, thing: IThing, left: number, top: number ): void; } } module PixelDrawr { "use strict"; /** * A front-end to PixelRendr to automate drawing mass amounts of sprites to a * primary canvas. A PixelRendr keeps track of sprite sources, while a * MapScreenr maintains boundary information on the screen. Global screen * refills may be done by drawing every Thing in the thingArrays, or by * Quadrants as a form of dirty rectangles. */ export class PixelDrawr implements IPixelDrawr { /** * A PixelRendr used to obtain raw sprite data and canvases. */ private PixelRender: PixelRendr.IPixelRendr; /** * The bounds of the screen for bounds checking (often a MapScreenr). */ private MapScreener: IScreenBoundaries; /** * The canvas element each Thing is to be drawn on. */ private canvas: HTMLCanvasElement; /** * The 2D canvas context associated with the canvas. */ private context: CanvasRenderingContext2D; /** * A separate canvas that keeps the background of the scene. */ private backgroundCanvas: HTMLCanvasElement; /** * The 2D canvas context associated with the background canvas. */ private backgroundContext: CanvasRenderingContext2D; /** * Arrays of Thing[]s that are to be drawn in each refill. */ private thingArrays: IThing[][]; /** * Utility Function to create a canvas. */ private createCanvas: (width: number, height: number) => HTMLCanvasElement; /** * How much to scale canvases on creation. */ private unitsize: number; /** * Utility Function to generate a class key for a Thing. */ private generateObjectKey: (thing: IThing) => string; /** * The maximum size of a SpriteMultiple to pre-render. */ private spriteCacheCutoff: number; /** * Whether refills should skip redrawing the background each time. */ private noRefill: boolean; /** * For refillQuadrant, an Array of String names to refill (bottom-to-top). */ private groupNames: string[]; /** * How often the screen redraws (1 for always, 2 for every other call, etc). */ private framerateSkip: number; /** * How many frames have been drawn so far. */ private framesDrawn: number; /** * An arbitrarily small minimum for opacity to be completely transparent. */ private epsilon: number; /** * String key under which Things store their height. */ private keyHeight: string; /** * String key under which Things store their width. */ private keyWidth: string; /** * String key under which Things store their top. */ private keyTop: string; /** * String key under which Things store their right. */ private keyRight: string; /** * String key under which Things store their bottom. */ private keyBottom: string; /** * String key under which Things store their left. */ private keyLeft: string; /** * String key under which Things store their horizontal offset. */ private keyOffsetX: string; /** * String key under which Things store their vertical offset. */ private keyOffsetY: string; /** * @param {IPixelDrawrSettings} settings */ constructor(settings: IPixelDrawrSettings) { if (!settings) { throw new Error("No settings object given to PixelDrawr."); } if (typeof settings.PixelRender === "undefined") { throw new Error("No PixelRender given to PixelDrawr."); } if (typeof settings.MapScreener === "undefined") { throw new Error("No MapScreener given to PixelDrawr."); } if (typeof settings.createCanvas === "undefined") { throw new Error("No createCanvas given to PixelDrawr."); } this.PixelRender = settings.PixelRender; this.MapScreener = settings.MapScreener; this.createCanvas = settings.createCanvas; this.unitsize = settings.unitsize || 1; this.noRefill = settings.noRefill; this.spriteCacheCutoff = settings.spriteCacheCutoff || 0; this.groupNames = settings.groupNames; this.framerateSkip = settings.framerateSkip || 1; this.framesDrawn = 0; this.epsilon = settings.epsilon || .007; this.keyWidth = settings.keyWidth || "width"; this.keyHeight = settings.keyHeight || "height"; this.keyTop = settings.keyTop || "top"; this.keyRight = settings.keyRight || "right"; this.keyBottom = settings.keyBottom || "bottom"; this.keyLeft = settings.keyLeft || "left"; this.keyOffsetX = settings.keyOffsetX; this.keyOffsetY = settings.keyOffsetY; this.generateObjectKey = settings.generateObjectKey || function (thing: IThing): string { return thing.toString(); }; this.resetBackground(); } /* Simple gets */ /** * @return {Number} How often refill calls should be skipped. */ getFramerateSkip(): number { return this.framerateSkip; } /** * @return {Array[]} The Arrays to be redrawn during refill calls. */ getThingArray(): IThing[][] { return this.thingArrays; } /** * @return {HTMLCanvasElement} The canvas element each Thing is to drawn on. */ getCanvas(): HTMLCanvasElement { return this.canvas; } /** * @return {CanvasRenderingContext2D} The 2D canvas context associated with * the canvas. */ getContext(): CanvasRenderingContext2D { return this.context; } /** * @return {HTMLCanvasElement} The canvas element used for the background. */ getBackgroundCanvas(): HTMLCanvasElement { return this.backgroundCanvas; } /** * @return {CanvasRenderingContext2D} The 2D canvas context associated with * the background canvas. */ getBackgroundContext(): CanvasRenderingContext2D { return this.backgroundContext; } /** * @return {Boolean} Whether refills should skip redrawing the background * each time. */ getNoRefill(): boolean { return this.noRefill; } /** * @return {Number} The minimum opacity that will be drawn. */ getEpsilon(): number { return this.epsilon; } /* Simple sets */ /** * @param {Number} framerateSkip How often refill calls should be skipped. */ setFramerateSkip(framerateSkip: number): void { this.framerateSkip = framerateSkip; } /** * @param {Array[]} thingArrays The Arrays to be redrawn during refill calls. */ setThingArrays(thingArrays: IThing[][]): void { this.thingArrays = thingArrays; } /** * Sets the currently drawn canvas and context, and recreates * drawThingOnContextBound. * * @param {HTMLCanvasElement} canvas The new primary canvas to be used. */ setCanvas(canvas: HTMLCanvasElement): void { this.canvas = canvas; this.context = canvas.getContext("2d"); } /** * @param {Boolean} noRefill Whether refills should now skip redrawing the * background each time. */ setNoRefill(noRefill: boolean): void { this.noRefill = noRefill; } /** * @param {Number} The minimum opacity that will be drawn. */ setEpsilon(epsilon: number): void { this.epsilon = epsilon; } /* Background manipulations */ /** * Creates a new canvas the size of MapScreener and sets the background * canvas to it, then recreates backgroundContext. */ resetBackground(): void { this.backgroundCanvas = this.createCanvas(this.MapScreener[this.keyWidth], this.MapScreener[this.keyHeight]); this.backgroundContext = this.backgroundCanvas.getContext("2d"); } /** * Refills the background canvas with a new fillStyle. * * @param {Mixed} fillStyle The new fillStyle for the background context. */ setBackground(fillStyle: any): void { this.backgroundContext.fillStyle = fillStyle; this.backgroundContext.fillRect(0, 0, this.MapScreener[this.keyWidth], this.MapScreener[this.keyHeight]); } /** * Draws the background canvas onto the main canvas' context. */ drawBackground(): void { this.context.drawImage(this.backgroundCanvas, 0, 0); } /* Core rendering */ /** * Goes through all the motions of finding and parsing a Thing's sprite. * This should be called whenever the sprite's appearance changes. * * @param {Thing} thing A Thing whose sprite must be updated. * @return {Self} */ setThingSprite(thing: IThing): void { // If it's set as hidden, don't bother updating it if (thing.hidden) { return; } // PixelRender does most of the work in fetching the rendered sprite thing.sprite = this.PixelRender.decode(this.generateObjectKey(thing), thing); // To do: remove dependency on .numSprites // For now, it's used to know whether it's had its sprite set, but // wouldn't physically having a .sprite do that? if (thing.sprite.constructor === PixelRendr.SpriteMultiple) { thing.numSprites = 0; this.refillThingCanvasMultiple(thing); } else { thing.numSprites = 1; this.refillThingCanvasSingle(thing); } } /** * Simply draws a thing's sprite to its canvas by getting and setting * a canvas::imageData object via context.getImageData(...). * * @param {Thing} thing A Thing whose canvas must be updated. */ refillThingCanvasSingle(thing: IThing): void { // Don't draw small Things. if (thing[this.keyWidth] < 1 || thing[this.keyHeight] < 1) { return; } // Retrieve the imageData from the Thing's canvas & renderingContext var canvas: HTMLCanvasElement = thing.canvas, context: CanvasRenderingContext2D = thing.context, imageData: ImageData = context.getImageData(0, 0, canvas[this.keyWidth], canvas[this.keyHeight]); // Copy the thing's sprite to that imageData and into the contextz this.PixelRender.memcpyU8(thing.sprite, imageData.data); context.putImageData(imageData, 0, 0); } /** * For SpriteMultiples, this copies the sprite information for each * sub-sprite into its own canvas, sets thing.sprites, then draws the newly * rendered information onto the thing's canvas. * * @param {Thing} thing A Thing whose canvas and sprites must be updated. */ refillThingCanvasMultiple(thing: IThing): void { if (thing[this.keyWidth] < 1 || thing[this.keyHeight] < 1) { return; } var spritesRaw: PixelRendr.SpriteMultiple = thing.sprite, canvases: any = thing.canvases = { "direction": spritesRaw.direction, "multiple": true }, canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, imageData: ImageData, i: string; thing.numSprites = 1; for (i in spritesRaw.sprites) { if (!spritesRaw.sprites.hasOwnProperty(i)) { continue; } // Make a new sprite for this individual component canvas = this.createCanvas(thing.spritewidth * this.unitsize, thing.spriteheight * this.unitsize); context = canvas.getContext("2d"); // Copy over this sprite's information the same way as refillThingCanvas imageData = context.getImageData(0, 0, canvas[this.keyWidth], canvas[this.keyHeight]); this.PixelRender.memcpyU8(spritesRaw.sprites[i], imageData.data); context.putImageData(imageData, 0, 0); // Record the canvas and context in thing.sprites canvases[i] = { "canvas": canvas, "context": context }; thing.numSprites += 1; } // Only pre-render multiple sprites if they're below the cutoff if (thing[this.keyWidth] * thing[this.keyHeight] < this.spriteCacheCutoff) { thing.canvas[this.keyWidth] = thing[this.keyWidth] * this.unitsize; thing.canvas[this.keyHeight] = thing[this.keyHeight] * this.unitsize; this.drawThingOnContextMultiple(thing.context, thing.canvases, thing, 0, 0); } else { thing.canvas[this.keyWidth] = thing.canvas[this.keyHeight] = 0; } } /* Core drawing */ /** * Called every upkeep to refill the entire main canvas. All Thing arrays * are made to call this.refillThingArray in order. */ refillGlobalCanvas(): void { this.framesDrawn += 1; if (this.framesDrawn % this.framerateSkip !== 0) { return; } if (!this.noRefill) { this.drawBackground(); } for (var i: number = 0; i < this.thingArrays.length; i += 1) { this.refillThingArray(this.thingArrays[i]); } } /** * Calls drawThingOnContext on each Thing in the Array. * * @param {Thing[]} array A listing of Things to be drawn onto the canvas. */ refillThingArray(array: IThing[]): void { for (var i: number = 0; i < array.length; i += 1) { this.drawThingOnContext(this.context, array[i]); } } /** * Refills the main canvas by calling refillQuadrants on each Quadrant in * the groups. * * @param {QuadrantRow[]} groups QuadrantRows (or QuadrantCols) to be * redrawn to the canvas. */ refillQuadrantGroups(groups: QuadsKeepr.IQuadrantRow[]): void { var i: number; this.framesDrawn += 1; if (this.framesDrawn % this.framerateSkip !== 0) { return; } for (i = 0; i < groups.length; i += 1) { this.refillQuadrants(groups[i].quadrants); } } /** * Refills (part of) the main canvas by drawing each Quadrant's canvas onto * it. * * @param {Quadrant[]} quadrants The Quadrants to have their canvases * refilled. */ refillQuadrants(quadrants: QuadsKeepr.IQuadrant[]): void { var quadrant: QuadsKeepr.IQuadrant, i: number; for (i = 0; i < quadrants.length; i += 1) { quadrant = quadrants[i]; if ( quadrant.changed && quadrant[this.keyTop] < this.MapScreener[this.keyHeight] && quadrant[this.keyRight] > 0 && quadrant[this.keyBottom] > 0 && quadrant[this.keyLeft] < this.MapScreener[this.keyWidth]) { this.refillQuadrant(quadrant); this.context.drawImage(quadrant.canvas, quadrant[this.keyLeft], quadrant[this.keyTop]); } } } /** * Refills a Quadrants's canvas by resetting its background and drawing all * its Things onto it. * * @param {Quadrant} quadrant A quadrant whose Things must be drawn onto * its canvas. */ refillQuadrant(quadrant: QuadsKeepr.IQuadrant): void { var group: IThing[], i: number, j: number; // This may be what's causing such bad performance. if (!this.noRefill) { quadrant.context.drawImage( this.backgroundCanvas, quadrant[this.keyLeft], quadrant[this.keyTop], quadrant.canvas[this.keyWidth], quadrant.canvas[this.keyHeight], 0, 0, quadrant.canvas[this.keyWidth], quadrant.canvas[this.keyHeight]); } for (i = this.groupNames.length - 1; i >= 0; i -= 1) { group = quadrant.things[this.groupNames[i]]; for (j = 0; j < group.length; j += 1) { this.drawThingOnQuadrant(group[j], quadrant); } } quadrant.changed = false; } /** * General Function to draw a Thing onto a context. This will call * drawThingOnContext[Single/Multiple] with more arguments * * @param {CanvasRenderingContext2D} context The context to have the Thing * drawn on it. * @param {Thing} thing The Thing to be drawn onto the context. */ drawThingOnContext(context: CanvasRenderingContext2D, thing: IThing): void { if ( thing.hidden || thing.opacity < this.epsilon || thing[this.keyHeight] < 1 || thing[this.keyWidth] < 1 || this.getTop(thing) > this.MapScreener[this.keyHeight] || this.getRight(thing) < 0 || this.getBottom(thing) < 0 || this.getLeft(thing) > this.MapScreener[this.keyWidth]) { return; } // If Thing hasn't had a sprite yet (previously hidden), do that first if (typeof thing.numSprites === "undefined") { this.setThingSprite(thing); } // Whether or not the thing has a regular sprite or a SpriteMultiple, // that sprite has already been drawn to the thing's canvas, unless it's // above the cutoff, in which case that logic happens now. if (thing.canvas[this.keyWidth] > 0) { this.drawThingOnContextSingle(context, thing.canvas, thing, this.getLeft(thing), this.getTop(thing)); } else { this.drawThingOnContextMultiple(context, thing.canvases, thing, this.getLeft(thing), this.getTop(thing)); } } /** * Draws a Thing onto a quadrant's canvas. This is a simple wrapper around * drawThingOnContextSingle/Multiple that also bounds checks. * * @param {Thing} thing * @param {Quadrant} quadrant */ drawThingOnQuadrant(thing: IThing, quadrant: QuadsKeepr.IQuadrant): void { if ( thing.hidden || this.getTop(thing) > quadrant[this.keyBottom] || this.getRight(thing) < quadrant[this.keyLeft] || this.getBottom(thing) < quadrant[this.keyTop] || this.getLeft(thing) > quadrant[this.keyRight] || thing.opacity < this.epsilon) { return; } // If there's just one sprite, it's pretty simple if (thing.numSprites === 1) { return this.drawThingOnContextSingle( quadrant.context, thing.canvas, thing, this.getLeft(thing) - quadrant[this.keyLeft], this.getTop(thing) - quadrant[this.keyTop]); } else { // For multiple sprites, some calculations will be needed return this.drawThingOnContextMultiple( quadrant.context, thing.canvases, thing, this.getLeft(thing) - quadrant[this.keyLeft], this.getTop(thing) - quadrant[this.keyTop]); } } /** * Draws a Thing's single canvas onto a context, commonly called by * this.drawThingOnContext. * * @param {CanvasRenderingContext2D} context The context being drawn on. * @param {Canvas} canvas The Thing's canvas being drawn onto the context. * @param {Thing} thing The Thing whose canvas is being drawn. * @param {Number} left The x-position to draw the Thing from. * @param {Number} top The y-position to draw the Thing from. */ drawThingOnContextSingle( context: CanvasRenderingContext2D, canvas: HTMLCanvasElement, thing: IThing, left: number, top: number): void { // If the sprite should repeat, use the pattern equivalent if (thing.repeat) { this.drawPatternOnContext(context, canvas, left, top, thing.unitwidth, thing.unitheight, thing.opacity || 1); } else if (thing.opacity !== 1) { // Opacities not equal to one must reset the context afterwards context.globalAlpha = thing.opacity; context.drawImage(canvas, left, top, canvas.width * thing.scale, canvas.height * thing.scale); context.globalAlpha = 1; } else { context.drawImage(canvas, left, top, canvas.width * thing.scale, canvas.height * thing.scale); } } /** * Draws a Thing's multiple canvases onto a context, typicall called by * drawThingOnContext. A variety of cases for canvases is allowed: * "vertical", "horizontal", and "corners". * * @param {CanvasRenderingContext2D} context The context being drawn on. * @param {Canvas} canvases The canvases being drawn onto the context. * @param {Thing} thing The Thing whose canvas is being drawn. * @param {Number} left The x-position to draw the Thing from. * @param {Number} top The y-position to draw the Thing from. */ drawThingOnContextMultiple( context: CanvasRenderingContext2D, canvases: IThingCanvases, thing: IThing, left: number, top: number): void { var sprite: PixelRendr.SpriteMultiple = thing.sprite, topreal: number = top, leftreal: number = left, rightreal: number = left + thing.unitwidth, bottomreal: number = top + thing.unitheight, widthreal: number = thing.unitwidth, heightreal: number = thing.unitheight, spritewidthpixels: number = thing.spritewidthpixels, spriteheightpixels: number = thing.spriteheightpixels, widthdrawn: number = Math.min(widthreal, spritewidthpixels), heightdrawn: number = Math.min(heightreal, spriteheightpixels), opacity: number = thing.opacity, diffhoriz: number, diffvert: number, canvasref: IThingSubCanvas; switch (canvases.direction) { // Vertical sprites may have 'top', 'bottom', 'middle' case "vertical": // If there's a bottom, draw that and push up bottomreal if ((canvasref = canvases[this.keyBottom])) { diffvert = sprite.bottomheight ? sprite.bottomheight * this.unitsize : spriteheightpixels; this.drawPatternOnContext( context, canvasref.canvas, leftreal, bottomreal - diffvert, widthreal, heightdrawn, opacity); bottomreal -= diffvert; heightreal -= diffvert; } // If there's a top, draw that and push down topreal if ((canvasref = canvases[this.keyTop])) { diffvert = sprite.topheight ? sprite.topheight * this.unitsize : spriteheightpixels; this.drawPatternOnContext(context, canvasref.canvas, leftreal, topreal, widthreal, heightdrawn, opacity); topreal += diffvert; heightreal -= diffvert; } break; // Horizontal sprites may have 'left', 'right', 'middle' case "horizontal": // If there's a left, draw that and push forward leftreal if ((canvasref = canvases[this.keyLeft])) { diffhoriz = sprite.leftwidth ? sprite.leftwidth * this.unitsize : spritewidthpixels; this.drawPatternOnContext(context, canvasref.canvas, leftreal, topreal, widthdrawn, heightreal, opacity); leftreal += diffhoriz; widthreal -= diffhoriz; } // If there's a right, draw that and push back rightreal if ((canvasref = canvases[this.keyRight])) { diffhoriz = sprite.rightwidth ? sprite.rightwidth * this.unitsize : spritewidthpixels; this.drawPatternOnContext( context, canvasref.canvas, rightreal - diffhoriz, topreal, widthdrawn, heightreal, opacity); rightreal -= diffhoriz; widthreal -= diffhoriz; } break; // Corner (vertical + horizontal + corner) sprites must have corners // in 'topRight', 'bottomRight', 'bottomLeft', and 'topLeft'. case "corners": // topLeft, left, bottomLeft diffvert = sprite.topheight ? sprite.topheight * this.unitsize : spriteheightpixels; diffhoriz = sprite.leftwidth ? sprite.leftwidth * this.unitsize : spritewidthpixels; this.drawPatternOnContext( context, canvases.topLeft.canvas, leftreal, topreal, widthdrawn, heightdrawn, opacity); this.drawPatternOnContext( context, canvases[this.keyLeft].canvas, leftreal, topreal + diffvert, widthdrawn, heightreal - diffvert * 2, opacity); this.drawPatternOnContext( context, canvases.bottomLeft.canvas, leftreal, bottomreal - diffvert, widthdrawn, heightdrawn, opacity); leftreal += diffhoriz; widthreal -= diffhoriz; // top, topRight diffhoriz = sprite.rightwidth ? sprite.rightwidth * this.unitsize : spritewidthpixels; this.drawPatternOnContext( context, canvases[this.keyTop].canvas, leftreal, topreal, widthreal - diffhoriz, heightdrawn, opacity); this.drawPatternOnContext( context, canvases.topRight.canvas, rightreal - diffhoriz, topreal, widthdrawn, heightdrawn, opacity); topreal += diffvert; heightreal -= diffvert; // right, bottomRight, bottom diffvert = sprite.bottomheight ? sprite.bottomheight * this.unitsize : spriteheightpixels; this.drawPatternOnContext( context, canvases[this.keyRight].canvas, rightreal - diffhoriz, topreal, widthdrawn, heightreal - diffvert, opacity); this.drawPatternOnContext( context, canvases.bottomRight.canvas, rightreal - diffhoriz, bottomreal - diffvert, widthdrawn, heightdrawn, opacity); this.drawPatternOnContext( context, canvases[this.keyBottom].canvas, leftreal, bottomreal - diffvert, widthreal - diffhoriz, heightdrawn, opacity); rightreal -= diffhoriz; widthreal -= diffhoriz; bottomreal -= diffvert; heightreal -= diffvert; break; default: throw new Error("Unknown or missing direction given in SpriteMultiple."); } // If there's still room, draw the actual canvas if ((canvasref = canvases.middle) && topreal < bottomreal && leftreal < rightreal) { if (sprite.middleStretch) { context.globalAlpha = opacity; context.drawImage(canvasref.canvas, leftreal, topreal, widthreal, heightreal); context.globalAlpha = 1; } else { this.drawPatternOnContext(context, canvasref.canvas, leftreal, topreal, widthreal, heightreal, opacity); } } } /* Position utilities (which should almost always be optimized) */ /** * @param {Thing} thing * @return {Number} The Thing's top position, accounting for vertical * offset if needed. */ private getTop(thing: IThing): number { if (this.keyOffsetY) { return thing[this.keyTop] + thing[this.keyOffsetY]; } else { return thing[this.keyTop]; } } /** * @param {Thing} thing * @return {Number} The Thing's right position, accounting for horizontal * offset if needed. */ private getRight(thing: IThing): number { if (this.keyOffsetX) { return thing[this.keyRight] + thing[this.keyOffsetX]; } else { return thing[this.keyRight]; } } /** * @param {Thing} thing * @return {Number} The Thing's bottom position, accounting for vertical * offset if needed. */ private getBottom(thing: IThing): number { if (this.keyOffsetX) { return thing[this.keyBottom] + thing[this.keyOffsetY]; } else { return thing[this.keyBottom]; } } /** * @param {Thing} thing * @return {Number} The Thing's left position, accounting for horizontal * offset if needed. */ private getLeft(thing: IThing): number { if (this.keyOffsetX) { return thing[this.keyLeft] + thing[this.keyOffsetX]; } else { return thing[this.keyLeft]; } } /* Utilities */ /** * Draws a source pattern onto a context. The pattern is clipped to the size * of MapScreener. * * @param {CanvasRenderingContext2D} context The context the pattern will * be drawn onto. * @param {Mixed} source The image being repeated as a pattern. This can * be a canvas, an image, or similar. * @param {Number} left The x-location to draw from. * @param {Number} top The y-location to draw from. * @param {Number} width How many pixels wide the drawing area should be. * @param {Number} left How many pixels high the drawing area should be. * @param {Number} opacity How transparent the drawing is, in [0,1]. */ private drawPatternOnContext( context: CanvasRenderingContext2D, source: any, left: number, top: number, width: number, height: number, opacity: number): void { context.globalAlpha = opacity; context.translate(left, top); context.fillStyle = context.createPattern(source, "repeat"); context.fillRect( 0, 0, // Math.max(width, left - MapScreener[keyRight]), // Math.max(height, top - MapScreener[keyBottom]) width, height); context.translate(-left, -top); context.globalAlpha = 1; } } }