fix: Playground surface on screen
This commit is contained in:
+93
-34
@@ -87,25 +87,48 @@ function tileAlpha(key) {
|
||||
|
||||
// ── Zoom / Pan state ──────────────────────────────────────────────────────────
|
||||
|
||||
const MIN_ZOOM = 1;
|
||||
const MAX_ZOOM = 12;
|
||||
const ZOOM_SPEED = 0.001; // wheel delta multiplier
|
||||
|
||||
/** Current zoom level (1 = whole map fits in canvas). */
|
||||
/** Returns the minimum zoom that fits the entire map in the canvas (1:1 tile ratio). */
|
||||
function minZoom() { return Math.min(canvas.width, canvas.height) / SVG_W; }
|
||||
|
||||
/** Current zoom level. */
|
||||
let zoom = 1;
|
||||
/** Top-left offset in *map-space* coordinates (0..1000). */
|
||||
let panX = 0;
|
||||
let panY = 0;
|
||||
|
||||
/** Clamp panX/panY so the viewport stays inside the map. */
|
||||
/**
|
||||
* Clamp panX/panY so the viewport stays within reachable map bounds.
|
||||
* When the whole map fits in a canvas axis, center it and lock pan on that axis.
|
||||
* When the map is larger than the canvas, constrain pan to keep the map filling the axis.
|
||||
*/
|
||||
function clampPan() {
|
||||
const vw = SVG_W / zoom;
|
||||
const vh = SVG_H / zoom;
|
||||
const vw = canvas.width / zoom;
|
||||
const vh = canvas.height / zoom;
|
||||
// X axis
|
||||
if (vw >= SVG_W) {
|
||||
panX = (SVG_W - vw) / 2; // center horizontally, no pan
|
||||
} else {
|
||||
panX = Math.max(0, Math.min(panX, SVG_W - vw));
|
||||
}
|
||||
// Y axis
|
||||
if (vh >= SVG_H) {
|
||||
panY = (SVG_H - vh) / 2; // center vertically, no pan
|
||||
} else {
|
||||
panY = Math.max(0, Math.min(panY, SVG_H - vh));
|
||||
}
|
||||
}
|
||||
|
||||
export function resetZoom() { zoom = 1; panX = 0; panY = 0; }
|
||||
/** Resets zoom to fit the entire map and centers it. */
|
||||
export function resetZoom() {
|
||||
zoom = minZoom();
|
||||
const vw = canvas.width / zoom;
|
||||
const vh = canvas.height / zoom;
|
||||
panX = (SVG_W - vw) / 2;
|
||||
panY = (SVG_H - vh) / 2;
|
||||
}
|
||||
|
||||
// ── Playfield mask ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -822,9 +845,9 @@ export function draw() {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
// viewport in map-space
|
||||
const vw = SVG_W / zoom;
|
||||
const vh = SVG_H / zoom;
|
||||
// viewport in map-space (canvas may be non-square)
|
||||
const vw = canvas.width / zoom;
|
||||
const vh = canvas.height / zoom;
|
||||
|
||||
ctx.save();
|
||||
|
||||
@@ -918,15 +941,14 @@ const mapAnimTextByType = {
|
||||
function triggerMapAnimation(type, x, y) {
|
||||
const cw = SVG_W / GRID_W;
|
||||
const ch = SVG_H / GRID_H;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
// Top-centre of the cell in screen pixels, relative to the canvas element
|
||||
const screenX = ((x * cw + cw / 2 - panX) * zoom / SVG_W) * rect.width;
|
||||
const screenY = ((y * ch - panY) * zoom / SVG_H) * rect.height;
|
||||
// Top-centre of the cell in canvas pixels (= CSS px since canvas buffer matches display size)
|
||||
const screenX = (x * cw + cw / 2 - panX) * zoom;
|
||||
const screenY = (y * ch - panY) * zoom;
|
||||
|
||||
// One cell height in screen pixels (travel distance)
|
||||
const cellScreenH = (ch * zoom / SVG_H) * rect.height;
|
||||
const fontSize = Math.max(8, (cw * zoom / SVG_W) * rect.width);
|
||||
// One cell height and width in screen pixels
|
||||
const cellScreenH = ch * zoom;
|
||||
const fontSize = Math.max(8, cw * zoom);
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = "mapAnimFloat";
|
||||
@@ -953,8 +975,8 @@ function triggerMapAnimation(type, x, y) {
|
||||
function pickCell(ev) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// Convert screen coords → map coords accounting for zoom/pan
|
||||
const mapX = (ev.clientX - rect.left) / rect.width * (SVG_W / zoom) + panX;
|
||||
const mapY = (ev.clientY - rect.top) / rect.height * (SVG_H / zoom) + panY;
|
||||
const mapX = (ev.clientX - rect.left) / rect.width * (canvas.width / zoom) + panX;
|
||||
const mapY = (ev.clientY - rect.top) / rect.height * (canvas.height / zoom) + panY;
|
||||
const x = Math.floor(mapX / (SVG_W / GRID_W));
|
||||
const y = Math.floor(mapY / (SVG_H / GRID_H));
|
||||
if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return null;
|
||||
@@ -1134,7 +1156,7 @@ async function onCanvasClick(ev) {
|
||||
// ── Not yet revealed: first click = spend user action to reveal ──
|
||||
if (!meta) {
|
||||
if (cooldownActive()) {
|
||||
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après 12h00 UTC.";
|
||||
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après la fin du cycle de réinitialisation.";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -1142,7 +1164,7 @@ async function onCanvasClick(ev) {
|
||||
if (res.status === 429) {
|
||||
actionsRemaining = 0;
|
||||
updateActionsDisplay();
|
||||
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après 12h00 UTC.";
|
||||
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après la fin du cycle de réinitialisation.";
|
||||
return;
|
||||
}
|
||||
if (res.status === 410) {
|
||||
@@ -1345,17 +1367,17 @@ canvas.addEventListener("wheel", (ev) => {
|
||||
ev.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// Cursor position in map-space before zoom
|
||||
const mx = (ev.clientX - rect.left) / rect.width * (SVG_W / zoom) + panX;
|
||||
const my = (ev.clientY - rect.top) / rect.height * (SVG_H / zoom) + panY;
|
||||
const mx = (ev.clientX - rect.left) / rect.width * (canvas.width / zoom) + panX;
|
||||
const my = (ev.clientY - rect.top) / rect.height * (canvas.height / zoom) + panY;
|
||||
|
||||
const oldZoom = zoom;
|
||||
// Negative deltaY = scroll up = zoom in
|
||||
zoom *= 1 - ev.deltaY * ZOOM_SPEED;
|
||||
zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
||||
zoom = Math.max(minZoom(), Math.min(MAX_ZOOM, zoom));
|
||||
|
||||
// Adjust pan so the map point under the cursor stays in place
|
||||
panX = mx - (ev.clientX - rect.left) / rect.width * (SVG_W / zoom);
|
||||
panY = my - (ev.clientY - rect.top) / rect.height * (SVG_H / zoom);
|
||||
panX = mx - (ev.clientX - rect.left) / rect.width * (canvas.width / zoom);
|
||||
panY = my - (ev.clientY - rect.top) / rect.height * (canvas.height / zoom);
|
||||
clampPan();
|
||||
|
||||
draw();
|
||||
@@ -1382,8 +1404,8 @@ canvas.addEventListener("mousedown", (ev) => {
|
||||
window.addEventListener("mousemove", (ev) => {
|
||||
if (!_dragging) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dx = (ev.clientX - _dragStartX) / rect.width * (SVG_W / zoom);
|
||||
const dy = (ev.clientY - _dragStartY) / rect.height * (SVG_H / zoom);
|
||||
const dx = (ev.clientX - _dragStartX) / rect.width * (canvas.width / zoom);
|
||||
const dy = (ev.clientY - _dragStartY) / rect.height * (canvas.height / zoom);
|
||||
panX = _panStartX - dx;
|
||||
panY = _panStartY - dy;
|
||||
clampPan();
|
||||
@@ -1433,8 +1455,8 @@ function touchDist(a, b) { return Math.hypot(a.clientX - b.clientX, a.clientY -
|
||||
function screenToMap(cx, cy) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
mx: (cx - rect.left) / rect.width * (SVG_W / zoom) + panX,
|
||||
my: (cy - rect.top) / rect.height * (SVG_H / zoom) + panY,
|
||||
mx: (cx - rect.left) / rect.width * (canvas.width / zoom) + panX,
|
||||
my: (cy - rect.top) / rect.height * (canvas.height / zoom) + panY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1478,8 +1500,8 @@ canvas.addEventListener("touchmove", (ev) => {
|
||||
if (_touches.length === 1) {
|
||||
// Single-finger pan
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dx = (_touches[0].clientX - _dragStartX) / rect.width * (SVG_W / zoom);
|
||||
const dy = (_touches[0].clientY - _dragStartY) / rect.height * (SVG_H / zoom);
|
||||
const dx = (_touches[0].clientX - _dragStartX) / rect.width * (canvas.width / zoom);
|
||||
const dy = (_touches[0].clientY - _dragStartY) / rect.height * (canvas.height / zoom);
|
||||
panX = _panStartX - dx;
|
||||
panY = _panStartY - dy;
|
||||
clampPan();
|
||||
@@ -1488,14 +1510,14 @@ canvas.addEventListener("touchmove", (ev) => {
|
||||
|
||||
if (_touches.length === 2 && _pinchDist0) {
|
||||
const newDist = touchDist(_touches[0], _touches[1]);
|
||||
zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, _pinchZoom0 * (newDist / _pinchDist0)));
|
||||
zoom = Math.max(minZoom(), Math.min(MAX_ZOOM, _pinchZoom0 * (newDist / _pinchDist0)));
|
||||
|
||||
// Keep the original midpoint anchored
|
||||
const midCx = (_touches[0].clientX + _touches[1].clientX) / 2;
|
||||
const midCy = (_touches[0].clientY + _touches[1].clientY) / 2;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
panX = _pinchMid0.mx - (midCx - rect.left) / rect.width * (SVG_W / zoom);
|
||||
panY = _pinchMid0.my - (midCy - rect.top) / rect.height * (SVG_H / zoom);
|
||||
panX = _pinchMid0.mx - (midCx - rect.left) / rect.width * (canvas.width / zoom);
|
||||
panY = _pinchMid0.my - (midCy - rect.top) / rect.height * (canvas.height / zoom);
|
||||
clampPan();
|
||||
draw();
|
||||
}
|
||||
@@ -1528,3 +1550,40 @@ canvas.addEventListener("touchend", (ev) => {
|
||||
_panStartY = panY;
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// ── Canvas resize ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Syncs the canvas pixel buffer to its CSS display size, clamps zoom/pan,
|
||||
* and redraws. Called once on boot (via requestAnimationFrame) and on resize.
|
||||
*/
|
||||
let _resizeInitDone = false;
|
||||
export function resizeCanvas() {
|
||||
const w = canvas.offsetWidth;
|
||||
const h = canvas.offsetHeight;
|
||||
if (w === 0 || h === 0) return;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
if (!_resizeInitDone) {
|
||||
// First call: fit the full map and center it
|
||||
_resizeInitDone = true;
|
||||
zoom = minZoom();
|
||||
const vw = canvas.width / zoom;
|
||||
const vh = canvas.height / zoom;
|
||||
panX = (SVG_W - vw) / 2;
|
||||
panY = (SVG_H - vh) / 2;
|
||||
} else {
|
||||
// Subsequent calls (window resize): preserve user zoom/pan, only enforce minimum
|
||||
zoom = Math.max(minZoom(), Math.min(MAX_ZOOM, zoom));
|
||||
clampPan();
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
resizeCanvas();
|
||||
refreshCursorFromLast();
|
||||
});
|
||||
|
||||
// Initial resize after first layout pass
|
||||
requestAnimationFrame(resizeCanvas);
|
||||
|
||||
+7
-12
@@ -1147,19 +1147,15 @@ button:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
/* ── Galaxy: square, fills viewport height, 1:1 ratio ────────────────────── */
|
||||
/* ── Galaxy: fills remaining viewport space, map maintains 1:1 tile ratio via JS ── */
|
||||
|
||||
.galaxyMain {
|
||||
/* Fit the tallest square that fits in the viewport without overlapping the info column */
|
||||
--map-size: min(100vh, calc(100vw - 340px));
|
||||
position: relative;
|
||||
width: var(--map-size);
|
||||
height: var(--map-size);
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
height: 100vh;
|
||||
background: #050810;
|
||||
overflow: hidden;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
canvas {
|
||||
@@ -1248,11 +1244,10 @@ canvas {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Galaxy scales to viewport width (square) */
|
||||
/* Galaxy fills the full screen on mobile */
|
||||
.galaxyMain {
|
||||
--map-size: 100vw;
|
||||
width: 100vw;
|
||||
height: 100vw;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
/* Scoreboard wraps nicely on narrow screens */
|
||||
|
||||
Reference in New Issue
Block a user