npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@phaserjs/phaser-editor-layout

v1.4.1

Published

Responsive layout runtime for Phaser 3 — anchors, zones and safe areas. The runtime counterpart to Phaser Editor's responsive layout system.

Downloads

1,104

Readme

@phaserjs/phaser-editor-layout

A small responsive layout runtime for Phaser: anchor game objects to zones and device safe areas, scale them to fit, fit the whole scene with the camera, reserve space for HUDs, and swap per-orientation properties — all re-flowing automatically when the screen resizes or the device orientation changes.

It is the runtime counterpart to Phaser Editor's responsive layout system — the same resolver runs in the editor's live preview and in your game, so what you see in the editor is what you get at run time. It also works perfectly well as a standalone library without the editor.

Phaser version. The declared peer dependency is Phaser 3 (^3.60.0); the library only uses stable, long-standing Phaser APIs (scene plugins, the scale manager, cameras, getBounds) and also runs under Phaser 4 (install with --legacy-peer-deps until the peer range is widened).

Install

npm install @phaserjs/phaser-editor-layout

phaser is a peer dependency — the library uses whatever Phaser build your project already has. The declared range is Phaser 3 (^3.60.0); on a Phaser 4 project install with npm install @phaserjs/phaser-editor-layout --legacy-peer-deps.

Concepts

  • Scene plugin (this.layout). The library is a Phaser Scene Plugin, reached as this.layout, exactly like this.physics. It keeps the set of laid-out objects and re-resolves them when the screen changes.
  • obj.layoutData. Per-object layout state lives on the object as obj.layoutData (cf. obj.body for physics). Its anchor properties are split into variants (base / portrait / landscape); you configure layout by setting plain fields on them.
  • Zones. A zone is a screen-relative rectangle defined by margins from each screen edge (also split into base / portrait / landscape). The built-ins are screen (full bounds) and safeArea (insets supplied by the host). Make your own with createZone({ base, portrait?, landscape? }).
  • Anchors. Per axis you pick an edge (left/center/right, top/center/ bottom, or none) plus a pixel offset. The object's matching edge/center is aligned to the target's, then nudged by the offset.
  • Variants. Both zones and layout data carry a base plus portrait / landscape overrides. The active variant (see this.layout.variant) selects which overrides apply on top of base, so one object can lay out differently per orientation.
  • Scaling. Beyond positioning, an object can be uniformly scaled to fit its target zone via scaleMode (cover / fit-down). See Scaling to fit.
  • Camera fit. Instead of (or on top of) per-object work, setCameraFit zooms the main camera so a whole design zone fits the canvas — the cheapest way to scale a fixed design.
  • Reservations. A HUD (spin bar, win bar) can reserveSafeArea so the rest of the layout reflows around its live bounds. See Reserving space.
  • Per-variant property bindings. Any property (texture, tint, font size, Spine skin…) can differ per variant via the bind* helpers. See Per-variant properties.

Quick start

1. Register the plugin

import Phaser from "phaser";
import { LayoutPlugin } from "@phaserjs/phaser-editor-layout";

new Phaser.Game({
    type: Phaser.AUTO,
    scale: {
        mode: Phaser.Scale.RESIZE, // so the layout follows the window/orientation
        width: 1080,
        height: 1920,
    },
    plugins: {
        scene: [
            { key: "LayoutPlugin", plugin: LayoutPlugin, mapping: "layout" },
        ],
    },
    scene: [MainScene],
});

The mapping: "layout" makes the plugin available as this.layout in every scene, and the bundled TypeScript types make this.layout and obj.layoutData strongly typed.

2. Anchor objects in a scene

export class MainScene extends Phaser.Scene {

