From d9a87b2727bd5cba9dadc1fc60055cb345bcac65 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Sun, 18 Oct 2020 18:34:13 -0400 Subject: [PATCH] added viewport zoom --- src/BoidDemo.ts | 5 +- src/DataTypes/RegionQuadTree.ts | 12 ++-- src/Events/GameEventType.ts | 10 ++++ src/Input/InputHandler.ts | 14 +++++ src/Input/InputReceiver.ts | 28 ++++++++- src/Loop/GameLoop.ts | 7 ++- src/Nodes/GameNode.ts | 5 ++ src/Nodes/Graphics/Rect.ts | 5 +- src/Nodes/Sprites/Sprite.ts | 7 ++- src/SceneGraph/SceneGraphQuadTree.ts | 4 +- src/SceneGraph/Viewport.ts | 60 ++++++++++++++++++- src/_DemoClasses/Boids/Boid.ts | 11 ++-- .../Boids/BoidStates/RunAwayFromPlayer.ts | 1 - src/main.ts | 4 +- 14 files changed, 147 insertions(+), 26 deletions(-) diff --git a/src/BoidDemo.ts b/src/BoidDemo.ts index 2bbb239..c968853 100644 --- a/src/BoidDemo.ts +++ b/src/BoidDemo.ts @@ -26,7 +26,10 @@ export default class BoidDemo extends Scene { this.boids = new Array(); // Add the player - this.add.graphic(Player, layer, new Vec2(0, 0)); + let player = this.add.graphic(Player, layer, new Vec2(0, 0)); + + this.viewport.follow(player); + this.viewport.enableZoom(); // Create a bunch of boids for(let i = 0; i < 100; i++){ diff --git a/src/DataTypes/RegionQuadTree.ts b/src/DataTypes/RegionQuadTree.ts index 4a82c33..205317a 100644 --- a/src/DataTypes/RegionQuadTree.ts +++ b/src/DataTypes/RegionQuadTree.ts @@ -212,15 +212,15 @@ export default class QuadTree implements Collection { * Renders the quadtree for demo purposes. * @param ctx */ - public render_demo(ctx: CanvasRenderingContext2D): void { + public render_demo(ctx: CanvasRenderingContext2D, origin: Vec2, zoom: number): void { ctx.strokeStyle = "#0000FF"; - ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh); + ctx.strokeRect((this.boundary.x - this.boundary.hw - origin.x)*zoom, (this.boundary.y - this.boundary.hh - origin.y)*zoom, 2*this.boundary.hw*zoom, 2*this.boundary.hh*zoom); if(this.divided){ - this.nw.render_demo(ctx); - this.ne.render_demo(ctx); - this.sw.render_demo(ctx); - this.se.render_demo(ctx); + this.nw.render_demo(ctx, origin, zoom); + this.ne.render_demo(ctx, origin, zoom); + this.sw.render_demo(ctx, origin, zoom); + this.se.render_demo(ctx, origin, zoom); } } diff --git a/src/Events/GameEventType.ts b/src/Events/GameEventType.ts index 5607f6c..c231308 100644 --- a/src/Events/GameEventType.ts +++ b/src/Events/GameEventType.ts @@ -27,6 +27,16 @@ export enum GameEventType { */ CANVAS_BLUR = "canvas_blur", + /** + * Mouse wheel up event. Has data: {} + */ + WHEEL_UP = "wheel_up", + + /** + * Mouse wheel down event. Has data: {} + */ + WHEEL_DOWN = "wheel_down", + /** * Start Recording event. Has data: {} */ diff --git a/src/Input/InputHandler.ts b/src/Input/InputHandler.ts index 2fa56c9..ba067fe 100644 --- a/src/Input/InputHandler.ts +++ b/src/Input/InputHandler.ts @@ -20,6 +20,7 @@ export default class InputHandler{ document.onkeyup = this.handleKeyUp; document.onblur = this.handleBlur; document.oncontextmenu = this.handleBlur; + document.onwheel = this.handleWheel; } private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => { @@ -62,6 +63,19 @@ export default class InputHandler{ event.stopPropagation(); } + private handleWheel = (event: WheelEvent): void => { + event.preventDefault(); + event.stopPropagation(); + + let gameEvent: GameEvent; + if(event.deltaY < 0){ + gameEvent = new GameEvent(GameEventType.WHEEL_UP, {}); + } else { + gameEvent = new GameEvent(GameEventType.WHEEL_DOWN, {}); + } + this.eventQueue.addEvent(gameEvent); + } + private getKey(keyEvent: KeyboardEvent){ return keyEvent.key.toLowerCase(); } diff --git a/src/Input/InputReceiver.ts b/src/Input/InputReceiver.ts index 4dcbae8..9f68626 100644 --- a/src/Input/InputReceiver.ts +++ b/src/Input/InputReceiver.ts @@ -14,10 +14,16 @@ export default class InputReceiver{ private mousePressed: boolean; private mouseJustPressed: boolean; + private keyJustPressed: Map; private keyPressed: Map; + private mousePosition: Vec2; private mousePressPosition: Vec2; + + private scrollDirection: number; + private justScrolled: boolean; + private eventQueue: EventQueue; private receiver: Receiver; private viewport: Viewport; @@ -30,11 +36,13 @@ export default class InputReceiver{ this.keyPressed = new Map(); this.mousePosition = new Vec2(0, 0); this.mousePressPosition = new Vec2(0, 0); + this.scrollDirection = 0; + this.justScrolled = false; this.eventQueue = EventQueue.getInstance(); // Subscribe to all input events this.eventQueue.subscribe(this.receiver, [GameEventType.MOUSE_DOWN, GameEventType.MOUSE_UP, GameEventType.MOUSE_MOVE, - GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR]); + GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR, GameEventType.WHEEL_UP, GameEventType.WHEEL_DOWN]); } static getInstance(): InputReceiver{ @@ -48,6 +56,8 @@ export default class InputReceiver{ // Reset the justPressed values to false this.mouseJustPressed = false; this.keyJustPressed.forEach((key: string) => this.keyJustPressed.set(key, false)); + this.justScrolled = false; + this.scrollDirection = 0; while(this.receiver.hasNextEvent()){ let event = this.receiver.getNextEvent(); @@ -91,6 +101,14 @@ export default class InputReceiver{ if(event.type === GameEventType.CANVAS_BLUR){ this.clearKeyPresses() } + + if(event.type === GameEventType.WHEEL_UP){ + this.scrollDirection = -1; + this.justScrolled = true; + } else if(event.type === GameEventType.WHEEL_DOWN){ + this.scrollDirection = 1; + this.justScrolled = true; + } } } @@ -123,6 +141,14 @@ export default class InputReceiver{ return this.mousePressed; } + didJustScroll(): boolean { + return this.justScrolled; + } + + getScrollDirection(): number { + return this.scrollDirection; + } + getMousePosition(): Vec2 { return this.mousePosition; } diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 032e78c..8a3922d 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -67,12 +67,13 @@ export default class GameLoop { this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke"); // Give the canvas a size and get the rendering context - this.WIDTH = gameConfig.viewportSize ? gameConfig.viewportSize.x : 800; - this.HEIGHT = gameConfig.viewportSize ? gameConfig.viewportSize.y : 500; + this.WIDTH = gameConfig.canvasSize ? gameConfig.canvasSize.x : 800; + this.HEIGHT = gameConfig.canvasSize ? gameConfig.canvasSize.y : 500; this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); // Size the viewport to the game canvas this.viewport = new Viewport(); + this.viewport.setCanvasSize(this.WIDTH, this.HEIGHT); this.viewport.setSize(this.WIDTH, this.HEIGHT); // Initialize all necessary game subsystems @@ -242,5 +243,5 @@ export default class GameLoop { } class GameConfig { - viewportSize: {x: number, y: number} + canvasSize: {x: number, y: number} } \ No newline at end of file diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index c92d1b1..915342f 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -82,5 +82,10 @@ export default abstract class GameNode implements Positioned, Unique, Updateable return this.scene.getViewport().getOrigin().mult(this.layer.getParallax()); } + getViewportScale(): number { + return this.scene.getViewport().getZoomLevel(); + } + + abstract update(deltaT: number): void; } \ No newline at end of file diff --git a/src/Nodes/Graphics/Rect.ts b/src/Nodes/Graphics/Rect.ts index a2133dd..cb6c09a 100644 --- a/src/Nodes/Graphics/Rect.ts +++ b/src/Nodes/Graphics/Rect.ts @@ -35,15 +35,16 @@ export default class Rect extends Graphic { render(ctx: CanvasRenderingContext2D): void { let origin = this.getViewportOriginWithParallax(); + let zoom = this.getViewportScale(); if(this.color.a !== 0){ ctx.fillStyle = this.color.toStringRGB(); - ctx.fillRect(this.position.x - this.size.x/2 - origin.x, this.position.y - this.size.y/2 - origin.y, this.size.x, this.size.y); + 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, this.position.y - this.size.y/2 - origin.y, this.size.x, this.size.y); + 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); } } \ No newline at end of file diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index 9474663..374650e 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -30,15 +30,16 @@ export default class Sprite extends CanvasNode { render(ctx: CanvasRenderingContext2D): void { let image = ResourceManager.getInstance().getImage(this.imageId); let origin = this.getViewportOriginWithParallax(); + let zoom = this.getViewportScale(); 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, this.position.y - origin.y - this.size.y*this.scale.y/2, - this.size.x * this.scale.x, this.size.y * this.scale.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.getBoundary(); - ctx.strokeRect(b.x - b.hw - origin.x, b.y - b.hh - origin.y, b.hw*2, b.hh*2); + 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/SceneGraph/SceneGraphQuadTree.ts b/src/SceneGraph/SceneGraphQuadTree.ts index 83da21d..44baf59 100644 --- a/src/SceneGraph/SceneGraphQuadTree.ts +++ b/src/SceneGraph/SceneGraphQuadTree.ts @@ -54,7 +54,9 @@ export default class SceneGraphQuadTree extends SceneGraph { } render(ctx: CanvasRenderingContext2D): void { - this.qt.render_demo(ctx); + let origin = this.viewport.getOrigin(); + let zoom = this.viewport.getZoomLevel(); + this.qt.render_demo(ctx, origin, zoom); } getVisibleSet(): Array { diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index 468dfbd..4b98c86 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -1,11 +1,11 @@ import Vec2 from "../DataTypes/Vec2"; -import Vec4 from "../DataTypes/Vec4"; import GameNode from "../Nodes/GameNode"; import CanvasNode from "../Nodes/CanvasNode"; import MathUtils from "../Utils/MathUtils"; import Queue from "../DataTypes/Queue"; import AABB from "../DataTypes/AABB"; import Debug from "../Debug/Debug"; +import InputReceiver from "../Input/InputReceiver"; export default class Viewport { private view: AABB; @@ -22,11 +22,21 @@ export default class Viewport { */ private smoothingFactor: number; + private scrollZoomEnabled: boolean; + private ZOOM_FACTOR: number = 1.2; + private canvasSize: Vec2; + constructor(){ this.view = new AABB(Vec2.ZERO, Vec2.ZERO); this.boundary = new AABB(Vec2.ZERO, Vec2.ZERO); this.lastPositions = new Queue(); this.smoothingFactor = 10; + this.scrollZoomEnabled = false; + this.canvasSize = Vec2.ZERO; + } + + enableZoom(): void { + this.scrollZoomEnabled = true; } /** @@ -92,6 +102,23 @@ export default class Viewport { } } + /** + * Sets the size of the canvas that the viewport is projecting to. + * @param vecOrX + * @param y + */ + setCanvasSize(vecOrX: Vec2 | number, y: number = null): void { + if(vecOrX instanceof Vec2){ + this.canvasSize = vecOrX.clone(); + } else { + this.canvasSize = new Vec2(vecOrX, y); + } + } + + getZoomLevel(): number { + return this.canvasSize.x/this.view.hw/2 + } + /** * Sets the smoothing factor for the viewport movement. * @param smoothingFactor The smoothing factor for the viewport @@ -141,6 +168,37 @@ export default class Viewport { } update(deltaT: number): void { + // If zoom is enabled + if(this.scrollZoomEnabled){ + let input = InputReceiver.getInstance(); + if(input.didJustScroll()){ + let currentSize = this.view.getHalfSize().clone(); + if(input.getScrollDirection() < 0){ + // Zoom in + currentSize.scale(1/this.ZOOM_FACTOR); + } else { + // Zoom out + currentSize.scale(this.ZOOM_FACTOR); + } + + if(currentSize.x > this.boundary.hw){ + let factor = this.boundary.hw/currentSize.x; + currentSize.x = this.boundary.hw; + currentSize.y *= factor; + } + + if(currentSize.y > this.boundary.hh){ + let factor = this.boundary.hh/currentSize.y; + currentSize.y = this.boundary.hh; + currentSize.x *= factor; + } + + this.view.setHalfSize(currentSize); + } + } + + Debug.log("vpzoom", "View size: " + this.view.getHalfSize()); + // If viewport is following an object if(this.following){ // Update our list of previous positions diff --git a/src/_DemoClasses/Boids/Boid.ts b/src/_DemoClasses/Boids/Boid.ts index 1b7a746..29d462a 100644 --- a/src/_DemoClasses/Boids/Boid.ts +++ b/src/_DemoClasses/Boids/Boid.ts @@ -23,6 +23,7 @@ export default class Boid extends Graphic { render(ctx: CanvasRenderingContext2D): void { let origin = this.getViewportOriginWithParallax(); + let zoom = this.getViewportScale(); let dirVec = this.direction.scaled(this.size.x, this.size.y); let finVec1 = this.direction.clone().rotateCCW(Math.PI/2).scale(this.size.x/2, this.size.y/2).sub(this.direction.scaled(this.size.x/1.5, this.size.y/1.5)); @@ -31,11 +32,11 @@ export default class Boid extends Graphic { ctx.lineWidth = 1; ctx.fillStyle = this.color.toString(); ctx.beginPath(); - ctx.moveTo(this.position.x - origin.x + dirVec.x, this.position.y - origin.y + dirVec.y); - ctx.lineTo(this.position.x - origin.x + finVec1.x, this.position.y - origin.y + finVec1.y); - ctx.lineTo(this.position.x - origin.x - dirVec.x/3, this.position.y - origin.y - dirVec.y/3); - ctx.lineTo(this.position.x - origin.x + finVec2.x, this.position.y - origin.y + finVec2.y); - ctx.lineTo(this.position.x - origin.x + dirVec.x, this.position.y - origin.y + dirVec.y); + ctx.moveTo((this.position.x - origin.x + dirVec.x)*zoom, (this.position.y - origin.y + dirVec.y)*zoom); + ctx.lineTo((this.position.x - origin.x + finVec1.x)*zoom, (this.position.y - origin.y + finVec1.y)*zoom); + ctx.lineTo((this.position.x - origin.x - dirVec.x/3)*zoom, (this.position.y - origin.y - dirVec.y/3)*zoom); + ctx.lineTo((this.position.x - origin.x + finVec2.x)*zoom, (this.position.y - origin.y + finVec2.y)*zoom); + ctx.lineTo((this.position.x - origin.x + dirVec.x)*zoom, (this.position.y - origin.y + dirVec.y)*zoom); ctx.fill(); } } \ No newline at end of file diff --git a/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts b/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts index b166bce..1c01b72 100644 --- a/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts +++ b/src/_DemoClasses/Boids/BoidStates/RunAwayFromPlayer.ts @@ -23,7 +23,6 @@ export default class RunAwayFromPlayer extends State { } onEnter(): void { - console.log("Entered Running away") this.runAwayDirection = Vec2.ZERO; this.lastPlayerPosition = Vec2.INF; this.timeElapsed = 0; diff --git a/src/main.ts b/src/main.ts index 8533bd5..8eae268 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,10 +7,10 @@ import MarioClone from "./_DemoClasses/MarioClone/MarioClone"; function main(){ // Create the game object - let game = new GameLoop({viewportSize: {x: 800, y: 600}}); + let game = new GameLoop({canvasSize: {x: 800, y: 600}}); game.start(); let sm = game.getSceneManager(); - sm.addScene(MarioClone); + sm.addScene(BoidDemo); } CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {