Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
container:
# must match the workspace's playwright version (root devDependencies)
image: mcr.microsoft.com/playwright:v1.61.1-noble
image: mcr.microsoft.com/playwright:v1.62.0-noble
steps:
- uses: actions/checkout@v5
- name: Setup node
Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.5/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"files": {
"includes": [
Expand Down
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,25 @@
},
"packageManager": "pnpm@10.32.1",
"dependencies": {
"@biomejs/biome": "2.4.11",
"@biomejs/biome": "2.5.5",
"@eslint/js": "^10.0.1",
"@types/node": "^25.6.0",
"@vitest/browser": "^4.1.10",
"@vitest/browser-playwright": "^4.1.10",
"eslint": "^10.6.0",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint": "^10.8.0",
"eslint-plugin-jsdoc": "^63.2.2",
"globals": "^17.7.0",
"lefthook": "^2.1.10",
"tsconfig": "workspace:^",
"tsx": "^4.23.0",
"turbo": "^2.10.4",
"tsx": "^4.23.1",
"turbo": "^2.10.7",
"typescript": "^6.0.2",
"typescript-eslint": "^8.63.0",
"typescript-eslint": "^8.65.0",
"vitest": "^4.1.10"
},
"devDependencies": {
"eslint-plugin-react-refresh": "^0.5.2",
"playwright": "^1.61.1"
"eslint-plugin-react-refresh": "^0.5.3",
"playwright": "^1.62.0"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand All @@ -57,10 +57,10 @@
"minimatch@<4.0.0": "3.1.4",
"minimatch@>=9.0.0 <9.0.7": "9.0.7",
"ajv@<6.14.0": "6.14.0",
"vite": "8.0.16",
"vite": "8.1.5",
"esbuild": "0.28.1",
"ws": "^8.21.0",
"shell-quote": "^1.8.4",
"shell-quote": "^1.9.0",
"markdown-it": "^14.2.0",
"react-router": "^7.15.1",
"vitest": "^4.1.10",
Expand Down
2 changes: 1 addition & 1 deletion packages/capacitor-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"esbuild": "^0.28.0",
"melonjs": "workspace:*",
"tsconfig": "workspace:*",
"tsx": "^4.23.0",
"tsx": "^4.23.1",
"typescript": "^6.0.2"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/debug-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"esbuild": "^0.27.3",
"melonjs": "workspace:*",
"tsconfig": "workspace:*",
"tsx": "^4.23.0",
"tsx": "^4.23.1",
"typescript": "^6.0.2"
},
"scripts": {
Expand Down
8 changes: 4 additions & 4 deletions packages/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
"@melonjs/tiled-inflate-plugin": "workspace:*",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"@vitejs/plugin-react": "^6.0.4",
"ace-builds": "^1.44.0",
"melonjs": "workspace:^",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.1",
"tsconfig": "workspace:^",
"typescript": "^6.0.2",
"vite": "8.0.16"
"vite": "8.1.5"
}
}
2 changes: 1 addition & 1 deletion packages/matter-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"esbuild": "^0.27.3",
"melonjs": "workspace:*",
"tsconfig": "workspace:*",
"tsx": "^4.23.0",
"tsx": "^4.23.1",
"typescript": "^6.0.2",
"vitest": "^4.1.10"
},
Expand Down
24 changes: 24 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## [19.9.1] (melonJS 2) - _2026-07-28_

