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.
@@ -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,200 +395,9 @@ const debugRender = ()=>
398
395
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
399
396
  }
400
397
 
401
- overlayContext.shadowBlur = 0;
398
+ overlayContext.restore();
402
399
  }
403
-
404
- debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
405
400
  }
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);
433
- }
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
- ];
595
401
  /**
596
402
  * LittleJS Utility Classes and Functions
597
403
  * <br> - General purpose math library
@@ -643,27 +449,27 @@ const mod = (a, b=1)=> ((a % b) + b) % b;
643
449
 
644
450
  /** Clamps the value beween max and min
645
451
  * @param {Number} value
646
- * @param {Number} [max=1]
647
452
  * @param {Number} [min=0]
453
+ * @param {Number} [max=1]
648
454
  * @return {Number}
649
455
  * @memberof Utilities */
650
- const clamp = (v, max=1, min=0)=> (ASSERT(max > min), v < min ? min : v > max ? max : v);
456
+ const clamp = (v, min=0, max=1)=> v < min ? min : v > max ? max : v;
651
457
 
652
458
  /** Returns what percentage the value is between max and min
653
459
  * @param {Number} value
654
- * @param {Number} [max=1]
655
460
  * @param {Number} [min=0]
461
+ * @param {Number} [max=1]
656
462
  * @return {Number}
657
463
  * @memberof Utilities */
658
- const percent = (v, max=1, min=0)=> max-min ? clamp((v-min) / (max-min)) : 0;
464
+ const percent = (v, min=0, max=1)=> max-min ? clamp((v-min) / (max-min)) : 0;
659
465
 
660
466
  /** Linearly interpolates the percent value between max and min
661
467
  * @param {Number} percent
662
- * @param {Number} [max=1]
663
468
  * @param {Number} [min=0]
469
+ * @param {Number} [max=1]
664
470
  * @return {Number}
665
471
  * @memberof Utilities */
666
- const lerp = (p, max=1, min=0)=> min + clamp(p) * (max-min);
472
+ const lerp = (p, min=0, max=1)=> min + clamp(p) * (max-min);
667
473
 
668
474
  /** Applies smoothstep function to the percentage value
669
475
  * @param {Number} value
@@ -678,11 +484,11 @@ const smoothStep = (p)=> p * p * (3 - 2 * p);
678
484
  const nearestPowerOfTwo = (v)=> 2**Math.ceil(Math.log2(v));
679
485
 
680
486
  /** Returns true if two axis aligned bounding boxes are overlapping
681
- * @param {Vector2} pointA - Center of box A
682
- * @param {Vector2} sizeA - Size of box A
683
- * @param {Vector2} pointB - Center of box B
684
- * @param {Vector2} sizeB - Size of box B
685
- * @return {Boolean} - True if overlapping
487
+ * @param {Vector2} pointA - Center of box A
488
+ * @param {Vector2} sizeA - Size of box A
489
+ * @param {Vector2} pointB - Center of box B
490
+ * @param {Vector2} [sizeB] - Size of box B
491
+ * @return {Boolean} - True if overlapping
686
492
  * @memberof Utilities */
687
493
  const isOverlapping = (pA, sA, pB, sB)=> abs(pA.x - pB.x)*2 < sA.x + sB.x & abs(pA.y - pB.y)*2 < sA.y + sB.y;
688
494
 
@@ -1071,7 +877,7 @@ class Timer
1071
877
 
1072
878
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1073
879
  * @return {Number} */
1074
- getPercent() { return this.isSet()? percent(this.time - time, 0, this.setTime) : 0; }
880
+ getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; }
1075
881
  }
1076
882
  /**
1077
883
  * LittleJS Engine Settings
@@ -1319,7 +1125,7 @@ let medalDisplayIconSize = 50;
1319
1125
  class EngineObject
1320
1126
  {
1321
1127
  /** Create an engine object and adds it to the list of objects
1322
- * @param {Vector2} [position=new Vector2(0,0)] - World space position of the object
1128
+ * @param {Vector2} [position=new Vector2()] - World space position of the object
1323
1129
  * @param {Vector2} [size=objectDefaultSize] - World space size of the object
1324
1130
  * @param {Number} [tileIndex=-1] - Tile to use to render object, untextured if -1
1325
1131
  * @param {Vector2} [tileSize=tileSizeDefault] - Size of tile in source pixels
@@ -1364,12 +1170,15 @@ class EngineObject
1364
1170
  this.gravityScale = 1;
1365
1171
  /** @property {Number} [renderOrder=0] - Objects are sorted by render order */
