Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ So your change should look like:
`obj.pos.x += 1` work again (#1109) - @ErikGXDev
- Fixed `isKeyDown` and `isButtonDown` getting stuck on game loosing focus
(#1101) - @Stanko
- Fixed touch coordinates in aspect-fit fullscreen with fixed canvas size and
letterbox disabled (#1106) - @imaginarny
- Fixed incorrect resolution and scale of the debug inspect mode (#1106) -
@imaginarny

## [4000.0.0-alpha.27.1] - 2026-05-12

Expand Down
1 change: 1 addition & 0 deletions scripts/dev/views/game.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<html>

<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
<%= name %>
</title>
Expand Down
41 changes: 8 additions & 33 deletions src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,6 @@ export const initApp = (
const state = initAppState(opt);
parseButtonBindings(state);
if (opt.fixedUpdateMode) setFixedSpeed(opt.fixedUpdateMode);
updateCanvasScale();

function updateCanvasScale() {
const pd = opt.pixelDensity || 1;
state.canvasScaleX = state.canvas.width / pd
/ state.canvas.offsetWidth;
state.canvasScaleY = state.canvas.height / pd
/ state.canvas.offsetHeight;
}

function dt() {
return state.dt * state.timeScale;
Expand Down Expand Up @@ -1010,8 +1001,6 @@ export const initApp = (
queueReleaseHeldInputs();
}

const pd = opt.pixelDensity || 1;

canvasEvents.blur = () => {
releaseHeldInputsOnFocusLoss();
};
Expand All @@ -1026,27 +1015,6 @@ export const initApp = (
const mousePos = canvasToViewport(new Vec2(e.offsetX, e.offsetY));
const mouseDeltaPos = new Vec2(e.movementX, e.movementY);

if (!opt.letterbox && isFullscreen()) {
const cw = state.canvas.width / pd;
const ch = state.canvas.height / pd;
const ww = window.innerWidth;
const wh = window.innerHeight;
const rw = ww / wh;
const rc = cw / ch;
if (rw > rc) {
const ratio = wh / ch;
const offset = (ww - (cw * ratio)) / 2;
mousePos.x = map(e.offsetX - offset, 0, cw * ratio, 0, cw);
mousePos.y = map(e.offsetY, 0, ch * ratio, 0, ch);
}
else {
const ratio = ww / cw;
const offset = (wh - (ch * ratio)) / 2;
mousePos.x = map(e.offsetX, 0, cw * ratio, 0, cw);
mousePos.y = map(e.offsetY - offset, 0, ch * ratio, 0, ch);
}
}

state.lastInputDevice = "mouse";
state.events.onOnce("input", () => {
state.isMouseMoved = true;
Expand Down Expand Up @@ -1342,6 +1310,14 @@ export const initApp = (
);
}

const updateCanvasScale = () => {
const pd = opt.pixelDensity || 1;
state.canvasScaleX = state.canvas.width / pd
/ state.canvas.offsetWidth;
state.canvasScaleY = state.canvas.height / pd
/ state.canvas.offsetHeight;
};

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target !== state.canvas) continue;
Expand All @@ -1351,7 +1327,6 @@ export const initApp = (
) return;
state.lastWidth = state.canvas.offsetWidth;
state.lastHeight = state.canvas.offsetHeight;
updateCanvasScale();
state.events.onOnce("input", () => {
state.events.trigger("resize");
});
Expand Down
12 changes: 4 additions & 8 deletions src/app/appEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,27 @@ export function initAppEvents() {
});

_k.app.onResize(() => {
const fixedSize = _k.globalOpt.width && _k.globalOpt.height;
if (fixedSize && !_k.globalOpt.letterbox) {
return;
}

_k.canvas.width = _k.canvas.offsetWidth * _k.gfx.pixelDensity;
_k.canvas.height = _k.canvas.offsetHeight * _k.gfx.pixelDensity;
_k.app.updateCanvasScale();

updateViewport();

if (!fixedSize) {
if (!_k.globalOpt.width || !_k.globalOpt.height) {
_k.gfx.frameBuffer.free();
_k.gfx.frameBuffer = new FrameBuffer(
_k.gfx.ggl,
_k.gfx.ggl.gl.drawingBufferWidth,
_k.gfx.ggl.gl.drawingBufferHeight,
);

_k.gfx.width = _k.gfx.ggl.gl.drawingBufferWidth
/ _k.gfx.pixelDensity
/ _k.globalOpt.scale;
_k.gfx.height = _k.gfx.ggl.gl.drawingBufferHeight
/ _k.gfx.pixelDensity
/ _k.globalOpt.scale;
}

updateViewport();
});

if (_k.globalOpt.debug !== false) {
Expand Down
2 changes: 0 additions & 2 deletions src/core/engineLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import type { Debug } from "../debug/debug";
import { SystemPhase } from "../ecs/systems/systems";
import type { Game } from "../game/game";
import { drawDebug } from "../gfx/draw/drawDebug";
import { drawFrame, transformFrame } from "../gfx/draw/drawFrame";
import { drawLoadScreen } from "../gfx/draw/drawLoadingScreen";
import { updateViewport } from "../gfx/viewport";
Expand Down Expand Up @@ -111,7 +110,6 @@ export function startEngineLoop(
}

drawFrame();
if (gopt.debug !== false) drawDebug();

for (const sys of game.systemsByEvent[SystemPhase.AfterDraw]) {
sys.run();
Expand Down
16 changes: 14 additions & 2 deletions src/core/frameRendering.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { App } from "../app/app";
import { BG_GRID_SIZE } from "../constants/general";
import type { Game } from "../game/game";
import { drawDebug } from "../gfx/draw/drawDebug";
import { drawTexture } from "../gfx/draw/drawTexture";
import { drawUnscaled } from "../gfx/draw/drawUnscaled";
import { drawUVQuad } from "../gfx/draw/drawUVQuad";
import type { AppGfxCtx } from "../gfx/gfxApp";
import { flush, height, width } from "../gfx/stack";
import { Quad } from "../math/math";
import { Vec2 } from "../math/Vec2";
import { _k } from "../shared";

/**
* A frame renderer.
Expand Down Expand Up @@ -63,8 +65,6 @@ export const createFrameRenderer = (
}

function frameEnd() {
// TODO: don't render debug UI with framebuffer
// TODO: polish framebuffer rendering / sizing issues
flush();
gfx.lastDrawCalls = gfx.renderer.numDraws;
gfx.frameBuffer.unbind();
Expand Down Expand Up @@ -96,6 +96,18 @@ export const createFrameRenderer = (
flush();
gfx.width = ow;
gfx.height = oh;

if (_k.globalOpt.debug !== false) {
gfx.gl.viewport(
gfx.viewport.x * pixelDensity,
gfx.viewport.y * pixelDensity,
gfx.viewport.width * pixelDensity,
gfx.viewport.height * pixelDensity,
);

drawDebug();
flush();
}
}

function fixedUpdateFrame() {
Expand Down
40 changes: 18 additions & 22 deletions src/gfx/viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ rendering the viewport
- Canvas size: The CSS size of the canvas element

- Buffer size: The quantity of pixels that are rendered by WebGL. It varies
depending of the
depending on the canvas size and pixel density

- Desired Size: The desired size is the size the user defines for keeping an
aspect ratio
- Desired size: The size the user defines to keep an aspect ratio

- Viewport size: The final rendered size
- Viewport size: The final rendered size, sub-rect of the canvas size

We update the canvas before run this, you should check initEvents.ts
in onResize method.
We update the canvas before we run this, you should check appEvents.ts
onResize method.
*/

export function updateViewport() {
Expand All @@ -37,29 +36,26 @@ export function updateViewport() {
let viewportWidth = canvasWidth;
let viewportHeight = canvasHeight;

if (_k.globalOpt.letterbox) {
if (!desiredWidth || !desiredHeight) {
if (!desiredWidth || !desiredHeight) {
if (_k.globalOpt.letterbox) {
throw new Error(
"Letterboxing requires width and height defined.",
);
}

}
else {
const canvasAspectRatio = canvasWidth / canvasHeight;
const disairedAspectRatio = desiredWidth / desiredHeight;
const desiredAspectRatio = desiredWidth / desiredHeight;

// In letterbox, we scale one width/height for keep aspect ratio,
// depending of what side is larger
if (canvasAspectRatio > disairedAspectRatio) {
const scaledWidth = canvasHeight * disairedAspectRatio;

x = (canvasWidth - scaledWidth) / 2;
viewportWidth = scaledWidth;
// We scale either width or height to keep aspect ratio, depending
// on what side is larger, creating a letterbox
if (canvasAspectRatio > desiredAspectRatio) {
viewportWidth = canvasHeight * desiredAspectRatio;
x = (canvasWidth - viewportWidth) / 2;
}
else {
const scaledHeight = canvasWidth / disairedAspectRatio;

viewportHeight = scaledHeight;
y = (canvasHeight - scaledHeight) / 2;
viewportHeight = canvasWidth / desiredAspectRatio;
y = (canvasHeight - viewportHeight) / 2;
}
}

Expand All @@ -68,7 +64,7 @@ export function updateViewport() {
y: y,
width: viewportWidth,
height: viewportHeight,
scale: (_k.gfx.viewport.width + _k.gfx.viewport.height)
scale: (viewportWidth + viewportHeight)
/ (_k.gfx.width + _k.gfx.height),
};

Expand Down
28 changes: 28 additions & 0 deletions tests/playtests/mouseSizeAligned.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// To test touch, enable touch simulation in dev tools responsive design mode
// then press and hold mouse down and move around
// ensure that red dot follows correctly in fullscreen (F)

kaplay({ width: 600, height: 600 });

canvas.parentElement.style.display = "flex";
canvas.style.cssText += `
margin: auto;
width: min(${width()}px, 100%, calc(100svh * ${width()} / ${height()}));
height: min(${height()}px, 100%, calc(100svw * ${height()} / ${width()}));
`;

const redDot = add([
anchor("center"),
circle(3),
color(RED),
pos(),
area(),
]);

onMouseMove(pos => {
redDot.pos = toWorld(pos);
});

onKeyPress("f", () => {
setFullscreen(!isFullscreen());
});
21 changes: 21 additions & 0 deletions tests/playtests/mouseSizeFixed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// To test touch, enable touch simulation in dev tools responsive design mode
// then press and hold mouse down and move around
// ensure that red dot follows correctly in fullscreen (F)

kaplay({ width: 600, height: 600 });

const redDot = add([
anchor("center"),
circle(3),
color(RED),
pos(),
area(),
]);

onMouseMove(pos => {
redDot.pos = toWorld(pos);
});

onKeyPress("f", () => {
setFullscreen(!isFullscreen());
});
Loading