make games crisp

This commit is contained in:
Chris Wanstrath 2025-09-28 13:48:09 -07:00
parent ebb1afebdb
commit 64e76502e8
5 changed files with 76 additions and 35 deletions

19
app/src/css/game.css Normal file
View File

@ -0,0 +1,19 @@
canvas {
display: block;
}
canvas:focus {
outline: none;
}
.game {
background-color: white;
z-index: 10;
}
.game.active {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@ -71,10 +71,6 @@
color: var(--c64-dark-blue); color: var(--c64-dark-blue);
} }
canvas:focus {
outline: none;
}
a { a {
color: var(--cyan); color: var(--cyan);
} }
@ -88,9 +84,10 @@ body {
font-family: var(--font-family); font-family: var(--font-family);
margin: 0; margin: 0;
height: 100%; height: 100%;
/* black bars */
background: var(--c64-light-blue); background: var(--c64-light-blue);
color: var(--c64-light-blue); color: var(--c64-light-blue);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -148,15 +148,3 @@
#scrollback .output { #scrollback .output {
white-space: pre-wrap; white-space: pre-wrap;
} }
/* games */
.game {
background-color: white;
}
.game.active {
position: absolute;
top: 0;
left: 0;
}

View File

@ -9,12 +9,10 @@ export const Layout: FC = async ({ children, title }) => (
<link href="/css/reset.css" rel="stylesheet" /> <link href="/css/reset.css" rel="stylesheet" />
<link href="/css/main.css" rel="stylesheet" /> <link href="/css/main.css" rel="stylesheet" />
<link href="/css/game.css" rel="stylesheet" />
<script type="importmap" dangerouslySetInnerHTML={{ <script type="importmap" dangerouslySetInnerHTML={{ __html: `{ "imports": { "@/": "/" } }` }} />
__html: `{ "imports": { "@/": "/" } }`
}} />
<script src="/js/main.js" type="module" async></script> <script src="/js/main.js" type="module" async></script>
</head> </head>
<body data-mode="tall"> <body data-mode="tall">
<main> <main>

View File

@ -1,17 +1,20 @@
import type { Message, InputState } from "../shared/types.js" import type { Message, InputState } from "../shared/types.js"
import { GameContext } from "../shared/game.js" import { GameContext } from "../shared/game.js"
import { focusInput } from "./focus.js" import { focusInput } from "./focus.js"
import { $ } from "./dom.js" import { $$, scrollback } from "./dom.js"
import { randomId } from "../shared/utils.js" import { randomId } from "../shared/utils.js"
import { setStatus, addOutput } from "./scrollback.js" import { setStatus, addOutput } from "./scrollback.js"
import { browserCommands } from "./commands.js" import { browserCommands } from "./commands.js"
const FPS = 30 const FPS = 30
const HEIGHT = 540
const WIDTH = 980
type Game = { init?: () => void, update?: (delta: number, input: InputState) => void, draw?: (ctx: GameContext) => void }
let oldMode = "cinema" let oldMode = "cinema"
let running = false let running = false
let canvas: HTMLCanvasElement let canvas: HTMLCanvasElement
type Game = { init?: () => void, update?: (delta: number, input: InputState) => void, draw?: (ctx: GameContext) => void }
const pressedStack = new Set<string>() const pressedStack = new Set<string>()
let pressed: InputState = { let pressed: InputState = {
@ -35,34 +38,41 @@ export async function handleGameStart(msg: Message) {
return return
} }
const canvasId = randomId()
addOutput(msgId, { html: `<canvas id="${canvasId}" class="game active" height="540" width="960" tabindex="0"></canvas>` })
if (document.body.dataset.mode === "tall") { if (document.body.dataset.mode === "tall") {
browserCommands.mode?.() browserCommands.mode?.()
oldMode = "tall" oldMode = "tall"
} }
canvas = $(canvasId) as HTMLCanvasElement canvas = createCanvas()
canvas.focus() canvas.focus()
setStatus(msgId, "ok") setStatus(msgId, "ok")
canvas.addEventListener("keydown", handleKeydown) canvas.addEventListener("keydown", handleKeydown)
canvas.addEventListener("keyup", handleKeyup) canvas.addEventListener("keyup", handleKeyup)
window.addEventListener("resize", resizeCanvas)
resizeCanvas()
gameLoop(new GameContext(canvas.getContext("2d")!), game) gameLoop(new GameContext(canvas.getContext("2d")!), game)
} }
function createCanvas(): HTMLCanvasElement {
const canvas = $$("canvas.game.active") as HTMLCanvasElement
canvas.id = randomId()
canvas.height = HEIGHT
canvas.width = WIDTH
canvas.tabIndex = 0
const main = document.querySelector("main")
main?.parentNode?.insertBefore(canvas, main)
return canvas
}
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
pressedStack.add(e.key)
e.preventDefault() e.preventDefault()
if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) { if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) {
running = false endGame()
if (oldMode === "tall") browserCommands.mode?.()
canvas.classList.remove("active")
canvas.style.height = canvas.height / 2 + "px"
canvas.style.width = canvas.width / 2 + "px"
focusInput()
} else { } else {
pressedStack.add(e.key)
pressed.key = e.key pressed.key = e.key
pressed.ctrl = e.ctrlKey pressed.ctrl = e.ctrlKey
pressed.shift = e.shiftKey pressed.shift = e.shiftKey
@ -80,6 +90,19 @@ function handleKeyup(e: KeyboardEvent) {
} }
} }
function resizeCanvas() {
const scale = Math.min(
window.innerWidth / 960,
window.innerHeight / 540
)
canvas.width = 960 * scale
canvas.height = 540 * scale
const ctx = canvas.getContext("2d")!
ctx.setTransform(scale, 0, 0, scale, 0, 0)
}
function gameLoop(ctx: GameContext, game: Game) { function gameLoop(ctx: GameContext, game: Game) {
running = true running = true
let last = 0 let last = 0
@ -100,3 +123,19 @@ function gameLoop(ctx: GameContext, game: Game) {
requestAnimationFrame(loop) requestAnimationFrame(loop)
} }
function endGame() {
running = false
if (oldMode === "tall") browserCommands.mode?.()
canvas.classList.remove("active")
canvas.style.height = HEIGHT / 2 + "px"
canvas.style.width = WIDTH / 2 + "px"
const output = $$("li.output")
output.append(canvas)
scrollback.append(output)
focusInput()
}