${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 | }
--------------------------------------------------------------------------------