    create() {
        // Built-in zones are always available: this.layout.screen and this.layout.safeArea.
        // Make a custom zone with per-variant margins (top/right/bottom/left insets).
        const reelsZone = this.layout.createZone({ base: { marginTop: 360, marginRight: 90, marginBottom: 470, marginLeft: 90 } });

        // Top-centered logo, anchored to the safe area. Configure the `base` variant.
        const logo = this.add.image(0, 0, "logo");
        const logoData = this.layout.add(logo);
        logoData.base.targetType = "safeArea";
        logoData.base.horizontalAnchor = "center";
        logoData.base.verticalAnchor = "top";
        logoData.base.verticalOffset = 24;

        // Bottom-right button, 40px in from the safe-area corner.
        const hud = this.add.image(0, 0, "hud");
        const hudData = this.layout.add(hud);
        hudData.base.targetType = "safeArea";
        hudData.base.horizontalAnchor = "right";
        hudData.base.horizontalOffset = -40;
        hudData.base.verticalAnchor = "bottom";
        hudData.base.verticalOffset = -40;

        // Centered in a custom zone — "custom" is the one target that needs targetZone.
        const reels = this.add.image(0, 0, "reels");
        const reelsData = this.layout.add(reels);
        reelsData.base.targetType = "custom";
        reelsData.base.targetZone = reelsZone;
        reelsData.base.horizontalAnchor = "center";
        reelsData.base.verticalAnchor = "center";
    }
}

add() returns the object's AnchorLayoutData; you can also configure it via logo.layoutData!. Setting only base (as above) means the object lays out the same in every orientation — see Variants & orientation to vary it.

3. Feed the device safe area (optional)

The safe area is unknown at author time, so the host supplies it. On the web that's the CSS env(safe-area-inset-*) values; in a native wrapper it's a device plugin. Set the safeArea margins and re-resolve:

this.layout.safeArea.base.marginTop = insets.top;
this.layout.safeArea.base.marginRight = insets.right;
this.layout.safeArea.base.marginBottom = insets.bottom;
this.layout.safeArea.base.marginLeft = insets.left;
this.layout.refresh();

Anchoring inside a prefab (Parent Bounds)

A prefab is a GameObject (e.g. a Container), not a Scene, so it reaches the plugin through this.scene.layout. Use targetType: "parent" to lay children out against the prefab's own bounds:

export class CollectorPrefab extends Phaser.GameObjects.Container {

    constructor(scene: Phaser.Scene, x: number, y: number) {
        super(scene, x, y);
        this.setSize(300, 260); // containers have no intrinsic size

        const coinIcon = scene.add.image(0, 0, "coin");
        this.add(coinIcon);

        const icon = scene.layout.add(coinIcon);
        icon.base.targetType = "parent"; // resolves against this container's bounds
        icon.base.horizontalAnchor = "left";
        icon.base.horizontalOffset = 20;
        icon.base.verticalAnchor = "center";
    }
}

Variants & orientation

Both obj.layoutData and every zone carry three variants — base, portrait, landscape. base is the baseline; portrait / landscape hold only the fields that differ. The resolver uses active[prop] ?? base[prop], where the active variant is this.layout.variant.

Pick the active variant in one of two ways (default is manual, "base"):

// Manual:
this.layout.setVariant("portrait");

// Or orientation-driven — follows the screen on every resize:
this.layout.autoVariant(true);

Author once, and rotating re-flows automatically:

// Safe area: notch on top in portrait, on the left in landscape.
this.layout.safeArea.portrait.marginTop = 120;
this.layout.safeArea.landscape.marginLeft = 120;

// A button: bottom-center in portrait, right-center in landscape.
const data = this.layout.add(button);
data.base.targetType = "safeArea";
data.portrait.horizontalAnchor = "center";
data.portrait.verticalAnchor = "bottom";
data.portrait.verticalOffset = -40;
data.landscape.horizontalAnchor = "right";
data.landscape.horizontalOffset = -40;
data.landscape.verticalAnchor = "center";

this.layout.autoVariant(true);

See the mobile-orientation example for the full picture.

Scaling to fit (scaleMode)

