Download Latest Version v20.10.0 source code.zip (57.0 MB) Google Add to Preferred Sources
Home / 20.9.0
Name Modified Size InfoDownloads / 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

  • scaleRounding application 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 to scaleMethod: "fit" only
  • Sprite-sheet particles (framewidth, frameheight, frames, randomFrame on ParticleEmitter): 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, where lerp slows 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 returning NaN
  • Trail3d: a fading, tapering ribbon that recedes properly into a 3D scene, for an exhaust streak, a speed trail or a spell arc. It is a Mesh, so unlike the 2D Trail it narrows along its own length, takes a post effect and a tint, and costs one indexed draw call rather than a filled path per segment. It needs a Camera3d (#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 one toBlob(), toDataURL() and toImageBitmap() are all built on
  • Renderable#setTint(color): tint any renderable from one chainable entry point, and tint itself now accepts a CSS string. Setting it meant choosing between setColor, setFloat, parseCSS, setHSL and setLinear, and the wrong choice is silent: it renders the wrong colour
  • Mesh takes a baseColorFactor setting: 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 wrote tint.setLinear(...) themselves, and a linear value pushed through setColor renders untextured midtones 60 to 70 counts too dark
  • Mesh#shadowGroundPoint: the point on the floor a ground shadow lands on, for ground that is not flat. shadowGroundY can 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 ceiling
  • Texture2d#isAtlas: whether a texture carries named regions addressable with getRegion(), 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 in pos, and audio.unfollow(id) detaches a sound and leaves it playing
  • A movable listener: audio.setListener(target) puts the ear on a renderable, while audio.listener() and audio.listenerOrientation() place it by hand and read it back. A Camera3d target 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" and panningModel: "equalpower", so it carries across a screen instead of going near silent a few tiles out. audio.setSpatialDefaults() and getSpatialDefaults() change what new voices inherit
  • getPool("text") and getPool("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, and hasGamepadVibration(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 one
  • UIPanel: a rectangular surface with a background, an optional border and children, where blocking decides whether a click that misses every child is swallowed. bobAmplitude and bobPeriod give it a slow float, and the offset goes into getAbsolutePosition(), so the panel and every widget in it stay clickable where they are drawn (#1722)
  • createPool is exported, with the Pool and CreatePoolOptions types 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 off pool.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 mapping other 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, and input.setGamepadMapping(id, mapping) still takes a mapping for anything else

Deprecated

  • pool, the legacy name-keyed object pool. register(), pull() and push() each print a notice once, naming both replacements: the constructor, or getPool() for a typed pool

Fixed

  • Maths: Vector3d#projectN and ObservableVector3d#projectN were copies of project, dividing by the target's length2(), which is the one thing projectN exists 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's reset and release callbacks was strong and nothing removed an entry, not even release(). Measured at 600,000 dropped clones: 150MB retained before, nothing after
  • Input: the gamepad axis threshold is 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 of 0.5 does 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() and noise() 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 onend callback 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-play onend
  • UI: a ProgressBar border was drawn centred on the bar's own edge, so half of it fell outside the declared width and height and getBounds() 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 ProgressBar twice threw Instance 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.bindKey activates the button from an input.KEY keycode or an action name. Held keys click once and blur clears the press without touching global bindings. The handlers fire with NO pointer, so an onClick(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 unmuteOnFocus already followed
  • Camera / state: a camera effect's onComplete ran 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 every onResetEvent ran twice
  • Rendering: toBlob(), toDataURL() and toImageBitmap() threw on a WebGPU render target. RenderTarget required every backend to implement a SYNCHRONOUS getImageData(), which WebGPU cannot, since reading a texture there means mapping a buffer. The contract is a promise now
  • Rendering: the RenderTarget contract no longer requires synchronous readback. getImageData() stays on the canvas and WebGL targets but leaves the shared interface, and WebGPURenderTarget#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 alpha 0 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, and ColorLayer does 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, strokeStyle and floating. Those three were written only when the setting was supplied, which a freshly constructed label never shows, and getPool("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#collisionMask was stripped from the published types while the PhysicsBody interface it implements requires it, and BitmapText#resize() was typed as returning the base class
  • Audio: stereo() and position() 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 first play() 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 throw TypeError: The provided value is non-finite and 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 put undefined into loopEnd. 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 off getMasterGain(), gated by the existing pauseOnBlur and stopOnBlur
  • Skills: the UI skill listed NineSliceSprite panels among what it covers but never mentioned them. It now shows both ways to make one and the three things that trip people up: width and height are mandatory, insetx / insety default 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 / at play options, the pixel-shaped panner defaults, and that all of them take world coordinates with y measured down. It also covers mix effects through getMasterGain()
  • updateWhenPaused on 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 unaffected
  • input.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.LX and GAMEPAD.BUTTONS.FACE_1 both being 0
  • audio.playTrack() stops the track it replaces. It only ever assigned the current-track slot and played, so the previous track kept looping underneath and stopTrack(), pauseTrack() and resumeTrack() 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 stopped
  • pool.push(obj) reports a refusal by returning false rather than throwing. The throw aborted whatever was running, which in both of the engine's own uses removed in 20.7.0 was a destroy() that had already recycled other state. Pass pool.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, or getPool("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-in Text or ColorLayer is destroyed where it used to be pooled: pass keepalive to removeChild() 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

Source: README.md, updated 2026-10-18