1366
1172
  this.renderOrder = renderOrder;
1173
+ /** @property {Vector2} [velocity=new Vector2()] - Velocity of the object */
1174
+ this.velocity = new Vector2();
1175
+ /** @property {Number} [angleVelocity=0] - Angular velocity of the object */
1176
+ this.angleVelocity = 0;
1367
1177
 
1368
1178
  // init other internal object stuff
1369
1179
  this.spawnTime = time;
1370
- this.velocity = vec2(this.collideSolidObjects = this.angleVelocity = 0);
1371
- this.collideTiles = 1;
1372
1180
  this.children = [];
1181
+ this.collideTiles = 1;
1373
1182
 
1374
1183
  // add to list of objects
1375
1184
  engineObjects.push(this);
@@ -1388,8 +1197,8 @@ class EngineObject
1388
1197
  }
1389
1198
 
1390
1199
  // limit max speed to prevent missing collisions
1391
- this.velocity.x = clamp(this.velocity.x, objectMaxSpeed, -objectMaxSpeed);
1392
- this.velocity.y = clamp(this.velocity.y, objectMaxSpeed, -objectMaxSpeed);
1200
+ this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
1201
+ this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
1393
1202
 
1394
1203
  // apply physics
1395
1204
  const oldPos = this.pos.copy();
@@ -1479,8 +1288,8 @@ class EngineObject
1479
1288
 
1480
1289
  // lerp betwen elastic or inelastic based on elasticity
1481
1290
  const elasticity = max(this.elasticity, o.elasticity);
1482
- this.velocity.y = lerp(elasticity, elastic0, inelastic);
1483
- o.velocity.y = lerp(elasticity, elastic1, inelastic);
1291
+ this.velocity.y = lerp(elasticity, inelastic, elastic0);
1292
+ o.velocity.y = lerp(elasticity, inelastic, elastic1);
1484
1293
  }
1485
1294
  }
1486
1295
  if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
@@ -1500,8 +1309,8 @@ class EngineObject
1500
1309
 
1501
1310
  // lerp betwen elastic or inelastic based on elasticity
1502
1311
  const elasticity = max(this.elasticity, o.elasticity);
1503
- this.velocity.x = lerp(elasticity, elastic0, inelastic);
1504
- o.velocity.x = lerp(elasticity, elastic1, inelastic);
1312
+ this.velocity.x = lerp(elasticity, inelastic, elastic0);
1313
+ o.velocity.x = lerp(elasticity, inelastic, elastic1);
1505
1314
  }
1506
1315
  else // bounce if other object is fixed
1507
1316
  this.velocity.x *= -this.elasticity;
@@ -1531,7 +1340,7 @@ class EngineObject
1531
1340
 
1532
1341
  // adjust next velocity to settle on ground
1533
1342
  const o = (oldPos.y - this.size.y/2|0) - (oldPos.y - this.size.y/2);
1534
- if (o < 0 && o > -1 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1343
+ if (o < 0 && o > this.damping * this.velocity.y + gravity * this.gravityScale)
1535
1344
  this.velocity.y = this.damping ? (o - gravity * this.gravityScale) / this.damping : 0;
1536
1345
 
1537
1346
  // move to previous position
@@ -1842,6 +1651,18 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
1842
1651
  context.restore();
1843
1652
  }
1844
1653
 