### Fixed
- video sprite frames stalled (~4fps) in fullscreen — a pending `requestVideoFrameCallback` now keeps the browser delivering frames at full rate
- video sprites never paused on `state.pause()` (wrong event name)
- `onVisibilityChange` fired a spurious leave/enter pair every frame for visible renderables
- destroyed cameras kept reacting to game reset / canvas resize; stages now destroy the cameras they construct (one leaked per state switch with `cameraClass`)
- camera bounds clamp ignored a non-zero world origin, making the far edges unreachable
- dead particles were pool-released while their removal was still deferred — same-frame respawns could silently vanish
- a completed `loop: false` animation could never be replayed via `play()` / `setCurrentAnimation()`
- animation frames with a `0` delay froze the game in an infinite loop
- `Container` / `Entity` `destroy()` passed an `Arguments` object to `onDestroyEvent` instead of the actual arguments
- `DropTarget.setCheckMethod()` accepted invalid method names (validated the wrong variable)
- `Container.getNextChild()` returned the first child for a non-member instead of `undefined`
- `Text` / `BitmapText` `destroy()` wiped a caller-provided string array (now copied on assignment)
- `NineSliceSprite` showed ~2px seams for region sizes not divisible by 4 (fractional corners were truncated)
- the canvas slowly grew on window resize inside auto-sized wrappers ([#1231](https://github.com/melonjs/melonJS/issues/1231)) — the canvas now defaults to `display: block` and auto-scale measures the container's content box
- `ShaderEffect.setTexture()` rejects `HTMLVideoElement` with a `TypeError` instead of silently freezing on the first frame
- BMFont `padding` was parsed with `padLeft` / `padRight` swapped (AngelCode order is up/right/down/left)
- removed the texture cache's dead frame-dimension refinement ([#1489](https://github.com/melonjs/melonJS/issues/1489)); the first-registered-atlas behavior is now explicit

### Performance
- video textures upload to the GPU only when a new frame was actually presented (previously every render tick, per sprite)

## [19.9.0] (melonJS 2) - _2026-07-14_

**Highlights:** shader effects made easy. Effects that used to demand WebGL expertise, like a pond rippling with the scene reflected in it, heat haze, or frosted glass, now take a few lines of shader code: the engine hands your effect the screen behind it and the right coordinates, animated noise textures come built-in, and shaders preload like any other asset. See the new **Water Overworld** example for all of it in action. Also in this release: named anchor presets (`"bottom"`, `"top-left"`, and friends) on every renderable, shapes with holes in `Path2D`/SVG fills, and a 40+ bug-fix sweep across the loader, audio, texture atlas, and WebGL rendering.
Expand Down
8 changes: 4 additions & 4 deletions packages/melonjs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "melonjs",
"version": "19.9.0",
"version": "19.9.1",
"description": "melonJS Game Engine",
"homepage": "http://www.melonjs.org/",
"type": "module",
Expand Down Expand Up @@ -66,12 +66,12 @@
"esbuild": "^0.27.3",
"serve": "^14.2.6",
"tsconfig": "workspace:^",
"tsx": "^4.23.0",
"tsx": "^4.23.1",
"type-fest": "^5.8.0",
"typedoc": "^0.28.20",
"typescript": "^6.0.2",
"vite": "8.0.16",
"vite-plugin-glsl": "^1.6.0"
"vite": "8.1.5",
"vite-plugin-glsl": "^1.6.1"
},
"scripts": {
"dev": "concurrently --raw \"pnpm build:watch\" \"pnpm tsc:watch\" \"pnpm doc:watch\"",
Expand Down
12 changes: 11 additions & 1 deletion packages/melonjs/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
if (physic === "none") {
return { adapter: undefined, physicLabel: "none" };
}
if (physic === undefined || physic === "builtin") {

Check warning on line 95 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, the types have no overlap

Check warning on line 95 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, the types have no overlap

Check warning on line 95 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary conditional, the types have no overlap
return { adapter: undefined, physicLabel: "builtin" };
}
// instance or { adapter } object — extract and pass through. The
Expand All @@ -102,7 +102,7 @@
// predating the `physicLabel` field.
const adapter =
typeof physic === "object" && "adapter" in physic ? physic.adapter : physic;
return { adapter, physicLabel: adapter?.physicLabel ?? "builtin" };

Check warning on line 105 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value

Check warning on line 105 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value

Check warning on line 105 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary optional chain on a non-nullish value
}

/**
Expand Down Expand Up @@ -337,7 +337,7 @@
this.settings = settings;

// identify parent element and/or the html target for resizing
this.parentElement = device.getElement(this.settings.parent!);

Check warning on line 340 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 340 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 340 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
if (typeof this.settings.scaleTarget !== "undefined") {
this.settings.scaleTarget = device.getElement(this.settings.scaleTarget);
}
Expand Down Expand Up @@ -468,7 +468,17 @@
});

// add our canvas (default to document.body if settings.parent is undefined)
this.parentElement.appendChild(this.renderer.getCanvas());
const canvas = this.renderer.getCanvas();
// the browser default `display: inline` puts the canvas on the text
// baseline, so an auto-sized ancestor measures canvas + baseline gap —
// and the auto-scale resize path then re-applies that few-px excess on
// EVERY resize event (the slow canvas-growth loop of #1231). `block`
// removes the gap. Only applied when the canvas carries no explicit
// inline display, so user styling stays authoritative.
if (canvas.style.display === "") {
canvas.style.display = "block";
}
this.parentElement.appendChild(canvas);

// Mobile browser hacks
if (device.platform.isMobile) {
Expand Down Expand Up @@ -548,7 +558,7 @@
if (this.settings.consoleHeader) {
if (this.world.physic === "none") {
console.log("physics: disabled");
} else if (this.world.adapter) {

Check warning on line 561 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 561 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 561 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary conditional, value is always truthy
const a = this.world.adapter as {
constructor: { name: string };
name?: string;
Expand Down Expand Up @@ -608,7 +618,7 @@
// point to the current active stage "default" camera
const current = state.get();
if (typeof current !== "undefined") {
this.viewport = current.cameras.get("default")!;

Check warning on line 621 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 621 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 621 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
}

// publish reset notification
Expand Down Expand Up @@ -702,12 +712,12 @@
this.parentElement) as ElementWithLegacyFullscreen;
/* eslint-disable @typescript-eslint/unbound-method -- `this` is restored explicitly via `.call(target)` below */
const request =
target.requestFullscreen ||

Check warning on line 715 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 715 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 715 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary conditional, value is always truthy
target.webkitRequestFullscreen ||
target.mozRequestFullScreen ||
target.msRequestFullscreen;
/* eslint-enable @typescript-eslint/unbound-method */
const result = request?.call(target);

Check warning on line 720 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value

Check warning on line 720 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value

Check warning on line 720 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary optional chain on a non-nullish value
if (result instanceof Promise) result.catch(console.error);
}
}
Expand All @@ -721,12 +731,12 @@
const doc = globalThis.document as DocumentWithLegacyExitFullscreen;
/* eslint-disable @typescript-eslint/unbound-method -- `this` is restored explicitly via `.call(doc)` below */
const exit =
doc.exitFullscreen ||

