@moderrkowo/jsgl
Version:
Client-side JavaScript library for creating web 2D games. Focusing at objective game.
508 lines (482 loc) • 15.5 kB
text/typescript
import { Game } from '../Game';
import { Sprite } from '../gameobjects/Sprite';
import { Vector2 } from '../structs/Vector2';
import { DrawSettings, defaultDrawSettings } from '../structs/DrawSettings';
import { ClickableGameObject } from '../gameobjects/ClickableGameObject';
import { Clamp01 } from '../utils/math/MathUtils';
import { RotationStyle } from '../enums/RotationStyle';
import { Rotation } from '../structs/Rotation';
/**
* Class represents Game Renderer.
* @group Important Classes
*/
export class Renderer {
/**
* Renderer owner.
*/
private readonly handler: Game;
/**
* How many pixels have one grid unit.
*/
public gridSize = 0;
/**
* The decimal midpoint of parent element size.
*/
public canvasParentSize = 1;
/**
* Constructs new Renderer.
* @param handler The {@link Game} reference
*/
public constructor(handler: Game) {
this.handler = handler;
}
/**
* Resizes canvas to parent element size by canvasParentSize (decimal midpoint).
* @method
* @example
* renderer.resizeCanvas();
*/
public resizeCanvas() {
if (!(this.handler.canvas instanceof HTMLCanvasElement))
throw new Error('Cannot resize undefined canvas!');
const canvas = this.handler.canvas;
const parent = canvas.parentElement;
if (parent == null) throw new Error('Parent cannot be null!');
const maxWidth = parent.clientWidth;
const maxHeight = parent.clientHeight;
const width = (maxWidth * this.canvasParentSize) / this.handler.grid.x;
const height = (maxHeight * this.canvasParentSize) / this.handler.grid.y;
this.gridSize = Math.floor(Math.min(width, height));
this.canvasWidth = this.handler.grid.x * this.gridSize;
this.canvasHeight = this.handler.grid.y * this.gridSize;
}
/**
* Gets canvas width in pixels.
*/
public get canvasWidth(): number {
return this.handler.canvas.width;
}
/**
* Sets canvas width in pixels.
* @param width The new width
*/
public set canvasWidth(width: number) {
this.handler.canvas.width = width;
}
/**
* Gets canvas height in pixels.
*/
public get canvasHeight(): number {
return this.handler.canvas.height;
}
/**
* Sets canvas height in pixels.
* @param height The new height
*/
public set canvasHeight(height: number) {
this.handler.canvas.height = height;
}
/**
* Gets the {@link CanvasRenderingContext2D}.
*/
public get ctx(): CanvasRenderingContext2D {
const context = this.handler.canvas.getContext('2d');
if (context == null)
throw new Error('There was a problem during getting the context');
return context;
}
/**
* Gets the Vector2 of width and height in pixels.
*/
public get canvasSize(): Vector2 {
return new Vector2(this.canvasWidth, this.canvasHeight);
}
/**
* Scales number from client coordinate to grid coordinate.
* @method
* @param a - The client coordinate
* @returns The scaled number
* @example
* const scaledToGrid = renderer.scale(5);
*/
public scale(a: number): number {
return a * this.gridSize;
}
/**
* Combines given draw settings with default draw settings.
* @method
* @param drawSettings - The given draw settings
* @returns The combined draw settings
* @example
* const drawSettings = renderer.combineDrawSettings({ color: 'red' });
*/
private combineDrawSettings(
drawSettings: DrawSettings | undefined
): DrawSettings {
return { ...defaultDrawSettings, ...drawSettings };
}
/**
* Sets canvas context properties to given settings.
* @param drawSettings - The draw settings
*/
private setContextSettings(drawSettings: DrawSettings) {
if (drawSettings.color !== undefined)
this.ctx.fillStyle = drawSettings.color;
if (drawSettings.borderColor !== undefined)
this.ctx.strokeStyle = drawSettings.borderColor;
if (drawSettings.borderSize !== undefined)
this.ctx.lineWidth = this.scale(drawSettings.borderSize / 64);
if (drawSettings.alpha !== undefined)
this.ctx.globalAlpha = Clamp01(drawSettings.alpha);
if (drawSettings.shadow?.color !== undefined)
this.ctx.shadowColor = drawSettings.shadow.color;
if (drawSettings.shadow?.offsetX !== undefined)
this.ctx.shadowOffsetX = this.scale(drawSettings.shadow.offsetX);
if (drawSettings.shadow?.offsetY !== undefined)
this.ctx.shadowOffsetY = this.scale(drawSettings.shadow.offsetY);
if (drawSettings.shadow?.blur !== undefined)
this.ctx.shadowBlur = this.scale(drawSettings.shadow.blur);
}
/**
* Draws a rect.
* @method
* @param worldX - The X-coordinate
* @param worldY - The Y-coordinate
* @param worldWidth - The width
* @param worldHeight - The height
* @param drawSettings - The draw settings
* @example
* renderer.drawRectangle(5, 2, 1, 1, { color: 'green', border: true });
*/
drawRectangle(
worldX: number,
worldY: number,
worldWidth: number,
worldHeight: number,
drawSettings?: DrawSettings
) {
drawSettings = this.combineDrawSettings(drawSettings);
const dx = this.scale(worldX - this.handler.canvasViewOffset.x);
const dy = this.scale(worldY - this.handler.canvasViewOffset.y);
const dw = this.scale(worldWidth);
const dh = this.scale(worldHeight);
this.ctx.save();
this.ctx.translate(dx + dw / 2, dy + dh / 2);
if (drawSettings.angle !== undefined) {
if (drawSettings.rotationStyle === RotationStyle.allAround) {
this.ctx.rotate(Rotation.ToRadians(drawSettings.angle));
}
}
this.ctx.translate(-dx - dw / 2, -dy - dh / 2);
this.setContextSettings(drawSettings);
//
if (drawSettings.fill) this.ctx.fillRect(dx, dy, dw, dh);
if (drawSettings.border) this.ctx.strokeRect(dx, dy, dw, dh);
//
this.ctx.restore();
}
/**
* Draws a circle.
* @method
* @param worldX - The X-coordinate
* @param worldY - The Y-coordinate
* @param diameter - The diameter
* @param drawSettings - The draw settings
* @example
* renderer.drawCircle(0, 0, 1, { color: 'yellow' });
*/
drawCircle(
worldX: number,
worldY: number,
diameter: number,
drawSettings?: DrawSettings
) {
drawSettings = this.combineDrawSettings(drawSettings);
const dx = this.scale(worldX - this.handler.canvasViewOffset.x);
const dy = this.scale(worldY - this.handler.canvasViewOffset.y);
const dr = this.scale(diameter / 2);
this.setContextSettings(drawSettings);
//
this.ctx.beginPath();
this.ctx.arc(dx + dr, dy + dr, dr, 0, 2 * Math.PI);
this.ctx.closePath();
if (drawSettings.fill) this.ctx.fill();
if (drawSettings.border) this.ctx.stroke();
}
/**
* Draws line from position to another position.
* @method
* @param fromWorldX - From X-coordinate
* @param fromWorldY - From Y-coordinate
* @param toWorldX - To X-coordinate
* @param toWorldY - To Y-coordinate
* @param drawSettings - The draw settings
* @example
* renderer.drawLine(2, 3, 3, 4);
*/
drawLine(
fromWorldX: number,
fromWorldY: number,
toWorldX: number,
toWorldY: number,
drawSettings?: DrawSettings
) {
drawSettings = this.combineDrawSettings(drawSettings);
this.setContextSettings(drawSettings);
this.ctx.beginPath();
this.ctx.moveTo(
this.scale(fromWorldX - this.handler.canvasViewOffset.x),
this.scale(fromWorldY - this.handler.canvasViewOffset.y)
);
this.ctx.lineTo(
this.scale(toWorldX - this.handler.canvasViewOffset.x),
this.scale(toWorldY - this.handler.canvasViewOffset.y)
);
this.ctx.closePath();
this.ctx.stroke();
}
/**
* Draws arrow from position to another position.
* @method
* @param fromWorldX - From X-coordinate
* @param fromWorldY - From Y-coordinate
* @param toWorldX - To X-coordinate
* @param toWorldY - To Y-coordinate
* @param drawSettings - The draw settings
* @example
* renderer.drawArrow(2, 3, 3, 4);
*/
drawArrow(
fromWorldX: number,
fromWorldY: number,
toWorldX: number,
toWorldY: number,
drawSettings?: DrawSettings
) {
drawSettings = this.combineDrawSettings(drawSettings);
// Scaling
const size = this.gridSize / 16;
this.ctx.lineWidth = this.gridSize / 64;
this.setContextSettings(drawSettings);
const p1 = new Vector2(
(fromWorldX - this.handler.canvasViewOffset.x) * this.gridSize,
(fromWorldY - this.handler.canvasViewOffset.y) * this.gridSize
);
const p2 = new Vector2(
(toWorldX - this.handler.canvasViewOffset.x) * this.gridSize,
(toWorldY - this.handler.canvasViewOffset.y) * this.gridSize
);
// Angle
const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
const hyp = Math.sqrt(
(p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
);
// Rotate
this.ctx.save();
this.ctx.translate(p1.x, p1.y);
this.ctx.rotate(angle);
// Line
this.ctx.beginPath();
this.ctx.moveTo(0, 0);
this.ctx.lineTo(hyp - size, 0);
this.ctx.closePath();
this.ctx.stroke();
// Triangle
this.ctx.beginPath();
this.ctx.lineTo(hyp - size, size);
this.ctx.lineTo(hyp, 0);
this.ctx.lineTo(hyp - size, -size);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
}
/**
* Clears the canvas.
* @method
* @example
* renderer.clearFrame();
*/
clearFrame() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
}
/**
* Fills the canvas with color property from settings.
* @param drawSettings - The settings
* @deprecated
*/
fillFrame(drawSettings: DrawSettings) {
drawSettings = this.combineDrawSettings(drawSettings);
this.setContextSettings(drawSettings);
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
}
/**
* Fills canvas with image.
* @param image - The image
* @deprecated
*/
fillImageFrame(image: HTMLCanvasElement) {
this.ctx.drawImage(image, 0, 0, this.canvasWidth, this.canvasHeight);
}
/**
* Fills canvas with content.
* @method
* @param content - The color, image or {@link DrawSettings}
* @example
* renderer.fill('white');
* renderer.fill({ color: 'white' })
* renderer.fill(game.GetImage('background'));
*/
fill(content: string | DrawSettings | HTMLCanvasElement) {
if (content instanceof HTMLCanvasElement) {
this.setContextSettings(defaultDrawSettings);
this.ctx.drawImage(content, 0, 0, this.canvasWidth, this.canvasHeight);
return;
} else if (typeof content === 'string') {
this.setContextSettings(this.combineDrawSettings({ color: content }));
} else {
this.setContextSettings(this.combineDrawSettings(content));
}
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
}
/**
* Draws image.
* @method
* @param image - The image
* @param worldX - The X-coordinate
* @param worldY - The Y-coordinate
* @param worldWidth - The width
* @param worldHeight - The height
* @param drawSettings - The draw settings
* @example
* renderer.drawImage(game.GetImage('player'), 0, 0, 1, 1, { angle: 45 });
*/
drawImage(
image: HTMLImageElement,
worldX: number,
worldY: number,
worldWidth: number,
worldHeight: number,
drawSettings?: DrawSettings
) {
drawSettings = this.combineDrawSettings(drawSettings);
const dx = this.scale(worldX - this.handler.canvasViewOffset.x);
const dy = this.scale(worldY - this.handler.canvasViewOffset.y);
const dw = this.scale(worldWidth);
const dh = this.scale(worldHeight);
this.ctx.save();
this.ctx.translate(dx + dw / 2, dy + dh / 2);
if (drawSettings.angle !== undefined) {
if (drawSettings.rotationStyle === RotationStyle.allAround) {
this.ctx.rotate(Rotation.ToRadians(drawSettings.angle));
}
}
this.ctx.translate(-dx - dw / 2, -dy - dh / 2);
this.ctx.drawImage(image, dx, dy, dw, dh);
this.ctx.restore();
}
/**
* Draws sprite texture.
* @method
* @param sprite - The sprite
* @returns is success?
* @example
* const exampleSprite = ...;
* renderer.drawSprite(exampleSprite);
*/
drawSprite(sprite: Sprite): boolean {
if (sprite.texture === undefined || sprite.texture === null) {
console.warn('The Sprite ', sprite, ' has not assigned texture!');
return false;
}
if (!(sprite.texture instanceof HTMLImageElement)) {
console.warn(
`Texture ${sprite.name}[${
sprite.id
}] cannot be ${typeof sprite.texture}!`
);
return false;
}
this.drawImage(
sprite.texture,
sprite.transform.position.x,
sprite.transform.position.y,
sprite.transform.scale.x,
sprite.transform.scale.y,
{
angle: sprite.transform.eulerAngles,
rotationStyle: sprite.rotationStyle,
}
);
return true;
}
/**
* Draws hitbox with direction arrow.
* @method
* @param clickableObject - The clickable game object
* @example
* const exampleClickableObject = ...;
* renderer.drawHitbox(exampleClickableObject);
*/
drawHitbox(clickableObject: ClickableGameObject) {
if (!clickableObject.showHitbox) return;
const pos = clickableObject.transform.position;
const center = clickableObject.transform.positionCenter;
const scale = clickableObject.transform.scale;
this.drawRectangle(pos.x, pos.y, scale.x, scale.y, {
borderColor: 'red',
fill: false,
border: true,
});
this.drawLine(pos.x, pos.y, pos.x + scale.x, pos.y + scale.y, {
borderColor: 'red',
});
this.drawLine(pos.x + scale.x, pos.y, pos.x, pos.y + scale.y, {
borderColor: 'red',
});
const angle = Rotation.ToRadians(clickableObject.transform.eulerAngles);
this.drawArrow(
center.x,
center.y,
center.x + (scale.x / 2) * Math.cos(angle),
center.y + (scale.y / 2) * Math.sin(angle),
{ color: 'red', borderColor: 'red' }
);
}
// /**
// * Draws text.
// * @method
// * @param content - Text content
// * @param worldX - World X-coordinate
// * @param worldY - World Y-coordinate
// * @param font - Font name
// * @param fontSize - Font size (grid)
// * @since v1.0.8
// */
// public drawText(
// content: string,
// worldX: number,
// worldY: number,
// drawSettings?: DrawSettings,
// font = 'serif',
// fontSize = 1){
// drawSettings = this.combineDrawSettings(drawSettings);
// const dx = this.scale(worldX - this.handler.canvasViewOffset.x);
// const dy = this.scale(worldY - this.handler.canvasViewOffset.y);
// const dh = this.scale(fontSize);
// this.ctx.font = `${dh}px ${font}`;
// const dw = this.ctx.measureText(content).width;
// this.ctx.save();
// this.ctx.translate(dx + dw / 2, dy + dh / 2);
// if (drawSettings.angle !== undefined){
// if (drawSettings.rotationStyle === RotationStyle.allAround){
// this.ctx.rotate(Rotation.ToRadians(drawSettings.angle));
// }
// }
// this.ctx.translate(- dx - dw / 2, - dy - dh / 2);
// if (drawSettings.fill)
// this.ctx.fillText(content, dx, dy, dw);
// if (drawSettings.border)
// this.ctx.strokeText(content, dx, dy, dw);
// this.ctx.restore();
// }
}