littlejsengine 1.1.26 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/engine/engine.js CHANGED
@@ -22,7 +22,7 @@
22
22
  const engineName = 'LittleJS';
23
23
 
24
24
  /** Version of engine */
25
- const engineVersion = '1.1.26';
25
+ const engineVersion = '1.2.0';
26
26
 
27
27
  /** Frames per second to update objects
28
28
  * @default */
@@ -59,7 +59,7 @@ let averageFPS, drawCount;
59
59
  // css text used for elements created by engine
60
60
  const styleBody = 'margin:0;overflow:hidden;background:#000' +
61
61
  ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
62
- const canvaStyle = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
62
+ const styleCanvas = 'position:absolute';
63
63
 
64
64
  ///////////////////////////////////////////////////////////////////////////////
65
65
 
@@ -84,7 +84,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
84
84
  document.body.style = styleBody;
85
85
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
86
86
  mainContext = mainCanvas.getContext('2d');
87
- mainCanvas.style = canvaStyle;
87
+ mainCanvas.style = styleCanvas;
88
88
 
89
89
  // init stuff and start engine
90
90
  debugInit();
@@ -93,7 +93,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
93
93
  // create overlay canvas for hud to appear above gl canvas
94
94
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
95
95
  overlayContext = overlayCanvas.getContext('2d');
96
- overlayCanvas.style = canvaStyle;
96
+ overlayCanvas.style = styleCanvas;
97
97
 
98
98
  gameInit();
99
99
  touchGamepadCreate();
@@ -109,7 +109,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
109
109
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
110
110
  frameTimeLastMS = frameTimeMS;
111
111
  if (debug || showWatermark)
112
- averageFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), averageFPS || 0);
112
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
113
113
  if (debug)
114
114
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
115
115
  timeReal += frameTimeDeltaMS / 1e3;
@@ -160,13 +160,24 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
160
160
 
161
161
  // fit to window by adding space on top or bottom if necessary
162
162
  const aspect = innerWidth / innerHeight;
163
- const fixedAspect = mainCanvas.width / mainCanvas.height;
164
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
165
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
166
- if (glCanvas)
163
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
164
+
165
+ // clear css
166
+ mainCanvas.style.width = overlayCanvas.style.width =
167
+ mainCanvas.style.height = overlayCanvas.style.height =
168
+ mainCanvas.style.top = overlayCanvas.style.top =
169
+ mainCanvas.style.left = overlayCanvas.style.left = '';
170
+
171
+ // letterbox center canvas
172
+ if (aspect < fixedAspect)
173
+ {
174
+ mainCanvas.style.width = overlayCanvas.style.width = innerWidth;
175
+ mainCanvas.style.top = overlayCanvas.style.top = (innerHeight - innerWidth/fixedAspect)/2;
176
+ }
177
+ else
167
178
  {
168
- glCanvas.style.width = mainCanvas.style.width;
169
- glCanvas.style.height = mainCanvas.style.height;
179
+ mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
180
+ mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
170
181
  }
171
182
  }
172
183
  else
@@ -174,6 +185,18 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
174
185
  // clear and set size to same as window
175
186
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
176
187
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
188
+
189
+ // center canvas
190
+ overlayCanvas.style.top = mainCanvas.style.top = (innerHeight - mainCanvas.height)/2;
191
+ overlayCanvas.style.left = mainCanvas.style.left = (innerWidth - mainCanvas.width)/2;
192
+ }
193
+
194
+ if (glEnable)
195
+ {
196
+ glCanvas.style.width = mainCanvas.style.width;
197
+ glCanvas.style.height = mainCanvas.style.height;
198
+ glCanvas.style.top = mainCanvas.style.top;
199
+ glCanvas.style.left = mainCanvas.style.left;
177
200
  }
178
201
 
179
202
  // render sort then render while removing destroyed objects
@@ -69,7 +69,7 @@ class Sound
69
69
  return; // out of range
70
70
 
71
71
  // attenuate volume by distance
72
- volume *= percent(lengthSquared**.5, range*this.taper, range);
72
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
73
73
  }
74
74
 
75
75
  // get pan from screen space coords
@@ -251,7 +251,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
251
251
  // connect source to gain
252
252
  (
253
253
  window.StereoPannerNode ? // create pan node if possible
254
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
254
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
255
255
  : source
256
256
  )
257
257
  .connect(gainNode);
@@ -87,7 +87,15 @@ if %ERRORLEVEL% NEQ 0 (
87
87
  )
