diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index cac769f..f753570 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -9,6 +9,8 @@ import SceneManager from "../Scene/SceneManager"; import AudioManager from "../Sound/AudioManager"; import Stats from "../Debug/Stats"; import ArrayUtils from "../Utils/ArrayUtils"; +import RenderingManager from "../Rendering/RenderingManager"; +import CanvasRenderer from "../Rendering/CanvasRenderer"; export default class GameLoop { gameOptions: GameOptions; @@ -70,6 +72,7 @@ export default class GameLoop { private resourceManager: ResourceManager; private sceneManager: SceneManager; private audioManager: AudioManager; + private renderingManager: RenderingManager; constructor(options?: Record){ // Typecast the config object to a GameConfig object @@ -100,7 +103,10 @@ export default class GameLoop { // Give the canvas a size and get the rendering context this.WIDTH = this.gameOptions.viewportSize.x; this.HEIGHT = this.gameOptions.viewportSize.y; - this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); + + // For now, just hard code a canvas renderer. We can do this with options later + this.renderingManager = new CanvasRenderer(); + this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); // Size the viewport to the game canvas this.viewport = new Viewport(); @@ -114,24 +120,12 @@ export default class GameLoop { this.inputReceiver.setViewport(this.viewport); this.recorder = new Recorder(); this.resourceManager = ResourceManager.getInstance(); - this.sceneManager = new SceneManager(this.viewport, this); + this.sceneManager = new SceneManager(this.viewport, this, this.renderingManager); this.audioManager = AudioManager.getInstance(); Stats.initStats(); } - private initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D { - canvas.width = width; - canvas.height = height; - let ctx = canvas.getContext("2d"); - - // For crisp pixel art - ctx.imageSmoothingEnabled = false; - - return ctx; - } - - // TODO - This currently also changes the rendering framerate /** * Changes the maximum allowed physics framerate of the game * @param initMax @@ -278,7 +272,7 @@ export default class GameLoop { */ render(): void { this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); - this.sceneManager.render(this.ctx); + this.sceneManager.render(); Debug.render(this.ctx); Stats.render(); } diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index ffb84e1..8708167 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -6,7 +6,7 @@ import AABB from "../DataTypes/Shapes/AABB"; /** * The representation of an object in the game world that can be drawn to the screen */ -export default abstract class CanvasNode extends GameNode implements Region, Renderable { +export default abstract class CanvasNode extends GameNode implements Region { private _size: Vec2; private _scale: Vec2; private _boundary: AABB; @@ -75,6 +75,4 @@ export default abstract class CanvasNode extends GameNode implements Region, Ren contains(x: number, y: number): boolean { return this._boundary.containsPoint(new Vec2(x, y)); } - - abstract render(ctx: CanvasRenderingContext2D): void; } \ No newline at end of file diff --git a/src/Nodes/Graphic.ts b/src/Nodes/Graphic.ts index f5902e1..6aa4cc2 100644 --- a/src/Nodes/Graphic.ts +++ b/src/Nodes/Graphic.ts @@ -6,7 +6,7 @@ import Color from "../Utils/Color"; */ export default abstract class Graphic extends CanvasNode { - protected color: Color; + color: Color; constructor(){ super(); diff --git a/src/Nodes/Graphics/Point.ts b/src/Nodes/Graphics/Point.ts index 3b30729..0aef392 100644 --- a/src/Nodes/Graphics/Point.ts +++ b/src/Nodes/Graphics/Point.ts @@ -10,14 +10,4 @@ export default class Point extends Graphic { } update(deltaT: number): void {} - - render(ctx: CanvasRenderingContext2D): void { - let origin = this.scene.getViewTranslation(this); - let zoom = this.scene.getViewScale(); - - ctx.fillStyle = this.color.toStringRGBA(); - ctx.fillRect((this.position.x - origin.x - this.size.x/2)*zoom, (this.position.y - origin.y - this.size.y/2)*zoom, - this.size.x*zoom, this.size.y*zoom); - } - } \ No newline at end of file diff --git a/src/Nodes/Graphics/Rect.ts b/src/Nodes/Graphics/Rect.ts index e0dbd0d..0c9130c 100644 --- a/src/Nodes/Graphics/Rect.ts +++ b/src/Nodes/Graphics/Rect.ts @@ -23,6 +23,10 @@ export default class Rect extends Graphic { this.borderColor = color; } + getBorderColor(): Color { + return this.borderColor; + } + /**Sets the border width of this rectangle * * @param width The width of the rectangle in pixels @@ -31,20 +35,9 @@ export default class Rect extends Graphic { this.borderWidth = width; } - update(deltaT: number): void {} - - render(ctx: CanvasRenderingContext2D): void { - let origin = this.scene.getViewTranslation(this); - let zoom = this.scene.getViewScale(); - - if(this.color.a !== 0){ - ctx.fillStyle = this.color.toStringRGB(); - ctx.fillRect((this.position.x - this.size.x/2 - origin.x)*zoom, (this.position.y - this.size.y/2 - origin.y)*zoom, this.size.x*zoom, this.size.y*zoom); - } - - ctx.strokeStyle = this.borderColor.toStringRGB(); - ctx.lineWidth = this.borderWidth; - ctx.strokeRect((this.position.x - this.size.x/2 - origin.x)*zoom, (this.position.y - this.size.y/2 - origin.y)*zoom, this.size.x*zoom, this.size.y*zoom); + getBorderWidth(): number { + return this.borderWidth; } + update(deltaT: number): void {} } \ No newline at end of file diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index dc285e0..f990070 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -6,8 +6,8 @@ import Vec2 from "../../DataTypes/Vec2"; * The representation of a sprite - an in-game image */ export default class Sprite extends CanvasNode { - private imageId: string; - private imageOffset: Vec2; + imageId: string; + imageOffset: Vec2; constructor(imageId: string){ super(); @@ -26,20 +26,4 @@ export default class Sprite extends CanvasNode { } update(deltaT: number): void {} - - render(ctx: CanvasRenderingContext2D): void { - let image = ResourceManager.getInstance().getImage(this.imageId); - let origin = this.scene.getViewTranslation(this); - let zoom = this.scene.getViewScale(); - - ctx.drawImage(image, - this.imageOffset.x, this.imageOffset.y, this.size.x, this.size.y, - (this.position.x - origin.x - this.size.x*this.scale.x/2)*zoom, (this.position.y - origin.y - this.size.y*this.scale.y/2)*zoom, - this.size.x * this.scale.x*zoom, this.size.y * this.scale.y*zoom); - - ctx.lineWidth = 4; - ctx.strokeStyle = "#00FF00" - let b = this.boundary; - ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2*zoom, b.hh*2*zoom); - } } \ No newline at end of file diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index e758b00..5a5fcbf 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -84,6 +84,4 @@ export default abstract class Tilemap extends CanvasNode { */ // TODO: This shouldn't use tiled data specifically - it should be more general protected abstract parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void; - - abstract render(ctx: CanvasRenderingContext2D): void; } \ No newline at end of file diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index a67d300..0a7f18d 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -128,27 +128,4 @@ export default class OrthogonalTilemap extends Tilemap { } update(deltaT: number): void {} - - // TODO: Don't render tiles that aren't on screen - render(ctx: CanvasRenderingContext2D) { - let previousAlpha = ctx.globalAlpha; - ctx.globalAlpha = this.getLayer().getAlpha(); - - let origin = this.scene.getViewTranslation(this); - let zoom = this.scene.getViewScale(); - - if(this.visible){ - for(let i = 0; i < this.data.length; i++){ - let tileIndex = this.data[i]; - - for(let tileset of this.tilesets){ - if(tileset.hasTile(tileIndex)){ - tileset.renderTile(ctx, tileIndex, i, this.numCols, origin, this.scale, zoom); - } - } - } - } - - ctx.globalAlpha = previousAlpha; - } } \ No newline at end of file diff --git a/src/Nodes/UIElement.ts b/src/Nodes/UIElement.ts index 476064c..28e3d4c 100644 --- a/src/Nodes/UIElement.ts +++ b/src/Nodes/UIElement.ts @@ -7,11 +7,11 @@ import Vec2 from "../DataTypes/Vec2"; */ export default abstract class UIElement extends CanvasNode { // Style attributes - protected backgroundColor: Color; - protected borderColor: Color; - protected borderRadius: number; - protected borderWidth: number; - protected padding: Vec2; + backgroundColor: Color; + borderColor: Color; + borderRadius: number; + borderWidth: number; + padding: Vec2; // EventAttributes onClick: Function; @@ -115,14 +115,14 @@ export default abstract class UIElement extends CanvasNode { /** * Overridable method for calculating background color - useful for elements that want to be colored on different after certain events */ - protected calculateBackgroundColor(): string { + calculateBackgroundColor(): string { return this.backgroundColor.toStringRGBA(); } /** * Overridable method for calculating border color - useful for elements that want to be colored on different after certain events */ - protected calculateBorderColor(): string { + calculateBorderColor(): string { return this.borderColor.toStringRGBA(); } } \ No newline at end of file diff --git a/src/Nodes/UIElements/Button.ts b/src/Nodes/UIElements/Button.ts index ac85831..4556773 100644 --- a/src/Nodes/UIElements/Button.ts +++ b/src/Nodes/UIElements/Button.ts @@ -12,7 +12,7 @@ export default class Button extends Label { this.textColor = new Color(255, 255, 255); } - protected calculateBackgroundColor(): string { + calculateBackgroundColor(): string { // Change the background color if clicked or hovered if(this.isEntered && !this.isClicked){ return this.backgroundColor.lighten().toStringRGBA(); diff --git a/src/Nodes/UIElements/Label.ts b/src/Nodes/UIElements/Label.ts index 82685ab..0071b7b 100644 --- a/src/Nodes/UIElements/Label.ts +++ b/src/Nodes/UIElements/Label.ts @@ -3,8 +3,8 @@ import Color from "../../Utils/Color"; import UIElement from "../UIElement"; export default class Label extends UIElement{ - protected textColor: Color; - protected text: string; + textColor: Color; + text: string; protected font: string; protected fontSize: number; protected hAlign: string; @@ -33,10 +33,14 @@ export default class Label extends UIElement{ this.textColor = color; } + getFontString(): string { + return this.fontSize + "px " + this.font; + } + /** * Overridable method for calculating text color - useful for elements that want to be colored on different after certain events */ - protected calculateTextColor(): string { + calculateTextColor(): string { return this.textColor.toStringRGBA(); } @@ -47,9 +51,8 @@ export default class Label extends UIElement{ /** * Calculate the offset of the text - this is useful for rendering text with different alignments - * */ - protected calculateTextOffset(ctx: CanvasRenderingContext2D): Vec2 { + calculateTextOffset(ctx: CanvasRenderingContext2D): Vec2 { let textWidth = this.calculateTextWidth(ctx); let offset = new Vec2(0, 0); @@ -80,49 +83,22 @@ export default class Label extends UIElement{ this.sizeAssigned = true; } - protected autoSize(ctx: CanvasRenderingContext2D){ + protected autoSize(ctx: CanvasRenderingContext2D): void { let width = this.calculateTextWidth(ctx); let height = this.fontSize; this.size.set(width + this.padding.x*2, height + this.padding.y*2); this.sizeAssigned = true; } - /** On the next render, size this element to it's current text using its current font size */ - sizeToText(): void { - this.sizeAssigned = false; - } - - render(ctx: CanvasRenderingContext2D): void { - // If the size is unassigned (by the user or automatically) assign it + handleInitialSizing(ctx: CanvasRenderingContext2D): void { if(!this.sizeAssigned){ this.autoSize(ctx); } - - // Grab the global alpha so we can adjust it for this render - let previousAlpha = ctx.globalAlpha; + } - let origin = this.scene.getViewTranslation(this); - - ctx.font = this.fontSize + "px " + this.font; - let offset = this.calculateTextOffset(ctx); - - // Stroke and fill a rounded rect and give it text - ctx.globalAlpha = this.backgroundColor.a; - ctx.fillStyle = this.calculateBackgroundColor(); - ctx.fillRoundedRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, - this.size.x, this.size.y, this.borderRadius); - - ctx.strokeStyle = this.calculateBorderColor(); - ctx.globalAlpha = this.borderColor.a; - ctx.lineWidth = this.borderWidth; - ctx.strokeRoundedRect(this.position.x - origin.x - this.size.x/2, this.position.y - origin.y - this.size.y/2, - this.size.x, this.size.y, this.borderRadius); - - ctx.fillStyle = this.calculateTextColor(); - ctx.globalAlpha = this.textColor.a; - ctx.fillText(this.text, this.position.x + offset.x - origin.x - this.size.x/2, this.position.y + offset.y - origin.y - this.size.y/2); - - ctx.globalAlpha = previousAlpha; + /** On the next render, size this element to it's current text using its current font size */ + sizeToText(): void { + this.sizeAssigned = false; } debug_render = (ctx: CanvasRenderingContext2D): void => { diff --git a/src/Nodes/UIElements/Slider.ts b/src/Nodes/UIElements/Slider.ts index ba981a7..f0150f3 100644 --- a/src/Nodes/UIElements/Slider.ts +++ b/src/Nodes/UIElements/Slider.ts @@ -6,8 +6,8 @@ import UIElement from "../UIElement"; export default class Slider extends UIElement { /** The value of the slider from [0, 1] */ protected value: number; - protected nibColor: Color; - protected sliderColor: Color; + public nibColor: Color; + public sliderColor: Color; public onValueChange: (value: number) => void; public onValueChangeEventId: string; @@ -24,6 +24,10 @@ export default class Slider extends UIElement { this.size.set(200, 20); } + getValue(): number { + return this.value; + } + protected valueChanged(): void { if(this.onValueChange){ this.onValueChange(this.value); @@ -43,34 +47,4 @@ export default class Slider extends UIElement { this.valueChanged(); } } - - render(ctx: CanvasRenderingContext2D): void { - // Grab the global alpha so we can adjust it for this render - let previousAlpha = ctx.globalAlpha; - ctx.globalAlpha = this.getLayer().getAlpha(); - - let origin = this.scene.getViewTranslation(this); - - // Calcualate the slider size - let sliderSize = new Vec2(this.size.x, 2); - - // Draw the slider - ctx.fillStyle = this.sliderColor.toString(); - ctx.fillRoundedRect(this.position.x - origin.x - sliderSize.x/2, this.position.y - origin.y - sliderSize.y/2, - sliderSize.x, sliderSize.y, this.borderRadius); - - // Calculate the nib size and position - let nibSize = new Vec2(10, this.size.y); - let x = MathUtils.lerp(this.position.x - this.size.x/2, this.position.x + this.size.x/2, this.value); - let nibPosition = new Vec2(x, this.position.y); - - // Draw the nib - ctx.fillStyle = this.nibColor.toString(); - ctx.fillRoundedRect(nibPosition.x - origin.x - nibSize.x/2, nibPosition.y - origin.y - nibSize.y/2, - nibSize.x, nibSize.y, this.borderRadius); - - - // Reset the alpha - ctx.globalAlpha = previousAlpha; - } } \ No newline at end of file diff --git a/src/Nodes/UIElements/TextInput.ts b/src/Nodes/UIElements/TextInput.ts index 4d0803c..198bfc7 100644 --- a/src/Nodes/UIElements/TextInput.ts +++ b/src/Nodes/UIElements/TextInput.ts @@ -58,24 +58,4 @@ export default class TextInput extends Label { } } } - - render(ctx: CanvasRenderingContext2D): void { - // Show a cursor sometimes - if(this.focused && this.cursorCounter % 60 > 30){ - this.text += "|"; - } - - super.render(ctx); - - if(this.focused){ - if(this.cursorCounter % 60 > 30){ - this.text = this.text.substring(0, this.text.length - 1); - } - - this.cursorCounter += 1; - if(this.cursorCounter >= 60){ - this.cursorCounter = 0; - } - } - } } \ No newline at end of file diff --git a/src/Rendering/CanvasRenderer.ts b/src/Rendering/CanvasRenderer.ts new file mode 100644 index 0000000..d44c08a --- /dev/null +++ b/src/Rendering/CanvasRenderer.ts @@ -0,0 +1,152 @@ +import Map from "../DataTypes/Map"; +import CanvasNode from "../Nodes/CanvasNode"; +import Graphic from "../Nodes/Graphic"; +import Point from "../Nodes/Graphics/Point"; +import Rect from "../Nodes/Graphics/Rect"; +import Sprite from "../Nodes/Sprites/Sprite"; +import Tilemap from "../Nodes/Tilemap"; +import OrthogonalTilemap from "../Nodes/Tilemaps/OrthogonalTilemap"; +import UIElement from "../Nodes/UIElement"; +import UILayer from "../Scene/Layers/UILayer"; +import Scene from "../Scene/Scene"; +import GraphicRenderer from "./CanvasRendering/GraphicRenderer"; +import RenderingManager from "./RenderingManager" +import TilemapRenderer from "./CanvasRendering/TilemapRenderer"; +import UIElementRenderer from "./CanvasRendering/UIElementRenderer"; +import Label from "../Nodes/UIElements/Label"; +import Button from "../Nodes/UIElements/Button"; +import Slider from "../Nodes/UIElements/Slider"; +import TextInput from "../Nodes/UIElements/TextInput"; + +export default class CanvasRenderer extends RenderingManager { + protected ctx: CanvasRenderingContext2D; + protected graphicRenderer: GraphicRenderer; + protected tilemapRenderer: TilemapRenderer; + protected uiElementRenderer: UIElementRenderer; + + constructor(){ + super();; + } + + setScene(scene: Scene){ + this.scene = scene; + this.graphicRenderer.setScene(scene); + this.tilemapRenderer.setScene(scene); + this.uiElementRenderer.setScene(scene); + } + + initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D { + canvas.width = width; + canvas.height = height; + + this.ctx = canvas.getContext("2d"); + + this.graphicRenderer = new GraphicRenderer(this.ctx); + this.tilemapRenderer = new TilemapRenderer(this.ctx); + this.uiElementRenderer = new UIElementRenderer(this.ctx) + + // For crisp pixel art + this.ctx.imageSmoothingEnabled = false; + + return this.ctx; + } + + render(visibleSet: CanvasNode[], tilemaps: Tilemap[], uiLayers: Map): void { + // Sort by depth, then by visible set by y-value + visibleSet.sort((a, b) => { + if(a.getLayer().getDepth() === b.getLayer().getDepth()){ + return (a.boundary.bottom) - (b.boundary.bottom); + } else { + return a.getLayer().getDepth() - b.getLayer().getDepth(); + } + }); + + // Render tilemaps + tilemaps.forEach(tilemap => { + this.renderTilemap(tilemap); + }); + + // Render visible set + visibleSet.forEach(node => { + if(node.visible){ + this.renderNode(node); + } + }); + + // Render the uiLayers + uiLayers.forEach(key => uiLayers.get(key).getItems().forEach(node => this.renderNode(node))); + } + + protected renderNode(node: CanvasNode): void { + if(node instanceof Sprite){ + this.renderSprite(node); + } else if(node instanceof Graphic){ + this.renderGraphic(node); + } else if(node instanceof UIElement){ + this.renderUIElement(node); + } + } + + protected renderSprite(sprite: Sprite): void { + // Get the image from the resource manager + let image = this.resourceManager.getImage(sprite.imageId); + + // Calculate the origin of the viewport according to this sprite + let origin = this.scene.getViewTranslation(sprite); + + // Get the zoom level of the scene + let zoom = this.scene.getViewScale(); + + /* + Coordinates in the space of the image: + image crop start -> x, y + image crop size -> w, h + Coordinates in the space of the world + image draw start -> x, y + image draw size -> w, h + */ + this.ctx.drawImage(image, + sprite.imageOffset.x, sprite.imageOffset.y, + sprite.size.x, sprite.size.y, + (sprite.position.x - origin.x - sprite.size.x*sprite.scale.x/2)*zoom, (sprite.position.y - origin.y - sprite.size.y*sprite.scale.y/2)*zoom, + sprite.size.x * sprite.scale.x*zoom, sprite.size.y * sprite.scale.y*zoom); + + // Debug mode + if(this.debug){ + this.ctx.lineWidth = 4; + this.ctx.strokeStyle = "#00FF00" + let b = sprite.boundary; + this.ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2*zoom, b.hh*2*zoom); + } + } + + protected renderAnimatedSprite(): void { + throw new Error("Method not implemented."); + } + + protected renderGraphic(graphic: Graphic): void { + if(graphic instanceof Point){ + this.graphicRenderer.renderPoint(graphic); + } else if(graphic instanceof Rect){ + this.graphicRenderer.renderRect(graphic); + } + } + + protected renderTilemap(tilemap: Tilemap): void { + if(tilemap instanceof OrthogonalTilemap){ + this.tilemapRenderer.renderOrthogonalTilemap(tilemap); + } + } + + protected renderUIElement(uiElement: UIElement): void { + if(uiElement instanceof Label){ + this.uiElementRenderer.renderLabel(uiElement); + } else if(uiElement instanceof Button){ + this.uiElementRenderer.renderButton(uiElement); + } else if(uiElement instanceof Slider){ + this.uiElementRenderer.renderSlider(uiElement); + } else if(uiElement instanceof TextInput){ + this.uiElementRenderer.renderTextInput(uiElement); + } + } +} \ No newline at end of file diff --git a/src/Rendering/CanvasRendering/GraphicRenderer.ts b/src/Rendering/CanvasRendering/GraphicRenderer.ts new file mode 100644 index 0000000..12adff0 --- /dev/null +++ b/src/Rendering/CanvasRendering/GraphicRenderer.ts @@ -0,0 +1,44 @@ +import Point from "../../Nodes/Graphics/Point"; +import Rect from "../../Nodes/Graphics/Rect"; +import ResourceManager from "../../ResourceManager/ResourceManager"; +import Scene from "../../Scene/Scene"; + +export default class GraphicRenderer { + protected resourceManager: ResourceManager; + protected scene: Scene; + protected ctx: CanvasRenderingContext2D; + + constructor(ctx: CanvasRenderingContext2D){ + this.resourceManager = ResourceManager.getInstance(); + this.ctx = ctx; + } + + setScene(scene: Scene): void { + this.scene = scene; + } + + renderPoint(point: Point): void { + let origin = this.scene.getViewTranslation(point); + let zoom = this.scene.getViewScale(); + + this.ctx.fillStyle = point.color.toStringRGBA(); + this.ctx.fillRect((point.position.x - origin.x - point.size.x/2)*zoom, (point.position.y - origin.y - point.size.y/2)*zoom, + point.size.x*zoom, point.size.y*zoom); + } + + renderRect(rect: Rect): void { + let origin = this.scene.getViewTranslation(rect); + let zoom = this.scene.getViewScale(); + + // Draw the interior of the rect + if(rect.color.a !== 0){ + this.ctx.fillStyle = rect.color.toStringRGB(); + this.ctx.fillRect((rect.position.x - rect.size.x/2 - origin.x)*zoom, (rect.position.y - rect.size.y/2 - origin.y)*zoom, rect.size.x*zoom, rect.size.y*zoom); + } + + // Draw the border of the rect + this.ctx.strokeStyle = rect.getBorderColor().toStringRGB(); + this.ctx.lineWidth = rect.getBorderWidth(); + this.ctx.strokeRect((rect.position.x - rect.size.x/2 - origin.x)*zoom, (rect.position.y - rect.size.y/2 - origin.y)*zoom, rect.size.x*zoom, rect.size.y*zoom); + } +} \ No newline at end of file diff --git a/src/Rendering/CanvasRendering/TilemapRenderer.ts b/src/Rendering/CanvasRendering/TilemapRenderer.ts new file mode 100644 index 0000000..6d53bf2 --- /dev/null +++ b/src/Rendering/CanvasRendering/TilemapRenderer.ts @@ -0,0 +1,79 @@ +import ResourceManager from "../../ResourceManager/ResourceManager"; +import Scene from "../../Scene/Scene"; +import OrthogonalTilemap from "../../Nodes/Tilemaps/OrthogonalTilemap"; +import Vec2 from "../../DataTypes/Vec2"; +import Tileset from "../../DataTypes/Tilesets/Tileset"; + +export default class TilemapRenderer { + protected resourceManager: ResourceManager; + protected scene: Scene; + protected ctx: CanvasRenderingContext2D; + + constructor(ctx: CanvasRenderingContext2D){ + this.resourceManager = ResourceManager.getInstance(); + this.ctx = ctx; + } + + setScene(scene: Scene): void { + this.scene = scene; + } + + renderOrthogonalTilemap(tilemap: OrthogonalTilemap): void { + let previousAlpha = this.ctx.globalAlpha; + this.ctx.globalAlpha = tilemap.getLayer().getAlpha(); + + let origin = this.scene.getViewTranslation(tilemap); + let size = this.scene.getViewport().getHalfSize(); + let zoom = this.scene.getViewScale(); + let bottomRight = origin.clone().add(size.scaled(2*zoom)); + + if(tilemap.visible){ + let minColRow = tilemap.getColRowAt(origin); + let maxColRow = tilemap.getColRowAt(bottomRight); + + for(let x = minColRow.x; x <= maxColRow.x; x++){ + for(let y = minColRow.y; y <= maxColRow.y; y++){ + // Get the tile at this position + let tile = tilemap.getTileAtRowCol(new Vec2(x, y)); + + // Find the tileset that owns this tile index and render + for(let tileset of tilemap.getTilesets()){ + if(tileset.hasTile(tile)){ + this.renderTile(tileset, tile, x, y, origin, tilemap.scale, zoom); + } + } + } + } + } + + this.ctx.globalAlpha = previousAlpha; + } + + protected renderTile(tileset: Tileset, tileIndex: number, tilemapRow: number, tilemapCol: number, origin: Vec2, scale: Vec2, zoom: number): void { + let image = this.resourceManager.getImage(tileset.getImageKey()); + + // Get the true index + let index = tileIndex - tileset.getStartIndex(); + + // Get the row and col of the tile in image space + let row = Math.floor(index / tileset.getNumCols()); + let col = index % tileset.getNumCols(); + let width = tileset.getTileSize().x; + let height = tileset.getTileSize().y; + + // Calculate the position to start a crop in the tileset image + let left = col * width; + let top = row * height; + + // Calculate the position in the world to render the tile + let x = Math.floor(tilemapRow * width * scale.x); + let y = Math.floor(tilemapCol * height * scale.y); + + // Render the tile + this.ctx.drawImage(image, + left, top, + width, height, + Math.floor((x - origin.x)*zoom), Math.floor((y - origin.y)*zoom), + Math.ceil(width * scale.x * zoom), Math.ceil(height * scale.y * zoom)); + } +} \ No newline at end of file diff --git a/src/Rendering/CanvasRendering/UIElementRenderer.ts b/src/Rendering/CanvasRendering/UIElementRenderer.ts new file mode 100644 index 0000000..f476750 --- /dev/null +++ b/src/Rendering/CanvasRendering/UIElementRenderer.ts @@ -0,0 +1,110 @@ +import Vec2 from "../../DataTypes/Vec2"; +import Button from "../../Nodes/UIElements/Button"; +import Label from "../../Nodes/UIElements/Label"; +import Slider from "../../Nodes/UIElements/Slider"; +import TextInput from "../../Nodes/UIElements/TextInput"; +import ResourceManager from "../../ResourceManager/ResourceManager"; +import Scene from "../../Scene/Scene"; +import MathUtils from "../../Utils/MathUtils"; + +export default class UIElementRenderer { + protected resourceManager: ResourceManager; + protected scene: Scene; + protected ctx: CanvasRenderingContext2D; + + constructor(ctx: CanvasRenderingContext2D){ + this.resourceManager = ResourceManager.getInstance(); + this.ctx = ctx; + } + + setScene(scene: Scene): void { + this.scene = scene; + } + + renderLabel(label: Label): void { + // If the size is unassigned (by the user or automatically) assign it + label.handleInitialSizing(this.ctx); + + // Grab the global alpha so we can adjust it for this render + let previousAlpha = this.ctx.globalAlpha; + + // Get the origin of the viewport according to this label + let origin = this.scene.getViewTranslation(label); + + // Get the font and text position in label + this.ctx.font = label.getFontString(); + let offset = label.calculateTextOffset(this.ctx); + + // Stroke and fill a rounded rect and give it text + this.ctx.globalAlpha = label.backgroundColor.a; + this.ctx.fillStyle = label.calculateBackgroundColor(); + this.ctx.fillRoundedRect(label.position.x - origin.x - label.size.x/2, label.position.y - origin.y - label.size.y/2, + label.size.x, label.size.y, label.borderRadius); + + this.ctx.strokeStyle = label.calculateBorderColor(); + this.ctx.globalAlpha = label.borderColor.a; + this.ctx.lineWidth = label.borderWidth; + this.ctx.strokeRoundedRect(label.position.x - origin.x - label.size.x/2, label.position.y - origin.y - label.size.y/2, + label.size.x, label.size.y, label.borderRadius); + + this.ctx.fillStyle = label.calculateTextColor(); + this.ctx.globalAlpha = label.textColor.a; + this.ctx.fillText(label.text, label.position.x + offset.x - origin.x - label.size.x/2, label.position.y + offset.y - origin.y - label.size.y/2); + + this.ctx.globalAlpha = previousAlpha; + } + + renderButton(button: Button): void { + this.renderLabel(button); + } + + renderSlider(slider: Slider): void { + // Grab the global alpha so we can adjust it for this render + let previousAlpha = this.ctx.globalAlpha; + this.ctx.globalAlpha = slider.getLayer().getAlpha(); + + let origin = this.scene.getViewTranslation(slider); + + // Calcualate the slider size + let sliderSize = new Vec2(slider.size.x, 2); + + // Draw the slider + this.ctx.fillStyle = slider.sliderColor.toString(); + this.ctx.fillRoundedRect(slider.position.x - origin.x - sliderSize.x/2, slider.position.y - origin.y - sliderSize.y/2, + sliderSize.x, sliderSize.y, slider.borderRadius); + + // Calculate the nib size and position + let nibSize = new Vec2(10, slider.size.y); + let x = MathUtils.lerp(slider.position.x - slider.size.x/2, slider.position.x + slider.size.x/2, slider.getValue()); + let nibPosition = new Vec2(x, slider.position.y); + + // Draw the nib + this.ctx.fillStyle = slider.nibColor.toString(); + this.ctx.fillRoundedRect(nibPosition.x - origin.x - nibSize.x/2, nibPosition.y - origin.y - nibSize.y/2, + nibSize.x, nibSize.y, slider.borderRadius); + + // Reset the alpha + this.ctx.globalAlpha = previousAlpha; + } + + renderTextInput(textInput: TextInput): void { + // Show a cursor sometimes + if(textInput.focused && textInput.cursorCounter % 60 > 30){ + textInput.text += "|"; + } + + this.renderLabel(textInput); + + if(textInput.focused){ + if(textInput.cursorCounter % 60 > 30){ + textInput.text = textInput.text.substring(0, textInput.text.length - 1); + } + + textInput.cursorCounter += 1; + if(textInput.cursorCounter >= 60){ + textInput.cursorCounter = 0; + } + } + } + +} \ No newline at end of file diff --git a/src/Rendering/RenderingManager.ts b/src/Rendering/RenderingManager.ts new file mode 100644 index 0000000..7a8cffd --- /dev/null +++ b/src/Rendering/RenderingManager.ts @@ -0,0 +1,39 @@ +import Map from "../DataTypes/Map"; +import CanvasNode from "../Nodes/CanvasNode"; +import Graphic from "../Nodes/Graphic"; +import Sprite from "../Nodes/Sprites/Sprite"; +import Tilemap from "../Nodes/Tilemap"; +import UIElement from "../Nodes/UIElement"; +import ResourceManager from "../ResourceManager/ResourceManager"; +import UILayer from "../Scene/Layers/UILayer"; +import Scene from "../Scene/Scene"; + +export default abstract class RenderingManager { + // Give the renderer access to the resource manager + protected resourceManager: ResourceManager; + protected scene: Scene; + debug: boolean; + + constructor(){ + this.resourceManager = ResourceManager.getInstance(); + this.debug = false; + } + + setScene(scene: Scene): void { + this.scene = scene; + } + + abstract initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): any; + + abstract render(visibleSet: Array, tilemaps: Array, uiLayers: Map): void; + + protected abstract renderSprite(sprite: Sprite): void; + + protected abstract renderAnimatedSprite(): void; + + protected abstract renderGraphic(graphic: Graphic): void; + + protected abstract renderTilemap(tilemap: Tilemap): void; + + protected abstract renderUIElement(uiElement: UIElement): void; +} \ No newline at end of file diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index ce3cb6d..3f1c0ab 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -21,6 +21,7 @@ import UILayer from "./Layers/UILayer"; import CanvasNode from "../Nodes/CanvasNode"; import GameNode from "../Nodes/GameNode"; import ArrayUtils from "../Utils/ArrayUtils"; +import RenderingManager from "../Rendering/RenderingManager"; export default class Scene implements Updateable, Renderable { /** The size of the game world. */ @@ -68,6 +69,9 @@ export default class Scene implements Updateable, Renderable { /** The AI manager of the Scene */ protected aiManager: AIManager; + /** The renderingManager of the scene */ + protected renderingManager: RenderingManager; + /** An interface that allows the adding of different nodes to the scene */ public add: FactoryManager; @@ -77,7 +81,7 @@ export default class Scene implements Updateable, Renderable { /** The configuration options for this scene */ public sceneOptions: SceneOptions; - constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop, options: Record){ + constructor(viewport: Viewport, sceneManager: SceneManager, renderingManager: RenderingManager, game: GameLoop, options: Record){ this.sceneOptions = SceneOptions.parse(options); this.worldSize = new Vec2(500, 500); @@ -99,6 +103,7 @@ export default class Scene implements Updateable, Renderable { this.physicsManager = new BasicPhysicsManager(this.sceneOptions.physics); this.navManager = new NavigationManager(); this.aiManager = new AIManager(); + this.renderingManager = renderingManager; this.add = new FactoryManager(this, this.tilemaps); @@ -143,9 +148,8 @@ export default class Scene implements Updateable, Renderable { this.viewport.update(deltaT); } - render(ctx: CanvasRenderingContext2D): void { - // For webGL, pass a visible set to the renderer - // We need to keep track of the order of things. + render(): void { + // Get the visible set of nodes let visibleSet = this.sceneGraph.getVisibleSet(); // Add parallax layer items to the visible set (we're rendering them all for now) @@ -158,31 +162,8 @@ export default class Scene implements Updateable, Renderable { } }); - // Sort by depth, then by visible set by y-value - visibleSet.sort((a, b) => { - if(a.getLayer().getDepth() === b.getLayer().getDepth()){ - return (a.boundary.bottom) - (b.boundary.bottom); - } else { - return a.getLayer().getDepth() - b.getLayer().getDepth(); - } - }); - - // Render scene graph for demo - this.sceneGraph.render(ctx); - - // Render tilemaps - this.tilemaps.forEach(tilemap => { - tilemap.render(ctx); - }); - - // Render visible set - visibleSet.forEach(node => node.visible ? node.render(ctx) : ""); - - // Debug render the physicsManager - this.physicsManager.debug_render(ctx); - - // Render the uiLayers - this.uiLayers.forEach(key => this.uiLayers.get(key).getItems().forEach(node => (node).render(ctx))); + // Send the visible set, tilemaps, and uiLayers to the renderer + this.renderingManager.render(visibleSet, this.tilemaps, this.uiLayers); } setRunning(running: boolean): void { diff --git a/src/Scene/SceneManager.ts b/src/Scene/SceneManager.ts index 2b6425d..200c403 100644 --- a/src/Scene/SceneManager.ts +++ b/src/Scene/SceneManager.ts @@ -2,19 +2,21 @@ import Scene from "./Scene"; import ResourceManager from "../ResourceManager/ResourceManager"; import Viewport from "../SceneGraph/Viewport"; import GameLoop from "../Loop/GameLoop"; +import RenderingManager from "../Rendering/RenderingManager"; export default class SceneManager { + protected currentScene: Scene; + protected viewport: Viewport; + protected resourceManager: ResourceManager; + protected game: GameLoop; + protected idCounter: number; + protected renderingManager: RenderingManager; - private currentScene: Scene; - private viewport: Viewport; - private resourceManager: ResourceManager; - private game: GameLoop; - private idCounter: number; - - constructor(viewport: Viewport, game: GameLoop){ + constructor(viewport: Viewport, game: GameLoop, renderingManager: RenderingManager){ this.resourceManager = ResourceManager.getInstance(); this.viewport = viewport; this.game = game; + this.renderingManager = renderingManager; this.idCounter = 0; } @@ -23,7 +25,7 @@ export default class SceneManager { * @param constr The constructor of the scene to add */ public addScene(constr: new (...args: any) => T, options: Record): void { - let scene = new constr(this.viewport, this, this.game, options); + let scene = new constr(this.viewport, this, this.renderingManager, this.game, options); this.currentScene = scene; // Enqueue all scene asset loads @@ -36,6 +38,8 @@ export default class SceneManager { scene.startScene(); scene.setRunning(true); }); + + this.renderingManager.setScene(scene); } /** @@ -57,8 +61,8 @@ export default class SceneManager { return this.idCounter++; } - public render(ctx: CanvasRenderingContext2D){ - this.currentScene.render(ctx); + public render(){ + this.currentScene.render(); } public update(deltaT: number){ diff --git a/src/Utils/Color.ts b/src/Utils/Color.ts index 4b7f0db..ec9789c 100644 --- a/src/Utils/Color.ts +++ b/src/Utils/Color.ts @@ -53,6 +53,13 @@ export default class Color { return new Color(255, 100, 0, 1); } + set(r: number, g: number, b: number, a: number = 1): void { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + /** * Returns a new color slightly lighter than the current color */