| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | 2026-10-18 | 17.0 kB | |
| v20.9.0 source code.tar.gz | 2026-10-18 | 55.9 MB | |
| v20.9.0 source code.zip | 2026-10-18 | 56.9 MB | |
| Totals: 3 Items | 112.8 MB | 2 | |
Added
scaleRoundingapplication setting: optional, and"fractional"by default, so nothing existing changes."integer"constrains the automatic scale factor to a whole number of device pixels per source pixel and letterboxes the remainder, which is what stops pixel art showing uneven stems and shimmering edges; it applies toscaleMethod: "fit"only- Sprite-sheet particles (
framewidth,frameheight,frames,randomFrameonParticleEmitter): a particle plays a frame sequence once over its own lifetime, or holds one frame picked at random when it is born. One sheet is one image, so a burst of varied debris still batches into a single draw call (#1610, thanks @snowyukitty) slerp(v, alpha)on all four vector classes: interpolate along the ARC between two directions at a constant angular rate, wherelerpslows at the ends and shortens the vector through the middle. 2D takes the shorter way round, and the same and opposite cases are handled rather than returningNaNTrail3d: a fading, tapering ribbon that recedes properly into a 3D scene, for an exhaust streak, a speed trail or a spell arc. It is aMesh, so unlike the 2DTrailit narrows along its own length, takes a post effect and atint, and costs one indexed draw call rather than a filled path per segment. It needs aCamera3d(#1621)BitmapText#glyphEffect: offset and tint individual glyphs for wave, shake and colour effects, inside the existing GPU batch. Layout and the typewriter reveal are unchanged (#1522, thanks @snowyukitty)RenderTarget#toImageData(): read back a render target's pixels as a promise. The portable readback, and the onetoBlob(),toDataURL()andtoImageBitmap()are all built onRenderable#setTint(color): tint any renderable from one chainable entry point, andtintitself now accepts a CSS string. Setting it meant choosing betweensetColor,setFloat,parseCSS,setHSLandsetLinear, and the wrong choice is silent: it renders the wrong colourMeshtakes abaseColorFactorsetting: a material's base colour as the LINEAR triple glTF states it in, encoded on the way in. It was the only material value with no setting, so the glTF loaders and every game building a mesh by hand wrotetint.setLinear(...)themselves, and a linear value pushed throughsetColorrenders untextured midtones 60 to 70 counts too darkMesh#shadowGroundPoint: the point on the floor a ground shadow lands on, for ground that is not flat.shadowGroundYcan only put the blob under the caster's own column, which is wrong anywhere down is not world down, such as on a sphere. It supersedes it and lifts the 75 degree tilt ceilingTexture2d#isAtlas: whether a texture carries named regions addressable withgetRegion(), so a game holding a texture of unknown kind can ask without a type test- Spatial audio placed in world coordinates:
audio.play(name, { follow: renderable })tracks a sound to a renderable every frame,{ at: { x, y } }pins one to a world point, and{ stopWithTarget: true }ends it when that renderable is destroyed. The numbers are the world pixels already inpos, andaudio.unfollow(id)detaches a sound and leaves it playing - A movable listener:
audio.setListener(target)puts the ear on a renderable, whileaudio.listener()andaudio.listenerOrientation()place it by hand and read it back. ACamera3dtarget contributes orientation too. Opt-in: no frame handler runs until a listener is asked for - Panner defaults shaped for pixels: a placed sound starts from
refDistance: 240,maxDistance: 10000,distanceModel: "inverse"andpanningModel: "equalpower", so it carries across a screen instead of going near silent a few tiles out.audio.setSpatialDefaults()andgetSpatialDefaults()change what new voices inherit getPool("text")andgetPool("colorLayer"): typed pools for the two renderables whose only recycling route was the legacy name-keyed pool.getPool("text").get(x, y, settings)hands back a label with its measurement cache already allocated- Gamepad vibration:
input.vibrateGamepad(index, options)plays a rumble effect,stopGamepadVibration(index)cuts a long one short, andhasGamepadVibration(index)reports whether a pad has motors at all. Safe to call blind, magnitudes default to FULL rather than the web API's zero, and a second call preempts the first on the same pad input.activeGamepad(): the index of the pad the player is actually holding, which the browser does not report. Slots are handed out in plug order, so index 0 is a guess: a wheel or a spare pad can hold it all session. This follows the last pad to produce a BOUND input, so a worn stick at rest cannot claim to be the active oneUIPanel: a rectangular surface with a background, an optional border and children, whereblockingdecides whether a click that misses every child is swallowed.bobAmplitudeandbobPeriodgive it a slow float, and the offset goes intogetAbsolutePosition(), so the panel and every widget in it stay clickable where they are drawn (#1722)createPoolis exported, with thePoolandCreatePoolOptionstypes it names, so a pool for one of your own classes needs no deep import and a field holding one can be declared.getPool()only reaches the pools the engine ships, so this is what a game moving offpool.register(name, Class, true)needs
Removed
- The built-in gamepad remappings for the OUYA controller and for the wired Xbox 360 controller under Firefox, along with the two axis normalizers they needed. A remapping is consulted only when the browser reports a
mappingother than"standard", which is the case these two entries were written for and not one a current browser puts a common pad in; the OUYA itself was discontinued in 2015 and its services closed in 2019. The PlayStation 3 and PlayStation 4 entries are unchanged, andinput.setGamepadMapping(id, mapping)still takes a mapping for anything else
Deprecated
pool, the legacy name-keyed object pool.register(),pull()andpush()each print a notice once, naming both replacements: the constructor, orgetPool()for a typed pool
Fixed
- Maths:
Vector3d#projectNandObservableVector3d#projectNwere copies ofproject, dividing by the target'slength2(), which is the one thingprojectNexists to skip. Unchanged for a target that really is unit and cheaper, and it now scales by the raw dot product for one that is not, as the 2D classes always have. All four vector classes are tested - Pooling: a pooled instance that was dropped rather than released could never be collected, so
clone()leaked. The registry holding each instance'sresetandreleasecallbacks was strong and nothing removed an entry, not evenrelease(). Measured at 600,000 dropped clones: 150MB retained before, nothing after - Input: the gamepad axis
thresholdis documented as what it is, a value ADDED to the deadzone rather than used instead of it. Under the default deadzone of 0.1 the documented figure of0.5does not fire until 0.6 of full travel. The behaviour is unchanged - Audio: a page left idle for thirty seconds before its first click could lose every loaded clip for the life of the page while
tone()andnoise()still played. WebKit never confirms a suspend asked for outside a user gesture, so the engine waited for a resume that could not arrive. A resume is now issued at once, and a gesture or the tab returning revives playback - Audio: a sound's
onendcallback could fire for a DIFFERENT sound. Stopping a voice cleared only the first callback bound to it, and playback ids are handed to the next voice to take the slot, so a leftover outlived its own sound. Most visible on a clip played many times with a per-playonend - UI: a
ProgressBarborder was drawn centred on the bar's own edge, so half of it fell outside the declared width and height andgetBounds()did not account for it. A square border is now four fills tiling the frame exactly and a rounded one is stroked on an inset path, which also fixes a 1px square border coming up short at the corners - UI: destroying a
ProgressBartwice threwInstance is already in pool.instead of doing nothing. It handed its cached colour back without clearing the handle, and a pool refuses a double release - UI:
UITextButton.bindKeyactivates the button from aninput.KEYkeycode or an action name. Held keys click once and blur clears the press without touching global bindings. The handlers fire with NO pointer, so anonClick(event)written for one throws on the first shortcut (#1722, thanks @snowyukitty) - Application: coming back to the tab resumed a game that had paused ITSELF, so switching away from an open pause menu and back left the run playing underneath it. Only a pause this application applied on blur is reversed on focus, which is the rule
unmuteOnFocusalready followed - Camera / state: a camera effect's
onCompleteran on every frame after the effect finished rather than once. A state transition defers the switch in that callback, so every frame queued another switch into the state being entered: the scene was built twice and everyonResetEventran twice - Rendering:
toBlob(),toDataURL()andtoImageBitmap()threw on a WebGPU render target.RenderTargetrequired every backend to implement a SYNCHRONOUSgetImageData(), which WebGPU cannot, since reading a texture there means mapping a buffer. The contract is a promise now - Rendering: the
RenderTargetcontract no longer requires synchronous readback.getImageData()stays on the canvas and WebGL targets but leaves the shared interface, andWebGPURenderTarget#readPixels()is removed - Tiled:
registerTiledObjectClass("Trigger", MyTrigger)called before the first map load was silently discarded, because the built-in classes were flushed when the first object was built and overwrote it. They are applied as defaults now, so a name a game has taken stays taken, and a built-in name can be taken over at any point - Text: a recycled label inherited everything a game drives through the base class, so one tweened to
alpha0 came back invisible and one scaled up came back large. The alpha, tint, blend mode, transform, anchor, flip and behaviour flags are restored before the new settings are applied, andColorLayerdoes the same - Text:
getPool("text").get(x, y, settings)ignored the position it was given, so a recycled label came back sitting wherever the previous one was left. The depth resets with it - Text: a recycled label built a new canvas and a new metrics cache every time, stranding the previous canvas and its GPU texture, since only
destroy()frees those. They are kept across a recycle now, which is most of what makes a pooled label worth pooling - Text: a recycled label inherited the previous label's
fillStyle,strokeStyleandfloating. Those three were written only when the setting was supplied, which a freshly constructed label never shows, andgetPool("text")makes the case reachable - Typings: another twelve public members reached TypeScript as
any, because the generated declarations named types they never imported, among them the mesh shadow light and the quadtree and octree containers.Body#collisionMaskwas stripped from the published types while thePhysicsBodyinterface it implements requires it, andBitmapText#resize()was typed as returning the base class - Audio:
stereo()andposition()drive the same per-instance panner node, and whichever was called first decided what kind of node it was, making the other a silent no-op for the life of that voice. The node now follows whichever was called last - Audio:
audio.panner(name, attributes)with no playback id wrote only the voices already playing and never the clip's own defaults, so a call before the firstplay()changed nothing and the getter read back construction-time values forever. It writes the group now, and merges rather than replaces - Camera / state: a
state.transition()flashed the outgoing scene for a frame once the fade or the iris had fully closed. The hide half is marked persistent so it keeps covering the screen across the switch, but the per-frame sweep destroyed any completed effect whether persistent or not. A completed persistent effect is now left for its owner to remove - Audio:
audio.play(name, { loop: true })could throwTypeError: The provided value is non-finiteand take the caller's frame with it. A queued play, from a clip still loading or a suspended context, hands the voice back before it has a loop window, and writing one then putundefinedintoloopEnd. The play itself sets it now - Audio: sound effects kept playing while the window did not have focus, because pausing the game only ever reached the current music track, so a looping engine hum or alarm played on over whatever the player had switched to. The mix is muted on blur and restored on focus, covering
tone(),noise()and anything hung offgetMasterGain(), gated by the existingpauseOnBlurandstopOnBlur - Skills: the UI skill listed
NineSliceSpritepanels among what it covers but never mentioned them. It now shows both ways to make one and the three things that trip people up:widthandheightare mandatory,insetx/insetydefault to a quarter of the image rather than its border, and the anchor is the centre - Skills: the audio skill now covers positional sound: the movable listener, the
follow/atplay options, the pixel-shaped panner defaults, and that all of them take world coordinates with y measured down. It also covers mix effects throughgetMasterGain() updateWhenPausedon a container now covers everything under it, so a pause menu flags itself once and its card, its labels and its entries follow. Before, every one of them had to be flagged by hand and a single missed label silently stopped reading input. A container that does not set it is unaffectedinput.unbindGamepad(index, button, type)takes the table the binding is in,"buttons"by default, and removes the binding outright rather than blanking it. An axis could not be unbound at all before: the call only ever reached the button table, and the codes collide,GAMEPAD.AXES.LXandGAMEPAD.BUTTONS.FACE_1both being 0audio.playTrack()stops the track it replaces. It only ever assigned the current-track slot and played, so the previous track kept looping underneath andstopTrack(),pauseTrack()andresumeTrack()could no longer reach it. Calling it with that track already playing now restarts it rather than stacking a second copy, and an unknown clip name throws before anything is stoppedpool.push(obj)reports a refusal by returningfalserather than throwing. The throw aborted whatever was running, which in both of the engine's own uses removed in 20.7.0 was adestroy()that had already recycled other state. Passpool.push(obj, true)where a missed registration is a bug you want to hear about (#1688)- Tiled tile images, atlas sprites and Tiled image layers are built from their classes directly rather than looked up by name, since
pool.pull("me.Sprite")never pooled anything. Re-registering one of those BUILT-IN names to substitute your own class no longer changes what they produce; your own names are unaffected (#1688) - The engine no longer registers its own classes in the legacy object pool, so
pool.pull("Sprite"),pool.pull("Text")and the other built-in names throw instead of returning an instance. Call the constructor, orgetPool("tween"),getPool("particle"),getPool("text"),getPool("colorLayer")for the recycling (#1688) - A child removed from a container goes back to the typed pool that built it, and is destroyed when no pool did.
pool.register(name, Class, true)no longer makes a class recyclable on removal, so a built-inTextorColorLayeris destroyed where it used to be pooled: passkeepalivetoremoveChild()to keep it (#1688)
Contributors
Thank you to the people outside the team who shipped code in this release.
@snowyukitty
- fix(ui): honor UITextButton keyboard bindings (#1732) (47f801d74)
- Add lifetime-animated and random sprite-sheet particles (#1728) (ff99baade)
- Add batched per-glyph effects to BitmapText (#1725) (cc610bbb3)
Full Changelog: https://github.com/melonjs/melonJS/compare/20.8.0...20.9.0