Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { GamepadSource } from "./gamepad-source.js";
import { MicrophoneInput } from "./microphone-input.js";
import { SpeechOutput } from "./speech-output.js";
import { MouseJoystickSource } from "./mouse-joystick-source.js";
import { calculateMouseCoordinates } from "./mouse-coordinates.js";
import { getFilterForMode } from "./canvas.js";
import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON, isSameModel } from "./snapshot.js";
import { isBemSnapshot, parseBemSnapshot } from "./bem-snapshot.js";
Expand Down Expand Up @@ -536,10 +537,8 @@ const cubMonitor = document.getElementById("cub-monitor");
function onCubMouseEvent(evt) {
audioHandler.tryResume();
if (document.activeElement !== document.body) document.activeElement.blur();
const cubRect = cubMonitor.getBoundingClientRect();
const screenRect = screenCanvas.getBoundingClientRect();
const x = (evt.offsetX - cubRect.left + screenRect.left) / screenCanvas.offsetWidth;
const y = (evt.offsetY - cubRect.top + screenRect.top) / screenCanvas.offsetHeight;
const { x, y } = calculateMouseCoordinates(evt, screenRect);

// Handle touchscreen
if (processor.touchScreen) processor.touchScreen.onMouse(x, y, evt.buttons);
Expand Down
16 changes: 16 additions & 0 deletions src/mouse-coordinates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Calculate normalized mouse coordinates relative to the screen canvas.
*
* Uses clientX/clientY against the canvas bounding rect so that coordinates
* are correct even when the canvas is inset within a monitor image (e.g.
* display filters with non-zero canvasLeft/canvasTop).
*
* @param {MouseEvent} evt - The mouse event
* @param {DOMRect} screenRect - Bounding client rect of the screen canvas
* @returns {{x: number, y: number}} Normalized coordinates in [0, 1]
*/
export function calculateMouseCoordinates(evt, screenRect) {
const x = (evt.clientX - screenRect.left) / screenRect.width;
const y = (evt.clientY - screenRect.top) / screenRect.height;
return { x, y };
}
65 changes: 65 additions & 0 deletions tests/unit/test-mouse-coordinates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { calculateMouseCoordinates } from "../../src/mouse-coordinates.js";

/**
* Test for mouse coordinate calculation logic.
*
* Issue #631: The mouse coordinate calculation in onCubMouseEvent mixed
* evt.offsetX/offsetY (relative to event target) with container bounding-rect
* positions. When the canvas is inset within the monitor image (e.g. display
* filters with non-zero canvasLeft/canvasTop), this produced incorrect coordinates.
*
* The correct approach is to use evt.clientX/clientY against the screen canvas
* rect directly.
*/

describe("Mouse coordinate calculation", () => {
it("should calculate correct coordinates when canvas is inset (display filters)", () => {
// Scenario: Canvas is inset within monitor image (e.g. CRT filter)
// Monitor container is at viewport position (100, 50)
// Canvas is inset by (20, 30) from the monitor edge
// User clicks at absolute viewport position (150, 100)

const evt = {
clientX: 150,
clientY: 100,
};

const screenRect = {
left: 120, // cubRect.left (100) + 20 (canvas inset)
top: 80, // cubRect.top (50) + 30 (canvas inset)
width: 160, // Canvas is smaller than monitor
height: 140,
};

const result = calculateMouseCoordinates(evt, screenRect);

// click at (150, 100) on canvas at (120, 80) with size (160, 140)
// x = (150-120)/160 = 0.1875, y = (100-80)/140 ≈ 0.1429
expect(result.x).toBeCloseTo(0.1875, 4);
expect(result.y).toBeCloseTo(0.142857, 4);
});

it("should calculate correct coordinates when canvas has no inset (no display filters)", () => {
// Scenario: Canvas fills the entire monitor (no display filter)
// Monitor and canvas have the same position and size

const evt = {
clientX: 150,
clientY: 100,
};

const screenRect = {
left: 100,
top: 50,
width: 200,
height: 200,
};

const result = calculateMouseCoordinates(evt, screenRect);

// x = (150-100)/200 = 0.25, y = (100-50)/200 = 0.25
expect(result.x).toBeCloseTo(0.25, 4);
expect(result.y).toBeCloseTo(0.25, 4);
});
});