<div id=parent></div>
<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 {
}
/* global colors, Phaser */

const { Overlaps } = Phaser.Geom.Rectangle;
const { Wrap } = Phaser.Math;

class Example extends Phaser.Scene {
  controls;
  map;
  mapRect;

  preload() {
    this.load.atlas(
      "bird",
      "assets/animations/bird.png",
      "assets/animations/bird.json"
    );
    this.load.image("melon", "assets/sprites/melon.png");
    this.load.image("tiles", "assets/tilemaps/tiles/tmw_desert_spacing.png");
    this.load.tilemapTiledJSON("map", "assets/tilemaps/maps/desert.json");

    this.textures.once("addtexture-bird", () => {
      this.anims.create({
        key: "walk",
        frames: this.anims.generateFrameNames("bird", {
          prefix: "frame",
          end: 9
        }),
        repeat: -1
      });
    });
  }

  create() {
    this.map = this.make.tilemap({ key: "map" });

    const { widthInPixels, heightInPixels } = this.map;

    this.mapRect = new Phaser.Geom.Rectangle(
      0,
      0,
      widthInPixels,
      heightInPixels
    );

    this.map.createLayer("Ground", this.map.addTilesetImage("Desert", "tiles"));

    this.add.image(0.5 * widthInPixels, 0.5 * heightInPixels, "melon");

    this.add.sprite(64, 64, "bird").play("walk");

    this.add
      .sprite(widthInPixels - 64, heightInPixels - 64, "bird")
      .play("walk")
      .setFlipX(true);

    // Guides
    this.add
      .graphics()
      .lineStyle(2, colors.hexColors.white, 0.5)
      .lineBetween(0, 0, widthInPixels, heightInPixels)
      .lineBetween(0, heightInPixels, widthInPixels, 0)
      .strokeEllipse(
        0.5 * heightInPixels,
        0.5 * widthInPixels,
        heightInPixels,
        widthInPixels,
        64
      );

    this.cameras.main.setName("main");

    this.cameras.add().setAlpha(0.8).setName("X");
    this.cameras.add().setAlpha(0.8).setName("Y");
    this.cameras.add().setAlpha(0.8).setName("XY");

    this.cursors = this.input.keyboard.createCursorKeys();
    
    const { left, right, up, down } = this.cursors;
    
    const controlConfig = {
      camera: this.cameras.main,
      left: left,
      right: right,
      up: up,
      down: down,
      speed: 1
    };
    this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
  }

  update(time, delta) {
    this.controls.update(delta);

    const { widthInPixels, heightInPixels } = this.map;
    const [main, camX, camY, camXY] = this.cameras.cameras;

    // Wrap the main camera view.
    
    main.scrollX = Wrap(main.scrollX, -main.width, widthInPixels - main.width);
    main.scrollY = Wrap(
      main.scrollY,
      -main.height,
      heightInPixels - main.height
    );

    const { scrollX, scrollY } = main;
    
    // Scroll the extra cameras.

    camX.scrollX =
      scrollX > 0 ? scrollX - widthInPixels : scrollX + widthInPixels;
    camX.scrollY = scrollY;

    camY.scrollX = scrollX;
    camY.scrollY =
      scrollY > 0 ? scrollY - heightInPixels : scrollY + heightInPixels;

    camXY.scrollX = camX.scrollX;
    camXY.scrollY = camY.scrollY;
    
    // Update worldViews.

    camX.preRender();
    camY.preRender();
    camXY.preRender();
    
    // Hide cameras scrolled outside the world.

    camX.visible = Overlaps(this.mapRect, camX.worldView);
    camY.visible = Overlaps(this.mapRect, camY.worldView);
    camXY.visible = Overlaps(this.mapRect, camXY.worldView);
    
    // Wrap sprites.

    Phaser.Actions.WrapInRectangle(
      this.sys.displayList.getChildren(),
      this.mapRect
    );
  }
}

class DebugCameraPlugin extends Phaser.Plugins.ScenePlugin {
  boot() {
    if (!this.systems.renderer.gameContext) {
      throw new Error("CANVAS renderer only");
    }

    this.systems.events
      .on("render", this.render, this)
      .on("destroy", this.sceneDestroy, this);
  }

  render() {
    const ctx = this.systems.renderer.gameContext;

    ctx.font = "16px monospace";
    ctx.textBaseline = "top";

    const tx = 0;
    let ty = 0;

    for (const cam of this.systems.cameras.cameras) {
      ctx.fillStyle = "rgba(0,0,0,0.5)";
      ctx.fillRect(tx, ty, cam.width, 20);
      ctx.fillStyle = "white";
      ctx.fillText(this.describe(cam), tx, ty, cam.width);

      ty += 20;
    }
  }

  sceneDestroy() {
    this.systems.events.off("render", this.render, this);
  }

  describe(cam) {
    const { name, scrollX, scrollY, visible, worldView } = cam;

    return (
      (visible ? "• " : "  ") +
      `${name} ` +
      `scroll=(${scrollX.toFixed(0)} ${scrollY.toFixed(0)}) ` +
      `worldView=(${worldView.left.toFixed(0)} ${worldView.top.toFixed(
        0
      )} ${worldView.right.toFixed(0)} ${worldView.bottom.toFixed(0)})`
    );
  }
}

const config = {
  type: Phaser.CANVAS,
  width: 640,
  height: 640,
  // backgroundColor: colors.hexColors.green,
  parent: "parent",
  pixelArt: true,
  scene: Example,
  loader: {
    baseURL: "https://labs.phaser.io/",
    crossOrigin: "anonymous"
  },
  plugins: {
    scene: [{ key: "DebugCamera", plugin: DebugCameraPlugin }]
  }
};

const game = new Phaser.Game(config);

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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

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