littlejsengine 1.1.26 → 1.2.9

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.
Files changed (40) hide show
  1. package/README.md +7 -9
  2. package/build.bat +1 -1
  3. package/engine/engine.all.js +369 -377
  4. package/engine/engine.all.min.js +1 -1
  5. package/engine/engine.all.release.js +338 -147
  6. package/engine/engine.js +7 -7
  7. package/engine/engineAudio.js +2 -2
  8. package/engine/engineBuild.bat +9 -1
  9. package/engine/engineDebug.js +32 -231
  10. package/engine/engineDraw.js +130 -20
  11. package/engine/engineFont.png +0 -0
  12. package/engine/engineInput.js +32 -9
  13. package/engine/engineMedals.js +7 -4
  14. package/engine/engineObject.js +30 -11
  15. package/engine/engineParticles.js +5 -3
  16. package/engine/engineSettings.js +5 -0
  17. package/engine/engineTileLayer.js +17 -14
  18. package/engine/engineUtilities.js +50 -18
  19. package/engine/engineWebGL.js +53 -59
  20. package/engine/index.d.ts +1644 -0
  21. package/examples/breakout/game.js +9 -7
  22. package/examples/breakout/gameObjects.js +3 -3
  23. package/examples/breakout/index.html +3 -3
  24. package/examples/particles/index.html +366 -0
  25. package/examples/particles/tiles.png +0 -0
  26. package/examples/platformer/game.js +5 -5
  27. package/examples/platformer/gameEffects.js +10 -10
  28. package/examples/platformer/gameLevel.js +3 -3
  29. package/examples/platformer/gameObjects.js +3 -3
  30. package/examples/platformer/gamePlayer.js +11 -6
  31. package/examples/platformer/index.html +6 -6
  32. package/examples/platformer/tiles.png +0 -0
  33. package/examples/puzzle/game.js +3 -3
  34. package/examples/puzzle/index.html +2 -2
  35. package/examples/screenshot.jpg +0 -0
  36. package/examples/stress/index.html +4 -4
  37. package/game.js +4 -7
  38. package/index.html +13 -13
  39. package/package.json +8 -1
  40. package/engine/engineBuildSetup.bat +0 -6
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.9';
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;top:50%;left:50%;transform:translate(-50%,-50%)';
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();
@@ -103,13 +103,11 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
103
103
  // main update loop
104
104
  const engineUpdate = (frameTimeMS=0)=>
105
105
  {
106
- requestAnimationFrame(engineUpdate);
107
-
108
106
  // update time keeping
109
107
  let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
110
108
  frameTimeLastMS = frameTimeMS;
111
109
  if (debug || showWatermark)
112
- averageFPS = lerp(.05, 1e3/(frameTimeDeltaMS||1), averageFPS || 0);
110
+ averageFPS = lerp(.05, averageFPS || 0, 1e3/(frameTimeDeltaMS||1));
113
111
  if (debug)
114
112
  frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
115
113
  timeReal += frameTimeDeltaMS / 1e3;
@@ -203,6 +201,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
203
201
  overlayContext.fillText(text, mainCanvas.width-2, 2);
204
202
  drawCount = 0;
205
203
  }
204
+
205
+ requestAnimationFrame(engineUpdate);
206
206
  }
207
207
 
208
208
  // set tile image source to load the image and start the engine
@@ -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%
@@ -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
@@ -159,27 +160,27 @@ const debugUpdate = ()=>
159
160
  {
160
161
  if (!debug)
161
162
  return;
162
-
163
+
163
164
  if (keyWasPressed(192)) // ~
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));
@@ -275,17 +273,11 @@ const debugRender = ()=>
275
273
  drawRect(mousePos.floor().add(vec2(.5)), vec2(1), new Color(0,0,1,.5));
276
274
  drawLine(mousePos, bestObject.pos, .1, !raycastHitPos ? new Color(0,1,0,.5) : new Color(1,0,0,.5));
277
275
 