1654
+ /** Enable normal or additive blend mode
1655
+ * @param {Boolean} [additive=0]
1656
+ * @param {Boolean} [useWebGL=glEnable]
1657
+ * @memberof Draw */
1658
+ function setBlendMode(additive, useWebGL=glEnable)
1659
+ {
1660
+ if (glEnable && useWebGL)
1661
+ glSetBlendMode(additive);
1662
+ else
1663
+ mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1664
+ }
1665
+
1845
1666
  /** Draw text on overlay canvas in world space
1846
1667
  * @param {String} text
1847
1668
  * @param {Vector2} pos
@@ -1867,16 +1688,93 @@ function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new
1867
1688
  overlayContext.fillText(text, pos.x, pos.y);
1868
1689
  }
1869
1690
 
1870
- /** Enable normal or additive blend mode
1871
- * @param {Boolean} [additive=0]
1872
- * @param {Boolean} [useWebGL=glEnable]
1873
- * @memberof Draw */
1874
- function setBlendMode(additive, useWebGL=glEnable)
1691
+ ///////////////////////////////////////////////////////////////////////////////
1692
+
1693
+ /**
1694
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
1695
+ * <br> - 96 characters (from space to tilde) are stored in an image
1696
+ * <br> - Uses a default 8x8 font if none is supplied
1697
+ * <br> - You can also use fonts from the main tile sheet
1698
+ * @example
1699
+ * // use built in font
1700
+ * const font = new ImageFont;
1701
+ *
1702
+ * // draw text
1703
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
1704
+ */
1705
+ class FontImage
1875
1706
  {
1876
- if (glEnable && useWebGL)
1877
- glSetBlendMode(additive);
1878
- else
1879
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1707
+ /** Create an image font
1708
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
1709
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
1710
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
1711
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
1712
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
1713
+ */
1714
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
1715
+ {
1716
+ if (!image)
1717
+ {
1718
+ // use default font image
1719
+ image = new Image();
1720
+ 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';
1721
+ }
1722
+
1723
+ this.image = image;
1724
+ this.tileSize = tileSize;
1725
+ this.paddingSize = paddingSize;
1726
+ this.startTileIndex = startTileIndex;
1727
+ this.context = context;
1728
+ }
1729
+
1730
+ /** Draw text in screen space using the image font
1731
+ * @param {String} text
1732
+ * @param {Vector2} pos
1733
+ * @param {Number} [scale=4]
1734
+ * @param {Boolean} [center]
1735
+ */
1736
+ drawTextScreen(text, pos, scale=4, center)
1737
+ {
1738
+ const context = this.context;
1739
+ context.save();
1740
+ context.imageSmoothingEnabled = !cavasPixelated;
1741
+
1742
+ const size = this.tileSize;
1743
+ const drawSize = size.add(this.paddingSize).scale(scale);
1744
+ const cols = this.image.width / this.tileSize.x |0;
1745
+ text.split('\n').forEach((line, i)=>
1746
+ {
1747
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
1748
+ for(let j=line.length; j--;)
1749
+ {
1750
+ // draw each character
1751
+ let charCode = line[j].charCodeAt();
1752
+ if (charCode < 32 || charCode > 127)
1753
+ charCode = 127; // unknown character
1754
+
1755
+ // get the character source location and draw it
1756
+ const tile = this.startTileIndex + charCode - 32;
1757
+ const x = tile % cols;
1758
+ const y = tile / cols |0;
1759
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
1760
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
1761
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
1762
+ }
1763
+ });
1764
+
1765
+ context.restore();
1766
+ }
1767
+
1768
+ /** Draw text in world space using the image font
1769
+ * @param {String} text
1770
+ * @param {Vector2} pos
1771
+ * @param {Number} [scale=.25]
1772
+ * @param {Boolean} [center]
1773
+ */
1774
+ drawText(text, pos, scale=1, center)
1775
+ {
1776
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
1777
+ }
1880
1778
  }
1881
1779
 
1882
1780
  ///////////////////////////////////////////////////////////////////////////////
@@ -2067,7 +1965,7 @@ const mouseToScreen = (mousePos)=>
2067
1965
 
2068
1966
  const rect = mainCanvas.getBoundingClientRect();
2069
1967
  return mainCanvasSize.multiply(
2070
- vec2(percent(mousePos.x, rect.right, rect.left), percent(mousePos.y, rect.bottom, rect.top)));
1968
+ vec2(percent(mousePos.x, rect.left, rect.right), percent(mousePos.y, rect.top, rect.bottom)));
2071
1969
  }
2072
1970
 
2073
1971
  ///////////////////////////////////////////////////////////////////////////////
@@ -2108,8 +2006,8 @@ function gamepadsUpdate()
2108
2006
  // read clamp dead zone of analog sticks
2109
2007
  const deadZone = .3, deadZoneMax = .8;
2110
2008
  const applyDeadZone = (v)=>
2111
- v > deadZone ? percent( v, deadZoneMax, deadZone) :
2112
- v < -deadZone ? -percent(-v, deadZoneMax, deadZone) : 0;
2009
+ v > deadZone ? percent( v, deadZone, deadZoneMax) :
2010
+ v < -deadZone ? -percent(-v, deadZone, deadZoneMax) : 0;
2113
2011
 