88
88
 
89
89
  rem lightly minify with uglify
90
- call uglifyjs -o %OUTPUT_FILENAME% -- %OUTPUT_FILENAME%
90
+ call uglifyjs -o %OUTPUT_FILENAME% -- %OUTPUT_FILENAME%
91
+ if %ERRORLEVEL% NEQ 0 (
92
+ pause
93
+ exit /b %ERRORLEVEL%
94
+ )
95
+
96
+ rem --- BUILD TYPESCRIPT DEFINITIONS ---
97
+
98
+ call tsc engine.all.js --declaration --allowJs --emitDeclarationOnly --outFile index.d.ts
91
99
  if %ERRORLEVEL% NEQ 0 (
92
100
  pause
93
101
  exit /b %ERRORLEVEL%
@@ -3,4 +3,5 @@ rem Install these tools to build LittleJS
3
3
  call npm install -g google-closure-compiler
4
4
  call npm install -g uglify-js
5
5
  call npm install -g roadroller
6
- call npm install -g ect-bin
6
+ call npm install -g ect-bin
7
+ call npm install -g typescript
@@ -7,6 +7,7 @@
7
7
  * LittleJS Debug System
8
8
  * <br> - Press ~ to show debug overlay with mouse pick
9
9
  * <br> - Number keys toggle debug functions
10
+ * <br> - +/- apply time scale
10
11
  * <br> - Debug primitive rendering
11
12
  * <br> - Save a 2d canvas as an image
12
13
  * @namespace Debug
@@ -164,22 +165,22 @@ const debugUpdate = ()=>
164
165
  debugOverlay = !debugOverlay;
165
166
  if (debugOverlay)
166
167
  {
168
+ if (keyWasPressed(48)) // 0
169
+ showWatermark = !showWatermark;
167
170
  if (keyWasPressed(49)) // 1
168
171
  debugPhysics = !debugPhysics, debugParticles = 0;
169
172
  if (keyWasPressed(50)) // 2
170
173
  debugParticles = !debugParticles, debugPhysics = 0;
171
174
  if (keyWasPressed(51)) // 3
175
+ debugGamepads = !debugGamepads;
176
+ if (keyWasPressed(52)) // 4
172
177
  godMode = !godMode;
173
178
  if (keyWasPressed(53)) // 5
174
179
  debugTakeScreenshot = 1;
175
180
  //if (keyWasPressed(54)) // 6
176
- // debugToggleParticleEditor();
177
- if (keyWasPressed(55)) // 7
178
- debugGamepads = !debugGamepads;
181
+ //if (keyWasPressed(55)) // 7
179
182
  //if (keyWasPressed(56)) // 8
180
183
  //if (keyWasPressed(57)) // 9
181
- if (keyWasPressed(48)) // 0
182
- showWatermark = !showWatermark;
183
184
  }
184
185
  }
185
186
 
@@ -200,21 +201,20 @@ const debugRender = ()=>
200
201
 
201
202
  if (debugGamepads && gamepadsEnable && navigator.getGamepads)
202
203
  {
203
- // poll gamepads
204
+ // gamepad debug display
204
205
  const gamepads = navigator.getGamepads();
205
206
  for (let i = gamepads.length; i--;)
206
207
  {
207
208
  const gamepad = gamepads[i];
208
209
  if (gamepad)
209
210
  {
210
- // gamepad debug display
211
211
  const stickScale = 1;
212
212
  const buttonScale = .2;
213
213
  const centerPos = cameraPos;
214
214
  const sticks = stickData[i];
215
215
  for (let j = sticks.length; j--;)
216
216
  {
217
- const drawPos = centerPos.add(vec2(j*stickScale*2,i*stickScale*3));
217
+ const drawPos = centerPos.add(vec2(j*stickScale*2, i*stickScale*3));
218
218
  const stickPos = drawPos.add(sticks[j].scale(stickScale));
219
219
  debugCircle(drawPos, stickScale, '#fff7',0,1);
220
220
  debugLine(drawPos, stickPos, '#f00');
@@ -251,16 +251,14 @@ const debugRender = ()=>
251
251
  bestObject = o
252
252
  }
253
253
 
254
- size.x = max(size.x, .2);
255
- size.y = max(size.y, .2);
256
-
254
+ // show object info
257
255
  const color = new Color(
258
256
  o.collideTiles?1:0,
259
257
  o.collideSolidObjects?1:0,
260
258
  o.isSolid?1:0,
261
259
  o.parent ? .2 : .5);
262
-
263
- // show object info
260
+ size.x = max(size.x, .2);
261
+ size.y = max(size.y, .2);
264
262
  drawRect(o.pos, size, color);
265
263
  drawRect(o.pos, size.scale(.8), o.parent ? new Color(1,1,1,.5) : new Color(0,0,0,.8));
266
264
  o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0,0,1,.5));
@@ -293,16 +291,15 @@ const debugRender = ()=>
293
291
  }
