Skip to content
Closed
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
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 @@ -146,15 +146,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 @@ -386,7 +392,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 @@ -370,11 +370,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
69 changes: 69 additions & 0 deletions packages/melonjs/tests/particle-spawn-area.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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) => p.pos.x);

Check failure on line 27 in packages/melonjs/tests/particle-spawn-area.spec.js

View workflow job for this annotation

GitHub Actions / lint

Expected block statement surrounding arrow body
const ys = emitter.getChildren().map((p) => p.pos.y);

Check failure on line 28 in packages/melonjs/tests/particle-spawn-area.spec.js

View workflow job for this annotation

GitHub Actions / lint

Expected block statement surrounding arrow body
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