2114
2012
  // read analog sticks
2115
2013
  for (let j = 0; j < gamepad.axes.length-1; j+=2)
@@ -2189,7 +2087,7 @@ if (isTouchDevice)
2189
2087
  // touch gamepad, virtual on screen gamepad emulator for touch devices
2190
2088
 
2191
2089
  // touch input internal variables
2192
- let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2();
2090
+ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick = vec2(), touchGamepadAnalog = 1;
2193
2091
 
2194
2092
  // create the touch gamepad, called automatically by the engine
2195
2093
  function touchGamepadCreate()
@@ -2235,7 +2133,14 @@ function touchGamepadCreate()
2235
2133
  if (touchPos.distance(stickCenter) < touchGamepadSize)
2236
2134
  {
2237
2135
  // virtual analog stick
2238
- touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2136
+ if (touchGamepadAnalog)
2137
+ touchGamepadStick = touchPos.subtract(stickCenter).clampLength(touchGamepadSize/2).scale(2/touchGamepadSize);
2138
+ else
2139
+ {
2140
+ // 8 way dpad
2141
+ const angle = touchPos.subtract(stickCenter).angle();
2142
+ touchGamepadStick.setAngle((angle * 4 / PI + 8.5 | 0) * PI / 4);
2143
+ }
2239
2144
  }
2240
2145
  else if (touchPos.distance(buttonCenter) < touchGamepadSize)
2241
2146
  {
@@ -2259,7 +2164,7 @@ function touchGamepadRender()
2259
2164
  return;
2260
2165
 
2261
2166
  // fade off when not touching or paused
2262
- const alpha = percent(touchGamepadTimer.get(), 3, 4);
2167
+ const alpha = percent(touchGamepadTimer.get(), 4, 3);
2263
2168
  if (!alpha || paused)
2264
2169
  return;
2265
2170
 
@@ -2362,7 +2267,7 @@ class Sound
2362
2267
  return; // out of range
2363
2268
 
2364
2269
  // attenuate volume by distance
2365
- volume *= percent(lengthSquared**.5, range*this.taper, range);
2270
+ volume *= percent(lengthSquared**.5, range, range*this.taper);
2366
2271
  }
2367
2272
 
2368
2273
  // get pan from screen space coords
@@ -2544,7 +2449,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=0)
2544
2449
  // connect source to gain
2545
2450
  (
2546
2451
  window.StereoPannerNode ? // create pan node if possible
2547
- source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, 1, -1)}))
2452
+ source.connect(new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)}))
2548
2453
  : source
2549
2454
  )
2550
2455
  .connect(gainNode);
@@ -2917,11 +2822,11 @@ class TileLayerData
2917
2822
  class TileLayer extends EngineObject
2918
2823
  {
2919
2824
  /** Create a tile layer object
2920
- * @param {Vector2} [position=new Vector2(0,0)] - World space position
2921
- * @param {Vector2} [size=objectDefaultSize] - World space size
2922
- * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2923
- * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2924
- * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2825
+ * @param {Vector2} [position=new Vector2()] - World space position
2826
+ * @param {Vector2} [size=objectDefaultSize] - World space size
2827
+ * @param {Vector2} [tileSize=tileSizeDefault] - Size of tiles in source pixels
2828
+ * @param {Vector2} [scale=new Vector2(1,1)] - How much to scale this layer when rendered
2829
+ * @param {Number} [renderOrder=0] - Objects sorted by renderOrder before being rendered
2925
2830
  */
2926
2831
  constructor(pos, size=tileCollisionSize, tileSize=tileSizeDefault, scale=vec2(1), renderOrder=0)
2927
2832
  {
@@ -2981,17 +2886,19 @@ class TileLayer extends EngineObject
2981
2886
  );
2982
2887
  }
2983
2888
 
2984
- /** Draw all the tile data to an offscreen canvas */
2889
+ /** Draw all the tile data to an offscreen canvas
2890
+ * - This may be slow in some browsers
2891
+ */
2985
2892
  redraw()
2986
2893
  {
2987
- this.redrawStart();
2894
+ this.redrawStart(1);
2988
2895
  this.drawAllTileData();
2989
2896
  this.redrawEnd();
2990
2897
  }
