diff --git a/packages/examples/src/examples/clipping/ExampleClipping.tsx b/packages/examples/src/examples/clipping/ExampleClipping.tsx index 7185e89c7d..ae3e9145b7 100644 --- a/packages/examples/src/examples/clipping/ExampleClipping.tsx +++ b/packages/examples/src/examples/clipping/ExampleClipping.tsx @@ -140,27 +140,19 @@ class PlayScreen extends Stage { 4, ); - // RIGHT — wrapper centered on its pos, with a clipping - // container offset back to the top-left. The wrapper's - // `currentTransform` gets a sinusoidal scale each frame, so - // the inner clip pulses around the wrapper's center. - const RIGHT_CENTER_X = 370; - const RIGHT_CENTER_Y = 160; + // RIGHT — wrapper at top-left with a clipping container as a + // direct child (no negative offset). The wrapper's + // `currentTransform` gets a sinusoidal scale each frame, + // composed as translate→scale→translate-back so the pulse + // breathes around the wrapper's visual center while keeping + // `pos` and bounds aligned with what's actually drawn. + const RIGHT_X = 280; + const RIGHT_Y = 80; const RIGHT_W = 180; const RIGHT_H = 160; - const wrapper = new Container( - RIGHT_CENTER_X, - RIGHT_CENTER_Y, - RIGHT_W, - RIGHT_H, - ); + const wrapper = new Container(RIGHT_X, RIGHT_Y, RIGHT_W, RIGHT_H); wrapper.clipping = false; - const innerClip = new Container( - -RIGHT_W / 2, - -RIGHT_H / 2, - RIGHT_W, - RIGHT_H, - ); + const innerClip = new Container(0, 0, RIGHT_W, RIGHT_H); innerClip.clipping = true; innerClip.addChild(new OverflowingRect("#e74c3c")); wrapper.addChild(innerClip); @@ -170,11 +162,17 @@ class PlayScreen extends Stage { wrapper.alwaysUpdate = true; let t = 0; const baseUpdate = wrapper.update.bind(wrapper); + const cx = RIGHT_W / 2; + const cy = RIGHT_H / 2; wrapper.update = function (dt: number) { t += dt; - this.currentTransform.identity(); const s = 1 + 0.25 * Math.sin(t * 0.0025); + // scale around (cx, cy) without moving the wrapper's pos: + // translate to center → scale → translate back. + this.currentTransform.identity(); + this.currentTransform.translate(cx, cy); this.currentTransform.scale(s, s); + this.currentTransform.translate(-cx, -cy); return baseUpdate(dt) || true; }; game.world.addChild(wrapper, 2); diff --git a/packages/melonjs/src/physics/bounds.ts b/packages/melonjs/src/physics/bounds.ts index 672800adb9..e6929f1f6f 100644 --- a/packages/melonjs/src/physics/bounds.ts +++ b/packages/melonjs/src/physics/bounds.ts @@ -286,11 +286,12 @@ export class Bounds { y1: number, m?: Matrix2d | Matrix3d, ) { - if (m === undefined) { - // no transform: fold the 4 corners' min/max directly into - // the AABB without any Point allocation. Mirrors `addPoint` - // for each corner — using Math.min/max so the caller may - // pass swapped corners (x1 < x0 etc.) without breaking. + if (m === undefined || m.isIdentity()) { + // no transform (or a no-op transform): fold the 4 corners' + // min/max directly into the AABB without any Point or + // `m.apply` work. Mirrors `addPoint` for each corner — + // using Math.min/max so the caller may pass swapped corners + // (x1 < x0 etc.) without breaking. const minX = Math.min(x0, x1); const maxX = Math.max(x0, x1); const minY = Math.min(y0, y1); diff --git a/packages/melonjs/src/video/webgl/webgl_renderer.js b/packages/melonjs/src/video/webgl/webgl_renderer.js index 01ca9a336b..290d337260 100644 --- a/packages/melonjs/src/video/webgl/webgl_renderer.js +++ b/packages/melonjs/src/video/webgl/webgl_renderer.js @@ -903,12 +903,12 @@ export default class WebGLRenderer extends Renderer { enableScissor(x, y, width, height) { const gl = this.gl; const canvas = this.getCanvas(); - // Walk the 4 corners through `currentTransform` to derive the - // screen-space AABB, matching the convention `clipRect` and - // `restore` use. `currentScissor` stores screen-space coords - // directly so save/restore can re-apply without re-running - // transform math (and so a scaled/rotated parent transform is - // honored, not just translation). Issue #1349. + // Derive the screen-space AABB via `Bounds.addFrame`, which + // short-circuits the 4-corner walk when `currentTransform` is + // identity. `currentScissor` stores screen-space coords directly + // so save/restore can re-apply without re-running transform math + // and so a scaled or rotated parent transform is honored — not + // just translation. Issue #1349. const aabb = this._clipAABB; aabb.clear(); aabb.addFrame(x, y, x + width, y + height, this.currentTransform); @@ -2237,11 +2237,12 @@ export default class WebGLRenderer extends Renderer { } // derive the screen-space AABB by feeding the rect's 4 corners - // through `currentTransform` via `Bounds.addFrame`. `gl.scissor` - // is not transform-aware, so any rotation collapses to the - // rotated-rect AABB on screen — Canvas's `context.clip()` would - // produce a true polygonal clip, but downstream rendering only - // observes the AABB anyway. Issue #1349. + // through `currentTransform` via `Bounds.addFrame` (which short- + // circuits the corner walk when the transform is identity). + // `gl.scissor` is not transform-aware, so any rotation collapses + // to the rotated-rect AABB on screen — Canvas's `context.clip()` + // would produce a true polygonal clip, but downstream rendering + // only observes the AABB anyway. Issue #1349. const aabb = this._clipAABB; aabb.clear(); aabb.addFrame(x, y, x + width, y + height, m); diff --git a/packages/melonjs/tests/bounds.spec.ts b/packages/melonjs/tests/bounds.spec.ts index b00839ac3f..d2fa1205d2 100644 --- a/packages/melonjs/tests/bounds.spec.ts +++ b/packages/melonjs/tests/bounds.spec.ts @@ -142,6 +142,23 @@ describe("Physics : Bounds", () => { expect(b.width).toBeCloseTo(100, 5); expect(b.height).toBeCloseTo(200, 5); }); + it("addFrame with an identity matrix takes the no-corner-walk fast path", () => { + // Sentinel: spy on `m.apply` and confirm the identity + // short-circuit in addFrame avoids calling it. Guards + // against a future refactor that drops the identity check. + const m = new Matrix3d(); + let applyCalls = 0; + const orig = m.apply.bind(m); + m.apply = (v) => { + applyCalls += 1; + return orig(v); + }; + const b = new Bounds(); + b.addFrame(10, 50, 110, 250, m); + expect(applyCalls).toBe(0); + expect(b.x).toBeCloseTo(10, 5); + expect(b.width).toBeCloseTo(100, 5); + }); it("addFrame with Matrix3d translation shifts the AABB", () => { const m = new Matrix3d(); m.translate(40, 30);