Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
12218f4
20.13.0: bump the version
claude Oct 31, 2026
acbb163
UI: checkbox, slider, skinned button, panel layout and scrolling (#1722)
claude Oct 31, 2026
1fbc5c9
UI: fixes from review
claude Oct 31, 2026
1bf6e4b
Examples: a taller level list in the UI example
claude Oct 31, 2026
4cacf94
Examples: the UI example's level list at the top right, beside the bars
claude Oct 31, 2026
32626f9
Examples: the UI example's level list shows the first 16 levels
claude Oct 31, 2026
2e3c8f1
UI: consistent widget API, and fixes from a second review
claude Oct 31, 2026
d4a577c
Examples: UI example lint
claude Oct 31, 2026
99c7c51
UI: drop shadows on UIPanel and ProgressBar, used in the UI example
claude Oct 31, 2026
12bedbd
Gamepad plugin: keep a touch's hover events on the controls from the …
claude Oct 31, 2026
59f6398
UI: keyboard and gamepad focus navigation, and a hover/press bounce
claude Oct 31, 2026
6f1acf6
UI: keep the focus navigation to the minimum
claude Oct 31, 2026
44ee453
UI: fixes from a review of the focus navigation and the bounce
claude Oct 31, 2026
5291863
UI: focus lives on the widgets, with no focus manager to set up
claude Oct 31, 2026
1440ba7
Remove a scratch test file committed by mistake
claude Oct 31, 2026
32a4edc
UI: fill the gaps in the widget tests, and fix what they found
claude Oct 31, 2026
596b23b
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Oct 31, 2026
d706c94
gamepad-plugin 1.0.2
claude Oct 31, 2026
d345737
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Oct 31, 2026
b39dd70
Skill: UI widgets, with a widget map and a complete menu example
claude Oct 31, 2026
ec8e1a4
Docs: UICheckbox and UISlider, with examples
claude Oct 31, 2026
57109da
Docs: UIPanel and ProgressBar, with examples
claude Oct 31, 2026
139c957
Docs: UIBaseElement, UITextButton, UISpriteElement, with examples
claude Oct 31, 2026
0a785e6
Changelog: onCancel also covers a called-off bindKey press
claude Oct 31, 2026
c365da3
UI: UISpriteElement lets events through by default, like the others
claude Oct 31, 2026
1d86e82
UI: a press stops at a UI element by default
claude Oct 31, 2026
6d971f4
UI: UISpriteElement is a UIBaseElement holding a Sprite
claude Nov 1, 2026
515a59a
Tests: UISpriteElement's public behaviour, old and new
claude Nov 1, 2026
71c91de
Input: keep the hover after a click, and end it when the pointer leav…
claude Nov 1, 2026
63bfee5
UI: fix the widget bugs found while writing the docs
claude Nov 1, 2026
8a41b06
UI: a radio group holds one choice from when its checkboxes are made
claude Nov 1, 2026
8a47072
Fixes from review: canvas leave reaches every hovered region; UISprit…
claude Nov 1, 2026
6b9b22b
UISpriteElement: setCurrentAnimation passes preserve_dt on
claude Nov 1, 2026
582b27c
Input: a cancelled pointer, or a lifted legacy touch, ends the hover
claude Nov 1, 2026
c70fe49
Changelog: a cancelled pointer ends the hover too
claude Nov 1, 2026
316a7c2
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
6d4a96a
Rendering: a clip inside a clip is the overlap of the two on WebGL an…
claude Nov 1, 2026
4d1c82e
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
c0c2258
Camera: a fade always calls onComplete before it is removed (#1810)
claude Nov 1, 2026
927b0c9
Tests: fadeIn / fadeOut onComplete through the game loop (#1810)
claude Nov 1, 2026
3ebc32a
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
39f681b
Tiled: seed object GUIDs past the map's ids when it has no nextobject…
claude Nov 1, 2026
26494a3
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
b311be0
fix(line): 선분 연장선의 점을 포함하지 않도록 수정
mmporong Oct 31, 2026
ec612a5
fix(color): hex 알파 정밀도를 보존
mmporong Oct 31, 2026
8c16d0c
fix(math): 2의 거듭제곱 입력을 검증
mmporong Oct 31, 2026
864eddd
Color: keep toHex8() output as it was; the hex alpha fix is in parseH…
claude Nov 1, 2026
404663f
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
6349541
Tests: a full local run of the engine suite comes out clean (#1817)
claude Nov 1, 2026
ba0a426
Tests: the audio unlock test waits for the clip to start (#1817)
claude Nov 1, 2026
22b59cd
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
c77ad59
Text: round outline corners by default, lineJoin / miterLimit, and no…
claude Nov 1, 2026
e91423d
Text: document every outline corner option, with tests for changes an…
claude Nov 1, 2026
794b85c
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
923dbef
Text: the outline is drawn behind the glyphs by default (paintOrder)
claude Nov 2, 2026
2c4cb79
Tests: the paint order screen check allows the rounding browsers diff…
claude Nov 2, 2026
1793a09
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 2, 2026
db29efc
Loader: send the game's own origin its credentials by default (#1801)
claude Nov 2, 2026
f6a7c88
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 2, 2026
e946013
Loader: request every asset with CORS, and fail clearly when it is bl…
claude Nov 2, 2026
b5faf66
Tests: a real request for every cross-origin scenario, JSON and image
claude Nov 2, 2026
64d9b51
Tests: the credentials test server allows the test page's origin only
claude Nov 2, 2026
ef4076a
Docs: skills, tilemap warning and examples follow the new asset requests
claude Nov 2, 2026
aa2604d
Changelog: 20.13.0 entries shorter, UI grouped, no fixes to new features
claude Nov 2, 2026
c7ee325
Tests: the fade test steps its tween past the end, not onto it
claude Nov 2, 2026
f0d7e0a
Changelog: only breaking API changes under Changed
claude Nov 2, 2026
4751933
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 2, 2026
c653313
Particles: emitters without an image share the renderer's white pixel…
claude Nov 2, 2026
b5efd63
Tests: many emitters, each its own effect and tint, sharing the white…
claude Nov 2, 2026
4c7e454
Particles: settings.image stays as given; particles pick the square a…
claude Nov 2, 2026
2564de9
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 2, 2026
591ff66
Particles: the constructor centres the spawn area, say so
claude Nov 2, 2026
b2fbd46
Tests: braces around the spawn-area spec's arrow bodies (lint)
claude Nov 2, 2026
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
21 changes: 14 additions & 7 deletions packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,21 @@ fountain and wrong for an explosion: additive sprites piled on one point clip
all three channels there however well they are tinted, so a burst reads as one
bright flash rather than as debris.

Give it the size of whatever broke. The area is measured from the emitter's own
position, NOT centred on it, so offset by half or the whole burst sits down and
to the right of its source:
Give it the size of whatever broke. The constructor centres the area on the
point it is given, so pass the centre of the source:

```js
const area = radius * 2;
const emitter = new ParticleEmitter(x - area / 2, y - area / 2, {
width: area, height: area,
});
const emitter = new ParticleEmitter(x, y, { width: area, height: area });
```

After that, `pos` is the area's top-left corner, as on any renderable. To move
the emitter to a new source later, offset by half, or the burst sits down and
to the right of it:

```js
emitter.pos.set(x - area / 2, y - area / 2); // re-aimed at (x, y)
emitter.burstParticles();
```

Under a `Camera3d`, `depth` (20.13) gives the area its third side: each
Expand Down Expand Up @@ -392,7 +398,8 @@ answer when you want discrete puffs rather than a continuous ribbon.
| frame rate drops with many effects | an emitter per event instead of one re-aimed emitter |
| a 3D burst is invisible under a `Camera3d` | before 20.8 a particle was culled at twice its depth; update |
| the whole burst launches from one pixel | `width`/`height` are the spawn area and default to `1` |
| the burst sits down-right of its source | the spawn area starts at the emitter's position; offset by half |
| the burst sits down-right of its source | the emitter was moved by setting `pos` to the source: `pos` is the area's top-left corner, so offset by half (only the constructor centres it) |
| the burst sits up-left of its source | the constructor was given the top-left corner: it centres the area on (x, y), so pass the centre |
| the middle of a burst goes white | additive sprites overlapping — `textureAdditive: false`, or spread them |
| debris chunks look like squares | the default sprite is a filled square; pass your own `image` |
| a ring tilts with the camera instead of staying on a surface | the launch axis is in WORLD axes, not the camera basis |
Expand Down
10 changes: 7 additions & 3 deletions packages/melonjs/src/particles/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,15 @@ export default class ParticleEmitter extends Container {
_spawnMap: Matrix3d | undefined;

/**
* @param x - x position of the particle emitter
* @param y - y position of the particle emitter
* The spawn area (`settings.width` by `settings.height`) is centred on
* (x, y). After construction `pos` is the area's top left corner, as on
* any renderable: to move the emitter to a new point, offset by half the
* area.
* @param x - x position of the centre of the spawn area
* @param y - y position of the centre of the spawn area
* @param [settings=ParticleEmitterSettings] - the settings for the particle emitter.
* @example
* // Create a particle emitter at position 100, 100
* // Create a particle emitter centred on 100, 100
* let emitter = new ParticleEmitter(100, 100, {
* width: 16,
* height : 16,
Expand Down
3 changes: 2 additions & 1 deletion packages/melonjs/src/particles/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import type ParticleEmitter from "./emitter.ts";
*/
export interface ParticleEmitterSettings {
/**
* Width of the particle spawn area.
* Width of the particle spawn area. The constructor centres the area on
* the point it is given; `pos` is then its top left corner.
* @default 1
*/
width: number;
Expand Down
73 changes: 73 additions & 0 deletions packages/melonjs/tests/particle-spawn-area.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { Application, boot, ParticleEmitter, video } from "../src/index.js";

/**
* Where the spawn area is. The constructor centres it on the point it is
* given; after that, `pos` is its top left corner, as for any renderable.
*/
describe("particle spawn area", () => {
let app;

beforeAll(async () => {
boot();
app = new Application(800, 600, {
parent: "screen",
renderer: video.CANVAS,
});
await app.init();
});

afterAll(() => {
app.destroy?.();
});

const spawned = (emitter) => {
app.world.addChild(emitter);
emitter.burstParticles(200);
const xs = emitter.getChildren().map((p) => {
return p.pos.x;
});
const ys = emitter.getChildren().map((p) => {
return p.pos.y;
});
app.world.removeChildNow(emitter);
return {
minX: Math.min(...xs),
maxX: Math.max(...xs),
minY: Math.min(...ys),
maxY: Math.max(...ys),
};
};

it("the constructor centres the area on (x, y)", () => {
const emitter = new ParticleEmitter(200, 150, {
width: 100,
height: 60,
referenceSpace: "world",
});
expect(emitter.pos.x).toBe(150);
expect(emitter.pos.y).toBe(120);
const area = spawned(emitter);
expect(area.minX).toBeGreaterThanOrEqual(150);
expect(area.maxX).toBeLessThanOrEqual(250);
expect(area.minY).toBeGreaterThanOrEqual(120);
expect(area.maxY).toBeLessThanOrEqual(180);
// and it is spread around that centre, not piled in one corner
expect(area.minX).toBeLessThan(170);
expect(area.maxX).toBeGreaterThan(230);
});

it("moving pos afterwards places the area's top left corner", () => {
const emitter = new ParticleEmitter(0, 0, {
width: 100,
height: 60,
referenceSpace: "world",
});
emitter.pos.set(300, 200);
const area = spawned(emitter);
expect(area.minX).toBeGreaterThanOrEqual(300);
expect(area.maxX).toBeLessThanOrEqual(400);
expect(area.minY).toBeGreaterThanOrEqual(200);
expect(area.maxY).toBeLessThanOrEqual(260);
});
});
Loading