278
- let pos = mousePos.copy(), height = vec2(0,.5);
279
- const printVec2 = (v)=> '(' + (v.x>0?' ':'') + (v.x).toFixed(2) + ',' + (v.y>0?' ':'') + (v.y).toFixed(2) + ')';
280
- const args = [.5, new Color, .05, undefined, undefined, 'monospace'];
281
-
282
- drawText('pos = ' + printVec2(bestObject.pos)
283
- + (bestObject.angle>0?' ':' ') + (bestObject.angle*180/PI).toFixed(1) + '°',
284
- pos = pos.add(height), ...args);
285
- drawText('vel = ' + printVec2(bestObject.velocity), pos = pos.add(height), ...args);
286
- drawText('size = ' + printVec2(bestObject.size), pos = pos.add(height), ...args);
287
- drawText('type = ' + ( bestObject.constructor.name), pos = mousePos.subtract(height), ...args);
288
- drawText('collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height.scale(2)), ...args);
276
+ const debugText = 'mouse pos = ' + mousePos +
277
+ '\nmouse collision = ' + getTileCollisionData(mousePos) +
278
+ '\n\n--- object info ---\n' +
279
+ bestObject.toString();
280
+ drawTextScreen(debugText, mousePosScreen, 24, new Color, .05, undefined, undefined, 'monospace');
289
281
  mainContext = saveContext;
290
282
  }
291
283
 
@@ -293,16 +285,16 @@ const debugRender = ()=>
293
285
  }
294
286
 
295
287
  {
296
- // render debug rects
297
- overlayContext.lineWidth = 1;
288
+ // draw debug primitives
289
+ overlayContext.lineWidth = 2;
298
290
  const pointSize = debugPointSize * cameraScale;
299
291
  debugPrimitives.forEach(p=>
300
292
  {
293
+ overlayContext.save();
294
+
301
295
  // create canvas transform from world space to screen space
302
296
  const pos = worldToScreen(p.pos);
303
297
 
304
- overlayContext.save();
305
- overlayContext.lineWidth = 2;
306
298
  overlayContext.translate(pos.x|0, pos.y|0);
307
299
  overlayContext.rotate(p.angle);
308
300
  overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
@@ -335,15 +327,17 @@ const debugRender = ()=>
335
327
  p.fill && overlayContext.fill();
336
328
  overlayContext.stroke();
337
329
  }
338
-
330
+
339
331
  overlayContext.restore();
340
332
  });
341
333
 
342
- overlayContext.fillStyle = overlayContext.strokeStyle = '#fff';
334
+ // remove expired pritives
335
+ debugPrimitives = debugPrimitives.filter(r=>r.time.get()<0);
343
336
  }
344
337
 
345
338
  {
346
- let x = 9, y = -20, h = 30;
339
+ // draw debug overlay
340
+ overlayContext.save();
347
341
  overlayContext.fillStyle = '#fff';
348
342
  overlayContext.textAlign = 'left';
349
343
  overlayContext.textBaseline = 'top';
@@ -351,6 +345,7 @@ const debugRender = ()=>
351
345
  overlayContext.shadowColor = '#000';
352
346
  overlayContext.shadowBlur = 9;
353
347
 
348
+ let x = 9, y = -20, h = 30;
354
349
  if (debugOverlay)
355
350
  {
356
351
  overlayContext.fillText(engineName, x, y += h);
@@ -363,14 +358,12 @@ const debugRender = ()=>
363
358
  overlayContext.fillText('1: Debug Physics', x, y += h);
364
359
  overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
365
360
  overlayContext.fillText('2: Debug Particles', x, y += h);
361
+ overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
362
+ overlayContext.fillText('3: Debug Gamepads', x, y += h);
366
363
  overlayContext.fillStyle = godMode ? '#f00' : '#fff';
367
- overlayContext.fillText('3: God Mode', x, y += h);
364
+ overlayContext.fillText('4: God Mode', x, y += h);
368
365
  overlayContext.fillStyle = '#fff';
369
366
  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
367
 
375
368
  let keysPressed = '';
376
369
  for(const i in inputData[0])
@@ -388,7 +381,6 @@ const debugRender = ()=>
388
381
  buttonsPressed += i + ' ' ;
389
382
  }
390
383
  buttonsPressed && overlayContext.fillText('Gamepad: ' + buttonsPressed, x, y += h);
391
-
392
384
  }
393
385
  else
394
386
  {
@@ -398,197 +390,6 @@ const debugRender = ()=>
398
390
  overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
399
391
  }
