UNPKG

@moderrkowo/jsgl

Version:

Client-side JavaScript library for creating web 2D games. Focusing at objective game.

508 lines (482 loc) 15.5 kB
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(); // } }