{"author_link":"\/users\/j3m","author_name":"j3m","author_uid":"j3m","comments":[],"epoch":1777196685,"event":"LD59","format":"md","ldjam_node_id":432113,"likes":7,"metadata":{"p_key":"96739","p_author":"j3m","p_authorkey":"1395577","p_urlkey":"312385","p_title":"Making a web game engine when time is measured in hours","p_cat":"LDJam ","p_event":"LD59","p_time":"1777196685","p_likes":"7","p_comments":"0","p_status":"WAYBACK","us_key":"1395577","us_name":"j3m","us_username":"j3m","event_start":"1776470400","event_key":"113","event_name":"Ludum Dare 59"},"node":{"_collation":{"body_sanitizer":"TextUtils::SanitizeHTML via existing importer","event":"LD59","removed_author":false},"_superparent":424249,"_trust":1,"author":395577,"body":"For LD59, I knew we were going to make a [web game](https:\/\/ldjam.com\/events\/ludum-dare\/59\/many-wearing-rapiers-are-afraid-of-goosequils). I don't like frameworks and engines, so I knew I'd be making it from scratch. So once we settled on our game idea Saturday morning, I put an HTML5 canvas on the easel, installed a Typescript compiler and got to work.\n\nOur game was going to be just a matter of clicking on buttons to navigate between screens. Mouse-only input. It could probably have been as a regular HTML page, no canvas, but I anticipated some basic animation which seemed like it would be at least equally annoying to code in CSS. So I opted for the direct control of a canvas.\n\n# The asset loader\n\nI knew from experience that the easiest way to get images into your canvas game is by embedding them into the HTML page as `<img \/>` tags, and then drawing them with `drawImage()`. The problem is, those img tags need to be finished loading by the time the call to `drawImage()` happens. So you always need some kind of \"loader\" code to wait for them to load before you actually run anything.\n\nWhat we want is a little object called, say, the *image manager* that we can tell about all the images in the game. It'll keep track of which ones are loaded, and only let the game start once they're all ready. Usage will look something like this:\n\n```typescript\nconst images = new ImageManager();\nfor (let image of document.querySelectorAll(\"img\"))\n{\n    images.add(image as HTMLImageElement);\n}\nimages.onComplete = startTheGame; \/\/ set a callback\nimages.finish();                  \/\/ that's all the images!\n```\n\nThe logic needed in the class is pretty obvious, and mine looks like this:\n\n```typescript\nclass ImageManager\n{\n    nLoading : number = 0;\n    complete : boolean = false;\n    onComplete : Function;\n\n    add(image : HTMLImageElement)\n    {\n        this[image.id] = image;\n\n        if (image.complete)\n        {\n            return;\n        }\n\n        this.nLoading += 1;\n\n        image.addEventListener(\"load\", this.onLoad.bind(this));\n    }\n\n    finish()\n    {\n        if (this.nLoading == 0)\n        {\n            this.complete = true;\n            this.onComplete();\n        }\n    }\n\n    onLoad()\n    {\n        this.nLoading -= 1;\n\n        if (this.nLoading == 0)\n        {\n            this.complete = true;\n            this.onComplete();\n        }\n    }\n}\n```\n\nThe one extra little flourish in there is the line\n\n```typescript\nthis[image.id] = image;\n```\n\nThis just provides a convenient way for me to get at my assets in the game code. All I have to do is give my image tags an ID, like `<img id=\"princess\" src=\"princess.png\" \/>`, and I can get them from code via `images[\"princess\"]`.\n\n# Dimensions and coordinates\n\nOne thing you gotta know about HTML canvases is that they have two sets of dimensions. They have the dimensions of the actual HTML element, and then they have the resolution of the canvas itself. The latter is set using the `width` and `height` properties on the canvas. So for example:\n\n```html\n<style>\n    canvas { width: 800px; height: 600px; }\n<\/style>\n<canvas width=\"100\" height=\"100\"><\/canvas>\n```\n\nIf I draw a 100x100 test image on this canvas, it won't sit in the top left corner of an 800x600 canvas, as you might expect. It will fill the 100x100 canvas that you've requested in HTML, and CSS will then scale that to 800x600.\n\nTo avoid this, you need some code like this, to make the resolution of the canvas match its true on screen dimensions at all times:\n\n```typescript\nfunction onResize()\n{\n    canvas.width = canvas.clientWidth;\n    canvas.height = canvas.clientHeight;\n}\n\naddEventListener(\"resize\", onResize);\n```\n\nThe game is going to be 16x9 aspect ratio. So if the user resizes the window, the canvas needs to scale to be the largest 16x9 box it can within that window. Probably the simplest way to do this by far is just to use CSS, I did something much, *much* more complicated. I told CSS to make the canvas fill the screen, and then computed the largest 16x9 box I could within that. So now my onResize looks like this:\n\n```typescript\nfunction onResize()\n{\n    canvas.width = canvas.clientWidth;\n    canvas.height = canvas.clientHeight;\n\n    let widthRatio = canvas.width \/ 1920;\n    let heightRatio = canvas.height \/ 1080;\n    let ratio = Math.min(1, widthRatio, heightRatio);\n\n    width = Math.floor(ratio * 1920);\n    height = Math.floor(ratio * 1080);\n    \n    x0 = (canvas.width - width) \/ 2;\n    y0 = (canvas.height - height) \/ 2;\n}\n```\n\nThis code is setting the global variables `x0, y0, width, height` which define a little box within the canvas that the actual graphics code will draw into. I think I did this because I imagined I might want to draw a border around the game if there was extra room, sort of like what the Super Gameboy used to do. But it adds a bunch of complexity. Not really worth it.\n\nThe simplest thing for a short game jam is to set up your code so that your actual graphics and gameplay code can simply assume that the drawing area starts at (0, 0) and is always exactly 1920 by 1080 pixels. So if you want to draw something at, say, the middle bottom of the screen, you can just hard code the coordinates 1920 \/ 2 and 1080. To make this possible, you need a piece of code responsible for resizing the canvas, like above, and then a layer wrapping the drawing API that basically converts the gameplay code's \"virtual coordinates\" into physical coordinates.\n\n# The \"engine\"\n\nNow we need to get our event loop set up, kind of like if we were making a Raylib or SDL game. So the top level code of our game looks like this:\n\n```typescript\nfunction onResize()\n{\n    \/\/ same as before\n}\n\nfunction onMouse(event : MouseEvent)\n{\n    let mouse = {};\n    mouse.clicked = false;\n    if (event !== undefined)\n    {\n        if (event.type == \"mousemove\")\n        {\n            let canvasX = event.clientX;\n            let canvasY = event.clientY;\n            mouse.x = (canvasX - x0) * (1920 \/ width); \/\/ convert physical coordinates into \"virtual\" coordinates for the gameplay code\n            mouse.y = (canvasY - y0) * (1080 \/ height);\n        }\n\n        if (event.type == \"click\")\n        {\n            mouse.clicked = true;\n        }\n    }\n\n    gameSpecificOnMouse(mouse);\n}\n\nfunction render()\n{\n    gameSpecificRender();\n}\n\nlet x0, y0, width, height : number; \/\/ globals to store canvas dimensions\n\naddEventListener(\"resize\", onResize);\naddEventListener(\"mousemove\", onMouse);\naddEventListener(\"click\", onMouse);\nsetInterval(render, 1000 \/ 60);\n```\n\n# Substructure: individual screens\n\nSince the game involves moving between several different individual \"screens\", it's nice to be able able to code each screen with its own, separate render and mouse event handler functions. So we introduce some types to represent this:\n\n```\nenum ScreenType { LETTER_SCREEN, PERSON_SCREEN, DEATH_SCREEN, FAILURE_SCREEN, VICTORY_SCREEN, TITLE_SCREEN, TUTORIAL_SCREEN }\n\ninterface Screen\n{\n    enter() : void\n    onMouseEvent(event : Mouse) : ScreenType | null\n    render(context : CanvasRenderingContext2D, x, y, w, h : number) : ScreenType | null\n}\n```\n\nThe purpose of the `enter()` function is that it'll get called when we first enter the screen. After that, `render()` will get called once per frame. Notice that the screen's `onMouseEvent` and `render` functions return a ScreenType enum. This lets the gameplay logic in those functions decide whether or not it's time to go to a different screen, and report that back to the top-level engine code:\n\n```typescript\n\/\/ in the top level render function\n\nlet nextScreen = screen.render(artist, x0, y0, width, height);\n\nif (nextScreen != null)\n{\n    switch (nextScreen)\n    {\n        case ScreenType.LETTER_SCREEN:\n            screen = letterScreen;\n            break;\n\n        case ScreenType.PERSON_SCREEN:\n            screen = personScreen;\n            break;\n\n        \/\/ etc\n    }\n\n    screen.enter()\n}\n```\n\nAnd that's basically it!","comments":0,"created":"2026-04-26T09:43:46Z","files":[],"files-timestamp":0,"id":432113,"love":7,"love-timestamp":"2026-04-26T16:52:24Z","meta":[],"modified":"2026-04-26T16:52:24Z","name":"Making a web game engine when time is measured in hours","node-timestamp":"2026-04-26T09:44:45Z","parent":426900,"parents":[1,5,9,424249,426900],"path":"\/events\/ludum-dare\/59\/many-wearing-rapiers-are-afraid-of-goosequils\/making-a-web-game-engine-when-time-is-measured-in-hours","published":"2026-04-26T09:44:45Z","scope":"public","slug":"making-a-web-game-engine-when-time-is-measured-in-hours","subsubtype":"","subtype":"","type":"post","version":1372546},"node_metadata":{"n_key":"432113","n_urlkey":"312385","n_parent":"426900","n_path":"\/events\/ludum-dare\/59\/many-wearing-rapiers-are-afraid-of-goosequils\/making-a-web-game-engine-when-time-is-measured-in-hours","n_slug":"making-a-web-game-engine-when-ti","n_type":"post","n_subtype":"","n_subsubtype":"","n_author":"395577","n_created":"1777196626","n_modified":"1777222344","n_version":"1372546","n_status":"WAYBACK"},"source_url":"https:\/\/ldjam.com\/events\/ludum-dare\/59\/many-wearing-rapiers-are-afraid-of-goosequils\/making-a-web-game-engine-when-time-is-measured-in-hours","text":"For LD59, I knew we were going to make a [web game](https:\/\/ldjam.com\/events\/ludum-dare\/59\/many-wearing-rapiers-are-afraid-of-goosequils). I don't like frameworks and engines, so I knew I'd be making it from scratch. So once we settled on our game idea Saturday morning, I put an HTML5 canvas on the easel, installed a Typescript compiler and got to work.\n\nOur game was going to be just a matter of clicking on buttons to navigate between screens. Mouse-only input. It could probably have been as a regular HTML page, no canvas, but I anticipated some basic animation which seemed like it would be at least equally annoying to code in CSS. So I opted for the direct control of a canvas.\n\n# The asset loader\n\nI knew from experience that the easiest way to get images into your canvas game is by embedding them into the HTML page as `<img \/>` tags, and then drawing them with `drawImage()`. The problem is, those img tags need to be finished loading by the time the call to `drawImage()` happens. So you always need some kind of \"loader\" code to wait for them to load before you actually run anything.\n\nWhat we want is a little object called, say, the *image manager* that we can tell about all the images in the game. It'll keep track of which ones are loaded, and only let the game start once they're all ready. Usage will look something like this:\n\n```typescript\nconst images = new ImageManager();\nfor (let image of document.querySelectorAll(\"img\"))\n{\n    images.add(image as HTMLImageElement);\n}\nimages.onComplete = startTheGame; \/\/ set a callback\nimages.finish();                  \/\/ that's all the images!\n```\n\nThe logic needed in the class is pretty obvious, and mine looks like this:\n\n```typescript\nclass ImageManager\n{\n    nLoading : number = 0;\n    complete : boolean = false;\n    onComplete : Function;\n\n    add(image : HTMLImageElement)\n    {\n        this[image.id] = image;\n\n        if (image.complete)\n        {\n            return;\n        }\n\n        this.nLoading += 1;\n\n        image.addEventListener(\"load\", this.onLoad.bind(this));\n    }\n\n    finish()\n    {\n        if (this.nLoading == 0)\n        {\n            this.complete = true;\n            this.onComplete();\n        }\n    }\n\n    onLoad()\n    {\n        this.nLoading -= 1;\n\n        if (this.nLoading == 0)\n        {\n            this.complete = true;\n            this.onComplete();\n        }\n    }\n}\n```\n\nThe one extra little flourish in there is the line\n\n```typescript\nthis[image.id] = image;\n```\n\nThis just provides a convenient way for me to get at my assets in the game code. All I have to do is give my image tags an ID, like `<img id=\"princess\" src=\"princess.png\" \/>`, and I can get them from code via `images[\"princess\"]`.\n\n# Dimensions and coordinates\n\nOne thing you gotta know about HTML canvases is that they have two sets of dimensions. They have the dimensions of the actual HTML element, and then they have the resolution of the canvas itself. The latter is set using the `width` and `height` properties on the canvas. So for example:\n\n```html\n<style>\n    canvas { width: 800px; height: 600px; }\n<\/style>\n<canvas width=\"100\" height=\"100\"><\/canvas>\n```\n\nIf I draw a 100x100 test image on this canvas, it won't sit in the top left corner of an 800x600 canvas, as you might expect. It will fill the 100x100 canvas that you've requested in HTML, and CSS will then scale that to 800x600.\n\nTo avoid this, you need some code like this, to make the resolution of the canvas match its true on screen dimensions at all times:\n\n```typescript\nfunction onResize()\n{\n    canvas.width = canvas.clientWidth;\n    canvas.height = canvas.clientHeight;\n}\n\naddEventListener(\"resize\", onResize);\n```\n\nThe game is going to be 16x9 aspect ratio. So if the user resizes the window, the canvas needs to scale to be the largest 16x9 box it can within that window. Probably the simplest way to do this by far is just to use CSS, I did something much, *much* more complicated. I told CSS to make the canvas fill the screen, and then computed the largest 16x9 box I could within that. So now my onResize looks like this:\n\n```typescript\nfunction onResize()\n{\n    canvas.width = canvas.clientWidth;\n    canvas.height = canvas.clientHeight;\n\n    let widthRatio = canvas.width \/ 1920;\n    let heightRatio = canvas.height \/ 1080;\n    let ratio = Math.min(1, widthRatio, heightRatio);\n\n    width = Math.floor(ratio * 1920);\n    height = Math.floor(ratio * 1080);\n    \n    x0 = (canvas.width - width) \/ 2;\n    y0 = (canvas.height - height) \/ 2;\n}\n```\n\nThis code is setting the global variables `x0, y0, width, height` which define a little box within the canvas that the actual graphics code will draw into. I think I did this because I imagined I might want to draw a border around the game if there was extra room, sort of like what the Super Gameboy used to do. But it adds a bunch of complexity. Not really worth it.\n\nThe simplest thing for a short game jam is to set up your code so that your actual graphics and gameplay code can simply assume that the drawing area starts at (0, 0) and is always exactly 1920 by 1080 pixels. So if you want to draw something at, say, the middle bottom of the screen, you can just hard code the coordinates 1920 \/ 2 and 1080. To make this possible, you need a piece of code responsible for resizing the canvas, like above, and then a layer wrapping the drawing API that basically converts the gameplay code's \"virtual coordinates\" into physical coordinates.\n\n# The \"engine\"\n\nNow we need to get our event loop set up, kind of like if we were making a Raylib or SDL game. So the top level code of our game looks like this:\n\n```typescript\nfunction onResize()\n{\n    \/\/ same as before\n}\n\nfunction onMouse(event : MouseEvent)\n{\n    let mouse = {};\n    mouse.clicked = false;\n    if (event !== undefined)\n    {\n        if (event.type == \"mousemove\")\n        {\n            let canvasX = event.clientX;\n            let canvasY = event.clientY;\n            mouse.x = (canvasX - x0) * (1920 \/ width); \/\/ convert physical coordinates into \"virtual\" coordinates for the gameplay code\n            mouse.y = (canvasY - y0) * (1080 \/ height);\n        }\n\n        if (event.type == \"click\")\n        {\n            mouse.clicked = true;\n        }\n    }\n\n    gameSpecificOnMouse(mouse);\n}\n\nfunction render()\n{\n    gameSpecificRender();\n}\n\nlet x0, y0, width, height : number; \/\/ globals to store canvas dimensions\n\naddEventListener(\"resize\", onResize);\naddEventListener(\"mousemove\", onMouse);\naddEventListener(\"click\", onMouse);\nsetInterval(render, 1000 \/ 60);\n```\n\n# Substructure: individual screens\n\nSince the game involves moving between several different individual \"screens\", it's nice to be able able to code each screen with its own, separate render and mouse event handler functions. So we introduce some types to represent this:\n\n```\nenum ScreenType { LETTER_SCREEN, PERSON_SCREEN, DEATH_SCREEN, FAILURE_SCREEN, VICTORY_SCREEN, TITLE_SCREEN, TUTORIAL_SCREEN }\n\ninterface Screen\n{\n    enter() : void\n    onMouseEvent(event : Mouse) : ScreenType | null\n    render(context : CanvasRenderingContext2D, x, y, w, h : number) : ScreenType | null\n}\n```\n\nThe purpose of the `enter()` function is that it'll get called when we first enter the screen. After that, `render()` will get called once per frame. Notice that the screen's `onMouseEvent` and `render` functions return a ScreenType enum. This lets the gameplay logic in those functions decide whether or not it's time to go to a different screen, and report that back to the top-level engine code:\n\n```typescript\n\/\/ in the top level render function\n\nlet nextScreen = screen.render(artist, x0, y0, width, height);\n\nif (nextScreen != null)\n{\n    switch (nextScreen)\n    {\n        case ScreenType.LETTER_SCREEN:\n            screen = letterScreen;\n            break;\n\n        case ScreenType.PERSON_SCREEN:\n            screen = personScreen;\n            break;\n\n        \/\/ etc\n    }\n\n    screen.enter()\n}\n```\n\nAnd that's basically it!","title":"Making a web game engine when time is measured in hours","wayback_source":[]}