From 93259a19e5109f14c1a0aee8e3949b5492a7530e Mon Sep 17 00:00:00 2001 From: j0code <42189560+j0code@users.noreply.github.com> Date: Sat, 23 Mar 2024 20:52:42 +0100 Subject: [PATCH] blur and freeze game when paused --- .eslintrc.json | 2 +- src/Graphics.ts | 56 +++++++++-- src/gui/state/ingame.ts | 179 +++++++++++++++++++++--------------- src/gui/state/ingameMenu.ts | 2 + src/main.ts | 1 + src/util/DebugScreen.ts | 3 +- src/util/TextRenderer.ts | 6 +- 7 files changed, 160 insertions(+), 89 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index ff80370..e3e8233 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -244,7 +244,7 @@ }], "lines-between-class-members": ["error", "always", {"exceptAfterSingleLine": true}], "max-len": ["warn", { - "code": 160, "tabWidth": 2, "comments": 160, "ignoreUrls": true + "code": 180, "tabWidth": 2, "comments": 160, "ignoreUrls": true }], "max-statements-per-line": ["error", {"max": 1}], "multiline-ternary": ["warn", "never"], diff --git a/src/Graphics.ts b/src/Graphics.ts index 2091012..0873007 100644 --- a/src/Graphics.ts +++ b/src/Graphics.ts @@ -2,10 +2,22 @@ import TextRenderer, { type TextRenderingOptions } from "./util/TextRenderer.js" export default class Graphics { - readonly ctx: CanvasRenderingContext2D + readonly ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D - constructor(private readonly canvas: HTMLCanvasElement, private readonly blockSize: number) { + private filters: { + brightness?: number, + blur?: number + } + + private filterSaves: ({ + brightness?: number, + blur?: number + })[] + + constructor(readonly canvas: HTMLCanvasElement | OffscreenCanvas, private readonly blockSize: number) { this.ctx = canvas.getContext("2d")! + this.filters = {} + this.filterSaves = [] } get fillStyle() { return this.ctx.fillStyle } @@ -18,20 +30,48 @@ export default class Graphics { set lineWidth(x) { this.ctx.lineWidth = x } set globalAlpha(x) { this.ctx.globalAlpha = x } - brightness(x: number) { - this.ctx.filter = `brightness(${x})` + brightness(x?: number) { + this.filters.brightness = x + this.applyFilters() } - save() { this.ctx.save() } - restore() { this.ctx.restore() } + blur(x?: number) { + this.filters.blur = x + this.applyFilters() + } + + private applyFilters() { + const filter: string[] = [] + + if (this.filters.brightness != undefined) { + filter.push(`brightness(${this.filters.brightness})`) + } + if (this.filters.blur != undefined) { + filter.push(`blur(${this.filters.blur}px)`) + } + + this.ctx.filter = filter.join(" ") + } + + save() { + this.ctx.save() + this.filterSaves.push(structuredClone(this.filters)) + } + + restore() { + this.ctx.restore() + this.filters = this.filterSaves.pop() || {} + this.applyFilters() + } reset() { this.ctx.reset() + this.filters = {} + this.filterSaves = [] + this.applyFilters() this.ctx.imageSmoothingEnabled = false this.ctx.fillStyle = "#78A7FF" this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) - - this.ctx.translate(this.canvas.width/2, this.canvas.height/2) // center game } diff --git a/src/gui/state/ingame.ts b/src/gui/state/ingame.ts index 3324c0a..ce425fc 100644 --- a/src/gui/state/ingame.ts +++ b/src/gui/state/ingame.ts @@ -9,7 +9,7 @@ import ContainerBlock from "../../block/ContainerBlock.js" import CreativeInventory from "../../CreativeInventory.js" import DebugScreen from "../../util/DebugScreen.js" import Dim2 from "../../dim/Dim2.js" -import type Graphics from "../../Graphics.js" +import Graphics from "../../Graphics.js" import Hotbar from "../../util/Hotbar.js" import ItemStack from "../../ItemStack.js" import MenuState from "../../enums/MenuState.js" @@ -26,6 +26,13 @@ export let world: World export let player: Player export let cam: Cam +// offscreen graphics +let og: Graphics + +export function init() { + og = new Graphics(new OffscreenCanvas(game.width, game.height), blockSize) +} + export function createWorld(name: string): World { world = new World(name, [-20, 20, -20, 20, -1, 1]) player = new Player("jens", "TinyJens", 0) @@ -56,92 +63,26 @@ export function tick() { } export function draw(g: Graphics) { - g.reset() - - g.save() - g.translate(gameOffset.x, gameOffset.y) // move game by offset - g.translate(-cam.x, -cam.y) // move game into view - - // world - world.draw(g) - - // hitboxes - if (debug.showDebugScreen && debug.showHitboxes) world.drawBoundingBoxes(g) - - - // origin / axis - if (debug.showDebugScreen && debug.showOrigin) { - g.ctx.strokeStyle = "lime" - g.ctx.beginPath() - g.ctx.moveTo(0, -100) - g.ctx.lineTo(0, 100) - g.ctx.moveTo(-100, 0) - g.ctx.lineTo(100, 0) - g.ctx.stroke() - } - - const mouseBlock = getMouseBlock() - const reachable = isBlockReachable(mouseBlock) - - // block highlight - if (menuState == MenuState.INGAME) { - g.save() - g.translate(mouseBlock.x, mouseBlock.y) - - g.fillStyle = "transparent" - g.strokeStyle = reachable ? "white" : "#707070" - g.lineWidth = reachable ? 2 : 1 - g.strokeRect(1, 1) + if (menuState == MenuState.INGAME) drawGame() - g.restore() - } - - // distance and player range (debug) - if (debug.showDebugScreen && debug.showRange) { - const range = (player.attributes.get("player.block_interaction_range", 0)!) * blockSize - - g.lineWidth = 2 - g.strokeStyle = "white" - g.fillStyle = "white" - - const blockpos = mouseBlock.add(new Dim2(0.5, 0.5)) - const playerpos = player.position.copy().add(new Dim2(0, player.eyeHeight)) + g.reset() + if (menuState == MenuState.INGAME_MENU) { g.save() - g.translate(blockpos.x + 0.2, blockpos.y) - g.drawText(blockpos.distanceTo(playerpos).toFixed(2)) + g.blur(4) + g.ctx.drawImage(og.canvas, 0, 0) g.restore() - - blockpos.scale(blockSize) - playerpos.scale(blockSize) - const { x, y } = playerpos - - g.ctx.beginPath() - g.ctx.ellipse(x, -y, range, range, 0, 0, 2 * Math.PI) - g.ctx.stroke() - - g.ctx.beginPath() - g.ctx.ellipse(blockpos.x, -blockpos.y, 10, 10, 0, 0, 2 * Math.PI) - g.ctx.moveTo(blockpos.x, -blockpos.y) - g.ctx.lineTo(playerpos.x, -playerpos.y) - g.ctx.ellipse(playerpos.x, -playerpos.y, 10, 10, 0, 0, 2 * Math.PI) - g.ctx.stroke() + } else { + g.ctx.drawImage(og.canvas, 0, 0) } - g.restore() - - // hotbar - Hotbar.drawHotbar(g) - - // container - Container.drawContainer(g) - // cursor if (menuState == MenuState.INGAME) { const { x, y } = getMousePos() const size = blockSize/2 g.save() + g.ctx.translate(game.width/2, game.height/2) // center game g.translate(gameOffset.x, gameOffset.y) // move game by offset g.translate(-cam.x, -cam.y) // move game into view g.translate(x, y) @@ -152,6 +93,9 @@ export function draw(g: Graphics) { const targetBlock = world.getBlock(x, y, z) const inaccessible = z < frontZ && frontBlock?.full + const mouseBlock = getMouseBlock() + const reachable = isBlockReachable(mouseBlock) + if (!reachable) { drawCrosshair(g, size, "#707070") } else if (floatingStack) { @@ -175,6 +119,91 @@ export function draw(g: Graphics) { if (debug.showDebugScreen) DebugScreen.draw(g, world, player) } +function drawGame() { + og.canvas.width = game.width + og.canvas.height = game.height + og.reset() + og.ctx.translate(og.canvas.width/2, og.canvas.height/2) // center game + + og.save() + og.translate(gameOffset.x, gameOffset.y) // move game by offset + og.translate(-cam.x, -cam.y) // move game into view + + // world + world.draw(og) + + // hitboxes + if (debug.showDebugScreen && debug.showHitboxes) world.drawBoundingBoxes(og) + + + // origin / axis + if (debug.showDebugScreen && debug.showOrigin) { + og.ctx.strokeStyle = "lime" + og.ctx.beginPath() + og.ctx.moveTo(0, -100) + og.ctx.lineTo(0, 100) + og.ctx.moveTo(-100, 0) + og.ctx.lineTo(100, 0) + og.ctx.stroke() + } + + const mouseBlock = getMouseBlock() + const reachable = isBlockReachable(mouseBlock) + + // block highlight + if (menuState == MenuState.INGAME) { + og.save() + og.translate(mouseBlock.x, mouseBlock.y) + + og.fillStyle = "transparent" + og.strokeStyle = reachable ? "white" : "#707070" + og.lineWidth = reachable ? 2 : 1 + og.strokeRect(1, 1) + + og.restore() + } + + // distance and player range (debug) + if (debug.showDebugScreen && debug.showRange) { + const range = (player.attributes.get("player.block_interaction_range", 0)!) * blockSize + + og.lineWidth = 2 + og.strokeStyle = "white" + og.fillStyle = "white" + + const blockpos = mouseBlock.add(new Dim2(0.5, 0.5)) + const playerpos = player.position.copy().add(new Dim2(0, player.eyeHeight)) + + og.save() + og.translate(blockpos.x + 0.2, blockpos.y) + og.drawText(blockpos.distanceTo(playerpos).toFixed(2)) + og.restore() + + blockpos.scale(blockSize) + playerpos.scale(blockSize) + const { x, y } = playerpos + + og.ctx.beginPath() + og.ctx.ellipse(x, -y, range, range, 0, 0, 2 * Math.PI) + og.ctx.stroke() + + og.ctx.beginPath() + og.ctx.ellipse(blockpos.x, -blockpos.y, 10, 10, 0, 0, 2 * Math.PI) + og.ctx.moveTo(blockpos.x, -blockpos.y) + og.ctx.lineTo(playerpos.x, -playerpos.y) + og.ctx.ellipse(playerpos.x, -playerpos.y, 10, 10, 0, 0, 2 * Math.PI) + og.ctx.stroke() + } + + og.restore() + + // hotbar + Hotbar.drawHotbar(og) + + // container + Container.drawContainer(og) +} + export function onKey(key: string) { if (Container.showingInventory()) if (Container.onKey(key)) return diff --git a/src/gui/state/ingameMenu.ts b/src/gui/state/ingameMenu.ts index 8251a92..26901e0 100644 --- a/src/gui/state/ingameMenu.ts +++ b/src/gui/state/ingameMenu.ts @@ -19,6 +19,8 @@ quitButton.on("click", () => { export function draw(g: Graphics) { const { ctx } = g + ctx.translate(g.canvas.width/2, g.canvas.height/2) // center game + ctx.textAlign = "center" TextRenderer.drawText(ctx, "Saved game!", 0, -280, { font: { size: 50 }, diff --git a/src/main.ts b/src/main.ts index 1c03e86..f4d8070 100644 --- a/src/main.ts +++ b/src/main.ts @@ -68,6 +68,7 @@ Button.loadAssets() Hotbar.loadTexture() Container.loadAssets() Entity.loadAssets() +ingameState.init() export const perf = { tick: [] as number[], diff --git a/src/util/DebugScreen.ts b/src/util/DebugScreen.ts index 04f1b4c..c8094a8 100644 --- a/src/util/DebugScreen.ts +++ b/src/util/DebugScreen.ts @@ -26,7 +26,6 @@ function gameInfo(g: Graphics, world: World, player: Player) { const lookingAtFluid = getFirstFluid(world, mouseBlock.x, mouseBlock.y) ctx.save() - ctx.translate(-game.width/2, -game.height/2) // uncenter ctx.translate(2, 2) ctx.textAlign = "left" @@ -76,7 +75,7 @@ function envInfo(g: Graphics) { // some of this might break const { ctx } = g ctx.save() - ctx.translate(game.width/2, -game.height/2) // uncenter (-> top right corner) + ctx.translate(game.width, 0) // -> top right corner ctx.translate(-2, 2) ctx.textAlign = "right" diff --git a/src/util/TextRenderer.ts b/src/util/TextRenderer.ts index 54b5a07..57be0e0 100644 --- a/src/util/TextRenderer.ts +++ b/src/util/TextRenderer.ts @@ -2,7 +2,7 @@ const SHADOW_BRIGHTNESS = "0.314" export default class TextRenderer { - static drawText(ctx: CanvasRenderingContext2D, text: string, x: number = 0, y: number = 0, options?: TextRenderingOptions) { + static drawText(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, text: string, x: number = 0, y: number = 0, options?: TextRenderingOptions) { const { color = ctx.fillStyle || "white", opacity = 1, bgOpacity = 0.35, @@ -87,7 +87,7 @@ export type TextRenderingOptions = { } function drawDecoratedText( - ctx: CanvasRenderingContext2D, + ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, text: string, x: number, y: number, @@ -104,7 +104,7 @@ function drawDecoratedText( if (strikethrough) drawLine(ctx, x, y - unit * 5, textWidth, unit) } -function drawLine(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { +function drawLine(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, x: number, y: number, width: number, height: number) { if (ctx.textAlign == "right") x -= width else if (ctx.textAlign == "center") x -= width / 2