Issue #5: Mouse handling — zoom-to-mouse + middle-click pan #10
@@ -1,14 +1,14 @@
|
|||||||
import Phaser from 'phaser'
|
import Phaser from 'phaser'
|
||||||
import { TILE_SIZE } from '../config'
|
import { TILE_SIZE } from '../config'
|
||||||
|
|
||||||
const GRID_TILES = 50 // world size in tiles
|
const GRID_TILES = 50 // world size in tiles
|
||||||
const MIN_ZOOM = 0.25
|
const MIN_ZOOM = 0.25
|
||||||
const MAX_ZOOM = 4.0
|
const MAX_ZOOM = 4.0
|
||||||
const ZOOM_STEP = 0.1
|
const ZOOM_STEP = 0.1
|
||||||
const MARKER_EVERY = 5 // small crosshair every N tiles
|
const MARKER_EVERY = 5 // small crosshair every N tiles
|
||||||
const LABEL_EVERY = 10 // coordinate label every N tiles
|
const LABEL_EVERY = 10 // coordinate label every N tiles
|
||||||
const CAMERA_SPEED = 400 // px/s
|
const CAMERA_SPEED = 400 // px/s
|
||||||
const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots
|
const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First test scene: observes pure Phaser default zoom behavior.
|
* First test scene: observes pure Phaser default zoom behavior.
|
||||||
@@ -19,6 +19,9 @@ const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots
|
|||||||
*/
|
*/
|
||||||
export class ZoomTestScene extends Phaser.Scene {
|
export class ZoomTestScene extends Phaser.Scene {
|
||||||
private logText!: Phaser.GameObjects.Text
|
private logText!: Phaser.GameObjects.Text
|
||||||
|
private hudCamera!: Phaser.Cameras.Scene2D.Camera
|
||||||
|
private worldObjects: Phaser.GameObjects.GameObject[] = []
|
||||||
|
private hudObjects: Phaser.GameObjects.GameObject[] = []
|
||||||
private keys!: {
|
private keys!: {
|
||||||
up: Phaser.Input.Keyboard.Key
|
up: Phaser.Input.Keyboard.Key
|
||||||
down: Phaser.Input.Keyboard.Key
|
down: Phaser.Input.Keyboard.Key
|
||||||
@@ -43,19 +46,18 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
this.drawGrid()
|
this.drawGrid()
|
||||||
this.setupCamera()
|
this.setupCamera()
|
||||||
this.setupInput()
|
this.setupInput()
|
||||||
this.createOverlay()
|
this.createHUD()
|
||||||
|
this.setupCameras()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the static world grid into world space.
|
* Draws the static world grid into world space.
|
||||||
* - Faint tile lines on every tile boundary
|
* All objects are registered in worldObjects for HUD-camera exclusion.
|
||||||
* - Small green crosshairs every MARKER_EVERY tiles
|
|
||||||
* - Yellow labeled crosshairs every LABEL_EVERY tiles
|
|
||||||
* - Red world border
|
|
||||||
*/
|
*/
|
||||||
private drawGrid(): void {
|
private drawGrid(): void {
|
||||||
const worldPx = GRID_TILES * TILE_SIZE
|
const worldPx = GRID_TILES * TILE_SIZE
|
||||||
const g = this.add.graphics()
|
const g = this.add.graphics()
|
||||||
|
this.worldObjects.push(g)
|
||||||
|
|
||||||
// Background fill
|
// Background fill
|
||||||
g.fillStyle(0x111811)
|
g.fillStyle(0x111811)
|
||||||
@@ -90,12 +92,13 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
// Coordinate labels at LABEL_EVERY intersections
|
// Coordinate labels at LABEL_EVERY intersections
|
||||||
for (let tx = 0; tx <= GRID_TILES; tx += LABEL_EVERY) {
|
for (let tx = 0; tx <= GRID_TILES; tx += LABEL_EVERY) {
|
||||||
for (let ty = 0; ty <= GRID_TILES; ty += LABEL_EVERY) {
|
for (let ty = 0; ty <= GRID_TILES; ty += LABEL_EVERY) {
|
||||||
this.add.text(
|
const label = this.add.text(
|
||||||
tx * TILE_SIZE + 4,
|
tx * TILE_SIZE + 4,
|
||||||
ty * TILE_SIZE + 4,
|
ty * TILE_SIZE + 4,
|
||||||
`${tx},${ty}`,
|
`${tx},${ty}`,
|
||||||
{ fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' }
|
{ fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' }
|
||||||
).setDepth(1)
|
).setDepth(1)
|
||||||
|
this.worldObjects.push(label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,27 +144,26 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
_dx: number,
|
_dx: number,
|
||||||
dy: number
|
dy: number
|
||||||
) => {
|
) => {
|
||||||
const zoomBefore = cam.zoom
|
const zoomBefore = cam.zoom
|
||||||
const scrollXBefore = cam.scrollX
|
const scrollXBefore = cam.scrollX
|
||||||
const scrollYBefore = cam.scrollY
|
const scrollYBefore = cam.scrollY
|
||||||
|
|
||||||
const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||||
cam.setZoom(newZoom)
|
cam.setZoom(newZoom)
|
||||||
|
|
||||||
// Log after Phaser has applied the zoom (next microtask so values are updated)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.writeLog('zoom', {
|
this.writeLog('zoom', {
|
||||||
direction: dy > 0 ? 'out' : 'in',
|
direction: dy > 0 ? 'out' : 'in',
|
||||||
zoomBefore: +zoomBefore.toFixed(4),
|
zoomBefore: +zoomBefore.toFixed(4),
|
||||||
zoomAfter: +cam.zoom.toFixed(4),
|
zoomAfter: +cam.zoom.toFixed(4),
|
||||||
scrollX_before: +scrollXBefore.toFixed(2),
|
scrollX_before: +scrollXBefore.toFixed(2),
|
||||||
scrollY_before: +scrollYBefore.toFixed(2),
|
scrollY_before: +scrollYBefore.toFixed(2),
|
||||||
scrollX_after: +cam.scrollX.toFixed(2),
|
scrollX_after: +cam.scrollX.toFixed(2),
|
||||||
scrollY_after: +cam.scrollY.toFixed(2),
|
scrollY_after: +cam.scrollY.toFixed(2),
|
||||||
scrollX_delta: +(cam.scrollX - scrollXBefore).toFixed(2),
|
scrollX_delta: +(cam.scrollX - scrollXBefore).toFixed(2),
|
||||||
scrollY_delta: +(cam.scrollY - scrollYBefore).toFixed(2),
|
scrollY_delta: +(cam.scrollY - scrollYBefore).toFixed(2),
|
||||||
mouseScreen: { x: +ptr.x.toFixed(1), y: +ptr.y.toFixed(1) },
|
mouseScreen: { x: +ptr.x.toFixed(1), y: +ptr.y.toFixed(1) },
|
||||||
mouseWorld: { x: +ptr.worldX.toFixed(2), y: +ptr.worldY.toFixed(2) },
|
mouseWorld: { x: +ptr.worldX.toFixed(2), y: +ptr.worldY.toFixed(2) },
|
||||||
centerWorld_after: {
|
centerWorld_after: {
|
||||||
x: +(cam.scrollX + (cam.width / cam.zoom) / 2).toFixed(2),
|
x: +(cam.scrollX + (cam.width / cam.zoom) / 2).toFixed(2),
|
||||||
y: +(cam.scrollY + (cam.height / cam.zoom) / 2).toFixed(2),
|
y: +(cam.scrollY + (cam.height / cam.zoom) / 2).toFixed(2),
|
||||||
@@ -176,9 +178,25 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the fixed HUD text overlay (scroll factor 0 = screen space).
|
* Creates all HUD elements: log overlay and screen-center crosshair.
|
||||||
|
* All objects are registered in hudObjects for main-camera exclusion.
|
||||||
|
* Uses a dedicated HUD camera (zoom=1, fixed) so elements are never scaled.
|
||||||
*/
|
*/
|
||||||
private createOverlay(): void {
|
private createHUD(): void {
|
||||||
|
const w = this.scale.width
|
||||||
|
const h = this.scale.height
|
||||||
|
|
||||||
|
// Screen-center crosshair (red)
|
||||||
|
const cross = this.add.graphics()
|
||||||
|
const arm = 16
|
||||||
|
cross.lineStyle(1, 0xff2222, 0.9)
|
||||||
|
cross.lineBetween(w / 2 - arm, h / 2, w / 2 + arm, h / 2)
|
||||||
|
cross.lineBetween(w / 2, h / 2 - arm, w / 2, h / 2 + arm)
|
||||||
|
cross.fillStyle(0xff2222, 1.0)
|
||||||
|
cross.fillCircle(w / 2, h / 2, 2)
|
||||||
|
this.hudObjects.push(cross)
|
||||||
|
|
||||||
|
// Log text overlay
|
||||||
this.logText = this.add.text(10, 10, '', {
|
this.logText = this.add.text(10, 10, '', {
|
||||||
fontSize: '13px',
|
fontSize: '13px',
|
||||||
color: '#e8e8e8',
|
color: '#e8e8e8',
|
||||||
@@ -186,16 +204,27 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
padding: { x: 10, y: 8 },
|
padding: { x: 10, y: 8 },
|
||||||
lineSpacing: 3,
|
lineSpacing: 3,
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
})
|
}).setDepth(100)
|
||||||
.setScrollFactor(0)
|
this.hudObjects.push(this.logText)
|
||||||
.setDepth(100)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a dedicated HUD camera (zoom=1, no scroll) and separates
|
||||||
|
* world objects from HUD objects so neither camera renders both layers.
|
||||||
|
*/
|
||||||
|
private setupCameras(): void {
|
||||||
|
this.hudCamera = this.cameras.add(0, 0, this.scale.width, this.scale.height)
|
||||||
|
this.hudCamera.setScroll(0, 0)
|
||||||
|
this.hudCamera.setZoom(1)
|
||||||
|
|
||||||
|
this.cameras.main.ignore(this.hudObjects)
|
||||||
|
this.hudCamera.ignore(this.worldObjects)
|
||||||
}
|
}
|
||||||
|
|
||||||
update(_time: number, delta: number): void {
|
update(_time: number, delta: number): void {
|
||||||
this.handleKeyboard(delta)
|
this.handleKeyboard(delta)
|
||||||
this.updateOverlay()
|
this.updateOverlay()
|
||||||
|
|
||||||
// Periodic snapshot
|
|
||||||
this.snapshotTimer += delta
|
this.snapshotTimer += delta
|
||||||
if (this.snapshotTimer >= SNAPSHOT_EVERY) {
|
if (this.snapshotTimer >= SNAPSHOT_EVERY) {
|
||||||
this.snapshotTimer = 0
|
this.snapshotTimer = 0
|
||||||
@@ -208,8 +237,8 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
* @param delta - Frame delta in milliseconds
|
* @param delta - Frame delta in milliseconds
|
||||||
*/
|
*/
|
||||||
private handleKeyboard(delta: number): void {
|
private handleKeyboard(delta: number): void {
|
||||||
const cam = this.cameras.main
|
const cam = this.cameras.main
|
||||||
const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom
|
const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom
|
||||||
const worldPx = GRID_TILES * TILE_SIZE
|
const worldPx = GRID_TILES * TILE_SIZE
|
||||||
|
|
||||||
let dx = 0, dy = 0
|
let dx = 0, dy = 0
|
||||||
@@ -231,27 +260,17 @@ export class ZoomTestScene extends Phaser.Scene {
|
|||||||
const cam = this.cameras.main
|
const cam = this.cameras.main
|
||||||
const ptr = this.input.activePointer
|
const ptr = this.input.activePointer
|
||||||
|
|
||||||
// Viewport size in world pixels (what is actually visible)
|
const vpWidthPx = cam.width / cam.zoom
|
||||||
const vpWidthPx = cam.width / cam.zoom
|
const vpHeightPx = cam.height / cam.zoom
|
||||||
const vpHeightPx = cam.height / cam.zoom
|
|
||||||
|
|
||||||
// Viewport size in tiles
|
|
||||||
const vpWidthTiles = vpWidthPx / TILE_SIZE
|
const vpWidthTiles = vpWidthPx / TILE_SIZE
|
||||||
const vpHeightTiles = vpHeightPx / TILE_SIZE
|
const vpHeightTiles = vpHeightPx / TILE_SIZE
|
||||||
|
const centerWorldX = cam.scrollX + vpWidthPx / 2
|
||||||
// Camera center in world coords
|
const centerWorldY = cam.scrollY + vpHeightPx / 2
|
||||||
const centerWorldX = cam.scrollX + vpWidthPx / 2
|
const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE)
|
||||||
const centerWorldY = cam.scrollY + vpHeightPx / 2
|
const mouseTileY = Math.floor(ptr.worldY / TILE_SIZE)
|
||||||
|
const centerTileX = Math.floor(centerWorldX / TILE_SIZE)
|
||||||
// Tile under mouse
|
const centerTileY = Math.floor(centerWorldY / TILE_SIZE)
|
||||||
const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE)
|
const renderer = this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas'
|
||||||
const mouseTileY = Math.floor(ptr.worldY / TILE_SIZE)
|
|
||||||
|
|
||||||
// Tile at camera center
|
|
||||||
const centerTileX = Math.floor(centerWorldX / TILE_SIZE)
|
|
||||||
const centerTileY = Math.floor(centerWorldY / TILE_SIZE)
|
|
||||||
|
|
||||||
const renderer = this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas'
|
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
'── ZOOM TEST [Phaser default] ──',
|
'── ZOOM TEST [Phaser default] ──',
|
||||||
|
|||||||
Reference in New Issue
Block a user