294
292
 
295
293
  {
296
- // render debug rects
297
- overlayContext.lineWidth = 1;
294
+ // draw debug primitives
295
+ overlayContext.save();
296
+ overlayContext.lineWidth = 2;
298
297
  const pointSize = debugPointSize * cameraScale;
299
298
  debugPrimitives.forEach(p=>
300
299
  {
301
300
  // create canvas transform from world space to screen space
302
301
  const pos = worldToScreen(p.pos);
303
302
 
304
- overlayContext.save();
305
- overlayContext.lineWidth = 2;
306
303
  overlayContext.translate(pos.x|0, pos.y|0);
307
304
  overlayContext.rotate(p.angle);
308
305
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
@@ -335,15 +332,17 @@ const debugRender = ()=>
335
332
  p.fill && overlayContext.fill();
336
333
  overlayContext.stroke();
337
334
  }
338
-
339
- overlayContext.restore();
340
335
  });
341
336
 
342
- overlayContext.fillStyle = overlayContext.strokeStyle = '#fff';
337
+ // remove expired pritives
338
+ debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
339
+
340
+ overlayContext.restore();
343
341
  }
344
342
 
345
343
  {
346
- let x = 9, y = -20, h = 30;
344
+ // draw debug overlay
345
+ overlayContext.save();
347
346
  overlayContext.fillStyle = '#fff';
348
347
  overlayContext.textAlign = 'left';
349
348
  overlayContext.textBaseline = 'top';
@@ -351,6 +350,7 @@ const debugRender = ()=>
351
350
  overlayContext.shadowColor = '#000';
352
351
  overlayContext.shadowBlur = 9;
353
352
 
353
+ let x = 9, y = -20, h = 30;
354
354
  if (debugOverlay)
355
355
  {
356
356
  overlayContext.fillText(engineName, x, y += h);
@@ -363,14 +363,12 @@ const debugRender = ()=>
363
363
  overlayContext.fillText('1: Debug Physics', x, y += h);
364
364
  overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
365
365
  overlayContext.fillText('2: Debug Particles', x, y += h);
366
+ overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
367
+ overlayContext.fillText('3: Debug Gamepads', x, y += h);
366
368
  overlayContext.fillStyle = godMode ? '#f00' : '#fff';
367
- overlayContext.fillText('3: God Mode', x, y += h);
369
+ overlayContext.fillText('4: God Mode', x, y += h);
368
370
  overlayContext.fillStyle = '#fff';
369
371
  overlayContext.fillText('5: Save Screenshot', x, y += h);
370
- //overlayContext.fillStyle = debugParticleEditor ? '#f00' : '#fff';
371
- //overlayContext.fillText('6: Particle Editor', x, y += h);
372
- overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
373
- overlayContext.fillText('7: Debug Gamepads', x, y += h);
374
372
 
375
373
  let keysPressed = '';
376
374
  for(const i in inputData[0])
@@ -388,7 +386,6 @@ const debugRender = ()=>
388
386
  buttonsPressed += i + ' ' ;
389
387
  }
390
388
  buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
391
-
392
389
  }
393
390
  else
394
391
  {
@@ -398,197 +395,6 @@ const debugRender = ()=>
398
395
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
399
396
  }
400
397
 
