├── .gitignore ├── FAQ.md ├── LICENSE ├── README.md ├── dist ├── littlejs.d.ts ├── littlejs.esm.js ├── littlejs.esm.min.js ├── littlejs.js ├── littlejs.min.js └── littlejs.release.js ├── docs ├── Audio.html ├── Color.html ├── Debug.html ├── Draw.html ├── Engine.html ├── EngineObject.html ├── FontImage.html ├── Input.html ├── Medal.html ├── Medals.html ├── Music.html ├── Newgrounds.html ├── Particle.html ├── ParticleEmitter.html ├── Random.html ├── RandomGenerator.html ├── Settings.html ├── Sound.html ├── SoundWave.html ├── TextureInfo.html ├── TileCollision.html ├── TileInfo.html ├── TileLayer.html ├── TileLayerData.html ├── Timer.html ├── Utilities.html ├── Vector2.html ├── WebGL.html ├── data │ └── search.json ├── engine.js.html ├── engineAudio.js.html ├── engineDebug.js.html ├── engineDraw.js.html ├── engineInput.js.html ├── engineMedals.js.html ├── engineObject.js.html ├── engineParticles.js.html ├── engineSettings.js.html ├── engineTileLayer.js.html ├── engineUtilities.js.html ├── engineWebGL.js.html ├── examples │ ├── favicon.png │ ├── logo.png │ └── screenshot.jpg ├── fonts │ ├── Inconsolata-Regular.ttf │ ├── OpenSans-Regular.ttf │ └── WorkSans-Bold.ttf ├── index.html ├── scripts │ ├── core.js │ ├── core.min.js │ ├── resize.js │ ├── search.js │ ├── search.min.js │ └── third-party │ │ ├── Apache-License-2.0.txt │ │ ├── fuse.js │ │ ├── hljs-line-num-original.js │ │ ├── hljs-line-num.js │ │ ├── hljs-original.js │ │ ├── hljs.js │ │ ├── popper.js │ │ ├── tippy.js │ │ ├── tocbot.js │ │ └── tocbot.min.js ├── static │ └── favicon.png └── styles │ ├── clean-jsdoc-theme-base.css │ ├── clean-jsdoc-theme-dark.css │ ├── clean-jsdoc-theme-light.css │ ├── clean-jsdoc-theme-scrollbar.css │ ├── clean-jsdoc-theme-without-scrollbar.min.css │ └── clean-jsdoc-theme.min.css ├── examples ├── box2d │ ├── game.js │ ├── gameObjects.js │ ├── index.html │ ├── scenes.js │ └── tiles.png ├── breakout │ ├── game.js │ ├── gameObjects.js │ ├── index.html │ └── tiles.png ├── breakoutTutorial │ ├── README.md │ ├── game.js │ ├── images │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ └── index.html ├── empty │ ├── game.js │ ├── index.html │ └── tiles.png ├── favicon.png ├── htmlMenu │ ├── game.js │ ├── index.html │ └── tiles.png ├── index.html ├── logo.png ├── module │ ├── game.js │ ├── index.html │ └── tiles.png ├── particles │ ├── index.html │ └── tiles.png ├── platformer │ ├── data │ │ ├── gameLevelData.tmx │ │ └── gameLevelData.tsx │ ├── game.js │ ├── gameCharacter.js │ ├── gameEffects.js │ ├── gameLevel.js │ ├── gameLevelData.js │ ├── gameObjects.js │ ├── gamePlayer.js │ ├── index.html │ ├── tiles.png │ └── tilesLevel.png ├── puzzle │ ├── game.js │ ├── index.html │ └── tiles.png ├── screenshot.jpg ├── shorts │ ├── animation.js │ ├── base.html │ ├── blending.js │ ├── clock.js │ ├── colors.js │ ├── helloWorld.js │ ├── particles.js │ ├── platformer.js │ ├── playSound.js │ ├── pong.js │ ├── shapes.js │ ├── spriteAtlas.js │ ├── systemFont.js │ ├── texture.js │ ├── tileLayer.js │ ├── tiles.png │ ├── tiltedView.js │ ├── timers.js │ └── topDown.js ├── starter │ ├── build.bat │ ├── build.js │ ├── game.js │ ├── index.html │ └── tiles.png ├── stress │ └── index.html └── uiSystem │ ├── game.js │ ├── index.html │ └── tiles.png ├── jsconfig.json ├── package-lock.json ├── package.json ├── plugins ├── Box2D_License.txt ├── Box2D_v2.3.1_min.wasm.js ├── Box2D_v2.3.1_min.wasm.wasm ├── box2d.js ├── newgrounds.js ├── postProcess.js └── uiSystem.js ├── reference.md └── src ├── engine.js ├── engineAudio.js ├── engineBuild.bat ├── engineBuild.js ├── engineDebug.js ├── engineDraw.js ├── engineExport.js ├── engineFont.png ├── engineInput.js ├── engineMedals.js ├── engineObject.js ├── engineParticles.js ├── engineRelease.js ├── engineSettings.js ├── engineTileLayer.js ├── engineUtilities.js ├── engineWebGL.js └── jsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # node files 2 | node_modules 3 | 4 | # example builds 5 | examples/starter/build 6 | examples/starter/*.zip 7 | examples/js13k/build 8 | examples/js13k/*.zip 9 | examples/typescript/build 10 | examples/electron/build 11 | examples/electron/LittleJSGame-win32-x64 12 | examples/electron/package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Frank Force 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ------------------------------------------------------------------------------- 24 | 25 | This project uses the Box2D physics engine, which is licensed under the zlib license. 26 | Copyright (c) 2006 Erin Catto (Box2D) 27 | Copyright (c) 2011 The box2d.js contributors 28 | 29 | This software is provided 'as-is', without any express or implied 30 | warranty. In no event will the authors be held liable for any damages 31 | arising from the use of this software. 32 | 33 | Permission is granted to anyone to use this software for any purpose, 34 | including commercial applications, and to alter it and redistribute it 35 | freely, subject to the following restrictions: 36 | 37 | 1. The origin of this software must not be misrepresented; you must not 38 | claim that you wrote the original software. If you use this software 39 | in a product, an acknowledgment in the product documentation would be 40 | appreciated but is not required. 41 | 2. Altered source versions must be plainly marked as such, and must not be 42 | misrepresented as being the original software. 43 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LittleJS - The Tiny Fast JavaScript Game Engine 2 | 3 |
4 | 5 | ![LittleJS Screenshot](examples/logo.png) 6 | 7 | [![NPM Package][npm]][npm-url] 8 | [![Build Size][build-size]][build-size-url] 9 | [![NPM Downloads][npm-downloads]][npmtrends-url] 10 | [![DeepScan][deepscan]][deepscan-url] 11 | [![Discord][discord]][discord-url] 12 | 13 |
14 | 15 | ## 🚂 All aboard! 16 | 17 | LittleJS is a fast, lightweight, and fully open source HTML5 game engine designed for simplicity and performance. 18 | Its small footprint is packed with a comprehensive feature set including hybrid rendering, physics, particles, sound, and input handling. 19 | The code is clean and well documented with some fun examples to get you started right away. Choo-Choo! 20 | 21 | ### 🚀 Join the LittleJS Game Jam 22 | 23 | *The Second Annual LittleJS Game Jam will take place From Oct 3 to Nov 3! Unleash your creativity and develop amazing games using the LittleJS game engine. 🕹️🎮 [Sign up today and get more info about the jam on itch.io!](https://itch.io/jam/littlejs-game-jam-2025)* 24 | 25 |
26 | 27 | ## [Demo](https://killedbyapixel.github.io/LittleJS/examples/starter/) | [Docs](https://killedbyapixel.github.io/LittleJS/docs) | [Trailer](https://youtu.be/chuBzGjv7Ms) | [Discord](https://discord.gg/zb7hcGkyZe) | [Tutorial](https://github.com/KilledByAPixel/LittleJS/blob/main/examples/breakoutTutorial/README.md) | [FAQ](https://github.com/KilledByAPixel/LittleJS/blob/main/FAQ.md) 28 | 29 |
30 | 31 | ![LittleJS Screenshot](examples/screenshot.jpg) 32 | 33 | ## About LittleJS Engine 34 | 35 | LittleJS is a small but powerful game engine with many features and no dependencies. 36 | 37 | ### ✨ Graphics 38 | 39 | - Super fast sprite and tile map rendering engine with WebGL2 40 | - Update and render 100,000+ sprites at a solid 60fps 41 | - Apply [Shadertoy](https://www.shadertoy.com) compatible shaders for post-processing effects 42 | - Robust particle effect system and [effect design tool](https://killedbyapixel.github.io/LittleJS/examples/particles/) 43 | 44 | ### 🔊 Audio 45 | 46 | - Positional sound effects with wave files, mp3s, or ZzFX 47 | - Use [ZzFX](https://killedbyapixel.github.io/ZzFX/) sound effect generator to play sounds without asset files 48 | - Music with mp3, ogg, wave, or [ZzFXM](https://keithclark.github.io/ZzFXM/) 49 | 50 | ### 🎮 Input 51 | 52 | - Comprehensive input handling for keyboard, mouse, gamepad, and touch 53 | - On screen touch gamepad designed for mobile devices 54 | 55 | ### 💥 Physics 56 | 57 | - Robust arcade physics system with collision handling 58 | - Very fast collision and raycasting for tile maps 59 | - Full Box2d support using [super fast wasm build of box2d.js](https://github.com/kripken/box2d.js/) 60 | 61 | ### 🚀 Flexibility 62 | 63 | - Compatible with all modern web browsers and on mobile devices 64 | - Support for TypeScript and Modules with example projects for both 65 | - Ideal for size coding competitions like [js13kGames](https://js13kgames.com/), [starter project builds to a 7KB zip](https://github.com/KilledByAPixel/LittleJS/tree/js13k) 66 | - Open Source with the [MIT license](https://github.com/KilledByAPixel/LittleJS/blob/main/LICENSE) so it can be used for anything you want forever 67 | 68 | ### 🛠️ And more... 69 | 70 | - Node.js build system 71 | - 2D vector math library 72 | - Debug primitive rendering system 73 | - Bitmap font rendering and built in engine font 74 | - Medal tracking system with [Newgrounds](https://www.newgrounds.com/) support 75 | 76 | ## How To Use LittleJS 77 | 78 | To get started download the latest LittleJS package from GitHub or install via npm: ```npm install littlejsengine``` 79 | 80 | *You need to run a local web server to run LittleJS games during development!* You may see a console error like "The image element contains cross-origin data." Don't panic, it's easy to fix! If you are using [Visual Studio Code](https://code.visualstudio.com/) there is a [Live Preview Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server) that will handle this for you automatically. Another option is to setup a simple local web server like [http-server](https://www.npmjs.com/package/http-server) via npm. 81 | 82 | 83 | - [Watch this GitNation talk](https://youtu.be/_dXKU0WgAj8?si=ZDXLYAFDWp54hrGT) to hear more about LittleJS works and get some tips on how to use it. 84 | - Learn how to make a simple game from scratch with [The Breakout Tutorial.](https://github.com/KilledByAPixel/LittleJS/tree/main/examples/breakoutTutorial) 85 | - [Make a ski game with LittleJS](https://eoinmcgrath.com/little-ski/tutorial.html) - Check out this tutorial by eoinmcg that shows how to make a pixel art style game. 86 | - [LittleJS Engine Quick Reference Sheet](https://github.com/KilledByAPixel/LittleJS/blob/main/reference.md) - This cheat sheet can help you get started. 87 | - [Check out The Little JS FAQ for more help getting started](https://github.com/KilledByAPixel/LittleJS/blob/main/FAQ.md). 88 | - Join our vibrant community on [Discord](https://discord.gg/zb7hcGkyZe) to get help, share your projects, and collaborate with others! 89 | 90 | ## Examples 91 | 92 | These demos are for both learning and using as starter projects to create your own games. 93 | 94 | - [Starter Project](https://killedbyapixel.github.io/LittleJS/examples/starter/) - Clean example with only a few things to get you started 95 | - [Breakout](https://killedbyapixel.github.io/LittleJS/examples/breakout/) - Block breaking game with post-processing effects 96 | - [Puzzle Game](https://killedbyapixel.github.io/LittleJS/examples/puzzle/) - Match 3 puzzle game with HD rendering and high score tracking 97 | - [Platformer](https://killedbyapixel.github.io/LittleJS/examples/platformer/) - Platformer/shooter with level data from [Tiled Editor](https://github.com/mapeditor/tiled) 98 | - [Box2D Demo](https://killedbyapixel.github.io/LittleJS/examples/box2d/) - Box2D plugin demonstration and testbed 99 | - [Stress Test](https://killedbyapixel.github.io/LittleJS/examples/stress/) - Max sprite/object test and music system demo 100 | - [Particle System Designer](https://killedbyapixel.github.io/LittleJS/examples/particles/) - Particle system editor and visualizer 101 | - [Example Browser](https://killedbyapixel.github.io/LittleJS/examples/) - Live example browser with all examples 102 | 103 | ## Builds 104 | 105 | To easily include LittleJS in your game, you can use one of the pre-built js files. 106 | 107 | - [littlejs.js](https://github.com/KilledByAPixel/LittleJS/blob/main/dist/littlejs.js) - The full game engine with debug mode available 108 | - [littlejs.release.js](https://github.com/KilledByAPixel/LittleJS/blob/main/dist/littlejs.release.js) - The engine optimized for release builds 109 | - [littlejs.min.js](https://github.com/KilledByAPixel/LittleJS/blob/main/dist/littlejs.min.js) - The engine in release mode and minified 110 | - [littlejs.esm.js](https://github.com/KilledByAPixel/LittleJS/blob/main/dist/littlejs.esm.js) - The engine exported as a module with debug mode available 111 | - [littlejs.esm.min.js](https://github.com/KilledByAPixel/LittleJS/blob/main/dist/littlejs.esm.min.js) - The engine exported as a minified module in release mode 112 | 113 | To rebuild the engine you must first run ```npm install``` to setup the necessary npm dependencies. Then call ```npm run build``` to build the engine. 114 | 115 | The starter example project includes a node js file [build.js](https://github.com/KilledByAPixel/LittleJS/blob/main/examples/starter/build.js) that compresses everything into a tiny zip file using Google Closure, UglifyJS, and ECT Zip. 116 | 117 | ## Games Made With LittleJS 118 | 119 | Here are a few of the many amazing games created with LittleJS... 120 | 121 | - [Space Huggers](https://www.newgrounds.com/portal/view/819609) - Rogulike platformer shoot-em-up game with procedural levels. by [KilledByAPixel](https://frankforce.com/) 122 | - [Undergrowth](https://undergrowth.squidband.uk/) - An interactive music videogame for the band Squid. by [KilledByAPixel](https://frankforce.com/) 123 | - [The Way of the Dodo](https://js13kgames.com/2024/games/the-way-of-the-dodo) - Single button platformer. JS13k 5th place winner! by [repsej](https://github.com/repsej) 124 | - [204Snake!](https://www.newgrounds.com/portal/view/960100) - A puzzle game that combines 2048 with snake. LittleJS Jam 1st place winner! by [Sodoj](https://sodoj.itch.io/) and [Shai-P](https://shai-p.itch.io/) 125 | - [GATOR](https://www.newgrounds.com/portal/view/960757) - Retro platformer shooter game where you rescue animals. LittleJS Jam 2nd place winner! by [eoinmcg](https://eoinmcg.itch.io/) 126 | - [A Hedgehog's search](https://willsm1111.itch.io/a-hedgehogs-search) - Adventure game staring a hedgehog. LittleJS Jam 3rd place winner! by [willsm1111](https://willsm1111.itch.io/) 127 | - [Wendol Village](https://js13kgames.com/2024/games/wendol-village) - Warcraft inspired RTS game. by [sanojian](https://github.com/sanojian) 128 | - [Dead Again](https://js13kgames.com/entries/dead-again) - Top down survial horror. by [sanojian & repsej](https://github.com/sanojian/js13k_2022) 129 | - [Isletopia](https://store.steampowered.com/app/1861260/Isletopia) - Relaxing strategy game of greenifying barren islands. by [Gamex Studio](https://x.com/gamesgamex) 130 | - [Tetrimals](https://nixn.itch.io/tetrimals) - A puzzle game mixing Tetris with animals. by [nixn](https://nixn.itch.io/) 131 | - [Watch the Pups](https://ma5a.itch.io/watch-the-pups) - The aim of the game is to take care of some puppies. by [masa](https://ma5a.itch.io/) 132 | - [LittleJS Game Jam Results](https://itch.io/jam/littlejs-game-jam/results) - Check out all the games from the first LittleJS Game Jam! 133 | 134 | ![LittleJS Logo](examples/favicon.png) 135 | 136 | [npm]: https://img.shields.io/npm/v/littlejsengine 137 | [npm-url]: https://www.npmjs.com/package/littlejsengine 138 | [build-size]: https://img.shields.io/bundlephobia/minzip/littlejsengine 139 | [build-size-url]: https://bundlephobia.com/result?p=littlejsengine 140 | [npm-downloads]: https://img.shields.io/npm/dw/littlejsengine 141 | [npmtrends-url]: https://www.npmtrends.com/littlejsengine 142 | [deepscan]: https://deepscan.io/api/teams/22950/projects/26229/branches/831487/badge/grade.svg 143 | [deepscan-url]: https://deepscan.io/dashboard#view=project&tid=22950&pid=26229&bid=831487 144 | [discord]: https://img.shields.io/discord/939926111469568050 145 | [discord-url]: https://discord.gg/zb7hcGkyZe 146 | -------------------------------------------------------------------------------- /docs/examples/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/docs/examples/favicon.png -------------------------------------------------------------------------------- /docs/examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/docs/examples/logo.png -------------------------------------------------------------------------------- /docs/examples/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/docs/examples/screenshot.jpg -------------------------------------------------------------------------------- /docs/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/docs/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/docs/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/WorkSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/docs/fonts/WorkSans-Bold.ttf -------------------------------------------------------------------------------- /docs/scripts/core.min.js: -------------------------------------------------------------------------------- 1 | var accordionLocalStorageKey="accordion-id",themeLocalStorageKey="theme",fontSizeLocalStorageKey="font-size",html=document.querySelector("html"),MAX_FONT_SIZE=30,MIN_FONT_SIZE=10,localStorage=window.localStorage;function getTheme(){var e=localStorage.getItem(themeLocalStorageKey);if(e)return e;switch(e=document.body.getAttribute("data-theme")){case"dark":case"light":return e;case"fallback-dark":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";case"fallback-light":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";default:return"dark"}}function localUpdateTheme(e){var t=document.body,o=document.querySelectorAll(".theme-svg-use"),n="dark"===e?"#light-theme-icon":"#dark-theme-icon";t.setAttribute("data-theme",e),t.classList.remove("dark","light"),t.classList.add(e),o.forEach(function(e){e.setAttribute("xlink:href",n)})}function updateTheme(e){localUpdateTheme(e),localStorage.setItem(themeLocalStorageKey,e)}function toggleTheme(){updateTheme("dark"===document.body.getAttribute("data-theme")?"light":"dark")}function setAccordionIdToLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));t[e]=e,localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function removeAccordionIdFromLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));delete t[e],localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function getAccordionIdsFromLocalStorage(){return JSON.parse(localStorage.getItem(accordionLocalStorageKey))||{}}function toggleAccordion(e){"false"===e.getAttribute("data-isopen")?(e.setAttribute("data-isopen","true"),setAccordionIdToLocalStorage(e.id)):(e.setAttribute("data-isopen","false"),removeAccordionIdFromLocalStorage(e.id))}function initAccordion(){void 0!==localStorage.getItem(accordionLocalStorageKey)&&null!==localStorage.getItem(accordionLocalStorageKey)||localStorage.setItem(accordionLocalStorageKey,"{}");var e=document.querySelectorAll(".sidebar-section-title"),t=getAccordionIdsFromLocalStorage();e.forEach(function(e){e.addEventListener("click",function(){toggleAccordion(e)}),e.id in t&&toggleAccordion(e)})}function isSourcePage(){return Boolean(document.querySelector("#source-page"))}function bringElementIntoView(e,t=!0){var o,n,i,c;e&&(tocbotInstance&&setTimeout(()=>tocbotInstance.updateTocListActiveElement(e),60),o=document.querySelector(".navbar-container"),n=document.querySelector(".main-content"),i=e.getBoundingClientRect().top,c=16,o&&(c+=o.scrollHeight),n&&n.scrollBy(0,i-c),t&&history.pushState(null,null,"#"+e.id))}function bringLinkToView(e){e.preventDefault(),e.stopPropagation();var e=e.currentTarget.getAttribute("href");!e||(e=document.getElementById(e.slice(1)))&&bringElementIntoView(e)}function bringIdToViewOnMount(){var e,t;isSourcePage()||""!==(e=window.location.hash)&&((t=document.getElementById(e.slice(1)))||(e=decodeURI(e),t=document.getElementById(e.slice(1))),t&&bringElementIntoView(t,!1))}function createAnchorElement(e){var t=document.createElement("a");return t.textContent="#",t.href="#"+e,t.classList.add("link-anchor"),t.onclick=bringLinkToView,t}function addAnchor(){var e=document.querySelector(".main-content").querySelector("section");[e.querySelectorAll("h1"),e.querySelectorAll("h2"),e.querySelectorAll("h3"),e.querySelectorAll("h4")].forEach(function(e){e.forEach(function(e){var t=createAnchorElement(e.id);e.classList.add("has-anchor"),e.append(t)})})}function copy(e){const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}function showTooltip(e){var t=document.getElementById(e);t.classList.add("show-tooltip"),setTimeout(function(){t.classList.remove("show-tooltip")},3e3)}function copyFunction(e){var t=document.getElementById(e);copy((t.querySelector(".linenums")||t.querySelector("code")).innerText.trim().replace(/(^\t)/gm,"")),showTooltip("tooltip-"+e)}function hideTocOnSourcePage(){isSourcePage()&&(document.querySelector(".toc-container").style.display="none")}function getPreTopBar(e,t=""){e='";return'
'+('
'+t.toLocaleUpperCase()+"
")+e+"
"}function getPreDiv(){var e=document.createElement("div");return e.classList.add("pre-div"),e}function processAllPre(){var e=document.querySelectorAll("pre"),t=document.querySelector("#PeOAagUepe"),o=document.querySelector("#VuAckcnZhf"),n=0,i=0,c=(t&&(i=t.getBoundingClientRect().height),o&&(n=o.getBoundingClientRect().height),window.innerHeight-n-i-250);e.forEach(function(e,t){var o,n=e.parentNode;n&&"true"===n.getAttribute("data-skip-pre-process")||(n=getPreDiv(),o=getPreTopBar(t="ScDloZOMdL"+t,e.getAttribute("data-lang")||"code"),n.innerHTML=o,e.style.maxHeight=c+"px",e.id=t,e.classList.add("prettyprint"),e.parentNode.insertBefore(n,e),n.appendChild(e))})}function highlightAndBringLineIntoView(){var e=window.location.hash.replace("#line","");try{var t='[data-line-number="'+e+'"',o=document.querySelector(t);o.scrollIntoView(),o.parentNode.classList.add("selected")}catch(e){console.error(e)}}function getFontSize(){var e=16;try{e=Number.parseInt(html.style.fontSize.split("px")[0],10)}catch(e){console.log(e)}return e}function localUpdateFontSize(e){html.style.fontSize=e+"px";var t=document.querySelector("#b77a68a492f343baabea06fad81f651e");t&&(t.innerHTML=e)}function updateFontSize(e){localUpdateFontSize(e),localStorage.setItem(fontSizeLocalStorageKey,e)}function incrementFont(e){var t=getFontSize();t 3 | 8 |
9 | ${e} 10 |
11 | 16 | 21 | 22 | 23 | `}function initTooltip(){tippy(".theme-toggle",{content:"Toggle Theme",delay:500}),tippy(".search-button",{content:"Search",delay:500}),tippy(".font-size",{content:"Change font size",delay:500}),tippy(".codepen-button",{content:"Open code in CodePen",placement:"left"}),tippy(".copy-code",{content:"Copy this code",placement:"left"}),tippy(".font-size",{content:fontSizeTooltip(),trigger:"click",interactive:!0,allowHTML:!0,placement:"left"})}function fixTable(){for(const t of document.querySelectorAll("table")){if(t.classList.contains("hljs-ln"))return;var e=document.createElement("div");e.classList.add("table-div"),t.parentNode.insertBefore(e,t),e.appendChild(t)}}function hideMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.remove("show"),t&&t.setAttribute("data-isopen","false"),o&&o.setAttribute("xlink:href","#menu-icon")}function showMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.add("show"),t&&t.setAttribute("data-isopen","true"),o&&o.setAttribute("xlink:href","#close-icon")}function onMobileMenuClick(){("true"===document.querySelector("#mobile-menu").getAttribute("data-isopen")?hideMobileMenu:showMobileMenu)()}function initMobileMenu(){var e=document.querySelector("#mobile-menu");e&&e.addEventListener("click",onMobileMenuClick)}function addHrefToSidebarTitle(){document.querySelectorAll(".sidebar-title-anchor").forEach(function(e){e.setAttribute("href",baseURL)})}function highlightActiveLinkInSidebar(){var e=document.location.href.split("/");const t=e[e.length-1];let o=document.querySelector(`.sidebar a[href*='${t}']`);if(!o)try{o=document.querySelector(`.sidebar a[href*='${t.split("#")[0]}']`)}catch(e){return void console.error(e)}o&&(o.parentElement.classList.add("active"),o.scrollIntoView())}function onDomContentLoaded(){var e=document.querySelectorAll(".theme-toggle");initMobileMenu(),e&&e.forEach(function(e){e.addEventListener("click",toggleTheme)}),hljs.addPlugin({"after:highlightElement":function(e){e.el.parentNode.setAttribute("data-lang","code")}}),hljs.highlightAll(),hljs.initLineNumbersOnLoad({singleLine:!0}),initAccordion(),addAnchor(),processAllPre(),hideTocOnSourcePage(),setTimeout(function(){bringIdToViewOnMount(),isSourcePage()&&highlightAndBringLineIntoView()},1e3),initTooltip(),fixTable(),addHrefToSidebarTitle(),highlightActiveLinkInSidebar()}updateTheme(getTheme()),function(){var e=getFontSize(),t=localStorage.getItem(fontSizeLocalStorageKey);t?(t=Number.parseInt(t,10))!==e&&updateFontSize(t):updateFontSize(e)}(),window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",e=>{e=new URL(e.newURL);""!==e.hash&&bringIdToViewOnMount(e.hash)}),window.addEventListener("storage",e=>{"undefined"!==e.newValue&&(initTooltip(),e.key===themeLocalStorageKey&&localUpdateTheme(e.newValue),e.key===fontSizeLocalStorageKey&&localUpdateFontSize(e.newValue))}); -------------------------------------------------------------------------------- /docs/scripts/resize.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | // This file is @deprecated 3 | 4 | var NAVBAR_OPTIONS = {}; 5 | 6 | (function() { 7 | var NAVBAR_RESIZE_LOCAL_STORAGE_KEY = 'NAVBAR_RESIZE_LOCAL_STORAGE_KEY'; 8 | 9 | var navbar = document.querySelector('#navbar'); 10 | var footer = document.querySelector('#footer'); 11 | var mainSection = document.querySelector('#main'); 12 | var localStorageResizeObject = JSON.parse( 13 | // eslint-disable-next-line no-undef 14 | localStorage.getItem(NAVBAR_RESIZE_LOCAL_STORAGE_KEY) 15 | ); 16 | 17 | /** 18 | * Check whether we have any resize value in local storage or not. 19 | * If we have resize value then resize the navbar. 20 | **/ 21 | if (localStorageResizeObject) { 22 | navbar.style.width = localStorageResizeObject.width; 23 | mainSection.style.marginLeft = localStorageResizeObject.width; 24 | footer.style.marginLeft = localStorageResizeObject.width; 25 | } 26 | 27 | var navbarSlider = document.querySelector('#navbar-resize'); 28 | 29 | function resizeNavbar(event) { 30 | var pageX = event.pageX, 31 | pageXPlusPx = event.pageX + 'px', 32 | min = Number.parseInt(NAVBAR_OPTIONS.min, 10) || 300, 33 | max = Number.parseInt(NAVBAR_OPTIONS.max, 10) || 600; 34 | 35 | /** 36 | * Just to add some checks. If min is smaller than 10 then 37 | * user may accidentally end up reducing the size of navbar 38 | * less than 10. In that case user will not able to resize navbar 39 | * because navbar slider will be hidden. 40 | */ 41 | if (min < 10) { 42 | min = 10; 43 | } 44 | 45 | /** 46 | * Only resize if pageX in range between min and max 47 | * allowed value. 48 | */ 49 | if (min < pageX && pageX < max) { 50 | navbar.style.width = pageXPlusPx; 51 | mainSection.style.marginLeft = pageXPlusPx; 52 | footer.style.marginLeft = pageXPlusPx; 53 | } 54 | } 55 | 56 | function setupEventListeners() { 57 | // eslint-disable-next-line no-undef 58 | window.addEventListener('mousemove', resizeNavbar); 59 | // eslint-disable-next-line no-undef 60 | window.addEventListener('touchmove', resizeNavbar); 61 | } 62 | 63 | function afterRemovingEventListeners() { 64 | // eslint-disable-next-line no-undef 65 | localStorage.setItem( 66 | NAVBAR_RESIZE_LOCAL_STORAGE_KEY, 67 | JSON.stringify({ 68 | width: navbar.style.width 69 | }) 70 | ); 71 | } 72 | 73 | function removeEventListeners() { 74 | // eslint-disable-next-line no-undef 75 | window.removeEventListener('mousemove', resizeNavbar); 76 | // eslint-disable-next-line no-undef 77 | window.removeEventListener('touchend', resizeNavbar); 78 | afterRemovingEventListeners(); 79 | } 80 | 81 | navbarSlider.addEventListener('mousedown', setupEventListeners); 82 | navbarSlider.addEventListener('touchstart', setupEventListeners); 83 | // eslint-disable-next-line no-undef 84 | window.addEventListener('mouseup', removeEventListeners); 85 | })(); 86 | 87 | // eslint-disable-next-line no-unused-vars 88 | function setupResizeOptions(options) { 89 | NAVBAR_OPTIONS = options; 90 | } 91 | -------------------------------------------------------------------------------- /docs/scripts/search.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | const searchId = 'LiBfqbJVcV'; 4 | const searchHash = '#' + searchId; 5 | const searchContainer = document.querySelector('#PkfLWpAbet'); 6 | const searchWrapper = document.querySelector('#iCxFxjkHbP'); 7 | const searchCloseButton = document.querySelector('#VjLlGakifb'); 8 | const searchInput = document.querySelector('#vpcKVYIppa'); 9 | const resultBox = document.querySelector('#fWwVHRuDuN'); 10 | 11 | function showResultText(text) { 12 | resultBox.innerHTML = `${text}`; 13 | } 14 | 15 | function hideSearch() { 16 | // eslint-disable-next-line no-undef 17 | if (window.location.hash === searchHash) { 18 | // eslint-disable-next-line no-undef 19 | history.go(-1); 20 | } 21 | 22 | // eslint-disable-next-line no-undef 23 | window.onhashchange = null; 24 | 25 | if (searchContainer) { 26 | searchContainer.style.display = 'none'; 27 | } 28 | } 29 | 30 | function listenCloseKey(event) { 31 | if (event.key === 'Escape') { 32 | hideSearch(); 33 | // eslint-disable-next-line no-undef 34 | window.removeEventListener('keyup', listenCloseKey); 35 | } 36 | } 37 | 38 | function showSearch() { 39 | try { 40 | // Closing mobile menu before opening 41 | // search box. 42 | // It is defined in core.js 43 | // eslint-disable-next-line no-undef 44 | hideMobileMenu(); 45 | } catch (error) { 46 | console.error(error); 47 | } 48 | 49 | // eslint-disable-next-line no-undef 50 | window.onhashchange = hideSearch; 51 | 52 | // eslint-disable-next-line no-undef 53 | if (window.location.hash !== searchHash) { 54 | // eslint-disable-next-line no-undef 55 | history.pushState(null, null, searchHash); 56 | } 57 | 58 | if (searchContainer) { 59 | searchContainer.style.display = 'flex'; 60 | // eslint-disable-next-line no-undef 61 | window.addEventListener('keyup', listenCloseKey); 62 | } 63 | 64 | if (searchInput) { 65 | searchInput.focus(); 66 | } 67 | } 68 | 69 | async function fetchAllData() { 70 | // eslint-disable-next-line no-undef 71 | const { hostname, protocol, port } = location; 72 | 73 | // eslint-disable-next-line no-undef 74 | const base = protocol + '//' + hostname + (port !== '' ? ':' + port : '') + baseURL; 75 | // eslint-disable-next-line no-undef 76 | const url = new URL('data/search.json', base); 77 | const result = await fetch(url); 78 | const { list } = await result.json(); 79 | 80 | return list; 81 | } 82 | 83 | // eslint-disable-next-line no-unused-vars 84 | function onClickSearchItem(event) { 85 | const target = event.currentTarget; 86 | 87 | if (target) { 88 | const href = target.getAttribute('href') || ''; 89 | let elementId = href.split('#')[1] || ''; 90 | let element = document.getElementById(elementId); 91 | 92 | if (!element) { 93 | elementId = decodeURI(elementId); 94 | element = document.getElementById(elementId); 95 | } 96 | 97 | if (element) { 98 | setTimeout(function() { 99 | // eslint-disable-next-line no-undef 100 | bringElementIntoView(element); // defined in core.js 101 | }, 100); 102 | } 103 | } 104 | } 105 | 106 | function buildSearchResult(result) { 107 | let output = ''; 108 | const removeHTMLTagsRegExp = /(<([^>]+)>)/ig; 109 | 110 | for (const res of result) { 111 | const { title = '', description = '' } = res.item; 112 | 113 | const _link = res.item.link.replace('.*/, ''); 114 | const _title = title.replace(removeHTMLTagsRegExp, ""); 115 | const _description = description.replace(removeHTMLTagsRegExp, ""); 116 | 117 | output += ` 118 | 119 |
${_title}
120 |
${_description || 'No description available.'}
121 |
122 | `; 123 | } 124 | 125 | return output; 126 | } 127 | 128 | function getSearchResult(list, keys, searchKey) { 129 | const defaultOptions = { 130 | shouldSort: true, 131 | threshold: 0.4, 132 | location: 0, 133 | distance: 100, 134 | maxPatternLength: 32, 135 | minMatchCharLength: 1, 136 | keys: keys 137 | }; 138 | 139 | const options = { ...defaultOptions }; 140 | 141 | // eslint-disable-next-line no-undef 142 | const searchIndex = Fuse.createIndex(options.keys, list); 143 | 144 | // eslint-disable-next-line no-undef 145 | const fuse = new Fuse(list, options, searchIndex); 146 | 147 | const result = fuse.search(searchKey); 148 | 149 | if (result.length > 20) { 150 | return result.slice(0, 20); 151 | } 152 | 153 | return result; 154 | } 155 | 156 | function debounce(func, wait, immediate) { 157 | let timeout; 158 | 159 | return function() { 160 | const args = arguments; 161 | 162 | clearTimeout(timeout); 163 | timeout = setTimeout(() => { 164 | timeout = null; 165 | if (!immediate) { 166 | // eslint-disable-next-line consistent-this, no-invalid-this 167 | func.apply(this, args); 168 | } 169 | }, wait); 170 | 171 | if (immediate && !timeout) { 172 | // eslint-disable-next-line consistent-this, no-invalid-this 173 | func.apply(this, args); 174 | } 175 | }; 176 | } 177 | 178 | let searchData; 179 | 180 | async function search(event) { 181 | const value = event.target.value; 182 | const keys = ['title', 'description']; 183 | 184 | if (!resultBox) { 185 | console.error('Search result container not found'); 186 | 187 | return; 188 | } 189 | 190 | if (!value) { 191 | showResultText('Type anything to view search result'); 192 | 193 | return; 194 | } 195 | 196 | if (!searchData) { 197 | showResultText('Loading...'); 198 | 199 | try { 200 | // eslint-disable-next-line require-atomic-updates 201 | searchData = await fetchAllData(); 202 | } catch (e) { 203 | console.log(e); 204 | showResultText('Failed to load result.'); 205 | 206 | return; 207 | } 208 | } 209 | 210 | const result = getSearchResult(searchData, keys, value); 211 | 212 | if (!result.length) { 213 | showResultText('No result found! Try some different combination.'); 214 | 215 | return; 216 | } 217 | 218 | // eslint-disable-next-line require-atomic-updates 219 | resultBox.innerHTML = buildSearchResult(result); 220 | } 221 | 222 | function onDomContentLoaded() { 223 | const searchButton = document.querySelectorAll('.search-button'); 224 | const debouncedSearch = debounce(search, 300); 225 | 226 | if (searchCloseButton) { 227 | searchCloseButton.addEventListener('click', hideSearch); 228 | } 229 | 230 | if (searchButton) { 231 | searchButton.forEach(function(item) { 232 | item.addEventListener('click', showSearch); 233 | }); 234 | } 235 | 236 | if (searchContainer) { 237 | searchContainer.addEventListener('click', hideSearch); 238 | } 239 | 240 | if (searchWrapper) { 241 | searchWrapper.addEventListener('click', function(event) { 242 | event.stopPropagation(); 243 | }); 244 | } 245 | 246 | if (searchInput) { 247 | searchInput.addEventListener('keyup', debouncedSearch); 248 | } 249 | 250 | // eslint-disable-next-line no-undef 251 | if (window.location.hash === searchHash) { 252 | showSearch(); 253 | } 254 | } 255 | 256 | // eslint-disable-next-line no-undef 257 | window.addEventListener('DOMContentLoaded', onDomContentLoaded); 258 | 259 | // eslint-disable-next-line no-undef 260 | window.addEventListener('hashchange', function() { 261 | // eslint-disable-next-line no-undef 262 | if (window.location.hash === searchHash) { 263 | showSearch(); 264 | } 265 | }); 266 | -------------------------------------------------------------------------------- /docs/scripts/search.min.js: -------------------------------------------------------------------------------- 1 | const searchId="LiBfqbJVcV",searchHash="#"+searchId,searchContainer=document.querySelector("#PkfLWpAbet"),searchWrapper=document.querySelector("#iCxFxjkHbP"),searchCloseButton=document.querySelector("#VjLlGakifb"),searchInput=document.querySelector("#vpcKVYIppa"),resultBox=document.querySelector("#fWwVHRuDuN");function showResultText(e){resultBox.innerHTML=`${e}`}function hideSearch(){window.location.hash===searchHash&&history.go(-1),window.onhashchange=null,searchContainer&&(searchContainer.style.display="none")}function listenCloseKey(e){"Escape"===e.key&&(hideSearch(),window.removeEventListener("keyup",listenCloseKey))}function showSearch(){try{hideMobileMenu()}catch(e){console.error(e)}window.onhashchange=hideSearch,window.location.hash!==searchHash&&history.pushState(null,null,searchHash),searchContainer&&(searchContainer.style.display="flex",window.addEventListener("keyup",listenCloseKey)),searchInput&&searchInput.focus()}async function fetchAllData(){var{hostname:e,protocol:t,port:n}=location,t=t+"//"+e+(""!==n?":"+n:"")+baseURL,e=new URL("data/search.json",t);const a=await fetch(e);n=(await a.json()).list;return n}function onClickSearchItem(t){const n=t.currentTarget;if(n){const a=n.getAttribute("href")||"";t=a.split("#")[1]||"";let e=document.getElementById(t);e||(t=decodeURI(t),e=document.getElementById(t)),e&&setTimeout(function(){bringElementIntoView(e)},100)}}function buildSearchResult(e){let t="";var n=/(<([^>]+)>)/gi;for(const s of e){const{title:c="",description:i=""}=s.item;var a=s.item.link.replace('.*/,""),o=c.replace(n,""),r=i.replace(n,"");t+=` 2 | 3 |
${o}
4 |
${r||"No description available."}
5 |
6 | `}return t}function getSearchResult(e,t,n){var t={...{shouldSort:!0,threshold:.4,location:0,distance:100,maxPatternLength:32,minMatchCharLength:1,keys:t}},a=Fuse.createIndex(t.keys,e);const o=new Fuse(e,t,a),r=o.search(n);return 20{o=null,a||t.apply(this,e)},n),a&&!o&&t.apply(this,e)}}let searchData;async function search(e){e=e.target.value;if(resultBox)if(e){if(!searchData){showResultText("Loading...");try{searchData=await fetchAllData()}catch(e){return console.log(e),void showResultText("Failed to load result.")}}e=getSearchResult(searchData,["title","description"],e);e.length?resultBox.innerHTML=buildSearchResult(e):showResultText("No result found! Try some different combination.")}else showResultText("Type anything to view search result");else console.error("Search result container not found")}function onDomContentLoaded(){const e=document.querySelectorAll(".search-button");var t=debounce(search,300);searchCloseButton&&searchCloseButton.addEventListener("click",hideSearch),e&&e.forEach(function(e){e.addEventListener("click",showSearch)}),searchContainer&&searchContainer.addEventListener("click",hideSearch),searchWrapper&&searchWrapper.addEventListener("click",function(e){e.stopPropagation()}),searchInput&&searchInput.addEventListener("keyup",t),window.location.hash===searchHash&&showSearch()}window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",function(){window.location.hash===searchHash&&showSearch()}); -------------------------------------------------------------------------------- /docs/scripts/third-party/hljs-line-num.js: -------------------------------------------------------------------------------- 1 | !function(r,o){"use strict";var e,l="hljs-ln",s="hljs-ln-line",f="hljs-ln-code",c="hljs-ln-numbers",u="hljs-ln-n",h="data-line-number",n=/\r\n|\r|\n/g;function t(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var e=parseInt(t.dataset.lineNumber),o=parseInt(r.dataset.lineNumber);if(e==o)return n;var a,i=t.textContent,l=r.textContent;for(o{6}',[s,c,u,h,f,a+t.startFrom,0{1}',[l,o])}return e}function m(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r{1}\n',[n,0=r.collapseDepth&&(t.isCollapsed=!0),o.push(t)}return e},{nest:[]})},selectHeadings:function(e,t){var l=t;r.ignoreSelector&&(l=t.split(",").map(function(e){return e.trim()+":not("+r.ignoreSelector+")"}));try{return e.querySelectorAll(l)}catch(e){return console.warn("Headers not found with selector: "+l),null}}}}function BuildHtml(i){var r,n=[].forEach,a=[].some,c=document.body,d=document.querySelector(i.contentSelector),u=!0,m=" ";function o(e,t){var l,t=t.appendChild(function(e){var t=document.createElement("li"),l=document.createElement("a");i.listItemClass&&t.setAttribute("class",i.listItemClass);i.onClick&&(l.onclick=i.onClick);i.includeTitleTags&&l.setAttribute("title",e.textContent);i.includeHtml&&e.childNodes.length?n.call(e.childNodes,function(e){l.appendChild(e.cloneNode(!0))}):l.textContent=e.textContent;return l.setAttribute("href",i.basePath+"#"+e.id),l.setAttribute("class",i.linkClass+m+"node-name--"+e.nodeName+m+i.extraLinkClasses),t.appendChild(l),t}(e));e.children.length&&(l=s(e.isCollapsed),e.children.forEach(function(e){o(e,l)}),t.appendChild(l))}function s(e){var t=i.orderedList?"ol":"ul",t=document.createElement(t),l=i.listClass+m+i.extraListClasses;return e&&(l=(l+=m+i.collapsibleClass)+(m+i.isCollapsedClass)),t.setAttribute("class",l),t}function f(e){var t=[].forEach,l=r.querySelectorAll("."+i.linkClass),l=(t.call(l,function(e){e.className=e.className.split(m+i.activeLinkClass).join("")}),r.querySelectorAll("."+i.listItemClass)),l=(t.call(l,function(e){e.className=e.className.split(m+i.activeListItemClass).join("")}),r.querySelector("."+i.linkClass+".node-name--"+e.nodeName+'[href="'+i.basePath+"#"+e.id.replace(/([ #;&,.+*~':"!^$[\]()=>|/@])/g,"\\$1")+'"]')),e=(l&&-1===l.className.indexOf(i.activeLinkClass)&&(l.className+=m+i.activeLinkClass),l&&l.parentNode),e=(e&&-1===e.className.indexOf(i.activeListItemClass)&&(e.className+=m+i.activeListItemClass),r.querySelectorAll("."+i.listClass+"."+i.collapsibleClass));t.call(e,function(e){-1===e.className.indexOf(i.isCollapsedClass)&&(e.className+=m+i.isCollapsedClass)}),l&&l.nextSibling&&-1!==l.nextSibling.className.indexOf(i.isCollapsedClass)&&(l.nextSibling.className=l.nextSibling.className.split(m+i.isCollapsedClass).join("")),function e(t){if(t&&-1!==t.className.indexOf(i.collapsibleClass)&&-1!==t.className.indexOf(i.isCollapsedClass))return t.className=t.className.split(m+i.isCollapsedClass).join(""),e(t.parentNode.parentNode);return t}(l&&l.parentNode.parentNode)}return{enableTocAnimation:function(){u=!0},disableTocAnimation:function(e){"string"==typeof(e=e.target||e.srcElement).className&&-1!==e.className.indexOf(i.linkClass)&&(u=!1)},render:function(e,t){var l=s(!1);if(t.forEach(function(e){o(e,l)}),null!==(r=e||r))return r.firstChild&&r.removeChild(r.firstChild),0===t.length?r:r.appendChild(l)},updateToc:function(e){n=i.scrollContainer&&document.querySelector(i.scrollContainer)?document.querySelector(i.scrollContainer).scrollTop:document.documentElement.scrollTop||c.scrollTop,i.positionFixedSelector&&(t=i.scrollContainer&&document.querySelector(i.scrollContainer)?document.querySelector(i.scrollContainer).scrollTop:document.documentElement.scrollTop||c.scrollTop,l=document.querySelector(i.positionFixedSelector),"auto"===i.fixedSidebarOffset&&(i.fixedSidebarOffset=r.offsetTop),t>i.fixedSidebarOffset?-1===l.className.indexOf(i.positionFixedClass)&&(l.className+=m+i.positionFixedClass):l.className=l.className.split(m+i.positionFixedClass).join(""));var n,t,l,o,s=e;u&&null!==r&&0l?(o=s[0===t?t:t-1],!0):t===s.length-1?(o=s[s.length-1],!0):void 0}),f(o))},updateListActiveElement:f}}function updateTocScroll(e){var t,l=e.tocElement||document.querySelector(e.tocSelector);l&&l.scrollHeight>l.clientHeight&&((e=l.querySelector("."+e.activeListItemClass))&&(t=l.getBoundingClientRect().top,l.scrollTop=e.offsetTop-t))}!function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.tocbot=t(e)}("undefined"!=typeof global?global:this.window||this.global,function(e){"use strict";var l,a,c,d,u={},m={},f=!!(e&&e.document&&e.document.querySelector&&e.addEventListener);if("undefined"!=typeof window||f)return d=Object.prototype.hasOwnProperty,m.destroy=function(){var e=C(u);null!==e&&(u.skipRendering||e&&(e.innerHTML=""),u.scrollContainer&&document.querySelector(u.scrollContainer)?(document.querySelector(u.scrollContainer).removeEventListener("scroll",this._scrollListener,!1),document.querySelector(u.scrollContainer).removeEventListener("resize",this._scrollListener,!1)):(document.removeEventListener("scroll",this._scrollListener,!1),document.removeEventListener("resize",this._scrollListener,!1)))},m.init=function(e){if(f){u=function(){for(var e={},t=0;t tile(i, 16, 0, 1); 33 | spriteAtlas = 34 | { 35 | circle: gameTile(0), 36 | dot: gameTile(1), 37 | circleOutline: gameTile(2), 38 | squareOutline: gameTile(3), 39 | wheel: gameTile(4), 40 | gear: gameTile(5), 41 | squareOutline2: gameTile(6), 42 | }; 43 | 44 | loadScene(scene); 45 | } 46 | 47 | /////////////////////////////////////////////////////////////////////////////// 48 | function gameUpdate() 49 | { 50 | // scale canvas to fit based on 1080p 51 | cameraScale = mainCanvasSize.y * 48 / 1080; 52 | 53 | // mouse controls 54 | if (mouseWasPressed(0)) 55 | { 56 | // grab object 57 | sound_click.play(mousePos); 58 | const object = box2dPointCast(mousePos); 59 | if (object) 60 | mouseJoint = box2dCreateMouseJoint(object, groundObject, mousePos); 61 | } 62 | if (mouseWasReleased(0)) 63 | { 64 | // release object 65 | sound_click.play(mousePos, 1, .5); 66 | if (mouseJoint) 67 | { 68 | box2dDestroyJoint(mouseJoint); 69 | mouseJoint = 0; 70 | } 71 | } 72 | if (mouseIsDown(1) || mouseIsDown('KeyZ')) 73 | { 74 | const isSet = repeatSpawnTimer.isSet(); 75 | if (!isSet || repeatSpawnTimer.elapsed()) 76 | { 77 | // spawn continuously after a delay 78 | isSet || repeatSpawnTimer.set(.5); 79 | spawnRandomObject(mousePos); 80 | } 81 | } 82 | else 83 | repeatSpawnTimer.unset(); 84 | if (mouseWasPressed(2) || keyWasPressed('KeyX')) 85 | explosion(mousePos); 86 | if (mouseJoint) 87 | mouseJoint.SetTarget(mousePos.getBox2d()); 88 | 89 | if (keyWasPressed('KeyR')) 90 | loadScene(scene); // reset scene 91 | if (keyWasPressed('ArrowUp') || keyWasPressed('ArrowDown')) 92 | { 93 | // change scene 94 | scene += keyWasPressed('ArrowUp') ? 1 : -1; 95 | scene = mod(scene, maxScenes); 96 | loadScene(scene); 97 | } 98 | 99 | if (car) 100 | { 101 | // update car control 102 | const input = keyDirection(); 103 | car.applyMotorInput(-input.x); 104 | } 105 | } 106 | 107 | /////////////////////////////////////////////////////////////////////////////// 108 | function gameUpdatePost() 109 | { 110 | 111 | } 112 | 113 | /////////////////////////////////////////////////////////////////////////////// 114 | function gameRender() 115 | { 116 | // draw a grey square in the background without using webgl 117 | drawRect(vec2(20,8), vec2(100), hsl(0,0,.8), 0, 0); 118 | 119 | if (scene == 5) 120 | { 121 | // raycast test 122 | const count = 100; 123 | const distance = 10; 124 | for (let i=count;i--;) 125 | { 126 | const start = mousePos; 127 | const end = mousePos.add(vec2(distance,0).rotate(i/count*PI*2)); 128 | const result = box2dRaycast(start, end); 129 | const color = result ? hsl(0,1,.5,.5) : hsl(.5,1,.5,.5); 130 | drawLine(start, result ? result.point : end, .1, color); 131 | } 132 | } 133 | } 134 | 135 | /////////////////////////////////////////////////////////////////////////////// 136 | function gameRenderPost() 137 | { 138 | if (mouseJoint) 139 | { 140 | // draw mouse joint 141 | const ab = vec2(mouseJoint.GetAnchorB()); 142 | drawTile(ab, vec2(.3), spriteAtlas.circle, BLACK); 143 | drawLine(mousePos, ab, .1, BLACK); 144 | } 145 | 146 | // draw to overlay canvas for hud rendering 147 | const pos = vec2(mainCanvasSize.x/2, 50); 148 | drawText('LittleJS Box2D Demo', 80, 80); 149 | drawText(sceneName, 60, 100); 150 | if (scene == 0) 151 | { 152 | drawText('Mouse Left = Grab'); 153 | drawText('Mouse Middle or Z = Spawn'); 154 | drawText('Mouse Right or X = Explode'); 155 | drawText('Arrows Up/Down = Change Scene'); 156 | } 157 | if (scene == 3) 158 | { 159 | drawText('Right = Accelerate'); 160 | drawText('Left = Reverse'); 161 | } 162 | 163 | function drawText(text, size=40, gap=50) 164 | { drawTextScreen(text, pos, size, WHITE, size*.1); pos.y += gap; } 165 | } 166 | 167 | /////////////////////////////////////////////////////////////////////////////// 168 | // Startup LittleJS Engine with Box2D 169 | 170 | box2dEngineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/box2d/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Box2D Example 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/box2d/scenes.js: -------------------------------------------------------------------------------- 1 | /* 2 | LittleJS Box2D Example - Scenes 3 | - Each scene demonstrates a feature of Box2D 4 | - Feel free to use tis code in your own projects 5 | - 0 = Shapes: box, circle, poly, and edge 6 | - 1 = Pyramid stack 7 | - 2 = Dominoes chain reaction 8 | - 3 = Car with controls 9 | - 4 = Rope with box attached 10 | - 5 = Raycasts in all directions 11 | - 6 = Joints demo with every joint type 12 | - 7 = Contacts begin and end callbacks 13 | - 8 = Mobile object with multiple bodies 14 | - 9 = Cloth object grid using rope joints 15 | - 10 = Softbody object grid using weld joints 16 | */ 17 | 18 | 'use strict'; 19 | 20 | function loadScene(_scene) 21 | { 22 | scene = _scene; 23 | 24 | // setup 25 | cameraPos = vec2(20,10); 26 | setGravity(-20); 27 | 28 | // destroy old scene 29 | engineObjectsDestroy(); 30 | mouseJoint = 0; 31 | car = 0; 32 | 33 | // create walls 34 | groundObject = spawnBox(vec2(0,-4), vec2(1e3,8), hsl(0,0,.2), box2dBodyTypeStatic); 35 | spawnBox(vec2(-4, 0), vec2(8,1e3), BLACK, box2dBodyTypeStatic); 36 | spawnBox(vec2(44, 0), vec2(8,1e3), BLACK, box2dBodyTypeStatic); 37 | spawnBox(vec2(0,100), vec2(1e3,8), BLACK, box2dBodyTypeStatic); 38 | 39 | if (scene == 0) 40 | { 41 | sceneName = 'Shapes'; 42 | spawnRandomEdges(); 43 | spawnBox(vec2(11,8), 4, randColor(), box2dBodyTypeStatic, false); 44 | spawnCircle(vec2(20,8), 4, randColor(), box2dBodyTypeStatic, false); 45 | spawnRandomPoly(vec2(29,8), 4, randColor(), box2dBodyTypeStatic, false); 46 | for (let i=500;i--;) 47 | spawnRandomObject(vec2(rand(1,39), rand(10,50))); 48 | } 49 | if (scene == 1) 50 | { 51 | sceneName = 'Pyramid'; 52 | spawnPyramid(vec2(20,0), 15); 53 | spawnBox(vec2(10,2), 4, randColor()); 54 | spawnCircle(vec2(30,2), 4, randColor()); 55 | } 56 | if (scene == 2) 57 | { 58 | sceneName = 'Dominoes'; 59 | spawnDominoes(vec2(11,11), 11); 60 | spawnDominoes(vec2(2,0), 13, vec2(1,3)); 61 | spawnCircle(vec2(10,20), 2, randColor()); 62 | spawnCircle(vec2(31.7,12), 2, randColor()); 63 | spawnBox(vec2(16,11), vec2(32,1), randColor(), box2dBodyTypeStatic, false); 64 | spawnBox(vec2(24,6.5), vec2(32,1), randColor(), box2dBodyTypeStatic, false, -.15); 65 | } 66 | if (scene == 3) 67 | { 68 | sceneName = 'Car'; 69 | car = new CarObject(vec2(10,2)); 70 | spawnBox(vec2(20,0), vec2(10,2), randColor(), box2dBodyTypeStatic, false, -.2); 71 | spawnPyramid(vec2(32,0), 6); 72 | } 73 | if (scene == 4) 74 | { 75 | sceneName = 'Rope'; 76 | const startPos = vec2(20, 14); 77 | const angle = PI/2; 78 | const color = randColor(); 79 | const count = 10; 80 | const endObject = spawnRope(startPos, count, angle, color); 81 | 82 | // connect box to end 83 | const endPos = endObject.localToWorld(vec2(0,-endObject.size.y/2)); 84 | const o = spawnBox(endPos, 3, randColor()); 85 | o.setAngularDamping(.5); 86 | o.setFilterData(2, 2); 87 | box2dCreateRevoluteJoint(endObject, o); 88 | spawnPyramid(vec2(20,0), 7); 89 | } 90 | if (scene == 5) 91 | { 92 | sceneName = 'Raycasts'; 93 | spawnRandomEdges(); 94 | for (let i=100;i--;) 95 | spawnRandomObject(vec2(rand(1,39), rand(20)), 2, box2dBodyTypeStatic, rand(PI*2)); 96 | } 97 | if (scene == 6) 98 | { 99 | sceneName = 'Joints'; 100 | { 101 | // prismatic joint 102 | const o1 = spawnBox(vec2(20,8), vec2(3,2), randColor()); 103 | box2dCreatePrismaticJoint(groundObject, o1, o1.pos, vec2(1,0), true); 104 | const o2 = spawnBox(vec2(20,15), vec2(2,3), randColor()); 105 | box2dCreatePrismaticJoint(groundObject, o2, o2.pos, vec2(0,1), true); 106 | 107 | // lines to make it look line a track 108 | const l1 = new EngineObject(vec2(20, 8), vec2(39,.5), 0, 0, GRAY); 109 | const l2 = new EngineObject(vec2(20,50), vec2(.5,99), 0, 0, GRAY); 110 | l1.gravityScale = l2.gravityScale = 0; 111 | l1.renderOrder = l2.renderOrder = -2; 112 | } 113 | { 114 | // pulley joint 115 | const anchorA = vec2(14,15); 116 | const anchorB = vec2(26,15); 117 | const oA = new PulleyJointObjects(vec2(15,8), vec2(1,2), randColor(), anchorA); 118 | const oB = new PulleyJointObjects(vec2(25,8), vec2(1,2), randColor(), anchorB); 119 | const aA = spawnCircle(anchorA, 2, randColor(), box2dBodyTypeStatic); 120 | const aB = spawnCircle(anchorB, 2, randColor(), box2dBodyTypeStatic); 121 | const oaA = oA.localToWorld(vec2(0,oA.size.y/2)); 122 | const oaB = oB.localToWorld(vec2(0,oB.size.y/2)); 123 | box2dCreatePulleyJoint(oA, oB, aA.pos, aB.pos, oaA, oaB); 124 | 125 | // a line to make it look like connecting rope 126 | const line = new EngineObject(vec2(20,15), vec2(10,.2), 0, 0, BLACK); 127 | line.gravityScale = 0; 128 | line.renderOrder = -1; 129 | } 130 | { 131 | // gear joint 132 | const o1 = spawnCircle(vec2(23.5,3), 3, randColor()); 133 | const j1 = box2dCreateRevoluteJoint(groundObject, o1, o1.pos); 134 | const o2 = spawnCircle(vec2(28,3), 6, randColor()); 135 | o1.tileInfo = o2.tileInfo = spriteAtlas.gear; 136 | const j2 = box2dCreateRevoluteJoint(groundObject, o2, o2.pos); 137 | box2dCreateGearJoint(o1, o2, j1, j2, 2); 138 | } 139 | { 140 | // weld joint 141 | const o1 = spawnBox(vec2(15,2), 4, randColor()); 142 | const o2 = spawnBox(vec2(17,2), 2, randColor()); 143 | box2dCreateWeldJoint(o1, o2); 144 | } 145 | { 146 | // distance joint 147 | const o1 = new Box2dObject(vec2(30,11), vec2(4), spriteAtlas.circleOutline, 0, randColor(), box2dBodyTypeStatic); 148 | o1.renderOrder = -2; 149 | const o2 = spawnCircle(vec2(30,8), 2, randColor()); 150 | box2dCreateDistanceJoint(o1, o2); 151 | } 152 | { 153 | // motor joint 154 | const o = new Box2dObject(vec2(10,8), vec2(4), spriteAtlas.circleOutline, 0, randColor(), box2dBodyTypeStatic); 155 | o.renderOrder = -2; 156 | new MotorJointObject(vec2(10,8), vec2(2), randColor(), o); 157 | } 158 | { 159 | // friction joint 160 | const o = spawnBox(vec2(10,15), 3, randColor()); 161 | o.tileInfo = spriteAtlas.squareOutline2; 162 | const joint = box2dCreateFrictionJoint(groundObject, o); 163 | joint.SetMaxForce(200); 164 | joint.SetMaxTorque(200); 165 | } 166 | } 167 | if (scene == 7) 168 | { 169 | sceneName = 'Contacts'; 170 | new ContactTester(vec2(15,8), vec2(5), RED, RED); 171 | new ContactTester(vec2(25,8), vec2(5), CYAN, CYAN, false, false); 172 | for (let i=200;i--;) 173 | spawnRandomObject(vec2(rand(1,39), rand(10,50))); 174 | } 175 | if (scene == 8) 176 | { 177 | sceneName = 'Mobile'; 178 | const pos = vec2(20, 16); 179 | const mobile = new MobileObject(pos, 12, 2, 5); 180 | box2dCreateRevoluteJoint(groundObject, mobile, pos); 181 | } 182 | if (scene == 9) 183 | { 184 | sceneName = 'Cloth'; 185 | new ClothObject(vec2(20, 9), vec2(15), vec2(24), randColor()); 186 | } 187 | if (scene == 10) 188 | { 189 | sceneName = 'Softbodies'; 190 | for(let i=3;i--;) 191 | new SoftBodyObject(vec2(20, 3+i*7), vec2(6-i), vec2(9-i), randColor()); 192 | } 193 | } -------------------------------------------------------------------------------- /examples/box2d/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/box2d/tiles.png -------------------------------------------------------------------------------- /examples/breakout/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Breakout Game 3 | - A simple breakout game 4 | - Includes sound and particles 5 | - Uses a post processing effect 6 | - Control with mouse, touch, or gamepad 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // show the LittleJS splash screen 12 | setShowSplashScreen(true); 13 | 14 | let levelSize, ball, paddle, score, brickCount; 15 | 16 | // sound effects 17 | const sound_start = new Sound([,0,500,,.04,.3,1,2,,,570,.02,.02,,,,.04]); 18 | const sound_break = new Sound([,,90,,.01,.03,4,,,,,,,9,50,.2,,.2,.01]); 19 | const sound_bounce = new Sound([,,1e3,,.03,.02,1,2,,,940,.03,,,,,.2,.6,,.06]); 20 | 21 | /////////////////////////////////////////////////////////////////////////////// 22 | function gameInit() 23 | { 24 | canvasFixedSize = vec2(1920, 1080); // 1080p 25 | levelSize = vec2(38, 20); 26 | cameraPos = levelSize.scale(.5); 27 | cameraScale = 48; 28 | paddle = new Paddle(vec2(levelSize.x/2-12, 1)); 29 | score = brickCount = 0; 30 | 31 | // spawn bricks 32 | const pos = vec2(); 33 | for (pos.x = 4; pos.x <= levelSize.x-4; pos.x += 2) 34 | for (pos.y = 12; pos.y <= levelSize.y-2; pos.y += 1) 35 | new Brick(pos); 36 | 37 | // create walls 38 | new Wall(vec2(-.5,levelSize.y/2), vec2(1,100)); // top 39 | new Wall(vec2(levelSize.x+.5,levelSize.y/2), vec2(1,100)); // left 40 | new Wall(vec2(levelSize.x/2,levelSize.y+.5), vec2(100,1)); // right 41 | 42 | setupPostProcess(); // set up a post processing shader 43 | } 44 | 45 | /////////////////////////////////////////////////////////////////////////////// 46 | function gameUpdate() 47 | { 48 | // spawn ball 49 | if (!ball && (mouseWasPressed(0) || gamepadWasPressed(0))) 50 | { 51 | ball = new Ball(vec2(levelSize.x/2, levelSize.y/2)); 52 | sound_start.play(); 53 | } 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | function gameUpdatePost() 58 | { 59 | 60 | } 61 | 62 | /////////////////////////////////////////////////////////////////////////////// 63 | function gameRender() 64 | { 65 | // draw a the background 66 | drawRect(cameraPos, levelSize.scale(2), hsl(0,0,.5)); 67 | drawRect(cameraPos, levelSize, hsl(0,0,.02)); 68 | } 69 | 70 | /////////////////////////////////////////////////////////////////////////////// 71 | function gameRenderPost() 72 | { 73 | // use built in image font for text 74 | const font = new FontImage; 75 | font.drawText('Score: ' + score, cameraPos.add(vec2(0,9.7)), .15, true); 76 | if (!brickCount) 77 | font.drawText('You Win!', cameraPos.add(vec2(0,-5)), .2, true); 78 | else if (!ball) 79 | font.drawText('Click to Play', cameraPos.add(vec2(0,-5)), .2, true); 80 | } 81 | 82 | /////////////////////////////////////////////////////////////////////////////// 83 | // an example shader that can be used to apply a post processing effect 84 | function setupPostProcess() 85 | { 86 | const televisionShader = ` 87 | // Simple TV Shader Code 88 | float hash(vec2 p) 89 | { 90 | p=fract(p*.3197); 91 | return fract(1.+sin(51.*p.x+73.*p.y)*13753.3); 92 | } 93 | float noise(vec2 p) 94 | { 95 | vec2 i=floor(p),f=fract(p),u=f*f*(3.-2.*f); 96 | return mix(mix(hash(i),hash(i+vec2(1,0)),u.x),mix(hash(i+vec2(0,1)),hash(i+1.),u.x),u.y); 97 | } 98 | void mainImage(out vec4 c, vec2 p) 99 | { 100 | // put uv in texture pixel space 101 | p /= iResolution.xy; 102 | 103 | // apply fuzz as horizontal offset 104 | const float fuzz = .0005; 105 | const float fuzzScale = 800.; 106 | const float fuzzSpeed = 9.; 107 | p.x += fuzz*(noise(vec2(p.y*fuzzScale, iTime*fuzzSpeed))*2.-1.); 108 | 109 | // init output color 110 | c = texture(iChannel0, p); 111 | 112 | // chromatic aberration 113 | const float chromatic = .002; 114 | c.r = texture(iChannel0, p - vec2(chromatic,0)).r; 115 | c.b = texture(iChannel0, p + vec2(chromatic,0)).b; 116 | 117 | // tv static noise 118 | const float staticNoise = .1; 119 | c += staticNoise * hash(p + mod(iTime, 1e3)); 120 | 121 | // scan lines 122 | const float scanlineScale = 1e3; 123 | const float scanlineAlpha = .1; 124 | c *= 1. + scanlineAlpha*sin(p.y*scanlineScale); 125 | 126 | // black vignette around edges 127 | const float vignette = 2.; 128 | const float vignettePow = 6.; 129 | float dx = 2.*p.x-1., dy = 2.*p.y-1.; 130 | c *= 1.-pow((dx*dx + dy*dy)/vignette, vignettePow); 131 | }`; 132 | 133 | const includeOverlay = true; 134 | initPostProcess(televisionShader, includeOverlay); 135 | } 136 | 137 | /////////////////////////////////////////////////////////////////////////////// 138 | // Startup LittleJS Engine 139 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/breakout/gameObjects.js: -------------------------------------------------------------------------------- 1 | /* 2 | LittleJS Breakout Objects 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | class PhysicsObject extends EngineObject 9 | { 10 | constructor(pos, size, tileInfo, angle, color) 11 | { 12 | super(pos, size, tileInfo, angle, color); 13 | this.setCollision(); // make object collide 14 | this.mass = 0; // make object have static physics 15 | } 16 | } 17 | 18 | /////////////////////////////////////////////////////////////////////////////// 19 | class Wall extends PhysicsObject 20 | { 21 | constructor(pos, size) 22 | { 23 | super(pos, size, 0, 0, new Color(0,0,0,0)); 24 | } 25 | } 26 | 27 | /////////////////////////////////////////////////////////////////////////////// 28 | class Paddle extends PhysicsObject 29 | { 30 | constructor(pos) 31 | { 32 | super(pos, vec2(5,.5)); 33 | } 34 | 35 | update() 36 | { 37 | // control with gamepad or mouse 38 | this.pos.x = isUsingGamepad ? this.pos.x + gamepadStick(0).x : mousePos.x; 39 | 40 | // keep paddle in bounds of level 41 | this.pos.x = clamp(this.pos.x, this.size.x/2, levelSize.x - this.size.x/2); 42 | } 43 | } 44 | 45 | /////////////////////////////////////////////////////////////////////////////// 46 | class Brick extends PhysicsObject 47 | { 48 | constructor(pos) 49 | { 50 | super(pos, vec2(2,1), tile(1, vec2(32,16)), 0, randColor()); 51 | ++brickCount; 52 | } 53 | 54 | collideWithObject(o) 55 | { 56 | // destroy brick when hit with ball 57 | this.destroy(); 58 | ++score; 59 | --brickCount; 60 | sound_break.play(this.pos); 61 | 62 | // make explosion effect 63 | const color1 = this.color; 64 | const color2 = color1.lerp(hsl(), .5); 65 | new ParticleEmitter( 66 | this.pos, 0, // pos, angle 67 | this.size, .1, 200, PI, // emitSize, emitTime, emitRate, emitCone 68 | tile(0, 16), // tileIndex, tileSize 69 | color1, color2, // colorStartA, colorStartB 70 | color1.scale(1,0), color2.scale(1,0), // colorEndA, colorEndB 71 | .3, .8, .3, .05, .05,// time, sizeStart, sizeEnd, speed, angleSpeed 72 | .99, .95, .4, PI, // damp, angleDamp, gravity, cone 73 | .1, .8, 0, 1 // fade, randomness, collide, additive 74 | ); 75 | 76 | // set ball trail color 77 | if (o.trailEffect) 78 | { 79 | o.trailEffect.colorStartA = this.color; 80 | o.trailEffect.colorStartB = this.color.lerp(hsl(), .5); 81 | } 82 | 83 | return 1; 84 | } 85 | } 86 | 87 | /////////////////////////////////////////////////////////////////////////////// 88 | class Ball extends PhysicsObject 89 | { 90 | constructor(pos) 91 | { 92 | super(pos, vec2(.5), tile(0)); 93 | 94 | // make a bouncy ball 95 | this.velocity = vec2(0, -.1); 96 | this.elasticity = 1; 97 | this.mass = 1; 98 | 99 | // attach a trail effect 100 | const color = hsl(0,0,.2); 101 | this.trailEffect = new ParticleEmitter( 102 | this.pos, 0, // pos, angle 103 | this.size, 0, 80, PI, // emitSize, emitTime, emitRate, emitCone 104 | tile(0, 16), // tileIndex, tileSize 105 | color, color, // colorStartA, colorStartB 106 | color.scale(0), color.scale(0), // colorEndA, colorEndB 107 | 2, .4, 1, .001, .05,// time, sizeStart, sizeEnd, speed, angleSpeed 108 | .99, .95, 0, PI, // damp, angleDamp, gravity, cone 109 | .1, .5, 0, 1 // fade, randomness, collide, additive 110 | ); 111 | this.addChild(this.trailEffect); 112 | } 113 | 114 | update() 115 | { 116 | if (this.pos.y < -1) 117 | { 118 | // destroy ball if it goes below the level 119 | ball = 0; 120 | this.destroy(); 121 | } 122 | 123 | // update physics 124 | super.update(); 125 | } 126 | 127 | collideWithObject(o) 128 | { 129 | // prevent colliding with paddle if moving upwards 130 | if (o == paddle && this.velocity.y > 0) 131 | return false; 132 | 133 | if (o == paddle) 134 | { 135 | // put english on the ball when it collides with paddle 136 | this.velocity = this.velocity.rotate(.2 * (o.pos.x - this.pos.x)); 137 | this.velocity.y = max(-this.velocity.y, .2); 138 | 139 | // speed up 140 | const speed = min(1.04*this.velocity.length(), .5); 141 | this.velocity = this.velocity.normalize(speed); 142 | sound_bounce.play(this.pos, 1, speed*2); 143 | 144 | return false; // prevent default collision code 145 | } 146 | 147 | return true; 148 | } 149 | } -------------------------------------------------------------------------------- /examples/breakout/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Breakout Game 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/breakout/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakout/tiles.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Breakout Tutorial 3 | - Shows how to make a simple breakout game 4 | - Includes sound and particles 5 | - Control with mouse or touch 6 | */ 7 | 8 | 'use strict'; 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | 12 | // globals 13 | const levelSize = vec2(38, 20); // size of play area 14 | let score = 0; // start score at 0 15 | let ball; // keep track of ball object 16 | let paddle; // keep track of player's paddle 17 | 18 | // sound effects 19 | const sound_bounce = new Sound([,,1e3,,.03,.02,1,2,,,940,.03,,,,,.2,.6,,.06], 0); 20 | const sound_break = new Sound([,,90,,.01,.03,4,,,,,,,9,50,.2,,.2,.01], 0); 21 | const sound_start = new Sound([,0,500,,.04,.3,1,2,,,570,.02,.02,,,,.04]); 22 | 23 | /////////////////////////////////////////////////////////////////////////////// 24 | 25 | class Paddle extends EngineObject 26 | { 27 | constructor() 28 | { 29 | super(vec2(0,1), vec2(6,.5)); // set object position and size 30 | this.setCollision(); // make object collide 31 | this.mass = 0; // make object have static physics 32 | } 33 | 34 | update() 35 | { 36 | this.pos.x = mousePos.x; // move paddle to mouse 37 | 38 | // clamp paddle to level size 39 | this.pos.x = clamp(this.pos.x, this.size.x/2, levelSize.x - this.size.x/2); 40 | } 41 | } 42 | 43 | class Ball extends EngineObject 44 | { 45 | constructor(pos) 46 | { 47 | super(pos, vec2(.5)); // set object position and size 48 | 49 | this.velocity = vec2(-.1, -.1); // give ball some movement 50 | this.setCollision(); // make object collide 51 | this.elasticity = 1; // make object bounce 52 | } 53 | collideWithObject(o) 54 | { 55 | // prevent colliding with paddle if moving upwards 56 | if (o == paddle && this.velocity.y > 0) 57 | return false; 58 | 59 | // speed up 60 | const speed = min(1.04*this.velocity.length(), .5); 61 | this.velocity = this.velocity.normalize(speed); 62 | 63 | // play bounce sound with pitch scaled by speed 64 | sound_bounce.play(this.pos, 1, speed); 65 | 66 | if (o == paddle) 67 | { 68 | // control bounce angle when ball collides with paddle 69 | const deltaX = o.pos.x - this.pos.x; 70 | this.velocity = this.velocity.rotate(.3 * deltaX); 71 | 72 | // make sure ball is moving upwards with a minimum speed 73 | this.velocity.y = max(-this.velocity.y, .2); 74 | 75 | // prevent default collision code 76 | return false; 77 | } 78 | 79 | return true; // allow object to collide 80 | } 81 | } 82 | 83 | class Wall extends EngineObject 84 | { 85 | constructor(pos, size) 86 | { 87 | super(pos, size); // set object position and size 88 | 89 | this.setCollision(); // make object collide 90 | this.mass = 0; // make object have static physics 91 | this.color = new Color(0,0,0,0); // make object invisible 92 | } 93 | } 94 | 95 | class Brick extends EngineObject 96 | { 97 | constructor(pos, size) 98 | { 99 | super(pos, size); 100 | 101 | this.setCollision(); // make object collide 102 | this.mass = 0; // make object have static physics 103 | this.color = randColor(); // give brick a random color 104 | } 105 | 106 | collideWithObject(o) 107 | { 108 | this.destroy(); // destroy block when hit 109 | sound_break.play(this.pos); // play brick break sound 110 | ++score; // award a point for each brick broke 111 | 112 | // create explosion effect 113 | const color = this.color; 114 | new ParticleEmitter( 115 | this.pos, 0, // pos, angle 116 | this.size, .1, 200, PI, // emitSize, emitTime, emitRate, emitCone 117 | undefined, // tileInfo 118 | color, color, // colorStartA, colorStartB 119 | color.scale(1,0), color.scale(1,0), // colorEndA, colorEndB 120 | .2, .5, 1, .1, .1, // time, sizeStart, sizeEnd, speed, angleSpeed 121 | .99, .95, .4, PI, // damp, angleDamp, gravity, cone 122 | .1, .5, false, true // fade, randomness, collide, additive 123 | ); 124 | 125 | return true; // allow object to collide 126 | } 127 | } 128 | 129 | /////////////////////////////////////////////////////////////////////////////// 130 | function gameInit() 131 | { 132 | // create bricks 133 | for(let x=2; x<=levelSize.x-2; x+=2) 134 | for(let y=12; y<=levelSize.y-2; y+=1) 135 | new Brick(vec2(x,y), vec2(2,1)); // create a brick 136 | 137 | cameraPos = levelSize.scale(.5); // center camera in level 138 | canvasFixedSize = vec2(1280, 720); // use a 720p fixed size canvas 139 | 140 | paddle = new Paddle; // create player's paddle 141 | 142 | // create walls 143 | new Wall(vec2(-.5,levelSize.y/2), vec2(1,100)) // top 144 | new Wall(vec2(levelSize.x+.5,levelSize.y/2), vec2(1,100)) // left 145 | new Wall(vec2(levelSize.x/2,levelSize.y+.5), vec2(100,1)) // right 146 | } 147 | 148 | /////////////////////////////////////////////////////////////////////////////// 149 | function gameUpdate() 150 | { 151 | if (ball && ball.pos.y < -1) // if ball is below level 152 | { 153 | // destroy old ball 154 | ball.destroy(); 155 | ball = 0; 156 | } 157 | if (!ball && mouseWasPressed(0)) // if there is no ball and left mouse is pressed 158 | { 159 | ball = new Ball(cameraPos); // create a ball 160 | sound_start.play(); // play start sound 161 | } 162 | } 163 | 164 | /////////////////////////////////////////////////////////////////////////////// 165 | function gameUpdatePost() 166 | { 167 | } 168 | 169 | /////////////////////////////////////////////////////////////////////////////// 170 | function gameRender() 171 | { 172 | drawRect(cameraPos, vec2(100), new Color(.5,.5,.5)); // draw background 173 | drawRect(cameraPos, levelSize, new Color(.1,.1,.1)); // draw level boundary 174 | } 175 | 176 | /////////////////////////////////////////////////////////////////////////////// 177 | function gameRenderPost() 178 | { 179 | drawTextScreen("Score " + score, vec2(mainCanvasSize.x/2, 70), 50); // show score 180 | } 181 | 182 | /////////////////////////////////////////////////////////////////////////////// 183 | // Startup LittleJS Engine 184 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost); -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/1.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/10.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/11.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/2.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/3.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/4.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/5.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/6.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/7.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/8.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/breakoutTutorial/images/9.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Breakout Tutorial 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/empty/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Hello World Demo 3 | - Just prints "Hello World!" 4 | - A good starting point for new projects 5 | */ 6 | 7 | 'use strict'; 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | function gameInit() 11 | { 12 | // called once after the engine starts up 13 | // setup the game 14 | } 15 | 16 | /////////////////////////////////////////////////////////////////////////////// 17 | function gameUpdate() 18 | { 19 | // called every frame at 60 frames per second 20 | // handle input and update the game state 21 | } 22 | 23 | /////////////////////////////////////////////////////////////////////////////// 24 | function gameUpdatePost() 25 | { 26 | // called after physics and objects are updated 27 | // setup camera and prepare for render 28 | } 29 | 30 | /////////////////////////////////////////////////////////////////////////////// 31 | function gameRender() 32 | { 33 | // called before objects are rendered 34 | // draw any background effects that appear behind objects 35 | } 36 | 37 | /////////////////////////////////////////////////////////////////////////////// 38 | function gameRenderPost() 39 | { 40 | // called after objects are rendered 41 | // draw effects or hud that appear above all objects 42 | drawTextScreen('Hello World!', mainCanvasSize.scale(.5), 80); 43 | } 44 | 45 | /////////////////////////////////////////////////////////////////////////////// 46 | // Startup LittleJS Engine 47 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/empty/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Hello World Demo 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/empty/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/empty/tiles.png -------------------------------------------------------------------------------- /examples/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/favicon.png -------------------------------------------------------------------------------- /examples/htmlMenu/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS HTML Menu Example Project 3 | - Setup a simple html menu system 4 | - Menu can be toggled 5 | - Pauses game when menu is visible 6 | - Shows several input types 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // show the LittleJS splash screen 12 | setShowSplashScreen(true); 13 | 14 | // sound effects 15 | const sound_click = new Sound([1,.5]); 16 | 17 | /////////////////////////////////////////////////////////////////////////////// 18 | function gameInit() 19 | { 20 | // show menu for demo 21 | setMenuVisible(true); 22 | } 23 | 24 | /////////////////////////////////////////////////////////////////////////////// 25 | function gameUpdate() 26 | { 27 | // play sound when mouse is pressed 28 | if (mouseWasPressed(0)) 29 | sound_click.play(mousePos); 30 | } 31 | 32 | /////////////////////////////////////////////////////////////////////////////// 33 | function gameUpdatePost() 34 | { 35 | // pause game when menu is visible 36 | const menuVisible = getMenuVisible(); 37 | paused = menuVisible; 38 | 39 | // toggle menu visibility 40 | if (keyWasPressed('KeyM')) 41 | setMenuVisible(!menuVisible); 42 | } 43 | 44 | /////////////////////////////////////////////////////////////////////////////// 45 | function gameRender() 46 | { 47 | // test game rendering 48 | drawRect(vec2(), vec2(1e3), hsl(0,0,.2)); 49 | for(let i=0; i<1e3; ++i) 50 | { 51 | const pos = vec2(30*Math.sin(i+time/9),20*Math.sin(i*i+time/9)); 52 | drawTile(pos, vec2(2), tile(3,128), hsl(i/9,1,.4), time+i, !(i%2), hsl(i/9,1,.1,0)); 53 | } 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | function gameRenderPost() 58 | { 59 | // draw to overlay canvas for hud rendering 60 | drawTextScreen('LittleJS HTML Menu Example\nM = Toggle menu', 61 | vec2(mainCanvasSize.x/2, 70), 60, // position, size 62 | hsl(0,0,1), 6, hsl(0,0,0)); // color, outline size and color 63 | } 64 | 65 | /////////////////////////////////////////////////////////////////////////////// 66 | // Startup LittleJS Engine 67 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/htmlMenu/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS HTML Menu Example 3 | 4 | 5 | 6 | 7 | 29 | 30 | 31 | 32 | 33 |
34 | 42 |
43 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/htmlMenu/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/htmlMenu/tiles.png -------------------------------------------------------------------------------- /examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/logo.png -------------------------------------------------------------------------------- /examples/module/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Module Demo 3 | - A simple starter project 4 | - Shows how to use LittleJS with modules 5 | */ 6 | 7 | 'use strict'; 8 | 9 | // import module 10 | import * as LittleJS from '../../dist/littlejs.esm.js'; 11 | const {tile, vec2, hsl} = LittleJS; 12 | 13 | // show the LittleJS splash screen 14 | LittleJS.setShowSplashScreen(true); 15 | 16 | // fix texture bleeding by shrinking tile slightly 17 | LittleJS.setTileFixBleedScale(.5); 18 | 19 | // sound effects 20 | const sound_click = new LittleJS.Sound([1,.5]); 21 | 22 | // medals 23 | const medal_example = new LittleJS.Medal(0, 'Example Medal', 'Welcome to LittleJS!'); 24 | LittleJS.medalsInit('Hello World'); 25 | 26 | // game variables 27 | let particleEmitter; 28 | 29 | /////////////////////////////////////////////////////////////////////////////// 30 | function gameInit() 31 | { 32 | // create tile collision and visible tile layer 33 | const tileCollisionSize = vec2(32, 16); 34 | LittleJS.initTileCollision(tileCollisionSize); 35 | const pos = vec2(); 36 | const tileLayer = new LittleJS.TileLayer(pos, tileCollisionSize); 37 | 38 | // get level data from the tiles image 39 | const mainContext = LittleJS.mainContext; 40 | const tileImage = LittleJS.textureInfos[0].image; 41 | mainContext.drawImage(tileImage, 0, 0); 42 | const imageData = mainContext.getImageData(0,0,tileImage.width,tileImage.height).data; 43 | for (pos.x = tileCollisionSize.x; pos.x--;) 44 | for (pos.y = tileCollisionSize.y; pos.y--;) 45 | { 46 | // check if this pixel is set 47 | const i = pos.x + tileImage.width*(15 + tileCollisionSize.y - pos.y); 48 | if (!imageData[4*i]) 49 | continue; 50 | 51 | // set tile data 52 | const tileIndex = 1; 53 | const direction = LittleJS.randInt(4) 54 | const mirror = !LittleJS.randInt(2); 55 | const color = LittleJS.randColor(); 56 | const data = new LittleJS.TileLayerData(tileIndex, direction, mirror, color); 57 | tileLayer.setData(pos, data); 58 | LittleJS.setTileCollisionData(pos, 1); 59 | } 60 | 61 | // draw tile layer with new data 62 | tileLayer.redraw(); 63 | 64 | // move camera to center of collision 65 | LittleJS.setCameraPos(tileCollisionSize.scale(.5)); 66 | LittleJS.setCameraScale(48); 67 | 68 | // enable gravity 69 | LittleJS.setGravity(-.01); 70 | 71 | // create particle emitter 72 | particleEmitter = new LittleJS.ParticleEmitter( 73 | vec2(16,9), 0, // emitPos, emitAngle 74 | 1, 0, 500, Math.PI, // emitSize, emitTime, emitRate, emitCone 75 | tile(0, 16), // tileIndex, tileSize 76 | hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB 77 | hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB 78 | 2, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 79 | .99, 1, 1, Math.PI, // damping, angleDamping, gravityScale, cone 80 | .05, .5, true, true // fadeRate, randomness, collide, additive 81 | ); 82 | particleEmitter.elasticity = .3; // bounce when it collides 83 | particleEmitter.trailScale = 2; // stretch in direction of motion 84 | } 85 | 86 | /////////////////////////////////////////////////////////////////////////////// 87 | function gameUpdate() 88 | { 89 | if (LittleJS.mouseWasPressed(0)) 90 | { 91 | // play sound when mouse is pressed 92 | sound_click.play(LittleJS.mousePos); 93 | 94 | // change particle color and set to fade out 95 | particleEmitter.colorStartA = hsl(); 96 | particleEmitter.colorStartB = LittleJS.randColor(); 97 | particleEmitter.colorEndA = particleEmitter.colorStartA.scale(1,0); 98 | particleEmitter.colorEndB = particleEmitter.colorStartB.scale(1,0); 99 | 100 | // unlock medals 101 | medal_example.unlock(); 102 | } 103 | 104 | // move particles to mouse location if on screen 105 | if (LittleJS.mousePosScreen.x) 106 | particleEmitter.pos = LittleJS.mousePos; 107 | } 108 | 109 | /////////////////////////////////////////////////////////////////////////////// 110 | function gameUpdatePost() 111 | { 112 | 113 | } 114 | 115 | /////////////////////////////////////////////////////////////////////////////// 116 | function gameRender() 117 | { 118 | // draw a grey square in the background without using webgl 119 | LittleJS.drawRect(vec2(16,8), vec2(20,14), hsl(0,0,.6), 0, false); 120 | 121 | // draw the logo as a tile 122 | LittleJS.drawTile(vec2(21,5), vec2(4.5), tile(3,128)); 123 | } 124 | 125 | /////////////////////////////////////////////////////////////////////////////// 126 | function gameRenderPost() 127 | { 128 | // draw to overlay canvas for hud rendering 129 | LittleJS.drawTextOverlay('LittleJS with Modules', vec2(LittleJS.mainCanvasSize.x/2, 80), 80); 130 | } 131 | 132 | /////////////////////////////////////////////////////////////////////////////// 133 | // Startup LittleJS Engine 134 | LittleJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/module/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Module Demo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/module/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/module/tiles.png -------------------------------------------------------------------------------- /examples/particles/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/particles/tiles.png -------------------------------------------------------------------------------- /examples/platformer/data/gameLevelData.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/platformer/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Platforming Game 3 | - A basic platforming starter project 4 | - Platforming physics and controls 5 | - Includes destructible terrain 6 | - Control with keyboard, mouse, touch, or gamepad 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // show the LittleJS splash screen 12 | setShowSplashScreen(true); 13 | 14 | let spriteAtlas, score, deaths; 15 | 16 | // enable touch gamepad on touch devices 17 | touchGamepadEnable = true; 18 | 19 | /////////////////////////////////////////////////////////////////////////////// 20 | function gameInit() 21 | { 22 | // engine settings 23 | gravity = -.01; 24 | objectDefaultDamping = .99; 25 | objectDefaultAngleDamping = .99; 26 | cameraScale = 4*16; 27 | 28 | // create a table of all sprites 29 | const gameTile = (i, size=16)=> tile(i, size, 0, 1); 30 | spriteAtlas = 31 | { 32 | // large tiles 33 | circle: gameTile(0), 34 | crate: gameTile(1), 35 | player: gameTile(2), 36 | enemy: gameTile(4), 37 | coin: gameTile(5), 38 | 39 | // small tiles 40 | gun: gameTile(vec2(0,2),8), 41 | grenade: gameTile(vec2(1,2),8), 42 | }; 43 | 44 | // setup level 45 | buildLevel(); 46 | 47 | // init game 48 | score = deaths = 0; 49 | } 50 | 51 | /////////////////////////////////////////////////////////////////////////////// 52 | function gameUpdate() 53 | { 54 | // respawn player 55 | if (player.deadTimer > 1) 56 | { 57 | player = new Player(playerStartPos); 58 | player.velocity = vec2(0,.1); 59 | sound_jump.play(); 60 | } 61 | 62 | // mouse wheel = zoom 63 | cameraScale = clamp(cameraScale*(1-mouseWheel/10), 1, 1e3); 64 | 65 | // T = drop test crate 66 | if (keyWasPressed('KeyT')) 67 | new Crate(mousePos); 68 | 69 | // E = drop enemy 70 | if (keyWasPressed('KeyE')) 71 | new Enemy(mousePos); 72 | 73 | // X = make explosion 74 | if (keyWasPressed('KeyX')) 75 | explosion(mousePos); 76 | 77 | // M = move player to mouse 78 | if (keyWasPressed('KeyM')) 79 | player.pos = mousePos; 80 | 81 | // R = restart level 82 | if (keyWasPressed('KeyR')) 83 | buildLevel(); 84 | } 85 | 86 | /////////////////////////////////////////////////////////////////////////////// 87 | function getCameraTarget() 88 | { 89 | // camera is above player 90 | const offset = 200/cameraScale*percent(mainCanvasSize.y, 300, 600); 91 | return player.pos.add(vec2(0, offset)); 92 | } 93 | 94 | /////////////////////////////////////////////////////////////////////////////// 95 | function gameUpdatePost() 96 | { 97 | // update camera 98 | cameraPos = cameraPos.lerp(getCameraTarget(), clamp(player.getAliveTime()/2)); 99 | } 100 | 101 | /////////////////////////////////////////////////////////////////////////////// 102 | function gameRender() 103 | { 104 | } 105 | 106 | /////////////////////////////////////////////////////////////////////////////// 107 | function gameRenderPost() 108 | { 109 | // draw to overlay canvas for hud rendering 110 | const drawText = (text, x, y, size=40) => 111 | { 112 | overlayContext.textAlign = 'center'; 113 | overlayContext.textBaseline = 'top'; 114 | overlayContext.font = size + 'px arial'; 115 | overlayContext.fillStyle = '#fff'; 116 | overlayContext.lineWidth = 3; 117 | overlayContext.strokeText(text, x, y); 118 | overlayContext.fillText(text, x, y); 119 | } 120 | drawText('Score: ' + score, overlayCanvas.width*1/4, 20); 121 | drawText('Deaths: ' + deaths, overlayCanvas.width*3/4, 20); 122 | } 123 | 124 | /////////////////////////////////////////////////////////////////////////////// 125 | // Startup LittleJS Engine 126 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png', 'tilesLevel.png']); -------------------------------------------------------------------------------- /examples/platformer/gameLevel.js: -------------------------------------------------------------------------------- 1 | /* 2 | LittleJS Platformer Example - Level Generator 3 | - Procedurally generates level geometry 4 | - Picks colors for the level and background 5 | - Creates ladders and spawns enemies and crates 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const tileType_ladder = -1; 11 | const tileType_empty = 0; 12 | const tileType_solid = 1; 13 | const tileType_breakable = 2; 14 | 15 | let player, playerStartPos, tileLayers, foregroundLayerIndex, sky; 16 | let levelSize, levelColor, levelBackgroundColor, levelOutlineColor; 17 | 18 | function buildLevel() 19 | { 20 | // destroy all objects 21 | engineObjectsDestroy(); 22 | 23 | // create the level 24 | levelColor = randColor(hsl(0,0,.2), hsl(0,0,.8)); 25 | levelBackgroundColor = levelColor.mutate().scale(.4,1); 26 | levelOutlineColor = levelColor.mutate().add(hsl(0,0,.4)).clamp(); 27 | loadLevel(); 28 | 29 | // create sky object with gradient and stars 30 | sky = new Sky; 31 | 32 | // create parallax layers 33 | for (let i=3; i--;) 34 | new ParallaxLayer(i); 35 | 36 | // spawn player 37 | player = new Player(playerStartPos); 38 | cameraPos = getCameraTarget(); 39 | } 40 | 41 | function loadLevel(level=0) 42 | { 43 | // load level data from an exported Tiled js file 44 | const tileMapData = TileMaps['gameLevelData']; 45 | levelSize = vec2(tileMapData.width, tileMapData.height); 46 | initTileCollision(levelSize); 47 | 48 | // create table for tiles in the level tilemap 49 | const tileLookup = 50 | { 51 | circle: 1, 52 | ground: 2, 53 | ladder: 4, 54 | metal: 5, 55 | player: 17, 56 | crate: 18, 57 | enemy: 19, 58 | coin: 20, 59 | } 60 | 61 | // set all level data tiles 62 | tileLayers = []; 63 | playerStartPos = vec2(1, levelSize.y); 64 | const layerCount = tileMapData.layers.length; 65 | foregroundLayerIndex = layerCount-1; 66 | for (let layer=layerCount; layer--;) 67 | { 68 | const layerData = tileMapData.layers[layer].data; 69 | const tileLayer = new TileLayer(vec2(), levelSize, tile(0,16,1)); 70 | tileLayer.renderOrder = -1e3+layer; 71 | tileLayers[layer] = tileLayer; 72 | 73 | for (let x=levelSize.x; x--;) 74 | for (let y=levelSize.y; y--;) 75 | { 76 | const pos = vec2(x,levelSize.y-1-y); 77 | const tile = layerData[y*levelSize.x+x]; 78 | 79 | if (tile >= tileLookup.player) 80 | { 81 | // create object instead of tile 82 | const objectPos = pos.add(vec2(.5)); 83 | if (tile == tileLookup.player) 84 | playerStartPos = objectPos; 85 | if (tile == tileLookup.crate) 86 | new Crate(objectPos); 87 | if (tile == tileLookup.enemy) 88 | new Enemy(objectPos); 89 | if (tile == tileLookup.coin) 90 | new Coin(objectPos); 91 | continue; 92 | } 93 | 94 | // get tile type for special tiles 95 | let tileType = tileType_empty; 96 | if (tile > 0) 97 | tileType = tileType_breakable; 98 | if (tile == tileLookup.ladder) 99 | tileType = tileType_ladder; 100 | if (tile == tileLookup.metal) 101 | tileType = tileType_solid; 102 | if (tileType) 103 | { 104 | // set collision for solid tiles 105 | if (layer == foregroundLayerIndex) 106 | setTileCollisionData(pos, tileType); 107 | 108 | // randomize tile appearance 109 | let direction, mirror, color; 110 | if (tileType == tileType_breakable) 111 | { 112 | direction = randInt(4); 113 | mirror = randInt(2); 114 | color = layer ? levelColor : levelBackgroundColor; 115 | color = color.mutate(.03); 116 | } 117 | 118 | // set tile layer data 119 | const data = new TileLayerData(tile-1, direction, mirror, color); 120 | tileLayer.setData(pos, data); 121 | } 122 | } 123 | tileLayer.redraw(); 124 | } 125 | 126 | // apply decoration to all level tiles 127 | const pos = vec2(); 128 | for (let layer=layerCount; layer--;) 129 | for (pos.x=levelSize.x; pos.x--;) 130 | for (pos.y=levelSize.y; pos.y--;) 131 | decorateTile(pos, layer); 132 | } 133 | 134 | function decorateTile(pos, layer=1) 135 | { 136 | ASSERT((pos.x|0) == pos.x && (pos.y|0)== pos.y); 137 | const tileLayer = tileLayers[layer]; 138 | 139 | if (layer == foregroundLayerIndex) 140 | { 141 | const tileType = getTileCollisionData(pos); 142 | if (tileType <= 0) 143 | { 144 | // force it to clear if it is empty 145 | tileType || tileLayer.setData(pos, new TileLayerData, 1); 146 | return; 147 | } 148 | if (tileType == tileType_breakable) 149 | for (let i=4;i--;) 150 | { 151 | // outline towards neighbors of differing type 152 | const neighborTileType = getTileCollisionData(pos.add(vec2().setDirection(i))); 153 | if (neighborTileType == tileType) 154 | continue; 155 | 156 | // make pixel perfect outlines 157 | const size = i&1 ? vec2(2, 16) : vec2(16, 2); 158 | tileLayer.context.fillStyle = levelOutlineColor.mutate(.1); 159 | const drawPos = pos.scale(16) 160 | .add(vec2(i==1?14:0,(i==0?14:0))) 161 | .subtract((i&1? vec2(0,8-size.y/2) : vec2(8-size.x/2,0))); 162 | tileLayer.context.fillRect( 163 | drawPos.x, tileLayer.canvas.height - drawPos.y, size.x, -size.y); 164 | } 165 | } 166 | else 167 | { 168 | // make round corners 169 | for (let i=4; i--;) 170 | { 171 | // check corner neighbors 172 | const neighborTileDataA = tileLayer.getData(pos.add(vec2().setDirection(i))).tile; 173 | const neighborTileDataB = tileLayer.getData(pos.add(vec2().setDirection((i+1)%4))).tile; 174 | if (neighborTileDataA > 0 || neighborTileDataB > 0) 175 | continue; 176 | 177 | const directionVector = vec2().setAngle(i*PI/2+PI/4, 10).floor(); 178 | const drawPos = pos.add(vec2(.5)) // center 179 | .scale(16).add(directionVector).floor(); // direction offset 180 | 181 | // clear rect without any scaling to prevent blur from filtering 182 | const s = 2; 183 | tileLayer.context.clearRect( 184 | drawPos.x - s/2, tileLayer.canvas.height - drawPos.y - s/2, s, s); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /examples/platformer/gamePlayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | LittleJS Platformer Example - Player 3 | - Extends character class 4 | - Uses character physics system 5 | - Player can jump, shoot, roll, and throw grenades 6 | - Supports keyboard, mouse, and gamepad controls 7 | - Keeps track of player deaths 8 | */ 9 | 10 | 'use strict'; 11 | 12 | class Player extends Character 13 | { 14 | update() 15 | { 16 | // movement control 17 | this.moveInput = isUsingGamepad ? gamepadStick(0) : keyDirection(); 18 | this.holdingJump = keyIsDown('ArrowUp') || gamepadIsDown(0); 19 | this.holdingShoot = !isUsingGamepad && mouseIsDown(0) || keyIsDown('KeyZ') || gamepadIsDown(2); 20 | this.pressingThrow = keyIsDown('KeyC') || mouseIsDown(1) || gamepadIsDown(1); 21 | this.pressedDodge = keyIsDown('KeyX') || mouseIsDown(2) || gamepadIsDown(3); 22 | super.update(); 23 | } 24 | 25 | kill() 26 | { 27 | ++deaths; 28 | super.kill(); 29 | } 30 | } -------------------------------------------------------------------------------- /examples/platformer/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Platforming Game 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/platformer/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/platformer/tiles.png -------------------------------------------------------------------------------- /examples/platformer/tilesLevel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/platformer/tilesLevel.png -------------------------------------------------------------------------------- /examples/puzzle/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Puzzle Game 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/puzzle/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/puzzle/tiles.png -------------------------------------------------------------------------------- /examples/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/screenshot.jpg -------------------------------------------------------------------------------- /examples/shorts/animation.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | drawRect(vec2(0,0), vec2(100), GRAY); // draw background 4 | 5 | { 6 | // animate using multiple frames 7 | const pos = vec2(-5, 2*abs(Math.sin(time*2*PI))); 8 | const frame = (time*4)%2|0; 9 | drawTile(pos, vec2(7), tile(3).frame(frame), RED); 10 | } 11 | { 12 | // animate with stretch and squash 13 | const s = Math.sin(time*9)*.5; 14 | const size = vec2(7-s,7+s); 15 | const pos = vec2(5,size.y-7); 16 | drawTile(pos, size, tile(5), GREEN); 17 | } 18 | } -------------------------------------------------------------------------------- /examples/shorts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/shorts/blending.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | const circleTile = tile(2, 128); // get the circle tile 4 | 5 | // additive blend 6 | setBlendMode(1); 7 | drawTile(vec2(-4,-2), vec2(7), circleTile, rgb(1,0,0)); 8 | drawTile(vec2(-6, 2), vec2(7), circleTile, rgb(0,1,0)); 9 | drawTile(vec2(-8,-2), vec2(7), circleTile, rgb(0,0,1)); 10 | 11 | // alpha blend 12 | setBlendMode(0); 13 | drawTile(vec2(4,-2), vec2(7), circleTile, rgb(1,0,0,.5)); 14 | drawTile(vec2(6, 2), vec2(7), circleTile, rgb(0,1,0,.5)); 15 | drawTile(vec2(8,-2), vec2(7), circleTile, rgb(0,0,1,.5)); 16 | } -------------------------------------------------------------------------------- /examples/shorts/clock.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // draw background 4 | for(let i=12; i--;) 5 | { 6 | const a = i/6*PI; 7 | const pos = vec2(0,7).rotate(a); 8 | drawRect(pos, vec2(.5,1), hsl(i/12,1,.5), a); 9 | } 10 | 11 | // get current time 12 | const d = Date().slice(16,24); 13 | const s = d.slice(6,8)|0; 14 | const m = d.slice(3,5)|0; 15 | const h = (d.slice(0,2)|0) + m/60; 16 | 17 | // draw clock hands 18 | drawLine(vec2(0,0), vec2(0,4).rotate(h/12*2*PI), 1); 19 | drawLine(vec2(0,0), vec2(0,6).rotate(m/60*2*PI), .4); 20 | drawLine(vec2(0,0), vec2(0,8).rotate(s/60*2*PI), .1); 21 | } -------------------------------------------------------------------------------- /examples/shorts/colors.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // color constants 4 | drawRect(vec2(-8, 5), vec2(3), RED); 5 | drawRect(vec2(-4, 5), vec2(3), YELLOW); 6 | drawRect(vec2(-0, 5), vec2(3), GREEN); 7 | drawRect(vec2( 4, 5), vec2(3), BLUE); 8 | drawRect(vec2( 8, 5), vec2(3), WHITE); 9 | 10 | // rgb and hsl color 11 | drawRect(vec2(-8, 0), vec2(3), new Color(1,0,0)); 12 | drawRect(vec2(-4, 0), vec2(3), rgb(0,1,1)); 13 | drawRect(vec2(-0, 0), vec2(3), hsl(0,1,.5)); 14 | drawRect(vec2( 4, 0), vec2(3), hsl(.6,.5,.5)); 15 | drawRect(vec2( 8, 0), vec2(3), hsl(0,0,1)); 16 | 17 | // color lerpping 18 | for(let i=5; i--;) 19 | { 20 | const color1 = RED; 21 | const color2 = YELLOW; 22 | const color = color1.lerp(color2, i/4); 23 | drawRect(vec2(-8+i*4, -5), vec2(3), color); 24 | } 25 | } -------------------------------------------------------------------------------- /examples/shorts/helloWorld.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // draw text 4 | drawText('Hello World', vec2(0,2), 3); 5 | 6 | // draw a tile 7 | drawTile(vec2(0,-2), vec2(7), tile(3,128)); 8 | } -------------------------------------------------------------------------------- /examples/shorts/particles.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | gravity = -.01; // set default gravity 4 | 5 | // fire 6 | new ParticleEmitter( 7 | vec2(-5,-2), 0, // pos, angle 8 | 2, 0, 200, PI, // emitSize, emitTime, emitRate, emitCone 9 | tile(0), // tileInfo 10 | rgb(1,.5,.1), rgb(1,.1,.1), // colorStartA, colorStartB 11 | rgb(1,.5,.1,0), rgb(1,.1,.1,0), // colorEndA, colorEndB 12 | .7, 2, 0, .2, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 13 | .9, 1, -1, PI, .05, // damp, angleDamp, gravity, particleCone, fade 14 | .5, 0, 1, 0, 1e9 // randomness, collide, additive, colorLinear, renderOrder 15 | ); 16 | 17 | // smoke 18 | new ParticleEmitter( 19 | vec2(5,-2), 0, // pos, angle 20 | 3, 0, 100, PI, // emitSize, emitTime, emitRate, emitCone 21 | tile(0), // tileInfo 22 | hsl(0,0,0,.5), hsl(0,0,1,.5), // colorStartA, colorStartB 23 | hsl(0,0,0,0), hsl(0,0,1,0), // colorEndA, colorEndB 24 | 1, 1, 5, .2, .01, // time, sizeStart, sizeEnd, speed, angleSpeed 25 | .85, 1, -1, PI, .3, // damp, angleDamp, gravity, particleCone, fade 26 | .5, 0, 0, 1, 1e8 // randomness, collide, additive, colorLinear, renderOrder 27 | ); 28 | } -------------------------------------------------------------------------------- /examples/shorts/platformer.js: -------------------------------------------------------------------------------- 1 | class PhysicsObject extends EngineObject 2 | { 3 | constructor(pos, size, color) 4 | { 5 | super(pos, size, 0, 0, color); 6 | this.setCollision(); // make object collide 7 | this.mass = 0; // make object have static physics 8 | } 9 | } 10 | 11 | class Player extends PhysicsObject 12 | { 13 | constructor(pos) 14 | { 15 | super(pos, vec2(2,4), RED); 16 | this.mass = 1; // make object have dynamic physics 17 | } 18 | 19 | update() 20 | { 21 | // apply movement controls 22 | const moveInput = keyDirection(); 23 | this.velocity.x += moveInput.x * (this.groundObject ? .1: .01); 24 | if (this.groundObject && moveInput.y > 0) 25 | this.velocity.y = .9; // jump 26 | 27 | super.update(); // call parent update function 28 | cameraPos = vec2(this.pos.x, 9); // move camera with player 29 | } 30 | } 31 | 32 | function gameInit() 33 | { 34 | // setup level 35 | gravity = -.05; 36 | new Player(vec2(0,4)); 37 | 38 | // create random objects 39 | new PhysicsObject(vec2(), vec2(1e3,4), GRAY); // ground 40 | for (let i = 1; i < 500; ++i) 41 | new PhysicsObject(vec2(i*10+randInt(4), 0), vec2(2+randInt(20),4+randInt(8)), GREEN); 42 | } -------------------------------------------------------------------------------- /examples/shorts/playSound.js: -------------------------------------------------------------------------------- 1 | class SoundButton extends EngineObject 2 | { 3 | constructor(pos, icon, sound) 4 | { 5 | super(pos, vec2(5,4)); 6 | this.icon = icon; 7 | this.sound = sound; 8 | } 9 | 10 | update() 11 | { 12 | // play sound when clicked 13 | if (mouseWasPressed(0) && isOverlapping(this.pos, this.size, mousePos)) 14 | this.sound.play(this.pos); 15 | } 16 | 17 | render() 18 | { 19 | // draw the button and icon 20 | drawRect(this.pos, this.size, hsl(0,0,.8)); 21 | drawTextOverlay(this.icon, this.pos, 3); 22 | } 23 | } 24 | 25 | function gameInit() 26 | { 27 | // create a grid of buttons with different sounds 28 | new SoundButton(vec2(-6, 5),'💰', new Sound([,,1675,,.06,.24,1,1.82,,,837,.06])); 29 | new SoundButton(vec2( 0, 5),'🥊', new Sound([,,925,.04,.3,.6,1,.3,,6.27,-184,.09,.17])); 30 | new SoundButton(vec2( 6, 5),'✨', new Sound([,,539,0,.04,.29,1,1.92,,,567,.02,.02,,,,.04])); 31 | new SoundButton(vec2(-6, 0),'🐁', new Sound([,.2,1e3,.02,,.01,2,,18,,475,.01,.01])); 32 | new SoundButton(vec2( 0, 0),'🎹', new Sound([1.5,.5,270,,.1,,1,1.5,,,,,,,,.1,.01])); 33 | new SoundButton(vec2( 6, 0),'🏌️', new Sound([,,150,.05,,.05,,1.3,,,,,,3])); 34 | new SoundButton(vec2(-6,-5),'🌊', new Sound([,.2,40,.5,,1.5,,11,,,,,,199])); 35 | new SoundButton(vec2( 0,-5),'🛰️', new Sound([,.5,847,.02,.3,.9,1,1.67,,,-294,.04,.13,,,,.1])); 36 | new SoundButton(vec2( 6,-5),'⚡', new Sound([,,471,,.09,.47,4,1.06,-6.7,,,,,.9,61,.1,,.82,.1])); 37 | } -------------------------------------------------------------------------------- /examples/shorts/pong.js: -------------------------------------------------------------------------------- 1 | // globals objects 2 | let paddle, ball; 3 | 4 | class PhysicsObject extends EngineObject 5 | { 6 | constructor(pos, size) 7 | { 8 | super(pos, size); // set object position and size 9 | this.setCollision(); // make object collide 10 | this.mass = 0; // make object have static physics 11 | } 12 | } 13 | 14 | function gameInit() 15 | { 16 | // setup level 17 | const levelSize = vec2(38, 21); // size of play area 18 | cameraPos = levelSize.scale(.5); // center camera in level 19 | canvasFixedSize = vec2(1280, 720); // use a 720p fixed size canvas 20 | 21 | // create objects 22 | paddle = new PhysicsObject(vec2(0,1), vec2(6,1)); // create player's paddle 23 | new PhysicsObject(vec2(-.5,levelSize.y/2), vec2(1,100)); // left wall 24 | new PhysicsObject(vec2(levelSize.x+.5,levelSize.y/2), vec2(1,100)); // right wall 25 | new PhysicsObject(vec2(levelSize.x/2,levelSize.y+.5), vec2(100,1)); // top wall 26 | } 27 | 28 | function gameUpdate() 29 | { 30 | if (!ball || ball.pos.y < -1) // spawn ball 31 | { 32 | ball && ball.destroy(); // destroy old ball 33 | ball = new PhysicsObject(cameraPos); // create a ball 34 | ball.velocity = vec2(.2); // give ball some movement 35 | ball.elasticity = 1; // make ball bounce 36 | ball.mass = 1; // make ball have dynamic physics 37 | } 38 | 39 | paddle.pos.x = mousePos.x; // move paddle to mouse 40 | } -------------------------------------------------------------------------------- /examples/shorts/shapes.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // polygon shapes 4 | for(let j=3; j<7; ++j) 5 | { 6 | let points = []; 7 | for(let i=0; i tile(i, size); 7 | spriteAtlas = 8 | { 9 | circle: gameTile(0), 10 | crate: gameTile(1), 11 | icon: gameTile(2), 12 | circleBig: gameTile(2, 128), 13 | iconBig: gameTile(3, 128), 14 | }; 15 | } 16 | 17 | function gameRender() 18 | { 19 | drawRect(vec2(0,0), vec2(100), GRAY); // draw background 20 | 21 | // draw a sprite from the atlas 22 | let pos = vec2(Math.sin(time)*5,-3);// world position to draw 23 | let angle = 0; // world space angle to draw the tile 24 | let size = vec2(9); // world size of the tile 25 | let mirror = 0; // should tile be mirrored? 26 | let color = hsl(0,0,1); // color to multiply the tile by 27 | let additiveColor = hsl(0,0,0,0); // color to add to 28 | drawTile(pos, size, spriteAtlas.iconBig, color, angle, mirror, additiveColor); 29 | 30 | // draw more sprites from the atlas 31 | drawTile(vec2(-7,4), vec2(5), spriteAtlas.crate); 32 | drawTile(vec2( 0,4), vec2(5), spriteAtlas.circle); 33 | drawTile(vec2( 7,4), vec2(5), spriteAtlas.circleBig); 34 | } -------------------------------------------------------------------------------- /examples/shorts/systemFont.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // draw text with built in font image system font 4 | const font = new FontImage; 5 | font.drawText('System Font Test', cameraPos.add(vec2(0,3)), .2, true); 6 | 7 | // show every character in the system font 8 | let s = ''; 9 | for(let i=32; i<128; ++i) 10 | { 11 | if (i%32 == 0) 12 | s += '\n'; 13 | s += String.fromCharCode(i); 14 | } 15 | font.drawText(s, cameraPos, .1, true); 16 | } -------------------------------------------------------------------------------- /examples/shorts/texture.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // show the full texture 4 | let pos = vec2(0,0); // world position to draw 5 | let size = vec2(15); // world size of the tile 6 | let color = hsl(0,0,1); // color to multiply the tile by 7 | let tilePos = vec2(0,0); // top left corner of tile in pixels 8 | let tileSize = vec2(256); // size of tile in pixels 9 | let tileInfo = new TileInfo(tilePos, tileSize); // tile info 10 | drawRect(pos, size, GRAY); // draw background 11 | drawTile(pos, size, tileInfo, color); 12 | } -------------------------------------------------------------------------------- /examples/shorts/tileLayer.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | cameraPos = vec2(16); // setup camera 4 | gravity = -.01; // enable gravity 5 | 6 | // create tile collision and visible tile layer 7 | initTileCollision(vec2(32)); 8 | const pos = vec2(); 9 | const tileLayer = new TileLayer(pos, tileCollisionSize); 10 | 11 | // init the tile layer 12 | for (pos.x = tileCollisionSize.x; pos.x--;) 13 | for (pos.y = tileCollisionSize.y; pos.y--;) 14 | { 15 | // check if tile should be solid 16 | if (rand() < .7) 17 | continue; 18 | 19 | // set tile data 20 | const tileIndex = 1; 21 | const direction = randInt(4) 22 | const mirror = !randInt(2); 23 | const color = randColor(WHITE, hsl(0,0,.2)); 24 | const data = new TileLayerData(tileIndex, direction, mirror, color); 25 | tileLayer.setData(pos, data); 26 | setTileCollisionData(pos, 1); 27 | } 28 | tileLayer.redraw(); // redraw tile layer with new data 29 | } 30 | 31 | function gameUpdate() 32 | { 33 | if (mouseWasPressed(0)) 34 | { 35 | // create particle emitter 36 | const hue = rand(); 37 | const particleEmitter = new ParticleEmitter( 38 | mousePos, 0, // emitPos, emitAngle 39 | 0, 0.1, 500, PI, // emitSize, emitTime, emitRate, emitCone 40 | tile(0, 16), // tileIndex, tileSize 41 | hsl(hue,1,.5), hsl(hue,1,1), // colorStartA, colorStartB 42 | hsl(hue,1,.5,0), hsl(hue,1,1,0), // colorEndA, colorEndB 43 | 2, .2, .2, .2, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 44 | .99, 1, 1, PI, // damping, angleDamping, gravityScale, cone 45 | .05, .8, true, false // fadeRate, randomness, collide, additive 46 | ); 47 | particleEmitter.elasticity = .5; // bounce when it collides 48 | particleEmitter.trailScale = 2; // stretch in direction of motion 49 | } 50 | } -------------------------------------------------------------------------------- /examples/shorts/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/shorts/tiles.png -------------------------------------------------------------------------------- /examples/shorts/tiltedView.js: -------------------------------------------------------------------------------- 1 | class GameObject extends EngineObject 2 | { 3 | update() 4 | { 5 | this.renderOrder = -this.pos.y; // sort by y position 6 | super.update(); 7 | } 8 | 9 | render() 10 | { 11 | // adjust draw postion to be at the bottom of the object 12 | const pos = this.pos.add(vec2(0,this.size.y/2).rotate(this.angle)); 13 | drawTile(pos, this.size, this.tileInfo, this.color, this.angle); 14 | } 15 | } 16 | 17 | class Player extends GameObject 18 | { 19 | update() 20 | { 21 | // apply movement controls 22 | const moveInput = keyDirection().clampLength(1).scale(.2); // clamp and scale input 23 | this.velocity = this.velocity.add(moveInput); // apply movement 24 | 25 | super.update(); // call parent update function 26 | cameraPos = this.pos.add(vec2(0,2)); // move camera with player 27 | } 28 | } 29 | 30 | 31 | function gameInit() 32 | { 33 | // setup level 34 | objectDefaultDamping = .7; 35 | new Player(vec2(), vec2(3), tile(5), 0, RED); 36 | 37 | // create background objects 38 | for (let i = 1; i < 300; ++i) 39 | new EngineObject(randInCircle(90), vec2(rand(59),rand(59)), 0, 0, hsl(.4,.3,rand(.1,.2),.8), -1e5); 40 | 41 | // create world objects 42 | for (let i = 1; i < 1e3; ++i) 43 | new GameObject(randInCircle(7+i,7), vec2(rand(1,2),rand(4,9)), 0, 0, hsl(.1,.5,rand(.2,.3))); 44 | } -------------------------------------------------------------------------------- /examples/shorts/timers.js: -------------------------------------------------------------------------------- 1 | class TimerButton extends EngineObject 2 | { 3 | constructor(pos, time) 4 | { 5 | super(pos, vec2(5.5)); 6 | this.time = time; 7 | this.timer = new Timer; 8 | } 9 | 10 | update() 11 | { 12 | // start or stop the timer when clicked 13 | if (mouseWasPressed(0) && isOverlapping(this.pos, this.size, mousePos)) 14 | this.timer.isSet() ? this.timer.unset() : this.timer.set(this.time); 15 | } 16 | 17 | render() 18 | { 19 | // get color based on timer state 20 | this.color = this.timer.isSet() ? this.timer.active() ? BLUE : RED : GRAY; 21 | 22 | // draw the button and timer text 23 | drawRect(this.pos, this.size, this.color); 24 | if (this.timer.isSet()) 25 | { 26 | drawTextOverlay(this.timer.get().toFixed(1), this.pos.add(vec2(0,1)), 2); 27 | const percent = this.timer.getPercent()*100|0; 28 | drawTextOverlay(percent+'%', this.pos.add(vec2(0,-1)), 2); 29 | } 30 | else 31 | drawTextOverlay('Click\nto set', this.pos, 2); 32 | } 33 | } 34 | 35 | function gameInit() 36 | { 37 | // create some timer buttons 38 | new TimerButton(vec2(-5, 0), 3); 39 | new TimerButton(vec2( 5, 0), 0); 40 | } -------------------------------------------------------------------------------- /examples/shorts/topDown.js: -------------------------------------------------------------------------------- 1 | class PhysicsObject extends EngineObject 2 | { 3 | constructor(pos, size, color) 4 | { 5 | super(pos, size, 0, 0, color); 6 | this.setCollision(); // make object collide 7 | this.mass = 0; // make object have static physics 8 | } 9 | } 10 | 11 | class Player extends PhysicsObject 12 | { 13 | constructor(pos) 14 | { 15 | super(pos, vec2(2), RED); 16 | this.mass = 1; // make object have dynamic physics 17 | this.renderOrder = 1; // render player on top of other objects 18 | } 19 | 20 | update() 21 | { 22 | // apply movement controls 23 | const moveInput = keyDirection().clampLength(1).scale(.2); // clamp and scale input 24 | this.velocity = this.velocity.add(moveInput); // apply movement 25 | 26 | super.update(); // call parent update function 27 | cameraPos = this.pos; // move camera with player 28 | } 29 | } 30 | 31 | function gameInit() 32 | { 33 | // setup level 34 | objectDefaultDamping = .7; 35 | new Player(); 36 | 37 | // create background objects 38 | for (let i = 1; i < 300; ++i) 39 | new EngineObject(randInCircle(90), vec2(rand(59),rand(59)), 0, rand(), hsl(.4,.5,rand(.2))); 40 | 41 | // create foreground objects 42 | for (let i = 1; i < 300; ++i) 43 | new PhysicsObject(randInCircle(7+i,7), vec2(rand(4,9),rand(4,9)), hsl(0,0,rand(.8,1))); 44 | } -------------------------------------------------------------------------------- /examples/starter/build.bat: -------------------------------------------------------------------------------- 1 | rem LittleJS Build Script 2 | call node build.js -------------------------------------------------------------------------------- /examples/starter/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * LittleJS Build System 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const PROGRAM_TITLE = 'Little JS Starter Project'; 10 | const PROGRAM_NAME = 'game'; 11 | const BUILD_FOLDER = 'build'; 12 | const sourceFiles = 13 | [ 14 | '../../dist/littlejs.release.js', 15 | 'game.js', 16 | // add your game's files here 17 | ]; 18 | const dataFiles = 19 | [ 20 | 'tiles.png', 21 | // add your game's data files here 22 | ]; 23 | 24 | console.log(`Building ${PROGRAM_NAME}...`); 25 | const startTime = Date.now(); 26 | const fs = require('node:fs'); 27 | const child_process = require('node:child_process'); 28 | 29 | // rebuild engine 30 | //child_process.execSync(`npm run build`, { stdio: 'inherit' }); 31 | 32 | // remove old files and setup build folder 33 | fs.rmSync(BUILD_FOLDER, { recursive: true, force: true }); 34 | fs.rmSync(`${PROGRAM_NAME}.zip`, { force: true }); 35 | fs.mkdirSync(BUILD_FOLDER); 36 | 37 | // copy data files 38 | for(const file of dataFiles) 39 | fs.copyFileSync(file, `${BUILD_FOLDER}/${file}`); 40 | 41 | Build 42 | ( 43 | `${BUILD_FOLDER}/index.js`, 44 | sourceFiles, 45 | [closureCompilerStep, uglifyBuildStep, htmlBuildStep, zipBuildStep] 46 | ); 47 | 48 | console.log(''); 49 | console.log(`Build Completed in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`); 50 | console.log(`Size of ${PROGRAM_NAME}.zip: ${fs.statSync(`${PROGRAM_NAME}.zip`).size} bytes`); 51 | 52 | /////////////////////////////////////////////////////////////////////////////// 53 | 54 | // A single build with its own source files, build steps, and output file 55 | // - each build step is a callback that accepts a single filename 56 | function Build(outputFile, files=[], buildSteps=[]) 57 | { 58 | // copy files into a buffer 59 | let buffer = ''; 60 | for (const file of files) 61 | buffer += fs.readFileSync(file) + '\n'; 62 | 63 | // output file 64 | fs.writeFileSync(outputFile, buffer, {flag: 'w+'}); 65 | 66 | // execute build steps in order 67 | for (const buildStep of buildSteps) 68 | buildStep(outputFile); 69 | } 70 | 71 | function closureCompilerStep(filename) 72 | { 73 | console.log(`Running closure compiler...`); 74 | 75 | const filenameTemp = filename + '.tmp'; 76 | fs.copyFileSync(filename, filenameTemp); 77 | child_process.execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --compilation_level=ADVANCED --warning_level=VERBOSE --jscomp_off=* --assume_function_wrapper`, {stdio: 'inherit'}); 78 | fs.rmSync(filenameTemp); 79 | }; 80 | 81 | function uglifyBuildStep(filename) 82 | { 83 | console.log(`Running uglify...`); 84 | child_process.execSync(`npx uglifyjs ${filename} -c -m -o ${filename}`, {stdio: 'inherit'}); 85 | }; 86 | 87 | function htmlBuildStep(filename) 88 | { 89 | console.log(`Building html...`); 90 | 91 | // create html file 92 | let buffer = ''; 93 | buffer += ''; 94 | buffer += `${PROGRAM_TITLE}`; 95 | buffer += ''; 96 | buffer += ''; 97 | buffer += ''; 100 | 101 | // output html file 102 | fs.writeFileSync(`${BUILD_FOLDER}/index.html`, buffer, {flag: 'w+'}); 103 | }; 104 | 105 | function zipBuildStep(filename) 106 | { 107 | console.log(`Zipping...`); 108 | 109 | const ect = '../../../node_modules/ect-bin/vendor/win32/ect.exe'; 110 | const args = ['-9', '-strip', '-zip', `../${PROGRAM_NAME}.zip`, 'index.html', ...dataFiles]; 111 | child_process.spawnSync(ect, args, {stdio: 'inherit', cwd: BUILD_FOLDER}); 112 | }; -------------------------------------------------------------------------------- /examples/starter/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Starter Project 3 | - A simple starter project for LittleJS 4 | - Demos all the main engine features 5 | - Builds to a zip file 6 | */ 7 | 8 | 'use strict'; 9 | 10 | // show the LittleJS splash screen 11 | setShowSplashScreen(true); 12 | 13 | // fix texture bleeding by shrinking tile slightly 14 | tileFixBleedScale = .5; 15 | 16 | // sound effects 17 | const sound_click = new Sound([1,.5]); 18 | 19 | // medals 20 | const medal_example = new Medal(0, 'Example Medal', 'Welcome to LittleJS!'); 21 | medalsInit('Hello World'); 22 | 23 | // game variables 24 | let particleEmitter; 25 | 26 | /////////////////////////////////////////////////////////////////////////////// 27 | function gameInit() 28 | { 29 | // create tile collision and visible tile layer 30 | initTileCollision(vec2(32,16)); 31 | const pos = vec2(); 32 | const tileLayer = new TileLayer(pos, tileCollisionSize); 33 | 34 | // get level data from the tiles image 35 | const tileImage = textureInfos[0].image; 36 | mainContext.drawImage(tileImage,0,0); 37 | const imageData = mainContext.getImageData(0,0,tileImage.width,tileImage.height).data; 38 | for (pos.x = tileCollisionSize.x; pos.x--;) 39 | for (pos.y = tileCollisionSize.y; pos.y--;) 40 | { 41 | // check if this pixel is set 42 | const i = pos.x + tileImage.width*(15 + tileCollisionSize.y - pos.y); 43 | if (!imageData[4*i]) 44 | continue; 45 | 46 | // set tile data 47 | const tileIndex = 1; 48 | const direction = randInt(4) 49 | const mirror = !randInt(2); 50 | const color = randColor(); 51 | const data = new TileLayerData(tileIndex, direction, mirror, color); 52 | tileLayer.setData(pos, data); 53 | setTileCollisionData(pos, 1); 54 | } 55 | 56 | // draw tile layer with new data 57 | tileLayer.redraw(); 58 | 59 | // setup camera 60 | cameraPos = vec2(16,8); 61 | cameraScale = 48; 62 | 63 | // enable gravity 64 | gravity = -.01; 65 | 66 | // create particle emitter 67 | particleEmitter = new ParticleEmitter( 68 | vec2(16,9), 0, // emitPos, emitAngle 69 | 0, 0, 500, PI, // emitSize, emitTime, emitRate, emitCone 70 | tile(0, 16), // tileIndex, tileSize 71 | hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB 72 | hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB 73 | 2, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 74 | .99, 1, 1, PI, // damping, angleDamping, gravityScale, cone 75 | .05, .5, true, true // fadeRate, randomness, collide, additive 76 | ); 77 | particleEmitter.elasticity = .3; // bounce when it collides 78 | particleEmitter.trailScale = 2; // stretch in direction of motion 79 | } 80 | 81 | /////////////////////////////////////////////////////////////////////////////// 82 | function gameUpdate() 83 | { 84 | if (mouseWasPressed(0)) 85 | { 86 | // play sound when mouse is pressed 87 | sound_click.play(mousePos); 88 | 89 | // change particle color and set to fade out 90 | particleEmitter.colorStartA = hsl(); 91 | particleEmitter.colorStartB = randColor(); 92 | particleEmitter.colorEndA = particleEmitter.colorStartA.scale(1,0); 93 | particleEmitter.colorEndB = particleEmitter.colorStartB.scale(1,0); 94 | 95 | // unlock medals 96 | medal_example.unlock(); 97 | } 98 | 99 | // move particles to mouse location if on screen 100 | if (mousePosScreen.x) 101 | particleEmitter.pos = mousePos; 102 | } 103 | 104 | /////////////////////////////////////////////////////////////////////////////// 105 | function gameUpdatePost() 106 | { 107 | 108 | } 109 | 110 | /////////////////////////////////////////////////////////////////////////////// 111 | function gameRender() 112 | { 113 | // draw a grey square in the background without using webgl 114 | drawRect(vec2(16,8), vec2(20,14), hsl(0,0,.6), 0, false); 115 | 116 | // draw the logo as a tile 117 | drawTile(vec2(21,5), vec2(4.5), tile(3,128)); 118 | } 119 | 120 | /////////////////////////////////////////////////////////////////////////////// 121 | function gameRenderPost() 122 | { 123 | // draw to overlay canvas for hud rendering 124 | drawTextScreen('LittleJS Demo', 125 | vec2(mainCanvasSize.x/2, 70), 80, // position, size 126 | hsl(0,0,1), 6, hsl(0,0,0)); // color, outline size and color 127 | } 128 | 129 | /////////////////////////////////////////////////////////////////////////////// 130 | // Startup LittleJS Engine 131 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Starter Project 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/starter/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/starter/tiles.png -------------------------------------------------------------------------------- /examples/stress/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Stress Test 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/uiSystem/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS UI System Example 3 | - Shows how to use the LittleJS UI plugin 4 | - Modal windows, buttons, text, checkboxes, and more 5 | */ 6 | 7 | 'use strict'; 8 | 9 | // show the LittleJS splash screen 10 | setShowSplashScreen(true); 11 | 12 | // sound effects 13 | const sound_ui = new Sound([1,0]); 14 | 15 | // UI system 16 | let uiRoot, uiMenu; 17 | const getMenuVisible =()=> uiMenu.visible; 18 | const setMenuVisible =(visible)=> uiMenu.visible = visible; 19 | 20 | // use a fixed size canvas 21 | canvasFixedSize = vec2(1920, 1080); // 1080p 22 | canvasPixelated = false; 23 | 24 | function createUI() 25 | { 26 | // setup root to attach all ui elements to 27 | uiRoot = new UIObject(); 28 | const uiInfo = new UIText(vec2(0,90), vec2(1e3, 70), 29 | 'LittleJS UI System Example\nM = Toggle menu'); 30 | uiInfo.textColor = WHITE; 31 | uiInfo.lineWidth = 8; 32 | uiRoot.addChild(uiInfo); 33 | 34 | // setup example menu 35 | uiMenu = new UIObject(vec2(0,450)); 36 | uiRoot.addChild(uiMenu); 37 | const uiBackground = new UIObject(vec2(0,0), vec2(450,580)); 38 | uiBackground.lineWidth = 8; 39 | uiMenu.addChild(uiBackground); 40 | 41 | // example large text 42 | const textTitle = new UIText(vec2(0,-220), vec2(500, 90), 'Test Title'); 43 | uiMenu.addChild(textTitle); 44 | textTitle.textColor = RED; 45 | textTitle.lineWidth = 4; 46 | textTitle.lineColor = BLUE; 47 | 48 | // example multiline text 49 | const textTest = new UIText(vec2(-60,-140), vec2(300, 50), 'Test Text\nSecond text line.') 50 | uiMenu.addChild(textTest); 51 | 52 | // example tile image 53 | const tileTest = new UITile(vec2(150,-130), vec2(110), tile(3,128)) 54 | uiMenu.addChild(tileTest); 55 | 56 | // example checkbox 57 | const checkbox = new UICheckbox(vec2(-140,-20), vec2(40)); 58 | uiMenu.addChild(checkbox); 59 | checkbox.onPress = ()=> 60 | { 61 | checkbox.checked = !checkbox.checked; 62 | console.log('Checkbox clicked'); 63 | sound_ui.play(0,.5,checkbox.checked?4:1); 64 | } 65 | 66 | // text attached to checkbox 67 | const checkboxText = new UIText(vec2(170,0), vec2(300, 40), 'Test Checkbox'); 68 | checkbox.addChild(checkboxText); 69 | 70 | // example scrollbar 71 | const scrollbar = new UIScrollbar(vec2(0,60), vec2(350, 50)); 72 | uiMenu.addChild(scrollbar); 73 | scrollbar.onChange = ()=> console.log('New scrollbar value:', scrollbar.value); 74 | scrollbar.onPress = ()=> sound_ui.play(0,.3,2); 75 | scrollbar.onRelease = ()=> sound_ui.play(0,.3,4); 76 | 77 | // example button 78 | const button1 = new UIButton(vec2(0,140), vec2(350, 50), 'Test Button'); 79 | uiMenu.addChild(button1); 80 | button1.onPress = ()=> 81 | { 82 | console.log('Button 1 clicked'); 83 | sound_ui.play(); 84 | } 85 | 86 | // exit button 87 | const button2 = new UIButton(vec2(0,220), vec2(350, 50), 'Exit Menu'); 88 | uiMenu.addChild(button2); 89 | button2.onPress = ()=> 90 | { 91 | console.log('Button 2 clicked'); 92 | sound_ui.play(0,.5,2); 93 | setMenuVisible(false); 94 | } 95 | } 96 | 97 | /////////////////////////////////////////////////////////////////////////////// 98 | function gameInit() 99 | { 100 | initUISystem(); 101 | createUI(); 102 | } 103 | 104 | /////////////////////////////////////////////////////////////////////////////// 105 | function gameUpdate() 106 | { 107 | } 108 | 109 | /////////////////////////////////////////////////////////////////////////////// 110 | function gameUpdatePost() 111 | { 112 | // center ui 113 | uiRoot.pos.x = mainCanvasSize.x/2; 114 | 115 | // menu system 116 | const menuVisible = getMenuVisible(); 117 | paused = menuVisible; 118 | 119 | // toggle menu visibility 120 | if (keyWasPressed('KeyM')) 121 | setMenuVisible(!menuVisible); 122 | } 123 | 124 | /////////////////////////////////////////////////////////////////////////////// 125 | function gameRender() 126 | { 127 | // test game rendering 128 | drawRect(vec2(), vec2(1e3), hsl(0,0,.2)); 129 | 130 | // test game rendering 131 | drawRect(vec2(), vec2(1e3), hsl(0,0,.2)); 132 | for(let i=0; i<1e3; ++i) 133 | { 134 | const pos = vec2(30*Math.sin(i+time/9),20*Math.sin(i*i+time/9)); 135 | drawTile(pos, vec2(2), tile(3,128), hsl(i/9,1,.4), time+i, !(i%2), hsl(i/9,1,.1,0)); 136 | } 137 | } 138 | 139 | /////////////////////////////////////////////////////////////////////////////// 140 | function gameRenderPost() 141 | { 142 | } 143 | 144 | /////////////////////////////////////////////////////////////////////////////// 145 | // Startup LittleJS Engine 146 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/uiSystem/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Starter Project 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/uiSystem/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/examples/uiSystem/tiles.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | }, 4 | "include": [ 5 | "./src/*.js", 6 | "./plugins/*.js", 7 | "./examples/**/*.js", 8 | "./shortExamples/**/*.js", 9 | ], 10 | "exclude": [ 11 | ] 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "littlejsengine", 3 | "version": "1.11.8", 4 | "description": "LittleJS - Tiny and Fast HTML5 Game Engine", 5 | "main": "dist/littlejs.esm.js", 6 | "types": "dist/littlejs.d.ts", 7 | "exports": { 8 | "types": "./dist/littlejs.d.ts", 9 | "production": "./dist/littlejs.esm.min.js", 10 | "default": "./dist/littlejs.esm.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/KilledByAPixel/LittleJS.git" 15 | }, 16 | "keywords": [ 17 | "LittleJS", 18 | "HTML5", 19 | "JavaScript", 20 | "game", 21 | "engine", 22 | "library", 23 | "JS13K", 24 | "webgl" 25 | ], 26 | "author": "Frank Force", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/KilledByAPixel/LittleJS/issues" 30 | }, 31 | "homepage": "https://github.com/KilledByAPixel/LittleJS", 32 | "scripts": { 33 | "build": "node src/engineBuild.js" 34 | }, 35 | "devDependencies": { 36 | "google-closure-compiler": "~20230502.0.0", 37 | "roadroller": "~2.1.0", 38 | "uglify-js": "~3.17.4", 39 | "typescript": "~5.1.6", 40 | "ect-bin": "~1.4.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /plugins/Box2D_License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2010 Erin Catto http://www.gphysics.com 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /plugins/Box2D_v2.3.1_min.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/plugins/Box2D_v2.3.1_min.wasm.wasm -------------------------------------------------------------------------------- /plugins/newgrounds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Newgrounds API 3 | * - NewgroundsMedal extends Medal with Newgrounds API functionality 4 | * - Call newgroundsInit to enable Newgrounds functionality 5 | * - Uses CryptoJS for encryption if optional cipher is provided 6 | * - Keeps connection alive and logs views 7 | * - Functions to interact with scoreboards 8 | * - Functions to unlock medals 9 | */ 10 | 11 | 'use strict'; 12 | 13 | /////////////////////////////////////////////////////////////////////////////// 14 | 15 | /** Newgrounds medal auto unlocks in newgrounds API */ 16 | class NewgroundsMedal extends Medal 17 | { 18 | /** Create a medal object and adds it to the list of medals */ 19 | constructor(id, name, description, icon, src) 20 | { super(id, name, description, icon, src); } 21 | 22 | /** Unlocks a medal if not already unlocked */ 23 | unlock() 24 | { 25 | super.unlock(); 26 | newgrounds && newgrounds.unlockMedal(this.id); 27 | } 28 | } 29 | 30 | /////////////////////////////////////////////////////////////////////////////// 31 | 32 | /** Global Newgrounds object */ 33 | let newgrounds; 34 | 35 | /** This can used to enable Newgrounds functionality 36 | * @param {Number} app_id - The newgrounds App ID 37 | * @param {String} [cipher] - The encryption Key (AES-128/Base64) 38 | * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher 39 | * @memberof Medals */ 40 | function newgroundsInit(app_id, cipher, cryptoJS) 41 | { newgrounds = new Newgrounds(app_id, cipher, cryptoJS); } 42 | 43 | /** 44 | * Newgrounds API wrapper object 45 | * @example 46 | * // create a newgrounds object, replace the app id with your own 47 | * const app_id = '52123:1ZuSTQ7l'; 48 | * newgrounds = new Newgrounds(app_id); 49 | */ 50 | class Newgrounds 51 | { 52 | /** Create a newgrounds object 53 | * @param {String} app_id - The newgrounds App ID 54 | * @param {String} [cipher] - The encryption Key (AES-128/Base64) 55 | * @param {Object} [cryptoJS] - An instance of CryptoJS, if there is a cipher */ 56 | constructor(app_id, cipher, cryptoJS) 57 | { 58 | ASSERT(!newgrounds, 'there can only be one newgrounds object'); 59 | ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher'); 60 | 61 | this.app_id = app_id; 62 | this.cipher = cipher; 63 | this.cryptoJS = cryptoJS; 64 | this.host = location ? location.hostname : ''; 65 | 66 | // get session id from url search params 67 | const url = new URL(location.href); 68 | this.session_id = url.searchParams.get('ngio_session_id'); 69 | 70 | if (!this.session_id) 71 | return; // only use newgrounds when logged in 72 | 73 | // get medals 74 | const medalsResult = this.call('Medal.getList'); 75 | this.medals = medalsResult ? medalsResult.result.data['medals'] : []; 76 | debugMedals && console.log(this.medals); 77 | for (const newgroundsMedal of this.medals) 78 | { 79 | const medal = medals[newgroundsMedal['id']]; 80 | if (medal) 81 | { 82 | // copy newgrounds medal data 83 | medal.image = new Image; 84 | medal.image.src = newgroundsMedal['icon']; 85 | medal.name = newgroundsMedal['name']; 86 | medal.description = newgroundsMedal['description']; 87 | medal.unlocked = newgroundsMedal['unlocked']; 88 | medal.difficulty = newgroundsMedal['difficulty']; 89 | medal.value = newgroundsMedal['value']; 90 | 91 | if (medal.value) // add value to description 92 | medal.description = medal.description + ` (${ medal.value })`; 93 | } 94 | } 95 | 96 | // get scoreboards 97 | const scoreboardResult = this.call('ScoreBoard.getBoards'); 98 | this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : []; 99 | debugMedals && console.log(this.scoreboards); 100 | 101 | // keep the session alive with a ping every 5 minutes 102 | const keepAliveMS = 5 * 60 * 1e3; 103 | setInterval(()=>this.call('Gateway.ping', 0, true), keepAliveMS); 104 | } 105 | 106 | /** Send message to unlock a medal by id 107 | * @param {Number} id - The medal id */ 108 | unlockMedal(id) { return this.call('Medal.unlock', {'id':id}, true); } 109 | 110 | /** Send message to post score 111 | * @param {Number} id - The scoreboard id 112 | * @param {Number} value - The score value */ 113 | postScore(id, value) { return this.call('ScoreBoard.postScore', {'id':id, 'value':value}, true); } 114 | 115 | /** Get scores from a scoreboard 116 | * @param {Number} id - The scoreboard id 117 | * @param {String} [user] - A user's id or name 118 | * @param {Number} [social] - If true, only social scores will be loaded 119 | * @param {Number} [skip] - Number of scores to skip before start 120 | * @param {Number} [limit] - Number of scores to include in the list 121 | * @return {Object} - The response JSON object 122 | */ 123 | getScores(id, user, social=0, skip=0, limit=10) 124 | { return this.call('ScoreBoard.getScores', {'id':id, 'user':user, 'social':social, 'skip':skip, 'limit':limit}); } 125 | 126 | /** Send message to log a view */ 127 | logView() { return this.call('App.logView', {'host':this.host}, true); } 128 | 129 | /** Send a message to call a component of the Newgrounds API 130 | * @param {String} component - Name of the component 131 | * @param {Object} [parameters] - Parameters to use for call 132 | * @param {Boolean} [async] - If true, don't wait for response before continuing 133 | * @return {Object} - The response JSON object 134 | */ 135 | call(component, parameters, async=false) 136 | { 137 | const call = {'component':component, 'parameters':parameters}; 138 | if (this.cipher) 139 | { 140 | // encrypt using AES-128 Base64 with cryptoJS 141 | const cryptoJS = this.cryptoJS; 142 | const aesKey = cryptoJS['enc']['Base64']['parse'](this.cipher); 143 | const iv = cryptoJS['lib']['WordArray']['random'](16); 144 | const encrypted = cryptoJS['AES']['encrypt'](JSON.stringify(call), aesKey, {'iv':iv}); 145 | call['secure'] = cryptoJS['enc']['Base64']['stringify'](iv.concat(encrypted['ciphertext'])); 146 | call['parameters'] = 0; 147 | } 148 | 149 | // build the input object 150 | const input = 151 | { 152 | 'app_id': this.app_id, 153 | 'session_id': this.session_id, 154 | 'call': call 155 | }; 156 | 157 | // build post data 158 | const formData = new FormData(); 159 | formData.append('input', JSON.stringify(input)); 160 | 161 | // send post data 162 | const xmlHttp = new XMLHttpRequest(); 163 | const url = 'https://newgrounds.io/gateway_v3.php'; 164 | xmlHttp.open('POST', url, !debugMedals && async); 165 | xmlHttp.send(formData); 166 | debugMedals && console.log(xmlHttp.responseText); 167 | return xmlHttp.responseText && JSON.parse(xmlHttp.responseText); 168 | } 169 | } -------------------------------------------------------------------------------- /plugins/postProcess.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Post Processing Plugin 3 | * - Supports shadertoy style post processing shaders 4 | * - call initPostProcess to set it up 5 | */ 6 | 7 | 'use strict'; 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // post processing - can be enabled to pass other canvases through a final shader 11 | 12 | let glPostShader, glPostTexture, glPostIncludeOverlay; 13 | 14 | /** Set up a post processing shader 15 | * @param {String} shaderCode 16 | * @param {Boolean} includeOverlay 17 | * @memberof WebGL */ 18 | function initPostProcess(shaderCode, includeOverlay=false) 19 | { 20 | ASSERT(!glPostShader, 'can only have 1 post effects shader'); 21 | if (headlessMode) return; 22 | 23 | if (!shaderCode) // default shader pass through 24 | shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}'; 25 | 26 | // create the shader 27 | glPostShader = glCreateProgram( 28 | '#version 300 es\n' + // specify GLSL ES version 29 | 'precision highp float;'+ // use highp for better accuracy 30 | 'in vec2 p;'+ // position 31 | 'void main(){'+ // shader entry point 32 | 'gl_Position=vec4(p+p-1.,1,1);'+ // set position 33 | '}' // end of shader 34 | , 35 | '#version 300 es\n' + // specify GLSL ES version 36 | 'precision highp float;'+ // use highp for better accuracy 37 | 'uniform sampler2D iChannel0;'+ // input texture 38 | 'uniform vec3 iResolution;'+ // size of output texture 39 | 'uniform float iTime;'+ // time 40 | 'out vec4 c;'+ // out color 41 | '\n' + shaderCode + '\n'+ // insert custom shader code 42 | 'void main(){'+ // shader entry point 43 | 'mainImage(c,gl_FragCoord.xy);'+ // call post process function 44 | 'c.a=1.;'+ // always use full alpha 45 | '}' // end of shader 46 | ); 47 | 48 | // create buffer and texture 49 | glPostTexture = glCreateTexture(undefined); 50 | glPostIncludeOverlay = includeOverlay; 51 | 52 | // Render the post processing shader, called automatically by the engine 53 | engineAddPlugin(undefined, postProcessRender); 54 | function postProcessRender() 55 | { 56 | if (headlessMode) return; 57 | 58 | // prepare to render post process shader 59 | if (glEnable) 60 | { 61 | glFlush(); // clear out the buffer 62 | mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas 63 | } 64 | else 65 | { 66 | // set the viewport 67 | glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height); 68 | } 69 | 70 | if (glPostIncludeOverlay) 71 | { 72 | // copy overlay canvas so it will be included in post processing 73 | mainContext.drawImage(overlayCanvas, 0, 0); 74 | overlayCanvas.width |= 0; 75 | } 76 | 77 | // setup shader program to draw one triangle 78 | glContext.useProgram(glPostShader); 79 | glContext.bindBuffer(glContext.ARRAY_BUFFER, glGeometryBuffer); 80 | glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, 1); 81 | glContext.disable(glContext.BLEND); 82 | 83 | // set textures, pass in the 2d canvas and gl canvas in separate texture channels 84 | glContext.activeTexture(glContext.TEXTURE0); 85 | glContext.bindTexture(glContext.TEXTURE_2D, glPostTexture); 86 | glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, mainCanvas); 87 | 88 | // set vertex position attribute 89 | const vertexByteStride = 8; 90 | const pLocation = glContext.getAttribLocation(glPostShader, 'p'); 91 | glContext.enableVertexAttribArray(pLocation); 92 | glContext.vertexAttribPointer(pLocation, 2, glContext.FLOAT, false, vertexByteStride, 0); 93 | 94 | // set uniforms and draw 95 | const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name); 96 | glContext.uniform1i(uniformLocation('iChannel0'), 0); 97 | glContext.uniform1f(uniformLocation('iTime'), time); 98 | glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1); 99 | glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, 4); 100 | } 101 | } -------------------------------------------------------------------------------- /plugins/uiSystem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS User Interface Plugin 3 | * - Nested Menus 4 | * - Text 5 | * - Buttons 6 | * - Checkboxes 7 | * - Images 8 | */ 9 | 10 | 'use strict'; 11 | 12 | /////////////////////////////////////////////////////////////////////////////// 13 | 14 | // ui defaults 15 | let uiDefaultColor = WHITE; 16 | let uiDefaultLineColor = BLACK; 17 | let uiDefaultTextColor = BLACK; 18 | let uiDefaultButtonColor = hsl(0,0,.5); 19 | let uiDefaultHoverColor = hsl(0,0,.7); 20 | let uiDefaultLineWidth = 4; 21 | let uiDefaultFont = 'arial'; 22 | 23 | // ui system 24 | let uiObjects = []; 25 | let uiContext; 26 | 27 | function initUISystem(context=overlayContext) 28 | { 29 | uiContext = context; 30 | engineAddPlugin(uiUpdate, uiRender); 31 | 32 | // setup recursive update and render 33 | function uiUpdate() 34 | { 35 | function updateObject(o) 36 | { 37 | if (!o.visible) 38 | return; 39 | if (o.parent) 40 | o.pos = o.localPos.add(o.parent.pos); 41 | o.update(); 42 | for(const c of o.children) 43 | updateObject(c); 44 | } 45 | uiObjects.forEach(o=> o.parent || updateObject(o)); 46 | } 47 | function uiRender() 48 | { 49 | function renderObject(o) 50 | { 51 | if (!o.visible) 52 | return; 53 | if (o.parent) 54 | o.pos = o.localPos.add(o.parent.pos); 55 | o.render(); 56 | for(const c of o.children) 57 | renderObject(c); 58 | } 59 | uiObjects.forEach(o=> o.parent || renderObject(o)); 60 | } 61 | } 62 | 63 | function drawUIRect(pos, size, color=uiDefaultColor, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor) 64 | { 65 | uiContext.fillStyle = color.toString(); 66 | uiContext.beginPath(); 67 | uiContext.rect(pos.x-size.x/2, pos.y-size.y/2, size.x, size.y); 68 | uiContext.fill(); 69 | if (lineWidth) 70 | { 71 | uiContext.strokeStyle = lineColor.toString(); 72 | uiContext.lineWidth = lineWidth; 73 | uiContext.stroke(); 74 | } 75 | } 76 | 77 | function drawUILine(posA, posB, thickness=uiDefaultLineWidth, color=uiDefaultLineColor) 78 | { 79 | uiContext.strokeStyle = color.toString(); 80 | uiContext.lineWidth = thickness; 81 | uiContext.beginPath(); 82 | uiContext.lineTo(posA.x, posA.y); 83 | uiContext.lineTo(posB.x, posB.y); 84 | uiContext.stroke(); 85 | } 86 | 87 | function drawUITile(pos, size, tileInfo, color=uiDefaultColor, angle=0, mirror=false) 88 | { 89 | drawTile(pos, size, tileInfo, color, angle, mirror, BLACK, false, true, uiContext); 90 | } 91 | 92 | function drawUIText(text, pos, size, color=uiDefaultColor, lineWidth=uiDefaultLineWidth, lineColor=uiDefaultLineColor, align='center', font=uiDefaultFont) 93 | { 94 | drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, size.x, uiContext); 95 | } 96 | 97 | /////////////////////////////////////////////////////////////////////////////// 98 | 99 | class UIObject 100 | { 101 | constructor(localPos=vec2(), size=vec2()) 102 | { 103 | this.localPos = localPos.copy(); 104 | this.pos = localPos.copy(); 105 | this.size = size.copy(); 106 | this.color = uiDefaultColor; 107 | this.lineColor = uiDefaultLineColor; 108 | this.textColor = uiDefaultTextColor; 109 | this.hoverColor = uiDefaultHoverColor; 110 | this.lineWidth = uiDefaultLineWidth; 111 | this.font = uiDefaultFont; 112 | this.visible = true; 113 | this.children = []; 114 | this.parent = null; 115 | uiObjects.push(this); 116 | } 117 | 118 | addChild(child) 119 | { 120 | ASSERT(!child.parent && !this.children.includes(child)); 121 | this.children.push(child); 122 | child.parent = this; 123 | } 124 | 125 | removeChild(child) 126 | { 127 | ASSERT(child.parent == this && this.children.includes(child)); 128 | this.children.splice(this.children.indexOf(child), 1); 129 | child.parent = 0; 130 | } 131 | 132 | update() 133 | { 134 | // track mouse input 135 | const mouseWasOver = this.mouseIsOver; 136 | const mouseDown = mouseIsDown(0); 137 | if (!mouseDown || isTouchDevice) 138 | { 139 | this.mouseIsOver = isOverlapping(this.pos, this.size, mousePosScreen); 140 | if (!mouseDown && isTouchDevice) 141 | this.mouseIsOver = false; 142 | if (this.mouseIsOver && !mouseWasOver) 143 | this.onEnter(); 144 | if (!this.mouseIsOver && mouseWasOver) 145 | this.onLeave(); 146 | } 147 | if (mouseWasPressed(0) && this.mouseIsOver) 148 | { 149 | this.mouseIsHeld = true; 150 | this.onPress(); 151 | if (isTouchDevice) 152 | this.mouseIsOver = false; 153 | } 154 | else if (this.mouseIsHeld && !mouseDown) 155 | { 156 | this.mouseIsHeld = false; 157 | this.onRelease(); 158 | } 159 | } 160 | render() 161 | { 162 | if (this.size.x && this.size.y) 163 | drawUIRect(this.pos, this.size, this.color, this.lineWidth, this.lineColor); 164 | } 165 | 166 | // callback functions 167 | onEnter() {} 168 | onLeave() {} 169 | onPress() {} 170 | onRelease() {} 171 | onChange() {} 172 | } 173 | 174 | /////////////////////////////////////////////////////////////////////////////// 175 | 176 | class UIText extends UIObject 177 | { 178 | constructor(pos, size, text='', align='center', font=fontDefault) 179 | { 180 | super(pos, size); 181 | 182 | this.text = text; 183 | this.align = align; 184 | this.font = font; 185 | this.lineWidth = 0; 186 | } 187 | render() 188 | { 189 | drawUIText(this.text, this.pos, this.size, this.textColor, this.lineWidth, this.lineColor, this.align, this.font); 190 | } 191 | } 192 | 193 | /////////////////////////////////////////////////////////////////////////////// 194 | 195 | class UITile extends UIObject 196 | { 197 | constructor(pos, size, tileInfo, color=WHITE, angle=0, mirror=false) 198 | { 199 | super(pos, size); 200 | 201 | this.tileInfo = tileInfo; 202 | this.color = color; 203 | this.angle = angle; 204 | this.mirror = mirror; 205 | } 206 | render() 207 | { 208 | drawUITile(this.pos, this.size, this.tileInfo, this.color, this.angle, this.mirror); 209 | } 210 | } 211 | 212 | /////////////////////////////////////////////////////////////////////////////// 213 | 214 | class UIButton extends UIObject 215 | { 216 | constructor(pos, size, text) 217 | { 218 | super(pos, size); 219 | this.text = text; 220 | this.color = uiDefaultButtonColor; 221 | } 222 | render() 223 | { 224 | const lineColor = this.mouseIsHeld ? this.color : this.lineColor; 225 | const color = this.mouseIsOver? this.hoverColor : this.color; 226 | drawUIRect(this.pos, this.size, color, this.lineWidth, lineColor); 227 | const textSize = vec2(this.size.x, this.size.y*.8); 228 | drawUIText(this.text, this.pos, textSize, 229 | this.textColor, 0, undefined, this.align, this.font); 230 | } 231 | } 232 | 233 | /////////////////////////////////////////////////////////////////////////////// 234 | 235 | class UICheckbox extends UIObject 236 | { 237 | constructor(pos, size, checked=false) 238 | { 239 | super(pos, size); 240 | this.checked = checked; 241 | } 242 | onPress() 243 | { 244 | this.checked = !this.checked; 245 | this.onChange(); 246 | } 247 | render() 248 | { 249 | const color = this.mouseIsOver? this.hoverColor : this.color; 250 | drawUIRect(this.pos, this.size, color, this.lineWidth, this.lineColor); 251 | if (this.checked) 252 | { 253 | // draw an X if checked 254 | drawUILine(this.pos.add(this.size.multiply(vec2(-.5,-.5))), this.pos.add(this.size.multiply(vec2(.5,.5))), this.lineWidth, this.lineColor); 255 | drawUILine(this.pos.add(this.size.multiply(vec2(-.5,.5))), this.pos.add(this.size.multiply(vec2(.5,-.5))), this.lineWidth, this.lineColor); 256 | } 257 | } 258 | } 259 | 260 | /////////////////////////////////////////////////////////////////////////////// 261 | 262 | class UIScrollbar extends UIObject 263 | { 264 | constructor(pos, size, value=.5, text='') 265 | { 266 | super(pos, size); 267 | this.value = value; 268 | this.text = text; 269 | this.color = uiDefaultButtonColor; 270 | this.handleColor = WHITE; 271 | } 272 | update() 273 | { 274 | super.update(); 275 | if (this.mouseIsHeld) 276 | { 277 | const handleSize = vec2(this.size.y); 278 | const handleWidth = this.size.x - handleSize.x; 279 | const p1 = this.pos.x - handleWidth/2; 280 | const p2 = this.pos.x + handleWidth/2; 281 | const oldValue = this.value; 282 | this.value = percent(mousePosScreen.x, p1, p2); 283 | this.value == oldValue || this.onChange(); 284 | } 285 | } 286 | render() 287 | { 288 | const lineColor = this.mouseIsHeld ? this.color : this.lineColor; 289 | const color = this.mouseIsOver? this.hoverColor : this.color; 290 | drawUIRect(this.pos, this.size, color, this.lineWidth, lineColor); 291 | 292 | const handleSize = vec2(this.size.y); 293 | const handleWidth = this.size.x - handleSize.x; 294 | const p1 = this.pos.x - handleWidth/2; 295 | const p2 = this.pos.x + handleWidth/2; 296 | const handlePos = vec2(lerp(this.value, p1, p2), this.pos.y); 297 | const barColor = this.mouseIsHeld ? this.color : this.handleColor; 298 | drawUIRect(handlePos, handleSize, barColor, this.lineWidth, this.lineColor); 299 | 300 | const textSize = vec2(this.size.x, this.size.y*.8); 301 | drawUIText(this.text, this.pos, textSize, 302 | this.textColor, 0, undefined, this.align, this.font); 303 | } 304 | } -------------------------------------------------------------------------------- /src/engineBuild.bat: -------------------------------------------------------------------------------- 1 | rem LittleJS Build Script 2 | call npm run build -------------------------------------------------------------------------------- /src/engineBuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * LittleJS Build System 5 | * - Combine input files 6 | * - Run custom build steps 7 | * - Check for errors 8 | * - Output to build folder 9 | */ 10 | 11 | 'use strict'; 12 | 13 | const ENGINE_NAME = 'littlejs'; 14 | const BUILD_FOLDER = 'dist'; 15 | const SOURCE_FOLDER = 'src'; 16 | const engineSourceFiles = 17 | [ 18 | `${SOURCE_FOLDER}/engineUtilities.js`, 19 | `${SOURCE_FOLDER}/engineSettings.js`, 20 | `${SOURCE_FOLDER}/engineObject.js`, 21 | `${SOURCE_FOLDER}/engineDraw.js`, 22 | `${SOURCE_FOLDER}/engineInput.js`, 23 | `${SOURCE_FOLDER}/engineAudio.js`, 24 | `${SOURCE_FOLDER}/engineTileLayer.js`, 25 | `${SOURCE_FOLDER}/engineParticles.js`, 26 | `${SOURCE_FOLDER}/engineMedals.js`, 27 | `${SOURCE_FOLDER}/engineWebGL.js`, 28 | `${SOURCE_FOLDER}/engine.js`, 29 | ]; 30 | const asciiArt =` 31 | ~~~~°°°°ooo°oOo°ooOooOooOo. 32 | __________ ________ ____'°oO. 33 | |LittleJS| |Engine| |[]|_._Y 34 | .|________|_._|______|_._|__|_|_|} 35 | OOO OOO OO OO OO=OO-oo\\ 36 | `; 37 | const license = '// LittleJS Engine - MIT License - Copyright 2021 Frank Force\n'+ 38 | '// https://github.com/KilledByAPixel/LittleJS\n'; 39 | 40 | console.log(asciiArt); 41 | console.log('Choo Choo... Building LittleJS Engine!\n'); 42 | const startTime = Date.now(); 43 | const fs = require('node:fs'); 44 | const child_process = require('node:child_process'); 45 | 46 | try 47 | { 48 | // Setup build folder 49 | fs.rmSync(BUILD_FOLDER, { recursive: true, force: true }); 50 | fs.mkdirSync(BUILD_FOLDER); 51 | } 52 | catch (e) { handleError(e, 'Failed to create build folder!'); } 53 | 54 | Build 55 | ( 56 | 'Build Engine -- all', 57 | `${BUILD_FOLDER}/${ENGINE_NAME}.js`, 58 | [`${SOURCE_FOLDER}/engineDebug.js`, ...engineSourceFiles], 59 | [], true 60 | ); 61 | 62 | Build 63 | ( 64 | 'Build Engine -- release', 65 | `${BUILD_FOLDER}/${ENGINE_NAME}.release.js`, 66 | [`${SOURCE_FOLDER}/engineRelease.js`, ...engineSourceFiles], 67 | [], true 68 | ); 69 | 70 | Build 71 | ( 72 | 'Build Engine -- minified', 73 | `${BUILD_FOLDER}/${ENGINE_NAME}.min.js`, 74 | [`${BUILD_FOLDER}/${ENGINE_NAME}.release.js`], 75 | [closureCompilerStep, uglifyBuildStep] 76 | ); 77 | 78 | Build 79 | ( 80 | 'Build Engine -- ESM', 81 | `${BUILD_FOLDER}/${ENGINE_NAME}.esm.js`, 82 | [`${BUILD_FOLDER}/${ENGINE_NAME}.js`, `${SOURCE_FOLDER}/engineExport.js`], 83 | [typeScriptBuildStep] 84 | ); 85 | 86 | Build 87 | ( 88 | 'Build Engine -- ESM minified release', 89 | `${BUILD_FOLDER}/${ENGINE_NAME}.esm.min.js`, 90 | [`${BUILD_FOLDER}/${ENGINE_NAME}.min.js`, `${SOURCE_FOLDER}/engineExport.js`], 91 | [uglifyBuildStep] 92 | ); 93 | 94 | console.log(`Engine built in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`); 95 | 96 | /////////////////////////////////////////////////////////////////////////////// 97 | 98 | // A single build with its own source files, build steps, and output file 99 | // - each build step is a callback that accepts a single filename 100 | function Build(message, outputFile, files=[], buildSteps=[], isPrimaryBuild) 101 | { 102 | console.log(message); 103 | 104 | // copy files into a buffer 105 | let buffer = ''; 106 | if (isPrimaryBuild) 107 | { 108 | // add license and strict mode to top 109 | buffer += license + '\n'; 110 | buffer += "'use strict';\n\n"; 111 | } 112 | 113 | for (const file of files) 114 | { 115 | // get file content 116 | let fileContent = fs.readFileSync(file) + '\n'; 117 | 118 | // remove first 'use strict' from each file 119 | if (isPrimaryBuild) 120 | fileContent = fileContent.replace("'use strict';", ''); 121 | 122 | // add it to the buffer 123 | buffer += fileContent; 124 | } 125 | 126 | // output file 127 | fs.writeFileSync(outputFile, buffer, {flag: 'w+'}); 128 | 129 | // execute build steps in order 130 | for (const buildStep of buildSteps) 131 | buildStep(outputFile); 132 | } 133 | 134 | // Process with Closure Compiler to minify and check for errors 135 | function closureCompilerStep(filename) 136 | { 137 | const filenameTemp = filename + '.tmp'; 138 | fs.copyFileSync(filename, filenameTemp); 139 | try 140 | { 141 | child_process.execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --warning_level=VERBOSE --jscomp_off=*`); 142 | fs.rmSync(filenameTemp); 143 | } 144 | catch (e) { handleError(e, 'Failed to run Closure Compiler step!'); } 145 | }; 146 | 147 | // Process with Uglify to minify 148 | function uglifyBuildStep(filename) 149 | { 150 | try 151 | { 152 | child_process.execSync(`npx uglifyjs ${filename} -o ${filename}`); 153 | } 154 | catch (e) { handleError(e,'Failed to run Uglify minification step!'); } 155 | }; 156 | 157 | // Build TypeScript definitions 158 | function typeScriptBuildStep(filename) 159 | { 160 | try 161 | { 162 | const tsFilename = `${BUILD_FOLDER}/${ENGINE_NAME}.d.ts` 163 | child_process.execSync(`npx tsc ${filename} --declaration --allowJs --emitDeclarationOnly --outFile ${tsFilename}`); 164 | 165 | // Make declare module part use the package name "littlejsengine" 166 | let fileContent = fs.readFileSync(tsFilename, 'utf8'); 167 | fileContent = fileContent.replace(`${ENGINE_NAME}\.esm`, 'littlejsengine') 168 | fs.writeFileSync(tsFilename, fileContent); 169 | 170 | } 171 | catch (e) { handleError(e, 'Failed to run TypeScript build step!'); } 172 | }; 173 | 174 | // display the error and exit 175 | function handleError(e,message) 176 | { 177 | console.error(e); 178 | console.error(message); 179 | process.exit(1); 180 | } -------------------------------------------------------------------------------- /src/engineExport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Module Export 3 | * - Export engine as a module 4 | */ 5 | 6 | export { 7 | 8 | // Engine 9 | engineName, 10 | engineVersion, 11 | frameRate, 12 | timeDelta, 13 | engineObjects, 14 | frame, 15 | time, 16 | timeReal, 17 | paused, 18 | setPaused, 19 | engineInit, 20 | engineObjectsUpdate, 21 | engineObjectsDestroy, 22 | engineObjectsCollect, 23 | engineObjectsCallback, 24 | engineObjectsRaycast, 25 | engineAddPlugin, 26 | 27 | // Globals 28 | debug, 29 | debugOverlay, 30 | showWatermark, 31 | 32 | // Debug 33 | ASSERT, 34 | debugRect, 35 | debugPoly, 36 | debugCircle, 37 | debugPoint, 38 | debugLine, 39 | debugOverlap, 40 | debugText, 41 | debugClear, 42 | debugScreenshot, 43 | debugSaveCanvas, 44 | debugSaveText, 45 | debugSaveDataURL, 46 | 47 | // Settings 48 | cameraPos, 49 | cameraScale, 50 | canvasMaxSize, 51 | canvasFixedSize, 52 | canvasPixelated, 53 | tilesPixelated, 54 | fontDefault, 55 | showSplashScreen, 56 | headlessMode, 57 | tileSizeDefault, 58 | tileFixBleedScale, 59 | enablePhysicsSolver, 60 | objectDefaultMass, 61 | objectDefaultDamping, 62 | objectDefaultAngleDamping, 63 | objectDefaultElasticity, 64 | objectDefaultFriction, 65 | objectMaxSpeed, 66 | gravity, 67 | particleEmitRateScale, 68 | glEnable, 69 | glOverlay, 70 | gamepadsEnable, 71 | gamepadDirectionEmulateStick, 72 | inputWASDEmulateDirection, 73 | touchGamepadEnable, 74 | touchGamepadAnalog, 75 | touchGamepadSize, 76 | touchGamepadAlpha, 77 | vibrateEnable, 78 | soundEnable, 79 | soundVolume, 80 | soundDefaultRange, 81 | soundDefaultTaper, 82 | medalDisplayTime, 83 | medalDisplaySlideTime, 84 | medalDisplaySize, 85 | medalDisplayIconSize, 86 | 87 | // Setters for globals 88 | setCameraPos, 89 | setCameraScale, 90 | setCanvasMaxSize, 91 | setCanvasFixedSize, 92 | setCanvasPixelated, 93 | setTilesPixelated, 94 | setFontDefault, 95 | setShowSplashScreen, 96 | setHeadlessMode, 97 | setGlEnable, 98 | setGlOverlay, 99 | setTileSizeDefault, 100 | setTileFixBleedScale, 101 | setEnablePhysicsSolver, 102 | setObjectDefaultMass, 103 | setObjectDefaultDamping, 104 | setObjectDefaultAngleDamping, 105 | setObjectDefaultElasticity, 106 | setObjectDefaultFriction, 107 | setObjectMaxSpeed, 108 | setGravity, 109 | setParticleEmitRateScale, 110 | setTouchInputEnable, 111 | setGamepadsEnable, 112 | setGamepadDirectionEmulateStick, 113 | setInputWASDEmulateDirection, 114 | setTouchGamepadEnable, 115 | setTouchGamepadAnalog, 116 | setTouchGamepadSize, 117 | setTouchGamepadAlpha, 118 | setVibrateEnable, 119 | setSoundEnable, 120 | setSoundVolume, 121 | setSoundDefaultRange, 122 | setSoundDefaultTaper, 123 | setMedalDisplayTime, 124 | setMedalDisplaySlideTime, 125 | setMedalDisplaySize, 126 | setMedalDisplayIconSize, 127 | setMedalsPreventUnlock, 128 | setShowWatermark, 129 | setDebugKey, 130 | 131 | // Utilities 132 | PI, 133 | abs, 134 | min, 135 | max, 136 | sign, 137 | mod, 138 | clamp, 139 | percent, 140 | distanceWrap, 141 | lerpWrap, 142 | distanceAngle, 143 | lerpAngle, 144 | lerp, 145 | smoothStep, 146 | nearestPowerOfTwo, 147 | isOverlapping, 148 | isIntersecting, 149 | wave, 150 | formatTime, 151 | 152 | // Random 153 | rand, 154 | randInt, 155 | randSign, 156 | randInCircle, 157 | randVector, 158 | randColor, 159 | 160 | // Utility Classes 161 | RandomGenerator, 162 | Vector2, 163 | Color, 164 | Timer, 165 | vec2, 166 | rgb, 167 | hsl, 168 | isColor, 169 | 170 | // Default Colors 171 | WHITE, 172 | BLACK, 173 | GRAY, 174 | RED, 175 | ORANGE, 176 | YELLOW, 177 | GREEN, 178 | CYAN, 179 | BLUE, 180 | PURPLE, 181 | MAGENTA, 182 | 183 | // Draw 184 | textureInfos, 185 | tile, 186 | TileInfo, 187 | TextureInfo, 188 | mainCanvas, 189 | mainContext, 190 | overlayCanvas, 191 | overlayContext, 192 | mainCanvasSize, 193 | screenToWorld, 194 | worldToScreen, 195 | drawTile, 196 | drawRect, 197 | drawLine, 198 | drawPoly, 199 | drawEllipse, 200 | drawCircle, 201 | drawCanvas2D, 202 | drawText, 203 | drawTextOverlay, 204 | drawTextScreen, 205 | setBlendMode, 206 | combineCanvases, 207 | engineFontImage, 208 | FontImage, 209 | isFullscreen, 210 | toggleFullscreen, 211 | setCursor, 212 | getCameraSize, 213 | 214 | // WebGL 215 | glCanvas, 216 | glContext, 217 | glCompileShader, 218 | glCopyToContext, 219 | glCreateProgram, 220 | glCreateTexture, 221 | glDraw, 222 | glFlush, 223 | glSetTexture, 224 | glSetAntialias, 225 | glClearCanvas, 226 | glAntialias, 227 | glShader, 228 | glActiveTexture, 229 | glArrayBuffer, 230 | glGeometryBuffer, 231 | glPositionData, 232 | glColorData, 233 | glInstanceCount, 234 | glAdditive, 235 | glBatchAdditive, 236 | 237 | // Input 238 | keyIsDown, 239 | keyWasPressed, 240 | keyWasReleased, 241 | keyDirection, 242 | clearInput, 243 | mouseIsDown, 244 | mouseWasPressed, 245 | mouseWasReleased, 246 | mousePos, 247 | mousePosScreen, 248 | mouseWheel, 249 | isUsingGamepad, 250 | preventDefaultInput, 251 | gamepadIsDown, 252 | gamepadWasPressed, 253 | gamepadWasReleased, 254 | gamepadStick, 255 | gamepadsUpdate, 256 | vibrate, 257 | vibrateStop, 258 | isTouchDevice, 259 | 260 | // Audio 261 | Sound, 262 | SoundWave, 263 | Music, 264 | playAudioFile, 265 | speak, 266 | speakStop, 267 | getNoteFrequency, 268 | audioContext, 269 | playSamples, 270 | zzfx, 271 | 272 | // Base Object 273 | EngineObject, 274 | 275 | // Tiles 276 | tileCollision, 277 | tileCollisionSize, 278 | initTileCollision, 279 | setTileCollisionData, 280 | getTileCollisionData, 281 | tileCollisionTest, 282 | tileCollisionRaycast, 283 | TileLayerData, 284 | TileLayer, 285 | 286 | // Particles 287 | ParticleEmitter, 288 | Particle, 289 | 290 | // Medals 291 | medals, 292 | medalsPreventUnlock, 293 | medalsInit, 294 | Medal, 295 | }; 296 | -------------------------------------------------------------------------------- /src/engineFont.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/5ca398a73e9097e7c65d09494c8d4b1625a571da/src/engineFont.png -------------------------------------------------------------------------------- /src/engineMedals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Medal System 3 | * - Tracks and displays medals 4 | * - Saves medals to local storage 5 | * - Newgrounds integration 6 | * @namespace Medals 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** List of all medals 12 | * @type {Object} 13 | * @memberof Medals */ 14 | const medals = {}; 15 | 16 | // Engine internal variables not exposed to documentation 17 | let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast; 18 | 19 | /////////////////////////////////////////////////////////////////////////////// 20 | 21 | /** Initialize medals with a save name used for storage 22 | * - Call this after creating all medals 23 | * - Checks if medals are unlocked 24 | * @param {String} saveName 25 | * @memberof Medals */ 26 | function medalsInit(saveName) 27 | { 28 | // check if medals are unlocked 29 | medalsSaveName = saveName; 30 | if (!debugMedals) 31 | medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]); 32 | 33 | // engine automatically renders medals 34 | engineAddPlugin(undefined, medalsRender); 35 | function medalsRender() 36 | { 37 | if (!medalsDisplayQueue.length) 38 | return; 39 | 40 | // update first medal in queue 41 | const medal = medalsDisplayQueue[0]; 42 | const time = timeReal - medalsDisplayTimeLast; 43 | if (!medalsDisplayTimeLast) 44 | medalsDisplayTimeLast = timeReal; 45 | else if (time > medalDisplayTime) 46 | { 47 | medalsDisplayTimeLast = 0; 48 | medalsDisplayQueue.shift(); 49 | } 50 | else 51 | { 52 | // slide on/off medals 53 | const slideOffTime = medalDisplayTime - medalDisplaySlideTime; 54 | const hidePercent = 55 | time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime : 56 | time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0; 57 | medal.render(hidePercent); 58 | } 59 | } 60 | } 61 | 62 | /** Calls a function for each medal 63 | * @param {Function} callback 64 | * @memberof Medals */ 65 | function medalsForEach(callback) 66 | { Object.values(medals).forEach(medal=>callback(medal)); } 67 | 68 | /////////////////////////////////////////////////////////////////////////////// 69 | 70 | /** 71 | * Medal - Tracks an unlockable medal 72 | * @example 73 | * // create a medal 74 | * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️'); 75 | * 76 | * // initialize medals 77 | * medalsInit('Example Game'); 78 | * 79 | * // unlock the medal 80 | * medal_example.unlock(); 81 | */ 82 | class Medal 83 | { 84 | /** Create a medal object and adds it to the list of medals 85 | * @param {Number} id - The unique identifier of the medal 86 | * @param {String} name - Name of the medal 87 | * @param {String} [description] - Description of the medal 88 | * @param {String} [icon] - Icon for the medal 89 | * @param {String} [src] - Image location for the medal 90 | */ 91 | constructor(id, name, description='', icon='🏆', src) 92 | { 93 | ASSERT(id >= 0 && !medals[id]); 94 | 95 | /** @property {Number} - The unique identifier of the medal */ 96 | this.id = id; 97 | 98 | /** @property {String} - Name of the medal */ 99 | this.name = name; 100 | 101 | /** @property {String} - Description of the medal */ 102 | this.description = description; 103 | 104 | /** @property {String} - Icon for the medal */ 105 | this.icon = icon; 106 | 107 | /** @property {Boolean} - Is the medal unlocked? */ 108 | this.unlocked = false; 109 | 110 | // load the source image if provided 111 | if (src) 112 | (this.image = new Image).src = src; 113 | 114 | // add this to list of medals 115 | medals[id] = this; 116 | } 117 | 118 | /** Unlocks a medal if not already unlocked */ 119 | unlock() 120 | { 121 | if (medalsPreventUnlock || this.unlocked) 122 | return; 123 | 124 | // save the medal 125 | ASSERT(medalsSaveName, 'save name must be set'); 126 | localStorage[this.storageKey()] = this.unlocked = true; 127 | medalsDisplayQueue.push(this); 128 | } 129 | 130 | /** Render a medal 131 | * @param {Number} [hidePercent] - How much to slide the medal off screen 132 | */ 133 | render(hidePercent=0) 134 | { 135 | const context = overlayContext; 136 | const width = min(medalDisplaySize.x, mainCanvas.width); 137 | const x = overlayCanvas.width - width; 138 | const y = -medalDisplaySize.y*hidePercent; 139 | 140 | // draw containing rect and clip to that region 141 | context.save(); 142 | context.beginPath(); 143 | context.fillStyle = new Color(.9,.9,.9).toString(); 144 | context.strokeStyle = new Color(0,0,0).toString(); 145 | context.lineWidth = 3; 146 | context.rect(x, y, width, medalDisplaySize.y); 147 | context.fill(); 148 | context.stroke(); 149 | context.clip(); 150 | 151 | // draw the icon and text 152 | this.renderIcon(vec2(x+15+medalDisplayIconSize/2, y+medalDisplaySize.y/2)); 153 | const pos = vec2(x+medalDisplayIconSize+30, y+28); 154 | drawTextScreen(this.name, pos, 38, new Color(0,0,0), 0, undefined, 'left'); 155 | pos.y += 32; 156 | drawTextScreen(this.description, pos, 24, new Color(0,0,0), 0, undefined, 'left'); 157 | context.restore(); 158 | } 159 | 160 | /** Render the icon for a medal 161 | * @param {Vector2} pos - Screen space position 162 | * @param {Number} [size=medalDisplayIconSize] - Screen space size 163 | */ 164 | renderIcon(pos, size=medalDisplayIconSize) 165 | { 166 | // draw the image or icon 167 | if (this.image) 168 | overlayContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size); 169 | else 170 | drawTextScreen(this.icon, pos, size*.7, new Color(0,0,0)); 171 | } 172 | 173 | // Get local storage key used by the medal 174 | storageKey() { return medalsSaveName + '_' + this.id; } 175 | } -------------------------------------------------------------------------------- /src/engineRelease.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS - Release Mode 3 | * - This file is used for release builds in place of engineDebug.js 4 | * - Debug functionality is disabled to reduce size and increase performance 5 | */ 6 | 7 | 'use strict'; 8 | 9 | let showWatermark = 0; 10 | let debugKey = ''; 11 | const debug = 0; 12 | const debugOverlay = 0; 13 | const debugPhysics = 0; 14 | const debugParticles = 0; 15 | const debugRaycast = 0; 16 | const debugGamepads = 0; 17 | const debugMedals = 0; 18 | 19 | // debug commands are automatically removed from the final build 20 | function ASSERT (){} 21 | function debugInit (){} 22 | function debugUpdate (){} 23 | function debugRender (){} 24 | function debugRect (){} 25 | function debugPoly (){} 26 | function debugCircle (){} 27 | function debugPoint (){} 28 | function debugLine (){} 29 | function debugOverlap (){} 30 | function debugText (){} 31 | function debugClear (){} 32 | function debugScreenshot (){} 33 | function debugSaveCanvas (){} 34 | function debugSaveText (){} 35 | function debugSaveDataURL(){} -------------------------------------------------------------------------------- /src/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "target": "ES6" 5 | }, 6 | "exclude": [ 7 | "engineRelease.js", 8 | "engineExport.js" 9 | ] 10 | } --------------------------------------------------------------------------------