400
392
 
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);
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)
393
+ overlayContext.restore();
440
394
  }
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
- ];
395
+ }
@@ -110,7 +110,7 @@ function drawTile(pos, size=vec2(1), tileIndex=-1, tileSize=tileSizeDefault, col
110
110
  if (tileIndex < 0)
111
111
  {
112
112
  // if negative tile index, force untextured
113
- context.fillStyle = color.rgba();
113
+ context.fillStyle = color;
114
114
  context.fillRect(-.5, -.5, 1, 1);
115
115
  }
116
116
  else
@@ -203,7 +203,20 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
203
203
  context.restore();
204
204
  }
205
205
 
206
- /** Draw text on overlay canvas in world space
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
+
218
+ /** Draw text on overlay canvas in screen space
219
+ * Automatically splits new lines into rows
207
220
  * @param {String} text
208
221
  * @param {Vector2} pos
209
222
  * @param {Number} [size=1]
@@ -212,32 +225,129 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, context = mainCont
212
225
  * @param {Color} [lineColor=new Color(0,0,0)]
213
226
  * @param {String} [textAlign='center']
214
227
  * @memberof Draw */
215
- function drawText(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
228
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault)
216
229
  {
217
- pos = worldToScreen(pos);
218
- overlayContext.font = size*cameraScale + 'px '+ font;
230
+ overlayContext.fillStyle = color;
231
+ overlayContext.lineWidth = lineWidth *= cameraScale;
232
+ overlayContext.strokeStyle = lineColor;
219
233
  overlayContext.textAlign = textAlign;
234
+ overlayContext.font = size + 'px '+ font;
220
235
  overlayContext.textBaseline = 'middle';
221
- if (lineWidth)
236
+
237
+ pos = pos.copy();
238
+ text.split('\n').forEach(line=>
222
239
  {
223
- overlayContext.lineWidth = lineWidth*cameraScale;
224
- overlayContext.strokeStyle = lineColor.rgba();
225
- overlayContext.strokeText(text, pos.x, pos.y);
226
- }
227
- overlayContext.fillStyle = color.rgba();
228
- overlayContext.fillText(text, pos.x, pos.y);
240
+ lineWidth && overlayContext.strokeText(line, pos.x, pos.y);
241
+ overlayContext.fillText(line, pos.x, pos.y);
242
+ pos.y += size;
243
+ });
229
244
  }
230
245
 
231
- /** Enable normal or additive blend mode
232
- * @param {Boolean} [additive=0]
233
- * @param {Boolean} [useWebGL=glEnable]
246
+ /** Draw text on overlay canvas in world space
247
+ * Automatically splits new lines into rows
248
+ * @param {String} text
249
+ * @param {Vector2} pos
250
+ * @param {Number} [size=1]
251
+ * @param {Color} [color=new Color(1,1,1)]
252
+ * @param {Number} [lineWidth=0]
253
+ * @param {Color} [lineColor=new Color(0,0,0)]
254
+ * @param {String} [textAlign='center']
234
255
  * @memberof Draw */
