<footer><div id=version></div></footer>
html, body {
  height: 100%;
}

body {
  margin: 0;
  padding: 0;
  background: #111 url(https://labs.phaser.io/assets/sprites/phaser3-logo-small.png) no-repeat 0 20px;
  color: #eee;
  font: caption;
}

#version {
  position: absolute;
  left: 0;
  top: 0;
  padding: 0;
  background: rgba(0, 0, 0, 0.5)
}
/* global colors, Phaser */

const { silver, gray, white, black, red, green, yellow } = colors.hexColors;

const { Clamp } = Phaser.Math;

class Window {
  constructor(scene, x, y, width, height, bounds) {
    // The container will hold the window decorations (for the scene's main camera).
    this.container = scene.sys.add.container(x, y);

    // This camera is the window viewport. It will hold the window contents.
    this.camera = scene.sys.cameras
      .add(x, y, width, height)
      .setBounds(bounds.x, bounds.y, bounds.width, bounds.height)
      .ignore(this.container);

    this.scrollBarX = new Phaser.GameObjects.Rectangle(
      scene,
      0,
      height,
      Clamp((width * width) / bounds.width, 16, width),
      16,
      silver
    )
      .setOrigin(0, 0)
      .setName("scrollBarX")
      .setInteractive({ draggable: true })
      .on("drag", this.onDragScrollX, this);

    this.scrollBarY = new Phaser.GameObjects.Rectangle(
      scene,
      width,
      0,
      16,
      Clamp((height * height) / bounds.height, 16, height),
      silver
    )
      .setOrigin(0, 0)
      .setName("scrollBarY")
      .setInteractive({ draggable: true })
      .on("drag", this.onDragScrollY, this);

    this.handle = new Phaser.GameObjects.Rectangle(
      scene,
      width,
      height,
      16,
      16,
      white
    )
      .setOrigin(0, 0)
      .setName("handle")
      .setInteractive({ cursor: "nwse-resize", draggable: true })
      .on("drag", this.onDragHandle, this);

    this.container.setName("window")
      .setInteractive({
        cursor: "grab",
        draggable: true,
        hitArea: new Phaser.Geom.Rectangle(0, 0, width, height),
        hitAreaCallback: Phaser.Geom.Rectangle.Contains
      })
      .on("drag", this.onDrag, this)
      .add(this.scrollBarX)
      .add(this.scrollBarY)
      .add(this.handle);
  }

  onDrag(pointer, dragX, dragY) {
    this.camera.setPosition(dragX, dragY);
    this.container.setPosition(dragX, dragY);
  }

  onDragScrollX(pointer, dragX, dragY) {
    // TODO
    dragX = Clamp(dragX, 0, this.camera.width - this.scrollBarX.width);
    this.scrollBarX.x = dragX;
    this.camera.scrollX =
      (dragX * (this.camera._bounds.width - this.camera.width)) /
      (this.camera.width - this.scrollBarX.width);
  }

  onDragScrollY(pointer, dragX, dragY) {
    // TODO
    dragY = Clamp(dragY, 0, this.camera.height - this.scrollBarY.height);
    this.scrollBarY.y = dragY;
    this.camera.scrollY =
      (dragY * (this.camera._bounds.height - this.camera.height)) /
      (this.camera.height - this.scrollBarY.height);
  }

  onDragHandle(pointer, dragX, dragY) {
    this.handle.setPosition(dragX, dragY);
    const { x, y } = this.handle;
    this.camera.setSize(x, y);
    this.container.input.hitArea.setSize(x, y);
    this.scrollBarX.x = (x * this.camera.scrollX) / this.camera._bounds.width;
    this.scrollBarX.y = y;
    this.scrollBarX.setSize(Clamp((x * x) / this.camera._bounds.width, 16, x), 16);
    // this.barX.visible = (this.barX.width < x);
    this.scrollBarY.x = x;
    this.scrollBarY.y = (y * this.camera.scrollY) / this.camera._bounds.height;
    this.scrollBarY.setSize(16, Clamp((y * y) / this.camera._bounds.height, 16, y));
    // this.barY.visible = (this.barY.height < y);
  }
}

class BootScene extends Phaser.Scene {
  preload() {
    this.load.image("grid", "assets/pics/debug-grid-1920x1920.png");
    this.load.image("nebula", "assets/tests/space/nebula.jpg");
  }

  create() {
    this.scene.start("play");
    this.scene.remove();
  }
}

class PlayScene extends Phaser.Scene {
  create() {
    const nebula = this.add.image(0, 0, "nebula").setOrigin(0, 0);
    const grid = this.add.image(0, 0, "grid").setOrigin(0, 0);

    const _window = new Window(this, 80, 80, 640, 640, {
      x: 0,
      y: 0,
      width: 1920,
      height: 1920
    });

    // The scrollbars
    _window.alpha = 0.5;

    console.log(_window);
    
    const layerInsideWindow = this.add.layer([
      grid
    ]).setName("layerInWindow");
    
    const layerOutsideWindow = this.add.layer([
      nebula,
      _window.container
    ]).setName("layerOutsideWindow");

    _window.camera.ignore(layerOutsideWindow);
    
    this.cameras.main.ignore(layerInsideWindow);
  }
}

document.getElementById("version").textContent = `Phaser v${Phaser.VERSION}`;

new Phaser.Game({
  scene: [new BootScene("boot"), new PlayScene("play")],
  loader: {
    baseURL: "https://labs.phaser.io",
    crossOrigin: "anonymous"
  }
});

Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.js
  2. https://cdn.jsdelivr.net/npm/@samme/colors@1.2.0/dist/colors.umd.js