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
19 changes: 15 additions & 4 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
### Added
- Effects: colors take an alpha. Every color option and `setColor` takes `[r, g, b]` or `[r, g, b, a]`, the alpha being the color's opacity (`[r, g, b]` stays opaque), `RadialGradientEffect` honours the alpha of its `Color`, and `DropShadowEffect` gains the `setColor` the others have
- Effects: `SwayEffect` leans a sprite or a layer from side to side, as if in a breeze, its top swaying most and everything from its anchor row down staying put. It is a `WaveEffect`, which now takes an `anchor` for the row its wave fades out at, and it wraps its own time so it never stutters on a phone's medium-precision floats
- Skills: the effects skill covers colors with an alpha and `SwayEffect`, with an example of each
- Rendering: `Renderable#sortOffset` lines a renderable up as if it stood that many pixels lower (or higher) when its container sorts on `"y"` or `"x"`, without moving it: a torch drawn over the wall it hangs on, dust sorted with the door it rises from, or art of different heights sorted by its base
- Lighting: `Light2d#softness` (0 to 1) fades the edge of a light's hole in the stage's ambient darkness, from a hard rim (0, the default, as before) to a darkness that thins straight from the center (1), along the same falloff as the glow. Hard and soft lights mix in one scene, overlapping lights multiply, and it works on every renderer
- Particles: `tintOverLife` gives the tints a particle takes over its life, evenly spaced and blended from the first at birth to the last at the end, such as a flame turning from white hot to deep red. It sets the color only (the fade still sets the opacity), and the blend is taken in 32 steps, which keeps the Canvas renderer to 32 tinted copies of the texture
- Animation: `setCurrentAnimation(name, { onFrame })` calls back with each frame index as playback reaches it, from frame 0 when the animation starts, so a footstep or a hit can land on its frame. It applies to the frame animations of a `Sprite` or a `Sprite3d`
- Audio: `playTrack(name, { volume, sprite })` plays one region of a clip as the track and loops on it. A region of a loop's exact length keeps an MP3 loop gapless in Safari, which plays the padding an MP3 encoder adds to the end of the file
- Tiled: a tile object with no class of its own is built by the class registered under its tile's class, as Tiled shows it, so a class set once on a tile in the tileset applies to every object placed from it. The object's own class or registered name still comes first, and a tile classed like a built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile
- Tiled: `TMXObject#resolvedProperty()` and `resolvedProperties()` return an object's custom properties together with those it inherits from its tile, the object's own winning, as the methods of the same names in Tiled's scripting API do. Only the object's own properties are set on it by name, as before
- Tiled: `TMXTileset#getTileClass()` returns the class set on a tile in Tiled's tileset editor
- Typings: `TMXObject`, the settings a class registered with `registerTiledObjectClass` (or a factory registered with `registerTiledObjectFactory`) receives, is exported as a type, and both functions are typed with it. A class or factory typed with its own settings still compiles

### Changed
- Rendering: a container sorted on `"x"` or `"y"` keeps its children in order as they move. With `autoSort` on, the default, it checks the order at the end of every update and re-sorts when anything is out of place, so a walking character no longer needs a `sort()` call to pass in front of and behind things. A frame where nothing moved costs one pass over the children and causes no redraw; turn `autoSort` off to keep the order you set. A `"z"` sort is unchanged