Anchoring positions an object; scaleMode additionally scales it uniformly to fit its target zone (the same zone the anchor targets). It is a per-variant AnchorProps field:

  • "none" (default) — never scaled.
  • "fit-down" — shrink to fit inside the zone, preserving aspect ratio; never upscales beyond the authored scale.
  • "cover" — scale to fill the zone (may overflow), preserving aspect ratio.
const data = this.layout.add(reels);
data.base.targetType = "safeArea";
data.base.horizontalAnchor = "center";
data.base.verticalAnchor = "center";
data.base.scaleMode = "fit-down"; // shrink the reels to fit the safe area in any orientation

The scale stage runs before anchoring, so the object is aligned at its final size. It is idempotent across passes (scaling is always computed from the authored base scale). Containers (which have no intrinsic size) are measured by their live getBounds(), so fit-down / cover work on composite objects and prefab instances too.

Camera fit

For a fixed-design scene, the highest-quality way to scale to the display is to zoom the camera rather than each object. setCameraFit(zone?) zooms the main camera so zone (default safeArea) fits the live canvas, then centers on it:

this.layout.setDesignSize(1080, 1920); // author size the design was built at
this.layout.setCameraFit();            // fit the safe area; re-applied every pass
// this.layout.setCameraFit(false);    // disable and reset the camera

The zoom is min(canvasWidth / zoneWidth, canvasHeight / zoneHeight). Pair it with a fixed design size (via setDesignSize, or the game config width/height under FIT/ENVELOP).

Reserving space (for HUDs)

A HUD that must not overlap the play field — a bottom spin bar, a top win bar — can reserve an edge of a zone. Each pass, the object's live bounds are subtracted from that edge, and everything else (camera-fit and anchored objects) reflows around it:

// Reserve the safe area's bottom edge for the spin bar (defaults to "bottom").
this.layout.reserveSafeArea(spinBar);

// Or a specific edge, per variant — reserve the bottom in portrait, nothing in landscape:
this.layout.reserveSafeArea(sideBar, { base: "bottom", landscape: "none" });

// Any zone, any edge:
this.layout.reserveZone(winBar, this.layout.safeArea, "top");

this.layout.unreserveZone(spinBar); // stop reserving

top/bottom reserve the bounds height, left/right the width; multiple objects on the same edge stack. The reserving object itself is immune (it anchors against the full, unreserved zone). Reservations auto-remove on the object's DESTROY.

Per-orientation game size

For a fixed-design game under FIT / ENVELOP, one authored size gets letterboxed or cropped in the opposite orientation. autoGameSize gives portrait and landscape each a world shaped like their viewport, calling scale.setGameSize(...) for the live orientation on every resize:

this.layout.autoGameSize({
    base:      { width: 920, height: 1600 },
    portrait:  { width: 920, height: 1600 },
    landscape: { width: 1600, height: 920 },
});
this.layout.autoVariant(true); // usually paired, so the layout variant switches too
// this.layout.autoGameSize(null); // disable

No-op while a design size is injected (the editor preview owns sizing there), and guarded against redundant resizes and their feedback.

Per-variant property bindings

Anchoring/scaling covers geometry; to vary other properties per orientation, bind them. On every pass (and immediately) the plugin resolves values[variant] ?? values.base and applies it. Bindings are keyed by target + property, auto-removed on DESTROY.

// Generic field assignment (obj[key] = value):
this.layout.bindProperty(title, "visible", { base: true, landscape: false });
this.layout.bindProperty(bg, "tint", { base: 0xffffff, portrait: 0xffcc00 });

// Applied through a setter method instead of a field:
this.layout.bindTexture(button, { base: { key: "btn" }, landscape: { key: "btn_wide" } });
this.layout.bindOriginX(label, { base: 0.5, landscape: 0 });
this.layout.bindMethod(scoreText, "setFontSize", { base: 48, portrait: 32 });

