chromium/ash/webui/camera_app_ui/resources/js/views/camera/layout.ts

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {CameraManager} from '../../device/index.js';
import * as dom from '../../dom.js';
import * as state from '../../state.js';
import {windowController} from '../../window_controller.js';

/**
 * Creates a controller to handle layouts of Camera view.
 */
export class Layout {
  private readonly previewBox = dom.get('#preview-box', HTMLDivElement);

  private readonly faceOverlay =
      dom.get('#preview-face-overlay', HTMLCanvasElement);

  private readonly rootStyle = document.documentElement.style;

  constructor(private readonly cameraManager: CameraManager) {}

  private setContentSize(width: number, height: number) {
    // Not using attributeStyleMap / StylePropertyMap here since custom
    // properties can only use CSSUnparsedValue, which doesn't make the code
    // simpler. (@property / CSS.registerProperty only applies when the var is
    // computed, but doesn't affect the type when the var is set, See
    // https://drafts.css-houdini.org/css-properties-values-api/#parsing-custom-properties)
    this.rootStyle.setProperty(
        '--preview-content-width', CSS.px(width).toString());
    this.rootStyle.setProperty(
        '--preview-content-height', CSS.px(height).toString());
    this.faceOverlay.width = width;
    this.faceOverlay.height = height;
  }

  private setViewportSize(width: number, height: number) {
    this.rootStyle.setProperty(
        '--preview-viewport-width', CSS.px(width).toString());
    this.rootStyle.setProperty(
        '--preview-viewport-height', CSS.px(height).toString());
  }

  /**
   * Sets the offset between video content and viewport.
   */
  private setContentOffset(dx: number, dy: number) {
    this.rootStyle.setProperty('--preview-content-left', CSS.px(dx).toString());
    this.rootStyle.setProperty('--preview-content-top', CSS.px(dy).toString());
  }

  /**
   * Updates the layout for video-size or window-size changes.
   */
  update(): void {
    const fullWindow = windowController.isFullscreenOrMaximized();
    const tall = window.innerHeight > window.innerWidth;
    state.set(state.State.TABLET_LANDSCAPE, fullWindow && !tall);
    state.set(state.State.MAX_WND, fullWindow);
    state.set(state.State.TALL, tall);

    const {width: boxW, height: boxH} = this.previewBox.getBoundingClientRect();
    const video = dom.get('#preview-video', HTMLVideoElement);

    // When the app is minimized, the width and height of the video will be
    // zero. Don't update the layout for such case.
    const {videoWidth, videoHeight} = video;
    if (videoWidth === 0 || videoHeight === 0) {
      return;
    }

    if (this.cameraManager.useSquareResolution()) {
      const viewportSize = Math.min(boxW, boxH);
      this.setViewportSize(viewportSize, viewportSize);
      const scale = viewportSize / Math.min(videoHeight, videoWidth);
      const contentW = scale * videoWidth;
      const contentH = scale * videoHeight;
      this.setContentSize(contentW, contentH);
      this.setContentOffset(
          (viewportSize - contentW) / 2, (viewportSize - contentH) / 2);
    } else {
      const scale = Math.min(boxH / videoHeight, boxW / videoWidth);
      const contentW = scale * videoWidth;
      const contentH = scale * videoHeight;
      this.setViewportSize(contentW, contentH);
      this.setContentSize(contentW, contentH);
      this.setContentOffset(0, 0);
    }
  }
}