diff --git a/src/Input/InputReceiver.ts b/src/Input/InputReceiver.ts index 9f68626..0ecd503 100644 --- a/src/Input/InputReceiver.ts +++ b/src/Input/InputReceiver.ts @@ -125,6 +125,16 @@ export default class InputReceiver{ } } + getKeysJustPressed(): Array { + let keys = Array(); + this.keyJustPressed.forEach(key => { + if(this.keyJustPressed.get(key)){ + keys.push(key); + } + }); + return keys; + } + isPressed(key: string): boolean { if(this.keyPressed.has(key)){ return this.keyPressed.get(key) diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index cb8190a..ffb84e1 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -1,12 +1,12 @@ import GameNode from "./GameNode"; import Vec2 from "../DataTypes/Vec2"; -import { Region } from "../DataTypes/Interfaces/Descriptors"; +import { Region, Renderable } from "../DataTypes/Interfaces/Descriptors"; 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 { +export default abstract class CanvasNode extends GameNode implements Region, Renderable { private _size: Vec2; private _scale: Vec2; private _boundary: AABB; @@ -21,8 +21,6 @@ export default abstract class CanvasNode extends GameNode implements Region { this._scale.setOnChange(() => this.scaleChanged()); this._boundary = new AABB(); this.updateBoundary(); - - this.size.set(101, 101); } get size(): Vec2 { diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 2c87d58..6592760 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -4,7 +4,7 @@ import Receiver from "../Events/Receiver"; import Emitter from "../Events/Emitter"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; -import { Physical, Positioned, isRegion, Unique, Updateable, Actor, AI } from "../DataTypes/Interfaces/Descriptors" +import { Physical, Positioned, isRegion, Unique, Updateable, Actor, AI, Debug_Renderable } from "../DataTypes/Interfaces/Descriptors" import Shape from "../DataTypes/Shapes/Shape"; import Map from "../DataTypes/Map"; import AABB from "../DataTypes/Shapes/AABB"; @@ -13,7 +13,7 @@ import NavigationPath from "../Pathfinding/NavigationPath"; /** * The representation of an object in the game world */ -export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor { +export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor, Debug_Renderable { /*---------- POSITIONED ----------*/ private _position: Vec2; @@ -227,4 +227,6 @@ export default abstract class GameNode implements Positioned, Unique, Updateable }; abstract update(deltaT: number): void; + + debug_render = (ctx: CanvasRenderingContext2D): void => {}; } \ No newline at end of file diff --git a/src/Nodes/UIElements/Label.ts b/src/Nodes/UIElements/Label.ts index b578de9..82685ab 100644 --- a/src/Nodes/UIElements/Label.ts +++ b/src/Nodes/UIElements/Label.ts @@ -100,7 +100,6 @@ export default class Label extends UIElement{ // 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); @@ -108,23 +107,30 @@ export default class Label extends UIElement{ 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; + } + debug_render = (ctx: CanvasRenderingContext2D): void => { + let origin = this.scene.getViewTranslation(this); + 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, b.hh*2); - } + }; } \ No newline at end of file diff --git a/src/Nodes/UIElements/Slider.ts b/src/Nodes/UIElements/Slider.ts new file mode 100644 index 0000000..ba981a7 --- /dev/null +++ b/src/Nodes/UIElements/Slider.ts @@ -0,0 +1,76 @@ +import Vec2 from "../../DataTypes/Vec2"; +import Color from "../../Utils/Color"; +import MathUtils from "../../Utils/MathUtils"; +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 onValueChange: (value: number) => void; + public onValueChangeEventId: string; + + constructor(position: Vec2){ + super(position); + + this.value = 0; + this.nibColor = Color.RED; + this.sliderColor = Color.BLACK; + this.backgroundColor = Color.TRANSPARENT; + this.borderColor = Color.TRANSPARENT; + + // Set a default size + this.size.set(200, 20); + } + + protected valueChanged(): void { + if(this.onValueChange){ + this.onValueChange(this.value); + } + + if(this.onValueChangeEventId){ + this.emitter.fireEvent(this.onValueChangeEventId, {target: this, value: this.value}); + } + } + + update(deltaT: number): void { + super.update(deltaT); + + if(this.isClicked){ + let val = MathUtils.invLerp(this.position.x - this.size.x/2, this.position.x + this.size.x/2, this.input.getMousePosition().x); + this.value = MathUtils.clamp01(val); + 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 new file mode 100644 index 0000000..4d0803c --- /dev/null +++ b/src/Nodes/UIElements/TextInput.ts @@ -0,0 +1,81 @@ +import Vec2 from "../../DataTypes/Vec2"; +import Color from "../../Utils/Color"; +import Label from "./Label"; + +export default class TextInput extends Label { + focused: boolean; + cursorCounter: number; + + constructor(position: Vec2){ + super(position, ""); + + this.focused = false; + this.cursorCounter = 0; + + // Give a default size to the x only + this.size.set(200, this.fontSize); + this.hAlign = "left"; + + this.borderColor = Color.BLACK; + this.backgroundColor = Color.WHITE; + } + + update(deltaT: number): void { + super.update(deltaT); + + if(this.input.isMouseJustPressed()){ + let clickPos = this.input.getMousePressPosition(); + if(this.contains(clickPos.x, clickPos.y)){ + this.focused = true; + this.cursorCounter = 30; + } else { + this.focused = false; + } + } + + if(this.focused){ + let keys = this.input.getKeysJustPressed(); + let nums = "1234567890"; + let specialChars = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?"; + let letters = "qwertyuiopasdfghjklzxcvbnm"; + let mask = nums + specialChars + letters; + // THIS ISN'T ACTUALLY AN ERROR, DISREGARD + keys = keys.filter(key => mask.includes(key)); + let shiftPressed = this.input.isPressed("shift"); + let backspacePressed = this.input.isJustPressed("backspace"); + let spacePressed = this.input.isJustPressed("space"); + + if(backspacePressed){ + this.text = this.text.substring(0, this.text.length - 1); + } else if(spacePressed){ + this.text += " "; + } else if(keys.length > 0) { + if(shiftPressed){ + this.text += keys[0].toUpperCase(); + } else { + this.text += keys[0]; + } + } + } + } + + 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/Nodes/UIElements/UIElementTypes.ts b/src/Nodes/UIElements/UIElementTypes.ts index cf97070..6530db2 100644 --- a/src/Nodes/UIElements/UIElementTypes.ts +++ b/src/Nodes/UIElements/UIElementTypes.ts @@ -1,4 +1,6 @@ export enum UIElementType { BUTTON = "BUTTON", LABEL = "LABEL", + SLIDER = "SLIDER", + TEXT_INPUT = "TEXTINPUT" } \ No newline at end of file diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index c1b0da2..f7bf4c4 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -1,15 +1,15 @@ import Scene from "../Scene"; import UIElement from "../../Nodes/UIElement"; -import Layer from "../Layer"; import Graphic from "../../Nodes/Graphic"; import Sprite from "../../Nodes/Sprites/Sprite"; import { GraphicType } from "../../Nodes/Graphics/GraphicTypes"; import { UIElementType } from "../../Nodes/UIElements/UIElementTypes"; import Point from "../../Nodes/Graphics/Point"; import Vec2 from "../../DataTypes/Vec2"; -import Shape from "../../DataTypes/Shapes/Shape"; 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 Rect from "../../Nodes/Graphics/Rect"; export default class CanvasNodeFactory { @@ -38,6 +38,12 @@ export default class CanvasNodeFactory { case UIElementType.LABEL: instance = this.buildLabel(options); break; + case UIElementType.SLIDER: + instance = this.buildSlider(options); + break; + case UIElementType.TEXT_INPUT: + instance = this.buildTextInput(options); + break; default: throw `UIElementType '${type}' does not exist, or is registered incorrectly.` } @@ -126,6 +132,18 @@ export default class CanvasNodeFactory { return new Label(options.position, options.text) } + buildSlider(options: Record): Slider { + this.checkIfPropExists("Slider", options, "position", Vec2, "Vec2"); + + return new Slider(options.position); + } + + buildTextInput(options: Record): TextInput { + this.checkIfPropExists("TextInput", options, "position", Vec2, "Vec2"); + + return new TextInput(options.position); + } + buildPoint(options?: Record): Point { this.checkIfPropExists("Point", options, "position", Vec2, "Vec2"); @@ -142,7 +160,7 @@ export default class CanvasNodeFactory { /* ---------- ERROR HANDLING ---------- */ checkIfPropExists(objectName: string, options: Record, prop: string, type: (new (...args: any) => T) | string, typeName?: string){ - if(!options || !options[prop]){ + if(!options || options[prop] === undefined){ // Check that the options object has the property throw `${objectName} object requires argument ${prop} of type ${typeName}, but none was provided.`; } else { diff --git a/src/Scene/Layer.ts b/src/Scene/Layer.ts index 3ac3e7b..97529ca 100644 --- a/src/Scene/Layer.ts +++ b/src/Scene/Layer.ts @@ -42,6 +42,10 @@ export default class Layer { this.depth = 0; } + getName(): string { + return this.name; + } + setPaused(pauseValue: boolean): void { this.paused = pauseValue; } diff --git a/src/_DemoClasses/Mario/Level1.ts b/src/_DemoClasses/Mario/Level1.ts index a5441de..f2fc12f 100644 --- a/src/_DemoClasses/Mario/Level1.ts +++ b/src/_DemoClasses/Mario/Level1.ts @@ -75,8 +75,8 @@ export default class Level1 extends Scene { // Add UI this.addUILayer("UI"); - this.coinCountLabel = this.add.uiElement(UIElementType.LABEL, "UI", {position: new Vec2(80, 30), text: "Coins: 0"}); - this.livesCountLabel = this.add.uiElement(UIElementType.LABEL, "UI", {position: new Vec2(600, 30), text: "Lives: 3"}); + this.coinCountLabel =