// Spine (no need to import the Spine plugin):
this.layout.bindSkin(hero, { base: "default", landscape: "wide" });
this.layout.bindSkeleton(hero, { base: "hero", landscape: "hero_alt" }, "hero-atlas");

this.layout.unbind(title, "visible"); // remove one binding (or unbind(obj) for all)

When does layout resolve?

  • Automatically once after the scene's create, and on every scale resize (window resize, orientation change, etc.).
  • Manually via this.layout.refresh() after you mutate obj.layoutData or the safeArea margins at run time. There is no setter to hook — fields are set directly — so an explicit refresh() is how changes take effect immediately.

refresh() always runs a full, ordered pass (parents before children), so a child anchored to its parent always sees the parent's resolved position. The pass is just arithmetic over the registered objects, so it is cheap.

Reacting to layout

The plugin fires layoutupdate after each pass:

import { LAYOUT_UPDATE_EVENT } from "@phaserjs/phaser-editor-layout";

this.layout.on(LAYOUT_UPDATE_EVENT, () => {
    // reposition non-layout decorations, redraw overlays, etc.
});

this.layout.on/once/off delegate to the plugin's events emitter (this.layout.events).

API

this.layout (LayoutPlugin)

| Member | Description | | --- | --- | | safeArea: LayoutZone | Screen inset by the device safe-area insets. Set safeArea.base.margin* (or per-variant) from the host. | | screen: LayoutZone | The full screen bounds. | | variant: VariantName | The active variant ("base" \| "portrait" \| "landscape"). Defaults to "base". | | add(obj): AnchorLayoutData | Enable layout for obj: attaches a default obj.layoutData, registers it, and returns the data to configure. | | remove(obj): void | Stop laying out obj and clear its layoutData. | | createZone(variants?): LayoutZone | Create a custom screen-relative zone, optionally setting per-variant margins ({ base?, portrait?, landscape? }, each a partial of the four margin* sides). Assign it to data.<variant>.targetZone with targetType: "custom". | | setVariant(variant): void | Manually set the active variant (turns off auto) and re-resolve. | | autoVariant(enabled?): void | Follow the screen orientation on every resize (portrait / landscape); disabling resets to "base". | | get orientation() | The screen's current orientation ("portrait" if height > width, else "landscape"). | | get designSize() | The injected fixed design size, or null. | | setDesignSize(w, h): void / clearDesignSize() | Set / clear the fixed design size zones and camera-fit are computed against (the editor preview injects this). | | autoGameSize(sizes \| null): void | Reshape the game world (scale.setGameSize) per orientation; null disables. See Per-orientation game size. | | setCameraFit(zone?): void | Zoom/center the main camera to fit zone (default safeArea); false/null disables. See Camera fit. | | reserveZone(obj, zone?, edge?): void | Reserve an edge of zone (default safeArea, "bottom") for obj's live bounds; edge may be per-variant. | | reserveSafeArea(obj, edge?): void | Sugar for reserveZone against safeArea. | | unreserveZone(obj): void | Stop reserving space for obj. | | bindProperty(obj, key, values, apply?): void | Bind a property to per-variant values (values[variant] ?? base), applied as obj[key] = value or via apply. | | bindTexture / bindOriginX / bindOriginY / bindMethod / bindSkin / bindSkeleton | Per-variant binders that apply through the matching setter (setTexture, setOrigin, obj[method](v), Spine skin/skeleton). | | unbind(obj, key?): void | Remove one binding (key) or all bindings for obj. | | refresh(): void | Re-resolve every enabled object now (full, ordered pass). | | resolveList(objects): void | Resolve a specific, pre-ordered list of objects (used by the editor preview). | | events: Phaser.Events.EventEmitter | Emits layoutupdate after each pass. | | on/once/off(...) | Delegates to events. |

AnchorLayoutData (obj.layoutData)

| Field | Type | Description | | --- | --- | --- | | enabled | boolean | When false, the object keeps its raw x/y and is skipped ("Absolute" mode). | | base | AnchorProps | Baseline anchor properties; the resolver falls back here for unset variant fields. | | portrait | AnchorProps | Overrides applied when variant === "portrait". | | landscape | AnchorProps | Overrides applied when variant === "landscape". |

Defaults from add(obj): enabled: true, base = { targetType: "screen", anchors "none", zero offsets }, empty portrait / landscape — i.e. inert until you choose anchors.

AnchorProps (data.base / data.portrait / data.landscape)

All fields optional; on portrait / landscape an unset field inherits from base.

| Field | Type | Description | | --- | --- | --- | | targetType | "screen" \| "safeArea" \| "parent" \| "custom" | What to anchor to: the screen, the device safe area, the object's parent-container bounds, or a custom zone. | | targetZone | LayoutZone? | The zone to anchor to when targetType === "custom". | | horizontalAnchor | "left" \| "center" \| "right" \| "none" | Horizontal anchor edge. none keeps the current x. | | horizontalOffset | number | Pixel offset applied after the horizontal anchor. | | verticalAnchor | "top" \| "center" \| "bottom" \| "none" | Vertical anchor edge. none keeps the current y. | | verticalOffset | number | Pixel offset applied after the vertical anchor. | | scaleMode | "none" \| "cover" \| "fit-down" | Uniformly scale the object to fit its target zone before anchoring. none (default) leaves scale alone. See Scaling to fit. |

LayoutZone

| Member | Description | | --- | --- | | base: { marginTop, marginRight, marginBottom, marginLeft } | Baseline insets from each screen edge, in pixels. | | portrait / landscape | Partial margin overrides for those variants; unset sides inherit base. | | setMargins({ base?, portrait?, landscape? }): this | Set margins for one or more variants in a single call, merging into what's there (only supplied sides change). Returns the zone for chaining. Handy for the built-in safeArea / screen. | | getRect(out?): Phaser.Geom.Rectangle | The effective rect at the current screen size and active variant. Pass out to avoid allocations. |

Resolution semantics

Given the target rect t and the object size w×h (its displayWidth / displayHeight), each axis aligns the object's edge/center to the target's, accounting for the object's origin:

  • left / top → align the object's left/top edge to t.left/t.top + offset.
  • right / bottom → align its right/bottom edge to t.right/t.bottom + offset.
  • center → align its center to the target's center + offset.
  • none → leave that axis unchanged.

For centered-origin objects (origin = 0.5) this reduces to the familiar x = t.left + offset + w / 2 form.

Each object is resolved in two stages: scale (if scaleMode !== "none") then anchor, so alignment uses the object's final size. Containers (and prefab instances) have no intrinsic size, so both stages measure them by their live getBounds() box — the visible box edge/center is aligned to the target, and content never pokes outside the zone.

The properties used (targetType, anchors, offsets, scaleMode) and the zone rect t are first resolved for the active variant as active[prop] ?? base[prop], so portrait/landscape overrides and per-variant zone margins both feed into the same alignment math.

Notes & current limitations

  • Camera. Zones are computed in screen pixels, treated as world coordinates; this is exactly what setCameraFit drives. A scrolled/zoomed camera you manage yourself (other than via setCameraFit) is not special-cased.
  • Parent Bounds geometry. With targetType: "parent", the parent container's rect is taken as (0, 0, parent.width, parent.height) in its local space, so call setSize(...) on the container. Children are aligned within that rect; parent rotation is not specially handled.
  • No non-uniform stretch. Scaling is uniform (fit-down / cover); a single axis "fill width minus margins" stretch is not expressible. Use scaleMode + camera fit for the common cases.
  • Single built-in safe area. There is one host-fed safeArea zone (plus your custom zones). A separate GUI safe zone is not built in.
  • Serialization is out of scope here. This package is the runtime; the editor owns how layout data and zone references are stored in .scene files.

License

MIT