├── examples ├── logo.png ├── games.jpg ├── logo2.png ├── favicon.png ├── box2d │ ├── tiles.png │ ├── index.html │ └── game.js ├── empty │ ├── tiles.png │ ├── index.html │ └── game.js ├── screenshot.jpg ├── shorts │ ├── song.mp3 │ ├── tiles.png │ ├── video.webm │ ├── helloWorld.js │ ├── fontImage.js │ ├── blending.js │ ├── starfield.js │ ├── animation.js │ ├── texture.js │ ├── cameraDrag.js │ ├── clock.js │ ├── empty.js │ ├── shader.js │ ├── shapes.js │ ├── colors.js │ ├── topDown.js │ ├── debugDraw.js │ ├── spriteAtlas.js │ ├── particles.js │ ├── tileRaycast.js │ ├── videoPlayer.js │ ├── platformer.js │ ├── piano.js │ ├── pongGame.js │ ├── box2d.js │ ├── flappyGame.js │ ├── sound.js │ ├── medals.js │ ├── nineSlice.js │ ├── maze.js │ ├── tileLayer.js │ ├── box2dTileLayer.js │ ├── slidingPuzzle.js │ ├── timers.js │ ├── hillGlideGame.js │ ├── spaceGame.js │ ├── landerGame.js │ ├── base.html │ ├── box2dCar.js │ ├── input.js │ ├── tiltedView.js │ ├── postProcess.js │ ├── uiSystem.js │ ├── music.js │ ├── parallax.js │ ├── fps.js │ ├── box2dPool.js │ ├── sequencer.js │ └── musicPlayer.js ├── breakout │ ├── tiles.png │ ├── index.html │ ├── gameObjects.js │ └── game.js ├── electron │ ├── tiles.png │ ├── package.json │ ├── index.html │ ├── electron.js │ ├── game.js │ └── build.mjs ├── htmlMenu │ ├── tiles.png │ ├── index.html │ └── game.js ├── module │ ├── tiles.png │ ├── index.html │ ├── build.mjs │ └── game.js ├── puzzle │ ├── tiles.png │ └── index.html ├── starter │ ├── tiles.png │ ├── build.bat │ ├── index.html │ ├── build.mjs │ └── game.js ├── uiSystem │ ├── tiles.png │ ├── index.html │ └── game.js ├── particles │ └── tiles.png ├── platformer │ ├── tiles.png │ ├── tilesLevel.png │ ├── data │ │ └── gameLevelData.tsx │ ├── index.html │ ├── gamePlayer.js │ └── game.js ├── typescript │ ├── tiles.png │ ├── tsconfig.json │ ├── index.html │ ├── build.mjs │ ├── game.js │ └── game.ts ├── breakoutTutorial │ ├── 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 │ └── game.js └── style.css ├── src ├── engineFont.png ├── engineBuild.bat ├── jsconfig.json ├── engineRelease.js └── engineMedals.js ├── dist └── box2d.wasm.wasm ├── docs ├── examples │ ├── logo.png │ ├── favicon.png │ └── screenshot.jpg ├── static │ └── favicon.png ├── fonts │ ├── WorkSans-Bold.ttf │ ├── OpenSans-Regular.ttf │ └── Inconsolata-Regular.ttf ├── styles │ ├── clean-jsdoc-theme-scrollbar.css │ └── clean-jsdoc-theme-dark.css └── scripts │ ├── third-party │ └── hljs-line-num.js │ ├── resize.js │ └── search.min.js ├── plugins ├── box2d.wasm.wasm ├── pluginExport.js ├── zzfxm.js └── drawUtilities.js ├── jsconfig.json ├── .gitignore ├── package.json └── LICENSE /examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/logo.png -------------------------------------------------------------------------------- /examples/games.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/games.jpg -------------------------------------------------------------------------------- /examples/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/logo2.png -------------------------------------------------------------------------------- /src/engineFont.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/src/engineFont.png -------------------------------------------------------------------------------- /dist/box2d.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/dist/box2d.wasm.wasm -------------------------------------------------------------------------------- /docs/examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/examples/logo.png -------------------------------------------------------------------------------- /examples/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/favicon.png -------------------------------------------------------------------------------- /docs/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/static/favicon.png -------------------------------------------------------------------------------- /examples/box2d/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/box2d/tiles.png -------------------------------------------------------------------------------- /examples/empty/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/empty/tiles.png -------------------------------------------------------------------------------- /examples/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/screenshot.jpg -------------------------------------------------------------------------------- /examples/shorts/song.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/shorts/song.mp3 -------------------------------------------------------------------------------- /plugins/box2d.wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/plugins/box2d.wasm.wasm -------------------------------------------------------------------------------- /docs/examples/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/examples/favicon.png -------------------------------------------------------------------------------- /examples/breakout/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakout/tiles.png -------------------------------------------------------------------------------- /examples/electron/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/electron/tiles.png -------------------------------------------------------------------------------- /examples/htmlMenu/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/htmlMenu/tiles.png -------------------------------------------------------------------------------- /examples/module/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/module/tiles.png -------------------------------------------------------------------------------- /examples/puzzle/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/puzzle/tiles.png -------------------------------------------------------------------------------- /examples/shorts/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/shorts/tiles.png -------------------------------------------------------------------------------- /examples/shorts/video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/shorts/video.webm -------------------------------------------------------------------------------- /examples/starter/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/starter/tiles.png -------------------------------------------------------------------------------- /examples/uiSystem/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/uiSystem/tiles.png -------------------------------------------------------------------------------- /docs/examples/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/examples/screenshot.jpg -------------------------------------------------------------------------------- /docs/fonts/WorkSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/fonts/WorkSans-Bold.ttf -------------------------------------------------------------------------------- /examples/particles/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/particles/tiles.png -------------------------------------------------------------------------------- /examples/platformer/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/platformer/tiles.png -------------------------------------------------------------------------------- /examples/typescript/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/typescript/tiles.png -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /examples/electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "littlejsgame", 3 | "version": "1.0.0", 4 | "main": "electron.js" 5 | } -------------------------------------------------------------------------------- /docs/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/docs/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /examples/platformer/tilesLevel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/platformer/tilesLevel.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/1.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/10.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/11.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/2.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/3.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/4.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/5.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/6.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/7.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/8.png -------------------------------------------------------------------------------- /examples/breakoutTutorial/images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/LittleJS/HEAD/examples/breakoutTutorial/images/9.png -------------------------------------------------------------------------------- /examples/empty/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Hello World Demo 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/engineBuild.bat: -------------------------------------------------------------------------------- 1 | rem LittleJS Build Script 2 | call npm run build 3 | if %errorlevel% neq 0 ( 4 | echo Build failed with error level %errorlevel% 5 | pause 6 | exit /b %errorlevel% 7 | ) -------------------------------------------------------------------------------- /examples/starter/build.bat: -------------------------------------------------------------------------------- 1 | rem LittleJS Build Script 2 | call node build.mjs 3 | if %errorlevel% neq 0 ( 4 | echo Build failed with error level %errorlevel% 5 | pause 6 | exit /b %errorlevel% 7 | ) -------------------------------------------------------------------------------- /src/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "target": "ES2020", 5 | "module": "esnext" 6 | }, 7 | "exclude": [ 8 | "engineRelease.js", 9 | "engineExport.js" 10 | ] 11 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node files 2 | node_modules 3 | package-lock.json 4 | 5 | # example builds 6 | examples/starter/build 7 | examples/starter/*.zip 8 | examples/typescript/build 9 | examples/electron/build 10 | examples/electron/*win32* -------------------------------------------------------------------------------- /examples/platformer/data/gameLevelData.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/breakout/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Breakout Game 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/puzzle/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Puzzle Game 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/shorts/helloWorld.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // draw background gradient 4 | drawRectGradient(cameraPos, getCameraSize(), BLACK, WHITE); 5 | 6 | // draw text 7 | drawText('LittleJS Engine', vec2(0,3), 3); 8 | 9 | // draw a tile 10 | drawTile(vec2(sin(time)*3,-2), vec2(7), tile(3,128)); 11 | } -------------------------------------------------------------------------------- /examples/breakoutTutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Breakout Tutorial 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "es2020", 5 | "target": "es2020", 6 | "outDir": "build", 7 | "moduleResolution": "bundler", 8 | "skipLibCheck": true 9 | }, 10 | "include": [ 11 | "*.ts" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "build" 16 | ] 17 | } -------------------------------------------------------------------------------- /examples/box2d/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Box2D Example 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/module/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Module Demo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/typescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS TypeScript Demo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/uiSystem/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Starter Project 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/platformer/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Platforming Game 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Starter Project 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/shorts/fontImage.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // draw text with built in engine font image 4 | const font = engineFontImage; 5 | font.drawText('Engine Font', vec2(0,3), 2); 6 | 7 | // show every character in the 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 | 16 | font.drawText(s, vec2()); 17 | } -------------------------------------------------------------------------------- /examples/shorts/blending.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // additive blending 4 | setBlendMode(1); 5 | drawCircle(vec2(-8,-2), 7, rgb(0,0,.5)); 6 | drawCircle(vec2(-6, 2), 7, rgb(0,1,0)); 7 | drawCircle(vec2(-4,-2), 7, rgb(1,0,0)); 8 | 9 | // alpha blending 10 | setBlendMode(0); 11 | drawCircle(vec2(4,-2), 7, hsl(time/9 ,1,.5)); 12 | drawCircle(vec2(8,-2), 7, hsl(time/9+1/3,1,.5,.5)); 13 | drawCircle(vec2(6, 2), 7, hsl(time/9+2/3,1,.5,.5)); 14 | } -------------------------------------------------------------------------------- /examples/shorts/starfield.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // precreate variables to avoid overhead 4 | const pos = vec2(), size = vec2(), color = rgb(); 5 | for (let i=2e3; i--;) 6 | { 7 | // use math to generate random star positions 8 | const offset = time*(9+i**2.1%15) + i**2.3; 9 | pos.x = offset%70 - 35; 10 | pos.y = i/110 - 9; 11 | size.x = size.y = i%.11 + .07; 12 | color.set(1,1,1,sin(i)**4); 13 | drawRect(pos, size, color); 14 | } 15 | } -------------------------------------------------------------------------------- /examples/shorts/animation.js: -------------------------------------------------------------------------------- 1 | canvasClearColor = GRAY; 2 | 3 | function gameRender() 4 | { 5 | { 6 | // animate by changing frames 7 | const pos = vec2(-5, 2*abs(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 = 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 | } -------------------------------------------------------------------------------- /docs/styles/clean-jsdoc-theme-scrollbar.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | height: 0.3125rem; 3 | width: 0.3125rem; 4 | 5 | } 6 | 7 | ::-webkit-scrollbar-thumb, 8 | ::-webkit-scrollbar-track { 9 | border-radius: 1rem; 10 | } 11 | 12 | ::-webkit-scrollbar-track { 13 | background: #333; 14 | } 15 | 16 | ::-webkit-scrollbar-thumb { 17 | background: #555; 18 | outline: 0.06125rem solid #555; 19 | } 20 | 21 | 22 | .light ::-webkit-scrollbar-track { 23 | background: #ddd; 24 | 25 | } 26 | 27 | .light ::-webkit-scrollbar-thumb { 28 | background: #aaa; 29 | outline: 0.06125rem solid #aaa; 30 | } -------------------------------------------------------------------------------- /examples/shorts/texture.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // show the full texture 4 | let pos = vec2(); // 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(); // top left corner in pixels 8 | let tileSize = vec2(256); // source size in pixels 9 | let tileInfo = new TileInfo(tilePos, tileSize); // tile info 10 | 11 | // draw background 12 | drawRect(pos, size, GRAY); 13 | 14 | // draw the tile 15 | drawTile(pos, size, tileInfo, color); 16 | } -------------------------------------------------------------------------------- /examples/shorts/cameraDrag.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // create some objects 4 | for (let i=500; i--;) 5 | { 6 | const pos = randInCircle(100); 7 | const size = vec2(rand(2,9),rand(2,9)); 8 | const color = randColor(); 9 | const angle = rand(PI); 10 | new EngineObject(pos, size, 0, angle, color); 11 | } 12 | } 13 | 14 | function gameUpdate() 15 | { 16 | // drag camera with mouse 17 | if (mouseIsDown(0)) 18 | cameraPos = cameraPos.subtract(mouseDelta); 19 | 20 | // zoom camera with mouse wheel 21 | cameraScale = clamp(cameraScale*(1-mouseWheel/5), 1, 1e3); 22 | } -------------------------------------------------------------------------------- /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(), vec2(0,4).rotate(h/12*2*PI), 1); 19 | drawLine(vec2(), vec2(0,6).rotate(m/60*2*PI), .4); 20 | drawLine(vec2(), vec2(0,8).rotate(s/60*2*PI), .1); 21 | } -------------------------------------------------------------------------------- /examples/shorts/empty.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // called once after the engine starts up 4 | // setup the game 5 | } 6 | 7 | function gameUpdate() 8 | { 9 | // called every frame at 60 frames per second 10 | // handle input and update the game state 11 | } 12 | 13 | function gameUpdatePost() 14 | { 15 | // called after physics and objects are updated 16 | // setup camera and prepare for render 17 | } 18 | 19 | function gameRender() 20 | { 21 | // called before objects are rendered 22 | // draw any background effects that appear behind objects 23 | } 24 | 25 | function gameRenderPost() 26 | { 27 | // called after objects are rendered 28 | // draw effects or hud that appear above all objects 29 | drawText('LittleJS Engine', vec2(0,6), 3); 30 | } -------------------------------------------------------------------------------- /examples/shorts/shader.js: -------------------------------------------------------------------------------- 1 | const shader = ` 2 | void mainImage(out vec4 c, vec2 p) 3 | { 4 | // normalize coordinates 5 | vec2 uv = (p - iResolution.xy * .5) / iResolution.y; 6 | 7 | // get distance and angle from center 8 | float distance = length(uv); 9 | float angle = atan(uv.y, uv.x); 10 | 11 | // color based on angle and distance 12 | c.rgb = vec3( 13 | .5 + .5 * sin(angle + iTime), 14 | .5 + .5 * sin(angle + iTime * 2.), 15 | .5 + .5 * sin(distance * 5. - iTime) 16 | ); 17 | 18 | // apply glow in center 19 | c += .1 / (distance + .1); 20 | 21 | // apply sine wave brightness 22 | c *= .5 + .5 * sin(distance * 20. - iTime * 3.); 23 | } 24 | `; 25 | 26 | function gameInit() 27 | { 28 | new PostProcessPlugin(shader); 29 | } -------------------------------------------------------------------------------- /examples/shorts/shapes.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | // circles and ellipses 4 | drawEllipse(vec2(-6,5), vec2(3,2), YELLOW, .5); 5 | drawCircle(vec2(0,5), 2+wave(.5), RED); 6 | drawEllipse(vec2(6,5), vec2(2,4), CLEAR_BLACK, .3, .5, CYAN) 7 | 8 | // polygon shapes 9 | drawRectGradient(vec2(-6,0), vec2(5,4), RED, BLUE) 10 | drawRegularPoly(vec2(), vec2(4), 3, PURPLE, .3, WHITE, time); 11 | let starPath = [] 12 | for (let i=10; i--;) 13 | starPath.push(vec2(0,2*(i%2?.4:1)).rotate(i/10*PI*2)); 14 | drawPoly(starPath, BLUE, .1, GREEN, vec2(6, 0)); 15 | 16 | // rects and lines 17 | drawRect(vec2(0,-5), vec2(4,3), ORANGE, time); 18 | drawLine(vec2(-5,-7), vec2(5,-3), 1, hsl(0,0,1,.5)); 19 | const zPath = [vec2(5,-3), vec2(-5,-3), vec2(5,-7), vec2(-5,-7)]; 20 | drawLineList(zPath, .4, MAGENTA); 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, .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 lerping 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/electron/electron.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | 3 | function createWindow() 4 | { 5 | // create the browser window 6 | const mainWindow = new BrowserWindow({width: 800, height: 600}); 7 | 8 | // hide the toolbar menu 9 | mainWindow.setMenu(null); 10 | 11 | // load the index.html of the app 12 | mainWindow.loadFile('index.html'); 13 | 14 | // go fullscreen 15 | //mainWindow.setFullScreen(true); 16 | 17 | // open the dev tools debugging 18 | //mainWindow.webContents.openDevTools() 19 | } 20 | 21 | // wait until the app is ready and create the window 22 | app.whenReady().then(createWindow); 23 | 24 | // quit when all windows are closed (except macOS convention) 25 | app.on('window-all-closed', ()=> 26 | { 27 | if (process.platform != 'darwin') 28 | app.quit(); 29 | }); 30 | 31 | app.on('activate', ()=> 32 | { 33 | if (BrowserWindow.getAllWindows().length == 0) 34 | createWindow(); 35 | }); -------------------------------------------------------------------------------- /examples/shorts/topDown.js: -------------------------------------------------------------------------------- 1 | class Player extends EngineObject 2 | { 3 | constructor(pos) 4 | { 5 | super(pos, vec2(2), tile(5), 0, RED); 6 | this.setCollision(); // make object collide 7 | this.renderOrder = 1; // render player on top 8 | } 9 | 10 | update() 11 | { 12 | // apply movement controls 13 | const moveInput = keyDirection().clampLength(1).scale(.2); 14 | this.velocity = this.velocity.add(moveInput); 15 | 16 | // move camera with player 17 | cameraPos = this.pos; 18 | } 19 | } 20 | 21 | function gameInit() 22 | { 23 | // setup level 24 | canvasClearColor = hsl(.3,.2,.6); 25 | objectDefaultDamping = .7; 26 | new Player; 27 | 28 | // create collision objects 29 | for (let i=300; i--;) 30 | { 31 | const pos = randInCircle(15+i,7); 32 | const size = vec2(rand(4,9),rand(4,9)); 33 | const color = hsl(.1,.5,rand(.2)); 34 | const o = new EngineObject(pos, size, 0, 0, color); 35 | o.setCollision(); // make object collide 36 | o.mass = 0; // make object have static physics 37 | } 38 | } -------------------------------------------------------------------------------- /examples/shorts/debugDraw.js: -------------------------------------------------------------------------------- 1 | class BounceObject extends EngineObject 2 | { 3 | constructor(pos, size, tile) 4 | { 5 | super(pos, size, tile); 6 | this.velocity = vec2(.1); 7 | } 8 | 9 | update() 10 | { 11 | // show debug info 12 | debugText('Debug Text', this.pos.add(vec2(0,3)), 1, WHITE); 13 | debugRect(this.pos, this.size, RED); 14 | const trianglePoints = [vec2(-2,-1), vec2(0,2), vec2(2,-1)]; 15 | debugPoly(mousePos, trianglePoints, YELLOW); 16 | debugLine(this.pos, mousePos, BLUE); 17 | 18 | // bounce off screen edges 19 | const cameraSize = getCameraSize(); 20 | if (abs(this.pos.x) > cameraSize.x/2) 21 | { 22 | debugCircle(this.pos, 3, GREEN, 1); 23 | this.velocity.x = -this.velocity.x; 24 | } 25 | if (abs(this.pos.y) > cameraSize.y/2) 26 | { 27 | debugCircle(this.pos, 3, GREEN, 1); 28 | this.velocity.y = -this.velocity.y; 29 | } 30 | } 31 | } 32 | 33 | function gameInit() 34 | { 35 | canvasClearColor = GRAY; 36 | new BounceObject(vec2(), vec2(5), tile(3,128)); 37 | } -------------------------------------------------------------------------------- /examples/shorts/spriteAtlas.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // create a table of all sprites 4 | const gameTile = (i, size=16)=> tile(i, size); 5 | spriteAtlas = 6 | { 7 | circle: gameTile(0), 8 | crate: gameTile(1), 9 | icon: gameTile(2), 10 | circleBig: gameTile(2, 128), 11 | iconBig: gameTile(3, 128), 12 | }; 13 | canvasClearColor = GRAY; 14 | } 15 | 16 | function gameRender() 17 | { 18 | // draw a sprite from the atlas 19 | let pos = vec2(sin(time)*5,-3);// world position 20 | let angle = 0; // world space angle 21 | let size = vec2(9); // world space size 22 | let mirror = 0; // should tile be mirrored? 23 | let color = hsl(0,0,1); // color to multiply by 24 | let additive = hsl(0,0,0,0); // color to add to 25 | drawTile(pos, size, spriteAtlas.iconBig, color, angle, mirror, additive); 26 | 27 | // draw more sprites from the atlas 28 | drawTile(vec2(-7,4), vec2(5), spriteAtlas.crate); 29 | drawTile(vec2( 0,4), vec2(5), spriteAtlas.circle); 30 | drawTile(vec2( 7,4), vec2(5), spriteAtlas.circleBig); 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "littlejsengine", 3 | "version": "1.17.12", 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.mjs" 34 | }, 35 | "devDependencies": { 36 | "bestzip": "^2.2.1", 37 | "electron": "^38.2.0", 38 | "electron-packager": "^17.1.2", 39 | "google-closure-compiler": "~20230502.0.0", 40 | "roadroller": "~2.1.0", 41 | "typescript": "~5.1.6", 42 | "uglify-js": "~3.17.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/htmlMenu/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS HTML Menu Example 3 | 4 | 5 | 6 | 7 | 30 | 31 | 32 | 33 |
34 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /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 | // import LittleJS module 13 | import * as LJS from '../../dist/littlejs.esm.js'; 14 | import * as GameCharacter from './gameCharacter.js'; 15 | import * as Game from './game.js'; 16 | 17 | export class Player extends GameCharacter.Character 18 | { 19 | update() 20 | { 21 | // movement control 22 | this.moveInput = LJS.isUsingGamepad ? LJS.gamepadStick(0) : LJS.keyDirection(); 23 | this.holdingJump = LJS.keyIsDown('ArrowUp') || LJS.gamepadIsDown(0); 24 | this.holdingShoot = !LJS.isUsingGamepad && LJS.mouseIsDown(0) || LJS.keyIsDown('KeyZ') || LJS.gamepadIsDown(2); 25 | this.pressingThrow = LJS.keyIsDown('KeyC') || LJS.mouseIsDown(1) || LJS.gamepadIsDown(1); 26 | this.pressedDodge = LJS.keyIsDown('KeyX') || LJS.mouseIsDown(2) || LJS.gamepadIsDown(3); 27 | super.update(); 28 | } 29 | 30 | kill() 31 | { 32 | Game.addToDeaths(); 33 | super.kill(); 34 | } 35 | } -------------------------------------------------------------------------------- /plugins/pluginExport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Module Plugins Export 3 | */ 4 | 5 | export 6 | { 7 | // Newgrounds 8 | newgrounds, 9 | NewgroundsPlugin, 10 | NewgroundsMedal, 11 | 12 | // Post Process 13 | postProcess, 14 | PostProcessPlugin, 15 | 16 | // ZzFXMusic 17 | ZzFXMusic, 18 | 19 | // UI System 20 | uiSystem, 21 | uiDebug, 22 | uiSetDebug, 23 | UISystemPlugin, 24 | UIObject, 25 | UIText, 26 | UITextInput, 27 | UITile, 28 | UIButton, 29 | UICheckbox, 30 | UIScrollbar, 31 | UIVideo, 32 | 33 | // Box2D Physics 34 | box2d, 35 | box2dDebug, 36 | box2dSetDebug, 37 | box2dInit, 38 | Box2dPlugin, 39 | Box2dObject, 40 | Box2dStaticObject, 41 | Box2dKinematicObject, 42 | Box2dTileLayer, 43 | Box2dRaycastResult, 44 | Box2dJoint, 45 | Box2dTargetJoint, 46 | Box2dDistanceJoint, 47 | Box2dPinJoint, 48 | Box2dRopeJoint, 49 | Box2dRevoluteJoint, 50 | Box2dGearJoint, 51 | Box2dPrismaticJoint, 52 | Box2dWheelJoint, 53 | Box2dWeldJoint, 54 | Box2dFrictionJoint, 55 | Box2dPulleyJoint, 56 | Box2dMotorJoint, 57 | 58 | // Drawing Utilities 59 | drawNineSlice, 60 | drawNineSliceScreen, 61 | drawThreeSlice, 62 | drawThreeSliceScreen, 63 | } -------------------------------------------------------------------------------- /examples/shorts/particles.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | gravity.y = -.01; // set default gravity 4 | 5 | // fire 6 | new ParticleEmitter( 7 | vec2(-5,-2), 0, // pos, angle 8 | 2, 0, 200, PI, // emitSize, emitTime, rate, cone 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 // randomness, collide, additive, colorLinear 15 | ); 16 | 17 | // smoke 18 | new ParticleEmitter( 19 | vec2(5,-2), 0, // pos, angle 20 | 3, 0, 100, PI, // emitSize, emitTime, rate, cone 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 // randomness, collide, additive, colorLinear 27 | ); 28 | } -------------------------------------------------------------------------------- /examples/shorts/tileRaycast.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // create tile layer 4 | const pos = vec2(); 5 | const tileLayer = new TileCollisionLayer(pos, vec2(28,16), 0); 6 | cameraPos = tileLayer.size.scale(.5); // setup camera 7 | for (pos.x = tileLayer.size.x; pos.x--;) 8 | for (pos.y = tileLayer.size.y; pos.y--;) 9 | { 10 | // create random solid tiles away from level center 11 | if (rand() < .7 || cameraPos.distanceSquared(pos) < 9) 12 | continue; 13 | 14 | // set tile data 15 | const data = new TileLayerData(1); 16 | tileLayer.setData(pos, data); 17 | tileLayer.setCollisionData(pos); 18 | } 19 | tileLayer.redraw(); 20 | } 21 | 22 | function gameRenderPost() 23 | { 24 | // cast ray from camera center to mouse 25 | drawLine(cameraPos, mousePos, .1, CYAN); 26 | const normal = vec2(); 27 | const hit = tileCollisionRaycast(cameraPos, mousePos, 0, normal); 28 | if (hit) 29 | { 30 | // draw hit tile 31 | const tilePos = hit.floor().add(vec2(.5)); 32 | drawRect(tilePos, vec2(1), RED); 33 | 34 | // draw hit point and normal 35 | drawRect(hit, vec2(.2), GREEN); 36 | drawLine(cameraPos, hit, .1, RED); 37 | drawLine(hit, hit.add(normal), .1, YELLOW); 38 | } 39 | } -------------------------------------------------------------------------------- /examples/shorts/videoPlayer.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | new UISystemPlugin; 4 | uiSystem.defaultCornerRadius = 20; 5 | uiSystem.defaultShadowColor = BLACK; 6 | canvasClearColor = hsl(0,0,.1); 7 | 8 | // video player 9 | const center = mainCanvasSize.scale(.5); 10 | const videoPos = center.add(vec2(0, -50)); 11 | const videoSize = vec2(480, 270); 12 | const filename = 'video.webm'; 13 | const autoplay = true; 14 | videoPlayer = new UIVideo(videoPos, videoSize, filename, autoplay); 15 | videoPlayer.lineWidth = 5; 16 | videoPlayer.lineColor = WHITE; 17 | 18 | // play/pause button 19 | const buttonSize = vec2(200, 100); 20 | const playButtonPos = center.add(vec2(-130, 170)); 21 | const playButton = new UIButton(playButtonPos, buttonSize); 22 | playButton.onClick = ()=> videoPlayer.isPaused() ? 23 | videoPlayer.play() : videoPlayer.pause(); 24 | playButton.onUpdate = ()=> playButton.text = 25 | videoPlayer.isPaused() ? 'Play' : 'Pause'; 26 | 27 | // status text 28 | const statusTextPos = center.add(vec2(130, 170)); 29 | const statusText = new UIText(statusTextPos, vec2(250, 90)); 30 | statusText.textColor = WHITE; 31 | statusText.onUpdate = ()=> statusText.text = 32 | videoPlayer.hasEnded() ? 'Ended' : 33 | videoPlayer.isPlaying() ? 'Playing' : 'Paused'; 34 | } -------------------------------------------------------------------------------- /examples/shorts/platformer.js: -------------------------------------------------------------------------------- 1 | class Player extends EngineObject 2 | { 3 | constructor(pos) 4 | { 5 | super(pos, vec2(2,4), 0, 0, RED); 6 | this.setCollision(); // make object collide 7 | } 8 | 9 | update() 10 | { 11 | // apply movement controls 12 | const moveInput = keyDirection(); 13 | this.velocity.x += moveInput.x * (this.groundObject ? .1: .01); 14 | if (this.groundObject && moveInput.y > 0) 15 | this.velocity.y = .9; // jump 16 | 17 | // move camera with player 18 | cameraPos = vec2(this.pos.x, 9); 19 | } 20 | } 21 | 22 | function gameInit() 23 | { 24 | // setup level 25 | gravity.y = -.05; 26 | new Player(vec2(5,6)); 27 | canvasClearColor = hsl(.6,.3,.5); 28 | 29 | // create tile layer 30 | const pos = vec2(); 31 | const tileLayer = new TileCollisionLayer(pos, vec2(256)); 32 | for (pos.x = tileLayer.size.x; pos.x--;) 33 | for (pos.y = tileLayer.size.y; pos.y--;) 34 | { 35 | // check if tile should be solid 36 | const levelHeight = pos.x<9 ? 2 : (pos.x/4|0)**3.1%7; 37 | if (pos.y > levelHeight) 38 | continue; 39 | 40 | // set tile data 41 | tileLayer.setData(pos, new TileLayerData(1)); 42 | tileLayer.setCollisionData(pos); 43 | } 44 | tileLayer.redraw(); // redraw tile layer with new data 45 | } -------------------------------------------------------------------------------- /examples/shorts/piano.js: -------------------------------------------------------------------------------- 1 | const pianoSound = new Sound([,0,220,,9]); 2 | 3 | class PianoKey extends UIButton 4 | { 5 | constructor(pos, size, semitone, color, hoverColor) 6 | { 7 | const keySize = 65; 8 | size = size.scale(keySize); 9 | pos = pos.scale(keySize).add(vec2(0,size.y/2-keySize*2)); 10 | pos = pos.add(mainCanvasSize.scale(.5)); 11 | super(pos, size, '', color); 12 | 13 | this.dragActivate = true; 14 | this.semitone = semitone; 15 | this.hoverColor = hoverColor; 16 | this.activeColor = RED; 17 | } 18 | onPress() { this.sound = pianoSound.playNote(this.semitone); } 19 | onRelease() { this.sound.stop(.2); } 20 | } 21 | 22 | function gameInit() 23 | { 24 | // initialize UI system 25 | new UISystemPlugin; 26 | 27 | // create piano keyboard 28 | for (let i=15; i--;) 29 | { 30 | // white keys 31 | const pos = vec2(i-7, 0); 32 | const size = vec2(1, 4); 33 | const semitone = [0,2,4,5,7,9,11][i%7]+(i/7|0)*12; 34 | new PianoKey(pos, size, semitone, WHITE, hsl(0,1,.9)); 35 | } 36 | for (let i=10; i--;) 37 | { 38 | // black keys 39 | const pos = vec2([1,2,4,5,6][i%5]+(i/5|0)*7-7.5, 0) 40 | const size = vec2(1, 2); 41 | const semitone = [1,3,6,8,10][i%5]+(i/5|0)*12; 42 | new PianoKey(pos, size, semitone, hsl(0,0,.3), hsl(0,1,.3)); 43 | } 44 | } -------------------------------------------------------------------------------- /examples/shorts/pongGame.js: -------------------------------------------------------------------------------- 1 | let paddle, ball; 2 | 3 | class PhysicsObject extends EngineObject 4 | { 5 | constructor(pos, size) 6 | { 7 | super(pos, size); // set object position and size 8 | this.setCollision(); // make object collide 9 | this.mass = 0; // make object have static physics 10 | } 11 | } 12 | 13 | function gameInit() 14 | { 15 | // setup level 16 | const levelSize = vec2(38, 21); // size of play area 17 | cameraPos = levelSize.scale(.5); // center camera in level 18 | canvasFixedSize = vec2(1280, 720); // use a 720p fixed size canvas 19 | 20 | // create objects 21 | paddle = new PhysicsObject(vec2(0,1), vec2(6,1)); // player 22 | 23 | const w = levelSize.x, h = levelSize.y; 24 | new PhysicsObject(vec2(-.5,h/2), vec2(1,100)); // left wall 25 | new PhysicsObject(vec2(w+.5,h/2), vec2(1,100)); // right wall 26 | new PhysicsObject(vec2(w/2,h+.5), vec2(100,1)); // top wall 27 | } 28 | 29 | function gameUpdate() 30 | { 31 | if (!ball || ball.pos.y < -1) // spawn ball 32 | { 33 | ball && ball.destroy(); // destroy old ball 34 | ball = new PhysicsObject(cameraPos); // create a ball 35 | ball.velocity = vec2(.2); // give ball some movement 36 | ball.restitution = 1; // make ball bounce 37 | ball.mass = 1; // make ball have dynamic physics 38 | } 39 | 40 | paddle.pos.x = mousePos.x; // move paddle to mouse 41 | } -------------------------------------------------------------------------------- /examples/shorts/box2d.js: -------------------------------------------------------------------------------- 1 | async function gameInit() 2 | { 3 | // setup box2d 4 | await box2dInit(); 5 | mouseJoint = 0; 6 | gravity.y = -50; 7 | canvasClearColor = hsl(0,0,.9); 8 | 9 | // create ground object 10 | groundObject = new Box2dStaticObject(vec2(-8)); 11 | groundObject.color = GRAY; 12 | groundObject.addBox(vec2(100,2)); 13 | 14 | // add some random objects 15 | for (let i=50; i--;) 16 | { 17 | const pos = randInCircle(5); 18 | const color = randColor(); 19 | const o = new Box2dObject(pos, vec2(), 0, 0, color); 20 | randInt(2) ? o.addCircle(rand(1,2)) : o.addRandomPoly(rand(1,2)); 21 | } 22 | } 23 | 24 | function gameUpdate() 25 | { 26 | // mouse controls 27 | if (mouseJoint) 28 | { 29 | // update mouse joint 30 | mouseJoint.setTarget(mousePos); 31 | if (mouseWasReleased(0)) 32 | { 33 | // release object 34 | mouseJoint = mouseJoint.destroy(); 35 | } 36 | } 37 | else if (mouseWasPressed(0)) 38 | { 39 | // grab object 40 | const object = box2d.pointCast(mousePos); 41 | if (object) 42 | mouseJoint = new Box2dTargetJoint(object, 43 | groundObject, mousePos); 44 | } 45 | } 46 | 47 | function gameRenderPost() 48 | { 49 | // draw mouse joint 50 | mouseJoint && drawLine(mousePos, mouseJoint.getAnchorB(), .2, RED); 51 | } -------------------------------------------------------------------------------- /examples/shorts/flappyGame.js: -------------------------------------------------------------------------------- 1 | class Wall extends EngineObject 2 | { 3 | constructor(pos, size) 4 | { 5 | super(pos, size, 0, 0, hsl(.3,.5,.3)); 6 | this.setCollision(); // make object collide 7 | this.mass = 0; 8 | } 9 | update() 10 | { 11 | this.pos.x -= .1; // move walls to the left 12 | } 13 | } 14 | 15 | class Player extends EngineObject 16 | { 17 | constructor(pos) 18 | { 19 | super(pos, vec2(1), tile(9), 0, YELLOW); 20 | this.drawSize = vec2(2); 21 | this.setCollision(); // make object collide 22 | } 23 | 24 | update() 25 | { 26 | super.update(); 27 | 28 | // flappy movement controls 29 | if (mouseWasPressed(0) || keyWasPressed('Space')) 30 | this.velocity = vec2(0,.2); 31 | this.angle = -this.velocity.y*2; 32 | this.pos.y = max(-50, this.pos.y); 33 | } 34 | 35 | collideWithObject(object) 36 | { 37 | // reset game 38 | engineObjectsDestroy(); 39 | gameInit(); 40 | } 41 | } 42 | 43 | function gameInit() 44 | { 45 | // setup level 46 | gravity.y = -.01; 47 | for (let i=100; i--;) 48 | { 49 | const h=100, y=-h/2-6+rand(9), spacing=15, gap=5; 50 | new Wall(vec2(14 + i*spacing,y+h + gap), vec2(3,h)); 51 | new Wall(vec2(14 + i*spacing,y), vec2(3,h)); 52 | } 53 | new Player(vec2(-7,6)); 54 | canvasClearColor = hsl(.55,1,.8); 55 | } -------------------------------------------------------------------------------- /examples/starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | LittleJS Starter Project 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/engineRelease.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS - Release Mode 3 | * - Replaces engineDebug.js in production builds 4 | * - All debug functions are stubbed out as no-ops 5 | * - Removes ASSERT and LOG calls to reduce file size 6 | * - Disables debug overlay, watermark, and visualizations 7 | * - Improves performance by eliminating debug overhead 8 | * - Significantly reduces final bundle size 9 | */ 10 | 11 | 'use strict'; 12 | 13 | let debugWatermark = 0; 14 | let debugKey = ''; 15 | const debug = 0; 16 | const debugOverlay = 0; 17 | const debugPhysics = 0; 18 | const debugParticles = 0; 19 | const debugRaycast = 0; 20 | const debugGamepads = 0; 21 | const debugMedals = 0; 22 | 23 | // debug commands are automatically removed from the final build 24 | function ASSERT (){} 25 | function LOG (){} 26 | function debugInit (){} 27 | function debugUpdate (){} 28 | function debugRender (){} 29 | function debugRenderPost (){} 30 | function debugRect (){} 31 | function debugPoly (){} 32 | function debugCircle (){} 33 | function debugPoint (){} 34 | function debugLine (){} 35 | function debugOverlap (){} 36 | function debugText (){} 37 | function debugClear (){} 38 | function debugScreenshot (){} 39 | function debugShowErrors(){} 40 | function debugVideoCaptureIsActive(){ return false; } 41 | function debugVideoCaptureStart (){} 42 | function debugVideoCaptureStop (){} 43 | function debugVideoCaptureUpdate(){} 44 | function debugProtectConstant(o){ return o; } -------------------------------------------------------------------------------- /examples/shorts/sound.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // initialize UI system 4 | new UISystemPlugin; 5 | uiSystem.defaultShadowColor = BLACK; 6 | canvasClearColor = hsl(.6,.3,.2); 7 | 8 | // create a grid of buttons with different sounds 9 | const w = 200, h = 150, gap = 20; 10 | function makeSoundButton(pos, icon, sound) 11 | { 12 | pos = pos.multiply(vec2(w+gap, h+gap)); 13 | pos = pos.add(mainCanvasSize.scale(.5)); 14 | const button = new UIButton(pos, vec2(w, h), icon); 15 | button.textHeight = 100; 16 | button.onClick = ()=> sound.play(); 17 | } 18 | makeSoundButton(vec2(-1, 1),'💰', 19 | new Sound([,,1675,,.06,.24,1,1.82,,,837,.06])); 20 | makeSoundButton(vec2( 0, 1),'🥊', 21 | new Sound([,,925,.04,.3,.6,1,.3,,6.27,-184,.09,.17])); 22 | makeSoundButton(vec2( 1, 1),'✨', 23 | new Sound([,,539,0,.04,.29,1,1.92,,,567,.02,.02,,,,.04])); 24 | makeSoundButton(vec2(-1, 0),'🐁', 25 | new Sound([,.2,1e3,.02,,.01,2,,18,,475,.01,.01])); 26 | makeSoundButton(vec2( 0, 0),'🎹', 27 | new Sound([1.5,.5,270,,.1,,1,1.5,,,,,,,,.1,.01])); 28 | makeSoundButton(vec2( 1, 0),'🏌️', 29 | new Sound([,,150,.05,,.05,,1.3,,,,,,3])); 30 | makeSoundButton(vec2(-1,-1),'🌊', 31 | new Sound([,.2,40,.5,,1.5,,11,,,,,,199])); 32 | makeSoundButton(vec2( 0,-1),'🛰️', 33 | new Sound([,.5,847,.02,.3,.9,1,1.67,,,-294,.04,.13,,,,.1])); 34 | makeSoundButton(vec2( 1,-1),'⚡', 35 | new Sound([,,471,,.09,.47,4,1.06,-6.7,,,,,.9,61,.1,,.82,.1])); 36 | } -------------------------------------------------------------------------------- /examples/shorts/medals.js: -------------------------------------------------------------------------------- 1 | // create example medals 2 | const medal_openedExample = new Medal(0, 'Open', 'Opened this example!'); 3 | const medal_leftClick = new Medal(1, 'Lefty', 'Left clicked!', '🐁'); 4 | const medal_rightClick = new Medal(2, 'Righty', 'Right clicked!', '🐭'); 5 | const medal_spacePressed = new Medal(3, 'Space', 'Pressed spacebar!', '🚀'); 6 | 7 | function gameInit() 8 | { 9 | // setup medals 10 | const saveName = 'Medals Example'; 11 | medalsInit(saveName); 12 | 13 | // clear unlocked medals for testing 14 | medalsForEach(medal=> medal.unlocked = false); 15 | 16 | // unlock the example medal 17 | medal_openedExample.unlock(); 18 | 19 | // set background color 20 | canvasClearColor = hsl(.5,.3,.2); 21 | } 22 | 23 | function gameUpdate() 24 | { 25 | // unlock example medals based on input 26 | if (mouseWasPressed(0)) 27 | medal_leftClick.unlock(); 28 | if (mouseWasPressed(2)) 29 | medal_rightClick.unlock(); 30 | if (keyWasPressed('Space')) 31 | medal_spacePressed.unlock(); 32 | } 33 | 34 | function gameRenderPost() 35 | { 36 | const size = 80; 37 | let pos = mainCanvasSize.scale(.5).subtract(vec2(0,40)); 38 | drawTextScreen('Unlocked Medals', pos, size); 39 | 40 | // show unlocked medals 41 | let medalsCount = 0; 42 | medalsForEach(medal=> medal.unlocked && medalsCount++); 43 | pos = pos.add(vec2((1-medalsCount)*size/2, 100)); 44 | medalsForEach(medal=> 45 | { 46 | if (!medal.unlocked) 47 | return; 48 | medal.renderIcon(pos, size); 49 | pos.x += size + 8; 50 | }); 51 | } -------------------------------------------------------------------------------- /examples/shorts/nineSlice.js: -------------------------------------------------------------------------------- 1 | function gameRender() 2 | { 3 | const nineSliceTile = tile(16); 4 | const threeSliceTile = tile(19); 5 | 6 | { 7 | // draw nine slice with thin border and color 8 | const pos = vec2(-7,4); 9 | const size = vec2(11+wave(.5,2), 6); 10 | const color = hsl(.1,.5,.9); 11 | const border = .5; 12 | drawNineSlice(pos, size, nineSliceTile, color, border); 13 | drawText('Nine Slice\nThin Border', pos, 1, BLACK); 14 | } 15 | { 16 | // draw nine slice in screen space with thick border and rotation 17 | const pos = vec2(700,150); 18 | const size = vec2(250); 19 | const border = 32; 20 | const angle = time/2; 21 | drawNineSliceScreen(pos, size, nineSliceTile, border, 2, angle); 22 | drawTextScreen('Nine Slice\nScreen Space', pos, 30, BLACK); 23 | } 24 | { 25 | // draw three slice with variable border and additive color 26 | const pos = vec2(-7,-4); 27 | const size = vec2(9, 7); 28 | const border = 2 + wave(.2)*2; 29 | const additive = hsl(time/30,.5,.5); 30 | drawThreeSlice(pos, size, threeSliceTile, WHITE, border, additive); 31 | drawText('Three Slice\nVariable\nBorder', pos, 1, BLACK); 32 | } 33 | { 34 | // draw three slice in screen space with changing size 35 | const pos = vec2(700,420); 36 | const size = vec2(350-wave(.3,90),120+wave(.3,60)); 37 | drawThreeSliceScreen(pos, size, threeSliceTile); 38 | drawTextScreen('Three Slice\nScreen Space', pos, 30, BLACK); 39 | } 40 | } -------------------------------------------------------------------------------- /examples/shorts/maze.js: -------------------------------------------------------------------------------- 1 | function generateMaze(size) 2 | { 3 | const maze = [], w = size.x, h = size.y; 4 | maze[1 + w] = 1; // start point 5 | for (let k=w*h*9|0; k--;) 6 | { 7 | // get a random position on odd coordinates 8 | const jx = randInt(w/2-(w%2?0:2)|0)*2 + 1; 9 | const jy = randInt(h/2-(h%2?1:2)|0)*2 + 1; 10 | const j = jx + jy*w; 11 | 12 | // get a random direction 13 | const d = randSign() * (randBool() ? 1 : w); 14 | 15 | // check if we are not the same line or column 16 | if (j%w != (j+d*2)%w && (j/w|0) != ((j+d*2)/w|0)) 17 | continue; 18 | 19 | // check if pos is open and the next 2 cells are closed 20 | if (maze[j] && !maze[j+d] && !maze[j+d*2]) 21 | maze[j+d] = maze[j+d*2] = 1; 22 | } 23 | return maze; 24 | } 25 | 26 | function gameInit() 27 | { 28 | // generate maze data 29 | const mazeSize = vec2(27,15); 30 | const maze = generateMaze(mazeSize); 31 | 32 | // create tile layer 33 | const pos = vec2(); 34 | const tileLayer = new TileCollisionLayer(pos, mazeSize); 35 | for (pos.x = tileLayer.size.x; pos.x--;) 36 | for (pos.y = tileLayer.size.y; pos.y--;) 37 | { 38 | // check if tile should be solid 39 | if (maze[pos.x + pos.y*mazeSize.x]) 40 | continue; 41 | 42 | // set tile data 43 | tileLayer.setData(pos, new TileLayerData(1)); 44 | } 45 | tileLayer.redraw(); // redraw tile layer with new data 46 | cameraPos = mazeSize.scale(.5); // center camera 47 | canvasClearColor = hsl(rand(),.3,.2); // random background color 48 | } -------------------------------------------------------------------------------- /examples/shorts/tileLayer.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | cameraPos = vec2(16); // setup camera 4 | gravity.y = -.01; // enable gravity 5 | canvasClearColor = hsl(0,0,.2); // background color 6 | 7 | // create tile layer 8 | const pos = vec2(); 9 | const tileLayer = new TileCollisionLayer(pos, vec2(32)); 10 | for (pos.x = tileLayer.size.x; pos.x--;) 11 | for (pos.y = tileLayer.size.y; pos.y--;) 12 | { 13 | // check if tile should be solid 14 | if (randBool(.7)) 15 | continue; 16 | 17 | // set tile data 18 | const tileIndex = 11; 19 | const direction = randInt(4) 20 | const mirror = randBool(); 21 | const color = randColor(WHITE, hsl(0,0,.2)); 22 | const data = new TileLayerData(tileIndex, direction, mirror, color); 23 | tileLayer.setData(pos, data); 24 | tileLayer.setCollisionData(pos); 25 | } 26 | tileLayer.redraw(); // redraw tile layer with new data 27 | } 28 | 29 | function gameUpdate() 30 | { 31 | if (mouseWasPressed(0)) 32 | { 33 | // create particle emitter to test the collision 34 | const hue = rand(); 35 | const particleEmitter = new ParticleEmitter( 36 | mousePos, 0, 37 | 0, 0.1, 500, PI, 38 | tile(0, 16), 39 | hsl(hue,1,.5), hsl(hue,1,1), 40 | hsl(hue,1,.5,0), hsl(hue,1,1,0), 41 | 2, .2, .2, .2, .05, 42 | .99, 1, 1, PI, 43 | .05, .8, true 44 | ); 45 | particleEmitter.restitution = .5; // bounce when it collides 46 | particleEmitter.trailScale = 2; // stretch as it moves 47 | } 48 | } -------------------------------------------------------------------------------- /examples/electron/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 | // sound effects 14 | const sound_click = new Sound([1,.5]); 15 | 16 | /////////////////////////////////////////////////////////////////////////////// 17 | function gameInit() 18 | { 19 | setCanvasClearColor(GRAY); 20 | } 21 | 22 | /////////////////////////////////////////////////////////////////////////////// 23 | function gameUpdate() 24 | { 25 | if (mouseWasPressed(0)) 26 | { 27 | // play sound when mouse is pressed 28 | sound_click.play(mousePos); 29 | } 30 | } 31 | 32 | /////////////////////////////////////////////////////////////////////////////// 33 | function gameUpdatePost() 34 | { 35 | } 36 | 37 | /////////////////////////////////////////////////////////////////////////////// 38 | function gameRender() 39 | { 40 | // draw the logo as a tile 41 | drawTile(vec2(sin(time)*4, 0), vec2(10), tile(3,128)); 42 | } 43 | 44 | /////////////////////////////////////////////////////////////////////////////// 45 | function gameRenderPost() 46 | { 47 | drawTextScreen('LittleJS\nElectron Demo', 48 | vec2(mainCanvasSize.x/2, 100), 80, // position, size 49 | hsl(0,0,1), 6, hsl(0,0,0)); // color, outline size and color 50 | } 51 | 52 | /////////////////////////////////////////////////////////////////////////////// 53 | // Startup LittleJS Engine 54 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/shorts/box2dTileLayer.js: -------------------------------------------------------------------------------- 1 | let box2DTileLayer; 2 | 3 | async function gameInit() 4 | { 5 | // setup box2d 6 | await box2dInit(); 7 | cameraPos = vec2(16); // setup camera 8 | gravity.y = -30; // enable gravity 9 | canvasClearColor = hsl(0,0,.2); // background color 10 | 11 | // create tile layer 12 | const pos = vec2(); 13 | const tileLayer = new TileCollisionLayer(pos, vec2(32)); 14 | for (pos.x = tileLayer.size.x; pos.x--;) 15 | for (pos.y = tileLayer.size.y; pos.y--;) 16 | { 17 | // check if tile should be solid 18 | if (randBool(.7)) 19 | continue; 20 | 21 | // set tile data 22 | const tileIndex = 11; 23 | const direction = randInt(4) 24 | const mirror = randBool(); 25 | const color = randColor(WHITE, hsl(0,0,.2)); 26 | const data = new TileLayerData(tileIndex, direction, mirror, color); 27 | tileLayer.setData(pos, data); 28 | tileLayer.setCollisionData(pos); 29 | } 30 | tileLayer.redraw(); // redraw tile layer with new data 31 | box2DTileLayer = new Box2dTileLayer(tileLayer); 32 | } 33 | 34 | function gameUpdate() 35 | { 36 | if (mouseWasPressed(0)) 37 | { 38 | // clear tile that was clicked 39 | box2DTileLayer.tileLayer.clearData(mousePos, true); 40 | box2DTileLayer.tileLayer.clearCollisionData(mousePos); 41 | box2DTileLayer.buildCollision(); 42 | 43 | // spawn box2d object at mouse position 44 | const o = new Box2dObject(mousePos, vec2(), 0, 0, randColor()); 45 | const friction = .2, restitution = .5; 46 | o.addCircle(rand(.5,1), vec2(), 1, friction, restitution); 47 | } 48 | } -------------------------------------------------------------------------------- /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 | // import LittleJS module 10 | import * as LJS from '../../dist/littlejs.esm.js'; 11 | const {vec2, rgb} = LJS; 12 | 13 | /////////////////////////////////////////////////////////////////////////////// 14 | function gameInit() 15 | { 16 | // called once after the engine starts up 17 | // setup the game 18 | } 19 | 20 | /////////////////////////////////////////////////////////////////////////////// 21 | function gameUpdate() 22 | { 23 | // called every frame at 60 frames per second 24 | // handle input and update the game state 25 | } 26 | 27 | /////////////////////////////////////////////////////////////////////////////// 28 | function gameUpdatePost() 29 | { 30 | // called after physics and objects are updated 31 | // setup camera and prepare for render 32 | } 33 | 34 | /////////////////////////////////////////////////////////////////////////////// 35 | function gameRender() 36 | { 37 | // called before objects are rendered 38 | // draw any background effects that appear behind objects 39 | } 40 | 41 | /////////////////////////////////////////////////////////////////////////////// 42 | function gameRenderPost() 43 | { 44 | // called after objects are rendered 45 | // draw effects or hud that appear above all objects 46 | LJS.drawTextScreen('Hello World!', LJS.mainCanvasSize.scale(.5), 80); 47 | } 48 | 49 | /////////////////////////////////////////////////////////////////////////////// 50 | // Startup LittleJS Engine 51 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/shorts/slidingPuzzle.js: -------------------------------------------------------------------------------- 1 | const gridSize = vec2(4,3); 2 | const pieceSize = vec2(5,4); 3 | let emptyGridPos = vec2(); 4 | 5 | class PuzzlePiece extends EngineObject 6 | { 7 | constructor(gridPos, size) 8 | { 9 | const x = gridPos.x, y = gridPos.y; 10 | const color = rgb(x/(gridSize.x-1), y/(gridSize.y-1), 1); 11 | super(gridPos.multiply(size), size, 0, 0, color); 12 | this.gridPos = gridPos; 13 | this.text = x + y*gridSize.x; 14 | this.moveTimer = new Timer; 15 | this.lastPos = this.pos; 16 | } 17 | 18 | update() 19 | { 20 | const deltaX = emptyGridPos.x - this.gridPos.x; 21 | const deltaY = emptyGridPos.y - this.gridPos.y; 22 | if (mouseWasPressed(0)) 23 | if (this.isOverlapping(mousePos)) 24 | if (abs(deltaX) + abs(deltaY) === 1) 25 | { 26 | // swap with empty space when clicked on 27 | this.lastPos = this.pos; 28 | this.pos = emptyGridPos.multiply(this.size); 29 | [emptyGridPos, this.gridPos] = [this.gridPos, emptyGridPos]; 30 | this.moveTimer.set(.2); 31 | } 32 | } 33 | 34 | render() 35 | { 36 | const movePercent = this.moveTimer.getPercent(); 37 | const pos = this.lastPos.lerp(this.pos, movePercent); 38 | drawRect(pos, this.size, this.color); 39 | drawText(this.text, pos, 2.5, BLACK); 40 | } 41 | } 42 | 43 | function gameInit() 44 | { 45 | // create puzzle pieces 46 | for (let x=gridSize.x; x--;) 47 | for (let y=gridSize.y; y--;) 48 | (x||y) && new PuzzlePiece(vec2(x,y), pieceSize); 49 | 50 | // center camera on grid 51 | cameraPos = gridSize.subtract(vec2(1)).multiply(pieceSize).scale(.5); 52 | } -------------------------------------------------------------------------------- /examples/shorts/timers.js: -------------------------------------------------------------------------------- 1 | const timerSound = new Sound([2,0,999,,,,,1.5,,.3,-99,.1,1.63,,,.11]); 2 | 3 | function gameInit() 4 | { 5 | // setup ui system plugin 6 | new UISystemPlugin(); 7 | uiSystem.defaultCornerRadius = 10; 8 | uiSystem.defaultShadowColor = BLACK; 9 | canvasClearColor = hsl(.3,.3,.2); 10 | 11 | // create timer button 12 | const pos = mainCanvasSize.scale(.5).add(vec2(0, -40)); 13 | timerButton = new UIButton(pos, vec2(200, 90), 'Start'); 14 | timerButton.timer = new Timer; 15 | timerButton.onClick = ()=> 16 | { 17 | timerButton.isSet = true; 18 | if (timerButton.timer.isSet()) 19 | { 20 | timerSound.play(0, .5, 2); 21 | timerButton.timer.unset(); 22 | timerButton.text = 'Start'; 23 | } 24 | else 25 | { 26 | timerSound.play(0, .5, .5); 27 | timerButton.timer.set(3); 28 | timerButton.text = 'Stop'; 29 | } 30 | } 31 | 32 | // create non-interactive slider to display timer 33 | timerSlider = new UIScrollbar(vec2(0, 100), vec2(400, 50)); 34 | timerSlider.interactive = false; 35 | timerSlider.update = ()=> 36 | { 37 | if (timerButton.isSet && timerButton.timer.elapsed()) 38 | { 39 | timerSound.play(); 40 | timerButton.isSet = 0; 41 | } 42 | 43 | // update the timer display 44 | const t = timerButton.timer.get(); 45 | const timeText = t.toFixed(2) + 's'; 46 | const isSet = timerButton.timer.isSet(); 47 | const setTime = timerButton.timer.getSetTime(); 48 | timerSlider.text = timeText; 49 | timerSlider.value = setTime ? 1+t/setTime : 1; 50 | timerSlider.color = isSet ? t < 0 ? CYAN : RED : GRAY; 51 | } 52 | timerButton.addChild(timerSlider); 53 | } -------------------------------------------------------------------------------- /examples/shorts/hillGlideGame.js: -------------------------------------------------------------------------------- 1 | class Player extends EngineObject 2 | { 3 | constructor(pos) 4 | { 5 | super(pos, vec2(2), tile(9), 0, RED); 6 | this.damping = 1; // disable damping 7 | this.clampSpeed = false; // disable speed clamping 8 | } 9 | 10 | update() 11 | { 12 | // check ground height 13 | const h = getGroundHeight(this.pos.x); 14 | if (this.pos.y < h + this.size.y/2) 15 | { 16 | // clamp to ground and reflect velocity 17 | this.pos.y = h + this.size.y/2; 18 | const h2 = getGroundHeight(this.pos.x+.1); 19 | const n = vec2((h-h2)/.1, 1).normalize(); 20 | this.velocity = this.velocity.reflect(n,0); 21 | } 22 | 23 | // apply movement controls 24 | if (mouseIsDown(0) || keyIsDown('Space')) 25 | this.applyAcceleration(vec2(0,-.05)); 26 | this.velocity.x = max(this.velocity.x,.4); 27 | this.angle = this.velocity.angle() - PI/2; 28 | 29 | // move camera with player 30 | cameraPos = vec2(this.pos.x+9,5); 31 | } 32 | } 33 | 34 | function getGroundHeight(x) 35 | { 36 | return sin(x/4)*2 + sin(x/17); 37 | } 38 | 39 | function gameInit() 40 | { 41 | gravity.y = -.01; 42 | new Player(vec2(0,5)); 43 | canvasClearColor = hsl(.6,1,.8); 44 | } 45 | 46 | function gameRender() 47 | { 48 | // draw ground as a series of thin rectangles 49 | const h = 100, w = 20; 50 | const pos = vec2(); 51 | const sizeTop = vec2(.4); 52 | const size = vec2(.2,h); 53 | const color = rgb(); 54 | for (let x=cameraPos.x-w; x 2 | 3 | 4 | 5 | 26 | -------------------------------------------------------------------------------- /examples/shorts/box2dCar.js: -------------------------------------------------------------------------------- 1 | async function gameInit() 2 | { 3 | // setup box2d and create the objects 4 | await box2dInit(); 5 | gravity.y = -20; 6 | 7 | // create edge list for ground 8 | const edgePoints = []; 9 | for (let i=0, y=0, s=0; i<1e3; ++i) 10 | { 11 | y = clamp(y+rand(-1,1),0,5); 12 | edgePoints.push(vec2(i*5-15, y-8)); 13 | } 14 | const ground = new Box2dStaticObject; 15 | ground.lineWidth = 1; 16 | ground.addEdgeList(edgePoints); 17 | 18 | // make a car 19 | new CarObject(vec2(0,-2)); 20 | canvasClearColor = hsl(0,0,.9); 21 | } 22 | 23 | class CarObject extends Box2dObject 24 | { 25 | constructor(pos) 26 | { 27 | super(pos); 28 | 29 | // create car with wheels 30 | this.color = RED; 31 | this.addBox(vec2(7,2)); 32 | const frequency = 4, maxTorque = 250; 33 | this.wheels = []; 34 | for (let i=2; i--;) 35 | { 36 | const wheelPos = pos.add(vec2(i?2:-2, -1)); 37 | const wheel = new Box2dObject(wheelPos, vec2(2), tile(7)); 38 | const joint = new Box2dWheelJoint(this, wheel); 39 | joint.setSpringFrequencyHz(frequency); 40 | joint.setMaxMotorTorque(maxTorque); 41 | joint.enableMotor(!i); 42 | const friction = 20; 43 | wheel.addCircle(2, vec2(), 1, friction); 44 | wheel.motorJoint = joint; 45 | this.wheels[i] = wheel; 46 | } 47 | } 48 | update() 49 | { 50 | // car controls - use mouse, arrow keys, or A/D to drive 51 | const maxSpeed = 40; 52 | const input = mouseIsDown(0) ? 1 : 53 | mouseIsDown(2) ? -1 : keyDirection().x ; 54 | let s = this.wheels[0].motorJoint.getMotorSpeed(); 55 | s = input ? clamp(s - input, -maxSpeed, maxSpeed) : 0; 56 | this.wheels[0].motorJoint.setMotorSpeed(s); 57 | cameraPos.x = this.pos.x; 58 | } 59 | } -------------------------------------------------------------------------------- /examples/shorts/input.js: -------------------------------------------------------------------------------- 1 | function gameUpdate() 2 | { 3 | touchGamepadEnable = 1; 4 | if (isTouchDevice || isUsingGamepad) 5 | { 6 | if (isTouchDevice) 7 | { 8 | debugText('Touch Gamepad Mode', vec2(0,5)); 9 | 10 | // touch input is routed to mouse 11 | debugPoint(mousePos, mouseIsDown(0) ? RED : YELLOW, 1); 12 | } 13 | else 14 | { 15 | debugText('Gamepad Mode', vec2(0,5)); 16 | debugText('Primary Gamepad: ' + gamepadPrimary, vec2(0,4)); 17 | } 18 | 19 | // analog sticks 20 | for (let i=2; i--;) 21 | { 22 | const stick = gamepadStick(i); 23 | const pos = vec2(i?3:-3, 1); 24 | debugCircle(pos, 4, WHITE); 25 | debugLine(pos, pos.add(stick.scale(2)), GREEN); 26 | } 27 | 28 | // buttons 29 | for (let i=16; i--;) 30 | { 31 | const pos = vec2(-7 + i%8*2, -3 - (i/8|0)*2); 32 | if (gamepadIsDown(i)) 33 | debugCircle(pos, 2, RED, 0, 1); 34 | debugCircle(pos, 2, WHITE); 35 | debugText(i, pos); 36 | } 37 | } 38 | else 39 | { 40 | debugText('Mouse and Keyboard Mode', vec2(0,5)); 41 | 42 | // keyboard key (space bar) 43 | debugRect(vec2(), vec2(4), WHITE); 44 | if (keyIsDown('Space')) 45 | debugRect(vec2(), vec2(4), RED, 0, 0, 1); 46 | 47 | // keyboard direction (arrow keys or WASD) 48 | const inputDirection = keyDirection(); 49 | debugLine(vec2(), inputDirection.scale(2), GREEN); 50 | 51 | // mouse pos 52 | debugPoint(mousePos, mouseIsDown(0) ? RED : YELLOW, 1); 53 | 54 | // mouse buttons 55 | for (let i=3; i--;) 56 | { 57 | const pos = vec2(-2 + i*2, -5); 58 | if (mouseIsDown(i)) 59 | debugCircle(pos, 2, RED, 0, 1); 60 | debugCircle(pos, 2, WHITE); 61 | debugText(i, pos); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /examples/shorts/tiltedView.js: -------------------------------------------------------------------------------- 1 | class GameObject extends EngineObject 2 | { 3 | update() 4 | { 5 | this.renderOrder = -this.pos.y; // sort by y position 6 | } 7 | 8 | render() 9 | { 10 | // adjust draw position to be at the bottom of the object 11 | const drawSize = this.drawSize || this.size; 12 | const offset = this.getUp(drawSize.y/2); 13 | const pos = this.pos.add(offset); 14 | drawTile(pos, drawSize, this.tileInfo, this.color, this.angle); 15 | } 16 | } 17 | 18 | class Player extends GameObject 19 | { 20 | update() 21 | { 22 | super.update(); 23 | 24 | // apply movement controls 25 | const moveInput = keyDirection().clampLength(1).scale(.2); 26 | this.velocity = this.velocity.add(moveInput); 27 | this.setCollision(); // make object collide 28 | 29 | // move camera with player 30 | cameraPos = this.pos.add(vec2(0,2)); 31 | } 32 | } 33 | 34 | function gameInit() 35 | { 36 | // setup level 37 | objectDefaultDamping = .7; 38 | const player = new Player(vec2(), vec2(3,1), tile(5), 0, RED); 39 | player.drawSize = vec2(3); 40 | 41 | // create background objects 42 | for (let i=1; i<300; ++i) 43 | { 44 | const pos = randInCircle(90); 45 | const size = vec2(rand(59), rand(59)); 46 | const color = hsl(.4,.2,rand(.4,.5),.8); 47 | new EngineObject(pos, size, 0, 0, color, -1e5); 48 | } 49 | 50 | // create world objects 51 | for (let i=1; i<1e3; ++i) 52 | { 53 | const pos = randInCircle(7+i,7); 54 | const isRock = randBool(); 55 | const size = vec2(isRock ? rand(2,4) : rand(1,2)); 56 | const color = hsl(.1,isRock ? 0 : .5,rand(.2,.3)); 57 | const o = new GameObject(pos, size, 0, 0, color); 58 | o.setCollision(); // make object collide 59 | o.mass = 0; // make object have static physics 60 | o.angle = rand(-.1,.1); // random tilt 61 | o.drawSize = vec2(size.x, isRock ? rand(2,4) : rand(5,10)); 62 | } 63 | } -------------------------------------------------------------------------------- /examples/typescript/build.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS TypeScript Example Build System 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import { fileURLToPath } from 'node:url'; 8 | import { dirname, join } from 'node:path'; 9 | import fs from 'node:fs'; 10 | import { execSync } from 'node:child_process'; 11 | 12 | const __dirname = dirname(fileURLToPath(import.meta.url)); 13 | const PROGRAM_NAME = 'game'; 14 | const BUILD_FOLDER = join(__dirname, 'build'); 15 | 16 | // Define TypeScript source files 17 | const tsSourceFiles = [ 18 | 'game.ts', 19 | // add your TypeScript files here 20 | ]; 21 | 22 | // Corresponding JS output files 23 | const jsSourceFiles = tsSourceFiles.map(file => file.replace('.ts', '.js')); 24 | 25 | console.log(`Building TypeScript for ${PROGRAM_NAME}...`); 26 | const startTime = Date.now(); 27 | 28 | console.log(`Removing old build folder...`); 29 | fs.rmSync(BUILD_FOLDER, { recursive: true, force: true }); 30 | 31 | console.log(`Compiling TypeScript...`); 32 | 33 | // Use tsconfig.json for compilation settings 34 | try 35 | { 36 | const result = execSync(`npx -p typescript tsc`, {cwd: __dirname, encoding: 'utf8', stdio: 'pipe'}); 37 | console.log(result); 38 | } catch (error) 39 | { 40 | console.error('TypeScript compilation errors:'); 41 | if (error.stdout) console.log(error.stdout); 42 | if (error.stderr) console.error(error.stderr); 43 | console.error('TypeScript compilation failed!'); 44 | process.exit(1); 45 | } 46 | 47 | console.log(`Copying js files back to root...`); 48 | for (const file of jsSourceFiles) 49 | { 50 | // TypeScript outputs to build/examples/typescript/ because of relative paths 51 | const buildFile = join(BUILD_FOLDER, 'examples', 'typescript', file); 52 | const targetFile = join(__dirname, file); 53 | console.log(`Copying ${file}...`); 54 | if (fs.existsSync(buildFile)) 55 | fs.copyFileSync(buildFile, targetFile); 56 | else 57 | console.error(`✗ Build file not found: ${buildFile}`); 58 | } 59 | 60 | console.log(`TypeScript built in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`); -------------------------------------------------------------------------------- /examples/shorts/postProcess.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | new PostProcessPlugin(tvShader); 4 | } 5 | 6 | function gameRender() 7 | { 8 | drawRect(vec2(), vec2(99), GRAY); 9 | drawTile(vec2(sin(time)*3, 0), vec2(12), tile(3,128)); 10 | } 11 | 12 | const tvShader = ` 13 | // Simple TV Shader Code 14 | float hash(vec2 p) 15 | { 16 | p=fract(p*.3197); 17 | return fract(1.+sin(51.*p.x+73.*p.y)*13753.3); 18 | } 19 | 20 | void mainImage(out vec4 c, vec2 p) 21 | { 22 | // setup the shader 23 | vec2 uv = p; 24 | p /= iResolution.xy; 25 | c = texture(iChannel0, p); 26 | 27 | // static noise 28 | const float staticAlpha = .1; 29 | const float staticScale = .002; 30 | c += staticAlpha * hash(floor(p/staticScale) + mod(iTime*500., 1e3)); 31 | 32 | // scan lines 33 | const float scanlineScale = 2.; 34 | const float scanlineAlpha = .5; 35 | c *= 1. - scanlineAlpha*cos(p.y*2.*iResolution.y/scanlineScale); 36 | 37 | { 38 | // bloom effect 39 | const float blurSize = .002; 40 | const float bloomIntensity = .2; 41 | 42 | // 5-tap Gaussian blur 43 | vec4 bloom = vec4(0); 44 | bloom += texture(iChannel0, p + vec2(-2.*blurSize, 0)) * .12; 45 | bloom += texture(iChannel0, p + vec2( -blurSize, 0)) * .24; 46 | bloom += texture(iChannel0, p) * .28; 47 | bloom += texture(iChannel0, p + vec2( blurSize, 0)) * .24; 48 | bloom += texture(iChannel0, p + vec2( 2.*blurSize, 0)) * .12; 49 | bloom += texture(iChannel0, p + vec2(0, -2.*blurSize)) * .12; 50 | bloom += texture(iChannel0, p + vec2(0, -blurSize)) * .24; 51 | bloom += texture(iChannel0, p) * .28; 52 | bloom += texture(iChannel0, p + vec2(0, blurSize)) * .24; 53 | bloom += texture(iChannel0, p + vec2(0, 2.*blurSize)) * .12; 54 | c += bloom * bloomIntensity; 55 | } 56 | 57 | // black vignette around edges 58 | const float vignette = 2.; 59 | const float vignettePow = 6.; 60 | float dx = 2.*p.x-1., dy = 2.*p.y-1.; 61 | c *= 1.-pow((dx*dx + dy*dy)/vignette, vignettePow); 62 | }`; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LittleJS Engine License (MIT License) 2 | 3 | Copyright (c) 2021 Frank Force http://www.frankforce.com 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 | Box2D License 26 | 27 | Copyright (c) 2006-2013 Erin Catto http://www.gphysics.com 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. -------------------------------------------------------------------------------- /examples/shorts/uiSystem.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // setup ui system plugin 4 | new UISystemPlugin; 5 | uiSystem.defaultSoundPress = new Sound([.5,0,220]); 6 | uiSystem.defaultSoundClick = new Sound([.5,0,440]); 7 | uiSystem.defaultCornerRadius = 8; 8 | uiSystem.defaultShadowColor = BLACK; 9 | 10 | // setup example menu 11 | let navigationIndex = 0; 12 | const uiMenu = new UIObject(mainCanvasSize.scale(.5), vec2(700,450)); 13 | canvasClearColor = hsl(0,0,.8); 14 | 15 | // example text 16 | uiMenu.addChild(new UIText(vec2(-100,-120), vec2(450, 80), 17 | 'LittleJS UI\nSystem Demo')); 18 | 19 | // example image 20 | uiMenu.addChild(new UITile(vec2(230,-140), vec2(170), tile(3, 128))); 21 | 22 | // example checkbox 23 | const checkbox = new UICheckbox(vec2(-170,0), vec2(50)); 24 | checkbox.navigationIndex = ++navigationIndex; 25 | checkbox.onChange = ()=> button1.disabled = checkbox.checked; 26 | uiMenu.addChild(checkbox); 27 | 28 | // example button 29 | const textInput = new UITextInput(vec2(50,0), vec2(300, 80), 'Text Input'); 30 | textInput.textHeight = 60; 31 | textInput.maxLength = 16; 32 | textInput.navigationIndex = ++navigationIndex; 33 | uiMenu.addChild(textInput); 34 | textInput.onChange = ()=> canvasClearColor = randColor(); 35 | 36 | // example scrollbar 37 | const scrollbar = new UIScrollbar(vec2(0,90), vec2(400, 50), 38 | soundVolume, 'Volume'); 39 | scrollbar.navigationIndex = ++navigationIndex; 40 | uiMenu.addChild(scrollbar); 41 | scrollbar.onChange = ()=> setSoundVolume(scrollbar.value); 42 | 43 | // exit button 44 | const button1 = new UIButton(vec2(0,170), vec2(200, 50), 'Exit Menu'); 45 | button1.textHeight = 40; 46 | button1.navigationIndex = ++navigationIndex; 47 | button1.navigationAutoSelect = true; 48 | uiMenu.addChild(button1); 49 | button1.onClick = ()=> uiSystem.showConfirmDialog('Exit menu?', 50 | ()=> { uiMenu.visible=false; buttonBack.visible=true; }); 51 | 52 | // example button that returns to menu 53 | const buttonBack = new UIButton(mainCanvasSize.scale(.5), vec2(200), 54 | 'Back\nto\nMenu'); 55 | buttonBack.visible = false; 56 | buttonBack.textHeight = 60; 57 | buttonBack.navigationIndex = ++navigationIndex; 58 | buttonBack.navigationAutoSelect = true; 59 | buttonBack.onClick = ()=> 60 | { uiMenu.visible=true; buttonBack.visible=false; } 61 | } -------------------------------------------------------------------------------- /examples/shorts/music.js: -------------------------------------------------------------------------------- 1 | const musicSound = new Sound('song.mp3'); 2 | let musicVolume = .8, musicInstance; 3 | 4 | function gameInit() 5 | { 6 | // setup ui system plugin 7 | new UISystemPlugin; 8 | uiSystem.defaultSoundPress = new Sound([.5,0,220]); 9 | uiSystem.defaultSoundClick = new Sound([.5,0,440]); 10 | uiSystem.defaultCornerRadius = 20; 11 | uiSystem.defaultShadowColor = BLACK; 12 | canvasClearColor = hsl(.9,.3,.2); 13 | 14 | // setup music player UI 15 | const center = mainCanvasSize.scale(.5); 16 | musicPlayer = new UIObject(center, vec2(500, 220)); 17 | 18 | // information text 19 | infoText = new UIText(vec2(0, -70), vec2(400, 50)); 20 | musicPlayer.addChild(infoText); 21 | 22 | // volume slider 23 | const volumeSlider = new UIScrollbar(vec2(0, -20), vec2(400, 30), 24 | musicVolume, 'Music Volume'); 25 | volumeSlider.fillMode = true; 26 | musicPlayer.addChild(volumeSlider); 27 | volumeSlider.onChange = ()=> 28 | { 29 | musicVolume = volumeSlider.value; 30 | musicInstance?.setVolume(musicVolume); 31 | }; 32 | 33 | // play button 34 | playButton = new UIButton(vec2(-90, 50), vec2(140, 50), 'Play'); 35 | musicPlayer.addChild(playButton); 36 | playButton.onClick = ()=> 37 | { 38 | if (!musicSound.isLoaded()) 39 | return; 40 | 41 | // handle play/pause toggle 42 | if (!musicInstance) 43 | musicInstance = musicSound.playMusic(musicVolume); 44 | else if (musicInstance.isPaused()) 45 | musicInstance.resume(); 46 | else 47 | musicInstance.pause(); 48 | }; 49 | 50 | // stop button 51 | stopButton = new UIButton(vec2(90, 50), vec2(140, 50), 'Stop'); 52 | stopButton.onClick = ()=> musicInstance?.stop(); 53 | musicPlayer.addChild(stopButton); 54 | } 55 | 56 | function gameUpdate() 57 | { 58 | // disable buttons while loading 59 | const isDisabled = !musicSound.isLoaded(); 60 | playButton.disabled = stopButton.disabled = isDisabled; 61 | 62 | // update ui 63 | if (isDisabled) 64 | { 65 | // update loading progress 66 | const loadingPercent = musicSound.loadedPercent * 100|0; 67 | infoText.text = `Loading: ${loadingPercent}%`; 68 | } 69 | else 70 | { 71 | // update ui text 72 | const isPlaying = musicInstance?.isPlaying(); 73 | playButton.text = isPlaying ? 'Pause' : 'Play'; 74 | const current = formatTime(musicInstance?.getCurrentTime()); 75 | const duration = formatTime(musicSound.getDuration()); 76 | infoText.text = current + ' / ' + duration; 77 | } 78 | } -------------------------------------------------------------------------------- /examples/shorts/parallax.js: -------------------------------------------------------------------------------- 1 | function gameInit() 2 | { 3 | // create parallax layers 4 | const levelColor = hsl(rand(), .5, .5); 5 | for (let i=3; i--;) 6 | { 7 | const topColor = levelColor.mutate(.3); 8 | const bottomColor = levelColor.subtract(CLEAR_WHITE).mutate(.3); 9 | new ParallaxLayer(topColor, bottomColor, i); 10 | } 11 | } 12 | 13 | class ParallaxLayer extends CanvasLayer 14 | { 15 | constructor(topColor, bottomColor, depth) 16 | { 17 | const renderOrder = depth; 18 | const canvasSize = vec2(512, 256); 19 | super(vec2(), vec2(), 0, renderOrder, canvasSize); 20 | this.depth = depth; 21 | 22 | // create a gradient for the mountains 23 | const w = canvasSize.x, h = canvasSize.y; 24 | for (let i = h; i--;) 25 | { 26 | // draw a 1 pixel gradient line on the left side 27 | const p = i/h; 28 | this.context.fillStyle = topColor.lerp(bottomColor, p); 29 | this.context.fillRect(0, i, 1, 1); 30 | } 31 | 32 | // draw random mountains 33 | const pointiness = .2; // how pointy the mountains are 34 | const levelness = .005; // how much the mountains level out 35 | const slopeRange = 1; // max slope of the mountains 36 | const startGroundLevel = h/2; 37 | let y = startGroundLevel; 38 | let groundSlope = rand(-slopeRange, slopeRange); 39 | for (let x=w; x--;) 40 | { 41 | // pull slope towards start ground level 42 | y += groundSlope -= (y-startGroundLevel)*levelness; 43 | 44 | // randomly change slope 45 | if (rand() < pointiness) 46 | groundSlope = rand(-slopeRange, slopeRange); 47 | 48 | // draw 1 pixel wide vertical slice of mountain 49 | this.context.drawImage(this.canvas, 0,0,1,h,x,y,1,h-y); 50 | } 51 | 52 | // remove gradient sliver from left side 53 | this.context.clearRect(0,0,1,h); 54 | 55 | // make WebGL texture 56 | this.updateWebGL(); 57 | } 58 | 59 | render() 60 | { 61 | const canvasSize = vec2(this.canvas.width, this.canvas.height); 62 | const viewerPos = mousePos; 63 | const depth = this.depth 64 | const distance = 3 + depth; 65 | const parallax = vec2(.2, .05).scale(depth**2+1); 66 | const cameraDeltaFromCenter = viewerPos.multiply(parallax); 67 | const positionOffset = vec2(0, 4-depth*3); 68 | this.pos = cameraDeltaFromCenter.add(positionOffset) 69 | this.size = canvasSize.scale(distance/cameraScale); 70 | super.render(); 71 | } 72 | } -------------------------------------------------------------------------------- /examples/htmlMenu/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS HTML Menu Example Project 3 | - Setup a simple html menu system 4 | - Menu can be opened and closed 5 | - Pauses game when menu is visible 6 | - Shows several input types 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // import LittleJS module 12 | import * as LJS from '../../dist/littlejs.esm.js'; 13 | const {vec2, hsl, tile} = LJS; 14 | 15 | // sound effects 16 | const sound_click = new LJS.Sound([1,.5]); 17 | 18 | /////////////////////////////////////////////////////////////////////////////// 19 | 20 | // html menu system 21 | const getMenuVisible = ()=> menu.style.visibility != 'hidden'; 22 | function setMenuVisible(visible) 23 | { 24 | menu.style.visibility = visible ? 'visible' : 'hidden'; 25 | LJS.setInputPreventDefault(!visible); // don't prevent default when menu is visible 26 | } 27 | 28 | /////////////////////////////////////////////////////////////////////////////// 29 | function gameInit() 30 | { 31 | button_test.onclick = function() { alert('Button was clicked!'); } 32 | button_exitMenu.onclick = function() { setMenuVisible(false); } 33 | input_test.onchange = function() { alert('New text: ' + this.value); } 34 | input_rangeTest.onchange = function() { alert('New value: ' + this.value); } 35 | LJS.setCanvasClearColor(hsl(0,0,.2)); 36 | 37 | // show menu for demo 38 | setMenuVisible(true); 39 | } 40 | 41 | /////////////////////////////////////////////////////////////////////////////// 42 | function gameUpdate() 43 | { 44 | // play sound when mouse is pressed 45 | if (LJS.mouseWasPressed(0)) 46 | sound_click.play(LJS.mousePos); 47 | } 48 | 49 | /////////////////////////////////////////////////////////////////////////////// 50 | function gameUpdatePost() 51 | { 52 | // open menu visibility 53 | if (LJS.keyWasPressed('KeyM')) 54 | setMenuVisible(true); 55 | 56 | // pause game when menu is visible 57 | LJS.setPaused(getMenuVisible()); 58 | } 59 | 60 | /////////////////////////////////////////////////////////////////////////////// 61 | function gameRender() 62 | { 63 | // test game rendering 64 | for (let i=0; i<1e3; ++i) 65 | { 66 | const time = LJS.time; 67 | const pos = vec2(30*LJS.sin(i+time/9),20*LJS.sin(i*i+time/9)); 68 | LJS.drawTile(pos, vec2(2), tile(3,128), hsl(i/9,1,.4), time+i, !(i%2), hsl(i/9,1,.1,0)); 69 | } 70 | } 71 | 72 | /////////////////////////////////////////////////////////////////////////////// 73 | function gameRenderPost() 74 | { 75 | LJS.drawTextScreen('LittleJS HTML Menu Example\nM = Open menu', 76 | vec2(LJS.mainCanvasSize.x/2, 70), 60, // position, size 77 | hsl(0,0,1), 6, hsl(0,0,0)); // color, outline size and color 78 | } 79 | 80 | /////////////////////////////////////////////////////////////////////////////// 81 | // Startup LittleJS Engine 82 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /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 (floor(p.x)**3&floor(p.y)**2)%30>5; 6 | 7 | function gameUpdate() 8 | { 9 | // update camera angle with mouse pointer lock 10 | if (mouseWasPressed(0)) 11 | pointerLockRequest(); 12 | if (keyWasPressed('Escape')) 13 | pointerLockExit() 14 | if (pointerLockIsActive() || isTouchDevice) 15 | playerAngle += mouseDelta.x * .03; 16 | 17 | // update player movement, prevent walking through walls 18 | const velocity = keyDirection().rotate(playerAngle).scale(.05); 19 | const normal = vec2(); 20 | let newPos = playerPos.add(velocity); 21 | if (lineTest(playerPos, newPos, levelTest, normal)) 22 | { 23 | // adjust velocity to slide along wall 24 | const d = velocity.dot(normal); 25 | newPos = newPos.subtract(vec2(d*normal.x, d*normal.y)); 26 | if (!levelTest(newPos)) 27 | playerPos = newPos; 28 | } 29 | else 30 | playerPos = newPos; 31 | } 32 | 33 | function gameRender() 34 | { 35 | { 36 | // draw horizontal slices to create floor and ceiling 37 | const h = 9; 38 | let pos = vec2(), size = vec2(39, .15), color = rgb(); 39 | for (let y=-h; y0?0:.5, .7-p*.7); 43 | pos.y = y; 44 | drawRect(pos, size, color); 45 | } 46 | } 47 | { 48 | // draw vertical slices to create the walls 49 | // create objects in advance for optimal performance 50 | const w = 15; 51 | const maxDistance = 50; 52 | const pos = vec2(), endPos = vec2(), size = vec2(.15); 53 | const tileInfo = new TileInfo(vec2(), vec2(0,16)); 54 | const normal = vec2(), light = vec2().setAngle(2); 55 | const color = rgb(); 56 | for (pos.x=-w; pos.x${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()}); -------------------------------------------------------------------------------- /examples/style.css: -------------------------------------------------------------------------------- 1 | html, body 2 | { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | #container1 8 | { 9 | height: 100%; 10 | display: flex; 11 | gap: 4px; 12 | } 13 | #container2 14 | { 15 | display: flex; 16 | flex-direction: column; 17 | gap: 4px; 18 | } 19 | #iframeContainer 20 | { 21 | border: 2px solid; 22 | background: #000; 23 | } 24 | iframe 25 | { 26 | width: 100%; 27 | height: 100%; 28 | border: none; 29 | } 30 | #selectExample 31 | { 32 | flex-grow: 1; 33 | min-height: 100px; 34 | } 35 | #divTextareas 36 | { 37 | flex-grow: 1; 38 | display: flex; 39 | flex-direction: column; 40 | min-height: 300px; 41 | } 42 | .CodeMirror, 43 | #textareaCode 44 | { 45 | flex-grow: 1; 46 | resize: none; 47 | color: #fff; 48 | background: #000; 49 | } 50 | #textareaConsole, 51 | #textareaError 52 | { 53 | flex-grow: .3; 54 | resize: none; 55 | display: none; 56 | color:#f22; 57 | background: #111; 58 | } 59 | #textareaConsole 60 | { 61 | color: #ff0; 62 | } 63 | #container3 64 | { 65 | width: 100%; 66 | height: 100%; 67 | display: flex; 68 | flex-direction: column; 69 | gap: 2px; 70 | } 71 | #titleInfo 72 | { 73 | margin: 5px; 74 | text-align: center; 75 | font-size: 40px; 76 | font-weight: bold; 77 | } 78 | #exampleInfo 79 | { 80 | margin: 0px; 81 | text-align: center; 82 | font-size: 20px; 83 | } 84 | #exampleLink 85 | { 86 | margin: 0px; 87 | text-align: center; 88 | font-size: 15px; 89 | display: inline-block; 90 | align-self: center; 91 | font-style: italic; 92 | } 93 | select 94 | { 95 | color:#fff; 96 | background-color: #111; 97 | width:100%; 98 | } 99 | hr 100 | { 101 | border: 1px solid #555; 102 | margin:9px; 103 | } 104 | button 105 | { 106 | color:#fff; 107 | background-color: #111; 108 | border: 1px solid #555; 109 | margin:0px 4px; 110 | padding: 2px 8px; 111 | cursor: pointer; 112 | } 113 | button:disabled 114 | { 115 | color: #aaa; 116 | background-color: #666; 117 | border-color: #666; 118 | } 119 | input[type='checkbox']:disabled 120 | { 121 | opacity: 0.5; 122 | } 123 | .error-line 124 | { 125 | background-color: #f004 !important; 126 | } 127 | .nowrap 128 | { 129 | white-space:nowrap; 130 | } 131 | 132 | /* LittleJS CodeMirror Theme */ 133 | .cm-s-littlejs.CodeMirror { background: #080808; color: #aef; } 134 | .cm-s-littlejs div.CodeMirror-selected { background: #068; } 135 | .cm-s-littlejs .CodeMirror-gutters {background: #222; border-right: 0px;} 136 | .cm-s-littlejs .CodeMirror-linenumber { color: #aaa; } 137 | .cm-s-littlejs .CodeMirror-cursor { border-left: 2px solid #fff; } 138 | .cm-s-littlejs .cm-keyword { color: #6e1; } 139 | .cm-s-littlejs .cm-def { color: #fab; } 140 | .cm-s-littlejs .cm-operator { color: #1be; } 141 | .cm-s-littlejs .cm-property, 142 | .cm-s-littlejs span.cm-variable, 143 | .cm-s-littlejs span.cm-variable-2, 144 | .cm-s-littlejs span.cm-variable-3 { color: #ddd; } 145 | .cm-s-littlejs .cm-builtin, 146 | .cm-s-littlejs .cm-number { color: #e15; } 147 | .cm-s-littlejs .cm-comment { color:#888; font-style:italic; } 148 | .cm-s-littlejs .cm-string, 149 | .cm-s-littlejs .cm-string-2 { color:#fda; } 150 | .cm-s-littlejs .CodeMirror-matchingbracket { background-color: #381 !important; } -------------------------------------------------------------------------------- /examples/shorts/box2dPool.js: -------------------------------------------------------------------------------- 1 | const hitSound = new Sound([,.1,2e3,,,.01,,,,,,,,1]); 2 | const maxHitDistance = 6; 3 | 4 | class Ball extends Box2dObject 5 | { 6 | constructor(pos, number=0) 7 | { 8 | const color = hsl(number/9, 1, number? .5 : 1); 9 | super(pos, vec2(), 0, 0, color); 10 | this.number = number; 11 | 12 | // setup pool ball physics 13 | const friction = 0, restitution = .95; 14 | this.addCircle(1, vec2(), 1, friction, restitution); 15 | this.setLinearDamping(.4); 16 | this.setBullet(true); 17 | this.setFixedRotation(true); 18 | } 19 | beginContact() 20 | { hitSound.play(this.pos, clamp(this.getSpeed()/20)); } 21 | canHit() 22 | { return this == cueBall && this.getSpeed() < 1; } 23 | getHitStrength() 24 | { return this.getHitOffset().length()/maxHitDistance; } 25 | getHitOffset() 26 | { 27 | // hit from cue ball to mouse position 28 | const deltaPos = mousePos.subtract(this.pos); 29 | const length = min(deltaPos.length(), maxHitDistance); 30 | return deltaPos.normalize(length); 31 | } 32 | update() 33 | { 34 | if (this.canHit() && mouseWasPressed(0)) 35 | { 36 | // hit the cue ball 37 | const accel = this.getHitOffset().scale(8); 38 | this.applyAcceleration(accel); 39 | hitSound.play(cueBall.pos, this.getHitStrength(), .5); 40 | } 41 | if (this.pocketed) 42 | this.destroy(); 43 | } 44 | render() 45 | { 46 | super.render(); 47 | 48 | // draw white circle and ball number 49 | drawCircle(this.pos, .6, WHITE); 50 | const textPos = this.pos.add(vec2(0,-.06)); 51 | if (this.number) 52 | drawText(this.number, textPos, .5, BLACK); 53 | if (this.canHit()) 54 | { 55 | // draw the aim line 56 | const endPos = this.pos.add(this.getHitOffset()); 57 | const width = this.getHitStrength(); 58 | drawLine(this.pos, endPos, width, hsl(0,1,.5,.5), 59 | vec2(), 0, false); 60 | } 61 | } 62 | } 63 | 64 | class Pocket extends Box2dStaticObject 65 | { 66 | constructor(pos, size) 67 | { 68 | super(pos, size, 0, 0, BLACK); 69 | 70 | // create a sensor circle for pocket 71 | this.addCircle(size.x); 72 | this.setSensor(true); 73 | } 74 | render() 75 | { 76 | // add ball size to pocket size for drawing 77 | drawCircle(this.pos, this.size.x + 1, BLACK); 78 | } 79 | beginContact(other) { other.pocketed = 1; } 80 | } 81 | 82 | async function gameInit() 83 | { 84 | // setup box2d 85 | await box2dInit(); 86 | canvasClearColor = hsl(.4,.5,.5); 87 | 88 | // create table walls 89 | const groundObject = new Box2dStaticObject; 90 | groundObject.color = hsl(.1,1,.2); 91 | groundObject.addBox(vec2(100,3), vec2( 0, 8.5)); 92 | groundObject.addBox(vec2(100,3), vec2( 0,-8.5)); 93 | groundObject.addBox(vec2(3,100), vec2( 15,0)); 94 | groundObject.addBox(vec2(3,100), vec2(-15,0)); 95 | 96 | // create pockets 97 | for (let j=0; j<2; ++j) 98 | for (let i=0; i<3; ++i) 99 | new Pocket(vec2(i-1, j-.5).scale(13), vec2(.5)); 100 | 101 | // create balls 102 | let number = 1; 103 | for (let i=5; i--;) 104 | for (let j=i+1; j--;) 105 | new Ball(vec2(6+i*.9, j-i/2), number++); 106 | cueBall = new Ball(vec2(-6, 0)); 107 | } -------------------------------------------------------------------------------- /examples/module/build.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Build System 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import { fileURLToPath } from 'node:url'; 8 | import { dirname, join } from 'node:path'; 9 | import fs from 'node:fs'; 10 | import { execSync } from 'node:child_process'; 11 | 12 | const __dirname = dirname(fileURLToPath(import.meta.url)); 13 | 14 | const PROGRAM_TITLE = 'Little JS Starter Project'; 15 | const PROGRAM_NAME = 'game'; 16 | const BUILD_FOLDER = join(__dirname, 'build'); 17 | const sourceFiles = 18 | [ 19 | 'game.js', 20 | // add your game's source files here 21 | ]; 22 | const engineFile = join(__dirname, '../../dist/littlejs.esm.min.js'); // Use the minified ES module 23 | const dataFiles = 24 | [ 25 | 'tiles.png', 26 | // add your game's data files here 27 | ]; 28 | 29 | console.log(`Building ${PROGRAM_NAME}...`); 30 | const startTime = Date.now(); 31 | 32 | // rebuild engine 33 | //execSync(`npm run build`, { stdio: 'inherit' }); 34 | 35 | // remove old files and setup build folder 36 | fs.rmSync(BUILD_FOLDER, { recursive: true, force: true }); 37 | fs.rmSync(join(__dirname, `${PROGRAM_NAME}.zip`), { force: true }); 38 | fs.mkdirSync(BUILD_FOLDER); 39 | 40 | // copy data files 41 | for (const file of dataFiles) 42 | fs.copyFileSync(join(__dirname, file), join(BUILD_FOLDER, file)); 43 | 44 | // copy engine module 45 | fs.copyFileSync(engineFile, join(BUILD_FOLDER, 'littlejs.esm.min.js')); 46 | 47 | Build 48 | ( 49 | sourceFiles, 50 | [htmlBuildStep, zipBuildStep] 51 | ); 52 | 53 | console.log(''); 54 | console.log(`Build Completed in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`); 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | 58 | // A single build with its own source files, build steps, and output file 59 | // - each build step is a callback that accepts a single filename 60 | function Build(files=[], buildSteps=[]) 61 | { 62 | // process each source file separately (don't concatenate modules!) 63 | for (const file of files) 64 | { 65 | const outputFile = join(BUILD_FOLDER, file); 66 | fs.copyFileSync(join(__dirname, file), outputFile); 67 | moduleFixStep(outputFile); 68 | uglifyBuildStep(outputFile); 69 | } 70 | 71 | // execute build steps in order 72 | for (const buildStep of buildSteps) 73 | buildStep(); 74 | } 75 | 76 | function moduleFixStep(filename) 77 | { 78 | console.log(`Fixing module imports in ${filename}...`); 79 | 80 | let code = fs.readFileSync(filename, 'utf8'); 81 | 82 | // update the import path to use the local minified version 83 | code = code.replace(/import \* as LJS from ['"].*littlejs\.esm(?:\.min)?\.js['"];?/g, 84 | "import * as LJS from './littlejs.esm.min.js';"); 85 | 86 | // also fix relative imports to other game modules 87 | code = code.replace(/from ['"]\.\.\//g, "from './"); 88 | 89 | fs.writeFileSync(filename, code); 90 | } 91 | 92 | function uglifyBuildStep(filename) 93 | { 94 | console.log('Running uglify...'); 95 | execSync(`npx uglifyjs ${filename} -c -m -o ${filename}`, {stdio: 'inherit'}); 96 | } 97 | 98 | function htmlBuildStep() 99 | { 100 | console.log('Building html...'); 101 | 102 | // create html file with module script tag pointing to main game file 103 | let buffer = '' 104 | buffer += ''; 105 | buffer += ''; 106 | buffer += `${PROGRAM_TITLE}`; 107 | buffer += ''; 108 | buffer += ''; 109 | buffer += ''; 110 | buffer += ''; 111 | buffer += ''; 112 | 113 | // output html file 114 | fs.writeFileSync(join(BUILD_FOLDER, 'index.html'), buffer, {flag: 'w+'}); 115 | } 116 | 117 | function zipBuildStep() 118 | { 119 | console.log('Zipping...'); 120 | const sources = ['index.html', 'littlejs.esm.min.js', ...sourceFiles, ...dataFiles]; 121 | const sourceList = sources.join(' '); 122 | execSync(`npx bestzip ../${PROGRAM_NAME}.zip ${sourceList}`, {cwd:BUILD_FOLDER, stdio: 'inherit'}); 123 | console.log(`Size of ${PROGRAM_NAME}.zip: ${fs.statSync(join(__dirname, `${PROGRAM_NAME}.zip`)).size} bytes`); 124 | } -------------------------------------------------------------------------------- /examples/starter/build.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Build System 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import { fileURLToPath } from 'node:url'; 8 | import { dirname, join } from 'node:path'; 9 | import fs from 'node:fs'; 10 | import { execSync } from 'node:child_process'; 11 | 12 | const __dirname = dirname(fileURLToPath(import.meta.url)); 13 | 14 | const PROGRAM_TITLE = 'Little JS Starter Project'; 15 | const PROGRAM_NAME = 'game'; 16 | const BUILD_FOLDER = join(__dirname, 'build'); 17 | const USE_ROADROLLER = false; // enable for extra compression 18 | const sourceFiles = 19 | [ 20 | join(__dirname, '../../dist/littlejs.release.js'), 21 | join(__dirname, 'game.js'), 22 | // add your game's files here 23 | ]; 24 | const dataFiles = 25 | [ 26 | 'tiles.png', 27 | // add your game's data files here 28 | ]; 29 | 30 | console.log(`Building ${PROGRAM_NAME}...`); 31 | const startTime = Date.now(); 32 | 33 | // rebuild engine 34 | //execSync(`npm run build`, { stdio: 'inherit' }); 35 | 36 | // remove old files and setup build folder 37 | fs.rmSync(BUILD_FOLDER, { recursive: true, force: true }); 38 | fs.rmSync(join(__dirname, `${PROGRAM_NAME}.zip`), { force: true }); 39 | fs.mkdirSync(BUILD_FOLDER); 40 | 41 | // copy data files 42 | for (const file of dataFiles) 43 | fs.copyFileSync(join(__dirname, file), join(BUILD_FOLDER, file)); 44 | 45 | Build 46 | ( 47 | join(BUILD_FOLDER, 'index.js'), 48 | sourceFiles, 49 | USE_ROADROLLER ? 50 | [closureCompilerStep, uglifyBuildStep, roadrollerBuildStep, htmlBuildStep, zipBuildStep] : 51 | [closureCompilerStep, uglifyBuildStep, htmlBuildStep, zipBuildStep] 52 | ); 53 | 54 | console.log(''); 55 | console.log(`Build Completed in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`); 56 | 57 | /////////////////////////////////////////////////////////////////////////////// 58 | 59 | // A single build with its own source files, build steps, and output file 60 | // - each build step is a callback that accepts a single filename 61 | function Build(outputFile, files=[], buildSteps=[]) 62 | { 63 | // copy files into a buffer 64 | let buffer = ''; 65 | for (const file of files) 66 | buffer += fs.readFileSync(file) + '\n'; 67 | 68 | // output file 69 | fs.writeFileSync(outputFile, buffer, {flag: 'w+'}); 70 | 71 | // execute build steps in order 72 | for (const buildStep of buildSteps) 73 | buildStep(outputFile); 74 | } 75 | 76 | function closureCompilerStep(filename) 77 | { 78 | console.log('Running closure compiler...'); 79 | 80 | // use closer compiler to minify the code 81 | const filenameTemp = filename + '.tmp'; 82 | fs.copyFileSync(filename, filenameTemp); 83 | execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --compilation_level=ADVANCED --warning_level=VERBOSE --jscomp_off=* --assume_function_wrapper`, {stdio: 'inherit'}); 84 | fs.rmSync(filenameTemp); 85 | } 86 | 87 | function uglifyBuildStep(filename) 88 | { 89 | console.log('Running uglify...'); 90 | execSync(`npx uglifyjs ${filename} -c -m -o ${filename}`, {stdio: 'inherit'}); 91 | } 92 | 93 | function roadrollerBuildStep(filename) 94 | { 95 | console.log('Running roadroller...'); 96 | execSync(`npx roadroller ${filename} -o ${filename}`, {stdio: 'inherit'}); 97 | } 98 | 99 | function htmlBuildStep(filename) 100 | { 101 | console.log('Building html...'); 102 | 103 | // create html file 104 | let buffer = '' 105 | buffer += ''; 106 | buffer += ''; 107 | buffer += `${PROGRAM_TITLE}`; 108 | buffer += ''; 109 | buffer += ''; 110 | buffer += ''; 111 | buffer += ''; 114 | 115 | // output html file 116 | fs.writeFileSync(join(BUILD_FOLDER, 'index.html'), buffer, {flag: 'w+'}); 117 | } 118 | 119 | function zipBuildStep(filename) 120 | { 121 | console.log('Zipping...'); 122 | const sources = ['index.html', ...dataFiles]; 123 | const sourceList = sources.join(' '); 124 | execSync(`npx bestzip ../${PROGRAM_NAME}.zip ${sourceList}`, {cwd:BUILD_FOLDER, stdio: 'inherit'}); 125 | console.log(`Size of ${PROGRAM_NAME}.zip: ${fs.statSync(join(__dirname, `${PROGRAM_NAME}.zip`)).size} bytes`); 126 | } -------------------------------------------------------------------------------- /examples/shorts/sequencer.js: -------------------------------------------------------------------------------- 1 | const stepCount = 8, trackCount = 12, sequencer = []; 2 | let currentStep = 0, stepTime = 0, tempo = 240; 3 | let isPlaying = false, eraseMode = false; 4 | 5 | // sound sequencer instruments 6 | const sound_piano = new Sound([.3,0,220,,.1]); 7 | const sound_drumKick = new Sound([,,99,,,.02,,,,,,,,2]); 8 | const sound_drumHat = new Sound([,,1e3,,,.01,4,,,,,,,,,,,,,,4e3]); 9 | 10 | // musical note scales 11 | const majorScale = [0,2,4,5,7,9,11]; 12 | const minorScale = [0,2,3,5,7,8,10]; 13 | const pentatonicScale = [0,3,5,7,10]; 14 | const scale = majorScale; 15 | 16 | class UISequencerButton extends UIButton 17 | { 18 | constructor(step, track) 19 | { 20 | const size = vec2(68, 35); 21 | let pos = vec2(step, trackCount-1-track); 22 | pos = pos.multiply(size); 23 | pos = pos.add(vec2(240, 40)); 24 | super(pos, size); 25 | 26 | this.step = step; 27 | this.track = track; 28 | this.cornerRadius = 0; 29 | this.dragActivate = true; 30 | this.isOn = false; 31 | this.shadowColor = CLEAR_BLACK; 32 | 33 | // set instrument and color based on track 34 | const pianoStart = 2; 35 | this.hue = track*.15; 36 | if (track >= pianoStart) 37 | { 38 | const octave = floor((track-pianoStart) / scale.length); 39 | const scaleNote = (track-pianoStart) % scale.length; 40 | this.semitone = scale[scaleNote] + 12*octave; 41 | this.sound = sound_piano; 42 | this.hue = .6 - scaleNote/40 + octave*.2; 43 | } 44 | else 45 | this.sound = [sound_drumKick, sound_drumHat][track]; 46 | } 47 | onPress() 48 | { 49 | // set the button on/off and update sequencer table 50 | if (mouseWasPressed(0)) 51 | eraseMode = this.isOn; 52 | this.isOn = !eraseMode; 53 | eraseMode || this.playSound(); 54 | const index = this.step + this.track*stepCount; 55 | sequencer[index] = eraseMode ? 0 : this; 56 | } 57 | render() 58 | { 59 | this.activeColor = eraseMode ? RED : WHITE; 60 | this.color = this.isActiveObject() ? BLACK : 61 | hsl(this.hue, this.isOn ? 1 : .5, this.isOn ? .5 : .15); 62 | if (isPlaying && this.step == currentStep) 63 | this.color = this.color.lerp(WHITE, .5); 64 | super.render(); 65 | } 66 | playSound() { this.sound.playNote(this.semitone); } 67 | } 68 | 69 | function gameInit() 70 | { 71 | // initialize UI system 72 | new UISystemPlugin; 73 | uiSystem.defaultCornerRadius = 8; 74 | uiSystem.defaultShadowColor = BLACK; 75 | canvasClearColor = GRAY; 76 | 77 | // create sequencer buttons 78 | for (let step=stepCount; step--;) 79 | for (let track=trackCount; track--;) 80 | new UISequencerButton(step, track); 81 | 82 | // create play/stop button 83 | const playButton = new UIButton(vec2(660,500), vec2(180,60), 'PLAY'); 84 | playButton.onClick = ()=> 85 | { 86 | isPlaying = !isPlaying; 87 | currentStep = stepTime = 0; 88 | playButton.text = isPlaying ? 'STOP' : 'PLAY'; 89 | }; 90 | 91 | // create tempo slider 92 | const minTempo = 120, maxTempo = 480; 93 | const tempoPercent = percent(tempo, minTempo, maxTempo); 94 | const tempoSlider = new UIScrollbar(vec2(380,500), vec2(340,40), tempoPercent); 95 | tempoSlider.onChange = ()=> 96 | { 97 | tempo = lerp(minTempo, maxTempo, tempoSlider.value); 98 | tempo = floor(tempo/10) * 10; // round to nearest 10th 99 | tempoSlider.text = `${tempo} BPM`; 100 | }; 101 | tempoSlider.onChange(); 102 | } 103 | 104 | function gameUpdate() 105 | { 106 | if (!isPlaying) 107 | return; 108 | 109 | // update step time based on tempo 110 | const lastStepTime = stepTime; 111 | const lastStep = currentStep; 112 | stepTime += timeDelta*tempo/60; 113 | currentStep = floor(stepTime) % stepCount; 114 | if (currentStep == lastStep && lastStepTime) 115 | return; 116 | 117 | // play sounds when step changes 118 | for (let i=trackCount; i--;) 119 | { 120 | const index = currentStep + i*stepCount; 121 | const noteButton = sequencer[index]; 122 | noteButton && noteButton.playSound(); 123 | } 124 | } -------------------------------------------------------------------------------- /examples/electron/build.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Build System 3 | */ 4 | 5 | 'use strict'; 6 | 7 | import { fileURLToPath } from 'node:url'; 8 | import { dirname, join } from 'node:path'; 9 | import fs from 'node:fs'; 10 | import { execSync } from 'node:child_process'; 11 | 12 | const __dirname = dirname(fileURLToPath(import.meta.url)); 13 | 14 | const PROGRAM_TITLE = 'Little JS Electron Project'; 15 | const PROGRAM_NAME = 'game'; 16 | const BUILD_FOLDER = join(__dirname, 'build'); 17 | const USE_ROADROLLER = false; // enable for extra compression 18 | const sourceFiles = 19 | [ 20 | join(__dirname, '../../dist/littlejs.release.js'), 21 | join(__dirname, 'game.js'), 22 | // add your game's files here 23 | ]; 24 | const dataFiles = 25 | [ 26 | 'tiles.png', 27 | // add your game's data files here 28 | ]; 29 | 30 | console.log(`Building ${PROGRAM_NAME}...`); 31 | const startTime = Date.now(); 32 | 33 | // rebuild engine 34 | //execSync(`npm run build`, { stdio: 'inherit' }); 35 | 36 | // remove old files and setup build folder 37 | fs.rmSync(BUILD_FOLDER, { recursive: true, force: true }); 38 | fs.rmSync(join(__dirname, `${PROGRAM_NAME}.zip`), { force: true }); 39 | fs.mkdirSync(BUILD_FOLDER); 40 | 41 | // copy data files 42 | for (const file of dataFiles) 43 | fs.copyFileSync(join(__dirname, file), join(BUILD_FOLDER, file)); 44 | 45 | Build 46 | ( 47 | join(BUILD_FOLDER, 'index.js'), 48 | sourceFiles, 49 | USE_ROADROLLER ? 50 | [closureCompilerStep, uglifyBuildStep, roadrollerBuildStep, htmlBuildStep, electronBuildStep] : 51 | [closureCompilerStep, uglifyBuildStep, htmlBuildStep, electronBuildStep] 52 | ); 53 | 54 | console.log(''); 55 | console.log(`Build Completed in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds!`); 56 | 57 | /////////////////////////////////////////////////////////////////////////////// 58 | 59 | // A single build with its own source files, build steps, and output file 60 | // - each build step is a callback that accepts a single filename 61 | function Build(outputFile, files=[], buildSteps=[]) 62 | { 63 | // copy files into a buffer 64 | let buffer = ''; 65 | for (const file of files) 66 | buffer += fs.readFileSync(file) + '\n'; 67 | 68 | // output file 69 | fs.writeFileSync(outputFile, buffer, {flag: 'w+'}); 70 | 71 | // execute build steps in order 72 | for (const buildStep of buildSteps) 73 | buildStep(outputFile); 74 | } 75 | 76 | function closureCompilerStep(filename) 77 | { 78 | console.log('Running closure compiler...'); 79 | 80 | // use closer compiler to minify the code 81 | const filenameTemp = filename + '.tmp'; 82 | fs.copyFileSync(filename, filenameTemp); 83 | execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --compilation_level=ADVANCED --warning_level=VERBOSE --jscomp_off=* --assume_function_wrapper`, {stdio: 'inherit'}); 84 | fs.rmSync(filenameTemp); 85 | } 86 | 87 | function uglifyBuildStep(filename) 88 | { 89 | console.log('Running uglify...'); 90 | execSync(`npx uglifyjs ${filename} -c -m -o ${filename}`, {stdio: 'inherit'}); 91 | } 92 | 93 | function roadrollerBuildStep(filename) 94 | { 95 | console.log('Running roadroller...'); 96 | execSync(`npx roadroller ${filename} -o ${filename}`, {stdio: 'inherit'}); 97 | } 98 | 99 | function htmlBuildStep(filename) 100 | { 101 | console.log('Building html...'); 102 | 103 | // create html file 104 | let buffer = ''; 105 | buffer += ''; 106 | buffer += ''; 107 | buffer += `${PROGRAM_TITLE}`; 108 | buffer += ''; 109 | buffer += ''; 110 | buffer += ''; 111 | buffer += ``; 112 | 113 | // output html file 114 | fs.writeFileSync(join(BUILD_FOLDER, 'index.html'), buffer, {flag: 'w+'}); 115 | } 116 | 117 | function electronBuildStep(filename) 118 | { 119 | console.log('Building executable with electron...'); 120 | 121 | // copy elecron files to build folder 122 | fs.copyFileSync(join(__dirname, 'electron.js'), join(BUILD_FOLDER, 'electron.js')); 123 | fs.copyFileSync(join(__dirname, 'package.json'), join(BUILD_FOLDER, 'package.json')); 124 | 125 | // run electron packager 126 | execSync(`npx electron-packager "${BUILD_FOLDER}" --overwrite --out="${__dirname}"`, {stdio: 'inherit'}); 127 | } -------------------------------------------------------------------------------- /examples/typescript/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS TypeScript Demo 3 | - A simple starter project 4 | - Shows how to use LittleJS with modules 5 | */ 6 | 'use strict'; 7 | // import LittleJS module 8 | import * as LJS from '../../dist/littlejs.esm.js'; 9 | const { tile, vec2, hsl } = LJS; 10 | // show the LittleJS splash screen 11 | LJS.setShowSplashScreen(true); 12 | // fix texture bleeding by shrinking tile slightly 13 | LJS.setTileDefaultBleed(.5); 14 | // sound effects 15 | const sound_click = new LJS.Sound([1, .5]); 16 | // medals 17 | const medal_example = new LJS.Medal(0, 'Example Medal', 'Welcome to LittleJS!'); 18 | LJS.medalsInit('Hello World'); 19 | // game variables 20 | let particleEmitter; 21 | /////////////////////////////////////////////////////////////////////////////// 22 | function gameInit() { 23 | // create tile collision and visible tile layer 24 | const pos = vec2(); 25 | const tileLayer = new LJS.TileCollisionLayer(pos, vec2(32, 16)); 26 | // get level data from the tiles image 27 | const mainContext = LJS.mainContext; 28 | const tileImage = LJS.textureInfos[0].image; 29 | mainContext.drawImage(tileImage, 0, 0); 30 | const imageData = mainContext.getImageData(0, 0, tileImage.width, tileImage.height).data; 31 | for (pos.x = tileLayer.size.x; pos.x--;) 32 | for (pos.y = tileLayer.size.y; pos.y--;) { 33 | // check if this pixel is set 34 | const i = pos.x + tileImage.width * (15 + tileLayer.size.y - pos.y); 35 | if (!imageData[4 * i]) 36 | continue; 37 | // set tile data 38 | const tileIndex = 1; 39 | const direction = LJS.randInt(4); 40 | const mirror = !LJS.randInt(2); 41 | const color = LJS.randColor(); 42 | const data = new LJS.TileLayerData(tileIndex, direction, mirror, color); 43 | tileLayer.setData(pos, data); 44 | tileLayer.setCollisionData(pos); 45 | } 46 | // draw tile layer with new data 47 | tileLayer.redraw(); 48 | // move camera to center of collision 49 | LJS.setCameraPos(tileLayer.size.scale(.5)); 50 | LJS.setCameraScale(32); 51 | // enable gravity 52 | LJS.setGravity(vec2(0, -.01)); 53 | // create particle emitter 54 | particleEmitter = new LJS.ParticleEmitter(vec2(16, 9), 0, // emitPos, emitAngle 55 | 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone 56 | tile(0, 16), // tileIndex, tileSize 57 | hsl(1, 1, 1), hsl(0, 0, 0), // colorStartA, colorStartB 58 | hsl(0, 0, 0, 0), hsl(0, 0, 0, 0), // colorEndA, colorEndB 59 | 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 60 | .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone 61 | .05, .5, true, true // fadeRate, randomness, collide, additive 62 | ); 63 | particleEmitter.restitution = .3; // bounce when it collides 64 | particleEmitter.trailScale = 2; // stretch stretch as it moves 65 | particleEmitter.velocityInheritance = .3; // inherit emitter velocity 66 | } 67 | /////////////////////////////////////////////////////////////////////////////// 68 | function gameUpdate() { 69 | if (LJS.mouseWasPressed(0)) { 70 | // play sound when mouse is pressed 71 | sound_click.play(LJS.mousePos); 72 | // change particle color and set to fade out 73 | particleEmitter.colorStartA = LJS.randColor(); 74 | particleEmitter.colorStartB = LJS.randColor(); 75 | particleEmitter.colorEndA = particleEmitter.colorStartA.scale(1, 0); 76 | particleEmitter.colorEndB = particleEmitter.colorStartB.scale(1, 0); 77 | // unlock medals 78 | medal_example.unlock(); 79 | } 80 | // move particles to mouse location if on screen 81 | if (LJS.mousePosScreen.x) 82 | particleEmitter.pos = LJS.mousePos; 83 | } 84 | /////////////////////////////////////////////////////////////////////////////// 85 | function gameUpdatePost() { 86 | } 87 | /////////////////////////////////////////////////////////////////////////////// 88 | function gameRender() { 89 | // draw a grey square in the background 90 | LJS.drawRect(vec2(16, 8), vec2(20, 14), hsl(0, 0, .6)); 91 | // draw the logo as a tile 92 | LJS.drawTile(vec2(21, 5), vec2(4.5), tile(3, 128)); 93 | } 94 | /////////////////////////////////////////////////////////////////////////////// 95 | function gameRenderPost() { 96 | LJS.drawTextScreen('LittleJS with TypeScript', vec2(LJS.mainCanvasSize.x / 2, 80), 80); 97 | } 98 | /////////////////////////////////////////////////////////////////////////////// 99 | // Startup LittleJS Engine 100 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); 101 | -------------------------------------------------------------------------------- /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 | // import LittleJS module 12 | import * as LJS from '../../dist/littlejs.esm.js'; 13 | import * as GameObjects from './gameObjects.js'; 14 | import * as GameEffects from './gameEffects.js'; 15 | import * as GamePlayer from './gamePlayer.js'; 16 | import * as GameLevel from './gameLevel.js'; 17 | const {vec2} = LJS; 18 | 19 | // globals 20 | export let gameLevelData, spriteAtlas, player, score, deaths; 21 | export function addToScore(delta=1) { score += delta; } 22 | export function addToDeaths() { ++deaths; } 23 | 24 | // enable touch gamepad on touch devices 25 | LJS.setTouchGamepadEnable(true); 26 | 27 | // limit canvas aspect ratios to support most modern HD devices 28 | LJS.setCanvasMinAspect(.4); 29 | LJS.setCanvasMaxAspect(2.5); 30 | 31 | // limit size to to 4k HD 32 | LJS.setCanvasMaxSize(vec2(3840, 2160)); 33 | 34 | /////////////////////////////////////////////////////////////////////////////// 35 | function loadLevel() 36 | { 37 | // setup level 38 | GameLevel.buildLevel(); 39 | 40 | // spawn player 41 | player = new GamePlayer.Player(GameLevel.playerStartPos); 42 | LJS.setCameraPos(GameLevel.getCameraTarget()); 43 | 44 | // init game 45 | score = deaths = 0; 46 | } 47 | 48 | /////////////////////////////////////////////////////////////////////////////// 49 | async function gameInit() 50 | { 51 | // load the game level data 52 | gameLevelData = await LJS.fetchJSON('gameLevelData.json'); 53 | 54 | // engine settings 55 | LJS.setGravity(vec2(0,-.01)); 56 | LJS.setObjectDefaultDamping(.99); 57 | LJS.setObjectDefaultAngleDamping(.99); 58 | LJS.setCameraScale(4*16); 59 | 60 | // create a table of all sprites 61 | const gameTile = (i, size=16)=> LJS.tile(i, size, 0, 1); 62 | spriteAtlas = 63 | { 64 | // large tiles 65 | circle: gameTile(0), 66 | crate: gameTile(1), 67 | player: gameTile(2), 68 | enemy: gameTile(4), 69 | coin: gameTile(5), 70 | 71 | // small tiles 72 | gun: gameTile(vec2(0,2),8), 73 | grenade: gameTile(vec2(1,2),8), 74 | }; 75 | 76 | loadLevel(); 77 | } 78 | 79 | /////////////////////////////////////////////////////////////////////////////// 80 | function gameUpdate() 81 | { 82 | // respawn player 83 | if (player.deadTimer > 1) 84 | { 85 | player = new GamePlayer.Player(GameLevel.playerStartPos); 86 | player.velocity = vec2(0,.1); 87 | GameEffects.sound_jump.play(); 88 | } 89 | 90 | // mouse wheel = zoom 91 | LJS.setCameraScale(LJS.clamp(LJS.cameraScale*(1-LJS.mouseWheel/10), 1, 1e3)); 92 | 93 | // T = drop test crate 94 | if (LJS.keyWasPressed('KeyT')) 95 | new GameObjects.Crate(LJS.mousePos); 96 | 97 | // E = drop enemy 98 | if (LJS.keyWasPressed('KeyE')) 99 | new GameObjects.Enemy(LJS.mousePos); 100 | 101 | // X = make explosion 102 | if (LJS.keyWasPressed('KeyX')) 103 | GameEffects.explosion(LJS.mousePos); 104 | 105 | // M = move player to mouse 106 | if (LJS.keyWasPressed('KeyM')) 107 | player.pos = LJS.mousePos; 108 | 109 | // R = restart level 110 | if (LJS.keyWasPressed('KeyR')) 111 | loadLevel(); 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////// 115 | function gameUpdatePost() 116 | { 117 | // update camera 118 | LJS.setCameraPos(LJS.cameraPos.lerp(GameLevel.getCameraTarget(), LJS.clamp(player.getAliveTime()/2))); 119 | } 120 | 121 | /////////////////////////////////////////////////////////////////////////////// 122 | function gameRender() 123 | { 124 | } 125 | 126 | /////////////////////////////////////////////////////////////////////////////// 127 | function gameRenderPost() 128 | { 129 | // draw to main canvas for hud rendering 130 | const drawText = (text, x, y, size=40)=> 131 | { 132 | const context = LJS.mainContext; 133 | context.textAlign = 'center'; 134 | context.textBaseline = 'top'; 135 | context.font = size + 'px arial'; 136 | context.fillStyle = '#fff'; 137 | context.lineWidth = 3; 138 | context.strokeText(text, x, y); 139 | context.fillText(text, x, y); 140 | } 141 | drawText('Score: ' + score, LJS.mainCanvas.width*1/4, 20); 142 | drawText('Deaths: ' + deaths, LJS.mainCanvas.width*3/4, 20); 143 | } 144 | 145 | /////////////////////////////////////////////////////////////////////////////// 146 | // Startup LittleJS Engine 147 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png', 'tilesLevel.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 LittleJS module 10 | import * as LJS from '../../dist/littlejs.esm.js'; 11 | const {tile, vec2, hsl} = LJS; 12 | 13 | // show the LittleJS splash screen 14 | LJS.setShowSplashScreen(true); 15 | 16 | // fix texture bleeding by shrinking tile slightly 17 | LJS.setTileDefaultBleed(.5); 18 | 19 | // sound effects 20 | const sound_click = new LJS.Sound([1,.5]); 21 | 22 | // medals 23 | const medal_example = new LJS.Medal(0, 'Example Medal', 'Welcome to LittleJS!'); 24 | LJS.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 pos = vec2(); 34 | const tileLayer = new LJS.TileCollisionLayer(pos, vec2(32,16)); 35 | 36 | // get level data from the tiles image 37 | const mainContext = LJS.mainContext; 38 | const tileImage = LJS.textureInfos[0].image; 39 | mainContext.drawImage(tileImage, 0, 0); 40 | const imageData = mainContext.getImageData(0,0,tileImage.width,tileImage.height).data; 41 | for (pos.x = tileLayer.size.x; pos.x--;) 42 | for (pos.y = tileLayer.size.y; pos.y--;) 43 | { 44 | // check if this pixel is set 45 | const i = pos.x + tileImage.width*(15 + tileLayer.size.y - pos.y); 46 | if (!imageData[4*i]) 47 | continue; 48 | 49 | // set tile data 50 | const tileIndex = 1; 51 | const direction = LJS.randInt(4) 52 | const mirror = !LJS.randInt(2); 53 | const color = LJS.randColor(); 54 | const data = new LJS.TileLayerData(tileIndex, direction, mirror, color); 55 | tileLayer.setData(pos, data); 56 | tileLayer.setCollisionData(pos); 57 | } 58 | 59 | // draw tile layer with new data 60 | tileLayer.redraw(); 61 | 62 | // move camera to center of collision 63 | LJS.setCameraPos(tileLayer.size.scale(.5)); 64 | LJS.setCameraScale(32); 65 | 66 | // enable gravity 67 | LJS.setGravity(vec2(0,-.01)); 68 | 69 | // create particle emitter 70 | particleEmitter = new LJS.ParticleEmitter( 71 | vec2(16,9), 0, // emitPos, emitAngle 72 | 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone 73 | tile(0, 16), // tileIndex, tileSize 74 | hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB 75 | hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB 76 | 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 77 | .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone 78 | .05, .5, true, true // fadeRate, randomness, collide, additive 79 | ); 80 | particleEmitter.restitution = .3; // bounce when it collides 81 | particleEmitter.trailScale = 2; // stretch as it moves 82 | particleEmitter.velocityInheritance = .3; // inherit emitter velocity 83 | } 84 | 85 | /////////////////////////////////////////////////////////////////////////////// 86 | function gameUpdate() 87 | { 88 | if (LJS.mouseWasPressed(0)) 89 | { 90 | // play sound when mouse is pressed 91 | sound_click.play(LJS.mousePos); 92 | 93 | // change particle color and set to fade out 94 | particleEmitter.colorStartA = LJS.randColor(); 95 | particleEmitter.colorStartB = LJS.randColor(); 96 | particleEmitter.colorEndA = particleEmitter.colorStartA.scale(1,0); 97 | particleEmitter.colorEndB = particleEmitter.colorStartB.scale(1,0); 98 | 99 | // unlock medals 100 | medal_example.unlock(); 101 | } 102 | 103 | // move particles to mouse location if on screen 104 | if (LJS.mousePosScreen.x) 105 | particleEmitter.pos = LJS.mousePos; 106 | } 107 | 108 | /////////////////////////////////////////////////////////////////////////////// 109 | function gameUpdatePost() 110 | { 111 | 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////// 115 | function gameRender() 116 | { 117 | // draw a grey square in the background 118 | LJS.drawRect(vec2(16,8), vec2(20,14), hsl(0,0,.6)); 119 | 120 | // draw the logo as a tile 121 | LJS.drawTile(vec2(21,5), vec2(4.5), tile(3,128)); 122 | } 123 | 124 | /////////////////////////////////////////////////////////////////////////////// 125 | function gameRenderPost() 126 | { 127 | LJS.drawTextScreen('LittleJS with Modules', vec2(LJS.mainCanvasSize.x/2, 80), 80); 128 | } 129 | 130 | /////////////////////////////////////////////////////////////////////////////// 131 | // Startup LittleJS Engine 132 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/typescript/game.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS TypeScript Demo 3 | - A simple starter project 4 | - Shows how to use LittleJS with modules 5 | */ 6 | 7 | 'use strict'; 8 | 9 | // import LittleJS module 10 | import * as LJS from '../../dist/littlejs.esm.js'; 11 | const {tile, vec2, hsl} = LJS; 12 | 13 | // show the LittleJS splash screen 14 | LJS.setShowSplashScreen(true); 15 | 16 | // fix texture bleeding by shrinking tile slightly 17 | LJS.setTileDefaultBleed(.5); 18 | 19 | // sound effects 20 | const sound_click = new LJS.Sound([1,.5]); 21 | 22 | // medals 23 | const medal_example = new LJS.Medal(0, 'Example Medal', 'Welcome to LittleJS!'); 24 | LJS.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 pos = vec2(); 34 | const tileLayer = new LJS.TileCollisionLayer(pos, vec2(32,16)); 35 | 36 | // get level data from the tiles image 37 | const mainContext = LJS.mainContext; 38 | const tileImage = LJS.textureInfos[0].image; 39 | mainContext.drawImage(tileImage, 0, 0); 40 | const imageData = mainContext.getImageData(0,0,tileImage.width,tileImage.height).data; 41 | for (pos.x = tileLayer.size.x; pos.x--;) 42 | for (pos.y = tileLayer.size.y; pos.y--;) 43 | { 44 | // check if this pixel is set 45 | const i = pos.x + tileImage.width*(15 + tileLayer.size.y - pos.y); 46 | if (!imageData[4*i]) 47 | continue; 48 | 49 | // set tile data 50 | const tileIndex = 1; 51 | const direction = LJS.randInt(4) 52 | const mirror = !LJS.randInt(2); 53 | const color = LJS.randColor(); 54 | const data = new LJS.TileLayerData(tileIndex, direction, mirror, color); 55 | tileLayer.setData(pos, data); 56 | tileLayer.setCollisionData(pos); 57 | } 58 | 59 | // draw tile layer with new data 60 | tileLayer.redraw(); 61 | 62 | // move camera to center of collision 63 | LJS.setCameraPos(tileLayer.size.scale(.5)); 64 | LJS.setCameraScale(32); 65 | 66 | // enable gravity 67 | LJS.setGravity(vec2(0,-.01)); 68 | 69 | // create particle emitter 70 | particleEmitter = new LJS.ParticleEmitter( 71 | vec2(16,9), 0, // emitPos, emitAngle 72 | 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone 73 | tile(0, 16), // tileIndex, tileSize 74 | hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB 75 | hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB 76 | 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 77 | .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone 78 | .05, .5, true, true // fadeRate, randomness, collide, additive 79 | ); 80 | particleEmitter.restitution = .3; // bounce when it collides 81 | particleEmitter.trailScale = 2; // stretch stretch as it moves 82 | particleEmitter.velocityInheritance = .3; // inherit emitter velocity 83 | } 84 | 85 | /////////////////////////////////////////////////////////////////////////////// 86 | function gameUpdate() 87 | { 88 | if (LJS.mouseWasPressed(0)) 89 | { 90 | // play sound when mouse is pressed 91 | sound_click.play(LJS.mousePos); 92 | 93 | // change particle color and set to fade out 94 | particleEmitter.colorStartA = LJS.randColor(); 95 | particleEmitter.colorStartB = LJS.randColor(); 96 | particleEmitter.colorEndA = particleEmitter.colorStartA.scale(1,0); 97 | particleEmitter.colorEndB = particleEmitter.colorStartB.scale(1,0); 98 | 99 | // unlock medals 100 | medal_example.unlock(); 101 | } 102 | 103 | // move particles to mouse location if on screen 104 | if (LJS.mousePosScreen.x) 105 | particleEmitter.pos = LJS.mousePos; 106 | } 107 | 108 | /////////////////////////////////////////////////////////////////////////////// 109 | function gameUpdatePost() 110 | { 111 | 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////// 115 | function gameRender() 116 | { 117 | // draw a grey square in the background 118 | LJS.drawRect(vec2(16,8), vec2(20,14), hsl(0,0,.6)); 119 | 120 | // draw the logo as a tile 121 | LJS.drawTile(vec2(21,5), vec2(4.5), tile(3,128)); 122 | } 123 | 124 | /////////////////////////////////////////////////////////////////////////////// 125 | function gameRenderPost() 126 | { 127 | LJS.drawTextScreen('LittleJS with TypeScript', vec2(LJS.mainCanvasSize.x/2, 80), 80); 128 | } 129 | 130 | /////////////////////////////////////////////////////////////////////////////// 131 | // Startup LittleJS Engine 132 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /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 | setTileDefaultBleed(.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 | const pos = vec2(); 31 | const tileLayer = new TileCollisionLayer(pos, vec2(32,16)); 32 | 33 | // get level data from the tiles image 34 | const tileImage = textureInfos[0].image; 35 | mainContext.drawImage(tileImage,0,0); 36 | const imageData = mainContext.getImageData(0,0,tileImage.width,tileImage.height).data; 37 | for (pos.x = tileLayer.size.x; pos.x--;) 38 | for (pos.y = tileLayer.size.y; pos.y--;) 39 | { 40 | // check if this pixel is set 41 | const i = pos.x + tileImage.width*(15 + tileLayer.size.y - pos.y); 42 | if (!imageData[4*i]) 43 | continue; 44 | 45 | // set tile data 46 | const tileIndex = 1; 47 | const direction = randInt(4) 48 | const mirror = randBool(); 49 | const color = randColor(); 50 | const data = new TileLayerData(tileIndex, direction, mirror, color); 51 | tileLayer.setData(pos, data); 52 | tileLayer.setCollisionData(pos); 53 | } 54 | 55 | // draw tile layer with new data 56 | tileLayer.redraw(); 57 | 58 | // setup camera 59 | setCameraPos(vec2(16,8)); 60 | setCameraScale(32); 61 | 62 | // enable gravity 63 | setGravity(vec2(0,-.01)); 64 | 65 | // create particle emitter 66 | particleEmitter = new ParticleEmitter( 67 | vec2(16,9), 0, // emitPos, emitAngle 68 | 0, 0, 500, 3.14, // emitSize, emitTime, rate, cone 69 | tile(0, 16), // tileIndex, tileSize 70 | hsl(1,1,1), hsl(0,0,0), // colorStartA, colorStartB 71 | hsl(0,0,0,0), hsl(0,0,0,0), // colorEndA, colorEndB 72 | 1, .2, .2, .1, .05, // time, sizeStart, sizeEnd, speed, angleSpeed 73 | .99, 1, 1, 3.14, // damping, angleDamping, gravityScale, cone 74 | .05, .5, true, true // fadeRate, randomness, collide, additive 75 | ); 76 | particleEmitter.restitution = .3; // bounce when it collides 77 | particleEmitter.trailScale = 2; // stretch as it moves 78 | particleEmitter.velocityInheritance = .3; // inherit emitter velocity 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 = randColor(); 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 | if (mouseWheel) 100 | { 101 | // zoom in and out with mouse wheel 102 | cameraScale -= sign(mouseWheel)*cameraScale/5; 103 | cameraScale = clamp(cameraScale, 10, 300); 104 | } 105 | 106 | // move particles to mouse location if on screen 107 | if (mousePosScreen.x) 108 | particleEmitter.pos = mousePos; 109 | } 110 | 111 | /////////////////////////////////////////////////////////////////////////////// 112 | function gameUpdatePost() 113 | { 114 | 115 | } 116 | 117 | /////////////////////////////////////////////////////////////////////////////// 118 | function gameRender() 119 | { 120 | // draw a grey square in the background 121 | drawRect(vec2(16,8), vec2(20,14), hsl(0,0,.6)); 122 | 123 | // draw the logo as a tile 124 | drawTile(vec2(21,5), vec2(4.5), tile(3,128)); 125 | } 126 | 127 | /////////////////////////////////////////////////////////////////////////////// 128 | function gameRenderPost() 129 | { 130 | drawTextScreen('LittleJS Demo', 131 | vec2(mainCanvasSize.x/2, 70), 80, // position, size 132 | hsl(0,0,1), 6, hsl(0,0,0)); // color, outline size and color 133 | } 134 | 135 | /////////////////////////////////////////////////////////////////////////////// 136 | // Startup LittleJS Engine 137 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/shorts/musicPlayer.js: -------------------------------------------------------------------------------- 1 | 2 | let musicVolume = .8, musicSound, musicInstance; 3 | 4 | function gameInit() 5 | { 6 | // setup ui system plugin 7 | new UISystemPlugin; 8 | uiSystem.defaultSoundPress = new Sound([.5,0,220]); 9 | uiSystem.defaultSoundClick = new Sound([.5,0,440]); 10 | uiSystem.defaultCornerRadius = 20; 11 | uiSystem.defaultGradientColor = WHITE; 12 | uiSystem.defaultShadowColor = BLACK; 13 | canvasClearColor = hsl(.9,.3,.2); 14 | 15 | // setup music player UI 16 | const center = mainCanvasSize.scale(.5); 17 | musicPlayer = new UIObject(center, vec2(500, 300)); 18 | const title = new UIText(vec2(0, -100), vec2(500, 40), 19 | 'LittleJS Music Player'); 20 | musicPlayer.addChild(title); 21 | 22 | // drop zone text 23 | const dropZoneText = new UIText(vec2(0, -60), vec2(450, 20), 24 | 'Drag & Drop Audio Files Here!'); 25 | dropZoneText.textColor = GRAY; 26 | musicPlayer.addChild(dropZoneText); 27 | 28 | // volume slider 29 | const volumeSlider = new UIScrollbar(vec2(0, -20), vec2(400, 30), 30 | musicVolume, 'Music Volume'); 31 | volumeSlider.fillMode = true; 32 | musicPlayer.addChild(volumeSlider); 33 | volumeSlider.onChange = ()=> 34 | { 35 | musicVolume = volumeSlider.value; 36 | musicInstance?.setVolume(musicVolume); 37 | }; 38 | 39 | // play button 40 | playButton = new UIButton(vec2(-90, 50), vec2(140, 50), 'Play'); 41 | musicPlayer.addChild(playButton); 42 | playButton.onClick = ()=> 43 | { 44 | if (!musicSound.isLoaded()) 45 | return; 46 | 47 | // handle play/pause toggle 48 | if (!musicInstance) 49 | musicInstance = musicSound.playMusic(musicVolume); 50 | else if (musicInstance.isPaused()) 51 | musicInstance.resume(); 52 | else 53 | musicInstance.pause(); 54 | }; 55 | 56 | // stop button 57 | stopButton = new UIButton(vec2(90, 50), vec2(140, 50), 'Stop'); 58 | stopButton.onClick = ()=> musicInstance?.stop(); 59 | musicPlayer.addChild(stopButton); 60 | 61 | // progress bar and scrollbar for seeking 62 | progressBar = new UIScrollbar(vec2(0, 120), vec2(400, 30), 0); 63 | progressBar.disabledColor = RED; 64 | progressBar.onChange = ()=> 65 | { 66 | // control music seek position 67 | const wasPlaying = musicInstance?.isPlaying(); 68 | if (!musicInstance) 69 | musicInstance = musicSound.playMusic(musicVolume, 1, 1); 70 | progressBar.value = min(progressBar.value, .999); // prevent wrap 71 | const seekTime = progressBar.value * musicSound.getDuration(); 72 | musicInstance.start(seekTime); 73 | if (!wasPlaying) 74 | musicInstance.pause(); 75 | }; 76 | musicPlayer.addChild(progressBar); 77 | 78 | { 79 | // setup drag and drop for audio files 80 | function onDragEnter() { musicPlayer.color = RED; }; 81 | function onDragLeave() { musicPlayer.color = WHITE; }; 82 | function onDrop(e) 83 | { 84 | musicPlayer.color = WHITE 85 | 86 | // get the dropped file 87 | const file = e.dataTransfer.files[0]; 88 | if (!file || !file.type.startsWith('audio')) 89 | return; 90 | 91 | // create new sound from dropped file 92 | const fileURL = URL.createObjectURL(file); 93 | musicSound = new Sound(fileURL, musicVolume); 94 | dropZoneText.text = file.name; 95 | 96 | // reset UI 97 | musicInstance?.stop(); 98 | musicInstance = undefined; 99 | progressBar.value = 0; 100 | } 101 | uiSystem.setupDragAndDrop(onDrop, onDragEnter, onDragLeave); 102 | } 103 | } 104 | 105 | function gameUpdate() 106 | { 107 | // disable buttons while loading 108 | const isDisabled = !musicSound || !musicSound.isLoaded(); 109 | playButton.disabled = isDisabled 110 | stopButton.disabled = isDisabled 111 | progressBar.disabled = isDisabled 112 | 113 | // update ui 114 | if (!musicSound) 115 | { 116 | // waiting for file 117 | progressBar.text = 'No File Loaded'; 118 | } 119 | else if (isDisabled) 120 | { 121 | // update loading progress 122 | const loadingPercent = musicSound.loadedPercent * 100|0; 123 | progressBar.text = `Loading: ${loadingPercent}%`; 124 | } 125 | else 126 | { 127 | // update ui text 128 | const isPlaying = musicInstance?.isPlaying(); 129 | playButton.text = isPlaying ? 'Pause' : 'Play'; 130 | const current = musicInstance?.getCurrentTime() || 0; 131 | const duration = musicSound.getDuration(); 132 | progressBar.text = formatTime(current) + 133 | ' / ' + formatTime(duration); 134 | if (!progressBar.isActiveObject()) 135 | progressBar.value = current / duration; 136 | } 137 | } -------------------------------------------------------------------------------- /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 | // import LittleJS module 10 | import * as LJS from '../../dist/littlejs.esm.js'; 11 | const {vec2, hsl, tile} = LJS; 12 | 13 | // UI system 14 | let uiRoot, uiMenu; 15 | const getMenuVisible =()=> uiMenu.visible; 16 | const setMenuVisible =(visible)=> uiMenu.visible = visible; 17 | 18 | // use a fixed size canvas 19 | LJS.setCanvasFixedSize(vec2(1920, 1080)); // 1080p 20 | LJS.setCanvasPixelated(false); 21 | 22 | function createUI() 23 | { 24 | LJS.uiSystem.defaultSoundPress = new LJS.Sound([.5,0,220]); 25 | LJS.uiSystem.defaultSoundClick = new LJS.Sound([.5,0,440]); 26 | LJS.uiSystem.defaultCornerRadius = 8; 27 | LJS.uiSystem.defaultGradientColor = LJS.WHITE; 28 | LJS.uiSystem.defaultShadowColor = LJS.BLACK; 29 | 30 | // setup root to attach all ui elements to 31 | uiRoot = new LJS.UIObject; 32 | const uiInfo = new LJS.UIText(vec2(0,90), vec2(1e3, 70), 33 | 'LittleJS UI System Example\nM = Toggle menu'); 34 | uiInfo.textColor = LJS.WHITE; 35 | uiInfo.textLineWidth = 8; 36 | uiRoot.addChild(uiInfo); 37 | 38 | // setup example menu 39 | uiMenu = new LJS.UIObject(vec2(0,500)); 40 | uiRoot.addChild(uiMenu); 41 | const uiBackground = new LJS.UIObject(vec2(), vec2(450,580)); 42 | uiBackground.lineWidth = 8; 43 | uiMenu.addChild(uiBackground); 44 | 45 | // example large text 46 | const textTitle = new LJS.UIText(vec2(0,-220), vec2(400, 120), 'Test Title'); 47 | uiMenu.addChild(textTitle); 48 | textTitle.textColor = LJS.RED; 49 | textTitle.textLineColor = LJS.BLUE; 50 | textTitle.textLineWidth = 4; 51 | 52 | // example multiline text 53 | const textTest = new LJS.UIText(vec2(-60,-120), vec2(300, 60), 'Test Text\nSecond text line.') 54 | uiMenu.addChild(textTest); 55 | 56 | // example tile image 57 | const tileTest = new LJS.UITile(vec2(150,-130), vec2(110), tile(3,128)) 58 | uiMenu.addChild(tileTest); 59 | 60 | // setup navigation index for gamepad and keyboard navigation 61 | let navigationIndex = 0; 62 | 63 | // example checkbox 64 | const checkbox = new LJS.UICheckbox(vec2(-140,-20), vec2(50)); 65 | uiMenu.addChild(checkbox); 66 | checkbox.onChange = ()=> button1.disabled = checkbox.checked; 67 | checkbox.navigationIndex = ++navigationIndex; 68 | checkbox.text = 'Test Checkbox'; 69 | 70 | // example scrollbar 71 | const scrollbar = new LJS.UIScrollbar(vec2(0,60), vec2(350, 50)); 72 | uiMenu.addChild(scrollbar); 73 | scrollbar.onChange = ()=> scrollbar.text = scrollbar.value.toFixed(2) 74 | scrollbar.onChange(); 75 | scrollbar.navigationIndex = ++navigationIndex; 76 | 77 | // example button 78 | const button1 = new LJS.UIButton(vec2(0,140), vec2(350, 50), 'Test Button'); 79 | uiMenu.addChild(button1); 80 | button1.onClick = ()=> uiBackground.color = hsl(LJS.rand(),1,.7); 81 | button1.navigationIndex = ++navigationIndex; 82 | 83 | // exit button 84 | const button2 = new LJS.UIButton(vec2(0,220), vec2(350, 50), 'Exit Menu'); 85 | uiMenu.addChild(button2); 86 | button2.onClick = ()=> LJS.uiSystem.showConfirmDialog('Exit menu?', 87 | ()=> setMenuVisible(false)); 88 | button2.navigationIndex = ++navigationIndex; 89 | button2.navigationAutoSelect = true; 90 | } 91 | 92 | /////////////////////////////////////////////////////////////////////////////// 93 | function gameInit() 94 | { 95 | new LJS.UISystemPlugin; 96 | createUI(); 97 | LJS.setCanvasClearColor(hsl(0,0,.2)); 98 | } 99 | 100 | /////////////////////////////////////////////////////////////////////////////// 101 | function gameUpdate() 102 | { 103 | } 104 | 105 | /////////////////////////////////////////////////////////////////////////////// 106 | function gameUpdatePost() 107 | { 108 | if (LJS.keyWasPressed('KeyM') && !LJS.uiSystem.confirmDialog) 109 | { 110 | // toggle menu visibility 111 | setMenuVisible(!getMenuVisible()); 112 | } 113 | 114 | // center ui 115 | uiRoot.pos.x = LJS.mainCanvasSize.x/2; 116 | 117 | // pause when menu is visible 118 | LJS.setPaused(getMenuVisible()) 119 | } 120 | 121 | /////////////////////////////////////////////////////////////////////////////// 122 | function gameRender() 123 | { 124 | // test game rendering 125 | for (let i=0; i<1e3; ++i) 126 | { 127 | const pos = vec2(30*LJS.sin(i+LJS.time/9),20*LJS.sin(i*i+LJS.time/9)); 128 | LJS.drawTile(pos, vec2(2), tile(3,128), hsl(i/9,1,.4), LJS.time+i, !(i%2), hsl(i/9,1,.1,0)); 129 | } 130 | } 131 | 132 | /////////////////////////////////////////////////////////////////////////////// 133 | function gameRenderPost() 134 | { 135 | } 136 | 137 | /////////////////////////////////////////////////////////////////////////////// 138 | // Startup LittleJS Engine 139 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /examples/breakout/gameObjects.js: -------------------------------------------------------------------------------- 1 | /* 2 | LittleJS Breakout Objects 3 | */ 4 | 5 | 'use strict'; 6 | 7 | // import LittleJS module 8 | import * as LJS from '../../dist/littlejs.esm.js'; 9 | import * as Game from './game.js'; 10 | const {tile, vec2, hsl} = LJS; 11 | 12 | /////////////////////////////////////////////////////////////////////////////// 13 | // sound effects 14 | const sound_start = new LJS.Sound([,0,500,,.04,.3,1,2,,,570,.02,.02,,,,.04]); 15 | const sound_break = new LJS.Sound([,,90,,.01,.03,4,,,,,,,9,50,.2,,.2,.01]); 16 | const sound_bounce = new LJS.Sound([,,1e3,,.03,.02,1,2,,,940,.03,,,,,.2,.6,,.06]); 17 | 18 | /////////////////////////////////////////////////////////////////////////////// 19 | export class PhysicsObject extends LJS.EngineObject 20 | { 21 | constructor(pos, size, tileInfo, angle, color) 22 | { 23 | super(pos, size, tileInfo, angle, color); 24 | this.setCollision(); // make object collide 25 | this.mass = 0; // make object have static physics 26 | } 27 | } 28 | 29 | /////////////////////////////////////////////////////////////////////////////// 30 | export class Wall extends PhysicsObject 31 | { 32 | constructor(pos, size) 33 | { 34 | super(pos, size, 0, 0, hsl(0,0,0,0)); 35 | } 36 | } 37 | 38 | /////////////////////////////////////////////////////////////////////////////// 39 | export class Paddle extends PhysicsObject 40 | { 41 | constructor(pos) 42 | { 43 | super(pos, vec2(5,.5)); 44 | } 45 | 46 | update() 47 | { 48 | // control with gamepad or mouse 49 | this.pos.x = LJS.isUsingGamepad ? this.pos.x + LJS.gamepadStick(0).x : LJS.mousePos.x; 50 | 51 | // keep paddle in bounds of level 52 | this.pos.x = LJS.clamp(this.pos.x, this.size.x/2, Game.levelSize.x - this.size.x/2); 53 | } 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | export class Brick extends PhysicsObject 58 | { 59 | constructor(pos) 60 | { 61 | super(pos, vec2(2,1), tile(1, vec2(32,16)), 0, LJS.randColor()); 62 | Game.changeBrickCount(1); 63 | } 64 | 65 | collideWithObject(o) 66 | { 67 | // destroy brick when hit with ball 68 | this.destroy(); 69 | Game.changeBrickCount(-1); 70 | sound_break.play(this.pos); 71 | 72 | // make explosion effect 73 | const color1 = this.color; 74 | const color2 = color1.lerp(hsl(), .5); 75 | new LJS.ParticleEmitter( 76 | this.pos, 0, // pos, angle 77 | this.size, .1, 200, 3.14, // emitSize, emitTime, rate, cone 78 | tile(0, 16), // tileIndex, tileSize 79 | color1, color2, // colorStartA, colorStartB 80 | color1.scale(1,0), color2.scale(1,0), // colorEndA, colorEndB 81 | .3, .8, .3, .05, .05,// time, sizeStart, sizeEnd, speed, angleSpeed 82 | .99, .95, .4, 3.14, // damp, angleDamp, gravity, cone 83 | .1, .8, 0, 1 // fade, randomness, collide, additive 84 | ); 85 | 86 | // set ball trail color 87 | if (o.trailEffect) 88 | { 89 | o.trailEffect.colorStartA = this.color; 90 | o.trailEffect.colorStartB = this.color.lerp(hsl(), .5); 91 | } 92 | 93 | return 1; 94 | } 95 | } 96 | 97 | /////////////////////////////////////////////////////////////////////////////// 98 | export class Ball extends PhysicsObject 99 | { 100 | constructor(pos) 101 | { 102 | super(pos, vec2(.5), tile(0)); 103 | 104 | // make a bouncy ball 105 | this.velocity = vec2(0, -.1); 106 | this.restitution = 1; 107 | this.mass = 1; 108 | 109 | // attach a trail effect 110 | const color = hsl(0,0,.2); 111 | this.trailEffect = new LJS.ParticleEmitter( 112 | this.pos, 0, // pos, angle 113 | this.size, 0, 80, 3.14, // emitSize, emitTime, rate, cone 114 | tile(0, 16), // tileIndex, tileSize 115 | color, color, // colorStartA, colorStartB 116 | color.scale(0), color.scale(0), // colorEndA, colorEndB 117 | 2, .4, 1, .001, .05,// time, sizeStart, sizeEnd, speed, angleSpeed 118 | .99, .95, 0, 3.14, // damp, angleDamp, gravity, cone 119 | .1, .5, 0, 1 // fade, randomness, collide, additive 120 | ); 121 | this.addChild(this.trailEffect); 122 | sound_start.play(this.pos); 123 | } 124 | 125 | collideWithObject(o) 126 | { 127 | // only need special handling when colliding with paddle 128 | if (o != Game.paddle) 129 | return true; 130 | 131 | // prevent colliding with paddle if moving upwards 132 | if (this.velocity.y > 0) 133 | return false; 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 = LJS.max(-this.velocity.y, .2); 138 | 139 | // speed up 140 | const speed = LJS.min(1.04*this.getSpeed(), .5); 141 | this.velocity = this.velocity.normalize(speed); 142 | sound_bounce.play(this.pos, 1, speed*2); 143 | 144 | // prevent default collision code 145 | return false; 146 | } 147 | } -------------------------------------------------------------------------------- /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 | // import LittleJS module 12 | import * as LJS from '../../dist/littlejs.esm.js'; 13 | import * as GameObjects from './gameObjects.js'; 14 | const {vec2, hsl} = LJS; 15 | 16 | /////////////////////////////////////////////////////////////////////////////// 17 | // game objects 18 | export let ball, score, brickCount, paddle; 19 | export const levelSize = vec2(38, 20); 20 | 21 | export function changeBrickCount(delta) 22 | { 23 | brickCount += delta; 24 | 25 | // increase score when brick is destroyed 26 | if (delta < 0) 27 | score -= delta; 28 | } 29 | 30 | /////////////////////////////////////////////////////////////////////////////// 31 | function gameReset() 32 | { 33 | // reset game objects 34 | LJS.engineObjectsDestroy(); 35 | score = 0; 36 | brickCount = 0; 37 | 38 | // spawn bricks 39 | const pos = vec2(); 40 | for (pos.x = 4; pos.x <= levelSize.x-4; pos.x += 2) 41 | for (pos.y = 12; pos.y <= levelSize.y-2; pos.y += 1) 42 | new GameObjects.Brick(pos); 43 | 44 | // create walls 45 | new GameObjects.Wall(vec2(-.5,levelSize.y/2), vec2(1,100)); // top 46 | new GameObjects.Wall(vec2(levelSize.x+.5,levelSize.y/2), vec2(1,100)); // left 47 | new GameObjects.Wall(vec2(levelSize.x/2,levelSize.y+.5), vec2(100,1)); // right 48 | 49 | // spawn player paddle 50 | paddle = new GameObjects.Paddle(vec2(levelSize.x/2-12, 1)); 51 | 52 | // reset ball 53 | ball = 0; 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | function gameInit() 58 | { 59 | LJS.setCanvasFixedSize(vec2(1920, 1080)); // 1080p 60 | LJS.setCameraPos(levelSize.scale(.5)); // center camera 61 | LJS.setCameraScale(48); 62 | 63 | // set up a post processing shader 64 | setupPostProcess(); 65 | 66 | // start a new game 67 | gameReset(); 68 | } 69 | 70 | /////////////////////////////////////////////////////////////////////////////// 71 | function gameUpdate() 72 | { 73 | // spawn ball 74 | if (!ball && (LJS.mouseWasPressed(0) || LJS.gamepadWasPressed(0))) 75 | ball = new GameObjects.Ball(vec2(levelSize.x/2, levelSize.y/2)); 76 | 77 | if (ball && ball.pos.y < -1) 78 | { 79 | // destroy ball if it goes below the level 80 | ball.destroy(); 81 | ball = 0; 82 | } 83 | 84 | if (LJS.keyWasPressed('KeyR')) 85 | gameReset(); 86 | } 87 | 88 | /////////////////////////////////////////////////////////////////////////////// 89 | function gameUpdatePost() 90 | { 91 | 92 | } 93 | 94 | /////////////////////////////////////////////////////////////////////////////// 95 | function gameRender() 96 | { 97 | // draw a the background 98 | LJS.drawRect(LJS.cameraPos, levelSize.scale(2), hsl(0,0,.5)); 99 | LJS.drawRect(LJS.cameraPos, levelSize, hsl(0,0,.02)); 100 | } 101 | 102 | /////////////////////////////////////////////////////////////////////////////// 103 | function gameRenderPost() 104 | { 105 | // use built in image font for text 106 | const font = LJS.engineFontImage; 107 | font.drawText('Score: ' + score, LJS.cameraPos.add(vec2(0,9.2)), 1); 108 | if (!brickCount) 109 | font.drawText('You Win!', LJS.cameraPos.add(vec2(0,-5)), 2); 110 | else if (!ball) 111 | font.drawText('Click to Play', LJS.cameraPos.add(vec2(0,-5)), 2); 112 | } 113 | 114 | /////////////////////////////////////////////////////////////////////////////// 115 | // an example shader that can be used to apply a post processing effect 116 | function setupPostProcess() 117 | { 118 | const televisionShader = ` 119 | // Simple TV Shader Code 120 | float hash(vec2 p) 121 | { 122 | p=fract(p*.3197); 123 | return fract(1.+sin(51.*p.x+73.*p.y)*13753.3); 124 | } 125 | 126 | void mainImage(out vec4 c, vec2 p) 127 | { 128 | // setup the shader 129 | vec2 uv = p; 130 | p /= iResolution.xy; 131 | c = texture(iChannel0, p); 132 | 133 | // static noise 134 | const float staticAlpha = .1; 135 | const float staticScale = .002; 136 | c += staticAlpha * hash(floor(p/staticScale) + mod(iTime*500., 1e3)); 137 | 138 | // scan lines 139 | const float scanlineScale = 2.; 140 | const float scanlineAlpha = .6; 141 | c *= 1. - scanlineAlpha*cos(p.y*2.*iResolution.y/scanlineScale); 142 | 143 | { 144 | // bloom effect 145 | const float blurSize = .002; 146 | const float bloomIntensity = .2; 147 | 148 | // 5-tap Gaussian blur 149 | vec4 bloom = vec4(0); 150 | bloom += texture(iChannel0, p + vec2(-2.*blurSize, 0)) * .12; 151 | bloom += texture(iChannel0, p + vec2( -blurSize, 0)) * .24; 152 | bloom += texture(iChannel0, p) * .28; 153 | bloom += texture(iChannel0, p + vec2( blurSize, 0)) * .24; 154 | bloom += texture(iChannel0, p + vec2( 2.*blurSize, 0)) * .12; 155 | bloom += texture(iChannel0, p + vec2(0, -2.*blurSize)) * .12; 156 | bloom += texture(iChannel0, p + vec2(0, -blurSize)) * .24; 157 | bloom += texture(iChannel0, p) * .28; 158 | bloom += texture(iChannel0, p + vec2(0, blurSize)) * .24; 159 | bloom += texture(iChannel0, p + vec2(0, 2.*blurSize)) * .12; 160 | c += bloom * bloomIntensity; 161 | } 162 | 163 | // black vignette around edges 164 | const float vignette = 2.; 165 | const float vignettePow = 6.; 166 | float dx = 2.*p.x-1., dy = 2.*p.y-1.; 167 | c *= 1.-pow((dx*dx + dy*dy)/vignette, vignettePow); 168 | }`; 169 | 170 | new LJS.PostProcessPlugin(televisionShader); 171 | } 172 | 173 | /////////////////////////////////////////////////////////////////////////////// 174 | // Startup LittleJS Engine 175 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); -------------------------------------------------------------------------------- /plugins/zzfxm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS ZzFXM Plugin 3 | * @namespace ZzFXM 4 | */ 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * Music Object - Stores a zzfx music track for later use 10 | * 11 | * Create music with the ZzFXM tracker. 12 | * @extends Sound 13 | * @memberof ZzFXM 14 | * @example 15 | * // create some music 16 | * const music_example = new Music( 17 | * [ 18 | * [ // instruments 19 | * [,0,400] // simple note 20 | * ], 21 | * [ // patterns 22 | * [ // pattern 1 23 | * [ // channel 0 24 | * 0, -1, // instrument 0, left speaker 25 | * 1, 0, 9, 1 // channel notes 26 | * ], 27 | * [ // channel 1 28 | * 0, 1, // instrument 0, right speaker 29 | * 0, 12, 17, -1 // channel notes 30 | * ] 31 | * ], 32 | * ], 33 | * [0, 0, 0, 0], // sequence, play pattern 0 four times 34 | * 90 // BPM 35 | * ]); 36 | * 37 | * // play the music 38 | * music_example.play(); 39 | */ 40 | class ZzFXMusic extends Sound 41 | { 42 | /** Create a music object and cache the zzfx music samples for later use 43 | * @param {[Array, Array, Array, number]} zzfxMusic - Array of zzfx music parameters 44 | */ 45 | constructor(zzfxMusic) 46 | { 47 | super(undefined); 48 | 49 | if (!soundEnable || headlessMode) return; 50 | this.randomness = 0; 51 | this.sampleChannels = zzfxM(...zzfxMusic); 52 | this.sampleRate = audioDefaultSampleRate; 53 | } 54 | 55 | /** Play the music that loops by default 56 | * @param {number} [volume] - Volume to play the music at 57 | * @param {boolean} [loop] - Should the music loop? 58 | * @return {SoundInstance} - The sound instance 59 | */ 60 | playMusic(volume=1, loop=true) 61 | { return super.play(undefined, volume, 1, 0, loop); } 62 | } 63 | 64 | /////////////////////////////////////////////////////////////////////////////// 65 | // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force 66 | 67 | /** Generate samples for a ZzFM song with given parameters 68 | * @param {Array} instruments - Array of ZzFX sound parameters 69 | * @param {Array} patterns - Array of pattern data 70 | * @param {Array} sequence - Array of pattern indexes 71 | * @param {number} [BPM] - Playback speed of the song in BPM 72 | * @return {Array} - Left and right channel sample data 73 | * @memberof ZzFXM */ 74 | function zzfxM(instruments, patterns, sequence, BPM = 125) 75 | { 76 | let i, j, k; 77 | let instrumentParameters; 78 | let note; 79 | let sample; 80 | let patternChannel; 81 | let notFirstBeat; 82 | let stop; 83 | let instrument; 84 | let attenuation; 85 | let outSampleOffset; 86 | let isSequenceEnd; 87 | let sampleOffset = 0; 88 | let nextSampleOffset; 89 | let sampleBuffer = []; 90 | let leftChannelBuffer = []; 91 | let rightChannelBuffer = []; 92 | let channelIndex = 0; 93 | let panning = 0; 94 | let hasMore = 1; 95 | let sampleCache = {}; 96 | let beatLength = audioDefaultSampleRate / BPM * 60 >> 2; 97 | 98 | // for each channel in order until there are no more 99 | for (; hasMore; channelIndex++) { 100 | 101 | // reset current values 102 | sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0]; 103 | 104 | // for each pattern in sequence 105 | sequence.forEach((patternIndex, sequenceIndex)=> { 106 | // get pattern for current channel, use empty 1 note pattern if none found 107 | patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0]; 108 | 109 | // check if there are more channels 110 | hasMore |= patterns[patternIndex][channelIndex]&&1; 111 | 112 | // get next offset, use the length of first channel 113 | nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength; 114 | // for each beat in pattern, plus one extra if end of sequence 115 | isSequenceEnd = sequenceIndex === sequence.length - 1; 116 | for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) { 117 | 118 | // 119 | note = patternChannel[i]; 120 | 121 | // stop if end, different instrument or new note 122 | stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd || 123 | instrument !== (patternChannel[0] || 0) || note | 0; 124 | 125 | // fill buffer with samples for previous beat, most cpu intensive part 126 | for (j = 0; j < beatLength && notFirstBeat; 127 | 128 | // fade off attenuation at end of beat if stopping note, prevents clicking 129 | j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0 130 | ) { 131 | // copy sample to stereo buffers with panning 132 | sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0; 133 | leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample; 134 | rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample; 135 | } 136 | 137 | // set up for next note 138 | if (note) { 139 | // set attenuation 140 | attenuation = note % 1; 141 | panning = patternChannel[1] || 0; 142 | if (note |= 0) { 143 | // get cached sample 144 | sampleBuffer = sampleCache[ 145 | [ 146 | instrument = patternChannel[sampleOffset = 0] || 0, 147 | note 148 | ] 149 | ] = sampleCache[[instrument, note]] || ( 150 | // add sample to cache 151 | instrumentParameters = [...instruments[instrument]], 152 | instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1), 153 | 154 | // allow negative values to stop notes 155 | note > 0 ? zzfxG(...instrumentParameters) : [] 156 | ); 157 | } 158 | } 159 | } 160 | 161 | // update the sample offset 162 | outSampleOffset = nextSampleOffset; 163 | }); 164 | } 165 | 166 | return [leftChannelBuffer, rightChannelBuffer]; 167 | } -------------------------------------------------------------------------------- /examples/box2d/game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Little JS Box2d Demo 3 | - Demonstrates how to use Box2D with LittleJS 4 | - Several scenes to demonstrate Box2D features 5 | - Every type of shape and joint 6 | - Contact begin and end callbacks 7 | - Raycasting and querying 8 | - Collision filtering 9 | - User Interaction 10 | */ 11 | 12 | 'use strict'; 13 | 14 | // import LittleJS module 15 | import * as LJS from '../../dist/littlejs.esm.js'; 16 | import * as GameObjects from './gameObjects.js'; 17 | import * as Scenes from './scenes.js'; 18 | const {vec2, hsl} = LJS; 19 | 20 | // use HD textures 21 | LJS.setCanvasPixelated(false); 22 | LJS.setTilesPixelated(false); 23 | 24 | /////////////////////////////////////////////////////////////////////////////// 25 | // game variables 26 | 27 | const maxScenes = 11; 28 | const startScene = 0; 29 | export let spriteAtlas, groundObject, mouseJoint, repeatSpawnTimer = new LJS.Timer; 30 | const sound_click = new LJS.Sound([.2,.1,,,,.01,,,,,,,,,,,,,,,-500]); 31 | 32 | /////////////////////////////////////////////////////////////////////////////// 33 | function setScene(scene) 34 | { 35 | // setup 36 | LJS.setCameraPos(vec2(20,10)); 37 | LJS.setGravity(vec2(0,-20)); 38 | 39 | // destroy old scene 40 | LJS.engineObjectsDestroy(); 41 | mouseJoint = 0; 42 | 43 | // create walls 44 | groundObject = GameObjects.spawnBox(vec2(0,-4), vec2(1e3,8), hsl(0,0,.2), LJS.box2d.bodyTypeStatic); 45 | GameObjects.spawnBox(vec2(-4, 0), vec2(8,1e3), LJS.BLACK, LJS.box2d.bodyTypeStatic); 46 | GameObjects.spawnBox(vec2(44, 0), vec2(8,1e3), LJS.BLACK, LJS.box2d.bodyTypeStatic); 47 | GameObjects.spawnBox(vec2(0,100), vec2(1e3,8), LJS.BLACK, LJS.box2d.bodyTypeStatic); 48 | 49 | // load the scene 50 | Scenes.loadScene(scene); 51 | } 52 | 53 | /////////////////////////////////////////////////////////////////////////////// 54 | async function gameInit() 55 | { 56 | // start up LittleJS Box2D plugin 57 | await LJS.box2dInit(); 58 | //LJS.box2dSetDebug(true); // enable box2d debug draw 59 | 60 | // create a table of all sprites 61 | const gameTile = (i)=> LJS.tile(i, 124, 0, 2); 62 | spriteAtlas = 63 | { 64 | circle: gameTile(0), 65 | dot: gameTile(1), 66 | circleOutline: gameTile(2), 67 | squareOutline: gameTile(3), 68 | wheel: gameTile(4), 69 | gear: gameTile(5), 70 | squareOutline2: gameTile(6), 71 | }; 72 | 73 | // startup the demo 74 | LJS.setCanvasClearColor(hsl(0,0,.8)); 75 | setScene(startScene); 76 | } 77 | 78 | /////////////////////////////////////////////////////////////////////////////// 79 | function gameUpdate() 80 | { 81 | // scale canvas to fit based on 1080p 82 | LJS.setCameraScale(LJS.mainCanvasSize.y * 48 / 1080); 83 | 84 | // mouse controls 85 | if (mouseJoint) 86 | { 87 | // update mouse joint 88 | mouseJoint.setTarget(LJS.mousePos); 89 | if (LJS.mouseWasReleased(0)) 90 | { 91 | // release object 92 | sound_click.play(LJS.mousePos, 1, .5); 93 | mouseJoint.destroy(); 94 | mouseJoint = 0; 95 | } 96 | } 97 | else if (LJS.mouseWasPressed(0)) 98 | { 99 | // grab object 100 | sound_click.play(LJS.mousePos); 101 | const object = LJS.box2d.pointCast(LJS.mousePos); 102 | if (object) 103 | mouseJoint = new LJS.Box2dTargetJoint(object, groundObject, LJS.mousePos); 104 | } 105 | 106 | // controls 107 | if (LJS.mouseIsDown(1) || LJS.keyIsDown('KeyZ')) 108 | { 109 | const isSet = repeatSpawnTimer.isSet(); 110 | if (!isSet || repeatSpawnTimer.elapsed()) 111 | { 112 | // spawn continuously after a delay 113 | isSet || repeatSpawnTimer.set(.5); 114 | GameObjects.spawnRandomObject(LJS.mousePos); 115 | } 116 | } 117 | else 118 | repeatSpawnTimer.unset(); 119 | if (LJS.mouseWasPressed(2) || LJS.keyWasPressed('KeyX')) 120 | GameObjects.explosion(LJS.mousePos); 121 | 122 | if (LJS.keyWasPressed('KeyR')) 123 | setScene(Scenes.scene); // reset scene 124 | if (LJS.keyWasPressed('ArrowUp') || LJS.keyWasPressed('ArrowDown')) 125 | { 126 | // change scene 127 | const upPressed = LJS.keyWasPressed('ArrowUp'); 128 | setScene(LJS.mod(Scenes.scene + (upPressed?1:-1), maxScenes)); 129 | } 130 | } 131 | 132 | /////////////////////////////////////////////////////////////////////////////// 133 | function gameUpdatePost() 134 | { 135 | 136 | } 137 | 138 | /////////////////////////////////////////////////////////////////////////////// 139 | function gameRender() 140 | { 141 | if (Scenes.scene == 5) 142 | { 143 | // raycast test 144 | const count = 100; 145 | const distance = 10; 146 | for (let i=count;i--;) 147 | { 148 | const start = LJS.mousePos; 149 | const end = start.add(vec2(distance,0).rotate(i/count*LJS.PI*2)); 150 | const result = LJS.box2d.raycast(start, end); 151 | const color = result ? hsl(0,1,.5,.5) : hsl(.5,1,.5,.5); 152 | LJS.drawLine(start, result ? result.point : end, .1, color); 153 | } 154 | } 155 | } 156 | 157 | /////////////////////////////////////////////////////////////////////////////// 158 | function gameRenderPost() 159 | { 160 | if (mouseJoint) 161 | { 162 | // draw mouse joint 163 | const ab = mouseJoint.getAnchorB(); 164 | LJS.drawTile(ab, vec2(.3), spriteAtlas.circle, LJS.BLACK); 165 | LJS.drawLine(LJS.mousePos, ab, .1, LJS.BLACK); 166 | } 167 | 168 | // draw demo info 169 | const pos = vec2(LJS.mainCanvasSize.x/2, 50); 170 | drawText('LittleJS Box2D Demo', 65, 70); 171 | drawText(Scenes.sceneName, 50, 100); 172 | if (Scenes.scene == 0) 173 | { 174 | drawText('Mouse Left = Grab'); 175 | drawText('Mouse Middle or Z = Spawn'); 176 | drawText('Mouse Right or X = Explode'); 177 | drawText('Arrows Up/Down = Change Scene'); 178 | } 179 | if (Scenes.scene == 3) 180 | { 181 | drawText('Right = Accelerate'); 182 | drawText('Left = Reverse'); 183 | } 184 | 185 | function drawText(text, size=40, gap=50) 186 | { LJS.drawTextScreen(text, pos, size, LJS.WHITE, size*.1); pos.y += gap; } 187 | } 188 | 189 | /////////////////////////////////////////////////////////////////////////////// 190 | // Startup LittleJS Engine with Box2D 191 | 192 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['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 | // import LittleJS module 11 | import * as LJS from '../../dist/littlejs.esm.js'; 12 | const {vec2, rgb} = LJS; 13 | 14 | /////////////////////////////////////////////////////////////////////////////// 15 | 16 | // globals 17 | const levelSize = vec2(38, 20); // size of play area 18 | let score = 0; // start score at 0 19 | let ball; // keep track of ball object 20 | let paddle; // keep track of player paddle 21 | 22 | // sound effects 23 | const sound_bounce = new LJS.Sound([,,1e3,,.03,.02,1,2,,,940,.03,,,,,.2,.6,,.06], 0); 24 | const sound_break = new LJS.Sound([,,90,,.01,.03,4,,,,,,,9,50,.2,,.2,.01], 0); 25 | const sound_start = new LJS.Sound([,0,500,,.04,.3,1,2,,,570,.02,.02,,,,.04]); 26 | 27 | /////////////////////////////////////////////////////////////////////////////// 28 | 29 | class Paddle extends LJS.EngineObject 30 | { 31 | constructor() 32 | { 33 | super(vec2(0,1), vec2(6,.5)); // set object position and size 34 | this.setCollision(); // make object collide 35 | this.mass = 0; // make object have static physics 36 | } 37 | 38 | update() 39 | { 40 | this.pos.x = LJS.mousePos.x; // move paddle to mouse 41 | 42 | // clamp paddle to level size 43 | this.pos.x = LJS.clamp(this.pos.x, this.size.x/2, levelSize.x - this.size.x/2); 44 | } 45 | } 46 | 47 | class Ball extends LJS.EngineObject 48 | { 49 | constructor(pos) 50 | { 51 | super(pos, vec2(.5)); // set object position and size 52 | 53 | this.velocity = vec2(-.1, -.1); // give ball some movement 54 | this.setCollision(); // make object collide 55 | this.restitution = 1; // make object bounce 56 | } 57 | collideWithObject(o) 58 | { 59 | // prevent colliding with paddle if moving upwards 60 | if (o == paddle && this.velocity.y > 0) 61 | return false; 62 | 63 | // speed up 64 | const speed = LJS.min(1.04*this.velocity.length(), .5); 65 | this.velocity = this.velocity.normalize(speed); 66 | 67 | // play bounce sound with pitch scaled by speed 68 | sound_bounce.play(this.pos, 1, speed); 69 | 70 | if (o == paddle) 71 | { 72 | // control bounce angle when ball collides with paddle 73 | const deltaX = o.pos.x - this.pos.x; 74 | this.velocity = this.velocity.rotate(.3 * deltaX); 75 | 76 | // make sure ball is moving upwards with a minimum speed 77 | this.velocity.y = LJS.max(-this.velocity.y, .2); 78 | 79 | // prevent default collision code 80 | return false; 81 | } 82 | 83 | return true; // allow object to collide 84 | } 85 | } 86 | 87 | class Wall extends LJS.EngineObject 88 | { 89 | constructor(pos, size) 90 | { 91 | super(pos, size); // set object position and size 92 | 93 | this.setCollision(); // make object collide 94 | this.mass = 0; // make object have static physics 95 | this.color = rgb(0,0,0,0); // make object invisible 96 | } 97 | } 98 | 99 | class Brick extends LJS.EngineObject 100 | { 101 | constructor(pos, size) 102 | { 103 | super(pos, size); 104 | 105 | this.setCollision(); // make object collide 106 | this.mass = 0; // make object have static physics 107 | this.color = LJS.randColor(); // give brick a random color 108 | } 109 | 110 | collideWithObject(o) 111 | { 112 | this.destroy(); // destroy block when hit 113 | sound_break.play(this.pos); // play brick break sound 114 | ++score; // award a point for each brick broke 115 | 116 | // create explosion effect 117 | const color = this.color; 118 | new LJS.ParticleEmitter( 119 | this.pos, 0, // pos, angle 120 | this.size, .1, 200, 3.14,// emitSize, emitTime, rate, cone 121 | undefined, // tileInfo 122 | color, color, // colorStartA, colorStartB 123 | color.scale(1,0), color.scale(1,0), // colorEndA, colorEndB 124 | .2, .5, 1, .1, .1, // time, sizeStart, sizeEnd, speed, angleSpeed 125 | .99, .95, .4, 3.14, // damp, angleDamp, gravity, cone 126 | .1, .5, false, true // fade, randomness, collide, additive 127 | ); 128 | 129 | return true; // allow object to collide 130 | } 131 | } 132 | 133 | /////////////////////////////////////////////////////////////////////////////// 134 | function gameInit() 135 | { 136 | // setup camera and canvas 137 | LJS.setCameraPos(levelSize.scale(.5)); // center camera in level 138 | LJS.setCanvasFixedSize(vec2(1280, 720)); // use a 720p fixed size canvas 139 | 140 | // create bricks 141 | for (let x=2; x<=levelSize.x-2; x+=2) 142 | for (let y=12; y<=levelSize.y-2; y+=1) 143 | new Brick(vec2(x,y), vec2(2,1)); // create a brick 144 | 145 | // create player paddle 146 | paddle = new Paddle; 147 | 148 | // create walls 149 | new Wall(vec2(-.5,levelSize.y/2), vec2(1,100)) // top 150 | new Wall(vec2(levelSize.x+.5,levelSize.y/2), vec2(1,100)) // left 151 | new Wall(vec2(levelSize.x/2,levelSize.y+.5), vec2(100,1)) // right 152 | } 153 | 154 | /////////////////////////////////////////////////////////////////////////////// 155 | function gameUpdate() 156 | { 157 | if (ball && ball.pos.y < -1) // if ball is below level 158 | { 159 | // destroy old ball 160 | ball.destroy(); 161 | ball = 0; 162 | } 163 | if (!ball && LJS.mouseWasPressed(0)) 164 | { 165 | // spawn new ball if there is no ball and left mouse pressed 166 | ball = new Ball(LJS.cameraPos); // create a ball 167 | sound_start.play(); // play start sound 168 | } 169 | } 170 | 171 | /////////////////////////////////////////////////////////////////////////////// 172 | function gameUpdatePost() 173 | { 174 | } 175 | 176 | /////////////////////////////////////////////////////////////////////////////// 177 | function gameRender() 178 | { 179 | LJS.drawRect(LJS.cameraPos, vec2(100), rgb(.5,.5,.5)); // draw background 180 | LJS.drawRect(LJS.cameraPos, levelSize, rgb(.1,.1,.1)); // draw level boundary 181 | } 182 | 183 | /////////////////////////////////////////////////////////////////////////////// 184 | function gameRenderPost() 185 | { 186 | LJS.drawTextScreen('Score ' + score, vec2(LJS.mainCanvasSize.x/2, 70), 50); // show score 187 | } 188 | 189 | /////////////////////////////////////////////////////////////////////////////// 190 | // Startup LittleJS Engine 191 | LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost); -------------------------------------------------------------------------------- /plugins/drawUtilities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Drawing Utilities Plugin 3 | * - Extra drawing functions for LittleJS 4 | * - Nine slice and three slice drawing 5 | * @namespace DrawUtilities 6 | */ 7 | 8 | 'use strict'; 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | 12 | /** Draw a scalable nine-slice UI element to the main canvas in screen space 13 | * This function can not apply color because it draws using the 2d context 14 | * @param {Vector2} pos - Screen space position 15 | * @param {Vector2} size - Screen space size 16 | * @param {TileInfo} startTile - Starting tile for the nine-slice pattern 17 | * @param {number} [borderSize] - Width of the border sections 18 | * @param {number} [extraSpace] - Extra spacing adjustment 19 | * @param {number} [angle] - Angle to rotate by 20 | * @memberof DrawUtilities */ 21 | function drawNineSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0) 22 | { 23 | drawNineSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true); 24 | } 25 | 26 | /** Draw a scalable nine-slice UI element in world space 27 | * This function can apply color and additive color if WebGL is enabled 28 | * @param {Vector2} pos - World space position 29 | * @param {Vector2} size - World space size 30 | * @param {TileInfo} startTile - Starting tile for the nine-slice pattern 31 | * @param {Color} [color] - Color to modulate with 32 | * @param {number} [borderSize] - Width of the border sections 33 | * @param {Color} [additiveColor] - Additive color 34 | * @param {number} [extraSpace] - Extra spacing adjustment 35 | * @param {number} [angle] - Angle to rotate by 36 | * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering 37 | * @param {boolean} [screenSpace] - Use screen space coordinates 38 | * @param {CanvasRenderingContext2D} [context] - Canvas context to use 39 | * @memberof DrawUtilities */ 40 | function drawNineSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context) 41 | { 42 | // setup nine slice tiles 43 | const centerTile = startTile.offset(startTile.size); 44 | const centerSize = size.add(vec2(extraSpace-borderSize*2)); 45 | const cornerSize = vec2(borderSize); 46 | const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5)); 47 | const flip = screenSpace ? -1 : 1; 48 | const rotateAngle = screenSpace ? -angle : angle; 49 | 50 | // center 51 | drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context); 52 | for (let i=4; i--;) 53 | { 54 | // sides 55 | const horizontal = i%2; 56 | const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-1:1)); 57 | const sideSize = vec2(horizontal ? borderSize : centerSize.x, horizontal ? centerSize.y : borderSize); 58 | const sideTile = centerTile.offset(startTile.size.multiply(vec2(i===1?1:i===3?-1:0,i===0?-flip:i===2?flip:0))) 59 | drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, angle, false, additiveColor, useWebGL, screenSpace, context); 60 | } 61 | for (let i=4; i--;) 62 | { 63 | // corners 64 | const flipX = i>1; 65 | const flipY = i && i<3; 66 | const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-1:1)); 67 | const cornerTile = centerTile.offset(startTile.size.multiply(vec2(flipX?-1:1,flipY?flip:-flip))); 68 | drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context); 69 | } 70 | } 71 | 72 | /** Draw a scalable three-slice UI element to the main canvas in screen space 73 | * This function can not apply color because it draws using the 2d context 74 | * @param {Vector2} pos - Screen space position 75 | * @param {Vector2} size - Screen space size 76 | * @param {TileInfo} startTile - Starting tile for the three-slice pattern 77 | * @param {number} [borderSize] - Width of the border sections 78 | * @param {number} [extraSpace] - Extra spacing adjustment 79 | * @param {number} [angle] - Angle to rotate by 80 | * @memberof DrawUtilities */ 81 | function drawThreeSliceScreen(pos, size, startTile, borderSize=32, extraSpace=2, angle=0) 82 | { 83 | drawThreeSlice(pos, size, startTile, WHITE, borderSize, BLACK, extraSpace, angle, false, true); 84 | } 85 | 86 | /** Draw a scalable three-slice UI element in world space 87 | * This function can apply color and additive color if WebGL is enabled 88 | * @param {Vector2} pos - World space position 89 | * @param {Vector2} size - World space size 90 | * @param {TileInfo} startTile - Starting tile for the three-slice pattern 91 | * @param {Color} [color] - Color to modulate with 92 | * @param {number} [borderSize] - Width of the border sections 93 | * @param {Color} [additiveColor] - Additive color 94 | * @param {number} [extraSpace] - Extra spacing adjustment 95 | * @param {number} [angle] - Angle to rotate by 96 | * @param {boolean} [useWebGL=glEnable] - Use WebGL for rendering 97 | * @param {boolean} [screenSpace] - Use screen space coordinates 98 | * @param {CanvasRenderingContext2D} [context] - Canvas context to use 99 | * @memberof DrawUtilities */ 100 | function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor, extraSpace=.05, angle=0, useWebGL=glEnable, screenSpace, context) 101 | { 102 | // setup three slice tiles 103 | const cornerTile = startTile.frame(0); 104 | const sideTile = startTile.frame(1); 105 | const centerTile = startTile.frame(2); 106 | const centerSize = size.add(vec2(extraSpace-borderSize*2)); 107 | const cornerSize = vec2(borderSize); 108 | const cornerOffset = size.scale(.5).subtract(cornerSize.scale(.5)); 109 | const flip = screenSpace ? -1 : 1; 110 | const rotateAngle = screenSpace ? -angle : angle; 111 | 112 | // center 113 | drawTile(pos, centerSize, centerTile, color, angle, false, additiveColor, useWebGL, screenSpace, context); 114 | for (let i=4; i--;) 115 | { 116 | // sides 117 | const a = angle + i*PI/2; 118 | const horizontal = i%2; 119 | const sidePos = cornerOffset.multiply(vec2(horizontal?i===1?1:-1:0, horizontal?0:i?-flip:flip)); 120 | const sideSize = vec2(horizontal ? centerSize.y : centerSize.x, borderSize); 121 | drawTile(pos.add(sidePos.rotate(rotateAngle)), sideSize, sideTile, color, a, false, additiveColor, useWebGL, screenSpace, context); 122 | } 123 | for (let i=4; i--;) 124 | { 125 | // corners 126 | const a = angle + i*PI/2; 127 | const flipX = !i || i>2; 128 | const flipY = i>1; 129 | const cornerPos = cornerOffset.multiply(vec2(flipX?-1:1, flipY?-flip:flip)); 130 | drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context); 131 | } 132 | } -------------------------------------------------------------------------------- /docs/styles/clean-jsdoc-theme-dark.css: -------------------------------------------------------------------------------- 1 | ::selection { 2 | background: #ffce76; 3 | color: #222; 4 | } 5 | 6 | body { 7 | background-color: #1a1a1a; 8 | color: #fff; 9 | } 10 | 11 | a, 12 | a:active { 13 | color: #0bf; 14 | } 15 | 16 | hr { 17 | color: #222; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | color: #fff; 27 | } 28 | 29 | .sidebar { 30 | background-color: #222; 31 | color: #999; 32 | } 33 | 34 | .sidebar-title { 35 | color: #999; 36 | } 37 | 38 | .sidebar-section-title { 39 | color: #999; 40 | } 41 | 42 | .sidebar-section-title:hover { 43 | background: #252525; 44 | } 45 | 46 | 47 | 48 | .with-arrow { 49 | fill: #999; 50 | } 51 | 52 | .sidebar-section-children-container { 53 | background: #292929; 54 | } 55 | 56 | .sidebar-section-children.active { 57 | background: #444; 58 | } 59 | 60 | .sidebar-section-children a:hover { 61 | background: #2c2c2c; 62 | } 63 | 64 | .sidebar-section-children a { 65 | color: #fff; 66 | } 67 | 68 | .navbar-container { 69 | background: #1a1a1a; 70 | } 71 | 72 | .icon-button svg, 73 | .navbar-item a { 74 | color: #999; 75 | fill: #999; 76 | } 77 | 78 | .font-size-tooltip .icon-button svg { 79 | fill: #fff; 80 | } 81 | 82 | .font-size-tooltip .icon-button.disabled { 83 | background: #999; 84 | } 85 | 86 | .icon-button:hover { 87 | background: #333; 88 | } 89 | 90 | .icon-button:active { 91 | background: #444; 92 | } 93 | 94 | .navbar-item a:active { 95 | background-color: #222; 96 | color: #aaa; 97 | } 98 | 99 | .navbar-item:hover { 100 | background: #202020; 101 | } 102 | 103 | .footer { 104 | background: #222; 105 | color: #999; 106 | } 107 | 108 | .footer a { 109 | color: #999; 110 | } 111 | 112 | .toc-link { 113 | color: #777; 114 | font-size: 0.875rem; 115 | transition: color 0.3s; 116 | } 117 | 118 | .toc-link.is-active-link { 119 | color: #fff; 120 | } 121 | 122 | .has-anchor .link-anchor { 123 | color: #555; 124 | } 125 | 126 | .has-anchor .link-anchor:hover { 127 | color: #888; 128 | } 129 | 130 | tt, 131 | code, 132 | kbd, 133 | samp { 134 | background: #333; 135 | } 136 | 137 | .signature-attributes { 138 | color: #aaa; 139 | } 140 | 141 | .ancestors { 142 | color: #999; 143 | } 144 | 145 | .ancestors a { 146 | color: #999 !important; 147 | } 148 | 149 | .important { 150 | color: #c51313; 151 | } 152 | 153 | .type-signature { 154 | color: #00918e; 155 | } 156 | 157 | .name, 158 | .name a { 159 | color: #f7f7f7; 160 | } 161 | 162 | .details { 163 | background: #222; 164 | color: #fff; 165 | } 166 | 167 | .prettyprint { 168 | background: #222; 169 | } 170 | 171 | .member-item-container strong, 172 | .method-member-container strong { 173 | color: #fff; 174 | } 175 | 176 | .pre-top-bar-container { 177 | background: #292929; 178 | } 179 | 180 | .prettyprint.source, 181 | .prettyprint code { 182 | background-color: #222; 183 | color: #c9d1d9; 184 | } 185 | 186 | .pre-div { 187 | background-color: #222; 188 | } 189 | 190 | .hljs .hljs-ln-numbers { 191 | color: #777; 192 | } 193 | 194 | .hljs .selected { 195 | background: #444; 196 | } 197 | 198 | .hljs .selected .hljs-ln-numbers { 199 | color: #eee; 200 | } 201 | 202 | 203 | table .name, 204 | .params .name, 205 | .props .name, 206 | .name code { 207 | color: #fff; 208 | } 209 | 210 | table td, 211 | .params td { 212 | background-color: #292929; 213 | } 214 | 215 | table thead th, 216 | .params thead th, 217 | .props thead th { 218 | background-color: #222; 219 | color: #fff; 220 | } 221 | 222 | /* stylelint-disable */ 223 | table .params thead tr, 224 | .params .params thead tr, 225 | .props .props thead tr { 226 | background-color: #222; 227 | color: #fff; 228 | } 229 | 230 | .disabled { 231 | color: #aaaaaa; 232 | } 233 | 234 | .code-lang-name { 235 | color: #ff8a00; 236 | } 237 | 238 | .tooltip { 239 | background: #ffce76; 240 | color: #222; 241 | } 242 | 243 | /* code */ 244 | .hljs-comment { 245 | color: #8b949e; 246 | } 247 | 248 | .hljs-doctag, 249 | .hljs-keyword, 250 | .hljs-template-tag, 251 | .hljs-variable.language_ { 252 | color: #ff7b72; 253 | } 254 | 255 | .hljs-template-variable, 256 | .hljs-type { 257 | color: #30ac7c; 258 | } 259 | 260 | .hljs-meta, 261 | .hljs-string, 262 | .hljs-regexp { 263 | color: #a5d6ff; 264 | } 265 | 266 | .hljs-title.class_, 267 | .hljs-title { 268 | color: #ffa657; 269 | } 270 | 271 | .hljs-title.class_.inherited__, 272 | .hljs-title.function_ { 273 | color: #d2a8ff; 274 | } 275 | 276 | .hljs-attr, 277 | .hljs-attribute, 278 | .hljs-literal, 279 | .hljs-meta, 280 | .hljs-number, 281 | .hljs-operator, 282 | .hljs-selector-attr, 283 | .hljs-selector-class, 284 | .hljs-selector-id, 285 | .hljs-variable { 286 | color: #79c0ff; 287 | } 288 | 289 | .hljs-meta .hljs-string, 290 | .hljs-regexp, 291 | .hljs-string { 292 | color: #a5d6ff; 293 | } 294 | 295 | .hljs-built_in, 296 | .hljs-symbol { 297 | color: #ffa657; 298 | } 299 | 300 | .hljs-code, 301 | .hljs-comment, 302 | .hljs-formula { 303 | color: #8b949e; 304 | } 305 | 306 | .hljs-name, 307 | .hljs-quote, 308 | .hljs-selector-pseudo, 309 | .hljs-selector-tag { 310 | color: #7ee787; 311 | } 312 | 313 | .hljs-subst { 314 | color: #c9d1d9; 315 | } 316 | 317 | .hljs-section { 318 | color: #1f6feb; 319 | font-weight: 700; 320 | } 321 | 322 | .hljs-bullet { 323 | color: #f2cc60; 324 | } 325 | 326 | .hljs-emphasis { 327 | color: #c9d1d9; 328 | font-style: italic; 329 | } 330 | 331 | .hljs-strong { 332 | color: #c9d1d9; 333 | font-weight: 700; 334 | } 335 | 336 | /* code end*/ 337 | 338 | blockquote { 339 | background: #222; 340 | color: #fff; 341 | } 342 | 343 | .search-container { 344 | background: rgba(255, 255, 255, 0.1); 345 | } 346 | 347 | .icon-button.search-close-button svg { 348 | fill: #a00; 349 | } 350 | 351 | .search-container .wrapper { 352 | background: #222; 353 | } 354 | 355 | .search-result-c { 356 | color: #666; 357 | } 358 | 359 | .search-box-c { 360 | fill: #333; 361 | } 362 | 363 | .search-input { 364 | background: #333; 365 | color: #fff; 366 | } 367 | 368 | .search-box-c svg { 369 | fill: #fff; 370 | } 371 | 372 | .search-result-item { 373 | background: #333; 374 | } 375 | 376 | .search-result-item:hover { 377 | background: #444; 378 | } 379 | 380 | .search-result-item:active { 381 | background: #555; 382 | } 383 | 384 | .search-result-item-title { 385 | color: #fff; 386 | } 387 | 388 | .search-result-item-p { 389 | color: #aaa; 390 | } 391 | 392 | .mobile-menu-icon-container .icon-button { 393 | background: #333; 394 | } 395 | 396 | .mobile-sidebar-container { 397 | background: #1a1a1a; 398 | } 399 | 400 | .mobile-sidebar-wrapper { 401 | background: #222; 402 | } 403 | 404 | 405 | .child-tutorial { 406 | border-color: #555; 407 | color: #f3f3f3; 408 | } 409 | 410 | .child-tutorial:hover { 411 | background: #222; 412 | } 413 | -------------------------------------------------------------------------------- /src/engineMedals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LittleJS Medal System 3 | * - Achievement/trophy system for games 4 | * - Medal class with name, description, icon, and unlock tracking 5 | * - Automatic saving to local storage 6 | * - Visual display queue with slide-in notifications 7 | * - Newgrounds API integration for online achievements 8 | * - Debug mode to unlock/reset medals during development 9 | * @namespace Medals 10 | */ 11 | 12 | 'use strict'; 13 | 14 | /** List of all medals 15 | * @type {Object} 16 | * @memberof Medals */ 17 | const medals = {}; 18 | 19 | // Engine internal variables not exposed to documentation 20 | let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast; 21 | 22 | /////////////////////////////////////////////////////////////////////////////// 23 | 24 | /** Initialize medals with a save name used for storage 25 | * - Call this after creating all medals 26 | * - Checks if medals are unlocked 27 | * @param {string} saveName 28 | * @memberof Medals */ 29 | function medalsInit(saveName) 30 | { 31 | // check if medals are unlocked 32 | medalsSaveName = saveName; 33 | if (!debugMedals) 34 | medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]); 35 | 36 | // engine automatically renders medals 37 | engineAddPlugin(undefined, medalsRender); 38 | 39 | // plugin functions 40 | function medalsRender() 41 | { 42 | if (!medalsDisplayQueue.length) return; 43 | 44 | // update first medal in queue 45 | const medal = medalsDisplayQueue[0]; 46 | const time = timeReal - medalsDisplayTimeLast; 47 | if (!medalsDisplayTimeLast) 48 | medalsDisplayTimeLast = timeReal; 49 | else if (time > medalDisplayTime) 50 | { 51 | medalsDisplayTimeLast = 0; 52 | medalsDisplayQueue.shift(); 53 | } 54 | else 55 | { 56 | // slide on/off medals 57 | const slideOffTime = medalDisplayTime - medalDisplaySlideTime; 58 | const hidePercent = 59 | time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime : 60 | time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0; 61 | medal.render(hidePercent); 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * @callback MedalCallbackFunction - Function that processes a medal 68 | * @param {Medal} medal 69 | * @memberof Medals 70 | */ 71 | 72 | /** Calls a function for each medal 73 | * @param {MedalCallbackFunction} callback 74 | * @memberof Medals */ 75 | function medalsForEach(callback) 76 | { Object.values(medals).forEach(medal=>callback(medal)); } 77 | 78 | /////////////////////////////////////////////////////////////////////////////// 79 | 80 | /** 81 | * Medal - Tracks an unlockable medal 82 | * @memberof Medals 83 | * @example 84 | * // create a medal 85 | * const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️'); 86 | * 87 | * // initialize medals 88 | * medalsInit('Example Game'); 89 | * 90 | * // unlock the medal 91 | * medal_example.unlock(); 92 | */ 93 | class Medal 94 | { 95 | /** Create a medal object and adds it to the list of medals 96 | * @param {number} id - The unique identifier of the medal 97 | * @param {string} name - Name of the medal 98 | * @param {string} [description] - Description of the medal 99 | * @param {string} [icon] - Icon for the medal 100 | * @param {string} [src] - Image location for the medal 101 | */ 102 | constructor(id, name, description='', icon='🏆', src) 103 | { 104 | ASSERT(id >= 0 && !medals[id]); 105 | 106 | /** @property {number} - The unique identifier of the medal */ 107 | this.id = id; 108 | 109 | /** @property {string} - Name of the medal */ 110 | this.name = name; 111 | 112 | /** @property {string} - Description of the medal */ 113 | this.description = description; 114 | 115 | /** @property {string} - Icon for the medal */ 116 | this.icon = icon; 117 | 118 | /** @property {boolean} - Is the medal unlocked? */ 119 | this.unlocked = false; 120 | 121 | // load the source image if provided 122 | if (src) 123 | (this.image = new Image).src = src; 124 | 125 | // add this to list of medals 126 | medals[id] = this; 127 | } 128 | 129 | /** Unlocks a medal if not already unlocked */ 130 | unlock() 131 | { 132 | if (medalsPreventUnlock || this.unlocked) return; 133 | 134 | // save the medal 135 | ASSERT(medalsSaveName, 'save name must be set'); 136 | localStorage[this.storageKey()] = this.unlocked = true; 137 | medalsDisplayQueue.push(this); 138 | } 139 | 140 | /** Render a medal 141 | * @param {number} [hidePercent] - How much to slide the medal off screen 142 | */ 143 | render(hidePercent=0) 144 | { 145 | const context = mainContext; 146 | const width = min(medalDisplaySize.x, mainCanvas.width); 147 | const height = medalDisplaySize.y; 148 | const x = mainCanvas.width - width; 149 | const y = -height*hidePercent; 150 | const backgroundColor = hsl(0,0,.9); 151 | 152 | // draw containing rect and clip to that region 153 | context.save(); 154 | context.beginPath(); 155 | context.fillStyle = backgroundColor.toString(); 156 | context.strokeStyle = BLACK.toString(); 157 | context.lineWidth = 3; 158 | context.rect(x, y, width, height); 159 | context.fill(); 160 | context.stroke(); 161 | context.clip(); 162 | 163 | // draw the icon 164 | const gap = vec2(.1, .05).scale(height); 165 | const medalDisplayIconSize = height - 2*gap.x; 166 | this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize); 167 | 168 | // draw the name 169 | const nameSize = height*.5; 170 | const descriptionSize = height*.3; 171 | const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2); 172 | const textWidth = width - medalDisplayIconSize - 3*gap.x; 173 | drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth); 174 | 175 | // draw the description 176 | pos.y = y + height - gap.y*2 - descriptionSize/2; 177 | drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth); 178 | context.restore(); 179 | } 180 | 181 | /** Render the icon for a medal 182 | * @param {Vector2} pos - Screen space position 183 | * @param {number} size - Screen space size 184 | */ 185 | renderIcon(pos, size) 186 | { 187 | // draw the image or icon 188 | if (this.image) 189 | mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size); 190 | else 191 | drawTextScreen(this.icon, pos, size*.7, BLACK); 192 | } 193 | 194 | // Get local storage key used by the medal 195 | storageKey() { return medalsSaveName + '_' + this.id; } 196 | } --------------------------------------------------------------------------------