diff --git a/client/assets.ts b/client/assets.ts index 9f3304e..b3dd998 100644 --- a/client/assets.ts +++ b/client/assets.ts @@ -1,4 +1,6 @@ -type Asset = HTMLImageElement | HTMLAudioElement | string | Record; +import { load_texture, Texture } from "./renderer.ts"; + +type Asset = Texture | HTMLAudioElement | string | Record; export class AssetManager { static instance = new AssetManager(); @@ -16,11 +18,13 @@ export class AssetManager { if (src.endsWith(".png")) { const img = new Image(); const promise = new Promise((resolve, reject) => { - img.onload = () => resolve(); + img.onload = () => { + this.assets[key] = load_texture(img); + resolve(); + }; img.onerror = () => reject(new Error(`Failed to load image: ${src}`)); }); img.src = src; - this.assets[key] = img; this.loading_promises.push(promise); } else if (src.endsWith(".ogg")) { const audio = new Audio(); diff --git a/client/client_world.ts b/client/client_world.ts index 4c5c808..1f89e74 100644 --- a/client/client_world.ts +++ b/client/client_world.ts @@ -10,49 +10,30 @@ import { UIRenderSystem } from "$/client/systems/ui_render_system.ts"; import { CropSystem } from "$/client/systems/crop_system.ts"; import { create_main_menu } from "./main_menu.ts"; import { ClickableSystem } from "./systems/clickable_system.ts"; +import { start_game } from "./game.ts"; +import { canvas, resize_canvas } from "./renderer.ts"; export class ClientWorld extends World { - canvas: HTMLCanvasElement; - ctx: CanvasRenderingContext2D; - paused = false; debugging = false; - constructor(canvas: HTMLCanvasElement) { - super("main_menu"); + constructor() { + super("game"); this.add_state("main_menu"); this.add_state("paused"); this.add_state("game"); - this.canvas = canvas; - const ctx = canvas.getContext("2d"); - if (!ctx) { - throw new Error("Could not get CanvasRenderingContext2D"); - } - this.ctx = ctx; - ctx.imageSmoothingEnabled = false; - - const resize = () => { - canvas.width = self.innerWidth; - canvas.height = self.innerHeight; - - canvas.style.width = canvas.width + "px"; - canvas.style.height = canvas.height + "px"; - - this.ctx.imageSmoothingEnabled = false; - }; - - self.addEventListener("resize", resize); - resize(); + self.addEventListener("resize", resize_canvas); + resize_canvas(); canvas.addEventListener("contextmenu", function (event) { event.preventDefault(); }); - DebugUI.initialize(this.ctx); + //DebugUI.initialize(this.ctx); - create_main_menu(this); + start_game(this); // Logic systems this.add_system(new UIInteractionSystem(), "main_menu"); @@ -64,7 +45,7 @@ export class ClientWorld extends World { this.add_system(new CropSystem(), "game"); // render systems - this.add_system(new RenderSystem(this.ctx), "game"); + this.add_system(new RenderSystem(), "game"); this.add_system(new DebugSystem(), "*"); this.add_system(new UIRenderSystem(), "main_menu"); this.add_system(new UIRenderSystem(), "paused"); diff --git a/client/components/dimension.ts b/client/components/dimension.ts index 4ef79e1..f49af15 100644 --- a/client/components/dimension.ts +++ b/client/components/dimension.ts @@ -1,5 +1,6 @@ import { Component } from "$/common/ecs/mod.ts"; import { AssetManager } from "../assets.ts"; +import { Texture } from "../renderer.ts"; interface Tile { x: number; @@ -9,7 +10,7 @@ interface Tile { } export class Dimension extends Component { - image: HTMLImageElement; + image: Texture; tiles: Tile[]; constructor(tiles: Tile[]) { diff --git a/client/components/sprite.ts b/client/components/sprite.ts index 38910fe..029a1ce 100644 --- a/client/components/sprite.ts +++ b/client/components/sprite.ts @@ -1,8 +1,9 @@ import { Component } from "$/common/ecs/mod.ts"; import { AssetManager } from "$/client/assets.ts"; +import { Texture } from "../renderer.ts"; export class Sprite extends Component { - image: HTMLImageElement; + image: Texture; width: number; height: number; source_x: number; @@ -13,7 +14,7 @@ export class Sprite extends Component { flip_y = false; constructor( - image: HTMLImageElement | string, + image: Texture | string, width: number, height: number, source_x = 0, @@ -23,7 +24,7 @@ export class Sprite extends Component { ) { super(); if (typeof image === "string") { - this.image = AssetManager.instance.get(image); + this.image = AssetManager.instance.get(image); } else { this.image = image; } @@ -45,7 +46,7 @@ interface AnimatedSpritePiece { } export class AnimatedSprite extends Component { - image: HTMLImageElement; + image: Texture; width: number; height: number; flip_x = false; @@ -57,7 +58,7 @@ export class AnimatedSprite extends Component { animation_frame = 0; constructor( - image: HTMLImageElement | string, + image: Texture | string, width: number, height: number, states: Record, @@ -65,7 +66,7 @@ export class AnimatedSprite extends Component { ) { super(); if (typeof image === "string") { - this.image = AssetManager.instance.get(image); + this.image = AssetManager.instance.get(image); } else { this.image = image; } diff --git a/client/debug_ui.ts b/client/debug_ui.ts index dfbaf74..194d913 100644 --- a/client/debug_ui.ts +++ b/client/debug_ui.ts @@ -1,6 +1,6 @@ import { point_inside_rec } from "$/common/utils.ts"; import { InputManager } from "$/client/input_manager.ts"; -import { draw_text, measure_text } from "$/client/text_rendering.ts"; +//import { draw_text, measure_text } from "$/client/text_rendering.ts"; const HEADER_HEIGHT = 24; const RESIZE_SIZE = 12; @@ -135,7 +135,7 @@ export class DebugUI { this.ctx.fillStyle = "#333"; this.ctx.fillRect(win.x, win.y, win.width, HEADER_HEIGHT); - draw_text(this.ctx, title, win.x + 8, win.y + 4, 2); + //draw_text(this.ctx, title, win.x + 8, win.y + 4, 2); // resize grip this.ctx.fillStyle = hovering_resize ? "#888" : "#666"; @@ -201,7 +201,7 @@ export class DebugUI { } static text(content: string) { - draw_text(this.ctx, content, this.cursor_x, this.cursor_y, 2); + //draw_text(this.ctx, content, this.cursor_x, this.cursor_y, 2); this.advance(20); } @@ -221,7 +221,7 @@ export class DebugUI { this.ctx.strokeRect(x, y, width, height); this.ctx.fillStyle = "white"; - draw_text(this.ctx, label, x + 6, y + 4, 2); + //draw_text(this.ctx, label, x + 6, y + 4, 2); this.advance(height + PADDING); @@ -253,7 +253,7 @@ export class DebugUI { } this.ctx.fillStyle = "white"; - draw_text(this.ctx, label, x + size + 6, y, 2); + //draw_text(this.ctx, label, x + size + 6, y, 2); this.advance(size + PADDING); @@ -295,7 +295,7 @@ export class DebugUI { this.ctx.strokeRect(x, y, width, height); this.ctx.fillStyle = "white"; - draw_text(this.ctx, `${label}: ${value.toFixed(2)}`, x + 5, y + 2, 2); + //draw_text(this.ctx, `${label}: ${value.toFixed(2)}`, x + 5, y + 2, 2); this.advance(height + PADDING); @@ -414,17 +414,17 @@ export class DebugUI { const text_x = x + 6; - draw_text(this.ctx, state.value, x + 6, y + 4, 2); + //draw_text(this.ctx, state.value, x + 6, y + 4, 2); // caret ! if (is_active && state.show_caret) { const before_text = state.value.substring(0, state.caret); - const caret_x = text_x + measure_text(this.ctx, before_text, 2); + //const caret_x = text_x + measure_text(this.ctx, before_text, 2); this.ctx.strokeStyle = "white"; this.ctx.beginPath(); - this.ctx.moveTo(caret_x, y + 4); - this.ctx.lineTo(caret_x, y + height - 4); + //this.ctx.moveTo(caret_x, y + 4); + //this.ctx.lineTo(caret_x, y + height - 4); this.ctx.stroke(); } diff --git a/client/game.ts b/client/game.ts index c403413..71b829f 100644 --- a/client/game.ts +++ b/client/game.ts @@ -7,6 +7,7 @@ import { UIButton } from "./components/ui_components.ts"; import { open_about } from "./about.ts"; import { create_crop_entity, Crop } from "./components/crop.ts"; import { generate } from "./generation.ts"; +import { canvas } from "./renderer.ts"; export function start_game(world: ClientWorld) { world.state = "game"; @@ -21,12 +22,12 @@ export function start_game(world: ClientWorld) { // UI ! const unpause_button = new Entity("unpausebutton"); - unpause_button.add(new Position(world.canvas.width / 2 - 150, world.canvas.height / 2 - 80)); + unpause_button.add(new Position(canvas.width / 2 - 150, canvas.height / 2 - 80)); unpause_button.add(new UIButton("Unpause", 320, 64, () => world.state = "paused")); world.add_entity(unpause_button); const about_button = new Entity("aboutbutton"); - about_button.add(new Position(world.canvas.width / 2 - 150, world.canvas.height / 2 + 80)); + about_button.add(new Position(canvas.width / 2 - 150, canvas.height / 2 + 80)); about_button.add(new UIButton("About", 320, 64, () => open_about())); world.add_entity(about_button); diff --git a/client/main.ts b/client/main.ts index 8f7f361..ffec27d 100644 --- a/client/main.ts +++ b/client/main.ts @@ -1,6 +1,7 @@ import { AssetManager } from "./assets.ts"; import { ClientWorld } from "./client_world.ts"; import { InputManager } from "./input_manager.ts"; +import { begin_drawing, end_drawing, init_window } from "./renderer.ts"; export class ClientLoop { running = false; @@ -34,9 +35,9 @@ export class ClientLoop { const delta = (time - this.last_time) / 1000; - const ctx = this.world.ctx; - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + begin_drawing(); this.world.update(delta); + end_drawing(); this.frame_count += 1; const now = performance.now(); @@ -59,6 +60,8 @@ if (!canvas) { throw Error("Canvas was not found"); } +init_window(canvas); + InputManager.initialize(canvas); AssetManager.instance.load("bworld:assets_text", "/assets/ASSETS.md"); @@ -72,7 +75,7 @@ AssetManager.instance.load("bworld:ui", "/assets/sprites/ui.png"); await AssetManager.instance.load_all(); -const client_world = new ClientWorld(canvas); +const client_world = new ClientWorld(); const loop = new ClientLoop(client_world); loop.start(); diff --git a/client/main_menu.ts b/client/main_menu.ts index 575ee10..65ac29c 100644 --- a/client/main_menu.ts +++ b/client/main_menu.ts @@ -4,10 +4,9 @@ import { ClientWorld } from "./client_world.ts"; import { UIButton } from "./components/ui_components.ts"; import { open_about } from "./about.ts"; import { start_game } from "./game.ts"; +import { canvas } from "./renderer.ts"; export function create_main_menu(world: ClientWorld) { - const canvas = world.canvas; - const play_button = new Entity("play"); play_button.add(new Position(canvas.width / 2 - 150, canvas.height / 2 - 80)); play_button.add(new UIButton("Play", 320, 64, () => start_game(world))); diff --git a/client/renderer.ts b/client/renderer.ts new file mode 100644 index 0000000..bb5453e --- /dev/null +++ b/client/renderer.ts @@ -0,0 +1,358 @@ +const MAX_SPRITES = 10000; +const VERTS_PER_SPRITE = 6; +const FLOATS_PER_VERT = 4; + +let gl: WebGLRenderingContext; +export let canvas: HTMLCanvasElement; +let program: WebGLProgram; +let buffer: WebGLBuffer; + +const vertex_data = new Float32Array(MAX_SPRITES * VERTS_PER_SPRITE * FLOATS_PER_VERT); +let vert_index = 0; + +let resolution_loc: WebGLUniformLocation | null; +let _texture_loc: WebGLUniformLocation | null; + +let current_texture: WebGLTexture | null = null; + +const glyph_size = 32; +let font_texture: WebGLTexture; +const glyphs: Record = {}; + +const vertex_src = ` +attribute vec2 aPos; +attribute vec2 aUV; +varying vec2 vUV; +uniform vec2 uResolution; + +void main(){ + vec2 zero = aPos / uResolution; + vec2 clip = zero * 2.0 - 1.0; + gl_Position = vec4(clip * vec2(1,-1),0,1); + vUV = aUV; +} +`; + +const fragmentSrc = ` +precision mediump float; +varying vec2 vUV; +uniform sampler2D uTex; + +void main(){ + gl_FragColor = texture2D(uTex,vUV); +} +`; + +export interface Texture { + tex: WebGLTexture; + width: number; + height: number; +} + +export function init_window(canvas_element: HTMLCanvasElement) { + canvas = canvas_element; + canvas.width = 1800; + canvas.height = 900; + const ctx = canvas.getContext("webgl"); + if (!ctx) { + throw new Error("WebGL not supported"); + } + + gl = ctx; + + program = create_program(vertex_src, fragmentSrc); + + buffer = gl.createBuffer()!; + + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + + const stride = FLOATS_PER_VERT * 4; + + const pos_loc = gl.getAttribLocation(program, "aPos"); + gl.enableVertexAttribArray(pos_loc); + gl.vertexAttribPointer(pos_loc, 2, gl.FLOAT, false, stride, 0); + + const uv_loc = gl.getAttribLocation(program, "aUV"); + gl.enableVertexAttribArray(uv_loc); + gl.vertexAttribPointer(uv_loc, 2, gl.FLOAT, false, stride, 8); + + resolution_loc = gl.getUniformLocation(program, "uResolution"); + _texture_loc = gl.getUniformLocation(program, "uTex"); + + gl.useProgram(program); + gl.uniform2f(resolution_loc, canvas.width, canvas.height); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + init_font(); +} + +function create_shader(type: number, src: string): WebGLShader { + const s = gl.createShader(type)!; + gl.shaderSource(s, src); + gl.compileShader(s); + return s; +} + +function create_program(vs: string, fs: string): WebGLProgram { + const program = gl.createProgram(); + + const v = create_shader(gl.VERTEX_SHADER, vs); + const f = create_shader(gl.FRAGMENT_SHADER, fs); + + gl.attachShader(program, v); + gl.attachShader(program, f); + gl.linkProgram(program); + + return program; +} + +export function load_texture(image: HTMLImageElement): Texture { + const texture = gl.createTexture(); + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.RGBA, + gl.UNSIGNED_BYTE, + image, + ); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + return { + tex: texture, + width: image.width, + height: image.height, + }; +} + +export function begin_drawing() { + vert_index = 0; +} + +export function clear_background(r: number, g: number, b: number, a = 1) { + gl.clearColor(r, g, b, a); + gl.clear(gl.COLOR_BUFFER_BIT); +} + +export function end_drawing() { + flush_batch(); +} + +export function draw_texture( + texture: Texture, + x: number, + y: number, + w: number, + h: number, + flip_x = false, + flip_y = false, +) { + if (current_texture !== texture.tex) { + flush_batch(); + current_texture = texture.tex; + } + + let u0 = 0, v0 = 0, u1 = 1, v1 = 1; + + if (flip_x) { + [u0, u1] = [u1, u0]; + } + if (flip_y) { + [v0, v1] = [v1, v0]; + } + + push_quad(x, y, w, h, u0, v0, u1, v1); +} + +export function draw_texture_region( + texture: Texture, + sx: number, + sy: number, + sw: number, + sh: number, + dx: number, + dy: number, + dw: number, + dh: number, + flip_x = false, + flip_y = false, +) { + if (current_texture !== texture.tex) { + flush_batch(); + current_texture = texture.tex; + } + + let u0 = sx / texture.width; + let v0 = sy / texture.height; + let u1 = (sx + sw) / texture.width; + let v1 = (sy + sh) / texture.height; + + if (flip_x) { + [u0, u1] = [u1, u0]; + } + if (flip_y) { + [v0, v1] = [v1, v0]; + } + + push_quad(dx, dy, dw, dh, u0, v0, u1, v1); +} + +export function draw_text(str: string, x: number, y: number) { + if (!font_texture) { + return; + } + + if (current_texture !== font_texture) { + flush_batch(); + current_texture = font_texture; + } + + for (const ch of str) { + const g = glyphs[ch]; + if (!g) { + x += glyph_size; + continue; + } + + push_quad( + x, + y, + glyph_size, + glyph_size, + g.u0, + g.v0, + g.u1, + g.v1, + ); + + x += glyph_size * 0.6; + } +} + +function flush_batch() { + if (vert_index === 0 || !current_texture) { + return; + } + + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, vertex_data.subarray(0, vert_index), gl.STREAM_DRAW); + + gl.useProgram(program); + + const posLoc = gl.getAttribLocation(program, "aPos"); + const uvLoc = gl.getAttribLocation(program, "aUV"); + gl.enableVertexAttribArray(posLoc); + gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0); + gl.enableVertexAttribArray(uvLoc); + gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8); + + gl.bindTexture(gl.TEXTURE_2D, current_texture); + gl.drawArrays(gl.TRIANGLES, 0, vert_index / 4); + vert_index = 0; +} + +function push_quad(dx: number, dy: number, dw: number, dh: number, u0: number, v0: number, u1: number, v1: number) { + const x2 = dx + dw; + const y2 = dy + dh; + + const v = [ + dx, + dy, + u0, + v0, + x2, + dy, + u1, + v0, + dx, + y2, + u0, + v1, + + x2, + dy, + u1, + v0, + x2, + y2, + u1, + v1, + dx, + y2, + u0, + v1, + ]; + + for (let i = 0; i < v.length; i += 1) { + vertex_data[vert_index++] = v[i]; + } +} + +function init_font() { + const c = document.createElement("canvas"); + c.width = 512; + c.height = 512; + + const ctx = c.getContext("2d")!; + ctx.fillStyle = "white"; + ctx.font = glyph_size + "px m6x11"; + + let x = 0; + let y = glyph_size; + + for (let i = 32; i < 127; i++) { + const ch = String.fromCharCode(i); + + ctx.fillText(ch, x, y); + + glyphs[ch] = { + u0: x / 512, + v0: (y - glyph_size) / 512, + u1: (x + glyph_size) / 512, + v1: y / 512, + }; + + x += glyph_size; + + if (x > 512 - glyph_size) { + x = 0; + y += glyph_size; + } + } + + font_texture = gl.createTexture()!; + + gl.bindTexture(gl.TEXTURE_2D, font_texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); +} + +export function resize_canvas() { + const width = self.innerWidth; + const height = self.innerHeight; + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + canvas.style.width = canvas.width + "px"; + canvas.style.height = canvas.height + "px"; + + gl.viewport(0, 0, width, height); + + gl.useProgram(program); + gl.uniform2f(resolution_loc, width, height); + } +} diff --git a/client/systems/clickable_system.ts b/client/systems/clickable_system.ts index a7bb80d..4575104 100644 --- a/client/systems/clickable_system.ts +++ b/client/systems/clickable_system.ts @@ -6,6 +6,7 @@ import { ClickableSprite } from "../components/clickable.ts"; import { point_inside_rec } from "$/common/utils.ts"; import { InputManager } from "../input_manager.ts"; import { Camera, screen_to_world } from "../components/camera.ts"; +import { canvas } from "../renderer.ts"; export class ClickableSystem extends System { update(world: ClientWorld, _delta: number): void { @@ -18,8 +19,8 @@ export class ClickableSystem extends System { mouse.x, mouse.y, camera, - world.canvas.width, - world.canvas.height, + canvas.width, + canvas.height, ); for (const entity of world.get_entities()) { diff --git a/client/systems/render_system.ts b/client/systems/render_system.ts index 93f04a0..73976f6 100644 --- a/client/systems/render_system.ts +++ b/client/systems/render_system.ts @@ -11,17 +11,11 @@ import { render_dimension } from "./rendering/dimension.ts"; import { render_player_inventory } from "./rendering/player.ts"; export class RenderSystem extends System { - ctx: CanvasRenderingContext2D; - - constructor(ctx: CanvasRenderingContext2D) { + constructor() { super(); - this.ctx = ctx; } update(world: World, _delta: number): void { - this.ctx.save(); - this.ctx.setTransform(1, 0, 0, 1, 0, 0); - const camera_entity = world.get_entities().values().find((e) => e.get(Camera)?.active); const camera = camera_entity?.get(Camera); @@ -30,45 +24,29 @@ export class RenderSystem extends System { } for (const entity of world.get_entities()) { - this.ctx.save(); - this.ctx.translate( - Math.floor(this.ctx.canvas.width / 2), - Math.floor(this.ctx.canvas.height / 2), - ); - - this.ctx.scale(camera.zoom, camera.zoom); - - this.ctx.translate( - -Math.floor(camera.x), - -Math.floor(camera.y), - ); - const position = entity.get(Position); const sprite = entity.get(Sprite); if (sprite && position) { - render_sprite(this.ctx, sprite, position); + render_sprite(sprite, position); } const animated_sprite = entity.get(AnimatedSprite); if (animated_sprite && position) { - render_animated_sprite(this.ctx, animated_sprite, position); + render_animated_sprite(animated_sprite, position); } const dimension = entity.get(Dimension); if (dimension) { - render_dimension(this.ctx, dimension, camera); + render_dimension(dimension, camera); } - // Ui - this.ctx.restore(); - const player_inventory = entity.get(PlayerInventory); if (player_inventory && player_inventory.is_open) { - render_player_inventory(this.ctx, player_inventory); + render_player_inventory(player_inventory); } } } diff --git a/client/systems/rendering/dimension.ts b/client/systems/rendering/dimension.ts index 8b1f54e..8b28d4b 100644 --- a/client/systems/rendering/dimension.ts +++ b/client/systems/rendering/dimension.ts @@ -2,25 +2,24 @@ import { get_sprite_region } from "$/common/utils.ts"; import { Dimension } from "$/client/components/dimension.ts"; import { TEXTURE_SIZE, TILE_SIZE } from "$/common/constants.ts"; import { Camera, world_to_screen } from "$/client/components/camera.ts"; +import { canvas, draw_texture_region } from "../../renderer.ts"; -export function render_dimension(ctx: CanvasRenderingContext2D, tilemap: Dimension, camera: Camera) { - ctx.save(); - +export function render_dimension(tilemap: Dimension, camera: Camera) { for (const tile of tilemap.tiles) { const region = get_sprite_region(tile.id); const x = tile.x * TILE_SIZE; const y = tile.y * TILE_SIZE; - const screen_position = world_to_screen(x, y, camera, ctx.canvas.width, ctx.canvas.height); + const screen_position = world_to_screen(x, y, camera, canvas.width, canvas.height); if ( - screen_position.x + TILE_SIZE < 0 || screen_position.x > ctx.canvas.width || - screen_position.y + TILE_SIZE < 0 || screen_position.y > ctx.canvas.height + screen_position.x + TILE_SIZE < 0 || screen_position.x > canvas.width || + screen_position.y + TILE_SIZE < 0 || screen_position.y > canvas.height ) { continue; } - ctx.drawImage( + draw_texture_region( tilemap.image, region.x * TEXTURE_SIZE, region.y * TEXTURE_SIZE, @@ -32,6 +31,4 @@ export function render_dimension(ctx: CanvasRenderingContext2D, tilemap: Dimensi TILE_SIZE, ); } - - ctx.restore(); } diff --git a/client/systems/rendering/player.ts b/client/systems/rendering/player.ts index 3bf166a..ce985a9 100644 --- a/client/systems/rendering/player.ts +++ b/client/systems/rendering/player.ts @@ -3,14 +3,14 @@ import { AssetManager } from "$/client/assets.ts"; import { PlayerInventory } from "$/client/components/inventory.ts"; import { InputManager } from "$/client/input_manager.ts"; import { draw_item, draw_nine_slice } from "./render_utils.ts"; +import { Texture } from "$/client/renderer.ts"; -export function render_player_inventory(ctx: CanvasRenderingContext2D, player_inventory: PlayerInventory) { +export function render_player_inventory(player_inventory: PlayerInventory) { const PADDING = 10; - const ui = AssetManager.instance.get("bworld:ui"); + const ui = AssetManager.instance.get("bworld:ui"); const layout = player_inventory.layout.slots; draw_nine_slice( - ctx, ui, 160, 0, @@ -30,7 +30,6 @@ export function render_player_inventory(ctx: CanvasRenderingContext2D, player_in const x = slot.x + PADDING; const y = slot.y + PADDING; draw_nine_slice( - ctx, ui, player_inventory.hovering_slot === index ? 19 * 16 : 160 + 32, player_inventory.hovering_slot === index ? 16 : 0, @@ -51,12 +50,12 @@ export function render_player_inventory(ctx: CanvasRenderingContext2D, player_in const y = slot.y + PADDING; const item = player_inventory.container.get_item(index); if (item) { - draw_item(ctx, item, x, y); + draw_item(item, x, y); } } if (player_inventory.holding_item) { const mouse = InputManager.get_mouse_position(); - draw_item(ctx, player_inventory.holding_item, mouse.x, mouse.y); + draw_item(player_inventory.holding_item, mouse.x, mouse.y); } } diff --git a/client/systems/rendering/render_utils.ts b/client/systems/rendering/render_utils.ts index 84ca9f8..47ea457 100644 --- a/client/systems/rendering/render_utils.ts +++ b/client/systems/rendering/render_utils.ts @@ -2,11 +2,10 @@ import { SLOT_SIZE } from "$/common/constants.ts"; import { get_sprite_region } from "$/common/utils.ts"; import { AssetManager } from "$/client/assets.ts"; import { ItemStack } from "$/client/components/inventory.ts"; -import { draw_text } from "$/client/text_rendering.ts"; +import { draw_text, draw_texture_region, Texture } from "$/client/renderer.ts"; export function draw_nine_slice( - ctx: CanvasRenderingContext2D, - image: HTMLImageElement, + image: Texture, source_x: number, source_y: number, source_width: number, @@ -27,13 +26,13 @@ export function draw_nine_slice( const dest_center_height = height - top - bottom; // top left - ctx.drawImage(image, source_x, source_y, left, top, x, y, left, top); + draw_texture_region(image, source_x, source_y, left, top, x, y, left, top); // top right - ctx.drawImage(image, source_x + source_width - right, source_y, right, top, x + width - right, y, right, top); + draw_texture_region(image, source_x + source_width - right, source_y, right, top, x + width - right, y, right, top); // bottom left - ctx.drawImage( + draw_texture_region( image, source_x, source_y + source_height - bottom, @@ -46,7 +45,7 @@ export function draw_nine_slice( ); // bottom right - ctx.drawImage( + draw_texture_region( image, source_x + source_width - right, source_y + source_height - bottom, @@ -59,10 +58,10 @@ export function draw_nine_slice( ); // top - ctx.drawImage(image, source_x + left, source_y, center_width, top, x + left, y, dest_center_width, top); + draw_texture_region(image, source_x + left, source_y, center_width, top, x + left, y, dest_center_width, top); // bottom - ctx.drawImage( + draw_texture_region( image, source_x + left, source_y + source_height - bottom, @@ -75,10 +74,10 @@ export function draw_nine_slice( ); // left - ctx.drawImage(image, source_x, source_y + top, left, center_height, x, y + top, left, dest_center_height); + draw_texture_region(image, source_x, source_y + top, left, center_height, x, y + top, left, dest_center_height); // right - ctx.drawImage( + draw_texture_region( image, source_x + source_width - right, source_y + top, @@ -91,7 +90,7 @@ export function draw_nine_slice( ); // center ! - ctx.drawImage( + draw_texture_region( image, source_x + left, source_y + top, @@ -104,9 +103,9 @@ export function draw_nine_slice( ); } -export function draw_item(ctx: CanvasRenderingContext2D, item: ItemStack, x: number, y: number) { +export function draw_item(item: ItemStack, x: number, y: number) { const sprite_region = get_sprite_region(item.type_id); - ctx.drawImage( + draw_texture_region( AssetManager.instance.get("bworld:textures"), sprite_region.x * 16, sprite_region.y * 16, @@ -119,24 +118,22 @@ export function draw_item(ctx: CanvasRenderingContext2D, item: ItemStack, x: num ); if (item.max_amount !== 1) { draw_text( - ctx, String(item.amount), x + SLOT_SIZE + 2 - 4, y + SLOT_SIZE + 2 - 4, - 2, - "#3f3f3f", - "bottom", - "right", + //2, + //"#3f3f3f", + //"bottom", + //"right", ); draw_text( - ctx, String(item.amount), x + SLOT_SIZE - 4, y + SLOT_SIZE - 4, - 2, - "white", - "bottom", - "right", + //2, + //"white", + //"bottom", + //"right", ); } } diff --git a/client/systems/rendering/sprites.ts b/client/systems/rendering/sprites.ts index 3853018..c7c80c8 100644 --- a/client/systems/rendering/sprites.ts +++ b/client/systems/rendering/sprites.ts @@ -1,65 +1,46 @@ import { Position } from "$/common/components/position.ts"; import { AnimatedSprite, Sprite } from "$/client/components/sprite.ts"; +import { draw_texture_region } from "../../renderer.ts"; +import { AssetManager } from "../../assets.ts"; -export function render_sprite(ctx: CanvasRenderingContext2D, sprite: Sprite, position: Position) { - ctx.save(); - - ctx.translate( - Math.floor(sprite.flip_x ? position.x + sprite.width : position.x), - Math.floor(sprite.flip_y ? position.y + sprite.height : position.y), - ); - ctx.scale( - sprite.flip_x ? -1 : 1, - sprite.flip_y ? -1 : 1, - ); - - ctx.drawImage( +export function render_sprite(sprite: Sprite, position: Position) { + draw_texture_region( sprite.image, sprite.source_x, sprite.source_y, sprite.source_width, sprite.source_height, - 0, - 0, + position.x, + position.y, sprite.width, sprite.height, + sprite.flip_x, + sprite.flip_y, ); - ctx.restore(); } export function render_animated_sprite( - ctx: CanvasRenderingContext2D, animated_sprite: AnimatedSprite, position: Position, ) { - ctx.save(); - const current_animation = animated_sprite.states[animated_sprite.current_state]; if (!current_animation) { console.error(`Missing animation for state ${animated_sprite.current_state}`); return; } - ctx.translate( - Math.floor(animated_sprite.flip_x ? position.x + animated_sprite.width : position.x), - Math.floor(animated_sprite.flip_y ? position.y + animated_sprite.height : position.y), - ); - - ctx.scale( - animated_sprite.flip_x ? -1 : 1, - animated_sprite.flip_y ? -1 : 1, - ); - - ctx.drawImage( - animated_sprite.image, + draw_texture_region( + AssetManager.instance.get("bworld:player"), current_animation.source_x[animated_sprite.animation_frame], current_animation.source_y[animated_sprite.animation_frame], current_animation.source_width, current_animation.source_height, - 0, - 0, + position.x, + position.y, animated_sprite.width, animated_sprite.height, + animated_sprite.flip_x, + animated_sprite.flip_y, ); animated_sprite.timer += 1; @@ -71,6 +52,4 @@ export function render_animated_sprite( animated_sprite.animation_frame = 0; } } - - ctx.restore(); } diff --git a/client/systems/ui_render_system.ts b/client/systems/ui_render_system.ts index 909d662..49f2c3b 100644 --- a/client/systems/ui_render_system.ts +++ b/client/systems/ui_render_system.ts @@ -2,9 +2,9 @@ import { System } from "$/common/ecs/mod.ts"; import { Position } from "$/common/components/position.ts"; import { ClientWorld } from "$/client/client_world.ts"; import { UIButton } from "$/client/components/ui_components.ts"; -import { draw_text, measure_text } from "../text_rendering.ts"; import { draw_nine_slice } from "./rendering/render_utils.ts"; import { AssetManager } from "../assets.ts"; +import { draw_text, Texture } from "../renderer.ts"; const SCALE = 4; @@ -12,18 +12,7 @@ export class UIRenderSystem implements System { constructor() {} update(world: ClientWorld, _delta: number) { - const ctx = world.ctx; - - ctx.save(); - ctx.resetTransform(); - - ctx.scale(SCALE, SCALE); - - // dark background - ctx.fillStyle = "rgba(0,0,0,0.6)"; - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - const ui = AssetManager.instance.get("bworld:ui"); + const ui = AssetManager.instance.get("bworld:ui"); for (const entity of world.get_entities()) { const position = entity.get(Position); @@ -31,7 +20,6 @@ export class UIRenderSystem implements System { if (position && button) { draw_nine_slice( - ctx, ui, 16 * (button.hovered ? 14 : 11), 16 * (button.hovered ? 1 : 0), @@ -46,18 +34,16 @@ export class UIRenderSystem implements System { button.width / SCALE, button.height / SCALE, ); - const measure = measure_text(ctx, button.text, 1.25); + // const measure = measure_text(ctx, button.text, 1.25); draw_text( - ctx, button.text, - (position.x / SCALE) + (button.width / SCALE / 2) - (measure / 2), + (position.x / SCALE) + (button.width / SCALE / 2) - (100 / 2), position.y / SCALE + (button.height / SCALE / 2), - 1.5, - "white", - "middle", + //1.5, + //"white", + //"middle", ); } } - ctx.restore(); } } diff --git a/client/text_rendering.ts b/client/text_rendering.ts deleted file mode 100644 index b3ccfb6..0000000 --- a/client/text_rendering.ts +++ /dev/null @@ -1,23 +0,0 @@ -const TEXT_SCALE = 8; - -export function measure_text(ctx: CanvasRenderingContext2D, text: string, scale = 1): number { - ctx.font = `${scale * TEXT_SCALE}px m6x11`; - return ctx.measureText(text).width; -} - -export function draw_text( - ctx: CanvasRenderingContext2D, - text: string, - x: number, - y: number, - scale = 1, - color = "white", - baseline: CanvasTextBaseline = "top", - align: CanvasTextAlign = "left", -) { - ctx.textBaseline = baseline; - ctx.textAlign = align; - ctx.font = `${scale * TEXT_SCALE}px m6x11`; - ctx.fillStyle = color; - ctx.fillText(text, Math.floor(x), Math.floor(y)); -}