2991
2898
 
2992
2899
  /** Call to start the redraw process
2993
- * @param {Boolean} [clear=1] - Should it clear the canvas before drawing */
2994
- redrawStart(clear = 1)
2900
+ * @param {Boolean} [clear=0] - Should it clear the canvas before drawing */
2901
+ redrawStart(clear = 0)
2995
2902
  {
2996
2903
  if (clear)
2997
2904
  {
@@ -3722,8 +3629,8 @@ function glInit()
3722
3629
 
3723
3630
  // create the canvas and tile texture
3724
3631
  glCanvas = document.createElement('canvas');
3725
- glContext = glCanvas.getContext('webgl');
3726
- glCanvas.style = canvaStyle;
3632
+ glContext = glCanvas.getContext('webgl', {antialias: false});
3633
+ glCanvas.style = styleCanvas;
3727
3634
  glTileTexture = glCreateTexture(tileImage);
3728
3635
 
3729
3636
  // some browsers are much faster without copying the gl buffer so we just overlay it instead
@@ -3733,14 +3640,13 @@ function glInit()
3733
3640
  glShader = glCreateProgram(
3734
3641
  'precision highp float;'+ // use highp for better accuracy, lowp for better perf
3735
3642
  'uniform mat4 m;'+ // transform matrix
3736
- 'attribute float a;'+ // angle
3737
- 'attribute vec2 p,s,t;'+ // position, size, uv
3738
- 'attribute vec4 c,b;'+ // color, additiveColor
3643
+ 'attribute vec2 p,t;'+ // position, uv
3644
+ 'attribute vec4 c,a;'+ // color, additiveColor
3739
3645
  'varying vec2 v;'+ // return uv
3740
3646
  'varying vec4 d,e;'+ // return color, additiveColor
3741
3647
  'void main(){'+ // shader entry point
3742
- 'gl_Position=m*vec4((s*cos(a)-vec2(-s.y,s)*sin(a))*.5+p,1,1);'+// transform position
3743
- 'v=t;d=c;e=b;'+ // pass stuff to fragment shader
3648
+ 'gl_Position=m*vec4(p,1,1);'+// transform position
3649
+ 'v=t;d=c;e=a;'+ // pass stuff to fragment shader
3744
3650
  '}' // end of shader
3745
3651
  ,
3746
3652
  'precision highp float;'+ // use highp for better accuracy, lowp for better perf
@@ -3767,12 +3673,10 @@ function glInit()
3767
3673
  glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
3768
3674
  offset += size*typeSize;
3769
3675
  }
3770
- initVertexAttribArray('a', gl_FLOAT, 4, 1); // angle
3771
3676
  initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
3772
- initVertexAttribArray('s', gl_FLOAT, 4, 2); // size
3773
3677
  initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
3774
3678
  initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
3775
- initVertexAttribArray('b', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3679
+ initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
3776
3680
  }
3777
3681
 
3778
3682
  /** Set the WebGl blend mode, normally you should call setBlendMode instead
@@ -3887,6 +3791,7 @@ function glPreRender(width, height, cameraX, cameraY, cameraScale)
3887
3791
 
3888
3792
  // clear and set to same size as main canvas
3889
3793
  glContext.viewport(0, 0, glCanvas.width = width, glCanvas.height = height);
3794
+ glContext.clear(gl_COLOR_BUFFER_BIT);
3890
3795
 
3891
3796
  // set up the shader
3892
3797
  glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = glTileTexture);
@@ -3933,12 +3838,10 @@ function glCopyToContext(context, forceDraw)
3933
3838
  if (!glEnable || !glBatchCount) return;
3934
3839
 
3935
3840
  glFlush();
3841
+
3842
+ // do not draw in overlay mode because the canvas is visible
3936
3843
  if (!glOverlay || forceDraw)
3937
- {
3938
- // do not draw/clear in overlay mode because the canvas is visible
3939
3844
  context.drawImage(glCanvas, 0, 0);
3940
- glContext.clear(gl_COLOR_BUFFER_BIT);
3941
- }
3942
3845
  }
3943
3846
 
3944
3847
  /** Add a sprite to the gl draw list, used by all gl draw functions
@@ -3961,51 +3864,49 @@ function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba=0xffffff
3961
3864
  // flush if there is no room for more verts or if different blend mode
3962
3865
  if (glBatchCount == gl_MAX_BATCH || glBatchAdditive != glAdditive)
3963
3866
  glFlush();
3867
+
3868
+ // prepare to create the verts from size and angle
3869
+ const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
3870
+ const cX = c*sizeX, cY = c*sizeY, sX = s*sizeX, sY = s*sizeY;
3964
3871
 
3965
3872
  // setup 2 triangles to form a quad
3966
3873
  let offset = glBatchCount++ * gl_VERTICES_PER_QUAD * gl_INDICIES_PER_VERT;
3967
3874
 
3968
3875
  // vertex 0
3969
- glPositionData[offset++] = angle;
3970
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3971
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3972
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3973
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3876
+ glPositionData[offset++] = x - cX - sY;
3877
+ glPositionData[offset++] = y - cY + sX;
3878
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3879
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3974
3880
 
3975
3881
  // vertex 1
3976
- glPositionData[offset++] = angle;
3977
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3978
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
3979
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3980
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3882
+ glPositionData[offset++] = x + cX + sY;
3883
+ glPositionData[offset++] = y + cY - sX;
3884
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3885
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3981
3886
 
3982
3887
  // vertex 2
3983
- glPositionData[offset++] = angle;
3984
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3985
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = sizeY;
3986
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3987
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3888
+ glPositionData[offset++] = x - cX + sY;
3889
+ glPositionData[offset++] = y + cY + sX;
3890
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv0Y;
3891
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3988
3892
 
3989
3893
  // vertex 0
3990
- glPositionData[offset++] = angle;
3991
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3992
- glPositionData[offset++] = -sizeX; glPositionData[offset++] = -sizeY;
3993
- glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3994
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3894
+ glPositionData[offset++] = x - cX - sY;
3895
+ glPositionData[offset++] = y - cY + sX;
3896
+ glPositionData[offset++] = uv0X; glPositionData[offset++] = uv1Y;
3897
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3995
3898
 
3996
3899
  // vertex 3
3997
- glPositionData[offset++] = angle;
3998
- glPositionData[offset++] = x; glPositionData[offset++] = y;
3999
- glPositionData[offset++] = sizeX; glPositionData[offset++] = -sizeY;
4000
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
4001
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3900
+ glPositionData[offset++] = x + cX - sY;
3901
+ glPositionData[offset++] = y - cY - sX;
3902
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv1Y;
3903
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
4002
3904
 
4003
3905
  // vertex 1
4004
- glPositionData[offset++] = angle;
4005
- glPositionData[offset++] = x; glPositionData[offset++] = y;
4006
- glPositionData[offset++] = sizeX; glPositionData[offset++] = sizeY;
4007
- glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
4008
- glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
3906
+ glPositionData[offset++] = x + cX + sY;
3907
+ glPositionData[offset++] = y + cY - sX;
3908
+ glPositionData[offset++] = uv1X; glPositionData[offset++] = uv0Y;
3909
+ glColorData[offset++] = rgba; glColorData[offset++] = rgbaAdditive;
4009
3910
  }
4010
3911
 
4011
3912
  ///////////////////////////////////////////////////////////////////////////////
@@ -4037,9 +3938,9 @@ gl_LINK_STATUS = 35714,
4037
3938
 
4038
3939
  // constants for batch rendering
4039
3940
  gl_VERTICES_PER_QUAD = 6,
4040
- gl_INDICIES_PER_VERT = 9,
3941
+ gl_INDICIES_PER_VERT = 6,
4041
3942
  gl_MAX_BATCH = 1<<16,
4042
- gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char * 4) * 2
3943
+ gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2; // vec2 * 2 + (char * 4) * 2
4043
3944
  /*
4044
3945
  LittleJS - The Tiny JavaScript Game Engine That Can!
4045
3946
  MIT License - Copyright 2021 Frank Force
@@ -4064,7 +3965,7 @@ gl_VERTEX_BYTE_STRIDE = 4 + (4 * 2) * 3 + (4) * 2; // float + vec2 * 3 + (char *
4064
3965
  const engineName = 'LittleJS';
4065
3966
 
4066
3967
  /** Version of engine */
4067
- const engineVersion = '1.1.26';
3968
+ const engineVersion = '1.2.0';
4068
3969
 
4069
3970
  /** Frames per second to update objects
4070
3971
  * @default */
@@ -4101,7 +4002,7 @@ let averageFPS, drawCount;
4101
4002
  // css text used for elements created by engine
4102
4003
  const styleBody = 'margin:0;overflow:hidden;background:#000' +
4103
4004
  ';touch-action:none;user-select:none;-webkit-user-select:none;-moz-user-select:none';
4104
- const canvaStyle = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
4005
+ const styleCanvas = 'position:absolute';
4105
4006
 
4106
4007
  ///////////////////////////////////////////////////////////////////////////////
4107
4008
 
@@ -4126,7 +4027,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4126
4027
  document.body.style = styleBody;
4127
4028
  document.body.appendChild(mainCanvas = document.createElement('canvas'));
4128
4029
  mainContext = mainCanvas.getContext('2d');
4129
- mainCanvas.style = canvaStyle;
4030
+ mainCanvas.style = styleCanvas;
4130
4031
 
4131
4032
  // init stuff and start engine
4132
4033
  debugInit();
@@ -4135,7 +4036,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4135
4036
  // create overlay canvas for hud to appear above gl canvas
4136
4037
  document.body.appendChild(overlayCanvas = document.createElement('canvas'));
4137
4038
  overlayContext = overlayCanvas.getContext('2d');
4138
- overlayCanvas.style = canvaStyle;
4039
+ overlayCanvas.style = styleCanvas;
4139
4040
 
4140
4041
  gameInit();
4141
4042
  touchGamepadCreate();
@@ -4151,7 +4052,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4151
4052
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
4152
4053
  frameTimeLastMS = frameTimeMS;
4153
4054
  if (debug || showWatermark)
4154
- averageFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), averageFPS || 0);
4055
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
4155
4056
  if (debug)
