melonjs 20.2.0 → 20.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -8
- package/build/application/settings.d.ts +3 -2
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/audio.d.ts +2 -2
- package/build/audio/audio.d.ts.map +1 -1
- package/build/audio/backend/buffer.d.ts +26 -0
- package/build/audio/backend/buffer.d.ts.map +1 -0
- package/build/audio/backend/cache.d.ts +2 -0
- package/build/audio/backend/cache.d.ts.map +1 -0
- package/build/audio/backend/core.d.ts +24 -0
- package/build/audio/backend/core.d.ts.map +1 -0
- package/build/audio/backend/engine.d.ts +54 -0
- package/build/audio/backend/engine.d.ts.map +1 -0
- package/build/audio/backend/sound.d.ts +79 -0
- package/build/audio/backend/sound.d.ts.map +1 -0
- package/build/audio/backend/spatial.d.ts +191 -0
- package/build/audio/backend/spatial.d.ts.map +1 -0
- package/build/audio/backend/types.d.ts +97 -0
- package/build/audio/backend/types.d.ts.map +1 -0
- package/build/audio/backend/voice.d.ts +33 -0
- package/build/audio/backend/voice.d.ts.map +1 -0
- package/build/audio/playback.d.ts +49 -4
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/{backend.d.ts → state.d.ts} +13 -11
- package/build/audio/state.d.ts.map +1 -0
- package/build/audio/types.d.ts +93 -1
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +5 -5
- package/build/const.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +8793 -10171
- package/build/index.js.map +4 -4
- package/build/input/input.d.ts +19 -0
- package/build/input/input.d.ts.map +1 -1
- package/build/lang/deprecated.d.ts +4 -2
- package/build/lang/deprecated.d.ts.map +1 -1
- package/build/level/gltf/GLTFScene.d.ts +12 -0
- package/build/level/gltf/GLTFScene.d.ts.map +1 -1
- package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
- package/build/loader/loader.d.ts +4 -40
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/parsers/audio.d.ts +31 -0
- package/build/loader/parsers/audio.d.ts.map +1 -0
- package/build/loader/parsers/shader.d.ts.map +1 -1
- package/build/physics/adapter.d.ts +3 -3
- package/build/physics/builtin/body.d.ts +23 -24
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/physics/builtin/detector.d.ts +1 -1
- package/build/plugin/plugin.d.ts +9 -1
- package/build/plugin/plugin.d.ts.map +1 -1
- package/build/polyfill/index.d.ts +1 -4
- package/build/polyfill/index.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +16 -0
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts +16 -10
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +7 -7
- package/build/renderable/sprite.d.ts.map +1 -1
- package/build/state/stage.d.ts +4 -2
- package/build/state/stage.d.ts.map +1 -1
- package/build/system/event.d.ts +1 -1
- package/build/system/platform.d.ts +21 -1
- package/build/system/platform.d.ts.map +1 -1
- package/build/utils/fetchdata.d.ts.map +1 -0
- package/build/video/blendmodes.d.ts +1 -1
- package/build/video/canvas/canvas_renderer.d.ts +5 -2
- package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
- package/build/video/effects/blur.d.ts +4 -4
- package/build/video/effects/blur.d.ts.map +1 -1
- package/build/video/effects/chromaticAberration.d.ts +5 -5
- package/build/video/effects/chromaticAberration.d.ts.map +1 -1
- package/build/video/effects/colorMatrix.d.ts +5 -5
- package/build/video/effects/colorMatrix.d.ts.map +1 -1
- package/build/video/effects/desaturate.d.ts +5 -5
- package/build/video/effects/desaturate.d.ts.map +1 -1
- package/build/video/effects/dissolve.d.ts +4 -4
- package/build/video/effects/dissolve.d.ts.map +1 -1
- package/build/video/effects/dropShadow.d.ts +5 -5
- package/build/video/effects/dropShadow.d.ts.map +1 -1
- package/build/video/effects/flash.d.ts +4 -4
- package/build/video/effects/flash.d.ts.map +1 -1
- package/build/video/effects/glow.d.ts +5 -5
- package/build/video/effects/glow.d.ts.map +1 -1
- package/build/video/effects/hologram.d.ts +4 -4
- package/build/video/effects/hologram.d.ts.map +1 -1
- package/build/video/effects/invert.d.ts +5 -5
- package/build/video/effects/invert.d.ts.map +1 -1
- package/build/video/effects/outline.d.ts +6 -6
- package/build/video/effects/outline.d.ts.map +1 -1
- package/build/video/effects/pixelate.d.ts +4 -4
- package/build/video/effects/pixelate.d.ts.map +1 -1
- package/build/video/effects/radialGradient.d.ts +2 -2
- package/build/video/effects/scanline.d.ts +6 -6
- package/build/video/effects/scanline.d.ts.map +1 -1
- package/build/video/effects/sepia.d.ts +5 -5
- package/build/video/effects/sepia.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts +12 -10
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/effects/shine.d.ts +3 -3
- package/build/video/effects/shine.d.ts.map +1 -1
- package/build/video/effects/tintPulse.d.ts +4 -4
- package/build/video/effects/tintPulse.d.ts.map +1 -1
- package/build/video/effects/vignette.d.ts +6 -6
- package/build/video/effects/vignette.d.ts.map +1 -1
- package/build/video/effects/wave.d.ts +4 -4
- package/build/video/effects/wave.d.ts.map +1 -1
- package/build/video/texture/atlas.d.ts +1 -1
- package/build/video/webgl/glshader.d.ts +2 -2
- package/build/video/webgl/webgl_renderer.d.ts +2 -1
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +2 -1
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +5 -9
- package/skills/AGENTS.md +67 -0
- package/skills/melonjs/SKILL.md +156 -0
- package/skills/melonjs-20-migration/SKILL.md +160 -0
- package/skills/melonjs-3d/SKILL.md +226 -0
- package/skills/melonjs-3d-assets/SKILL.md +239 -0
- package/skills/melonjs-audio/SKILL.md +215 -0
- package/skills/melonjs-camera-and-drawing/SKILL.md +171 -0
- package/skills/melonjs-deployment/SKILL.md +113 -0
- package/skills/melonjs-effects-and-shaders/SKILL.md +177 -0
- package/skills/melonjs-events/SKILL.md +159 -0
- package/skills/melonjs-getting-started/SKILL.md +183 -0
- package/skills/melonjs-input/SKILL.md +166 -0
- package/skills/melonjs-lighting/SKILL.md +139 -0
- package/skills/melonjs-loading-assets/SKILL.md +157 -0
- package/skills/melonjs-particles-and-trails/SKILL.md +136 -0
- package/skills/melonjs-performance/SKILL.md +178 -0
- package/skills/melonjs-physics/SKILL.md +321 -0
- package/skills/melonjs-plugins/SKILL.md +191 -0
- package/skills/melonjs-renderables/SKILL.md +214 -0
- package/skills/melonjs-renderer-backends/SKILL.md +173 -0
- package/skills/melonjs-scenes-and-state/SKILL.md +180 -0
- package/skills/melonjs-sprites-and-animation/SKILL.md +173 -0
- package/skills/melonjs-tilemaps/SKILL.md +193 -0
- package/skills/melonjs-ui-and-text/SKILL.md +175 -0
- package/build/audio/backend.d.ts.map +0 -1
- package/build/loader/parsers/fetchdata.d.ts.map +0 -1
- /package/build/{loader/parsers → utils}/fetchdata.d.ts +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-audio
|
|
3
|
+
description: "Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, and procedural tone/noise generation. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, sprite audio, stereo, panner, audio.tone, audio.noise, getAudioContext, autoplay."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Audio
|
|
8
|
+
|
|
9
|
+
## Two setup rules
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { audio, loader } from "melonjs";
|
|
13
|
+
|
|
14
|
+
audio.init("mp3,ogg"); // 1. BEFORE preloading any audio
|
|
15
|
+
|
|
16
|
+
await loader.preload([
|
|
17
|
+
{ name: "cling", type: "audio", src: "data/sfx/" }, // 2. a PREFIX, not a file
|
|
18
|
+
]);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
1. **`audio.init(format)` must run before any audio asset is preloaded**, and
|
|
22
|
+
lists the formats you actually ship. Preloading first throws
|
|
23
|
+
`target audio extension(s) should be set through me.audio.init() before
|
|
24
|
+
calling the preloader.` It returns `false` when no audio backend is
|
|
25
|
+
available, so it is worth checking. The format list defaults to `"mp3"`.
|
|
26
|
+
2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus
|
|
27
|
+
each configured extension and uses the first that loads — so
|
|
28
|
+
`data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename
|
|
29
|
+
gives you a 404. A `data:audio/...` URL is the one exception: it is used
|
|
30
|
+
verbatim, prefix and extension skipped.
|
|
31
|
+
|
|
32
|
+
Order matters — melonJS tries the listed formats left to right, so put the
|
|
33
|
+
preferred one first. Two formats (`"webm,mp3"` or `"mp3,ogg"`) is the usual
|
|
34
|
+
belt-and-braces for codec coverage. Accepted tokens: `mp3`, `mpeg`, `opus`,
|
|
35
|
+
`ogg`, `oga`, `wav`, `aac`, `caf`, `m4a`, `m4b`, `mp4`, `weba`, `webm`, `dolby`,
|
|
36
|
+
`flac`.
|
|
37
|
+
|
|
38
|
+
## Effects versus music
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
audio.play("cling"); // fire and forget
|
|
42
|
+
audio.play("cling", false, null, 0.5); // loop, onend, volume
|
|
43
|
+
const id = audio.play("engine", true); // looping; keep the id to stop it
|
|
44
|
+
|
|
45
|
+
audio.playTrack("theme"); // background music
|
|
46
|
+
audio.stopTrack();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always
|
|
50
|
+
loops, and it records the name as the current track — which is what
|
|
51
|
+
`app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via
|
|
52
|
+
`state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,
|
|
53
|
+
`resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there
|
|
54
|
+
is only ever one current track. Stop it in `onDestroyEvent`.
|
|
55
|
+
|
|
56
|
+
Every per-clip function throws `audio clip <name> does not exist` if the clip was
|
|
57
|
+
never loaded, and `play` additionally throws when a named sprite region is
|
|
58
|
+
missing.
|
|
59
|
+
|
|
60
|
+
## Audio sprites
|
|
61
|
+
|
|
62
|
+
One file carrying many effects, with named regions in milliseconds:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
await loader.preload([{
|
|
66
|
+
name: "sfx", type: "audio", src: "data/sfx/",
|
|
67
|
+
sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },
|
|
68
|
+
}]);
|
|
69
|
+
|
|
70
|
+
audio.play("sfx", { sprite: "jump" });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The optional third element marks a region as looping. `play()`'s second argument
|
|
74
|
+
takes either the original `loop` boolean or a `PlayOptions` object
|
|
75
|
+
(`sprite`, `loop`, `onend`, `volume`) — both forms work. Object fields win over
|
|
76
|
+
the positional `onend` / `volume` arguments when both are supplied.
|
|
77
|
+
|
|
78
|
+
## Clip state
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
audio.duration("theme"); // seconds — 0 while still loading
|
|
82
|
+
audio.duration("sfx", id); // duration of the region THAT instance plays
|
|
83
|
+
audio.playing("theme"); // is anything playing?
|
|
84
|
+
audio.playing("theme", id); // is that instance playing?
|
|
85
|
+
audio.state("theme"); // "unloaded" | "loading" | "loaded"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`stop()`, `pause()` and `resume()` take an optional instance `id` too; omit it
|
|
89
|
+
and the whole group is affected. `audio.stop()` with no arguments at all stops
|
|
90
|
+
every sound. `audio.seek(name)` reads the position, `audio.seek(name, s, id?)`
|
|
91
|
+
writes it; `audio.rate` and `audio.fade(name, from, to, ms, id?)` round it out.
|
|
92
|
+
|
|
93
|
+
## Volume and muting
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
audio.setVolume(0.5); // global, 0..1
|
|
97
|
+
audio.getVolume(); // read it back
|
|
98
|
+
audio.mute("cling"); // one clip — mute(name, id?, shouldMute = true)
|
|
99
|
+
audio.unmute("cling");
|
|
100
|
+
audio.muteAll(); // everything (audio.disable() is an alias)
|
|
101
|
+
audio.unmuteAll(); // (audio.enable() is an alias)
|
|
102
|
+
audio.muted(); // global mute state
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`
|
|
106
|
+
also clears the current-track pointer if it named that clip.
|
|
107
|
+
|
|
108
|
+
## Spatial audio
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
audio.position("engine", x, y, z);
|
|
112
|
+
audio.orientation("engine", x, y, z);
|
|
113
|
+
audio.stereo("engine", -1); // -1 left … 1 right
|
|
114
|
+
audio.panner("engine", { coneInnerAngle: 90, /* … */ });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`position(name, x, y = 0, z = -0.5, id?)` and
|
|
118
|
+
`orientation(name, x, y, z, id?)` both default `id` to "the whole group".
|
|
119
|
+
Called with just the clip name, each of the four returns the current value —
|
|
120
|
+
`stereo` gives `0` and `position` gives `[0, 0, 0]` before they have ever been
|
|
121
|
+
set.
|
|
122
|
+
|
|
123
|
+
**Spatial audio is WebAudio-only.** On a clip loaded with `stream: true` (or
|
|
124
|
+
`html5: true`) `stereo` / `position` / `orientation` return early and do
|
|
125
|
+
nothing — which is exactly the case for the long music tracks people reach for
|
|
126
|
+
first.
|
|
127
|
+
|
|
128
|
+
## Procedural sound
|
|
129
|
+
|
|
130
|
+
For beeps, blips and noise without shipping files. Both take **one options
|
|
131
|
+
object**, and `duration` (in seconds) is required:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
audio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });
|
|
135
|
+
audio.tone({ freq: [440, 660, 880], duration: 0.4 }); // layered partials
|
|
136
|
+
audio.noise({ duration: 0.5, filter: { type: "lowpass", frequency: 800 } });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`tone` also takes `wave` (default `"sine"`), `attack` (`0.005`) and `pitchSlide`
|
|
140
|
+
(`1`). `noise` takes `type` (`"white"` | `"pink"` | `"brown"`, default
|
|
141
|
+
`"white"`), `attack`, `pan`, `filter` and `filterSweep`. `gain` defaults to `0.1`
|
|
142
|
+
on both.
|
|
143
|
+
|
|
144
|
+
Both are **silent no-ops** when there is no WebAudio context — they bail on
|
|
145
|
+
`getAudioContext() === null` rather than throwing. Neither needs the clip to be
|
|
146
|
+
loaded; neither goes through the file-playback path at all.
|
|
147
|
+
|
|
148
|
+
`audio.getAudioContext()` and `audio.getMasterGain()` are the escape hatches for
|
|
149
|
+
custom Web Audio work; both return `null` when audio is unavailable, so guard.
|
|
150
|
+
Connect a custom graph to the master gain rather than `ctx.destination` if you
|
|
151
|
+
want it to respect `setVolume` / `muteAll`.
|
|
152
|
+
|
|
153
|
+
## Autoplay: sound is silent until the first gesture
|
|
154
|
+
|
|
155
|
+
Browsers create the audio context in the `suspended` state and keep it there
|
|
156
|
+
until a user interaction. Nothing errors — audio simply does not play, which
|
|
157
|
+
reads as a bug. melonJS installs one-shot `touchstart` / `touchend` / `click` /
|
|
158
|
+
`keydown` unlock listeners on `document`, so the first real gesture anywhere
|
|
159
|
+
unlocks it and fires the clip's `on.unlock` callback. Design for it: start music
|
|
160
|
+
on the first click or key press, not on load. `tone` / `noise` additionally call
|
|
161
|
+
`ctx.resume()` themselves, best-effort, on every invocation.
|
|
162
|
+
|
|
163
|
+
Playback also starts **asynchronously**: `play()` returns an id while the
|
|
164
|
+
instance is still starting, so `playing()` can legitimately be `false` on the
|
|
165
|
+
very next line.
|
|
166
|
+
|
|
167
|
+
## Load-time options
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
{ name: "sfx", type: "audio", src: "data/sfx/",
|
|
171
|
+
sprite: { jump: [0, 450] },
|
|
172
|
+
pool: 5, // finished instances kept for reuse — NOT a concurrency cap
|
|
173
|
+
rate: 1.0, // initial playback rate, 0.5..4.0
|
|
174
|
+
mute: false,
|
|
175
|
+
loop: false,
|
|
176
|
+
autoplay: false,
|
|
177
|
+
preload: true, // or "metadata"
|
|
178
|
+
format: "mp3", // explicit hint when the URL has no usable extension
|
|
179
|
+
stream: true, // long music: stream rather than decode into memory
|
|
180
|
+
on: { play() {}, end() {}, stop() {}, unlock() {} },
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`pool` (default `5`) is a **reuse cap on finished voices**, not a limit on
|
|
185
|
+
simultaneous playback: when the number of ended voices exceeds it, the surplus
|
|
186
|
+
are disconnected and dropped. Raise it for a clip triggered in rapid bursts, not
|
|
187
|
+
to allow more overlap.
|
|
188
|
+
|
|
189
|
+
Use `stream: true` for long music tracks — it plays through an HTML5 `<audio>`
|
|
190
|
+
element instead of decoding the whole file into memory. (`html5: true` is the
|
|
191
|
+
same switch under its backend name.) `on` accepts the full lifecycle set:
|
|
192
|
+
`play`, `pause`, `stop`, `end`, `fade`, `seek`, `rate`, `volume`, `mute`,
|
|
193
|
+
`unlock`.
|
|
194
|
+
|
|
195
|
+
Re-preloading a manifest that already contains a loaded clip is a no-op —
|
|
196
|
+
`audio.unload(name)` first if you genuinely want to reload it.
|
|
197
|
+
|
|
198
|
+
## Symptom → cause
|
|
199
|
+
|
|
200
|
+
| symptom | cause |
|
|
201
|
+
|---|---|
|
|
202
|
+
| audio assets 404 | `src` given as a full filename instead of a directory prefix |
|
|
203
|
+
| `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |
|
|
204
|
+
| `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |
|
|
205
|
+
| nothing plays until the user clicks | browser autoplay policy — expected, design around it |
|
|
206
|
+
| `playing()` is false right after `play()` | playback starts asynchronously |
|
|
207
|
+
| music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |
|
|
208
|
+
| `stereo` / `position` do nothing | the clip was loaded with `stream: true` — spatial is WebAudio-only |
|
|
209
|
+
| one format works, another does not | that codec is not in the `audio.init()` list |
|
|
210
|
+
| `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |
|
|
211
|
+
| `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |
|
|
212
|
+
|
|
213
|
+
## Related skills
|
|
214
|
+
|
|
215
|
+
- `melonjs-getting-started` — preloading and asset ordering
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-camera-and-drawing
|
|
3
|
+
description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, and baking with CanvasRenderTarget. Triggers on: Camera2d, viewport, follow, setBounds, shake, fadeIn, fadeOut, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, Rect, Ellipse, Polygon, Line, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Camera and immediate-mode drawing
|
|
8
|
+
|
|
9
|
+
## Following a target
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
app.viewport.follow(player, app.viewport.AXIS.BOTH, 0.1);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Pass the renderable, not `renderable.pos`.** `follow()` accepts a `Renderable`,
|
|
16
|
+
a `Vector2d` or a `Vector3d` and **throws** `invalid target for me.Camera2d.follow`
|
|
17
|
+
for anything else. A renderable's `pos` is an `ObservableVector3d`, which is not
|
|
18
|
+
a `Vector3d` subclass, so `follow(player.pos, …)` throws.
|
|
19
|
+
|
|
20
|
+
The third argument is a damping factor in `[0 .. 1]` — lower is smoother and
|
|
21
|
+
laggier, `1` snaps. Omit it for a hard lock (it defaults to `1`). It is
|
|
22
|
+
frame-rate independent since 19.7. `AXIS.HORIZONTAL` / `AXIS.VERTICAL` /
|
|
23
|
+
`AXIS.BOTH` / `AXIS.NONE`.
|
|
24
|
+
|
|
25
|
+
Call it from the followed entity's `onActivateEvent`, using `this.parentApp`.
|
|
26
|
+
|
|
27
|
+
## Bounds are the thing people forget
|
|
28
|
+
|
|
29
|
+
The camera's bounds default to the **renderer size**, so it cannot scroll until
|
|
30
|
+
you widen them to the world:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
app.viewport.setBounds(0, 0, mapWidth, mapHeight);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Symptom: `follow()` is called, looks right, and the view never moves. Note a
|
|
37
|
+
canvas resize resets the default camera's bounds to `(0, 0, w, h)`, so re-apply
|
|
38
|
+
them from a `CANVAS_ONRESIZE` handler if your game resizes. Loading a Tiled map
|
|
39
|
+
sets them for you from the map size.
|
|
40
|
+
|
|
41
|
+
## Camera effects
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
app.viewport.shake(4, 200); // intensity, ms
|
|
45
|
+
app.viewport.fadeIn("#000000", 500); // fade TO black
|
|
46
|
+
app.viewport.fadeOut("#000000", 500); // start black, fade back to normal
|
|
47
|
+
app.viewport.colorMatrix.contrast(1.1).saturate(1.1);
|
|
48
|
+
app.viewport.addPostEffect(new VignetteEffect(app.renderer));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The fade pair is named from the *colour's* point of view, not the scene's:
|
|
52
|
+
`fadeIn` fades the colour in (screen goes to `color`), `fadeOut` fades it out
|
|
53
|
+
(screen starts at `color` and clears). Both default to a 1000 ms duration and
|
|
54
|
+
take an optional completion callback.
|
|
55
|
+
|
|
56
|
+
`colorMatrix` (`brightness` / `contrast` / `saturate` / `hueRotate` / `sepia` /
|
|
57
|
+
`invertColors`, all chainable) is the cheap way to grade a whole camera: the
|
|
58
|
+
camera appends it as a final post-effect pass by itself whenever the matrix is
|
|
59
|
+
non-identity, and drops it again after the frame. It is **not** shader-free —
|
|
60
|
+
it is a `ColorMatrixEffect`, i.e. a `ShaderEffect`, so like every post effect it
|
|
61
|
+
is inert on the Canvas renderer.
|
|
62
|
+
|
|
63
|
+
## Coordinate conversion
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
app.viewport.worldToLocal(x, y); // world → screen
|
|
67
|
+
app.viewport.localToWorld(x, y); // screen → world
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Both return a `Vector2d`; with no third argument that vector comes from the
|
|
71
|
+
shared pool, so in a per-frame path pass your own `v` as the third argument, or
|
|
72
|
+
release it with `getPool("vector2d").release(v)`.
|
|
73
|
+
|
|
74
|
+
Pointer events already carry every frame of reference: `gameWorldX`/`gameWorldY`
|
|
75
|
+
(world), `gameScreenX`/`gameScreenY` (screen), `gameLocalX`/`gameLocalY`
|
|
76
|
+
(relative to the registered region's container) and `gameX`/`gameY`, which is
|
|
77
|
+
whichever of world/screen matches the region — screen for a `floating` region,
|
|
78
|
+
world otherwise. Use the screen pair for anything that *moves* the camera, or
|
|
79
|
+
the drag feeds back on itself.
|
|
80
|
+
|
|
81
|
+
## Secondary cameras
|
|
82
|
+
|
|
83
|
+
Cameras are first-class — a minimap is a second camera with `autoResize = false`
|
|
84
|
+
and an overridden `postDraw`:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
this.cameras.set("minimap", new MinimapCamera());
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Immediate-mode drawing
|
|
91
|
+
|
|
92
|
+
Inside a custom `draw(renderer)` you can draw shapes directly. Remember the two
|
|
93
|
+
renderable rules: read `this.pos`, and zero the anchor.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
draw(renderer) {
|
|
97
|
+
renderer.setColor("#ff8800");
|
|
98
|
+
renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);
|
|
99
|
+
|
|
100
|
+
renderer.setColor("#ffffff");
|
|
101
|
+
renderer.lineWidth = 2; // property, not setLineWidth()
|
|
102
|
+
renderer.stroke(new Line(0, 0, [/* … */]));
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Geometry available: `Rect`, `RoundRect`, `Ellipse`, `Polygon`, `Line`, `Point`
|
|
107
|
+
(and a `Bounds`). `renderer.fill(shape)` and `renderer.stroke(shape)` take any
|
|
108
|
+
of them and throw `Invalid geometry for fill/stroke` for anything else.
|
|
109
|
+
|
|
110
|
+
`Gradient` gives linear and radial fills, built with
|
|
111
|
+
`renderer.createLinearGradient(x0, y0, x1, y1)` /
|
|
112
|
+
`createRadialGradient(x0, y0, r0, x1, y1, r1)` and then handed to
|
|
113
|
+
`renderer.setColor(gradient)`. `NoiseTexture2d` generates cellular and simplex
|
|
114
|
+
noise — including seamless tiling and `asNormalMap` — so you rarely need to ship
|
|
115
|
+
noise PNGs.
|
|
116
|
+
|
|
117
|
+
## Clipping and masking
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
renderer.clipRect(x, y, w, h); // rectangular clip
|
|
121
|
+
renderable.mask = new Ellipse(/* … */); // shape mask on an object
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Note that a `Container` with `clipping` enabled clips **after** its own
|
|
125
|
+
`translate(pos)`, in container-local coordinates — `clipRect(0, 0, width,
|
|
126
|
+
height)`. It also silently does nothing unless the container is non-root and has
|
|
127
|
+
a finite `width`/`height`: a `Container` built with no explicit size is
|
|
128
|
+
`Infinity × Infinity`, and `clipping = true` on it is a no-op.
|
|
129
|
+
|
|
130
|
+
## Bake once, draw many
|
|
131
|
+
|
|
132
|
+
When you would otherwise issue hundreds of identical draws every frame, render
|
|
133
|
+
them once into a `CanvasRenderTarget` and draw the result:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const rt = new CanvasRenderTarget(width, height, { context: "2d" });
|
|
137
|
+
const ctx = rt.context; // a CanvasRenderingContext2D — draw into it once…
|
|
138
|
+
// then draw the backing canvas, not the target object:
|
|
139
|
+
renderer.drawImage(rt.canvas, x, y);
|
|
140
|
+
// or wrap it: new Sprite(0, 0, { image: rt.canvas })
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Pass `rt.canvas`, never `rt` — a `CanvasRenderTarget` is a wrapper, not a
|
|
144
|
+
drawable, and the engine's own bakes (the particle emitter's default texture)
|
|
145
|
+
hand `.canvas` on for exactly that reason.
|
|
146
|
+
|
|
147
|
+
This is the sanctioned performance idiom before reaching for a custom shader.
|
|
148
|
+
|
|
149
|
+
`renderer.toFrameTexture()` is the related tool for capturing the *current frame*
|
|
150
|
+
into a texture for a shader to sample.
|
|
151
|
+
|
|
152
|
+
## Symptom → cause
|
|
153
|
+
|
|
154
|
+
| symptom | cause |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `follow()` set but the view never moves | viewport bounds still at renderer size |
|
|
157
|
+
| `invalid target for me.Camera2d.follow` | passed `player.pos` — pass the renderable, a `Vector2d` or a `Vector3d` |
|
|
158
|
+
| camera drag jumps around | using world (`gameWorldX`) instead of screen (`gameScreenX`) coordinates |
|
|
159
|
+
| `colorMatrix` does nothing | it is a shader effect — inert on the Canvas renderer |
|
|
160
|
+
| custom drawing appears at the origin | `draw()` not reading `this.pos` |
|
|
161
|
+
| custom drawing offset by half its size | centred `anchorPoint` not zeroed |
|
|
162
|
+
| `setLineWidth` is not a function | it is the `lineWidth` property |
|
|
163
|
+
| clipping in the wrong place inside a container | container clips in local coordinates, after its own translate |
|
|
164
|
+
| `container.clipping = true` does nothing | container has no explicit size, so `width`/`height` are `Infinity` |
|
|
165
|
+
| baked target draws nothing / throws | passed the `CanvasRenderTarget`, not its `.canvas` |
|
|
166
|
+
| frame rate drops with many static draws | bake into a `CanvasRenderTarget` instead |
|
|
167
|
+
|
|
168
|
+
## Related skills
|
|
169
|
+
|
|
170
|
+
- `melonjs-renderables` — the `draw()` contract and anchors
|
|
171
|
+
- `melonjs-3d` — `Camera3d`, which extends `Camera2d` and keeps all of the above
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-deployment
|
|
3
|
+
description: "Use this skill when packaging, shipping or scaffolding a melonJS game — starting a project with create-melonjs, building for the web, wrapping as a native app with Capacitor or Cordova, publishing to WeChat Mini Games or Facebook Instant Games, browser support and transpiling. Triggers on: create-melonjs, scaffold, new project, build, deploy, publish, Capacitor, Cordova, native app, iOS, Android, WeChat, Instant Games, browser support, transpile, ES2022, bundle size, tree-shaking."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Starting and shipping a project
|
|
8
|
+
|
|
9
|
+
## Scaffolding
|
|
10
|
+
|
|
11
|
+
Start from the template rather than assembling a build by hand:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create melonjs@latest my-game
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The project name is required — without it the command prints its usage and
|
|
18
|
+
exits. It downloads the melonJS TypeScript boilerplate (Vite, TypeScript and the
|
|
19
|
+
debug plugin already wired in), which saves you the top-level-await and
|
|
20
|
+
asset-path issues that a hand-rolled config usually hits first.
|
|
21
|
+
|
|
22
|
+
## Adding melonJS to an existing project
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install melonjs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { Application, Sprite, loader } from "melonjs";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package is a tree-shakeable ES module with TypeScript declarations included
|
|
33
|
+
and **no runtime dependencies**.
|
|
34
|
+
|
|
35
|
+
A CDN build exists for quick experiments, but not for production:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<script type="module">
|
|
39
|
+
import { Application } from "https://cdn.jsdelivr.net/npm/melonjs@20/+esm";
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Browser support and transpiling
|
|
44
|
+
|
|
45
|
+
The published bundle targets **ES2022** and uses private class members. A browser
|
|
46
|
+
that predates that **cannot parse the file at all** — it fails at load, not when
|
|
47
|
+
a feature is used, so the symptom is a blank page with a syntax error.
|
|
48
|
+
|
|
49
|
+
To support older browsers, transpile melonJS **together with your own code**.
|
|
50
|
+
The usual trap is that build setups skip `node_modules` by default, leaving
|
|
51
|
+
melonJS untouched however low you set your target:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// vite.config.js — esbuild downlevels dependencies too
|
|
55
|
+
export default { build: { target: "es2020" } };
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// webpack: the default `exclude: /node_modules/` would skip melonjs
|
|
60
|
+
{ test: /\.js$/, exclude: /node_modules\/(?!melonjs)/, use: "babel-loader" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Two caveats worth knowing:
|
|
64
|
+
|
|
65
|
+
- **Transpiling handles syntax only.** Built-in methods need separate polyfills;
|
|
66
|
+
a bundler's `target` setting will never add them.
|
|
67
|
+
- Downlevelled private class members become `WeakMap` lookups, and melonJS uses
|
|
68
|
+
them in the renderer's hot paths. This is a real trade, not a free switch.
|
|
69
|
+
|
|
70
|
+
## Native apps
|
|
71
|
+
|
|
72
|
+
**Capacitor** is the current recommendation for iOS and Android — see
|
|
73
|
+
`@melonjs/capacitor-plugin`. **Cordova** also works and is documented, but is the
|
|
74
|
+
older route.
|
|
75
|
+
|
|
76
|
+
The thing that breaks first in both is asset loading: the app is served from
|
|
77
|
+
`file://`, where `fetch()` cannot read local files. melonJS's loader falls back
|
|
78
|
+
to XHR for that scheme, so engine-loaded assets work — but any `fetch()` in
|
|
79
|
+
*your* game code will fail there.
|
|
80
|
+
|
|
81
|
+
## Mini-game platforms
|
|
82
|
+
|
|
83
|
+
**WeChat Mini Games** and **Facebook Instant Games** are both documented in the
|
|
84
|
+
wiki. Note that `device.platform.isWeixin` detects WeChat's **in-app browser**,
|
|
85
|
+
which is a normal WebView. WeChat *Mini Games* are a different runtime with no
|
|
86
|
+
DOM, and need platform-specific adaptation rather than just detection — the one
|
|
87
|
+
hook the engine offers is that the renderer adopts a `globalThis.canvas` when
|
|
88
|
+
the runtime's adapter provides one, instead of creating its own.
|
|
89
|
+
|
|
90
|
+
## Size
|
|
91
|
+
|
|
92
|
+
The whole engine is roughly **250 KB minzipped**. Tree-shaking helps less than
|
|
93
|
+
you might expect — the engine's subsystems reach each other, so a game importing
|
|
94
|
+
a handful of symbols still pulls most of it. Budget for the full figure.
|
|
95
|
+
|
|
96
|
+
Practical wins are in assets, not the engine: pack sprites into atlases, prefer
|
|
97
|
+
one tileset over several, and use `stream: true` for long music tracks so they
|
|
98
|
+
are not decoded into memory.
|
|
99
|
+
|
|
100
|
+
## Symptom → cause
|
|
101
|
+
|
|
102
|
+
| symptom | cause |
|
|
103
|
+
|---|---|
|
|
104
|
+
| blank page, syntax error in the console | browser older than the ES2022 target |
|
|
105
|
+
| transpiling changes nothing | build config excludes `node_modules` |
|
|
106
|
+
| assets 404 in a packaged native app | `fetch()` in game code — `file://` needs XHR |
|
|
107
|
+
| a built-in method is missing after transpiling | transpiling is syntax-only; polyfills are separate |
|
|
108
|
+
| bundle much larger than expected | tree-shaking recovers little — subsystems are interconnected |
|
|
109
|
+
|
|
110
|
+
## Related skills
|
|
111
|
+
|
|
112
|
+
- `melonjs` — the companion packages, including the Capacitor plugin
|
|
113
|
+
- `melonjs-getting-started` — the Application bootstrap the template produces
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: melonjs-effects-and-shaders
|
|
3
|
+
description: "Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: ShaderEffect, addPostEffect, removePostEffect, getPostEffect, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter."
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Post effects and custom shaders
|
|
8
|
+
|
|
9
|
+
> melonJS ships a catalogue of post effects and a dual-language shader system.
|
|
10
|
+
> The recurring trap is that every shader path needs a GPU backend and
|
|
11
|
+
> **degrades to nothing** on Canvas rather than erroring. Blend modes are the
|
|
12
|
+
> exception — those work on all three renderers.
|
|
13
|
+
|
|
14
|
+
## Applying an effect
|
|
15
|
+
|
|
16
|
+
Effects attach to a renderable **or** to a camera (for full-screen grading):
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { VignetteEffect, GlowEffect } from "melonjs";
|
|
20
|
+
|
|
21
|
+
sprite.addPostEffect(new GlowEffect(app.renderer));
|
|
22
|
+
app.viewport.addPostEffect(new VignetteEffect(app.renderer));
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys
|
|
26
|
+
whatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`.
|
|
27
|
+
|
|
28
|
+
## Toggle with `enabled`, do not remove
|
|
29
|
+
|
|
30
|
+
**`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and
|
|
31
|
+
frees GPU resources, so the instance cannot be re-added. Same for
|
|
32
|
+
`clearPostEffects()` and for reassigning the deprecated `.shader`.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// ✗ destroys it; re-adding later fails or forces a recompile
|
|
36
|
+
sprite.removePostEffect(effect);
|
|
37
|
+
|
|
38
|
+
// ✓
|
|
39
|
+
effect.enabled = false;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
An effect with `effect.shared === true` opts out of auto-destroy because it is
|
|
43
|
+
reused across renderables.
|
|
44
|
+
|
|
45
|
+
## Built-in effects
|
|
46
|
+
|
|
47
|
+
Eighteen presets, every one constructed as `new XEffect(renderer, options)` and
|
|
48
|
+
every one shipping both a GLSL and a WGSL body, so they all run on either GPU
|
|
49
|
+
backend: `VignetteEffect`, `ScanlineEffect` (optional CRT curvature),
|
|
50
|
+
`GlowEffect`, `BlurEffect`, `PixelateEffect`, `ChromaticAberrationEffect`,
|
|
51
|
+
`DropShadowEffect`, `FlashEffect`, `OutlineEffect`, `ColorMatrixEffect`,
|
|
52
|
+
`DesaturateEffect`, `DissolveEffect`, `TintPulseEffect`, `WaveEffect`,
|
|
53
|
+
`InvertEffect`, `SepiaEffect`, `HologramEffect`, `ShineEffect`.
|
|
54
|
+
|
|
55
|
+
For grading without writing a shader yourself, the camera has a colour matrix:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
app.viewport.colorMatrix.contrast(1.1).saturate(1.1);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It is still *implemented* as one: a non-identity matrix makes the camera append
|
|
62
|
+
an internal `ColorMatrixEffect` to its post-effect chain for that frame, so it
|
|
63
|
+
needs a GPU backend like everything else here.
|
|
64
|
+
|
|
65
|
+
## Custom effects are dual-language
|
|
66
|
+
|
|
67
|
+
A custom `ShaderEffect` supplies a fragment **body**, not a whole program. To run
|
|
68
|
+
on both GPU backends it needs GLSL *and* WGSL — a GLSL-only effect silently does
|
|
69
|
+
nothing when `video.AUTO` lands on WebGPU.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const effect = new ShaderEffect(app.renderer, { glsl, wgsl });
|
|
73
|
+
effect.setUniform("uStrength", 0.5);
|
|
74
|
+
effect.setTexture("uScene", tex);
|
|
75
|
+
effect.setTime(timer.getTime() / 1000); // setTime takes SECONDS; getTime is ms
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Uniform names are shared across the two bodies — in WGSL they are the members of
|
|
79
|
+
one `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` call
|
|
80
|
+
feeds whichever backend is live. `setTime` is a convenience for a `uTime`
|
|
81
|
+
uniform and silently does nothing if the shader does not declare one.
|
|
82
|
+
|
|
83
|
+
Shaders can also be preloaded as assets with loader type `"shader"`.
|
|
84
|
+
|
|
85
|
+
### Screen-reading builtins
|
|
86
|
+
|
|
87
|
+
For refraction, distortion and anything that samples what is already drawn:
|
|
88
|
+
|
|
89
|
+
- **`screen_texture`** — a sampler kept filled with everything drawn so far
|
|
90
|
+
(GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)
|
|
91
|
+
- **`screen_uv`** — this fragment's 0..1 position in that capture
|
|
92
|
+
- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame
|
|
93
|
+
|
|
94
|
+
In WGSL the capture is sampled through `screen_sampler` (clamped) or
|
|
95
|
+
`screen_sampler_repeat` (wrapping) instead of GLSL's `: screen_texture`
|
|
96
|
+
annotation. `screen_uv` is y-**up** in GLSL and y-**down** in WGSL, which exactly
|
|
97
|
+
matches each backend's own capture orientation — so a body that samples
|
|
98
|
+
`screen_texture` at `screen_uv` needs no flip in *either* language.
|
|
99
|
+
|
|
100
|
+
Two places where a straight port really does differ:
|
|
101
|
+
|
|
102
|
+
- **A capture you bind yourself.** `renderer.toFrameTexture()` grabs the frame
|
|
103
|
+
into a `Texture2d` you hand to `setTexture` — the supported replacement for
|
|
104
|
+
`readPixels` tricks. The GL capture is bottom-up (GLSL bodies sample it with
|
|
105
|
+
`1.0 - uv.y`) and opaque RGB; the WebGPU capture is top-down (the WGSL twin
|
|
106
|
+
must *not* flip) and preserves alpha.
|
|
107
|
+
- **Vertical UV offsets** — a drop shadow, a directional smear. The WebGL pooled
|
|
108
|
+
multi-effect path composites through bottom-up FBOs, so declare a `uUVYDir`
|
|
109
|
+
uniform (initialise it to `1.0`) and multiply vertical offsets by it; the
|
|
110
|
+
renderer feeds `-1` on that path and `+1` everywhere else.
|
|
111
|
+
|
|
112
|
+
## Blend modes
|
|
113
|
+
|
|
114
|
+
Fourteen modes; thirteen of them are honoured by all three renderers:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
sprite.blendMode = "additive"; // or "multiply", "screen", "overlay", …
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Six ride fixed-function blend state and cost nothing extra: `normal`,
|
|
121
|
+
`additive` (spelled `add` or `lighter` too), `multiply`, `screen`, `exclusion`,
|
|
122
|
+
and `none` (blending off — the source replaces the destination, alpha included).
|
|
123
|
+
`none` is the one mode the Canvas backend does not implement; it reports
|
|
124
|
+
`"normal"` instead.
|
|
125
|
+
|
|
126
|
+
The other eight cannot be expressed as a multiply-add on the destination, so the
|
|
127
|
+
GPU backends capture the destination and composite through `BlendEffect` — one
|
|
128
|
+
capture plus one composite **per draw**: `difference`, `overlay`, `hard-light`,
|
|
129
|
+
`color-dodge`, `color-burn`, `soft-light`, `darken`, `lighten`. Right for
|
|
130
|
+
accents; expensive for hundreds of objects. The Canvas renderer reaches all
|
|
131
|
+
eight through `globalCompositeOperation` instead, at no extra cost.
|
|
132
|
+
|
|
133
|
+
Anything else collapses to `"normal"`, and so do 3D meshes and `Gradient` fills
|
|
134
|
+
whatever you ask for — the latter with a one-time console warning.
|
|
135
|
+
`renderer.setBlendMode(mode)` returns the mode it actually applied, so comparing
|
|
136
|
+
that against your request detects the fallback.
|
|
137
|
+
|
|
138
|
+
## The Canvas fallback
|
|
139
|
+
|
|
140
|
+
`ShaderEffect` on a Canvas renderer logs a warning, leaves `enabled = false`, and
|
|
141
|
+
turns every method into a no-op. Nothing throws. The same happens on a GPU
|
|
142
|
+
backend when no body matches its language — a GLSL-only effect under WebGPU.
|
|
143
|
+
|
|
144
|
+
That is fine for decoration. It is **not** fine when the shader draws your
|
|
145
|
+
content — the carrier renderable then shows through raw. A 1×1 white texture
|
|
146
|
+
stretched to 280×156 renders as a white box where the effect should be.
|
|
147
|
+
|
|
148
|
+
Guard when the effect is load-bearing:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
if (app.renderer.shaderLanguage === null) {
|
|
152
|
+
// no programmable pipeline — skip the carrier draw entirely
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`shaderLanguage` (`"glsl"`, `"wgsl"`, or `null`) is the flag to test here rather
|
|
157
|
+
than a backend name or `supportsDepthBuffer`: it answers "can this renderer
|
|
158
|
+
compile what I am about to hand it". Per-effect, `effect.enabled` answers the
|
|
159
|
+
same question after construction.
|
|
160
|
+
|
|
161
|
+
## Symptom → cause
|
|
162
|
+
|
|
163
|
+
| symptom | cause |
|
|
164
|
+
|---|---|
|
|
165
|
+
| effect does nothing, warning in console | Canvas fallback — no programmable pipeline |
|
|
166
|
+
| effect does nothing on some machines only | GLSL-only shader, `video.AUTO` chose WebGPU |
|
|
167
|
+
| a white or solid box where the effect should be | carrier renderable drawn while the effect is disabled |
|
|
168
|
+
| effect cannot be re-enabled | `removePostEffect()` destroyed it — use `enabled` |
|
|
169
|
+
| ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |
|
|
170
|
+
| shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |
|
|
171
|
+
| frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |
|
|
172
|
+
| animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |
|
|
173
|
+
|
|
174
|
+
## Related skills
|
|
175
|
+
|
|
176
|
+
- `melonjs-renderables` — where post effects attach, and the destroy trap
|
|
177
|
+
- `melonjs-3d` — custom mesh shaders and the GPU-backend requirement
|