401
- overlayContext.shadowBlur = 0;
402
- }
403
-
404
- debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
405
- }
406
-
407
- ///////////////////////////////////////////////////////////////////////////////
408
- // particle system editor (work in progress)
409
- let debugParticleEditor = 0, debugParticleSystem, debugParticleSystemDiv, particleSystemCode;
410
-
411
- const debugToggleParticleEditor = ()=>
412
- {
413
- debugParticleEditor = !debugParticleEditor;
414
-
415
- if (debugParticleEditor)
416
- {
417
- if (!debugParticleSystem || debugParticleSystem.destroyed)
418
- debugParticleSystem = new ParticleEmitter(cameraPos);
419
- }
420
- else if (debugParticleSystem && !debugParticleSystem.destroyed)
421
- debugParticleSystem.destroy();
422
-
423
-
424
- const colorToHex = (color)=>
425
- {
426
- const componentToHex = (c)=>
427
- {
428
- const hex = (c*255|0).toString(16);
429
- return hex.length == 1 ? '0' + hex : hex;
430
- }
431
-
432
- return '#' + componentToHex(color.r) + componentToHex(color.g) + componentToHex(color.b);
398
+ overlayContext.restore();
433
399
  }
434
- const hexToColor = (hex)=>
435
- {
436
- return new Color(
437
- parseInt(hex.substr(1,2), 16)/255,
438
- parseInt(hex.substr(3,2), 16)/255,
439
- parseInt(hex.substr(5,2), 16)/255)
440
- }
441
-
442
- if (!debugParticleSystemDiv)
443
- {
444
- const div = debugParticleSystemDiv = document.createElement('div');
445
- div.innerHTML = '<big><b>Particle Editor';
446
- div.style = 'position:absolute;top:10;left:10;color:#fff';
447
- document.body.appendChild(div);
448
-
449
- for ( const setting of debugParticleSettings)
450
- {
451
- const input = setting[2] = document.createElement('input');
452
- const name = setting[0];
453
- const type = setting[1];
454
- if (type)
455
- {
456
- if (type == 'color')
457
- {
458
- input.type = type;
459
- const color = debugParticleSystem[name];
460
- input.value = colorToHex(color);
461
- }
462
- else if (type == 'alpha' && name == 'colorStartAlpha')
463
- input.value = debugParticleSystem.colorStartA.a;
464
- else if (type == 'alpha' && name == 'colorEndAlpha')
465
- input.value = debugParticleSystem.colorEndA.a;
466
- else if (name == 'tileSizeX')
467
- input.value = debugParticleSystem.tileSize.x;
468
- else if (name == 'tileSizeY')
469
- input.value = debugParticleSystem.tileSize.y;
470
- }
471
- else
472
- input.value = debugParticleSystem[name] || '0';
473
-
474
- input.oninput = (e)=>
475
- {
476
- const inputFloat = parseFloat(input.value) || 0;
477
- if (type)
478
- {
479
- if (type == 'color')
480
- {
481
- const color = hexToColor(input.value);
482
- debugParticleSystem[name].r = color.r;
483
- debugParticleSystem[name].g = color.g;
484
- debugParticleSystem[name].b = color.b;
485
- }
486
- else if (type == 'alpha' && name == 'colorStartAlpha')
487
- {
488
- debugParticleSystem.colorStartA.a = clamp(inputFloat);
489
- debugParticleSystem.colorStartB.a = clamp(inputFloat);
490
- }
491
- else if (type == 'alpha' && name == 'colorEndAlpha')
492
- {
493
- debugParticleSystem.colorEndA.a = clamp(inputFloat);
494
- debugParticleSystem.colorEndB.a = clamp(inputFloat);
495
- }
496
- else if (name == 'tileSizeX')
497
- {
498
- debugParticleSystem.tileSize = vec2(parseInt(input.value), debugParticleSystem.tileSize.y);
499
- }
500
- else if (name == 'tileSizeY')
501
- {
502
- debugParticleSystem.tileSize.y = vec2(debugParticleSystem.tileSize.x, parseInt(input.value));
503
- }
504
- }
505
- else
506
- debugParticleSystem[name] = inputFloat;
507
-
508
- updateCode();
509
- }
510
- div.appendChild(document.createElement('br'));
511
- div.appendChild(input);
512
- div.appendChild(document.createTextNode(' ' + name));
513
- }
514
-
515
- div.appendChild(document.createElement('br'));
516
- div.appendChild(document.createElement('br'));
517
- div.appendChild(particleSystemCode = document.createElement('input'));
518
- particleSystemCode.disabled = true;
519
- div.appendChild(document.createTextNode(' code'));
520
-
521
- div.appendChild(document.createElement('br'));
522
- const button = document.createElement('button')
523
- div.appendChild(button);
524
- button.innerHTML = 'Copy To Clipboard';
525
-
526
- button.onclick = (e)=> navigator.clipboard.writeText(particleSystemCode.value);
527
-
528
- const updateCode = ()=>
529
- {
530
- let code = '';
531
- let count = 0;
532
- for ( const setting of debugParticleSettings)
533
- {
534
- const name = setting[0];
535
- const type = setting[1];
536
- let value;
537
- if (name == 'tileSizeX' || type == 'alpha')
538
- continue;
539
-
540
- if (count++)
541
- code += ', ';
542
-
543
- if (name == 'tileSizeY')
544
- {
545
- value = `vec2(${debugParticleSystem.tileSize.x},${debugParticleSystem.tileSize.y})`;
546
- }
547
- else if (type == 'color')
548
- {
549
- const c = debugParticleSystem[name];
550
- value = `new Color(${c.r},${c.g},${c.b},${c.a})`;
551
- }
552
- else
553
- value = debugParticleSystem[name];
554
- code += value;
555
- }
556
-
557
- particleSystemCode.value = '...[' + code + ']';
558
- }
559
- updateCode();
560
- }
561
- debugParticleSystemDiv.style.display = debugParticleEditor ? '' : 'none'
562
- }
563
-
564
- const debugParticleSettings =
565
- [
566
- ['emitSize'],
567
- ['emitTime'],
568
- ['emitRate'],
569
- ['emitConeAngle'],
570
- ['tileIndex'],
571
- ['tileSizeX', 'tileSize'],
572
- ['tileSizeY', 'tileSize'],
573
- ['colorStartA', 'color'],
574
- ['colorStartB', 'color'],
575
- ['colorStartAlpha', 'alpha'],
576
- ['colorEndA', 'color'],
577
- ['colorEndB', 'color'],
578
- ['colorEndAlpha', 'alpha'],
579
- ['particleTime'],
580
- ['sizeStart'],
581
- ['sizeEnd'],
582
- ['speed'],
583
- ['angleSpeed'],
584
- ['damping'],
585
- ['angleDamping'],
586
- ['gravityScale'],
587
- ['particleConeAngle'],
588
- ['fadeRate'],
589
- ['randomness'],
590
- ['collideTiles'],
591
- ['additive'],
592
- ['randomColorComponents'],
593
- ['renderOrder'],
594
- ];
400
+ }
@@ -203,6 +203,18 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
203
203
  context.restore();