4156
4057
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
4157
4058
  timeReal += frameTimeDeltaMS / 1e3;
@@ -4202,13 +4103,24 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4202
4103
 
4203
4104
  // fit to window by adding space on top or bottom if necessary
4204
4105
  const aspect = innerWidth / innerHeight;
4205
- const fixedAspect = mainCanvas.width / mainCanvas.height;
4206
- mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
4207
- mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
4208
- if (glCanvas)
4106
+ const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
4107
+
4108
+ // clear css
4109
+ mainCanvas.style.width = overlayCanvas.style.width =
4110
+ mainCanvas.style.height = overlayCanvas.style.height =
4111
+ mainCanvas.style.top = overlayCanvas.style.top =
4112
+ mainCanvas.style.left = overlayCanvas.style.left = '';
4113
+
4114
+ // letterbox center canvas
4115
+ if (aspect < fixedAspect)
4116
+ {
4117
+ mainCanvas.style.width = overlayCanvas.style.width = innerWidth;
4118
+ mainCanvas.style.top = overlayCanvas.style.top = (innerHeight - innerWidth/fixedAspect)/2;
4119
+ }
4120
+ else
4209
4121
  {
4210
- glCanvas.style.width = mainCanvas.style.width;
4211
- glCanvas.style.height = mainCanvas.style.height;
4122
+ mainCanvas.style.height = overlayCanvas.style.height = innerHeight;
4123
+ mainCanvas.style.left = overlayCanvas.style.left = (innerWidth - innerHeight*fixedAspect)/2;
4212
4124
  }
4213
4125
  }
4214
4126
  else
@@ -4216,6 +4128,18 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4216
4128
  // clear and set size to same as window
4217
4129
  mainCanvas.width = min(innerWidth, canvasMaxSize.x);
4218
4130
  mainCanvas.height = min(innerHeight, canvasMaxSize.y);
4131
+
4132
+ // center canvas
4133
+ overlayCanvas.style.top = mainCanvas.style.top = (innerHeight - mainCanvas.height)/2;
4134
+ overlayCanvas.style.left = mainCanvas.style.left = (innerWidth - mainCanvas.width)/2;
4135
+ }
4136
+
4137
+ if (glEnable)
4138
+ {
4139
+ glCanvas.style.width = mainCanvas.style.width;
4140
+ glCanvas.style.height = mainCanvas.style.height;
4141
+ glCanvas.style.top = mainCanvas.style.top;
4142
+ glCanvas.style.left = mainCanvas.style.left;
4219
4143
  }
4220
4144
 
4221
4145
  // render sort then render while removing destroyed objects