### Fixed
- Effects: a color given four values no longer breaks the effect. WebGL rejected it, and on WebGPU the fourth value overwrote the next setting, such as an outline's width; a color of any other length now throws a `TypeError` saying so
- Effects: a colored drop shadow below full opacity is no longer too bright, nor are the soft edges of an outline. Both added their color at full strength whatever its opacity
Expand All @@ -28,9 +35,13 @@
- Physics: the builtin `raycast` measures a body from the frame its renderable draws in, as collisions have since 20.7. On a body whose anchor is not its top left corner, such as a `Sprite`'s default centre, a ray hit half the body's size away from where it collides, or missed it
- Camera: a zoomed camera follows its target, stays within its bounds and centres on `focusOn` in world units. Zoomed out, it kept the target off centre and scrolled past its bounds; zoomed in, it stopped short of them
- Camera: `localToWorld` and `worldToLocal` take the camera's zoom and its place on the screen (`screenX`, `screenY`) into account. A pointer on a zoomed camera, or on one drawn in a corner of the screen, hit the world somewhere else than it pointed
- Skills: the lighting skill no longer says a sprite without a normal map is lit flatly. Only a normal-mapped sprite is shaded by a `Light2d`; any other sprite just sits under its glow and its cutout
- Skills: the drawing skill never said that on a GPU backend a `CanvasRenderTarget` keeps its texture for as long as the renderer lives. It now says to create one once and reuse it, and to call `invalidate()` after painting it again
- Skills: the tilemaps skill now explains that `sortOn = "y"` sorts objects and never tiles, so scenery a character walks in front of and behind belongs on the character's own object layer as tile objects. It also warns that Tiled objects sort by the top of their image, and that a character with the default collision mask collides with the default body of every class-less tile object
- Effects: a glow, outline, blur, drop shadow, chromatic aberration, wave, sway, pixelate or bloom on a sprite cut from an atlas or a tileset keeps to the sprite's own frame. Along the frame's edges it picked up the neighbouring frames, and drew a glow or a line from pixels that are not part of the sprite
- Camera: a zoomed camera draws its floating renderables, such as a HUD or a `ColorLayer`, and a `ColorLayer` fills all of its view. It skipped them all, as a minimap does, on every renderer
- Lighting: on the Canvas renderer, a light's hole in the ambient darkness is cut where the light is under a scrolled camera. The mask kept the darkness inside the light and cleared it around it instead
- Lighting: on the Canvas renderer, the ambient darkness of a zoomed camera, or of one drawn at an offset such as a minimap, has its holes where the lights are. They were cut as if the camera were neither zoomed nor offset
- Lighting: the ambient darkness covers the first column and row of a camera at a fractional position, such as one following its target with damping. Unless `subPixel` was set, they were left lit
- Lighting: the ambient darkness covers the whole view of a zoomed-out camera. It covered as much of the world as the camera is wide, leaving the rest of the view lit
- Audio: a sprite region declared looping (`[start, duration, true]`) loops when played with `audio.play(name, { sprite })`. It played only once unless `loop: true` was passed as well
- Text: a label whose text is empty or only spaces no longer blanks the whole frame on WebGPU. Its canvas was sized 0 pixels wide, which WebGPU refuses to upload
- Typings: `TextureAtlas#getAnimationSettings()` is typed as the settings it returns, so they pass to a `Sprite` constructor from TypeScript without a cast. It returned `object`, and `Sprite3d` typed the named animations an aseprite atlas carries as an array

