fix: Playground surface on screen
This commit is contained in:
+95
-36
@@ -87,25 +87,48 @@ function tileAlpha(key) {
|
|||||||
|
|
||||||
// ── Zoom / Pan state ──────────────────────────────────────────────────────────
|
// ── Zoom / Pan state ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const MIN_ZOOM = 1;
|
|
||||||
const MAX_ZOOM = 12;
|
const MAX_ZOOM = 12;
|
||||||
const ZOOM_SPEED = 0.001; // wheel delta multiplier
|
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;
|
let zoom = 1;
|
||||||
/** Top-left offset in *map-space* coordinates (0..1000). */
|
/** Top-left offset in *map-space* coordinates (0..1000). */
|
||||||
let panX = 0;
|
let panX = 0;
|
||||||
let panY = 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() {
|
function clampPan() {
|
||||||
const vw = SVG_W / zoom;
|
const vw = canvas.width / zoom;
|
||||||
const vh = SVG_H / zoom;
|
const vh = canvas.height / zoom;
|
||||||
panX = Math.max(0, Math.min(panX, SVG_W - vw));
|
// X axis
|
||||||
panY = Math.max(0, Math.min(panY, SVG_H - vh));
|
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 ────────────────────────────────────────────────────────────
|
// ── Playfield mask ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -822,9 +845,9 @@ export function draw() {
|
|||||||
const w = canvas.width;
|
const w = canvas.width;
|
||||||
const h = canvas.height;
|
const h = canvas.height;
|
||||||
|
|
||||||
// viewport in map-space
|
// viewport in map-space (canvas may be non-square)
|
||||||
const vw = SVG_W / zoom;
|
const vw = canvas.width / zoom;
|
||||||
const vh = SVG_H / zoom;
|
const vh = canvas.height / zoom;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
@@ -918,15 +941,14 @@ const mapAnimTextByType = {
|
|||||||
function triggerMapAnimation(type, x, y) {
|
function triggerMapAnimation(type, x, y) {
|
||||||
const cw = SVG_W / GRID_W;
|
const cw = SVG_W / GRID_W;
|
||||||
const ch = SVG_H / GRID_H;
|
const ch = SVG_H / GRID_H;
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Top-centre of the cell in screen pixels, relative to the canvas element
|
// Top-centre of the cell in canvas pixels (= CSS px since canvas buffer matches display size)
|
||||||
const screenX = ((x * cw + cw / 2 - panX) * zoom / SVG_W) * rect.width;
|
const screenX = (x * cw + cw / 2 - panX) * zoom;
|
||||||
const screenY = ((y * ch - panY) * zoom / SVG_H) * rect.height;
|
const screenY = (y * ch - panY) * zoom;
|
||||||
|
|
||||||
// One cell height in screen pixels (travel distance)
|
// One cell height and width in screen pixels
|
||||||
const cellScreenH = (ch * zoom / SVG_H) * rect.height;
|
const cellScreenH = ch * zoom;
|
||||||
const fontSize = Math.max(8, (cw * zoom / SVG_W) * rect.width);
|
const fontSize = Math.max(8, cw * zoom);
|
||||||
|
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "mapAnimFloat";
|
el.className = "mapAnimFloat";
|
||||||
@@ -953,8 +975,8 @@ function triggerMapAnimation(type, x, y) {
|
|||||||
function pickCell(ev) {
|
function pickCell(ev) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
// Convert screen coords → map coords accounting for zoom/pan
|
// Convert screen coords → map coords accounting for zoom/pan
|
||||||
const mapX = (ev.clientX - rect.left) / rect.width * (SVG_W / zoom) + panX;
|
const mapX = (ev.clientX - rect.left) / rect.width * (canvas.width / zoom) + panX;
|
||||||
const mapY = (ev.clientY - rect.top) / rect.height * (SVG_H / zoom) + panY;
|
const mapY = (ev.clientY - rect.top) / rect.height * (canvas.height / zoom) + panY;
|
||||||
const x = Math.floor(mapX / (SVG_W / GRID_W));
|
const x = Math.floor(mapX / (SVG_W / GRID_W));
|
||||||
const y = Math.floor(mapY / (SVG_H / GRID_H));
|
const y = Math.floor(mapY / (SVG_H / GRID_H));
|
||||||
if (x < 0 || x >= GRID_W || y < 0 || y >= GRID_H) return null;
|
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 ──
|
// ── Not yet revealed: first click = spend user action to reveal ──
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
if (cooldownActive()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1142,7 +1164,7 @@ async function onCanvasClick(ev) {
|
|||||||
if (res.status === 429) {
|
if (res.status === 429) {
|
||||||
actionsRemaining = 0;
|
actionsRemaining = 0;
|
||||||
updateActionsDisplay();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (res.status === 410) {
|
if (res.status === 410) {
|
||||||
@@ -1345,17 +1367,17 @@ canvas.addEventListener("wheel", (ev) => {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
// Cursor position in map-space before zoom
|
// Cursor position in map-space before zoom
|
||||||
const mx = (ev.clientX - rect.left) / rect.width * (SVG_W / zoom) + panX;
|
const mx = (ev.clientX - rect.left) / rect.width * (canvas.width / zoom) + panX;
|
||||||
const my = (ev.clientY - rect.top) / rect.height * (SVG_H / zoom) + panY;
|
const my = (ev.clientY - rect.top) / rect.height * (canvas.height / zoom) + panY;
|
||||||
|
|
||||||
const oldZoom = zoom;
|
const oldZoom = zoom;
|
||||||
// Negative deltaY = scroll up = zoom in
|
// Negative deltaY = scroll up = zoom in
|
||||||
zoom *= 1 - ev.deltaY * ZOOM_SPEED;
|
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
|
// Adjust pan so the map point under the cursor stays in place
|
||||||
panX = mx - (ev.clientX - rect.left) / rect.width * (SVG_W / zoom);
|
panX = mx - (ev.clientX - rect.left) / rect.width * (canvas.width / zoom);
|
||||||
panY = my - (ev.clientY - rect.top) / rect.height * (SVG_H / zoom);
|
panY = my - (ev.clientY - rect.top) / rect.height * (canvas.height / zoom);
|
||||||
clampPan();
|
clampPan();
|
||||||
|
|
||||||
draw();
|
draw();
|
||||||
@@ -1382,8 +1404,8 @@ canvas.addEventListener("mousedown", (ev) => {
|
|||||||
window.addEventListener("mousemove", (ev) => {
|
window.addEventListener("mousemove", (ev) => {
|
||||||
if (!_dragging) return;
|
if (!_dragging) return;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const dx = (ev.clientX - _dragStartX) / rect.width * (SVG_W / zoom);
|
const dx = (ev.clientX - _dragStartX) / rect.width * (canvas.width / zoom);
|
||||||
const dy = (ev.clientY - _dragStartY) / rect.height * (SVG_H / zoom);
|
const dy = (ev.clientY - _dragStartY) / rect.height * (canvas.height / zoom);
|
||||||
panX = _panStartX - dx;
|
panX = _panStartX - dx;
|
||||||
panY = _panStartY - dy;
|
panY = _panStartY - dy;
|
||||||
clampPan();
|
clampPan();
|
||||||
@@ -1433,8 +1455,8 @@ function touchDist(a, b) { return Math.hypot(a.clientX - b.clientX, a.clientY -
|
|||||||
function screenToMap(cx, cy) {
|
function screenToMap(cx, cy) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
mx: (cx - rect.left) / rect.width * (SVG_W / zoom) + panX,
|
mx: (cx - rect.left) / rect.width * (canvas.width / zoom) + panX,
|
||||||
my: (cy - rect.top) / rect.height * (SVG_H / zoom) + panY,
|
my: (cy - rect.top) / rect.height * (canvas.height / zoom) + panY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,8 +1500,8 @@ canvas.addEventListener("touchmove", (ev) => {
|
|||||||
if (_touches.length === 1) {
|
if (_touches.length === 1) {
|
||||||
// Single-finger pan
|
// Single-finger pan
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const dx = (_touches[0].clientX - _dragStartX) / rect.width * (SVG_W / zoom);
|
const dx = (_touches[0].clientX - _dragStartX) / rect.width * (canvas.width / zoom);
|
||||||
const dy = (_touches[0].clientY - _dragStartY) / rect.height * (SVG_H / zoom);
|
const dy = (_touches[0].clientY - _dragStartY) / rect.height * (canvas.height / zoom);
|
||||||
panX = _panStartX - dx;
|
panX = _panStartX - dx;
|
||||||
panY = _panStartY - dy;
|
panY = _panStartY - dy;
|
||||||
clampPan();
|
clampPan();
|
||||||
@@ -1488,14 +1510,14 @@ canvas.addEventListener("touchmove", (ev) => {
|
|||||||
|
|
||||||
if (_touches.length === 2 && _pinchDist0) {
|
if (_touches.length === 2 && _pinchDist0) {
|
||||||
const newDist = touchDist(_touches[0], _touches[1]);
|
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
|
// Keep the original midpoint anchored
|
||||||
const midCx = (_touches[0].clientX + _touches[1].clientX) / 2;
|
const midCx = (_touches[0].clientX + _touches[1].clientX) / 2;
|
||||||
const midCy = (_touches[0].clientY + _touches[1].clientY) / 2;
|
const midCy = (_touches[0].clientY + _touches[1].clientY) / 2;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
panX = _pinchMid0.mx - (midCx - rect.left) / rect.width * (SVG_W / zoom);
|
panX = _pinchMid0.mx - (midCx - rect.left) / rect.width * (canvas.width / zoom);
|
||||||
panY = _pinchMid0.my - (midCy - rect.top) / rect.height * (SVG_H / zoom);
|
panY = _pinchMid0.my - (midCy - rect.top) / rect.height * (canvas.height / zoom);
|
||||||
clampPan();
|
clampPan();
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
@@ -1528,3 +1550,40 @@ canvas.addEventListener("touchend", (ev) => {
|
|||||||
_panStartY = panY;
|
_panStartY = panY;
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { 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);
|
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 {
|
.galaxyMain {
|
||||||
/* Fit the tallest square that fits in the viewport without overlapping the info column */
|
|
||||||
--map-size: min(100vh, calc(100vw - 340px));
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--map-size);
|
flex: 1 1 0;
|
||||||
height: var(--map-size);
|
min-width: 0;
|
||||||
flex-shrink: 0;
|
height: 100vh;
|
||||||
flex-grow: 0;
|
|
||||||
background: #050810;
|
background: #050810;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
align-self: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
@@ -1248,11 +1244,10 @@ canvas {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Galaxy scales to viewport width (square) */
|
/* Galaxy fills the full screen on mobile */
|
||||||
.galaxyMain {
|
.galaxyMain {
|
||||||
--map-size: 100vw;
|
width: 100%;
|
||||||
width: 100vw;
|
height: 100dvh;
|
||||||
height: 100vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scoreboard wraps nicely on narrow screens */
|
/* Scoreboard wraps nicely on narrow screens */
|
||||||
|
|||||||
Reference in New Issue
Block a user