perf: cache static walls, fast-path sprite draws, cache canvas rect (0.9.1, v57)

This commit is contained in:
Quintine committed 2026-08-13 13:11:16 +10:00
1 parent 6cc8132046
commit d60baa6b29
8 files changed
+84 -47

No files matched your search

+22 -22
View File
@@ -6,32 +6,32 @@
<meta name="theme-color" content="#0a0305">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22><text y=%2252%22 x=%2250%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-size=%2252%22>🔪</text></svg>">
<title>MEATSLICER</title>
<link rel="stylesheet" href="css/style.css?v=56">
<link rel="stylesheet" href="css/style.css?v=57">
</head>
<body>
<div id="wrap">
<canvas id="game" width="960" height="640"></canvas>
</div>
<script src="js/utils.js?v=56"></script>
<script src="js/state.js?v=56"></script>
<script src="js/input.js?v=56"></script>
<script src="js/sfx.js?v=56"></script>
<script src="js/sfxbank.js?v=56"></script>
<script src="js/audio.js?v=56"></script>
<script src="js/sprites.js?v=56"></script>
<script src="js/particles.js?v=56"></script>
<script src="js/weapons.js?v=56"></script>
<script src="js/bullets.js?v=56"></script>
<script src="js/pickups.js?v=56"></script>
<script src="js/items.js?v=56"></script>
<script src="js/perks.js?v=56"></script>
<script src="js/enemies.js?v=56"></script>
<script src="js/bosses.js?v=56"></script>
<script src="js/rooms.js?v=56"></script>
<script src="js/player.js?v=56"></script>
<script src="js/hud.js?v=56"></script>
<script src="js/help.js?v=56"></script>
<script src="js/debug.js?v=56"></script>
<script src="js/main.js?v=56"></script>
<script src="js/utils.js?v=57"></script>
<script src="js/state.js?v=57"></script>
<script src="js/input.js?v=57"></script>
<script src="js/sfx.js?v=57"></script>
<script src="js/sfxbank.js?v=57"></script>
<script src="js/audio.js?v=57"></script>
<script src="js/sprites.js?v=57"></script>
<script src="js/particles.js?v=57"></script>
<script src="js/weapons.js?v=57"></script>
<script src="js/bullets.js?v=57"></script>
<script src="js/pickups.js?v=57"></script>
<script src="js/items.js?v=57"></script>
<script src="js/perks.js?v=57"></script>
<script src="js/enemies.js?v=57"></script>
<script src="js/bosses.js?v=57"></script>
<script src="js/rooms.js?v=57"></script>
<script src="js/player.js?v=57"></script>
<script src="js/hud.js?v=57"></script>
<script src="js/help.js?v=57"></script>
<script src="js/debug.js?v=57"></script>
<script src="js/main.js?v=57"></script>
</body>
</html>
+5 -1
View File
@@ -9,6 +9,10 @@ const Input = {
anyKey: false,
};
let _rect = null;
Input.updateRect = function () { _rect = canvas ? canvas.getBoundingClientRect() : null; };
function initInput(canvas) {
window.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
@@ -21,7 +25,7 @@ function initInput(canvas) {
window.addEventListener('blur', () => { Input.keys = {}; Input.mdown = false; });
function setMouse(e) {
const r = canvas.getBoundingClientRect();
const r = _rect || (_rect = canvas.getBoundingClientRect());
Input.mx = (e.clientX - r.left) * (W / r.width);
Input.my = (e.clientY - r.top) * (H / r.height);
}
+1
View File
@@ -105,6 +105,7 @@ function fitCanvas() {
const k = Math.min(window.innerWidth / W, window.innerHeight / H) * 0.97;
canvas.style.width = Math.floor(W * k) + 'px';
canvas.style.height = Math.floor(H * k) + 'px';
if (Input.updateRect) Input.updateRect();
}
function resetRun() {
+45 -18
View File
@@ -94,6 +94,8 @@ function enterRoom(gx, gy) {
G.boss = null;
G.roomLayer = null;
G.roomLayerKey = '';
G.wallLayer = null;
G.wallLayerKey = '';
G.roomDamaged = false;
G.roomEnterT = G.time;
G.entryFresh = false;
@@ -435,8 +437,7 @@ function drawRoom(ctx) {
drawWalls(ctx, r);
}
function drawWalls(ctx, r) {
const locked = roomLocked();
function drawWallBands(target, r) {
const a = roomBounds(r);
const theme = ROOM_THEMES[r.theme] || ROOM_THEMES.abattoir;
const wallPool = theme.wall;
@@ -444,43 +445,69 @@ function drawWalls(ctx, r) {
const top = a.y0 - WALL, bottom = a.y1 + WALL;
function clipBand(dir) {
ctx.beginPath();
target.beginPath();
if (dir === 'n' || dir === 's') {
const y = dir === 'n' ? top : a.y1;
if (r.doors[dir]) {
ctx.rect(left, y, a.cx - DOOR_HALF - left, WALL);
ctx.rect(a.cx + DOOR_HALF, y, right - a.cx - DOOR_HALF, WALL);
} else ctx.rect(left, y, right - left, WALL);
target.rect(left, y, a.cx - DOOR_HALF - left, WALL);
target.rect(a.cx + DOOR_HALF, y, right - a.cx - DOOR_HALF, WALL);
} else target.rect(left, y, right - left, WALL);
} else {
const x = dir === 'w' ? left : a.x1;
if (r.doors[dir]) {
ctx.rect(x, top, WALL, a.cy - DOOR_HALF - top);
ctx.rect(x, a.cy + DOOR_HALF, WALL, bottom - a.cy - DOOR_HALF);
} else ctx.rect(x, top, WALL, bottom - top);
target.rect(x, top, WALL, a.cy - DOOR_HALF - top);
target.rect(x, a.cy + DOOR_HALF, WALL, bottom - a.cy - DOOR_HALF);
} else target.rect(x, top, WALL, bottom - top);
}
ctx.clip();
target.clip();
}
for (const dir of ['n', 's']) {
ctx.save(); clipBand(dir);
target.save(); clipBand(dir);
const y = dir === 'n' ? top + TILE / 2 : a.y1 + TILE / 2;
for (let x = left; x < right; x += TILE) {
const h = hashTile(Math.floor(x / TILE) + r.gx * 17, (dir === 'n' ? -1 : 11) + r.gy * 19);
Sprites.draw(ctx, wallPool[h % wallPool.length], x + TILE / 2, y, 0, TILE);
Sprites.draw(target, wallPool[h % wallPool.length], x + TILE / 2, y, 0, TILE);
}
ctx.restore();
target.restore();
}
for (const dir of ['w', 'e']) {
ctx.save(); clipBand(dir);
target.save(); clipBand(dir);
const x = dir === 'w' ? left + TILE / 2 : a.x1 + TILE / 2;
for (let y = top; y < bottom; y += TILE) {
const h = hashTile((dir === 'w' ? -1 : 16) + r.gx * 17, Math.floor(y / TILE) + r.gy * 19);
Sprites.draw(ctx, wallPool[h % wallPool.length], x, y + TILE / 2, Math.PI / 2, TILE);
Sprites.draw(target, wallPool[h % wallPool.length], x, y + TILE / 2, Math.PI / 2, TILE);
}
ctx.restore();
target.restore();
}
target.strokeStyle = 'rgba(0,0,0,0.5)'; target.lineWidth = 3;
target.strokeRect(a.x0, a.y0, a.w, a.h);
}
function drawWalls(ctx, r) {
const locked = roomLocked();
const a = roomBounds(r);
// static wall tiles are cached; door sprites are dynamic per frame
const canCache = typeof document !== 'undefined' && document.createElement && G.imagesLoaded;
if (canCache) {
const key = r.gx + ',' + r.gy + ':' + G.floor + ':' + r.shape + ':' + r.theme + ':' + hdScale();
if (!G.wallLayer || G.wallLayerKey !== key) {
const rs = hdScale();
const layer = document.createElement('canvas');
layer.width = (a.x1 + WALL) * rs;
layer.height = (a.y1 + WALL) * rs;
const lctx = layer.getContext('2d');
lctx.setTransform(rs, 0, 0, rs, 0, 0);
lctx.imageSmoothingEnabled = !G.hdRemaster;
drawWallBands(lctx, r);
G.wallLayer = layer;
G.wallLayerKey = key;
}
ctx.drawImage(G.wallLayer, 0, 0, G.wallLayer.width / hdScale(), G.wallLayer.height / hdScale());
} else {
drawWallBands(ctx, r);
}
ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 3;
ctx.strokeRect(a.x0, a.y0, a.w, a.h);
for (const [dir, has] of Object.entries(r.doors)) {
if (!has) continue;
+1 -1
View File
@@ -111,7 +111,7 @@ const SfxBank = {
if (this.buffers.has(name) || this.pending.has(name) || this.failed.has(name)) return;
const file = this.FILES[name];
if (!file || !this.ctx || typeof fetch !== 'function') return;
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=56')
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=57')
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); })
.then(bytes => this.ctx.decodeAudioData(bytes))
.then(buffer => this.buffers.set(name, this.trim(buffer)))
+7 -2
View File
@@ -88,10 +88,10 @@ const Sprites = {
const finish = () => { if (--pending === 0) G.imagesLoaded = true; };
img.onload = () => { this.imgs[name] = img; if (typeof img.decode === 'function') img.decode().then(finish, finish); else finish(); };
img.onerror = () => {
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=56'; return; }
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=57'; return; }
finish();
};
img.src = hd ? 'assets/hd/' + name + '.webp?v=56' : 'assets/' + name + '.png?v=56';
img.src = hd ? 'assets/hd/' + name + '.webp?v=57' : 'assets/' + name + '.png?v=57';
}
},
@@ -161,6 +161,11 @@ const Sprites = {
draw(ctx, name, x, y, rot, targetW, flash, alpha, scaleX, scaleY) {
const img = this.imgs[name];
const tw = targetW || 32;
if (img && !rot && scaleX === undefined && scaleY === undefined && alpha === undefined && !flash) {
const dh = img.height * (tw / img.width);
ctx.drawImage(img, x - tw / 2, y - dh / 2, tw, dh);
return;
}
ctx.save();
ctx.translate(x, y);
if (rot) ctx.rotate(rot);
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meatslicer",
"productName": "MeatSlicer",
"version": "0.9.0",
"version": "0.9.1",
"private": true,
"description": "A Butcher's Descent - clear, harvest, descend.",
"main": "desktop/main.js",
+2 -2
View File
@@ -216,7 +216,7 @@ check('HD loader loads all images (fallbacks)', ctx.G.imagesLoaded === true);
let hdFallbackOk = 0;
for (const name of ctx.SPRITE_MANIFEST) {
const urls = attemptedUrls[name];
const ok = urls && urls.length === 2 && urls[0] === 'assets/hd/' + name + '.webp?v=56' && urls[1] === 'assets/' + name + '.png?v=56';
const ok = urls && urls.length === 2 && urls[0] === 'assets/hd/' + name + '.webp?v=57' && urls[1] === 'assets/' + name + '.png?v=57';
if (ok) hdFallbackOk++;
}
check('HD loader tries WebP then PNG per sprite', hdFallbackOk === ctx.SPRITE_MANIFEST.length);
@@ -241,7 +241,7 @@ check('SD loader loads all images', ctx.G.imagesLoaded === true);
let sdUrlOk = 0;
for (const name of ctx.SPRITE_MANIFEST) {
const urls = attemptedUrls[name];
const ok = urls && urls.length === 1 && urls[0] === 'assets/' + name + '.png?v=56';
const ok = urls && urls.length === 1 && urls[0] === 'assets/' + name + '.png?v=57';
if (ok) sdUrlOk++;
}
check('SD loader requests one PNG per sprite', sdUrlOk === ctx.SPRITE_MANIFEST.length);