204
204
  }
205
205
 
206
+ /** Enable normal or additive blend mode
207
+ * @param {Boolean} [additive=0]
208
+ * @param {Boolean} [useWebGL=glEnable]
209
+ * @memberof Draw */
210
+ function setBlendMode(additive, useWebGL=glEnable)
211
+ {
212
+ if (glEnable && useWebGL)
213
+ glSetBlendMode(additive);
214
+ else
215
+ mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
216
+ }
217
+
206
218
  /** Draw text on overlay canvas in world space
207
219
  * @param {String} text
208
220
  * @param {Vector2} pos
@@ -228,16 +240,93 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
228
240
  overlayContext.fillText(text, pos.x, pos.y);
229
241
  }
230
242
 
231
- /** Enable normal or additive blend mode
232
- * @param {Boolean} [additive=0]
233
- * @param {Boolean} [useWebGL=glEnable]
234
- * @memberof Draw */
235
- function setBlendMode(additive, useWebGL=glEnable)
243
+ ///////////////////////////////////////////////////////////////////////////////
244
+
245
+ /**
246
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
247
+ * <br> - 96 characters (from space to tilde) are stored in an image
248
+ * <br> - Uses a default 8x8 font if none is supplied
249
+ * <br> - You can also use fonts from the main tile sheet
250
+ * @example
251
+ * // use built in font
252
+ * const font = new ImageFont;
253
+ *
254
+ * // draw text
255
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
256
+ */
257
+ class FontImage
236
258
  {
237
- if (glEnable && useWebGL)
238
- glSetBlendMode(additive);
239
- else
240
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
259
+ /** Create an image font
260
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
261
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
262
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
263
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
264
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
265
+ */
266
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
267
+ {
268
+ if (!image)
269
+ {
270
+ // use default font image
271
+ image = new Image();
272
+ image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6MCIBdFY+Q0PMNgf0yCgsSAaZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWw1hPWNRcEyWh2npdpBmSUBrRaN1WSlQAWXaWLF0kkwR7C/N7XUvIaQr4xpeqeQigjLwBRZ/6KkufAACcv5z7GYhvgWHII8xN/4C2hvLCkmZSVgIJMHNTxpYOsJOiitEjUQnIQAFFP+opoQfCbjWmfzWiNzCjEvxAPFARmw/Y5Te0sl2zEp4M+m7cToJjdJS7y1xcFlFT7EtWBZ8nl7uLSff7UgYewtlyY6yHM4RrjNrnsfn/W0DBK9xNZctZ7gsTeXTe5eqLqXFlKnAn+vkNXzH6TqPW10ksKoSlauKk1TcncZLSTxNnQ8Av/8RGPXOFIuMCZ/fpwIZx5vU4QIRQ44HwUyHx/QrZhm5VtXFex7ja2se0KBpnPT0DSwS4xd86uAy+SqnLBgkPY5JolhJ33ENyK6n0VyRzCnzebIkFdY/NUtM66lTdhix1KUss6q5snH5ftVJXG0QITb1gUDvzD3z/BbyOLMirwCtPzAAAAAElFTkSuQmCC';
273
+ }
274
+
275
+ this.image = image;
276
+ this.tileSize = tileSize;
277
+ this.paddingSize = paddingSize;
278
+ this.startTileIndex = startTileIndex;
279
+ this.context = context;
280
+ }
281
+
282
+ /** Draw text in screen space using the image font
283
+ * @param {String} text
284
+ * @param {Vector2} pos
285
+ * @param {Number} [scale=4]
286
+ * @param {Boolean} [center]
287
+ */
288
+ drawTextScreen(text, pos, scale=4, center)
289
+ {
290
+ const context = this.context;
291
+ context.save();
292
+ context.imageSmoothingEnabled = !cavasPixelated;
293
+
294
+ const size = this.tileSize;
295
+ const drawSize = size.add(this.paddingSize).scale(scale);
296
+ const cols = this.image.width / this.tileSize.x |0;
297
+ text.split('\n').forEach((line, i)=>
298
+ {
299
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
300
+ for(let j=line.length; j--;)
301
+ {
302
+ // draw each character
303
+ let charCode = line[j].charCodeAt();
304
+ if (charCode < 32 || charCode > 127)
305
+ charCode = 127; // unknown character
306
+
307
+ // get the character source location and draw it
308
+ const tile = this.startTileIndex + charCode - 32;
309
+ const x = tile % cols;
310
+ const y = tile / cols |0;
311
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
312
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
313
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
314
+ }
315
+ });
316
+
317
+ context.restore();
318
+ }
319
+
320
+ /** Draw text in world space using the image font
321
+ * @param {String} text
322
+ * @param {Vector2} pos
323
+ * @param {Number} [scale=.25]
324
+ * @param {Boolean} [center]
325
+ */
326
+ drawText(text, pos, scale=1, center)
327
+ {
328
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
329
+ }
241
330
  }