Expand Down
23 changes: 22 additions & 1 deletion packages/melonjs/skills/melonjs-audio/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,27 @@ await loader.preload([{
audio.play("sfx", { sprite: "jump" });
```

The optional third element marks a region as looping. `play()`'s second argument
### A loop with no gap

An MP3 encoder pads the end of its file. Chrome and Firefox trim the padding when
they decode it, Safari keeps it, so a looping MP3 has a short silence at every
loop there (27 ms on a 12 s clip at 22 kHz, measured). Declare a region of the
loop's exact length and loop on that: the voice never reaches the padding.

```js
await loader.preload([{
name: "ambience", type: "audio", src: "data/sfx/",
sprite: { loop: [0, 12000] },
}]);
audio.playTrack("ambience", { sprite: "loop", volume: 0.6 });
// or, as a plain clip: audio.play("ambience", { sprite: "loop", loop: true })
```

Ogg Vorbis carries no such padding.

The optional third element marks a region as looping: it loops unless `play()` is
told `loop: false` (before 20.11 it only looped when `loop: true` was passed as
well). `play()`'s second argument
takes either the original `loop` boolean or a `PlayOptions` object
(`sprite`, `loop`, `onend`, `volume`, plus the spatial `follow`, `at` and
`stopWithTarget`) — both forms work. Object fields win over the positional
Expand Down Expand Up @@ -467,6 +487,7 @@ either way — blank screen, empty console.
| a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |
| a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |
| a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |
| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see "A loop with no gap") |
| sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |

## Related skills
Expand Down
9 changes: 9 additions & 0 deletions packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ covers. `[r, g, b]` is opaque, as it always was. Any other length throws.
sprite.addPostEffect(new OutlineEffect(app.renderer, { color: [1, 1, 1, 0.5] }));
```

**On a sprite cut from an atlas or a tileset, the effects that read around a
pixel keep to the sprite's own frame.** `GlowEffect`, `OutlineEffect`,
`BlurEffect`, `DropShadowEffect`, `ChromaticAberrationEffect`, `WaveEffect`,
`SwayEffect`, `PixelateEffect` and `BloomEffect` read the neighbouring frames
as transparent, as if the sprite were drawn on its own, so they never show
along its edges and the atlas needs no padding for them. Past the texture's own
edge (a standalone image, or the outside of an atlas) nothing changes. Their `textureSize` is still the size of the whole atlas image: it is
what turns a width in texels into a distance in the texture.

`SwayEffect` leans a sprite or a layer from side to side, as if in a breeze: the
top sways most and everything from its `anchor` row down stays put, so a tree's
trunk or a reed's root stays planted. It suits anything planted at its bottom;
Expand Down
31 changes: 29 additions & 2 deletions packages/melonjs/skills/melonjs-lighting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ normal-mapped sprites.
returns immediately while its alpha is 0 — so with no ambient colour set there
is nothing to cut through and the lights just look like soft additive blobs.

## Soft edges: `softness`

A light's hole in the darkness is hard-edged by default: full light inside its
radius, full darkness outside. `light.softness` (0 to 1) fades it instead:

```js
const torch = new Light2d(x, y, 220, 220, "#ffb46a", 0.4);
torch.softness = 0.8; // lit core of 20% of the radius, then fading out
```

- `1` thins the darkness straight from the centre to the edge, the same falloff
as the glow; in between, a fully lit core of `1 - softness` of the radius.
- Hard and soft lights mix in one scene. Where lights overlap, what is left of the
darkness is what each leaves, multiplied, so two half-lights make three
quarters light.
- The hole is the light's size. For an opening wider than the glow (a torch whose
light reaches further than its flame's halo), add a second, bigger light with
`illuminationOnly = true`: it cuts the darkness and draws nothing.
- Up to 32 lights in view are cut by one soft pass; lights outside the view do not
count. A scene with no soft light draws exactly as before.
- To keep a camera out of the darkness (a minimap showing the level lit),
override `drawLighting` on the stage and skip that camera.

## Multiple lights

Add as many as you need — the glow quads and the ambient cutouts accumulate with
Expand Down Expand Up @@ -141,8 +164,9 @@ Less than you would expect. `Light2d` is renderer-agnostic: it just calls
`renderer.drawLight(this)`, and every backend implements it — the GPU backends
through a shared procedural radial-falloff shader, the Canvas renderer by
rasterising a cached `Gradient` into one shared render target and compositing it
with `drawImage`. The ambient cutout pass is plain `setMask` + `fillRect`. So the
lit-spot look works everywhere, Canvas included.
with `drawImage`. The hard ambient cutout pass is plain `setMask` + `fillRect`;
the soft one is one shader pass on the GPU backends and an offscreen canvas on
Canvas. So the lit-spot look, soft or hard, works everywhere, Canvas included.

What Canvas cannot do is **per-pixel normal-map lighting**:
`renderer.setLightUniforms()` is a no-op there, and the first frame that passes a
Expand Down Expand Up @@ -172,6 +196,9 @@ See the `melonjs-3d` skill.
| symptom | cause |
|---|---|
| lights added but the scene never darkens | no ambient colour set — `ambientLight` alpha is 0, so the pass returns early |
| the lit area has a hard rim | `softness` is 0 (the default): set it on the light |
| the darkness opens no wider than the glow | the hole is the light's size: add a bigger `illuminationOnly` light for the opening |
| a minimap or second camera is dark too | every camera draws the stage's darkness: override `drawLighting` and skip that camera |
| sprite not shaded by a light, only glowed over | no `normalMap` on the sprite, or the Canvas backend is active |
| normal-mapped sprite is pure black where unlit | `Stage.ambientLightingColor` left at black |
| the 33rd light shades nothing | `MAX_LIGHTS = 32` for the normal-map pipeline |
Expand Down
22 changes: 21 additions & 1 deletion packages/melonjs/skills/melonjs-math/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: melonjs-math
description: "Use this skill for vector and matrix maths in melonJS — Vector2d, Vector3d, their observable variants, Matrix2d, Matrix3d, and the math utility functions. Covers which methods mutate and which allocate, the vector pool and when clone() is the wrong tool, interpolation with lerp, slerp, damp and moveTowards, and the argument defaults that silently drop a component. Triggers on: Vector2d, Vector3d, ObservableVector2d, ObservableVector3d, Matrix2d, Matrix3d, vector, vector maths, dot, cross, normalize, length, distance, angle, lerp, slerp, damp, moveTowards, project, clamp, degToRad, radToDeg, clone, vector pool, vector3dPool, setMuted, setCallback, my vector is wrong, z is zero, scale ignores z., moveTowards ignores z, projectN, currentTransform, setTransform, matrix, transform matrix, column-major, val, Float32Array, basis matrix, orientation matrix."
description: "Use this skill for vector and matrix maths in melonJS — Vector2d, Vector3d, their observable variants, Matrix2d, Matrix3d, and the math utility functions. Covers which methods mutate and which allocate, the vector pool and when clone() is the wrong tool, interpolation with lerp, slerp, damp and moveTowards, and the argument defaults that silently drop a component. Triggers on: Vector2d, Vector3d, ObservableVector2d, ObservableVector3d, Matrix2d, Matrix3d, vector, vector maths, dot, cross, normalize, length, distance, angle, lerp, slerp, damp, moveTowards, project, clamp, degToRad, radToDeg, clone, vector pool, vector3dPool, setMuted, setCallback, my vector is wrong, z is zero, scale ignores z., moveTowards ignores z, projectN, currentTransform, setTransform, matrix, transform matrix, column-major, val, Float32Array, basis matrix, orientation matrix, Noise, getNoise1d, flicker, wobble."
license: MIT
---

Expand Down Expand Up @@ -291,6 +291,25 @@ math.pow(n); // n squared
math.DEG_TO_RAD math.RAD_TO_DEG math.TAU math.ETA math.EPSILON
```

### Values that wobble over time

`Noise`, the generator behind `NoiseTexture2d`, also samples directly. For a
value that should drift without repeating (a torch's flicker, a sway, a camera
wander), sample it with time:

```js
import { Noise } from "melonjs";

const noise = new Noise({ seed: 7, frequency: 1.5, octaves: 3 });
// around 1; give each object its own offset so they do not move together
const flicker = 1 + 0.2 * noise.getNoise1d(seconds + offset);
```

`getNoise1d` returns `[-1, 1]` (with fractal octaves most values fall within
about ±0.6), `frequency` is in changes per unit of the input, and the same seed
gives the same field every run. A sum of sines repeats where people notice it,
and `Math.random()` every frame jitters instead of drifting.

## Symptoms

| symptom | cause |
Expand All @@ -305,6 +324,7 @@ math.DEG_TO_RAD math.RAD_TO_DEG math.TAU math.ETA math.EPSILON
| you built your own 16-number array | `Renderable.currentTransform` is already a `Matrix3d`; write its `val` |
| a value read back from a matrix differs | `val` is `Float32Array`; it holds float32 |
| easing differs at 120fps | per-frame `lerp`; use `damp` |
| a flicker or sway looks mechanical, or visibly repeats | a sum of sines or a per-frame `Math.random()`: sample `Noise#getNoise1d(time)` |

## Related skills

Expand Down
Loading
Loading