Source: Layout.js

/**
 * @license GPL-3.0-only
 *
 * @author Mark Mayes / mm-dev
 *
 *
 * @module Layout
 *
 * @description
 * ## Set some important variables for other classes to use, based on the size and orientation of the viewport
 *
 */

import { PD } from "./PD/CONST.js";
import { ASPECT_RATIO, HORIZ_ALIGN, RECTANGLE, VERT_ALIGN } from "./PD/ENUM.js";
import * as GAME from "./PD/GAME.js";

import { Game } from "./Game.js";
import { Text } from "./Text.js";
import { __, roundToPlaces } from "./utils.js";

class Layout {}

/**
 * @function init
 * @static
 *
 * @description
 * ##### Simple initialisation
 * This just calls the update function with a default gameplay area size so that the caclulations have something to work with
 */
Layout.init = function () {
  __("Layout.init()::", PD.FMT_LAYOUT);
  // TODO magic but means fill canvas, used when this function is called the first time
  Layout.update({ gameplayAreaToCanvasLateralRatio: 1 });
};

/**
 * @function setProportionalSizes
 * @static
 *
 * @description
 * ##### Calculate some values which are used heavily throughout the app
 * - `Layout.proportionalMultiplier` - Used to shrink/grow things in relation to an ideal (`GAME.SCALING_TARGET_SIZE`), to try to make things feel consistent between highly variable screen sizes
 * - `Layout.mainPaddingProportional` - Common padding used eg between text/icons and the edge of the screen
 * - `Layout.textBgPaddingProportional` - Extra spacing around text and the borders of its background
 * - `Layout.hitAreaPaddingProportional` - Invisible spacing around tappable items (eg icons) to make them easier to hit
 */
Layout.setProportionalSizes = function () {
  // used to relatively-size some objects based on canvas size
  Layout.proportionalMultiplier =
    Math.sqrt(Layout.canvas_rect.right * Layout.canvas_rect.bottom) /
    GAME.SCALING_TARGET_SIZE;
  Layout.proportionalMultiplier = roundToPlaces(
    Layout.proportionalMultiplier,
    4
  );
  __(
    "Layout.proportionalMultiplier: " + Layout.proportionalMultiplier,
    PD.FMT_LAYOUT
  );

  Layout.mainPaddingProportional = Math.ceil(
    GAME.MAIN_PADDING_PX * Layout.proportionalMultiplier
  );

  Layout.textBgPaddingProportional = Math.ceil(
    Text.drawnCharWidth *
      GAME.TEXT_BG_PADDING_TO_CHARWIDTH_RATIO *
      Layout.proportionalMultiplier
  );

  Layout.hitAreaPaddingProportional =
    Layout.mainPaddingProportional + Layout.textBgPaddingProportional;
};

/**
 * @function update
 * @static
 *
 * @description
 * ##### Calculate sizes (particularly recctangles) for lots of important parts of the game and UI
 *
 * @param {object} _data
 * @param {number} _data.gameplayAreaToCanvasLateralRatio - This value is set per-level and sets the size of the gameplay area (between the pipe walls) as a ratio of the canvas size
 */
