From 23cb073bda126747056f0013cf8e43062d1c0069 Mon Sep 17 00:00:00 2001 From: soulrap <135220351+soulrap@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:12:43 +0300 Subject: [PATCH] reserve the UI bar height when scaling the game area The game area and the UI bar are laid out in two design boxes that happen to share the same 1.78 ratio - the board in the game's `mainSizesMap` (1422x800 for scatter), the bar in `STANDARD_MAIN_SIZES_MAP` (1920x1080), bottom-aligned via ``. Once the canvas is wider than that ratio the height binds for both. The board takes the full canvas height and centres; the bar is anchored to the bottom of the same canvas; the bottom row of the board renders behind it. Reproduced on scatter at 1194x518 (ratio 2.31) - the last symbol row is completely covered. At 984x858 (ratio 1.15) width binds instead and there is no overlap, which is why this is easy to miss on a normal desktop window. Add an optional `safeAreaBottomMap` to `createLayout`. The reserved strip is subtracted before the scale is derived, and the game area centres in what is left rather than in the whole canvas. It defaults to 0, so a layout that does not set it produces identical numbers to before. scatter opts in here as a worked example. The other five samples have the same latent overlap and can adopt it with their own values. Co-Authored-By: Claude Opus 5 --- apps/scatter/src/game/stateLayout.ts | 7 +++ .../utils-layout/src/createLayout.svelte.ts | 47 ++++++++++++------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/scatter/src/game/stateLayout.ts b/apps/scatter/src/game/stateLayout.ts index bd94c7eac..2293b97b7 100644 --- a/apps/scatter/src/game/stateLayout.ts +++ b/apps/scatter/src/game/stateLayout.ts @@ -11,4 +11,11 @@ export const { stateLayout, stateLayoutDerived } = createLayout({ landscape: { width: 1600, height: 900 }, portrait: { width: 800, height: 1422 }, }, + // height the UI bar takes along the bottom, in mainSizesMap units + safeAreaBottomMap: { + desktop: 220, + tablet: 260, + landscape: 240, + portrait: 380, + }, }); diff --git a/packages/utils-layout/src/createLayout.svelte.ts b/packages/utils-layout/src/createLayout.svelte.ts index 8706e3035..a39aca903 100644 --- a/packages/utils-layout/src/createLayout.svelte.ts +++ b/packages/utils-layout/src/createLayout.svelte.ts @@ -31,6 +31,14 @@ export const createLayout = (layoutOptions: { portrait: number; }; mainSizesMap: MainSizesMap; + /** + * Height the UI chrome occupies along the bottom, per layout type, in the same + * units as `mainSizesMap`. Reserving it keeps the game area clear of the UI bar + * when the canvas is wider than the design ratio - without it the game area + * takes the full canvas height and the bottom of the board renders behind the + * bar. Omit it and the layout is unchanged. + */ + safeAreaBottomMap?: Partial>; }) => { const canvasSizes = () => ({ width: innerWidth.current ?? 1, height: innerHeight.current ?? 1 }); // because of resizeTo: window const canvasRatio = () => getRatio(canvasSizes()); @@ -56,25 +64,32 @@ export const createLayout = (layoutOptions: { }; const isStacked = () => ['portrait', 'almostSquare'].includes(layoutType()); - const createMainLayout = (mainSizesMap: MainSizesMap) => () => { - const x = canvasSizes().width * 0.5; - const y = canvasSizes().height * 0.5; - const mainSizes = mainSizesMap[layoutType()]; - const widthScale = canvasSizes().width / mainSizes.width; - const heightScale = canvasSizes().height / mainSizes.height; - const scale = Math.min(widthScale, heightScale); + const createMainLayout = + ( + mainSizesMap: MainSizesMap, + safeAreaBottomMap?: Partial>, + ) => + () => { + const mainSizes = mainSizesMap[layoutType()]; + const safeAreaBottom = safeAreaBottomMap?.[layoutType()] ?? 0; + const widthScale = canvasSizes().width / mainSizes.width; + const heightScale = canvasSizes().height / (mainSizes.height + safeAreaBottom); + const scale = Math.min(widthScale, heightScale); + const x = canvasSizes().width * 0.5; + // centre in the area above the reserved strip, not in the whole canvas + const y = canvasSizes().height * 0.5 - safeAreaBottom * scale * 0.5; - return { - x, - y, - scale, - width: mainSizes.width, - height: mainSizes.height, - anchor: 0.5, + return { + x, + y, + scale, + width: mainSizes.width, + height: mainSizes.height, + anchor: 0.5, + }; }; - }; - const mainLayout = createMainLayout(layoutOptions.mainSizesMap); + const mainLayout = createMainLayout(layoutOptions.mainSizesMap, layoutOptions.safeAreaBottomMap); const mainLayoutStandard = createMainLayout(STANDARD_MAIN_SIZES_MAP);