242
331
 
243
332
  ///////////////////////////////////////////////////////////////////////////////
@@ -159,7 +159,7 @@ const mouseToScreen = (mousePos)=>
159
159
 
160
160
  const rect = mainCanvas.getBoundingClientRect();
161
161
  return mainCanvasSize.multiply(
162
- vec2(percent(mousePos.x, rect.right, rect.left), percent(mousePos.y, rect.bottom, rect.top)));
162
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
163
163
  }
164
164
 
165
165
  ///////////////////////////////////////////////////////////////////////////////
@@ -200,8 +200,8 @@ function gamepadsUpdate()
200
200
  // read clamp dead zone of analog sticks
201
201
  const deadZone = .3, deadZoneMax = .8;
202
202
  const applyDeadZone = (v)=>
203
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
204
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
203
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
204
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
205
205
 
206
206
  // read analog sticks
207
207
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -281,7 +281,7 @@ if (isTouchDevice)
281
281
  // touch gamepad, virtual on screen gamepad emulator for touch devices
282
282
 
283
283
  // touch input internal variables
284
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
284
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
285
285
 
286
286
  // create the touch gamepad, called automatically by the engine
287
287
  function touchGamepadCreate()
@@ -327,7 +327,14 @@ function touchGamepadCreate()
327
327
  if (touchPos.distance(stickCenter) < touchGamepadSize)
328
328
  {
329
329
  // virtual analog stick
330
- touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
330
+ if (touchGamepadAnalog)
331
+ touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
332
+ else
333
+ {
334
+ // 8 way dpad
335
+ const angle = touchPos.subtract(stickCenter).angle();
336
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
337
+ }
331
338
  }
332
339
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
333
340
  {
@@ -351,7 +358,7 @@ function touchGamepadRender()
351
358
  return;
352
359
 
353
360
  // fade off when not touching or paused
354
- const alpha = percent(touchGamepadTimer.get(), 3, 4);
361
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
355
362
  if (!alpha || paused)
356
363
  return;
357
364