Layout.update = function (_data) {
  __("Layout.update()::", PD.FMT_LAYOUT);
  Layout.gameplayAreaToCanvasLateralRatio =
    _data.gameplayAreaToCanvasLateralRatio;

  Layout.canvasWidth = Math.round(
    document.documentElement.clientWidth / GAME.PIXEL_SCALE
  );
  Layout.canvasHeight = Math.round(
    document.documentElement.clientHeight / GAME.PIXEL_SCALE
  );

  // Don't switch aspect ratios during a session
  if (!Layout.sessionAspectRatio) {
    if (Layout.canvasWidth > Layout.canvasHeight) {
      Layout.sessionAspectRatio = ASPECT_RATIO.LANDSCAPE;
      Layout.currentAspectUI = GAME.UI_LANDSCAPE;
    } else {
      Layout.sessionAspectRatio = ASPECT_RATIO.PORTRAIT;
      Layout.currentAspectUI = GAME.UI_PORTRAIT;
    }
  }

  Layout.gameplayWidth = Layout.canvasWidth;
  Layout.gameplayHeight = Layout.canvasHeight;
  // Size field-of-play as a % of canvas size depending on level (`gameplayAreaToCanvasLateralRatio` comes from level data)
  if (Layout.sessionAspectRatio === ASPECT_RATIO.LANDSCAPE) {
    Layout.gameplayHeight *= Layout.gameplayAreaToCanvasLateralRatio;
  } else {
    Layout.gameplayWidth *= Layout.gameplayAreaToCanvasLateralRatio;
  }
  __("\tLayout.gameplayHeight: " + Layout.gameplayHeight, PD.FMT_LAYOUT);
  __("\tLayout.gameplayWidth: " + Layout.gameplayWidth, PD.FMT_LAYOUT);

  Layout.canvas_rect = Layout.getRectangle(RECTANGLE.CANVAS);
  Layout.gameplay_rect = Layout.getRectangle(RECTANGLE.GAMEAREA);
  Layout.background_rect = Layout.getRectangle(RECTANGLE.BACKGROUND);
  Layout.floating_rect = Layout.getRectangle(RECTANGLE.FLOATING);
  __(
    "\tLayout.canvas_rect: " + JSON.stringify(Layout.canvas_rect),
    PD.FMT_LAYOUT
  );
  __(
    "\tLayout.gameplay_rect: " + JSON.stringify(Layout.gameplay_rect),
    PD.FMT_LAYOUT
  );
  __(
    "\tLayout.background_rect: " + JSON.stringify(Layout.background_rect),
    PD.FMT_LAYOUT
  );
  __(
    "\tLayout.floating_rect: " + JSON.stringify(Layout.floating_rect),
    PD.FMT_LAYOUT
  );

  // Convert % control area sizes to px
  if (Game.deviceIsTouchEnabled) {
    Layout.currentAspectUI.controller.height =
      ((Layout.canvas_rect.bottom - Layout.canvas_rect.top) / 100) *
      Layout.currentAspectUI.controller.touchHeightPercent;
    Layout.currentAspectUI.controller.width =
      ((Layout.canvas_rect.right - Layout.canvas_rect.left) / 100) *
      Layout.currentAspectUI.controller.touchWidthPercent;
  } else {
    Layout.currentAspectUI.controller.height =
      ((Layout.canvas_rect.bottom - Layout.canvas_rect.top) / 100) *
      Layout.currentAspectUI.controller.mouseHeightPercent;
    Layout.currentAspectUI.controller.width =
      ((Layout.canvas_rect.right - Layout.canvas_rect.left) / 100) *
      Layout.currentAspectUI.controller.mouseWidthPercent;
  }

  if (Layout.sessionAspectRatio === ASPECT_RATIO.LANDSCAPE) {
    Layout.gameAreaOffsetLateral =
      (Layout.canvasHeight - Layout.gameplayHeight) / 2;
  } else {
    Layout.gameAreaOffsetLateral =
      (Layout.canvasWidth - Layout.gameplayWidth) / 2;
  }

  Layout.setProportionalSizes();

  Layout.soundToggleIcon_rect = Layout.getRectangle(RECTANGLE.SOUNDTOGGLEICON);

  Layout.fullscreenToggleIcon_rect = Layout.getRectangle(
    RECTANGLE.FULLSCREENICON
  );

  Layout.mainTitle_rect = Layout.getRectangle(RECTANGLE.MAINTITLE);
};

/**
 * @function getRectangle
 * @static
 *
 * @description
 * ##### Based on current layout/dimensions, get a rectangle describing the position and size of a specific object (eg gameplay area, title image, sound icon)
 *
 * @param {RECTANGLE} _type - Which rectangle do we want?
 *
 * @returns {object} A rectangle with `top`, `right`, `bottom`, `left` properties
 */