Check warning on line 734 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 734 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 734 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary conditional, value is always truthy
doc.webkitExitFullscreen ||
doc.mozCancelFullScreen ||
doc.msExitFullscreen;
/* eslint-enable @typescript-eslint/unbound-method */
const result = exit?.call(doc);

Check warning on line 739 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value

Check warning on line 739 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value

Check warning on line 739 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary optional chain on a non-nullish value
if (result instanceof Promise) result.catch(console.error);
}

Expand Down Expand Up @@ -778,7 +788,7 @@
globalThis.removeEventListener("resize", this._onResize);
globalThis.removeEventListener(
"orientationchange",
this._onOrientationChange!,

Check warning on line 791 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 791 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 791 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
);
globalThis.removeEventListener("scroll", this._onScroll!);
if (device.screenOrientation) {
Expand Down
37 changes: 33 additions & 4 deletions packages/melonjs/src/application/resize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,45 @@ export function onresize(game: Application): void {
canvasMaxHeight = parseInt(style.maxHeight, 10) || Infinity;
}

let measuredElement;
if (typeof game.settings.scaleTarget !== "undefined") {
// get the bounds of the given scale target
nodeBounds = device.getElementBounds(game.settings.scaleTarget);
measuredElement = game.settings.scaleTarget;
nodeBounds = device.getElementBounds(measuredElement);
} else {
// get the maximum canvas size within the parent div containing the canvas container
nodeBounds = device.getParentBounds(game.getParentElement());
measuredElement = device.getParentElement(game.getParentElement());
nodeBounds = device.getElementBounds(measuredElement);
}

const _max_width = Math.min(canvasMaxWidth, nodeBounds.width);
const _max_height = Math.min(canvasMaxHeight, nodeBounds.height);
let availWidth = nodeBounds.width;
let availHeight = nodeBounds.height;
// The space available to a child is the measured element's CONTENT
// box; `getBoundingClientRect` returns the border box. On an
// auto-sized container the difference (padding + border) gets baked
// into the canvas size and re-measured on the next pass — growing
// the canvas by that amount on every resize event (#1231). The
// window-fallback path (string target / document.body) has no
// padding concept and is left untouched.
if (
typeof measuredElement === "object" &&
typeof globalThis.getComputedStyle === "function"
) {
const style = globalThis.getComputedStyle(measuredElement);
availWidth -=
(parseFloat(style.paddingLeft) || 0) +
(parseFloat(style.paddingRight) || 0) +
(parseFloat(style.borderLeftWidth) || 0) +
(parseFloat(style.borderRightWidth) || 0);
availHeight -=
(parseFloat(style.paddingTop) || 0) +
(parseFloat(style.paddingBottom) || 0) +
(parseFloat(style.borderTopWidth) || 0) +
(parseFloat(style.borderBottomWidth) || 0);
}

const _max_width = Math.min(canvasMaxWidth, Math.max(0, availWidth));
const _max_height = Math.min(canvasMaxHeight, Math.max(0, availHeight));

// calculate final canvas width & height
const screenRatio = _max_width / _max_height;
Expand Down
22 changes: 18 additions & 4 deletions packages/melonjs/src/camera/camera2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CANVAS_ONRESIZE,
emit,
GAME_RESET,
off,
on,
VIEWPORT_ONCHANGE,
VIEWPORT_ONRESIZE,
Expand Down Expand Up @@ -322,9 +323,12 @@ export default class Camera2d extends Renderable {
_followH(target: Vector2d | Vector3d): number {
let targetX = this.pos.x;
if (target.x - this.pos.x > this.deadzone.right) {
// clamp against the bounds' right EDGE — `bounds.width` is
// `right - left`, so using it as a max only worked for a
// zero-origin world and cut `left` px off the far side
targetX = Math.min(
target.x - this.deadzone.right,
this.bounds.width - this.width,
this.bounds.right - this.width,
);
} else if (target.x - this.pos.x < this.deadzone.pos.x) {
targetX = Math.max(target.x - this.deadzone.pos.x, this.bounds.left);
Expand All @@ -338,7 +342,7 @@ export default class Camera2d extends Renderable {
if (target.y - this.pos.y > this.deadzone.bottom) {
targetY = Math.min(
target.y - this.deadzone.bottom,
this.bounds.height - this.height,
this.bounds.bottom - this.height,
);
} else if (target.y - this.pos.y < this.deadzone.pos.y) {
targetY = Math.max(target.y - this.deadzone.pos.y, this.bounds.top);
Expand Down Expand Up @@ -591,8 +595,10 @@ export default class Camera2d extends Renderable {
const _x = this.pos.x;
const _y = this.pos.y;

this.pos.x = clamp(x, this.bounds.left, this.bounds.width - this.width);
this.pos.y = clamp(y, this.bounds.top, this.bounds.height - this.height);
// clamp against the bounds' far edges — `bounds.width/height` are
// extents (`right - left`), correct as a max only for a zero origin
this.pos.x = clamp(x, this.bounds.left, this.bounds.right - this.width);
this.pos.y = clamp(y, this.bounds.top, this.bounds.bottom - this.height);

//publish the VIEWPORT_ONCHANGE event if necessary
if (_x !== this.pos.x || _y !== this.pos.y) {
Expand Down Expand Up @@ -1106,6 +1112,14 @@ export default class Camera2d extends Renderable {
* @ignore
*/
override destroy(): void {
// unsubscribe the constructor-registered global listeners — without
// this a destroyed camera stays reachable forever and keeps reacting
// to GAME_RESET (TypeError on freed state) and CANVAS_ONRESIZE
// (duplicate VIEWPORT_ONRESIZE emits from zombie cameras)
/* eslint-disable @typescript-eslint/unbound-method -- listener identity must match the `on(...)` registration */
off(GAME_RESET, this.reset, this);
off(CANVAS_ONRESIZE, this.resize, this);
/* eslint-enable @typescript-eslint/unbound-method */
// clean up the internal colorMatrix effect (may not be in postEffects if identity)
if (this._colorMatrixEffect) {
if (typeof this._colorMatrixEffect.destroy === "function") {
Expand Down
12 changes: 10 additions & 2 deletions packages/melonjs/src/particles/particle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,16 @@ export default class Particle extends Renderable {

if (this.alive && this.life <= 0) {
const parent = this.ancestor as Container;
// use true for keepalive since we recycle the instance directly here after
parent.removeChild(this, true);
// IMMEDIATE removal (not the deferred `removeChild`) because the
// instance is released to the pool on the next line: a deferred
// removal leaves a stale `removeChildNow` pending against this
// instance, so a same-frame respawn recycling it would be
// silently killed by that timer, and a same-frame emitter
// teardown would destroy an instance already sitting in the
// pool (poisoning every later `particlePool.get()`). The
// immediate splice is safe here — `Container.update` walks its
// children in reverse. keepalive=true since we recycle directly.
parent.removeChildNow(this, true);
particlePool.release(this);
this.alive = false;
return false;
Expand Down
4 changes: 3 additions & 1 deletion packages/melonjs/src/particles/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ export interface ParticleEmitterSettings {
blendMode: string;

/**
* Update particles only in the viewport; remove when out of viewport.
* Only repaint particles while they are inside the viewport (off-screen
* particles keep simulating and living out their lifetime, but do not
* mark the scene dirty).
* @default true
*/
onlyInViewport: boolean;
Expand Down
24 changes: 16 additions & 8 deletions packages/melonjs/src/renderable/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,9 +578,11 @@ export default class Container extends Renderable {
* @returns {Renderable} child
*/
getNextChild(child) {
const index = this.getChildren().indexOf(child) + 1;
if (index >= 0 && index < this.getChildren().length) {
return this.getChildAt(index);
const index = this.getChildren().indexOf(child);
// `indexOf` returns -1 for a non-member — without this check the +1
// would alias "not found" to index 0 and return the first child
if (index !== -1 && index + 1 < this.getChildren().length) {
return this.getChildAt(index + 1);
}
return undefined;
}
Expand Down Expand Up @@ -1098,8 +1100,10 @@ export default class Container extends Renderable {
destroy() {
// empty the container
this.reset();
// call the parent destroy method
super.destroy(arguments);
// call the parent destroy method, spreading the actual arguments —
// passing the `arguments` object itself would hand subclasses'
// `onDestroyEvent(app)` an Arguments wrapper instead of the value
super.destroy(...arguments);

colorPool.release(this.backgroundColor);
}
Expand Down Expand Up @@ -1129,14 +1133,18 @@ export default class Container extends Renderable {
globalFloatingCounter++;
}

// check if object is in any active cameras
obj.inViewport = false;
// check if object is in any active cameras — accumulate into a
// local and assign ONCE: assigning `false` then `true` through
// the setter would fire `onVisibilityChange` (a change-detecting
// setter) twice per frame for every continuously-visible object
let inViewport = false;
// iterate through all cameras
cameras.forEach((camera) => {
if (camera.isVisible(obj, isFloating)) {
obj.inViewport = true;
inViewport = true;
}
});
obj.inViewport = inViewport;

// update our object
this.isDirty |= (obj.inViewport || obj.alwaysUpdate) && obj.update(dt);
Expand Down
2 changes: 1 addition & 1 deletion packages/melonjs/src/renderable/dragndrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class DropTarget extends Renderable {
setCheckMethod(checkMethod) {
// We can improve this check,
// because now you can use every method in theory
if (typeof this.getBounds()[this.checkMethod] === "function") {
if (typeof this.getBounds()[checkMethod] === "function") {
this.checkMethod = checkMethod;
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/melonjs/src/renderable/entity/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,10 @@ export default class Entity extends Renderable {
this.children.splice(0, 1);
}

// call the parent destroy method
super.destroy(arguments);
// call the parent destroy method, spreading the actual arguments —
// passing the `arguments` object itself would hand subclasses'
// `onDestroyEvent(app)` an Arguments wrapper instead of the value
super.destroy(...arguments);
}

/**
Expand Down
Loading