✨ add ZoomTestScene with Phaser default zoom for analysis
Separate test environment at /test.html (own Vite entry, own Phaser instance). ZoomTestScene renders a 50×50 tile grid with crosshair markers and a live HUD overlay showing zoom, scroll, viewport in px and tiles, mouse world/screen/tile coords, and renderer info. Zoom uses plain cam.setZoom() — no mouse tracking — to observe Phaser's default center-anchor behavior.
This commit is contained in:
239
src/test/ZoomTestScene.ts
Normal file
239
src/test/ZoomTestScene.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import Phaser from 'phaser'
|
||||
import { TILE_SIZE } from '../config'
|
||||
|
||||
const GRID_TILES = 50 // world size in tiles
|
||||
const MIN_ZOOM = 0.25
|
||||
const MAX_ZOOM = 4.0
|
||||
const ZOOM_STEP = 0.1
|
||||
const MARKER_EVERY = 5 // small crosshair every N tiles
|
||||
const LABEL_EVERY = 10 // coordinate label every N tiles
|
||||
const CAMERA_SPEED = 400 // px/s
|
||||
|
||||
/**
|
||||
* First test scene: observes pure Phaser default zoom behavior.
|
||||
* No custom scroll compensation — cam.setZoom() only, zoom anchors to camera center.
|
||||
*
|
||||
* Controls: Scroll wheel to zoom, WASD / Arrow keys to pan.
|
||||
*/
|
||||
export class ZoomTestScene extends Phaser.Scene {
|
||||
private logText!: Phaser.GameObjects.Text
|
||||
private keys!: {
|
||||
up: Phaser.Input.Keyboard.Key
|
||||
down: Phaser.Input.Keyboard.Key
|
||||
left: Phaser.Input.Keyboard.Key
|
||||
right: Phaser.Input.Keyboard.Key
|
||||
w: Phaser.Input.Keyboard.Key
|
||||
s: Phaser.Input.Keyboard.Key
|
||||
a: Phaser.Input.Keyboard.Key
|
||||
d: Phaser.Input.Keyboard.Key
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super({ key: 'ZoomTest' })
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.drawGrid()
|
||||
this.setupCamera()
|
||||
this.setupInput()
|
||||
this.createOverlay()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the static world grid into world space.
|
||||
* - Faint tile lines on every tile boundary
|
||||
* - Small green crosshairs every MARKER_EVERY tiles
|
||||
* - Yellow labeled crosshairs every LABEL_EVERY tiles
|
||||
* - Red world border
|
||||
*/
|
||||
private drawGrid(): void {
|
||||
const worldPx = GRID_TILES * TILE_SIZE
|
||||
const g = this.add.graphics()
|
||||
|
||||
// Background fill
|
||||
g.fillStyle(0x111811)
|
||||
g.fillRect(0, 0, worldPx, worldPx)
|
||||
|
||||
// Tile grid lines
|
||||
g.lineStyle(1, 0x223322, 0.5)
|
||||
for (let i = 0; i <= GRID_TILES; i++) {
|
||||
const p = i * TILE_SIZE
|
||||
g.lineBetween(p, 0, p, worldPx)
|
||||
g.lineBetween(0, p, worldPx, p)
|
||||
}
|
||||
|
||||
// Crosshair markers
|
||||
for (let tx = 0; tx <= GRID_TILES; tx += MARKER_EVERY) {
|
||||
for (let ty = 0; ty <= GRID_TILES; ty += MARKER_EVERY) {
|
||||
const px = tx * TILE_SIZE
|
||||
const py = ty * TILE_SIZE
|
||||
const isLabel = tx % LABEL_EVERY === 0 && ty % LABEL_EVERY === 0
|
||||
const color = isLabel ? 0xffff00 : 0x00ff88
|
||||
const arm = isLabel ? 10 : 6
|
||||
|
||||
g.lineStyle(1, color, isLabel ? 1.0 : 0.7)
|
||||
g.lineBetween(px - arm, py, px + arm, py)
|
||||
g.lineBetween(px, py - arm, px, py + arm)
|
||||
|
||||
g.fillStyle(color, 1.0)
|
||||
g.fillCircle(px, py, isLabel ? 2.5 : 1.5)
|
||||
}
|
||||
}
|
||||
|
||||
// Coordinate labels at LABEL_EVERY intersections
|
||||
for (let tx = 0; tx <= GRID_TILES; tx += LABEL_EVERY) {
|
||||
for (let ty = 0; ty <= GRID_TILES; ty += LABEL_EVERY) {
|
||||
this.add.text(
|
||||
tx * TILE_SIZE + 4,
|
||||
ty * TILE_SIZE + 4,
|
||||
`${tx},${ty}`,
|
||||
{ fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' }
|
||||
).setDepth(1)
|
||||
}
|
||||
}
|
||||
|
||||
// World border
|
||||
g.lineStyle(2, 0xff4444, 1.0)
|
||||
g.strokeRect(0, 0, worldPx, worldPx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets camera bounds and centers the view on the world.
|
||||
*/
|
||||
private setupCamera(): void {
|
||||
const cam = this.cameras.main
|
||||
const worldPx = GRID_TILES * TILE_SIZE
|
||||
cam.setBounds(0, 0, worldPx, worldPx)
|
||||
cam.scrollX = worldPx / 2 - cam.width / 2
|
||||
cam.scrollY = worldPx / 2 - cam.height / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers scroll wheel zoom and stores keyboard key references.
|
||||
* Zoom uses cam.setZoom() only — pure Phaser default, anchors to camera center.
|
||||
*/
|
||||
private setupInput(): void {
|
||||
const cam = this.cameras.main
|
||||
const kb = this.input.keyboard!
|
||||
|
||||
this.keys = {
|
||||
up: kb.addKey(Phaser.Input.Keyboard.KeyCodes.UP),
|
||||
down: kb.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN),
|
||||
left: kb.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT),
|
||||
right: kb.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT),
|
||||
w: kb.addKey(Phaser.Input.Keyboard.KeyCodes.W),
|
||||
s: kb.addKey(Phaser.Input.Keyboard.KeyCodes.S),
|
||||
a: kb.addKey(Phaser.Input.Keyboard.KeyCodes.A),
|
||||
d: kb.addKey(Phaser.Input.Keyboard.KeyCodes.D),
|
||||
}
|
||||
|
||||
this.input.on('wheel', (
|
||||
_ptr: Phaser.Input.Pointer,
|
||||
_objs: unknown,
|
||||
_dx: number,
|
||||
dy: number
|
||||
) => {
|
||||
const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
||||
cam.setZoom(newZoom)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the fixed HUD text overlay (scroll factor 0 = screen space).
|
||||
*/
|
||||
private createOverlay(): void {
|
||||
this.logText = this.add.text(10, 10, '', {
|
||||
fontSize: '13px',
|
||||
color: '#e8e8e8',
|
||||
backgroundColor: '#000000bb',
|
||||
padding: { x: 10, y: 8 },
|
||||
lineSpacing: 3,
|
||||
fontFamily: 'monospace',
|
||||
})
|
||||
.setScrollFactor(0)
|
||||
.setDepth(100)
|
||||
}
|
||||
|
||||
update(_time: number, delta: number): void {
|
||||
this.handleKeyboard(delta)
|
||||
this.updateOverlay()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves camera with WASD / arrow keys at CAMERA_SPEED px/s (world space).
|
||||
* @param delta - Frame delta in milliseconds
|
||||
*/
|
||||
private handleKeyboard(delta: number): void {
|
||||
const cam = this.cameras.main
|
||||
const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom
|
||||
const worldPx = GRID_TILES * TILE_SIZE
|
||||
|
||||
let dx = 0, dy = 0
|
||||
if (this.keys.left.isDown || this.keys.a.isDown) dx -= speed
|
||||
if (this.keys.right.isDown || this.keys.d.isDown) dx += speed
|
||||
if (this.keys.up.isDown || this.keys.w.isDown) dy -= speed
|
||||
if (this.keys.down.isDown || this.keys.s.isDown) dy += speed
|
||||
|
||||
if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707 }
|
||||
|
||||
cam.scrollX = Phaser.Math.Clamp(cam.scrollX + dx, 0, worldPx - cam.width / cam.zoom)
|
||||
cam.scrollY = Phaser.Math.Clamp(cam.scrollY + dy, 0, worldPx - cam.height / cam.zoom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes and renders all diagnostic values to the HUD overlay each frame.
|
||||
*/
|
||||
private updateOverlay(): void {
|
||||
const cam = this.cameras.main
|
||||
const ptr = this.input.activePointer
|
||||
|
||||
// Viewport size in world pixels (what is actually visible)
|
||||
const vpWidthPx = cam.width / cam.zoom
|
||||
const vpHeightPx = cam.height / cam.zoom
|
||||
|
||||
// Viewport size in tiles
|
||||
const vpWidthTiles = vpWidthPx / TILE_SIZE
|
||||
const vpHeightTiles = vpHeightPx / TILE_SIZE
|
||||
|
||||
// Camera center in world coords
|
||||
const centerWorldX = cam.scrollX + vpWidthPx / 2
|
||||
const centerWorldY = cam.scrollY + vpHeightPx / 2
|
||||
|
||||
// Tile under mouse
|
||||
const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE)
|
||||
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 = [
|
||||
'── ZOOM TEST [Phaser default] ──',
|
||||
'',
|
||||
`Zoom: ${cam.zoom.toFixed(4)}`,
|
||||
`scrollX / scrollY: ${cam.scrollX.toFixed(2)} / ${cam.scrollY.toFixed(2)}`,
|
||||
'',
|
||||
`Viewport (screen): ${cam.width} × ${cam.height} px`,
|
||||
`Viewport (world): ${vpWidthPx.toFixed(2)} × ${vpHeightPx.toFixed(2)} px`,
|
||||
`Viewport (tiles): ${vpWidthTiles.toFixed(3)} × ${vpHeightTiles.toFixed(3)}`,
|
||||
'',
|
||||
`Center world: ${centerWorldX.toFixed(2)}, ${centerWorldY.toFixed(2)}`,
|
||||
`Center tile: ${centerTileX}, ${centerTileY}`,
|
||||
'',
|
||||
`Mouse screen: ${ptr.x.toFixed(1)}, ${ptr.y.toFixed(1)}`,
|
||||
`Mouse world: ${ptr.worldX.toFixed(2)}, ${ptr.worldY.toFixed(2)}`,
|
||||
`Mouse tile: ${mouseTileX}, ${mouseTileY}`,
|
||||
'',
|
||||
`Canvas: ${this.scale.width} × ${this.scale.height} px`,
|
||||
`TILE_SIZE: ${TILE_SIZE} px`,
|
||||
`roundPixels: ${(this.game.renderer.config as Record<string, unknown>)['roundPixels']}`,
|
||||
`Renderer: ${renderer}`,
|
||||
'',
|
||||
'[Scroll] Zoom [WASD / ↑↓←→] Pan',
|
||||
]
|
||||
|
||||
this.logText.setText(lines)
|
||||
}
|
||||
}
|
||||
21
src/test/main.ts
Normal file
21
src/test/main.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import Phaser from 'phaser'
|
||||
import { ZoomTestScene } from './ZoomTestScene'
|
||||
|
||||
const config: Phaser.Types.Core.GameConfig = {
|
||||
type: Phaser.AUTO,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
backgroundColor: '#0d1a0d',
|
||||
scene: [ZoomTestScene],
|
||||
scale: {
|
||||
mode: Phaser.Scale.RESIZE,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
},
|
||||
render: {
|
||||
pixelArt: false,
|
||||
antialias: true,
|
||||
roundPixels: true,
|
||||
},
|
||||
}
|
||||
|
||||
new Phaser.Game(config)
|
||||
16
test.html
Normal file
16
test.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Game — Test Scenes</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #000; overflow: hidden; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/src/test/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
@@ -7,6 +8,12 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsInlineLimit: 0
|
||||
}
|
||||
assetsInlineLimit: 0,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
test: resolve(__dirname, 'test.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user