← Back to home
Workshop series

Scene Composition and Catalog Grid

Adaptive Layout for a Poki HTML5 Game Section 5 of 11

7. Complex Scene Composition Stays in Game Components

Some scenes need more than centering and scaling. The item scene has an album, title, progress bar, and buttons.

ItemSceneLayoutComponent describes the composition:

private applyLayout(): void {
  const orientation = this.node.screen.getOrientation();
  const screen = this.node.screen.getVirtualSize();
  this.applyButtonScale(orientation);
  this.positionAlbumStage(screen.width, screen.height, orientation);
  this.sizeTitleCover(screen.width);
  this.sizeAlbumProgress();
  this.positionAlbumProgress();
  this.positionTitle();
  this.positionPlayButton(screen.height);
  this.positionAlbumsButton(screen.height);
}

The album size uses different axes:

const targetScale = orientation === 'port'
  ? (screenWidth * this.config.portImageWidthScreenPercent) / frameWidth
  : (screenHeight * this.config.landImageHeightScreenPercent) / frameHeight;

albumStage.setScale(targetScale, targetScale);
albumStage.setPosition(Math.round(screenWidth / 2), Math.round(screenHeight / 2));

In portrait, width matters more. In landscape, height matters more.

The layout JSON exposes this as config:

{
  "landImageHeightScreenPercent": 0.6,
  "portImageWidthScreenPercent": 0.6,
  "landPlayButtonScale": 1,
  "portPlayButtonScale": 1
}

This is adaptive layout, not just responsive scaling.

8. Catalog Grid: Different Columns per Orientation

The items scene changes the album catalog grid:

  • landscape: 5 columns;
  • portrait: 2 columns.

Config:

{
  "itemsAlbumsGrid": {
    "type": "ItemsAlbumsGridComponent",
    "enabled": true,
    "data": {
      "landColumns": 5,
      "portColumns": 2,
      "horizontalPaddingPx": 48,
      "bottomPaddingPx": 48,
      "columnGapPx": 28,
      "rowGapPx": 28,
      "maxScale": 1
    }
  }
}

Code:

private resolveColumns(): number {
  return this.node.screen.getOrientation() === 'land'
    ? this.config.landColumns
    : this.config.portColumns;
}

After that, the grid measures cards, computes the total grid size, and fits the result into the available area:

const fitScale = ScreenFitLayoutResolver.resolveScale({
  contentWidth: gridWidth <= 0 ? 1 : gridWidth,
  contentHeight: gridHeight <= 0 ? 1 : gridHeight,
  availableWidth,
  availableHeight,
  minScale: 0,
  maxScale: this.config.maxScale,
});

This is a practical pattern:

  1. Choose the UX structure for the current orientation.
  2. Measure the actual content.
  3. Fit the result into the screen.