Layout.getRectangle = function (_type) {
  var rect, pos, w, h;

  if (_type === RECTANGLE.CANVAS) {
    rect = {
      left: 0,
      right: Layout.canvasWidth,
      top: 0,
      bottom: Layout.canvasHeight,
    };
    //
    //
  } else if (_type === RECTANGLE.BACKGROUND) {
    // The player is always laterally in the midpoint of the canvas
    // So the furthest outside the gameplay area that will be displayed is half a canvas
    rect = {
      left: 0 - Layout.gameplayWidth / 2,
      right: Layout.canvasWidth + Layout.gameplayWidth / 2,
      top: 0 - Layout.gameplayHeight / 2,
      bottom: Layout.canvasHeight + Layout.gameplayHeight / 2,
    };
    //
    //
  } else if (_type === RECTANGLE.FLOATING) {
    rect = {
      top: Layout.canvas_rect.top,
      right: Layout.canvas_rect.right,
      bottom: Layout.canvas_rect.bottom,
      left: Layout.canvas_rect.left,
    };
    if (Layout.sessionAspectRatio === ASPECT_RATIO.LANDSCAPE) {
      rect.top -= (Layout.canvasHeight / 2) * GAME.FLOATING_LATERAL_MULTIPLIER;
      rect.bottom +=
        (Layout.canvasHeight / 2) * GAME.FLOATING_LATERAL_MULTIPLIER;
    } else {
      rect.left -= (Layout.canvasWidth / 2) * GAME.FLOATING_LATERAL_MULTIPLIER;
      rect.right += (Layout.canvasWidth / 2) * GAME.FLOATING_LATERAL_MULTIPLIER;
    }
    //
    //
  } else if (_type === RECTANGLE.GAMEAREA) {
    rect = {
      left: 0,
      right: Layout.gameplayWidth,
      top: 0,
      bottom: Layout.gameplayHeight,
    };
    if (Layout.sessionAspectRatio === ASPECT_RATIO.LANDSCAPE) {
      rect.right *= 2;
    } else {
      rect.bottom *= 2;
    }
    //
    //
  } else if (_type === RECTANGLE.MAINTITLE) {
    w = Layout.canvasWidth - Layout.mainPaddingProportional * 2;
    h = w / (GAME.MAINTITLE_WIDTH_PX / GAME.MAINTITLE_HEIGHT_PX);
    w *= Layout.currentAspectUI.mainTitle.sizeRatio;
    h *= Layout.currentAspectUI.mainTitle.sizeRatio;
    pos = Layout.getAlignedPos(
      Layout.currentAspectUI.mainTitle.alignH,
      Layout.currentAspectUI.mainTitle.alignV,
      w,
      h,
      0,
      //0
      Layout.currentAspectUI.mainTitle.offsetByCharsV * Text.drawnCharHeight
    );
    rect = {
      left: pos.x,
      right: pos.x + w,
      top: pos.y,
      bottom: pos.y + h,
    };
    //
    //
  } else if (_type === RECTANGLE.SOUNDTOGGLEICON) {
    pos = Layout.getAlignedPos(
      Layout.currentAspectUI.soundToggle.alignH,
      Layout.currentAspectUI.soundToggle.alignV,
      Text.drawnCharWidth,
      Text.drawnCharHeight
    );
    rect = {
      left: pos.x - Layout.hitAreaPaddingProportional,
      right: pos.x + Text.drawnCharWidth + Layout.hitAreaPaddingProportional,
      top: pos.y - Layout.hitAreaPaddingProportional,
      bottom: pos.y + Text.drawnCharHeight + Layout.hitAreaPaddingProportional,
    };
    //
    //
  } else if (_type === RECTANGLE.FULLSCREENICON) {
    pos = Layout.getAlignedPos(
      Layout.currentAspectUI.fullscreenToggle.alignH,
      Layout.currentAspectUI.fullscreenToggle.alignV,
      Text.drawnCharWidth,
      Text.drawnCharHeight
    );
    rect = {
      left: pos.x - Layout.hitAreaPaddingProportional,
      right: pos.x + Text.drawnCharWidth + Layout.hitAreaPaddingProportional,
      top: pos.y - Layout.hitAreaPaddingProportional,
      bottom: pos.y + Text.drawnCharHeight + Layout.hitAreaPaddingProportional,
    };
  }
  return rect;
};

/**
 * @function getAlignedPos
 * @static
 *
 * @description
 * ##### Based on a desired alignment and size of an item, calculate its coordinates
 *
 * @param {HORIZ_ALIGN} _alignmentH - Horizontal alignment
 * @param {VERT_ALIGN} _alignmentV - Vertical alignment
 * @param {number} _itemWidth - The height of the item
 * @param {number} _itemHeight - The width of the item
 * @param {number} [_offsetH] - A horizontal offset to be applied after the basic calculation
 * @param {number} [_offsetV] - A vertical offset to be applied after the basic calculation
 *
 * @returns {object}  x/y coordinates for the item
 */
Layout.getAlignedPos = function (
  _alignmentH,
  _alignmentV,
  _itemWidth,
  _itemHeight,
  _offsetH,
  _offsetV
) {
  var x,
    y,
    padding = Layout.mainPaddingProportional,
    offsetH = _offsetH || 0,
    offsetV = _offsetV || 0;

  if (_alignmentH === HORIZ_ALIGN.LEFT) {
    x = padding;
  } else if (_alignmentH === HORIZ_ALIGN.RIGHT) {
    x = Layout.canvas_rect.right - padding - _itemWidth;
  } else if (_alignmentH === HORIZ_ALIGN.CENTER) {
    x = Layout.canvas_rect.right / 2 - _itemWidth / 2;
  } else {
    __(
      "Layout::getAlignedPos:: _alignmentH NOT RECOGNISED: " + _alignmentH,
      PD.FMT_ERROR
    );
  }

  if (_alignmentV === VERT_ALIGN.TOP) {
    y = padding;
  } else if (_alignmentV === VERT_ALIGN.BOTTOM) {
    y = Layout.canvas_rect.bottom - padding - _itemHeight;
  } else if (_alignmentV === VERT_ALIGN.CENTER) {
    y = Layout.canvas_rect.bottom / 2 - _itemHeight / 2;
  } else {
    __(
      "Layout::getAlignedPos:: _alignmentV NOT RECOGNISED: " + _alignmentH,
      PD.FMT_ERROR
    );
  }
  x += offsetH;
  y += offsetV;
  return { x: Math.round(x), y: Math.round(y) };
};

export { Layout };