feat: implement responsive canvas scaling with Phaser.Scale.FIT and DOM overlay sync
- Replace manual CSS scaling in index.html with Phaser's built-in FIT mode, ensuring canvas always fits viewport while maintaining 1920×1080 aspect ratio. - Add scale configuration to game config (Phaser.Scale.FIT, CENTER_BOTH). - Refactor all DOM UI overlays to use fixed 1920×1080 dimensions with transform-origin: top-left and dynamic scaling based on canvas rect. - Implement _syncOverlayToCanvas() in MainMenuScene, NewPuzzleScene, and PuzzleScene to keep DOM overlays perfectly aligned during resize events. - Fix UI element positions in MainMenuScene and NewPuzzleScene to match new coordinate system (e.g., title from -30→45, buttons repositioned). - Update Phaser CDN version from 3.9.0 to 3.90.0 (likely typo fix). - Fix particle emitter creation syntax and stop() instead of on=false. - Clean up resize listeners on scene shutdown to prevent memory leaks.
This commit is contained in:
parent
0af6e00469
commit
4d07fcb1c4
10
index.html
10
index.html
|
|
@ -10,14 +10,6 @@
|
||||||
body { display: flex; justify-content: center; align-items: center; }
|
body { display: flex; justify-content: center; align-items: center; }
|
||||||
#game-container canvas {
|
#game-container canvas {
|
||||||
display: block;
|
display: block;
|
||||||
/* Scale the 1920×1080 canvas to fit the viewport at 16:9.
|
|
||||||
- width: fills viewport width, height = 56.25% of that (9/16)
|
|
||||||
- max-height: 100vh prevents overflow on portrait/square displays;
|
|
||||||
max-width: 177.78vh (16/9 * 100) clamps width on tall displays. */
|
|
||||||
width: 100vw;
|
|
||||||
height: 56.25vw;
|
|
||||||
max-height: 100vh;
|
|
||||||
max-width: 177.78vh;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -25,7 +17,7 @@
|
||||||
<div id="game-container"></div>
|
<div id="game-container"></div>
|
||||||
|
|
||||||
<!-- Phaser 3.9 -->
|
<!-- Phaser 3.9 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/phaser@3.9.0/dist/phaser.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/phaser@3.90.0/dist/phaser.min.js"></script>
|
||||||
|
|
||||||
<!-- Utilities (no dependencies) -->
|
<!-- Utilities (no dependencies) -->
|
||||||
<script src="js/puzzle/ConnectorGeometry.js"></script>
|
<script src="js/puzzle/ConnectorGeometry.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ const config = {
|
||||||
height: 1080,
|
height: 1080,
|
||||||
backgroundColor: '#12121e',
|
backgroundColor: '#12121e',
|
||||||
parent: 'game-container',
|
parent: 'game-container',
|
||||||
|
scale: {
|
||||||
|
mode: Phaser.Scale.FIT,
|
||||||
|
autoCenter: Phaser.Scale.CENTER_BOTH
|
||||||
|
},
|
||||||
scene: [MainMenuScene, NewPuzzleScene, PuzzleScene]
|
scene: [MainMenuScene, NewPuzzleScene, PuzzleScene]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,21 +156,33 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
|
|
||||||
// ─── DOM UI ──────────────────────────────────────────────────────────
|
// ─── DOM UI ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_syncOverlayToCanvas() {
|
||||||
|
if (!this._uiLayer) return;
|
||||||
|
const rect = this.sys.game.canvas.getBoundingClientRect();
|
||||||
|
Object.assign(this._uiLayer.style, {
|
||||||
|
left: rect.left + 'px',
|
||||||
|
top: rect.top + 'px',
|
||||||
|
transform: `scale(${rect.width / 1920}, ${rect.height / 1080})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_buildDomUI() {
|
_buildDomUI() {
|
||||||
this._uiLayer = document.createElement('div');
|
this._uiLayer = document.createElement('div');
|
||||||
Object.assign(this._uiLayer.style, {
|
Object.assign(this._uiLayer.style, {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: '50%',
|
width: '1920px',
|
||||||
left: '50%',
|
height: '1080px',
|
||||||
transform: 'translate(-50%, -50%)',
|
transformOrigin: 'top left',
|
||||||
width: '100vw',
|
pointerEvents: 'none',
|
||||||
height: '56.25vw',
|
zIndex: '10',
|
||||||
maxHeight: '100vh',
|
|
||||||
maxWidth: '177.78vh',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: '10',
|
|
||||||
});
|
});
|
||||||
document.body.appendChild(this._uiLayer);
|
document.body.appendChild(this._uiLayer);
|
||||||
|
this._syncOverlayToCanvas();
|
||||||
|
|
||||||
|
// Keep overlay aligned when the window resizes
|
||||||
|
this._onResizeScale = () => this._syncOverlayToCanvas();
|
||||||
|
this.scale.on('resize', this._onResizeScale);
|
||||||
|
window.addEventListener('resize', this._onResizeScale);
|
||||||
|
|
||||||
// Check for saved puzzle
|
// Check for saved puzzle
|
||||||
const saved = StorageManager.loadCurrent();
|
const saved = StorageManager.loadCurrent();
|
||||||
|
|
@ -187,7 +199,7 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.assign(this._continueBtn.style, {
|
Object.assign(this._continueBtn.style, {
|
||||||
top: '630px',
|
top: '700px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
width: '340px',
|
width: '340px',
|
||||||
|
|
@ -206,7 +218,7 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
this.scene.start('NewPuzzleScene');
|
this.scene.start('NewPuzzleScene');
|
||||||
});
|
});
|
||||||
Object.assign(this._newBtn.style, {
|
Object.assign(this._newBtn.style, {
|
||||||
top: '550px',
|
top: '620px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
width: '340px',
|
width: '340px',
|
||||||
|
|
@ -220,7 +232,7 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
this._showJoinDialog();
|
this._showJoinDialog();
|
||||||
});
|
});
|
||||||
Object.assign(this._joinBtn.style, {
|
Object.assign(this._joinBtn.style, {
|
||||||
top: '710px',
|
top: '780px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
width: '340px',
|
width: '340px',
|
||||||
|
|
@ -241,7 +253,7 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Object.assign(this._fullscreenBtn.style, {
|
Object.assign(this._fullscreenBtn.style, {
|
||||||
top: '790px',
|
top: '860px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
width: '340px',
|
width: '340px',
|
||||||
|
|
@ -267,6 +279,10 @@ class MainMenuScene extends Phaser.Scene {
|
||||||
|
|
||||||
this.events.once('shutdown', () => {
|
this.events.once('shutdown', () => {
|
||||||
document.removeEventListener('fullscreenchange', this._onFullscreenChange);
|
document.removeEventListener('fullscreenchange', this._onFullscreenChange);
|
||||||
|
if (this._onResizeScale) {
|
||||||
|
this.scale.off('resize', this._onResizeScale);
|
||||||
|
window.removeEventListener('resize', this._onResizeScale);
|
||||||
|
}
|
||||||
this._destroyDomUI();
|
this._destroyDomUI();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ class NewPuzzleScene extends Phaser.Scene {
|
||||||
this.add.image(960, 540, 'main_menu_bg').setDisplaySize(1920, 1080);
|
this.add.image(960, 540, 'main_menu_bg').setDisplaySize(1920, 1080);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title — fixed position (canvas is always 1920×1080)
|
// Title — centered in the top bar area
|
||||||
this.add.text(960, -30, 'Choose Your Puzzle', {
|
this.add.text(960, 45, 'Choose Your Puzzle', {
|
||||||
fontFamily: 'Georgia, serif',
|
fontFamily: 'Georgia, serif',
|
||||||
fontSize: '58px',
|
fontSize: '58px',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
|
|
@ -83,16 +83,16 @@ class NewPuzzleScene extends Phaser.Scene {
|
||||||
|
|
||||||
// Faint full-width rule
|
// Faint full-width rule
|
||||||
sep.lineStyle(1, 0x64b5f6, 0.4);
|
sep.lineStyle(1, 0x64b5f6, 0.4);
|
||||||
sep.lineBetween(77, -81, 1843, -81);
|
sep.lineBetween(77, 88, 1843, 88);
|
||||||
|
|
||||||
// Bright centre segment
|
// Bright centre segment
|
||||||
sep.lineStyle(2, 0x1e88e5, 1);
|
sep.lineStyle(2, 0x1e88e5, 1);
|
||||||
sep.lineBetween(653, -81, 1267, -81);
|
sep.lineBetween(653, 88, 1267, 88);
|
||||||
|
|
||||||
// Diamond accent at the midpoint
|
// Diamond accent at the midpoint
|
||||||
sep.fillStyle(0xffb74d, 1);
|
sep.fillStyle(0xffb74d, 1);
|
||||||
sep.fillTriangle(960, 100, 966, 105, 960, 110);
|
sep.fillTriangle(960, 83, 966, 88, 960, 93);
|
||||||
sep.fillTriangle(960, 100, 954, 105, 960, 110);
|
sep.fillTriangle(960, 83, 954, 88, 960, 93);
|
||||||
|
|
||||||
// Read puzzle list and thumbnail manifest from cache
|
// Read puzzle list and thumbnail manifest from cache
|
||||||
this._puzzleImages = this.cache.json.get('puzzle_list');
|
this._puzzleImages = this.cache.json.get('puzzle_list');
|
||||||
|
|
@ -136,27 +136,45 @@ class NewPuzzleScene extends Phaser.Scene {
|
||||||
|
|
||||||
// ─── DOM UI ──────────────────────────────────────────────────────────
|
// ─── DOM UI ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_syncOverlayToCanvas() {
|
||||||
|
if (!this._uiLayer) return;
|
||||||
|
const rect = this.sys.game.canvas.getBoundingClientRect();
|
||||||
|
Object.assign(this._uiLayer.style, {
|
||||||
|
left: rect.left + 'px',
|
||||||
|
top: rect.top + 'px',
|
||||||
|
transform: `scale(${rect.width / 1920}, ${rect.height / 1080})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_buildDomUI() {
|
_buildDomUI() {
|
||||||
this._uiLayer = document.createElement('div');
|
this._uiLayer = document.createElement('div');
|
||||||
Object.assign(this._uiLayer.style, {
|
Object.assign(this._uiLayer.style, {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: '50%',
|
width: '1920px',
|
||||||
left: '50%',
|
height: '1080px',
|
||||||
transform: 'translate(-50%, -50%)',
|
transformOrigin: 'top left',
|
||||||
width: '100vw',
|
pointerEvents: 'none',
|
||||||
height: '56.25vw',
|
zIndex: '10',
|
||||||
maxHeight: '100vh',
|
|
||||||
maxWidth: '177.78vh',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: '10',
|
|
||||||
});
|
});
|
||||||
document.body.appendChild(this._uiLayer);
|
document.body.appendChild(this._uiLayer);
|
||||||
|
this._syncOverlayToCanvas();
|
||||||
|
|
||||||
|
// Keep overlay aligned when the window resizes
|
||||||
|
this._onResizeScale = () => this._syncOverlayToCanvas();
|
||||||
|
this.scale.on('resize', this._onResizeScale);
|
||||||
|
window.addEventListener('resize', this._onResizeScale);
|
||||||
|
|
||||||
this._buildPuzzleGrid(this._uiLayer);
|
this._buildPuzzleGrid(this._uiLayer);
|
||||||
this._buildControlsBar(this._uiLayer);
|
this._buildControlsBar(this._uiLayer);
|
||||||
|
|
||||||
this._refreshStartButton();
|
this._refreshStartButton();
|
||||||
this.events.once('shutdown', () => this._destroyDomUI());
|
this.events.once('shutdown', () => {
|
||||||
|
if (this._onResizeScale) {
|
||||||
|
this.scale.off('resize', this._onResizeScale);
|
||||||
|
window.removeEventListener('resize', this._onResizeScale);
|
||||||
|
}
|
||||||
|
this._destroyDomUI();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildPuzzleGrid(uiLayer) {
|
_buildPuzzleGrid(uiLayer) {
|
||||||
|
|
|
||||||
|
|
@ -2058,25 +2058,37 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
|
|
||||||
// ─── DOM UI ──────────────────────────────────────────────────────────
|
// ─── DOM UI ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_syncOverlayToCanvas() {
|
||||||
|
if (!this._uiLayer) return;
|
||||||
|
const rect = this.sys.game.canvas.getBoundingClientRect();
|
||||||
|
Object.assign(this._uiLayer.style, {
|
||||||
|
left: rect.left + 'px',
|
||||||
|
top: rect.top + 'px',
|
||||||
|
transform: `scale(${rect.width / 1920}, ${rect.height / 1080})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_buildDomUI() {
|
_buildDomUI() {
|
||||||
// Overlay div that mirrors the canvas CSS dimensions exactly.
|
// Overlay div that mirrors the canvas dimensions exactly.
|
||||||
// pointer-events:none lets clicks pass through to the canvas everywhere
|
// pointer-events:none lets clicks pass through to the canvas everywhere
|
||||||
// except on the interactive children.
|
// except on the interactive children.
|
||||||
this._uiLayer = document.createElement('div');
|
this._uiLayer = document.createElement('div');
|
||||||
Object.assign(this._uiLayer.style, {
|
Object.assign(this._uiLayer.style, {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: '50%',
|
width: '1920px',
|
||||||
left: '50%',
|
height: '1080px',
|
||||||
transform: 'translate(-50%, -50%)',
|
transformOrigin: 'top left',
|
||||||
width: '100vw',
|
pointerEvents: 'none',
|
||||||
height: '56.25vw',
|
zIndex: '10',
|
||||||
maxHeight: '100vh',
|
fontFamily: 'Arial, sans-serif',
|
||||||
maxWidth: '177.78vh',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
zIndex: '10',
|
|
||||||
fontFamily: 'Arial, sans-serif',
|
|
||||||
});
|
});
|
||||||
document.body.appendChild(this._uiLayer);
|
document.body.appendChild(this._uiLayer);
|
||||||
|
this._syncOverlayToCanvas();
|
||||||
|
|
||||||
|
// Keep overlay aligned when the window resizes
|
||||||
|
this._onResizeScale = () => this._syncOverlayToCanvas();
|
||||||
|
this.scale.on('resize', this._onResizeScale);
|
||||||
|
window.addEventListener('resize', this._onResizeScale);
|
||||||
|
|
||||||
// Room code + share link — bottom-right
|
// Room code + share link — bottom-right
|
||||||
const roomCode = this.cfg.roomCode;
|
const roomCode = this.cfg.roomCode;
|
||||||
|
|
@ -2491,7 +2503,13 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
this._buildStatsPanel();
|
this._buildStatsPanel();
|
||||||
|
|
||||||
// Clean up DOM when the scene shuts down
|
// Clean up DOM when the scene shuts down
|
||||||
this.events.once('shutdown', () => this._destroyDomUI());
|
this.events.once('shutdown', () => {
|
||||||
|
if (this._onResizeScale) {
|
||||||
|
this.scale.off('resize', this._onResizeScale);
|
||||||
|
window.removeEventListener('resize', this._onResizeScale);
|
||||||
|
}
|
||||||
|
this._destroyDomUI();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_showDomCompletion() {
|
_showDomCompletion() {
|
||||||
|
|
@ -2821,9 +2839,7 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
|
|
||||||
// Create one emitter per color
|
// Create one emitter per color
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
const manager = this.add.particles(key);
|
const emitter = this.add.particles(0, 0, key, {
|
||||||
manager.setDepth(1003);
|
|
||||||
const emitter = manager.createEmitter({
|
|
||||||
x: { min: 0, max: width },
|
x: { min: 0, max: width },
|
||||||
y: -10,
|
y: -10,
|
||||||
speedY: { min: 80, max: 220 },
|
speedY: { min: 80, max: 220 },
|
||||||
|
|
@ -2836,8 +2852,9 @@ class PuzzleScene extends Phaser.Scene {
|
||||||
frequency: 50,
|
frequency: 50,
|
||||||
gravityY: 60
|
gravityY: 60
|
||||||
});
|
});
|
||||||
|
emitter.setDepth(1003);
|
||||||
|
|
||||||
this.time.delayedCall(2500, () => { emitter.on = false; });
|
this.time.delayedCall(2500, () => { emitter.stop(); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue