diff --git a/README.md b/README.md
index 489569478..9dd8e5346 100644
--- a/README.md
+++ b/README.md
@@ -163,30 +163,29 @@ Browse all examples [here](https://melonjs.github.io/melonJS/examples/)
### Basic Hello World Example
```JavaScript
-import * as me from "https://cdn.jsdelivr.net/npm/melonjs/+esm";
-
-me.device.onReady(function () {
- // initialize the display canvas once the device/browser is ready
- if (!me.video.init(1218, 562, {parent : "screen", scale : "auto"})) {
- alert("Your browser does not support HTML5 canvas.");
- return;
- }
-
- // set a gray background color
- me.game.world.backgroundColor.parseCSS("#202020");
-
- // add a font text display object
- me.game.world.addChild(new me.Text(609, 281, {
- font: "Arial",
- size: 160,
- fillStyle: "#FFFFFF",
- textBaseline : "middle",
- textAlign : "center",
- text : "Hello World !"
- }));
+import { Application, Text } from "https://cdn.jsdelivr.net/npm/melonjs/+esm";
+
+// create a new melonJS application
+const app = new Application(1218, 562, {
+ parent: "screen",
+ scale: "auto",
+ backgroundColor: "#202020",
});
+
+// set a gray background color
+app.world.backgroundColor.parseCSS("#202020");
+
+// add a font text display object
+app.world.addChild(new Text(609, 281, {
+ font: "Arial",
+ size: 160,
+ fillStyle: "#FFFFFF",
+ textBaseline: "middle",
+ textAlign: "center",
+ text: "Hello World !",
+}));
```
-> Simple hello world using melonJS 2
+> Simple hello world using melonJS
Documentation
-------------------------------------------------------------------------------
diff --git a/packages/examples/src/examples/platformer/createGame.ts b/packages/examples/src/examples/platformer/createGame.ts
index 136c5e468..1cd697599 100644
--- a/packages/examples/src/examples/platformer/createGame.ts
+++ b/packages/examples/src/examples/platformer/createGame.ts
@@ -1,5 +1,6 @@
import { DebugPanelPlugin } from "@melonjs/debug-plugin";
import {
+ Application,
audio,
device,
event,
@@ -9,7 +10,6 @@ import {
pool,
state,
TextureAtlas,
- video,
} from "melonjs";
import { CoinEntity } from "./entities/coin.js";
import { FlyEnemyEntity, SlimeEnemyEntity } from "./entities/enemies.js";
@@ -19,63 +19,52 @@ import { PlayScreen } from "./play.js";
import { resources } from "./resources.js";
export const createGame = () => {
- // init the video
- if (
- !video.init(800, 600, {
- parent: "screen",
- scaleMethod: "flex-width",
- renderer: video.AUTO,
- preferWebGL1: false,
- depthTest: "z-buffer",
- subPixel: false,
- })
- ) {
- alert("Your browser does not support HTML5 canvas.");
- return;
- }
+ // create a new melonJS Application
+ const app = new Application(800, 600, {
+ parent: "screen",
+ scaleMethod: "flex-width",
+ renderer: 2, // AUTO
+ preferWebGL1: false,
+ depthTest: "z-buffer",
+ subPixel: false,
+ });
// register the debug plugin
plugin.register(DebugPanelPlugin, "debugPanel");
- // initialize the "sound engine"
+ // initialize the sound engine
audio.init("mp3,ogg");
// allow cross-origin for image/texture loading
loader.setOptions({ crossOrigin: "anonymous" });
- // set all ressources to be loaded
+ // preload all resources
loader.preload(resources, () => {
- // set the "Play/Ingame" Screen Object
+ // set the Play screen
state.set(state.PLAY, new PlayScreen());
// set the fade transition effect
state.transition("fade", "#FFFFFF", 250);
- // register our objects entity in the object pool
+ // register entity classes in the object pool
pool.register("mainPlayer", PlayerEntity);
pool.register("SlimeEntity", SlimeEnemyEntity);
pool.register("FlyEntity", FlyEnemyEntity);
pool.register("CoinEntity", CoinEntity, true);
- // load the texture atlas file
- // this will be used by renderable object later
+ // load the texture atlas
gameState.texture = new TextureAtlas(
loader.getJSON("texture"),
loader.getImage("texture"),
);
- // add some keyboard shortcuts
- event.on(event.KEYDOWN, (_action, keyCode /*, edge */) => {
- // change global volume setting
+ // keyboard shortcuts for volume and fullscreen
+ event.on(event.KEYDOWN, (_action, keyCode) => {
if (keyCode === input.KEY.PLUS) {
- // increase volume
audio.setVolume(audio.getVolume() + 0.1);
} else if (keyCode === input.KEY.MINUS) {
- // decrease volume
audio.setVolume(audio.getVolume() - 0.1);
}
-
- // toggle fullscreen on/off
if (keyCode === input.KEY.F) {
if (!device.isFullscreen()) {
device.requestFullscreen();
@@ -85,7 +74,7 @@ export const createGame = () => {
}
});
- // switch to PLAY state
+ // switch to the Play state
state.change(state.PLAY);
});
};
diff --git a/packages/examples/src/examples/platformer/entities/player.ts b/packages/examples/src/examples/platformer/entities/player.ts
index 7ecfde607..7b7c9291d 100644
--- a/packages/examples/src/examples/platformer/entities/player.ts
+++ b/packages/examples/src/examples/platformer/entities/player.ts
@@ -2,7 +2,6 @@ import {
audio,
Body,
collision,
- game,
input,
level,
Rect,
@@ -61,9 +60,6 @@ export class PlayerEntity extends Sprite {
this.multipleJump = 1;
- // set the viewport to follow this renderable on both axis, and enable damping
- game.viewport.follow(this, game.viewport.AXIS.BOTH, 0.1);
-
// enable keyboard
input.bindKey(input.KEY.LEFT, "left");
input.bindKey(input.KEY.RIGHT, "right");
@@ -150,7 +146,16 @@ export class PlayerEntity extends Sprite {
}
/**
- ** update the force applied
+ * called when added to the game world
+ */
+ onActivateEvent() {
+ const app = this.parentApp;
+ // set the viewport to follow this renderable on both axis, and enable damping
+ app.viewport.follow(this, app.viewport.AXIS.BOTH, 0.1);
+ }
+
+ /**
+ * update the force applied
*/
update(dt) {
if (input.isKeyPressed("left")) {
@@ -192,12 +197,13 @@ export class PlayerEntity extends Sprite {
// check if we fell into a hole
if (!this.inViewport && this.getBounds().top > video.renderer.height) {
+ const app = this.parentApp;
// if yes reset the game
- game.world.removeChild(this);
- game.viewport.fadeIn("#fff", 150, () => {
+ app.world.removeChild(this);
+ app.viewport.fadeIn("#fff", 150, () => {
audio.play("die", false);
level.reload();
- game.viewport.fadeOut("#fff", 150);
+ app.viewport.fadeOut("#fff", 150);
});
return true;
}
@@ -211,7 +217,7 @@ export class PlayerEntity extends Sprite {
}
/**
- * colision handler
+ * collision handler
*/
onCollision(response, other) {
switch (other.body.collisionType) {
@@ -228,7 +234,7 @@ export class PlayerEntity extends Sprite {
) {
// Disable collision on the x axis
response.overlapV.x = 0;
- // Repond to the platform (it is solid)
+ // Respond to the platform (it is solid)
return true;
}
// Do not respond to the platform (pass through)
@@ -286,7 +292,7 @@ export class PlayerEntity extends Sprite {
});
// flash the screen
- game.viewport.fadeIn("#FFFFFF", 75);
+ this.parentApp.viewport.fadeIn("#FFFFFF", 75);
audio.play("die", false);
}
}
diff --git a/packages/examples/src/examples/platformer/play.ts b/packages/examples/src/examples/platformer/play.ts
index 28b0d5dec..cf003527b 100644
--- a/packages/examples/src/examples/platformer/play.ts
+++ b/packages/examples/src/examples/platformer/play.ts
@@ -1,4 +1,4 @@
-import { audio, device, game, level, plugin, Stage } from "melonjs";
+import { type Application, audio, device, level, plugin, Stage } from "melonjs";
import { VirtualJoypad } from "./entities/controls";
import UIContainer from "./entities/HUD";
import { MinimapCamera } from "./entities/minimap";
@@ -11,7 +11,7 @@ export class PlayScreen extends Stage {
/**
* action to perform on state change
*/
- override onResetEvent() {
+ override onResetEvent(app: Application) {
// load a level
level.load("map1");
@@ -27,14 +27,14 @@ export class PlayScreen extends Stage {
if (typeof this.HUD === "undefined") {
this.HUD = new UIContainer();
}
- game.world.addChild(this.HUD);
+ app.world.addChild(this.HUD);
// display if debugPanel is enabled or on mobile
if (plugin.cache.debugPanel?.panel.visible || device.touch) {
if (typeof this.virtualJoypad === "undefined") {
this.virtualJoypad = new VirtualJoypad();
}
- game.world.addChild(this.virtualJoypad);
+ app.world.addChild(this.virtualJoypad);
}
// play some music
@@ -44,15 +44,15 @@ export class PlayScreen extends Stage {
/**
* action to perform on state change
*/
- override onDestroyEvent() {
+ override onDestroyEvent(app: Application) {
// remove the HUD from the game world
if (this.HUD) {
- game.world.removeChild(this.HUD);
+ app.world.removeChild(this.HUD);
}
// remove the joypad if initially added
- if (this.virtualJoypad && game.world.hasChild(this.virtualJoypad)) {
- game.world.removeChild(this.virtualJoypad);
+ if (this.virtualJoypad && app.world.hasChild(this.virtualJoypad)) {
+ app.world.removeChild(this.virtualJoypad);
}
// stop some music
diff --git a/packages/examples/src/examples/ui/ExampleUI.tsx b/packages/examples/src/examples/ui/ExampleUI.tsx
index 480263259..af1dfb6ec 100644
--- a/packages/examples/src/examples/ui/ExampleUI.tsx
+++ b/packages/examples/src/examples/ui/ExampleUI.tsx
@@ -1,6 +1,7 @@
import {
+ Application as App,
+ type Application,
ColorLayer,
- game,
loader,
Stage,
state,
@@ -41,7 +42,6 @@ class ButtonUI extends UISpriteElement {
fillStyle: "black",
textAlign: "center",
textBaseline: "middle",
- offScreenCanvas: video.renderer.WebGLVersion >= 1,
});
}
@@ -123,7 +123,6 @@ class CheckBoxUI extends UISpriteElement {
textAlign: "left",
textBaseline: "middle",
text: offLabel,
- offScreenCanvas: true,
});
this.getBounds().width += this.font.measureText().width;
@@ -204,8 +203,8 @@ class UIContainer extends UIBaseElement {
}
class PlayScreen extends Stage {
- override onResetEvent() {
- game.world.addChild(
+ override onResetEvent(app: Application) {
+ app.world.addChild(
new ColorLayer("background", "rgba(248, 194, 40, 1.0)"),
0,
);
@@ -243,21 +242,16 @@ class PlayScreen extends Stage {
panel.addChild(new ButtonUI(30, 250, "green", "Accept"));
panel.addChild(new ButtonUI(230, 250, "yellow", "Cancel"));
- game.world.addChild(panel, 1);
+ app.world.addChild(panel, 1);
}
}
const createGame = () => {
- if (
- !video.init(800, 600, {
- parent: "screen",
- scale: "auto",
- scaleMethod: "flex-width",
- })
- ) {
- alert("Your browser does not support HTML5 canvas.");
- return;
- }
+ const app = new App(800, 600, {
+ parent: "screen",
+ scale: "auto",
+ scaleMethod: "flex-width",
+ });
const resources = [
{ name: "UI_Assets-0", type: "image", src: `${base}img/UI_Assets-0.png` },
diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md
index 5925ecfe8..9e75a793f 100644
--- a/packages/melonjs/CHANGELOG.md
+++ b/packages/melonjs/CHANGELOG.md
@@ -14,6 +14,12 @@
- Application: new `canvas` getter, `resize()`, and `destroy()` convenience methods
- Application: `GAME_INIT` event now passes the Application instance as parameter
- Stage: `onResetEvent(app, ...args)` now receives the Application instance as first parameter, followed by any extra arguments from `state.change()`
+- Stage: `onDestroyEvent(app)` now receives the Application instance as parameter
+- Container: default dimensions are now `Infinity` (no intrinsic size, no clipping) — removes dependency on `game.viewport`. `anchorPoint` is always `(0, 0)` as containers act as grouping/transform nodes
+- ImageLayer: decoupled from `game` singleton — uses `parentApp` for viewport and renderer access; `resize`, `createPattern` and event listeners deferred to `onActivateEvent`
+- TMXTileMap: `addTo()` resolves viewport from the container tree via `getRootAncestor().app` instead of `game.viewport`
+- Input: pointer and pointerevent modules now receive the Application instance via `GAME_INIT` event instead of importing the `game` singleton
+- video: `video.renderer` and `video.init()` are now deprecated — use `new Application(width, height, options)` and `app.renderer` instead. `video.renderer` is kept in sync via `VIDEO_INIT` for backward compatibility
- EventEmitter: native context parameter support — `addListener(event, fn, context)` and `addListenerOnce(event, fn, context)` now accept an optional context, eliminating `.bind()` closure overhead and enabling proper `removeListener()` by original function reference
- EventEmitter: `event.on()` and `event.once()` no longer create `.bind()` closures when a context is provided
@@ -25,6 +31,8 @@
- Application: prevent white flash on load by setting a black background on the parent element when no background is defined
- WebGLRenderer: `setBlendMode()` now tracks the `premultipliedAlpha` flag — previously only the mode name was checked, causing incorrect GL blend function when mixing PMA and non-PMA textures with the same blend mode
- TMX: fix crash in `getObjects(false)` when a map contains an empty object group (Container.children lazily initialized)
+- Container: fix `updateBounds()` producing NaN when container has Infinity dimensions (skip parent bounds computation for non-finite containers, derive bounds from children only)
+- Container: fix circular import in `BitmapTextData` pool registration (`pool.ts` ↔ `bitmaptextdata.ts`)
- EventEmitter: `removeAllListeners()` now correctly clears once-listeners (previously only cleared regular listeners)
- Loader: fix undefined `crossOrigin` variable in script parser, unsafe regex match in video parser, missing error parameter in video/fontface error callbacks, `fetchData` Promise constructor antipattern and silent error swallowing
diff --git a/packages/melonjs/src/application/application.ts b/packages/melonjs/src/application/application.ts
index 53540f21b..273780381 100644
--- a/packages/melonjs/src/application/application.ts
+++ b/packages/melonjs/src/application/application.ts
@@ -25,6 +25,7 @@ import {
VIDEO_INIT,
WINDOW_ONORIENTATION_CHANGE,
WINDOW_ONRESIZE,
+ WINDOW_ONSCROLL,
} from "../system/event.ts";
import timer from "../system/timer.ts";
import { getUriFragment } from "../utils/utils.ts";
@@ -41,9 +42,32 @@ import type {
} from "./settings.ts";
/**
- * An Application represents a single melonJS game, and is responsible for updating (each frame) all the related object status and draw them.
+ * The Application class is the main entry point for creating a melonJS game.
+ * It initializes the renderer, creates the game world and viewport, registers DOM event
+ * listeners (resize, orientation, scroll), and starts the game loop.
+ *
+ * The Application instance provides access to the core game systems:
+ * - {@link Application#renderer renderer} — the active Canvas or WebGL renderer
+ * - {@link Application#world world} — the root container for all game objects
+ * - {@link Application#viewport viewport} — the default camera / viewport
+ *
+ * The app instance is automatically passed to {@link Stage#onResetEvent} and
+ * {@link Stage#onDestroyEvent}, and is accessible from any renderable via
+ * {@link Renderable#parentApp parentApp}.
* @category Application
- * @see {@link game}
+ * @example
+ * // create a new melonJS Application
+ * const app = new Application(800, 600, {
+ * parent: "screen",
+ * scaleMethod: "flex-width",
+ * renderer: 2, // AUTO
+ * });
+ *
+ * // add objects to the world
+ * app.world.addChild(new Sprite(0, 0, { image: "player" }));
+ *
+ * // access the viewport
+ * app.viewport.follow(player, app.viewport.AXIS.BOTH);
*/
export default class Application {
/**
@@ -98,7 +122,7 @@ export default class Application {
* @default true
* @example
* // keep the default game instance running even when losing focus
- * me.game.pauseOnBlur = false;
+ * app.pauseOnBlur = false;
*/
pauseOnBlur: boolean;
@@ -131,15 +155,29 @@ export default class Application {
// min update step size
stepSize: number;
+
+ // DOM event handlers (stored for cleanup in destroy)
+ private _onResize?: (e: Event) => void;
+ private _onOrientationChange?: (e: Event) => void;
+ private _onScroll?: (e: Event) => void;
updateDelta: number;
lastUpdateStart: number | null;
updateAverageDelta: number;
/**
+ * Create and initialize a new melonJS Application.
+ * This is the recommended way to start a melonJS game.
* @param width - The width of the canvas viewport
* @param height - The height of the canvas viewport
* @param options - The optional parameters for the application and default renderer
* @throws {Error} Will throw an exception if it fails to instantiate a renderer
+ * @example
+ * const app = new Application(1024, 768, {
+ * parent: "game-container",
+ * scale: "auto",
+ * scaleMethod: "fit",
+ * renderer: 2, // AUTO
+ * });
*/
constructor(
width: number,
@@ -279,7 +317,24 @@ export default class Application {
// make this the active game instance for modules that reference the global
setDefaultGame(this);
- // register to the channel
+ // bridge DOM events to the melonJS event system
+ this._onResize = (e: Event) => {
+ emit(WINDOW_ONRESIZE, e);
+ };
+ this._onOrientationChange = (e: Event) => {
+ emit(WINDOW_ONORIENTATION_CHANGE, e);
+ };
+ this._onScroll = (e: Event) => {
+ emit(WINDOW_ONSCROLL, e);
+ };
+ globalThis.addEventListener("resize", this._onResize);
+ globalThis.addEventListener("orientationchange", this._onOrientationChange);
+ if (device.screenOrientation) {
+ globalThis.screen.orientation.onchange = this._onOrientationChange;
+ }
+ globalThis.addEventListener("scroll", this._onScroll);
+
+ // react to resize/orientation changes
on(WINDOW_ONRESIZE, () => {
onresize(this);
});
@@ -392,7 +447,7 @@ export default class Application {
* Additionally the level id will also be passed to the called function.
* @example
* // call myFunction () everytime a level is loaded
- * me.game.onLevelLoaded = this.myFunction.bind(this);
+ * app.onLevelLoaded = this.myFunction.bind(this);
*/
onLevelLoaded(): void {}
@@ -467,6 +522,19 @@ export default class Application {
off(STAGE_RESET, this.reset, this);
/* eslint-enable @typescript-eslint/unbound-method */
+ // remove DOM event listeners
+ if (this._onResize) {
+ globalThis.removeEventListener("resize", this._onResize);
+ globalThis.removeEventListener(
+ "orientationchange",
+ this._onOrientationChange!,
+ );
+ globalThis.removeEventListener("scroll", this._onScroll!);
+ if (device.screenOrientation) {
+ globalThis.screen.orientation.onchange = null;
+ }
+ }
+
// destroy the world and all its children
if (this.world) {
this.world.destroy();
@@ -596,6 +664,8 @@ export default class Application {
/**
* The default game application instance.
* Set via {@link setDefaultGame} during engine initialization.
+ * When using {@link Application} directly, prefer using the app instance
+ * (e.g. from {@link Stage#onResetEvent} or {@link Renderable#parentApp}).
*/
export let game: Application;
diff --git a/packages/melonjs/src/application/settings.ts b/packages/melonjs/src/application/settings.ts
index 5c42b2cae..37c5e0059 100644
--- a/packages/melonjs/src/application/settings.ts
+++ b/packages/melonjs/src/application/settings.ts
@@ -107,8 +107,8 @@ export type ApplicationSettings = {
batcher?: (new (renderer: any) => Batcher) | undefined;
} & (
| {
- // the DOM parent element to hold the canvas in the HTML file
- parent: HTMLElement;
+ // the DOM parent element (or its string ID) to hold the canvas in the HTML file
+ parent: string | HTMLElement;
canvas?: never;
}
| {
diff --git a/packages/melonjs/src/camera/camera2d.ts b/packages/melonjs/src/camera/camera2d.ts
index b90a61f16..14a7868a8 100644
--- a/packages/melonjs/src/camera/camera2d.ts
+++ b/packages/melonjs/src/camera/camera2d.ts
@@ -56,7 +56,7 @@ const targetV = new Vector2d();
* // create a minimap camera in the top-right corner showing the full level
* const minimap = new Camera2d(0, 0, 180, 100);
* minimap.name = "minimap";
- * minimap.screenX = game.viewport.width - 190;
+ * minimap.screenX = app.viewport.width - 190;
* minimap.screenY = 10;
* minimap.autoResize = false;
* minimap.setBounds(0, 0, levelWidth, levelHeight);
@@ -495,11 +495,11 @@ export default class Camera2d extends Renderable {
* set the camera to follow the specified renderable.
* (this will put the camera center around the given target)
* @param target - renderable or position vector to follow
- * @param [axis=me.game.viewport.AXIS.BOTH] - Which axis to follow (see {@link Camera2d.AXIS})
+ * @param [axis=app.viewport.AXIS.BOTH] - Which axis to follow (see {@link Camera2d.AXIS})
* @param [damping=1] - default damping value
* @example
* // set the camera to follow this renderable on both axis, and enable damping
- * me.game.viewport.follow(this, me.game.viewport.AXIS.BOTH, 0.1);
+ * app.viewport.follow(this, app.viewport.AXIS.BOTH, 0.1);
*/
follow(
target: Renderable | Vector2d | Vector3d,
@@ -545,7 +545,7 @@ export default class Camera2d extends Renderable {
* @param y - vertical offset
* @example
* // Move the camera up by four pixels
- * me.game.viewport.move(0, -4);
+ * app.viewport.move(0, -4);
*/
move(x: number, y: number): void {
this.moveTo(this.pos.x + x, this.pos.y + y);
@@ -675,12 +675,12 @@ export default class Camera2d extends Renderable {
* @param intensity - maximum offset that the screen can be moved
* while shaking
* @param duration - expressed in milliseconds
- * @param [axis=me.game.viewport.AXIS.BOTH] - specify on which axis to apply the shake effect (see {@link Camera2d.AXIS})
+ * @param [axis=app.viewport.AXIS.BOTH] - specify on which axis to apply the shake effect (see {@link Camera2d.AXIS})
* @param [onComplete] - callback once shaking effect is over
* @param [force] - if true this will override the current effect
* @example
* // shake it baby !
- * me.game.viewport.shake(10, 500, me.game.viewport.AXIS.BOTH);
+ * app.viewport.shake(10, 500, app.viewport.AXIS.BOTH);
*/
shake(
intensity: number,
@@ -706,10 +706,10 @@ export default class Camera2d extends Renderable {
* @param [onComplete] - callback once effect is over
* @example
* // fade the camera to white upon dying, reload the level, and then fade out back
- * me.game.viewport.fadeIn("#fff", 150, function() {
+ * app.viewport.fadeIn("#fff", 150, function() {
* me.audio.play("die", false);
* me.level.reload();
- * me.game.viewport.fadeOut("#fff", 150);
+ * app.viewport.fadeOut("#fff", 150);
* });
*/
fadeOut(
@@ -736,7 +736,7 @@ export default class Camera2d extends Renderable {
* @param [onComplete] - callback once effect is over
* @example
* // flash the camera to white for 75ms
- * me.game.viewport.fadeIn("#FFFFFF", 75);
+ * app.viewport.fadeIn("#FFFFFF", 75);
*/
fadeIn(
color: Color | string,
diff --git a/packages/melonjs/src/input/pointer.ts b/packages/melonjs/src/input/pointer.ts
index bdabfb7b8..a32e863ff 100644
--- a/packages/melonjs/src/input/pointer.ts
+++ b/packages/melonjs/src/input/pointer.ts
@@ -1,8 +1,7 @@
-import { game } from "../application/application.ts";
import { Vector2d } from "../math/vector2d.ts";
import { Bounds } from "./../physics/bounds.ts";
import { globalToLocal } from "./input.ts";
-import { locked } from "./pointerevent.ts";
+import { _app, locked } from "./pointerevent.ts";
/**
* a temporary vector object
@@ -284,8 +283,8 @@ class Pointer extends Bounds {
this.type = event.type;
// get the current screen to game world offset
- if (typeof game.viewport !== "undefined") {
- game.viewport.localToWorld(this.gameScreenX, this.gameScreenY, tmpVec);
+ if (typeof _app?.viewport !== "undefined") {
+ _app.viewport.localToWorld(this.gameScreenX, this.gameScreenY, tmpVec);
}
/* Initialize the two coordinate space properties. */
diff --git a/packages/melonjs/src/input/pointerevent.ts b/packages/melonjs/src/input/pointerevent.ts
index e9dbbf436..9338d6278 100644
--- a/packages/melonjs/src/input/pointerevent.ts
+++ b/packages/melonjs/src/input/pointerevent.ts
@@ -1,9 +1,15 @@
-import { game } from "../application/application.ts";
+import type Application from "../application/application.ts";
import { Rect } from "./../geometries/rectangle.ts";
import type { Vector2d } from "../math/vector2d.ts";
import { vector2dPool } from "../math/vector2d.ts";
import * as device from "./../system/device.js";
-import { emit, POINTERLOCKCHANGE, POINTERMOVE } from "../system/event.ts";
+import {
+ emit,
+ GAME_INIT,
+ on,
+ POINTERLOCKCHANGE,
+ POINTERMOVE,
+} from "../system/event.ts";
import timer from "./../system/timer.ts";
import { remove } from "./../utils/array.ts";
import { throttle } from "./../utils/function.ts";
@@ -29,6 +35,15 @@ const eventHandlers: Map = new Map();
// a cache rect represeting the current pointer area
let currentPointer: Rect;
+/**
+ * reference to the active application instance
+ * @ignore
+ */
+export let _app: Application;
+on(GAME_INIT, (app: Application) => {
+ _app = app;
+});
+
// some useful flags
let pointerInitialized = false;
@@ -123,6 +138,9 @@ function registerEventListener(
*/
function enablePointerEvent(): void {
if (!pointerInitialized) {
+ if (!_app) {
+ throw new Error("Pointer events require an initialized Application");
+ }
// the current pointer area
currentPointer = new Rect(0, 0, 1, 1);
@@ -133,7 +151,7 @@ function enablePointerEvent(): void {
if (pointerEventTarget === null || pointerEventTarget === undefined) {
// default pointer event target
- pointerEventTarget = game.renderer.getCanvas();
+ pointerEventTarget = _app.renderer.getCanvas();
}
if (device.pointerEvent) {
@@ -202,7 +220,7 @@ function enablePointerEvent(): void {
() => {
// change the locked status accordingly
locked =
- globalThis.document.pointerLockElement === game.getParentElement();
+ globalThis.document.pointerLockElement === _app.getParentElement();
// emit the corresponding internal event
emit(POINTERLOCKCHANGE, locked);
},
@@ -309,14 +327,14 @@ function dispatchEvent(normalizedEvents: Pointer[]): boolean {
}
// fetch valid candiates from the game world container
- let candidates = game.world.broadphase.retrieve(
+ let candidates = _app.world.broadphase.retrieve(
currentPointer,
- (a: any, b: any) => game.world._sortReverseZ(a, b),
+ (a: any, b: any) => _app.world._sortReverseZ(a, b),
undefined,
);
// add the main game viewport to the list of candidates
- candidates = candidates.concat([game.viewport]);
+ candidates = candidates.concat([_app.viewport]);
for (
let c = candidates.length, candidate;
@@ -594,11 +612,11 @@ export function hasRegisteredEvents(): boolean {
*/
export function globalToLocal(x: number, y: number, v?: Vector2d): Vector2d {
v = v || vector2dPool.get();
- const rect = device.getElementBounds(game.renderer.getCanvas());
+ const rect = device.getElementBounds(_app.renderer.getCanvas());
const pixelRatio = globalThis.devicePixelRatio || 1;
x -= rect.left + (globalThis.pageXOffset || 0);
y -= rect.top + (globalThis.pageYOffset || 0);
- const scale = game.renderer.scaleRatio;
+ const scale = _app.renderer.scaleRatio;
if (scale.x !== 1.0 || scale.y !== 1.0) {
x /= scale.x;
y /= scale.y;
@@ -817,7 +835,7 @@ export function releaseAllPointerEvents(region: any): void {
*/
export function requestPointerLock(): boolean {
if (device.hasPointerLockSupport) {
- const element = game.getParentElement();
+ const element = _app.getParentElement();
void element.requestPointerLock();
return true;
}
diff --git a/packages/melonjs/src/level/level.js b/packages/melonjs/src/level/level.js
index c48bf57b4..270defb3a 100644
--- a/packages/melonjs/src/level/level.js
+++ b/packages/melonjs/src/level/level.js
@@ -150,7 +150,7 @@ export const level = {
* levelContainer.currentTransform.rotate(0.05);
* levelContainer.currentTransform.translate(-levelContainer.width / 2, -levelContainer.height / 2 );
* // add it to the game world
- * me.game.world.addChild(levelContainer);
+ * app.world.addChild(levelContainer);
*/
load(levelId, options) {
options = Object.assign(
@@ -203,7 +203,7 @@ export const level = {
/**
* return the current level definition.
* for a reference to the live instantiated level,
- * rather use the container in which it was loaded (e.g. me.game.world)
+ * rather use the container in which it was loaded (e.g. app.world)
* @name getCurrentLevel
* @memberof level
* @public
diff --git a/packages/melonjs/src/level/tiled/TMXGroup.js b/packages/melonjs/src/level/tiled/TMXGroup.js
index 9e40ae36b..0aedfa891 100644
--- a/packages/melonjs/src/level/tiled/TMXGroup.js
+++ b/packages/melonjs/src/level/tiled/TMXGroup.js
@@ -5,7 +5,7 @@ import { applyTMXProperties, tiledBlendMode } from "./TMXUtils.js";
/**
* object group definition as defined in Tiled.
- * (group definition is translated into the virtual `me.game.world` using `me.Container`)
+ * (group definition is translated into the virtual `app.world` using `me.Container`)
* @ignore
*/
export default class TMXGroup {
diff --git a/packages/melonjs/src/level/tiled/TMXLayer.js b/packages/melonjs/src/level/tiled/TMXLayer.js
index 16212008e..7f572cb14 100644
--- a/packages/melonjs/src/level/tiled/TMXLayer.js
+++ b/packages/melonjs/src/level/tiled/TMXLayer.js
@@ -291,7 +291,7 @@ export default class TMXLayer extends Renderable {
* @returns {Tile} corresponding tile or null if there is no defined tile at the coordinate or if outside of the layer bounds
* @example
* // get the TMX Map Layer called "Front layer"
- * let layer = me.game.world.getChildByName("Front Layer")[0];
+ * let layer = app.world.getChildByName("Front Layer")[0];
* // get the tile object corresponding to the latest pointer position
* let tile = layer.getTile(me.input.pointer.x, me.input.pointer.y);
*/
@@ -369,7 +369,7 @@ export default class TMXLayer extends Renderable {
* @param {number} x - X coordinate (in map coordinates: row/column)
* @param {number} y - Y coordinate (in map coordinates: row/column)
* @example
- * me.game.world.getChildByType(me.TMXLayer).forEach(function(layer) {
+ * app.world.getChildByType(me.TMXLayer).forEach(function(layer) {
* // clear all tiles at the given x,y coordinates
* layer.clearTile(x, y);
* });
diff --git a/packages/melonjs/src/level/tiled/TMXObject.js b/packages/melonjs/src/level/tiled/TMXObject.js
index 4a97817a3..9d2dd7c94 100644
--- a/packages/melonjs/src/level/tiled/TMXObject.js
+++ b/packages/melonjs/src/level/tiled/TMXObject.js
@@ -36,7 +36,7 @@ function detectShape(settings) {
/**
* a TMX Object defintion, as defined in Tiled
- * (Object definition is translated into the virtual `me.game.world` using `me.Renderable`)
+ * (Object definition is translated into the virtual `app.world` using `me.Renderable`)
* @ignore
*/
export default class TMXObject {
diff --git a/packages/melonjs/src/level/tiled/TMXTileMap.js b/packages/melonjs/src/level/tiled/TMXTileMap.js
index 55a59e342..615cf021c 100644
--- a/packages/melonjs/src/level/tiled/TMXTileMap.js
+++ b/packages/melonjs/src/level/tiled/TMXTileMap.js
@@ -1,4 +1,3 @@
-import { game } from "../../application/application.ts";
import { warning } from "../../lang/console.js";
import { vector2dPool } from "../../math/vector2d.ts";
import { collision } from "../../physics/collision.js";
@@ -129,7 +128,7 @@ export default class TMXTileMap {
* // create a new level object based on the TMX JSON object
* let level = new me.TMXTileMap(levelId, me.loader.getTMX(levelId));
* // add the level to the game world container
- * level.addTo(me.game.world, true);
+ * level.addTo(app.world, true);
*/
constructor(levelId, data) {
/**
@@ -378,7 +377,7 @@ export default class TMXTileMap {
* // create a new level object based on the TMX JSON object
* let level = new me.TMXTileMap(levelId, me.loader.getTMX(levelId));
* // add the level to the game world container
- * level.addTo(me.game.world, true, true);
+ * level.addTo(app.world, true, true);
*/
addTo(container, flatten, setViewportBounds) {
const _sort = container.autoSort;
@@ -414,27 +413,29 @@ export default class TMXTileMap {
* callback funtion for the viewport resize event
* @ignore
*/
- function _setBounds(width, height) {
- // adjust the viewport bounds if level is smaller
- game.viewport.setBounds(
- 0,
- 0,
- Math.max(levelBounds.width, width),
- Math.max(levelBounds.height, height),
- );
- // center the map if smaller than the current viewport
- container.pos.set(
- Math.max(0, ~~((width - levelBounds.width) / 2)),
- Math.max(0, ~~((height - levelBounds.height) / 2)),
- // don't change the container z position if defined
- container.pos.z,
- );
- }
-
if (setViewportBounds === true) {
+ const app = container.getRootAncestor().app;
+
+ function _setBounds(width, height) {
+ // adjust the viewport bounds if level is smaller
+ app.viewport.setBounds(
+ 0,
+ 0,
+ Math.max(levelBounds.width, width),
+ Math.max(levelBounds.height, height),
+ );
+ // center the map if smaller than the current viewport
+ container.pos.set(
+ Math.max(0, ~~((width - levelBounds.width) / 2)),
+ Math.max(0, ~~((height - levelBounds.height) / 2)),
+ // don't change the container z position if defined
+ container.pos.z,
+ );
+ }
+
off(VIEWPORT_ONRESIZE, _setBounds);
// force viewport bounds update
- _setBounds(game.viewport.width, game.viewport.height);
+ _setBounds(app.viewport.width, app.viewport.height);
// Replace the resize handler
on(VIEWPORT_ONRESIZE, _setBounds);
}
diff --git a/packages/melonjs/src/particles/emitter.ts b/packages/melonjs/src/particles/emitter.ts
index 47194a503..44b0a33e7 100644
--- a/packages/melonjs/src/particles/emitter.ts
+++ b/packages/melonjs/src/particles/emitter.ts
@@ -70,7 +70,7 @@ export default class ParticleEmitter extends Container {
* });
*
* // Add the emitter to the game world
- * me.game.world.addChild(emitter);
+ * app.world.addChild(emitter);
*
* // Launch all particles one time and stop, like an explosion
* emitter.burstParticles();
@@ -80,7 +80,7 @@ export default class ParticleEmitter extends Container {
*
* // At the end, remove emitter from the game world
* // call this in onDestroyEvent function
- * me.game.world.removeChild(emitter);
+ * app.world.removeChild(emitter);
*/
constructor(x: number, y: number, settings: Record = {}) {
// call the super constructor
diff --git a/packages/melonjs/src/physics/collision.js b/packages/melonjs/src/physics/collision.js
index 4ca520217..9d61ec786 100644
--- a/packages/melonjs/src/physics/collision.js
+++ b/packages/melonjs/src/physics/collision.js
@@ -111,7 +111,7 @@ export const collision = {
* // starting point relative to the initial position
* new me.Vector2d(0, 0),
* // ending point
- * new me.Vector2d(me.game.viewport.width, me.game.viewport.height)
+ * new me.Vector2d(app.viewport.width, app.viewport.height)
* ]);
*
* // check for collition
diff --git a/packages/melonjs/src/physics/detector.js b/packages/melonjs/src/physics/detector.js
index 6dddb1b0f..a51b265f7 100644
--- a/packages/melonjs/src/physics/detector.js
+++ b/packages/melonjs/src/physics/detector.js
@@ -258,7 +258,7 @@ class Detector {
* // starting point relative to the initial position
* new Vector2d(0, 0),
* // ending point
- * new Vector2d(me.game.viewport.width, me.game.viewport.height)
+ * new Vector2d(app.viewport.width, app.viewport.height)
* ]);
*
* // check for collition
diff --git a/packages/melonjs/src/physics/world.js b/packages/melonjs/src/physics/world.js
index d0ee0cd9f..cbd6c022c 100644
--- a/packages/melonjs/src/physics/world.js
+++ b/packages/melonjs/src/physics/world.js
@@ -26,8 +26,8 @@ export default class World extends Container {
/**
* @param {number} [x=0] - position of the container (accessible via the inherited pos.x property)
* @param {number} [y=0] - position of the container (accessible via the inherited pos.y property)
- * @param {number} [width=game.viewport.width] - width of the container
- * @param {number} [height=game.viewport.height] - height of the container
+ * @param {number} [width=Infinity] - width of the world container
+ * @param {number} [height=Infinity] - height of the world container
*/
constructor(x = 0, y = 0, width = Infinity, height = Infinity) {
// call the super constructor
@@ -52,7 +52,7 @@ export default class World extends Container {
* @default "builtin"
* @example
* // disable builtin physic
- * me.game.world.physic = "none";
+ * app.world.physic = "none";
*/
this.physic = "builtin";
diff --git a/packages/melonjs/src/plugin/plugin.ts b/packages/melonjs/src/plugin/plugin.ts
index 1a08f41fd..159cf8301 100644
--- a/packages/melonjs/src/plugin/plugin.ts
+++ b/packages/melonjs/src/plugin/plugin.ts
@@ -40,11 +40,11 @@ export class BasePlugin {
* @param name - target function
* @param fn - replacement function
* @example
- * // redefine the me.game.update function with a new one
- * me.plugin.patch(me.game, "update", function () {
+ * // redefine the app.update function with a new one
+ * me.plugin.patch(app, "update", function () {
* // display something in the console
* console.log("duh");
- * // call the original me.game.update function
+ * // call the original app.update function
* this._patched();
* });
*/
diff --git a/packages/melonjs/src/renderable/container.js b/packages/melonjs/src/renderable/container.js
index c13fbfbb6..7db5d191e 100644
--- a/packages/melonjs/src/renderable/container.js
+++ b/packages/melonjs/src/renderable/container.js
@@ -1,4 +1,3 @@
-import { game } from "../application/application.ts";
import { colorPool } from "../math/color.ts";
import Body from "../physics/body.js";
import state from "../state/state.ts";
@@ -42,32 +41,27 @@ let globalFloatingCounter = 0;
*/
/**
- * Container represents a collection of child objects
+ * Container represents a collection of child objects.
+ * When no explicit dimensions are given, width and height default to Infinity,
+ * meaning the container has no intrinsic size, no clipping, and acts as a pure
+ * grouping/transform node (similar to PixiJS or Phaser containers).
+ * In this case, anchorPoint is treated as (0, 0) since there is no meaningful
+ * center for an infinite area. Bounds are then derived entirely from children
+ * when {@link Container#enableChildBoundsUpdate} is enabled.
* @category Container
*/
export default class Container extends Renderable {
/**
* @param {number} [x=0] - position of the container (accessible via the inherited pos.x property)
* @param {number} [y=0] - position of the container (accessible via the inherited pos.y property)
- * @param {number} [width=game.viewport.width] - width of the container
- * @param {number} [height=game.viewport.height] - height of the container
+ * @param {number} [width=Infinity] - width of the container. Defaults to Infinity (no intrinsic size, no clipping).
+ * @param {number} [height=Infinity] - height of the container. Defaults to Infinity (no intrinsic size, no clipping).
+ * @param {boolean} [root=false] - internal flag, true for the world root container
+ * @ignore root
*/
- constructor(x = 0, y = 0, width, height, root = false) {
+ constructor(x = 0, y = 0, width = Infinity, height = Infinity, root = false) {
// call the super constructor
- super(
- x,
- y,
- typeof width === "undefined"
- ? typeof game.viewport !== "undefined"
- ? game.viewport.width
- : Infinity
- : width,
- typeof height === "undefined"
- ? typeof game.viewport !== "undefined"
- ? game.viewport.height
- : Infinity
- : height,
- );
+ super(x, y, width, height);
/**
* keep track of pending sort
@@ -156,6 +150,10 @@ export default class Container extends Renderable {
// enable collision and event detection
this.isKinematic = false;
+ // container anchorPoint is always (0, 0) — children position from the
+ // container's origin (top-left), matching the convention used by other engines
+ // (PixiJS, Phaser). This also avoids Infinity * 0.5 = Infinity issues
+ // when the container has no explicit size.
this.anchorPoint.set(0, 0);
// subscribe on the canvas resize event
@@ -569,8 +567,13 @@ export default class Container extends Renderable {
updateBounds(absolute = true) {
const bounds = this.getBounds();
- // call parent method
- super.updateBounds(absolute);
+ if (this.isFinite()) {
+ // call parent method only when container has finite dimensions
+ super.updateBounds(absolute);
+ } else if (this.enableChildBoundsUpdate === true) {
+ // clear bounds so child aggregation starts fresh
+ bounds.clear();
+ }
if (this.enableChildBoundsUpdate === true) {
this.forEach((child) => {
diff --git a/packages/melonjs/src/renderable/imagelayer.js b/packages/melonjs/src/renderable/imagelayer.js
index 603fe762f..78cccb12a 100644
--- a/packages/melonjs/src/renderable/imagelayer.js
+++ b/packages/melonjs/src/renderable/imagelayer.js
@@ -1,4 +1,3 @@
-import { game } from "../application/application.ts";
import { vector2dPool } from "../math/vector2d.ts";
import {
LEVEL_LOADED,
@@ -33,7 +32,7 @@ export default class ImageLayer extends Sprite {
* @param {number|Vector2d} [settings.anchorPoint=<0.0,0.0>] - Define how the image is anchored to the viewport bound. By default, its upper-left corner is anchored to the viewport bounds upper left corner.
* @example
* // create a repetitive background pattern on the X axis using the citycloud image asset
- * me.game.world.addChild(new me.ImageLayer(0, 0, {
+ * app.world.addChild(new me.ImageLayer(0, 0, {
* image:"citycloud",
* repeat :"repeat-x"
* }), 1);
@@ -83,9 +82,6 @@ export default class ImageLayer extends Sprite {
}
this.repeat = settings.repeat || "repeat";
-
- // on context lost, all previous textures are destroyed
- on(ONCONTEXT_RESTORED, this.createPattern, this);
}
/**
@@ -123,15 +119,23 @@ export default class ImageLayer extends Sprite {
this.repeatY = true;
break;
}
- this.resize(game.viewport.width, game.viewport.height);
- this.createPattern();
}
// called when the layer is added to the game world or a container
onActivateEvent() {
- // register to the viewport change notification
+ if (!this.parentApp) {
+ throw new Error(
+ "ImageLayer requires a parent Application (must be added to an app's world container)",
+ );
+ }
+ const viewport = this.parentApp.viewport;
+ // set the initial size to match the viewport
+ this.resize(viewport.width, viewport.height);
+ this.createPattern();
+ // register to the viewport change notification and context restore
on(VIEWPORT_ONCHANGE, this.updateLayer, this);
on(VIEWPORT_ONRESIZE, this.resize, this);
+ on(ONCONTEXT_RESTORED, this.createPattern, this);
// force a first refresh when the level is loaded
on(LEVEL_LOADED, this.updateLayer, this);
// in case the level is not added to the root container,
@@ -159,10 +163,10 @@ export default class ImageLayer extends Sprite {
* @ignore
*/
createPattern() {
- const renderer = this.parentApp?.renderer ?? game.renderer;
- if (renderer) {
- this._pattern = renderer.createPattern(this.image, this._repeat);
- }
+ this._pattern = this.parentApp.renderer.createPattern(
+ this.image,
+ this._repeat,
+ );
}
/**
@@ -173,7 +177,7 @@ export default class ImageLayer extends Sprite {
const rx = this.ratio.x;
const ry = this.ratio.y;
- const viewport = game.viewport;
+ const viewport = this.parentApp.viewport;
if (rx === 0 && ry === 0) {
// static image
@@ -263,7 +267,7 @@ export default class ImageLayer extends Sprite {
x = this.pos.x + ax * (bw - width);
y = this.pos.y + ay * (bh - height);
} else {
- // parallax — compute position from the current viewport, not game.viewport
+ // parallax — compute position from the current viewport passed to draw()
x =
ax * (rx - 1) * (bw - viewport.width) +
this.offset.x -
@@ -300,6 +304,7 @@ export default class ImageLayer extends Sprite {
off(VIEWPORT_ONCHANGE, this.updateLayer, this);
off(VIEWPORT_ONRESIZE, this.resize, this);
off(LEVEL_LOADED, this.updateLayer, this);
+ off(ONCONTEXT_RESTORED, this.createPattern, this);
}
/**
@@ -309,7 +314,6 @@ export default class ImageLayer extends Sprite {
destroy() {
vector2dPool.release(this.ratio);
this.ratio = undefined;
- off(ONCONTEXT_RESTORED, this.createPattern, this);
super.destroy();
}
}
diff --git a/packages/melonjs/src/renderable/renderable.js b/packages/melonjs/src/renderable/renderable.js
index 58bbbb5a8..6368bbeb0 100644
--- a/packages/melonjs/src/renderable/renderable.js
+++ b/packages/melonjs/src/renderable/renderable.js
@@ -104,7 +104,7 @@ export default class Renderable extends Rect {
* this.isKinematic = false;
*
* // set the display to follow our position on both axis
- * me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH);
+ * app.viewport.follow(this.pos, app.viewport.AXIS.BOTH);
* }
*
* ...
@@ -116,7 +116,7 @@ export default class Renderable extends Rect {
/**
* (G)ame (U)nique (Id)entifier"
* a GUID will be allocated for any renderable object added
- * to an object container (including the `me.game.world` container)
+ * to an object container (including the `app.world` container)
* @type {string}
*/
this.GUID = undefined;
diff --git a/packages/melonjs/src/renderable/text/bitmaptext.js b/packages/melonjs/src/renderable/text/bitmaptext.js
index f8bbb43a0..f22e2cccf 100644
--- a/packages/melonjs/src/renderable/text/bitmaptext.js
+++ b/packages/melonjs/src/renderable/text/bitmaptext.js
@@ -37,7 +37,7 @@ export default class BitmapText extends Renderable {
* // either call the draw function from your Renderable draw function
* myFont.draw(renderer, "Hello!", 0, 0);
* // or just add it to the world container
- * me.game.world.addChild(myFont);
+ * app.world.addChild(myFont);
*/
constructor(x, y, settings) {
// call the parent constructor
diff --git a/packages/melonjs/src/renderable/text/bitmaptextdata.ts b/packages/melonjs/src/renderable/text/bitmaptextdata.ts
index 789ef3282..026c7f180 100644
--- a/packages/melonjs/src/renderable/text/bitmaptextdata.ts
+++ b/packages/melonjs/src/renderable/text/bitmaptextdata.ts
@@ -1,4 +1,4 @@
-import { createPool } from "../../pool.ts";
+import { createPool } from "../../system/pool.ts";
import Glyph from "./glyph.ts";
// bitmap constants
diff --git a/packages/melonjs/src/state/stage.js b/packages/melonjs/src/state/stage.js
index 59d6a675c..d963e9610 100644
--- a/packages/melonjs/src/state/stage.js
+++ b/packages/melonjs/src/state/stage.js
@@ -53,7 +53,7 @@ export default class Stage {
* // set a dark ambient light
* this.ambientLight.parseCSS("#1117");
* // make the light follow the mouse
- * me.input.registerPointerEvent("pointermove", me.game.viewport, (event) => {
+ * me.input.registerPointerEvent("pointermove", app.viewport, (event) => {
* whiteLight.centerOn(event.gameX, event.gameY);
* });
*/
@@ -181,7 +181,7 @@ export default class Stage {
* destroy function
* @ignore
*/
- destroy() {
+ destroy(app) {
// clear all cameras
this.cameras.clear();
// clear all lights
@@ -190,7 +190,7 @@ export default class Stage {
});
this.lights.clear();
// notify the object
- this.onDestroyEvent.apply(this, arguments);
+ this.onDestroyEvent(app);
}
/**
@@ -215,11 +215,12 @@ export default class Stage {
* called by the state manager before switching to another state
* @name onDestroyEvent
* @memberof Stage
+ * @param {Application} [app] - the current application instance
*/
- onDestroyEvent() {
+ onDestroyEvent(app) {
// execute onDestroyEvent function if given through the constructor
if (typeof this.settings.onDestroyEvent === "function") {
- this.settings.onDestroyEvent.apply(this, arguments);
+ this.settings.onDestroyEvent.call(this, app);
}
}
}
diff --git a/packages/melonjs/src/state/state.ts b/packages/melonjs/src/state/state.ts
index 64203927a..7a85ec3d3 100644
--- a/packages/melonjs/src/state/state.ts
+++ b/packages/melonjs/src/state/state.ts
@@ -120,7 +120,7 @@ function _switchState(stateId: number): void {
// call the stage destroy method
if (_stages[_state]) {
// just notify the object
- _stages[_state].stage.destroy();
+ _stages[_state].stage.destroy(_app);
}
if (_stages[stateId]) {
@@ -352,7 +352,7 @@ const state = {
* class MenuScreen extends me.Stage {
* onResetEvent() {
* // Load background image
- * me.game.world.addChild(
+ * app.world.addChild(
* new me.ImageLayer(0, 0, {
* image : "bg",
* z: 0 // z-index
@@ -360,7 +360,7 @@ const state = {
* );
*
* // Add a button
- * me.game.world.addChild(
+ * app.world.addChild(
* new MenuButton(350, 200, { "image" : "start" }),
* 1 // z-index
* );
diff --git a/packages/melonjs/src/system/bootstrap.ts b/packages/melonjs/src/system/bootstrap.ts
index 41ca21c56..2401a30bc 100644
--- a/packages/melonjs/src/system/bootstrap.ts
+++ b/packages/melonjs/src/system/bootstrap.ts
@@ -27,12 +27,10 @@ export let initialized = false;
/**
* Initialize the melonJS library.
- * This is called automatically in two cases:
- * - On DOMContentLoaded, unless {@link skipAutoInit} is set to true
- * - By {@link Application.init} when creating a new game instance
- *
+ * This is called automatically by the {@link Application} constructor.
* Multiple calls are safe — boot() is idempotent.
- * @see {@link skipAutoInit}
+ * When using {@link Application} directly, calling boot() manually is not needed.
+ * @see {@link Application}
*/
export function boot() {
// don't do anything if already initialized (should not happen anyway)
diff --git a/packages/melonjs/src/system/device.js b/packages/melonjs/src/system/device.js
index 8b2951249..e8a90dcdd 100644
--- a/packages/melonjs/src/system/device.js
+++ b/packages/melonjs/src/system/device.js
@@ -388,6 +388,7 @@ export let autoFocus = true;
* me.device.onReady(function () {
* game.onload();
* });
+ * @deprecated since 18.3.0 — no longer needed when using {@link Application} as entry point.
* @category Application
*/
export function onReady(fn) {
@@ -771,10 +772,10 @@ export function focus() {
* @returns {boolean} false if not supported or permission not granted by the user
* @example
* // try to enable device accelerometer event on user gesture
- * me.input.registerPointerEvent("pointerleave", me.game.viewport, function() {
+ * me.input.registerPointerEvent("pointerleave", app.viewport, function() {
* if (me.device.watchAccelerometer() === true) {
* // Success
- * me.input.releasePointerEvent("pointerleave", me.game.viewport);
+ * me.input.releasePointerEvent("pointerleave", app.viewport);
* } else {
* // ... fail at enabling the device accelerometer event
* }
@@ -828,10 +829,10 @@ export function unwatchAccelerometer() {
* @returns {boolean} false if not supported or permission not granted by the user
* @example
* // try to enable device orientation event on user gesture
- * me.input.registerPointerEvent("pointerleave", me.game.viewport, function() {
+ * me.input.registerPointerEvent("pointerleave", app.viewport, function() {
* if (me.device.watchDeviceOrientation() === true) {
* // Success
- * me.input.releasePointerEvent("pointerleave", me.game.viewport);
+ * me.input.releasePointerEvent("pointerleave", app.viewport);
* } else {
* // ... fail at enabling the device orientation event
* }
diff --git a/packages/melonjs/src/system/legacy_pool.js b/packages/melonjs/src/system/legacy_pool.js
index b69947a42..36658ce8c 100644
--- a/packages/melonjs/src/system/legacy_pool.js
+++ b/packages/melonjs/src/system/legacy_pool.js
@@ -119,8 +119,8 @@ class ObjectPool {
* // ...
* // when we want to destroy existing object, the remove
* // function will ensure the object can then be reallocated later
- * me.game.world.removeChild(enemy);
- * me.game.world.removeChild(bullet);
+ * app.world.removeChild(enemy);
+ * app.world.removeChild(bullet);
*/
pull(name, ...args) {
const className = this.objectClass[name];
diff --git a/packages/melonjs/src/video/video.js b/packages/melonjs/src/video/video.js
index 1fc55fd80..b9ca5c0b7 100644
--- a/packages/melonjs/src/video/video.js
+++ b/packages/melonjs/src/video/video.js
@@ -2,13 +2,7 @@ import { game } from "../application/application.ts";
import { defaultApplicationSettings } from "../application/defaultApplicationSettings.ts";
import { initialized } from "../system/bootstrap.ts";
import * as device from "./../system/device.js";
-import {
- emit,
- VIDEO_INIT,
- WINDOW_ONORIENTATION_CHANGE,
- WINDOW_ONRESIZE,
- WINDOW_ONSCROLL,
-} from "../system/event.ts";
+import { on, VIDEO_INIT } from "../system/event.ts";
/**
* @namespace video
@@ -27,13 +21,19 @@ export { AUTO, CANVAS, WEBGL } from "../const";
/**
* A reference to the active Canvas or WebGL renderer.
- * Only available after calling {@link video.init}.
- * When using {@link Application} directly, use `app.renderer` instead.
* @memberof video
* @type {CanvasRenderer|WebGLRenderer}
+ * @deprecated since 18.3.0 — use {@link Application#renderer app.renderer} instead.
+ * @see Application#renderer
*/
export let renderer = null;
+// backward compatibility: keep video.renderer in sync
+// when Application is used directly instead of video.init()
+on(VIDEO_INIT, (r) => {
+ renderer = r;
+});
+
/**
* Initialize the "video" system (create a canvas based on the given arguments, and the related renderer).
* @memberof video
@@ -41,8 +41,18 @@ export let renderer = null;
* @param {number} height - The height of the canvas viewport
* @param {ApplicationSettings} [options] - optional parameters for the renderer
* @returns {boolean} false if initialization failed (canvas not supported)
+ * @deprecated since 18.3.0 — use {@link Application} constructor instead:
+ * `const app = new Application(width, height, options)`
+ * @see Application
* @example
- * // init the video with a 640x480 canvas
+ * // using the new Application entry point (recommended)
+ * const app = new Application(640, 480, {
+ * parent : "screen",
+ * scale : "auto",
+ * scaleMethod : "fit"
+ * });
+ *
+ * // legacy usage (still supported)
* me.video.init(640, 480, {
* parent : "screen",
* renderer : me.video.AUTO,
@@ -68,53 +78,7 @@ export function init(width, height, options) {
return false;
}
- // set the public renderer reference
- renderer = game.renderer;
-
- //add a channel for the onresize/onorientationchange event
- globalThis.addEventListener(
- "resize",
- (e) => {
- emit(WINDOW_ONRESIZE, e);
- },
- false,
- );
-
- // Screen Orientation API
- globalThis.addEventListener(
- "orientationchange",
- (e) => {
- emit(WINDOW_ONORIENTATION_CHANGE, e);
- },
- false,
- );
-
- // pre-fixed implementation on mozzila
- globalThis.addEventListener(
- "onmozorientationchange",
- (e) => {
- emit(WINDOW_ONORIENTATION_CHANGE, e);
- },
- false,
- );
-
- if (device.screenOrientation === true) {
- globalThis.screen.orientation.onchange = (e) => {
- emit(WINDOW_ONORIENTATION_CHANGE, e);
- };
- }
-
- // Automatically update relative canvas position on scroll
- globalThis.addEventListener(
- "scroll",
- (e) => {
- emit(WINDOW_ONSCROLL, e);
- },
- false,
- );
-
- // notify the video has been initialized
- emit(VIDEO_INIT, game.renderer);
+ // DOM event listeners and VIDEO_INIT are now handled by Application.init()
return true;
}
@@ -157,6 +121,8 @@ export function createCanvas(width, height, returnOffscreenCanvas = false) {
* return a reference to the parent DOM element holding the main canvas
* @memberof video
* @returns {HTMLElement} the HTML parent element
+ * @deprecated since 18.3.0 — use {@link Application#getParentElement app.getParentElement()} instead.
+ * @see Application#getParentElement
*/
export function getParent() {
return game.getParentElement();
diff --git a/packages/melonjs/tests/container.spec.js b/packages/melonjs/tests/container.spec.js
index 8231b3865..a8059c267 100644
--- a/packages/melonjs/tests/container.spec.js
+++ b/packages/melonjs/tests/container.spec.js
@@ -970,4 +970,183 @@ describe("Container", () => {
expect(child.visibleInAllCameras).toEqual(true);
});
});
+
+ describe("default dimensions", () => {
+ it("should default to Infinity when no dimensions are provided", () => {
+ const c = new Container();
+ expect(c.width).toEqual(Infinity);
+ expect(c.height).toEqual(Infinity);
+ });
+
+ it("should default to (0, 0) position when no position is provided", () => {
+ const c = new Container();
+ expect(c.pos.x).toEqual(0);
+ expect(c.pos.y).toEqual(0);
+ });
+
+ it("should use explicit dimensions when provided", () => {
+ const c = new Container(10, 20, 200, 150);
+ expect(c.pos.x).toEqual(10);
+ expect(c.pos.y).toEqual(20);
+ expect(c.width).toEqual(200);
+ expect(c.height).toEqual(150);
+ });
+
+ it("should accept children when dimensions are Infinity", () => {
+ const c = new Container();
+ const child = new Renderable(50, 50, 32, 32);
+ c.addChild(child);
+ expect(c.getChildren().length).toEqual(1);
+ });
+
+ it("should not clip children when dimensions are Infinity", () => {
+ const c = new Container();
+ c.clipping = true;
+ const farChild = new Renderable(99999, 99999, 10, 10);
+ c.addChild(farChild);
+ // draw() guards clipRect with bounds.isFinite(), so even with
+ // clipping enabled, Infinity bounds prevent actual clipping
+ const bounds = c.getBounds();
+ expect(bounds.isFinite()).toEqual(false);
+ // child bounds should still be valid and reachable
+ const childBounds = farChild.getBounds();
+ expect(childBounds.isFinite()).toEqual(true);
+ expect(childBounds.left).toBeGreaterThan(0);
+ });
+
+ it("getBounds() should return valid bounds with Infinity dimensions", () => {
+ const c = new Container();
+ const bounds = c.getBounds();
+ expect(bounds).toBeDefined();
+ expect(typeof bounds.width).toEqual("number");
+ expect(typeof bounds.height).toEqual("number");
+ });
+
+ it("getBounds() should grow to fit children with explicit dimensions", () => {
+ const c = new Container(0, 0, 100, 100);
+ c.enableChildBoundsUpdate = true;
+ const child = new Renderable(10, 20, 50, 60);
+ c.addChild(child);
+ c.updateBounds();
+ const bounds = c.getBounds();
+ expect(bounds.width).toBeGreaterThanOrEqual(50);
+ expect(bounds.height).toBeGreaterThanOrEqual(60);
+ });
+
+ it("getChildByName should work with Infinity-sized container", () => {
+ const c = new Container();
+ const child = new Renderable(0, 0, 32, 32);
+ child.name = "testChild";
+ c.addChild(child);
+ const found = c.getChildByName("testChild");
+ expect(found.length).toEqual(1);
+ expect(found[0]).toBe(child);
+ });
+
+ it("sort should work with Infinity-sized container", () => {
+ const c = new Container();
+ c.autoSort = true;
+ const child1 = new Renderable(0, 0, 10, 10);
+ child1.pos.z = 5;
+ const child2 = new Renderable(0, 0, 10, 10);
+ child2.pos.z = 1;
+ c.addChild(child1);
+ c.addChild(child2);
+ c.sort();
+ expect(c.getChildAt(0).pos.z).toBeLessThanOrEqual(c.getChildAt(1).pos.z);
+ });
+
+ it("isFinite() should return false for Infinity-sized container", () => {
+ const c = new Container();
+ expect(c.isFinite()).toEqual(false);
+ });
+
+ it("isFinite() should return true for explicitly-sized container", () => {
+ const c = new Container(0, 0, 100, 100);
+ expect(c.isFinite()).toEqual(true);
+ });
+
+ it("getBounds().isFinite() should return false for Infinity-sized container", () => {
+ const c = new Container();
+ const bounds = c.getBounds();
+ expect(bounds.isFinite()).toEqual(false);
+ });
+
+ it("getBounds().isFinite() should return true for explicitly-sized container", () => {
+ const c = new Container(0, 0, 200, 150);
+ const bounds = c.getBounds();
+ expect(bounds.isFinite()).toEqual(true);
+ });
+
+ it("updateBounds should not produce NaN for Infinity-sized container", () => {
+ const c = new Container();
+ c.enableChildBoundsUpdate = true;
+ const child = new Renderable(10, 10, 50, 50);
+ c.addChild(child);
+ const bounds = c.updateBounds();
+ expect(Number.isNaN(bounds.width)).toEqual(false);
+ expect(Number.isNaN(bounds.height)).toEqual(false);
+ });
+
+ it("updateBounds should return finite child bounds for Infinity container", () => {
+ const c = new Container();
+ c.enableChildBoundsUpdate = true;
+ const child = new Renderable(10, 10, 50, 50);
+ c.addChild(child);
+ const bounds = c.updateBounds();
+ expect(bounds.isFinite()).toEqual(true);
+ expect(bounds.width).toBeGreaterThanOrEqual(50);
+ expect(bounds.height).toBeGreaterThanOrEqual(50);
+ });
+
+ it("clipping should be skipped for Infinity-sized container", () => {
+ const c = new Container();
+ // clipping requires finite bounds — verify the guard condition
+ expect(c.clipping).toEqual(false);
+ c.clipping = true;
+ const bounds = c.getBounds();
+ // draw() checks bounds.isFinite() before clipping
+ expect(bounds.isFinite()).toEqual(false);
+ });
+
+ it("clipping preconditions should be met for finite container", () => {
+ const c = new Container(0, 0, 200, 150);
+ c.clipping = true;
+ const bounds = c.getBounds();
+ // all three guard conditions in draw() should pass
+ expect(c.root).toEqual(false);
+ expect(c.clipping).toEqual(true);
+ expect(bounds.isFinite()).toEqual(true);
+ expect(bounds.width).toEqual(200);
+ expect(bounds.height).toEqual(150);
+ });
+
+ it("clipping defaults to false", () => {
+ expect(new Container().clipping).toEqual(false);
+ expect(new Container(0, 0, 100, 100).clipping).toEqual(false);
+ });
+
+ it("nested Infinity containers should not produce NaN bounds", () => {
+ const parent = new Container();
+ const child = new Container();
+ child.enableChildBoundsUpdate = true;
+ const renderable = new Renderable(5, 5, 20, 20);
+ child.addChild(renderable);
+ parent.addChild(child);
+ parent.enableChildBoundsUpdate = true;
+ const bounds = parent.updateBounds();
+ expect(Number.isNaN(bounds.width)).toEqual(false);
+ expect(Number.isNaN(bounds.height)).toEqual(false);
+ });
+
+ it("anchorPoint should always be (0,0) for containers", () => {
+ const infinite = new Container();
+ expect(infinite.anchorPoint.x).toEqual(0);
+ expect(infinite.anchorPoint.y).toEqual(0);
+
+ const finite = new Container(0, 0, 200, 100);
+ expect(finite.anchorPoint.x).toEqual(0);
+ expect(finite.anchorPoint.y).toEqual(0);
+ });
+ });
});
diff --git a/packages/melonjs/tests/eventEmitter.spec.ts b/packages/melonjs/tests/eventEmitter.spec.ts
index 06dc9ceef..cda159745 100644
--- a/packages/melonjs/tests/eventEmitter.spec.ts
+++ b/packages/melonjs/tests/eventEmitter.spec.ts
@@ -1,5 +1,5 @@
import { beforeAll, describe, expect, it, test, vi } from "vitest";
-import { boot, event, video } from "../src/index.js";
+import { Application, event } from "../src/index.js";
import { EventEmitter } from "../src/system/eventEmitter";
test("addListener()", () => {
@@ -302,11 +302,9 @@ test("listener without context has undefined this", () => {
// ---------------------------------------------------------------
describe("event.ts public API", () => {
beforeAll(() => {
- boot();
- video.init(64, 64, {
+ new Application(64, 64, {
parent: "screen",
scale: "auto",
- renderer: video.AUTO,
});
});
diff --git a/packages/melonjs/tests/font.spec.js b/packages/melonjs/tests/font.spec.js
index 06eaa00ad..cb6f09043 100644
--- a/packages/melonjs/tests/font.spec.js
+++ b/packages/melonjs/tests/font.spec.js
@@ -17,7 +17,6 @@ describe("Font : Text", () => {
size: 8,
fillStyle: "white",
text: "test",
- offScreenCanvas: false,
});
});
diff --git a/packages/melonjs/tests/imagelayer.spec.js b/packages/melonjs/tests/imagelayer.spec.js
new file mode 100644
index 000000000..ef3af9225
--- /dev/null
+++ b/packages/melonjs/tests/imagelayer.spec.js
@@ -0,0 +1,83 @@
+import { beforeAll, describe, expect, it } from "vitest";
+import { boot, game, ImageLayer, video } from "../src/index.js";
+
+describe("ImageLayer", () => {
+ let testImage;
+
+ beforeAll(async () => {
+ boot();
+ video.init(800, 600, {
+ parent: "screen",
+ scale: "auto",
+ renderer: video.AUTO,
+ });
+
+ // create a small canvas to use as the image source
+ testImage = document.createElement("canvas");
+ testImage.width = 64;
+ testImage.height = 64;
+ });
+
+ describe("createPattern", () => {
+ it("should create pattern when added to game world", () => {
+ const layer = new ImageLayer(0, 0, {
+ image: testImage,
+ name: "test",
+ repeat: "repeat",
+ });
+ game.world.addChild(layer);
+ expect(layer._pattern).toBeDefined();
+ game.world.removeChildNow(layer);
+ });
+ });
+
+ describe("parentApp usage", () => {
+ it("should access viewport via parentApp when in world", () => {
+ const layer = new ImageLayer(0, 0, {
+ image: testImage,
+ name: "test",
+ repeat: "no-repeat",
+ });
+ game.world.addChild(layer);
+ expect(layer.parentApp).toBeDefined();
+ expect(layer.parentApp.viewport).toBe(game.viewport);
+ game.world.removeChildNow(layer);
+ });
+
+ it("should resize to viewport dimensions on activate", () => {
+ const layer = new ImageLayer(0, 0, {
+ image: testImage,
+ name: "test",
+ repeat: "no-repeat",
+ });
+ game.world.addChild(layer);
+ expect(layer.width).toEqual(game.viewport.width);
+ expect(layer.height).toEqual(game.viewport.height);
+ game.world.removeChildNow(layer);
+ });
+
+ it("repeat-x should set width to Infinity on activate", () => {
+ const layer = new ImageLayer(0, 0, {
+ image: testImage,
+ name: "test",
+ repeat: "repeat-x",
+ });
+ game.world.addChild(layer);
+ expect(layer.width).toEqual(Infinity);
+ expect(layer.height).toEqual(game.viewport.height);
+ game.world.removeChildNow(layer);
+ });
+
+ it("repeat-y should set height to Infinity on activate", () => {
+ const layer = new ImageLayer(0, 0, {
+ image: testImage,
+ name: "test",
+ repeat: "repeat-y",
+ });
+ game.world.addChild(layer);
+ expect(layer.width).toEqual(game.viewport.width);
+ expect(layer.height).toEqual(Infinity);
+ game.world.removeChildNow(layer);
+ });
+ });
+});