Files
OpenFrontIO/src/client/graphics/TransformHandler.ts
T
2025-03-08 10:44:27 -08:00

245 lines
7.5 KiB
TypeScript

import { colord } from "colord";
import { EventBus } from "../../core/EventBus";
import { Cell, Game, Player } from "../../core/game/Game";
import {
calculateBoundingBox,
calculateBoundingBoxCenter,
} from "../../core/Util";
import { ZoomEvent, DragEvent, CenterCameraEvent } from "../InputHandler";
import { GoToPlayerEvent, GoToUnitEvent } from "./layers/Leaderboard";
import { placeName } from "./NameBoxCalculator";
import { GameView } from "../../core/game/GameView";
export class TransformHandler {
public scale: number = 1.8;
private offsetX: number = -350;
private offsetY: number = -200;
private target: Cell;
private intervalID = null;
private changed = false;
constructor(
private game: GameView,
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
) {
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));
this.eventBus.on(DragEvent, (e) => this.onMove(e));
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e));
this.eventBus.on(GoToUnitEvent, (e) => this.onGoToUnit(e));
this.eventBus.on(CenterCameraEvent, () => this.centerCamera());
}
boundingRect(): DOMRect {
return this.canvas.getBoundingClientRect();
}
width(): number {
return this.boundingRect().width;
}
hasChanged(): boolean {
return this.changed;
}
handleTransform(context: CanvasRenderingContext2D) {
// Disable image smoothing for pixelated effect
context.imageSmoothingEnabled = false;
// Apply zoom and pan
context.setTransform(
this.scale,
0,
0,
this.scale,
this.game.width() / 2 - this.offsetX * this.scale,
this.game.height() / 2 - this.offsetY * this.scale,
);
this.changed = false;
}
worldToScreenCoordinates(cell: Cell): { x: number; y: number } {
// Step 1: Convert from Cell coordinates to game coordinates
// (reverse of Math.floor operation - we'll use the exact values)
const gameX = cell.x;
const gameY = cell.y;
// Step 2: Reverse the game center offset calculation
// Original: gameX = centerX + this.game.width() / 2
// Therefore: centerX = gameX - this.game.width() / 2
const centerX = gameX - this.game.width() / 2;
const centerY = gameY - this.game.height() / 2;
// Step 3: Reverse the world point calculation
// Original: centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX
// Therefore: canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2
const canvasX =
(centerX - this.offsetX) * this.scale + this.game.width() / 2;
const canvasY =
(centerY - this.offsetY) * this.scale + this.game.height() / 2;
// Step 4: Convert canvas coordinates back to screen coordinates
const canvasRect = this.boundingRect();
const screenX = canvasX + canvasRect.left;
const screenY = canvasY + canvasRect.top;
return { x: screenX, y: screenY };
}
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
const canvasRect = this.boundingRect();
const canvasX = screenX - canvasRect.left;
const canvasY = screenY - canvasRect.top;
// Calculate the world point we want to zoom towards
const centerX =
(canvasX - this.game.width() / 2) / this.scale + this.offsetX;
const centerY =
(canvasY - this.game.height() / 2) / this.scale + this.offsetY;
const gameX = centerX + this.game.width() / 2;
const gameY = centerY + this.game.height() / 2;
return new Cell(Math.floor(gameX), Math.floor(gameY));
}
screenBoundingRect(): [Cell, Cell] {
const LeftX = -this.game.width() / 2 / this.scale + this.offsetX;
const TopY = -this.game.height() / 2 / this.scale + this.offsetY;
const gameLeftX = LeftX + this.game.width() / 2;
const gameTopY = TopY + this.game.height() / 2;
const rightX =
(screen.width - this.game.width() / 2) / this.scale + this.offsetX;
const rightY =
(screen.height - this.game.height() / 2) / this.scale + this.offsetY;
const gameRightX = rightX + this.game.width() / 2;
const gameBottomY = rightY + this.game.height() / 2;
return [
new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)),
new Cell(Math.floor(gameRightX), Math.floor(gameBottomY)),
];
}
isOnScreen(cell: Cell): boolean {
const [topLeft, bottomRight] = this.screenBoundingRect();
return (
cell.x > topLeft.x &&
cell.x < bottomRight.x &&
cell.y > topLeft.y &&
cell.y < bottomRight.y
);
}
screenCenter(): { screenX: number; screenY: number } {
const [upperLeft, bottomRight] = this.screenBoundingRect();
return {
screenX: upperLeft.x + Math.floor((bottomRight.x - upperLeft.x) / 2),
screenY: upperLeft.y + Math.floor((bottomRight.y - upperLeft.y) / 2),
};
}
onGoToPlayer(event: GoToPlayerEvent) {
this.clearTarget();
this.target = new Cell(
event.player.nameLocation().x,
event.player.nameLocation().y,
);
this.intervalID = setInterval(() => this.goTo(), 1);
}
onGoToUnit(event: GoToUnitEvent) {
this.clearTarget();
this.target = new Cell(
this.game.x(event.unit.lastTile()),
this.game.y(event.unit.lastTile()),
);
this.intervalID = setInterval(() => this.goTo(), 1);
}
centerCamera() {
this.clearTarget();
const player = this.game.myPlayer();
if (!player || !player.nameLocation()) return;
this.target = new Cell(player.nameLocation().x, player.nameLocation().y);
this.intervalID = setInterval(() => this.goTo(), 1);
}
private goTo() {
const { screenX, screenY } = this.screenCenter();
const screenMapCenter = new Cell(screenX, screenY);
if (
this.game.manhattanDist(
this.game.ref(screenX, screenY),
this.game.ref(this.target.x, this.target.y),
) < 2
) {
this.clearTarget();
return;
}
const dX = Math.abs(screenMapCenter.x - this.target.x);
if (dX > 2) {
const offsetDx = Math.max(1, Math.floor(dX / 25));
if (screenMapCenter.x > this.target.x) {
this.offsetX -= offsetDx;
} else {
this.offsetX += offsetDx;
}
}
const dY = Math.abs(screenMapCenter.y - this.target.y);
if (dY > 2) {
const offsetDy = Math.max(1, Math.floor(dY / 25));
if (screenMapCenter.y > this.target.y) {
this.offsetY -= offsetDy;
} else {
this.offsetY += offsetDy;
}
}
this.changed = true;
}
onZoom(event: ZoomEvent) {
this.clearTarget();
const oldScale = this.scale;
const zoomFactor = 1 + event.delta / 600;
this.scale /= zoomFactor;
// Clamp the scale to prevent extreme zooming
this.scale = Math.max(0.2, Math.min(20, this.scale));
const canvasRect = this.boundingRect();
const canvasX = event.x - canvasRect.left;
const canvasY = event.y - canvasRect.top;
// Calculate the world point we want to zoom towards
const zoomPointX =
(canvasX - this.game.width() / 2) / oldScale + this.offsetX;
const zoomPointY =
(canvasY - this.game.height() / 2) / oldScale + this.offsetY;
// Adjust the offset
this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale;
this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale;
this.changed = true;
}
onMove(event: DragEvent) {
this.clearTarget();
this.offsetX -= event.deltaX / this.scale;
this.offsetY -= event.deltaY / this.scale;
this.changed = true;
}
private clearTarget() {
if (this.intervalID != null) {
clearInterval(this.intervalID);
this.intervalID = null;
}
this.target = null;
}
}