235
- function setBlendMode(additive, useWebGL=glEnable)
256
+ function drawText(text, pos, size=1, color, lineWidth, lineColor, textAlign, font)
236
257
  {
237
- if (glEnable && useWebGL)
238
- glSetBlendMode(additive);
239
- else
240
- mainContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
258
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth, lineColor, textAlign, font);
259
+ }
260
+
261
+ ///////////////////////////////////////////////////////////////////////////////
262
+
263
+ /**
264
+ * Font Image Object - Draw text on a 2D canvas by using characters in an image
265
+ * <br> - 96 characters (from space to tilde) are stored in an image
266
+ * <br> - Uses a default 8x8 font if none is supplied
267
+ * <br> - You can also use fonts from the main tile sheet
268
+ * @example
269
+ * // use built in font
270
+ * const font = new ImageFont;
271
+ *
272
+ * // draw text
273
+ * font.drawTextScreen("LittleJS\nHello World!", vec2(200, 50));
274
+ */
275
+
276
+ let engineFontImage;
277
+
278
+ class FontImage
279
+ {
280
+ /** Create an image font
281
+ * @param {Image} [image] - The image the font is stored in, if undefined the default font is used
282
+ * @param {Vector2} [tileSize=vec2(8)] - The size of the font source tiles
283
+ * @param {Vector2} [paddingSize=vec2(0,1)] - How much extra space to add between characters
284
+ * @param {Number} [startTileIndex=0] - Tile index in image where font starts
285
+ * @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
286
+ */
287
+ constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), startTileIndex=0, context=overlayContext)
288
+ {
289
+ if (!image && !engineFontImage)
290
+ {
291
+ // load default font image
292
+ engineFontImage = new Image();
293
+ engineFontImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAYAQAAAAA9+x6JAAAAAnRSTlMAAHaTzTgAAAGiSURBVHjaZZABhxxBEIUf6ECLBdFY+Q0PMNgf0yCgsSAGZcT9sgIPtBWwIA5wgAPEoHUyJeeSlW+gjK+fegWwtROWpVQEyWh2npdpBmTUFVhb29RINgLIukoXr5LIAvYQ5ve+1FqWEMqNKTX3FAJHyQDRZvmKWubAACcv5z5Gtg2oyCWE+Yk/8JZQX1jTTCpKAFGIgza+dJCNBF2UskRlsgwitHbSV0QLgt9sTPtsRlvJjEr8C/FARWA2bJ/TtJ7lko34dNDn6usJUMzuErP89UUBJbWeozrwLLncXczd508deAjLWipLO4Q5XGPcJvPu92cNDaN0P5G1FL0nSOzddZOrJ6rNhbXGmeDvO3TF7DeJWl4bvaYQTNHCTeuqKZmbjHaSOFes+IX/+IhHrnAkXOAsfn24EM68XieIECoccD4KZLk/odiwzeo2rovYdhvb2HYFgyznJyDpYJdYOmfXgVdJTaUi4xA2uWYNYec9BLeqdl9EsoTw582mSFDX2DxVLbNt9U3YYoeatBad1c2Tj8t2akrjaIGJNywKB/7h75/gN3vCMSaadIUTAAAAAElFTkSuQmCC';
294
+ }
295
+
296
+ this.image = image || engineFontImage;
297
+ this.tileSize = tileSize;
298
+ this.paddingSize = paddingSize;
299
+ this.startTileIndex = startTileIndex;
300
+ this.context = context;
301
+ }
302
+
303
+ /** Draw text in screen space using the image font
304
+ * @param {String} text
305
+ * @param {Vector2} pos
306
+ * @param {Number} [scale=4]
307
+ * @param {Boolean} [center]
308
+ */
309
+ drawTextScreen(text, pos, scale=4, center)
310
+ {
311
+ const context = this.context;
312
+ context.save();
313
+ context.imageSmoothingEnabled = !cavasPixelated;
314
+
315
+ const size = this.tileSize;
316
+ const drawSize = size.add(this.paddingSize).scale(scale);
317
+ const cols = this.image.width / this.tileSize.x |0;
318
+ text.split('\n').forEach((line, i)=>
319
+ {
320
+ const centerOffset = center ? line.length * size.x * scale / 2 |0 : 0;
321
+ for(let j=line.length; j--;)
322
+ {
323
+ // draw each character
324
+ let charCode = line[j].charCodeAt();
325
+ if (charCode < 32 || charCode > 127)
326
+ charCode = 127; // unknown character
327
+
328
+ // get the character source location and draw it
329
+ const tile = this.startTileIndex + charCode - 32;
330
+ const x = tile % cols;
331
+ const y = tile / cols |0;
332
+ const drawPos = pos.add(vec2(j,i).multiply(drawSize));
333
+ context.drawImage(this.image, x * size.x, y * size.y, size.x, size.y,
334
+ drawPos.x - centerOffset, drawPos.y, size.x * scale, size.y * scale);
335
+ }
336
+ });
337
+
338
+ context.restore();
339
+ }
340
+
341
+ /** Draw text in world space using the image font
342
+ * @param {String} text
343
+ * @param {Vector2} pos
344
+ * @param {Number} [scale=.25]
345
+ * @param {Boolean} [center]
346
+ */
347
+ drawText(text, pos, scale=1, center)
348
+ {
349
+ this.drawTextScreen(text, worldToScreen(pos).floor(), scale*cameraScale|0, center);
350
+ }
241
351
  }
242
352
 
243
353
  ///////////////////////////////////////////////////////////////////////////////
Binary file