import get from 'lodash/get';
import standardPalette from './standardPalette';
/**
* Represents a game screen for low resolution games.
* Has simple drawing functions using an indexed palette of a maximum of 256 colors
* The origin (0, 0) position of the screen is on the bottom left
*/
class Screen {
constructor() {
/**
* The id of the container div to make the Screen a child of
*/
this.conainerId = 'bitmelo-container';
/**
* The dom object the Screen will be a child of.
*/
this.container = null;
/**
* How many pixels wide is the screen?
*/
this.width = 320;
/**
* How many pixels tall is the screen?
*/
this.height = 180;
/**
* The scale of the pixels in the screen.
*/
this.scale = 3;
/**
* Maximum scale of the screen.
*/
this.maxScale = -1;
/**
* Minimum scale of the screen.
*/
this.minScale = 1;
/**
* The scale mode of the screen.
* Screen.SCALE_CONSTANT: 1,
* Screen.SCALE_FIT_WINDOW: 2
*/
this.scaleMode = Screen.SCALE_CONSTANT;
/**
* How many horizontal pixels to ignore when using a dynamic scale.
*/
this.horizontalScaleCushion = 0;
/**
* How many vertical pixels to ignore when using a dynamic scale.
*/
this.verticalScaleCushion = 0;
/**
* When using dynamic scaling, should we rescale when the window is resized?
*/
this.rescaleOnWindowResize = true;
/**
* Should the cursor be hidden when placed over the screen?
*/
this.hideCursor = false;
/**
* Reference to an instance of TileData used by the screen.
*/
this.tileData = null;
/**
* Reference to an instance of MapData used by the screen.
*/
this.mapData = null;
/**
* Reference to an instance of FontData used by the screen.
*/
this.fontData = null;
/**
* Callback that is called whenever the scale is changed.
* Used by the Engine to change values in the Input class.
*/
this.onScaleChange = null;
/**
* The DOM canvas used by this screen.
*/
this.canvas = null;
/**
* This mode runs the engine without drawing to a canvas or playing audio.
* This is useful to use the engine to generate image data.
*/
this.dataOnlyMode = false;
/**
* The canvas context used by this screen.
*/
this._context = null;
/**
* The image data of the context.
*/
this._imageData = null;
/**
* The pixel data drawn to using Screen methods such as setPixel or drawLine
*/
this._screenData = null;
/**
* The palette data given by the user
*/
this._palette = null;
/**
* Typed Array of paletted data generated from _palette and used by the Screen.
*/
this._generatedPalette = null;
/**
* Does this computer use little endian formatting?
*/
this._isLittleEndian = true;
}
/**
* Do initial setup such as creating the canvas and building the palette
*/
init() {
if ( this.dataOnlyMode ) {
this._screenData = new Uint8ClampedArray( this.width * this.height );
if ( !this._palette ) {
this._palette = standardPalette;
}
return;
}
this.container = document.getElementById( this.conainerId );
this.canvas = document.createElement( 'canvas' );
this.canvas.setAttribute( 'id', 'game-device' );
this.canvas.setAttribute( 'width', this.width );
this.canvas.setAttribute( 'height', this.height );
this._setScale();
if ( this.rescaleOnWindowResize && this.scaleMode !== Screen.SCALE_CONSTANT ) {
window.onresize = () => {
this._setScale();
this._setCanvasStyle();
};
}
this._setCanvasStyle();
this.container.appendChild( this.canvas );
this._context = this.canvas.getContext( '2d', { alpha: false } );
this._context.imageSmoothingEnabled = false;
this._screenData = new Uint8ClampedArray( this.width * this.height );
this._imageData = this._context.getImageData( 0, 0, this.width, this.height );
// check if we are little endian
const buffer = new ArrayBuffer( 4 );
const test8 = new Uint8ClampedArray( buffer );
const test32 = new Uint32Array( buffer );
test32[0] = 0x0a0b0c0d;
if ( test8[0] === 0x0a
&& test8[1] === 0x0b
&& test8[2] === 0x0c
&& test8[3] === 0x0d
) {
this._isLittleEndian = false;
}
if ( !this._palette ) {
this._palette = standardPalette;
}
this._buildPalette();
}
/**
* Sets the scale of the Screen.
*/
_setScale() {
if ( this.dataOnlyMode ) {
return;
}
if ( this.scaleMode === Screen.SCALE_FIT_WINDOW ) {
const maxWidth = window.innerWidth - this.horizontalScaleCushion;
const maxHeight = window.innerHeight - this.verticalScaleCushion;
const maxHorizScale = Math.floor( maxWidth / this.width );
const maxVerticalScale = Math.floor( maxHeight / this.height );
this.scale = maxHorizScale < maxVerticalScale ? maxHorizScale : maxVerticalScale;
if ( this.scale < this.minScale ) {
this.scale = this.minScale;
}
if ( this.maxScale > 0 && this.scale > this.maxScale ) {
this.scale = this.maxScale;
}
}
if ( this.onScaleChange ) {
this.onScaleChange( this.scale );
}
}
/**
* Sets css styling on the container dom object.
*/
_setCanvasStyle() {
if ( this.dataOnlyMode ) {
return;
}
let containerStyle = '';
containerStyle += `width: ${ this.width * this.scale }px;`;
containerStyle += `height: ${ this.height * this.scale }px;`;
this.container.setAttribute( 'style', containerStyle );
let canvasStyle = '';
canvasStyle += 'transform-origin: 50% 0%;';
canvasStyle += `transform: scale(${ this.scale });`;
canvasStyle += 'image-rendering: -webkit-optimize-contrast;';
canvasStyle += 'image-rendering: -moz-crisp-edges;';
canvasStyle += 'image-rendering: crisp-edges;';
canvasStyle += 'image-rendering: pixelated;';
if ( this.hideCursor ) {
canvasStyle += 'cursor: none';
}
this.canvas.setAttribute( 'style', canvasStyle );
}
/**
* Set the palette that will used by the Screen.
* All colors are drawn fully opaque exept for the palette index at 0 which is transparent
*
* @example
* const palette = [
* '000000', // black, the 0 index is transparent
* '000000', // black
* 'ffffff', // white
* 'ff0000', // red
* '00ff00', // green
* '0000ff' // blue
* ];
*
* screen.setPalette( palette );
*
* @param {Array} palette - The array of colors to be used by the screen.
* Each index should be a color described by an array of 3 integers in rgb order.
* Each integer has a min value of 0 and a max value of 255.
*/
setPalette( palette ) {
this._palette = palette;
this._buildPalette();
}
/**
* Change a single palette color
*
* @param {number[]} color - The color we want to add
* @param {number} index - this palette index we want to set
*/
setPaletteColorAtIndex( color, index ) {
this._palette[index] = color;
this._buildPalette();
}
/**
* Build the palette by converting _palette to the _generatedPalette we will be using internally
*/
_buildPalette() {
this._generatedPalette = new Uint32Array( this._palette.length );
let currentColor = null;
if ( this._isLittleEndian ) {
for ( let i = 0; i < this._palette.length; i += 1 ) {
currentColor = this._palette[i];
let r = 0;
let g = 0;
let b = 0;
if ( typeof currentColor === 'string' ) {
r = Number.parseInt( currentColor.slice( 0, 2 ), 16 );
g = Number.parseInt( currentColor.slice( 2, 4 ), 16 );
b = Number.parseInt( currentColor.slice( 4, 6 ), 16 );
}
else {
r = currentColor[0];
g = currentColor[1];
b = currentColor[2];
}
this._generatedPalette[i] = (
( 255 << 24 ) // a
| ( b << 16 ) // b
| ( g << 8 ) // g
| r // r
);
}
}
else {
for ( let i = 0; i < this._palette.length; i += 1 ) {
currentColor = this._palette[i];
let r = 0;
let g = 0;
let b = 0;
if ( typeof currentColor === 'string' ) {
r = Number.parseInt( currentColor.slice( 0, 2 ), 16 );
g = Number.parseInt( currentColor.slice( 2, 4 ), 16 );
b = Number.parseInt( currentColor.slice( 4, 6 ), 16 );
}
else {
r = currentColor[0];
g = currentColor[1];
b = currentColor[2];
}
this._generatedPalette[i] = (
( r << 24 ) // r
| ( g << 16 ) // g
| ( b << 8 ) // b
| 255 // a
);
}
}
}
/**
* Set a pixel on the screen.
* The origin (0, 0) of the screen is on the bottom left
* @param {number} x - x position
* @param {number} y - y position
* @param {number} paletteId - palette color index
*/
setPixel( x, y, paletteId ) {
if ( !paletteId ) {
return;
}
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
if ( x < 0 || x >= this.width || y < 0 || y >= this.height ) {
return;
}
this._screenData[y * this.width + x] = paletteId;
}
/**
* Set a pixel on the screen, without doing any bounds checking
* @param {number} x - x position
* @param {number} y - y position
* @param {number} paletteId - palette color index
*/
setPixelUnsafe( x, y, paletteId ) {
if ( !paletteId ) {
return;
}
this._screenData[y * this.width + x] = paletteId;
}
/**
* Get the pallete index at a given position on the screen
* @param {*} x - x position
* @param {*} y - y position
*/
getPixel( x, y ) {
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
return this._screenData[y * this.width + x];
}
/**
* Fill the screen with the given palette index
* @param {*} paletteId - the palette index / defaults to 0 if unspecified
*/
clear( paletteId ) {
if ( paletteId ) {
this._screenData.fill( paletteId );
}
else {
this._screenData.fill( 0 );
}
}
/**
* Draw a line between 2 positions
* @param {*} x1 - first x position
* @param {*} y1 - first y position
* @param {*} x2 - second x position
* @param {*} y2 - second y position
* @param {*} paletteId - palette index to be drawn
*/
drawLine( x1, y1, x2, y2, paletteId ) {
// eslint-disable-next-line no-param-reassign
x1 = Math.floor( x1 );
// eslint-disable-next-line no-param-reassign
y1 = Math.floor( y1 );
// eslint-disable-next-line no-param-reassign
x2 = Math.floor( x2 );
// eslint-disable-next-line no-param-reassign
y2 = Math.floor( y2 );
if ( x1 === x2 && y1 === y2 ) {
// same coordinate, draw a pixel
this.setPixel( x1, x2, paletteId );
return;
}
if ( x1 === x2 ) {
// vertical line
if ( x1 < 0 || x1 >= this.width ) {
return;
}
let firstY = y1 < y2 ? y1 : y2;
let secondY = y1 < y2 ? y2 : y1;
if ( secondY < 0 ) {
return;
}
if ( firstY >= this.height ) {
return;
}
if ( firstY < 0 ) {
firstY = 0;
}
if ( secondY >= this.height ) {
secondY = this.height - 1;
}
for ( let currentY = firstY; currentY <= secondY; currentY += 1 ) {
this.setPixelUnsafe( x1, currentY, paletteId );
}
return;
}
if ( y1 === y2 ) {
// horizontal line
if ( y1 < 0 || y1 >= this.height ) {
return;
}
let firstX = x1 < x2 ? x1 : x2;
let secondX = x1 < x2 ? x2 : x1;
if ( secondX < 0 ) {
return;
}
if ( firstX >= this.width ) {
return;
}
if ( firstX < 0 ) {
firstX = 0;
}
if ( secondX >= this.width ) {
secondX = this.width - 1;
}
for ( let currentX = firstX; currentX <= secondX; currentX += 1 ) {
this.setPixelUnsafe( currentX, y1, paletteId );
}
return;
}
if ( Math.abs( y2 - y1 ) < Math.abs( x2 - x1 ) ) {
// slope is less than 1
if ( x1 > x2 ) {
this._drawLineLow( x2, y2, x1, y1, paletteId );
}
else {
this._drawLineLow( x1, y1, x2, y2, paletteId );
}
}
else {
// slope is greater than 1
if ( y1 > y2 ) {
this._drawLineHigh( x2, y2, x1, y1, paletteId );
}
else {
this._drawLineHigh( x1, y1, x2, y2, paletteId );
}
}
}
/**
* Draw a line with a slope less than or equal to 1
*/
_drawLineLow( x1, y1, x2, y2, id ) {
const dx = x2 - x1;
let dy = y2 - y1;
let yIncrement = 1;
if ( dy < 0 ) {
yIncrement = -1;
dy = -dy;
}
let decision = 2 * dy - dx;
let y = y1;
for ( let x = x1; x <= x2; x += 1 ) {
this.setPixel( x, y, id );
if ( decision > 0 ) {
y += yIncrement;
decision = decision - ( 2 * dx );
}
decision = decision + ( 2 * dy );
}
}
/**
* Draw a line with a slope greater than 1
*/
_drawLineHigh( x1, y1, x2, y2, id ) {
let dx = x2 - x1;
const dy = y2 - y1;
let xIncrement = 1;
if ( dx < 0 ) {
xIncrement = -1;
dx = -dx;
}
let decision = 2 * dx - dy;
let x = x1;
for ( let y = y1; y <= y2; y += 1 ) {
this.setPixel( x, y, id );
if ( decision > 0 ) {
x += xIncrement;
decision = decision - ( 2 * dy );
}
decision = decision + ( 2 * dx );
}
}
/**
* Draw a filled rectangle
*
* @param {*} x - bottom left x position
* @param {*} y - bottom left y position
* @param {*} width - width of the rectangle
* @param {*} height - height of the rectangle
* @param {*} paletteId - the palette color to draw
*/
drawRect( x, y, width, height, paletteId ) {
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
// eslint-disable-next-line no-param-reassign
width = Math.floor( width );
// eslint-disable-next-line no-param-reassign
height = Math.floor( height );
if ( x >= this.width ) {
return;
}
if ( y >= this.height ) {
return;
}
let x1 = x;
let y1 = y;
let x2 = x + width - 1;
let y2 = y + height - 1;
if ( x2 < 0 ) {
return;
}
if ( y2 < 0 ) {
return;
}
if ( x1 < 0 ) {
x1 = 0;
}
if ( y1 < 0 ) {
y1 = 0;
}
if ( x2 >= this.width ) {
x2 = this.width - 1;
}
if ( y2 >= this.height ) {
y2 = this.height - 1;
}
for ( let currentY = y1; currentY <= y2; currentY += 1 ) {
for ( let currentX = x1; currentX <= x2; currentX += 1 ) {
this.setPixelUnsafe( currentX, currentY, paletteId );
}
}
}
/**
* Draw a rectangle border
*
* @param {*} x - bottom left x position
* @param {*} y - bottom left y position
* @param {*} width - width of the rectangle
* @param {*} height - height of the rectangle
* @param {*} paletteId - the palette color to draw
*/
drawRectBorder( x, y, width, height, paletteId ) {
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
// eslint-disable-next-line no-param-reassign
width = Math.floor( width );
// eslint-disable-next-line no-param-reassign
height = Math.floor( height );
if ( x >= this.width ) {
return;
}
if ( y >= this.height ) {
return;
}
if ( width === 1 && height === 1 ) {
this.setPixel( x, y, paletteId );
return;
}
const x2 = x + width - 1;
const y2 = y + height - 1;
if ( x2 < 0 ) {
return;
}
if ( y2 < 0 ) {
return;
}
if ( width === 1 ) {
this.drawLine( x, y, x, y2, paletteId );
return;
}
if ( height === 1 ) {
this.drawLine( x, y, x2, y, paletteId );
return;
}
this.drawLine( x, y, x, y2, paletteId ); // left
this.drawLine( x, y2, x2, y2, paletteId ); // top
this.drawLine( x2, y2, x2, y, paletteId ); // right
this.drawLine( x2, y, x, y, paletteId ); // bottom
}
/**
* Draw a filled circle
*
* @param {number} centerX - the x coordinate of the center of the circle
* @param {numbe} centerY - the y coordinate of the center of the circle
* @param {number} radius - the radius of the circle
* @param {number} paletteId - the palette color to draw
*/
drawCircle( centerX, centerY, radius, paletteId ) {
if ( radius <= 0 ) {
return;
}
// eslint-disable-next-line no-param-reassign
centerX = Math.floor( centerX );
// eslint-disable-next-line no-param-reassign
centerY = Math.floor( centerY );
// eslint-disable-next-line no-param-reassign
radius = Math.floor( radius );
if ( radius === 1 ) {
this.drawCircleBorder( centerX, centerY, radius, paletteId );
this.setPixel( centerX, centerY, paletteId );
return;
}
let x = 0;
let y = radius;
this.drawLine( centerX - radius, centerY, centerX + radius, centerY, paletteId );
let decision = 3 - 2 * radius;
while ( y >= x ) {
x += 1;
if ( decision > 0 ) {
y -= 1;
decision = decision + 4 * ( x - y ) + 10;
}
else {
decision = decision + 4 * x + 6;
}
this._drawCircleFilledOctants( centerX, centerY, x, y, paletteId );
}
}
/**
* Draw a circle border
*
* @param {number} centerX - the x coordinate of the center of the circle
* @param {numbe} centerY - the y coordinate of the center of the circle
* @param {number} radius - the radius of the circle
* @param {number} paletteId - the palette color to draw
*/
drawCircleBorder( centerX, centerY, radius, paletteId ) {
if ( radius <= 0 ) {
return;
}
// eslint-disable-next-line no-param-reassign
centerX = Math.floor( centerX );
// eslint-disable-next-line no-param-reassign
centerY = Math.floor( centerY );
// eslint-disable-next-line no-param-reassign
radius = Math.floor( radius );
let x = 0;
let y = radius;
this._drawCircleBorderOctants( centerX, centerY, x, y, paletteId );
let decision = 3 - 2 * radius;
while ( y >= x ) {
x += 1;
if ( decision > 0 ) {
y -= 1;
decision = decision + 4 * ( x - y ) + 10;
}
else {
decision = decision + 4 * x + 6;
}
this._drawCircleBorderOctants( centerX, centerY, x, y, paletteId );
}
}
/**
* helper method for drawing filled circles
*/
_drawCircleFilledOctants( centerX, centerY, x, y, paletteId ) {
this.drawLine( centerX - x, centerY + y, centerX + x, centerY + y, paletteId );
this.drawLine( centerX - x, centerY - y, centerX + x, centerY - y, paletteId );
this.drawLine( centerX - y, centerY + x, centerX + y, centerY + x, paletteId );
this.drawLine( centerX - y, centerY - x, centerX + y, centerY - x, paletteId );
}
/**
* helper method for drawing circle borders
*/
_drawCircleBorderOctants( centerX, centerY, x, y, paletteId ) {
this.setPixel( centerX + x, centerY + y, paletteId );
this.setPixel( centerX - x, centerY + y, paletteId );
this.setPixel( centerX + x, centerY - y, paletteId );
this.setPixel( centerX - x, centerY - y, paletteId );
this.setPixel( centerX + y, centerY + x, paletteId );
this.setPixel( centerX - y, centerY + x, paletteId );
this.setPixel( centerX + y, centerY - x, paletteId );
this.setPixel( centerX - y, centerY - x, paletteId );
}
/**
* Draw a tile
* @param {number} gid - the gid of the tile
* @param {number} x - the x position on the screen
* @param {number} y - the y position on the screen
* @param {number} flip - should we flip the tile? 0: no, 1: x, 2: y, 3: xy
* @param {number} rotate - The number of degrees to rotate. Only 90 degree increments are supported.
*/
drawTile( gid, x, y, flip = 0, rotate = 0 ) {
if ( !gid ) {
return;
}
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
if ( x >= this.width ) {
return;
}
if ( y >= this.height ) {
return;
}
const { tileSize } = this.tileData;
if ( x + tileSize < 0 ) {
return;
}
if ( y + tileSize < 0 ) {
return;
}
const flipX = flip === 1 || flip === 3;
const flipY = flip === 2 || flip === 3;
let xIndex = 0;
let yIndex = 0;
// only rotate in 90 degree increments
let rotValue = 0;
if ( rotate === 90 || rotate === -270 ) {
rotValue = 1;
}
else if ( rotate === 180 || rotate === -180 ) {
rotValue = 2;
}
else if ( rotate === 270 || rotate === -90 ) {
rotValue = 3;
}
const basePosition = ( gid - 1 ) * tileSize * tileSize;
for ( let tileY = 0; tileY < tileSize; tileY += 1 ) {
for ( let tileX = 0; tileX < tileSize; tileX += 1 ) {
if ( flipX ) {
xIndex = tileSize - tileX - 1;
}
else {
xIndex = tileX;
}
if ( flipY ) {
yIndex = tileSize - tileY - 1;
}
else {
yIndex = tileY;
}
const paletteId = this.tileData.data[basePosition + yIndex * tileSize + xIndex];
if ( rotValue === 3 ) {
// 270
this.setPixel( x + tileSize - tileY - 1, y + tileX, paletteId );
}
else if ( rotValue === 2 ) {
// 180
this.setPixel( x + tileSize - tileX - 1, y + tileSize - tileY - 1, paletteId );
}
else if ( rotValue === 1 ) {
// 90
this.setPixel( x + tileY, y + tileSize - tileX - 1, paletteId );
}
else {
this.setPixel( x + tileX, y + tileY, paletteId );
}
}
}
}
/**
* Draw a section of a tile map
* @param {number} gid - the gid of bottom left tile in the section
* @param {number} width - the width in tiles of the section
* @param {number} height - the height in tiles of the section
* @param {number} screenX - the x position on the screen
* @param {number} screenY - the y position on the screen
* @param {number} flip - should we flip the tiles? 0: no, 1: x, 2: y, 3: xy
* @param {number} rotate - The number of degrees to rotate. Only 90 degree increments are supported.
*/
drawTileSection( gid, width, height, screenX, screenY, flip = 0, rotate = 0 ) {
const tileSectionData = this.tileData.getTileSectionData( gid, width, height );
if ( tileSectionData ) {
this.drawArray(
tileSectionData.data,
tileSectionData.width,
tileSectionData.height,
screenX,
screenY,
flip,
rotate,
);
}
}
/**
* Draw an array
* @param {array} arrayData - array of palette data
* @param {number} arrayWidth - the width of the array data
* @param {number} arrayHeight - the height of the array data
* @param {number} screenX - the x position on the screen
* @param {number} screenY - the y position on the screen
* @param {number} flip - should we flip the tiles? 0: no, 1: x, 2: y, 3: xy
* @param {number} rotate - The number of degrees to rotate. Only 90 degree increments are supported.
*/
drawArray( arrayData, arrayWidth, arrayHeight, screenX, screenY, flip = 0, rotate = 0 ) {
const flipX = flip === 1 || flip === 3;
const flipY = flip === 2 || flip === 3;
let rotValue = 0;
if ( rotate === 90 || rotate === -270 ) {
rotValue = 1;
}
else if ( rotate === 180 || rotate === -180 ) {
rotValue = 2;
}
else if ( rotate === 270 || rotate === -90 ) {
rotValue = 3;
}
let xIndex = 0;
let yIndex = 0;
for ( let y = 0; y < arrayHeight; y += 1 ) {
for ( let x = 0; x < arrayWidth; x += 1 ) {
if ( flipX ) {
xIndex = arrayWidth - x - 1;
}
else {
xIndex = x;
}
if ( flipY ) {
yIndex = arrayHeight - y - 1;
}
else {
yIndex = y;
}
const paletteId = arrayData[yIndex * arrayWidth + xIndex];
if ( rotValue === 3 ) {
// 270
this.setPixel( screenX + arrayHeight - y - 1, screenY + x, paletteId );
}
else if ( rotValue === 2 ) {
// 180
this.setPixel( screenX + arrayWidth - x - 1, screenY + arrayHeight - y - 1, paletteId );
}
else if ( rotValue === 1 ) {
// 90
this.setPixel( screenX + y, screenY + arrayWidth - x - 1, paletteId );
}
else {
this.setPixel( screenX + x, screenY + y, paletteId );
}
}
}
}
/**
* Draw a TileMap layer to the screen
* @param {number} x - origin x position on the TileMap
* @param {number} y - origin y position on the TileMap
* @param {number} width - how many tiles wide to draw, -1 is the width of the Tile Map
* @param {number} height - how many tiles high to draw, -1 is the height of the Tile Map
* @param {number} screenX - origin x position on the screen
* @param {number} screenY - origin y position on the screen
* @param {number} map - the index of the tilemap to draw
* @param {number} layer - the index of the layer to draw
* @param {number} onDrawTile - function to override individual tile values, (gid, x, y) => { gid, flip, rotate };
*/
drawMap( x, y, width = -1, height = -1, screenX = 0, screenY = 0, map = 0, layer = 0, onDrawTile = null ) {
// eslint-disable-next-line no-param-reassign
screenX = Math.floor( screenX );
// eslint-disable-next-line no-param-reassign
screenY = Math.floor( screenY );
const tileMap = this.mapData.tileMaps[map];
const layerData = tileMap.layers[layer];
const { tileSize } = this.tileData;
let maxX = x + width - 1;
let maxY = y + height - 1;
if ( maxX >= tileMap.width || width < 0 ) {
maxX = tileMap.width - 1;
}
if ( maxY >= tileMap.height || height < 0 ) {
maxY = tileMap.height - 1;
}
const offsetX = x * tileSize;
const offsetY = y * tileSize;
for ( let currentY = y; currentY <= maxY; currentY += 1 ) {
for ( let currentX = x; currentX <= maxX; currentX += 1 ) {
let gid = layerData[currentY * tileMap.width + currentX];
let flip = 0;
let rotate = 0;
if ( onDrawTile ) {
const onDrawResult = onDrawTile( gid, currentX, currentY );
gid = get( onDrawResult, 'gid', 0 );
flip = get( onDrawResult, 'flip', 0 );
rotate = get( onDrawResult, 'rotate', 0 );
}
if ( gid ) {
this.drawTile(
gid,
screenX + currentX * tileSize - offsetX,
screenY + currentY * tileSize - offsetY,
flip,
rotate,
);
}
}
}
}
/**
* Draw an array of Tile gids to the screen
* @param {array} arrayData - array of tile gid data
* @param {number} arrayWidth - the width of the array data
* @param {number} arrayHeight - the height of the array data
* @param {number} screenX - origin x position on the screen
* @param {number} screenY - origin y position on the screen
* @param {number} onDrawTile - function to override individual tile values, (gid, x, y) => { gid, flip, rotate };
*/
drawMapArray( arrayData, arrayWidth, arrayHeight, screenX = 0, screenY = 0, onDrawTile = null ) {
// eslint-disable-next-line no-param-reassign
screenX = Math.floor( screenX );
// eslint-disable-next-line no-param-reassign
screenY = Math.floor( screenY );
const { tileSize } = this.tileData;
for ( let currentY = 0; currentY < arrayHeight; currentY += 1 ) {
for ( let currentX = 0; currentX < arrayWidth; currentX += 1 ) {
let gid = arrayData[currentY * arrayWidth + currentX];
let flip = 0;
let rotate = 0;
if ( onDrawTile ) {
const onDrawResult = onDrawTile( gid, currentX, currentY );
gid = get( onDrawResult, 'gid', 0 );
flip = get( onDrawResult, 'flip', 0 );
rotate = get( onDrawResult, 'rotate', 0 );
}
if ( gid ) {
this.drawTile(
gid,
screenX + currentX * tileSize,
screenY + currentY * tileSize,
flip,
rotate,
);
}
}
}
}
/**
* Draw a line of text to the screen.
* Newlines are not supported, this will draw just a single line
* @param {string} text - the text to draw
* @param {number} x - the x position on the screen to draw to
* @param {number} y - the y position on the screen to draw to
* @param {number} paletteId - the palette if for the main color
* @param {number} outlinePaletteId - the palette id for the outline color
* @param {number} font - the index of the font to use
*/
drawText( text, x, y, paletteId, outlinePaletteId = 0, font = 0 ) {
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
const currentFont = this.fontData.fonts[font];
let currentX = x;
for ( let i = 0; i < text.length; i += 1 ) {
const charCode = text.charCodeAt( i );
this.drawChar( charCode, currentX, y, paletteId, outlinePaletteId, font );
currentX += currentFont.widthForChar( charCode );
currentX += currentFont.letterSpacing;
}
}
/**
* Draw an individual character to the screen
* @param {number} charCode - the unicode point to draw
* @param {number} x - the x position on the screen to draw to
* @param {number} y - the y position on the screen to draw to
* @param {number} paletteId - the palette id for the main color
* @param {number} outlinePaletteId - the palette id for the outline color
* @param {number} font - the index of the font to use
*/
drawChar( charCode, x, y, paletteId, outlinePaletteId = 0, font = 0 ) {
// eslint-disable-next-line no-param-reassign
x = Math.floor( x );
// eslint-disable-next-line no-param-reassign
y = Math.floor( y );
const currentFont = this.fontData.fonts[font];
const { tileSize, originX, originY } = currentFont;
const basePosition = currentFont.baseIndexForChar( charCode );
for ( let fontY = 0; fontY < tileSize; fontY += 1 ) {
for ( let fontX = 0; fontX < tileSize; fontX += 1 ) {
const id = currentFont.data[basePosition + fontY * tileSize + fontX];
if ( id === 1 ) {
this.setPixel( x + fontX - originX, y + fontY - originY, paletteId );
}
else if ( id === 2 ) {
this.setPixel( x + fontX - originX, y + fontY - originY, outlinePaletteId );
}
}
}
}
/**
* draw the data from {@link _screenData} to the canvas
*/
drawScreen() {
if ( this.dataOnlyMode ) {
return;
}
const buffer = new ArrayBuffer( this._imageData.data.length );
const data8 = new Uint8ClampedArray( buffer );
const data32 = new Uint32Array( buffer );
let index = 0;
let screenY = 0;
for ( let y = 0; y < this.height; y += 1 ) {
for ( let x = 0; x < this.width; x += 1 ) {
screenY = this.height - y - 1; // origin from top left to bottom left
index = this._screenData[screenY * this.width + x];
data32[y * this.width + x] = this._generatedPalette[index];
}
}
this._imageData.data.set( data8 );
this._context.putImageData( this._imageData, 0, 0 );
}
}
Screen.SCALE_CONSTANT = 1;
Screen.SCALE_FIT_WINDOW = 2;
export default Screen;