littlejsengine 1.18.8 → 1.18.12

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.
@@ -423,14 +423,15 @@ class SoundInstance
423
423
 
424
424
  /** Speak text with passed in settings
425
425
  * @param {string} text - The text to speak
426
- * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
427
426
  * @param {number} [volume] - How much to scale volume by
428
427
  * @param {number} [rate] - How quickly to speak
429
428
  * @param {number} [pitch] - How much to change the pitch by
429
+ * @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
430
430
  * @return {SpeechSynthesisUtterance} - The utterance that was spoken
431
431
  * @memberof Audio */
432
- function speak(text, language='', volume=1, rate=1, pitch=1)
432
+ function speak(text, volume=1, rate=1, pitch=1, language='')
433
433
  {
434
+ ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
434
435
  if (!soundEnable || headlessMode) return;
435
436
  if (!speechSynthesis) return;
436
437
 
@@ -502,7 +502,8 @@ function debugRender()
502
502
  debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
503
503
  x, y += h);
504
504
  debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
505
- debugContext.fillText('Draw Count: ' + drawCount, x, y += h);
505
+ debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
506
+ debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
506
507
  debugContext.fillText('---------', x, y += h);
507
508
  debugContext.fillStyle = '#f00';
508
509
  debugContext.fillText('ESC: Debug Overlay', x, y += h);
@@ -573,7 +574,8 @@ function debugRenderPost()
573
574
  mainContext.font = '1em monospace';
574
575
  mainContext.fillStyle = '#000';
575
576
  const text = engineName + ' v' + engineVersion + ' / '
576
- + drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
577
+ + drawCount + ' / ' + primitiveCount + ' / '
578
+ + engineObjects.length + ' / ' + averageFPS.toFixed(1)
577
579
  + (glEnable ? ' GL' : ' 2D') ;
578
580
  mainContext.fillText(text, mainCanvas.width-3, 3);
579
581
  mainContext.fillStyle = '#fff';
package/src/engineDraw.js CHANGED
@@ -72,6 +72,12 @@ let textureInfos = [];
72
72
  * @memberof Draw */
73
73
  let drawCount;
74
74
 
75
+ /** Keeps track of how many primitives were drawn each frame for debugging
76
+ * A single draw call can render many primitives (e.g. a WebGL sprite batch).
77
+ * @type {number}
78
+ * @memberof Draw */
79
+ let primitiveCount;
80
+
75
81
  // internal predicates for tint short-circuiting in canvas2D draw paths
76
82
  // isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
77
83
  // isBlack includes alpha so additive colors that only contribute alpha are not skipped
@@ -312,19 +318,19 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
312
318
  }
313
319
  else
314
320
  {
315
- // if no tile info, force untextured by zeroing rgba (so whatever
316
- // texture is bound doesn't leak in) and folding color+additive
317
- // into the additive slot matches the Canvas2D path's
318
- // color.add(additiveColor) on line ~337.
321
+ // untextured: glDrawUntextured picks the optimal path (poly
322
+ // tristrip if already in poly mode, otherwise instanced with
323
+ // uvs/rgba zeroed). Color+additive are folded together to match
324
+ // the Canvas2D path's color.add(additiveColor) on line ~337.
319
325
  const combined = additiveColor ? color.add(additiveColor) : color;
320
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
321
- 0, combined.rgbaInt());
326
+ glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
322
327
  }
323
328
  }
324
329
  else
325
330
  {
326
331
  // normal canvas 2D rendering method (slower)
327
332
  ++drawCount;
333
+ ++primitiveCount;
328
334
  size = new Vector2(size.x, -size.y); // flip upside down sprites
329
335
  drawCanvas2D(pos, size, angle, mirror, (context)=>
330
336
  {
@@ -364,13 +370,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
364
370
  * @param {Vector2} pos
365
371
  * @param {Vector2} [size=vec2(1)]
366
372
  * @param {Color} [colorTop=WHITE]
367
- * @param {Color} [colorBottom=BLACK]
373
+ * @param {Color} [colorBottom=CLEAR_WHITE]
368
374
  * @param {number} [angle]
369
375
  * @param {boolean} [useWebGL=glEnable]
370
376
  * @param {boolean} [screenSpace]
371
377
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
372
378
  * @memberof Draw */
373
- function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0, useWebGL=glEnable, screenSpace=false, context)
379
+ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
374
380
  {
375
381
  ASSERT(isVector2(pos), 'pos must be a vec2');
376
382
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -410,6 +416,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
410
416
  {
411
417
  // normal canvas 2D rendering method (slower)
412
418
  ++drawCount;
419
+ ++primitiveCount;
413
420
  size = new Vector2(size.x, -size.y); // fix upside down sprites
414
421
  drawCanvas2D(pos, size, angle, false, (context)=>
415
422
  {
@@ -472,8 +479,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
472
479
  return;
473
480
  }
474
481
 
475
- // Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
482
+ // Canvas2D path — increment counts here (WebGL counts via glFlush)
476
483
  ++drawCount;
484
+ ++primitiveCount;
477
485
 
478
486
  if (!screenSpace)
479
487
  {
@@ -547,6 +555,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
547
555
  {
548
556
  // normal canvas 2D rendering method (slower)
549
557
  ++drawCount;
558
+ ++primitiveCount;
550
559
  drawCanvas2D(pos, vec2(1), angle, false, (context)=>
551
560
  {
552
561
  context.strokeStyle = color.toString();
@@ -727,6 +736,77 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
727
736
  drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
728
737
  }
729
738
 
739
+ /** Draw a circle filled with a radial gradient from the center to the rim
740
+ * - Best when batched with other untextured polys
741
+ * - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
742
+ * - Stacking gradients at the exact same position may show a faint vertical artifact
743
+ * @param {Vector2} pos
744
+ * @param {number} [size=1] - Diameter
745
+ * @param {Color} [colorInner=WHITE]
746
+ * @param {Color} [colorOuter=CLEAR_WHITE]
747
+ * @param {boolean} [useWebGL=glEnable]
748
+ * @param {boolean} [screenSpace]
749
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
750
+ * @memberof Draw */
751
+ let drawCircleGradientOffset = 0;
752
+ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
753
+ {
754
+ ASSERT(isVector2(pos), 'pos must be a vec2');
755
+ ASSERT(isNumber(size), 'size must be a number');
756
+ ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
757
+ ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
758
+
759
+ if (headlessMode) return;
760
+
761
+ if (useWebGL && glEnable)
762
+ {
763
+ ASSERT(!!glContext, 'WebGL is not enabled!');
764
+ if (screenSpace)
765
+ {
766
+ // convert to world space
767
+ pos = screenToWorld(pos);
768
+ size /= cameraScale;
769
+ }
770
+ // fan as tristrip; rotate the boundary vertex by one slice per call
771
+ // so back-to-back gradients at the same position have their hole
772
+ // (from gpu edge-rule on the boundary line-degen) at different rim
773
+ // verts and don't visibly stack
774
+ const sides = glCircleSides;
775
+ const radius = size/2;
776
+ const innerInt = colorInner.rgbaInt();
777
+ const outerInt = colorOuter.rgbaInt();
778
+ const offset = drawCircleGradientOffset++;
779
+ const startA = (offset%sides)/sides*PI*2;
780
+ const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
781
+ const colors = [outerInt];
782
+ for (let i=sides; i--;)
783
+ {
784
+ const a = ((i+offset)%sides)/sides*PI*2;
785
+ points.push(pos);
786
+ colors.push(innerInt);
787
+ points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
788
+ colors.push(outerInt);
789
+ }
790
+ glDrawColoredPoints(points, colors);
791
+ }
792
+ else
793
+ {
794
+ // normal canvas 2D rendering method (slower)
795
+ ++drawCount;
796
+ ++primitiveCount;
797
+ drawCanvas2D(pos, vec2(size), 0, false, (context)=>
798
+ {
799
+ const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
800
+ gradient.addColorStop(0, colorInner.toString());
801
+ gradient.addColorStop(1, colorOuter.toString());
802
+ context.fillStyle = gradient;
803
+ context.beginPath();
804
+ context.ellipse(0, 0, .5, .5, 0, 0, 9);
805
+ context.fill();
806
+ }, screenSpace, context);
807
+ }
808
+ }
809
+
730
810
  /**
731
811
  * @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
732
812
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
@@ -187,6 +187,8 @@ export
187
187
  shareURL,
188
188
  readSaveData,
189
189
  writeSaveData,
190
+ noise1D,
191
+ noise2D,
190
192
 
191
193
  // Random
192
194
  rand,
@@ -240,6 +242,7 @@ export
240
242
  mainCanvasSize,
241
243
  textureInfos,
242
244
  drawCount,
245
+ primitiveCount,
243
246
  screenToWorld,
244
247
  worldToScreen,
245
248
  screenToWorldDelta,
@@ -255,6 +258,7 @@ export
255
258
  drawRegularPoly,
256
259
  drawEllipse,
257
260
  drawCircle,
261
+ drawCircleGradient,
258
262
  drawCanvas2D,
259
263
  drawText,
260
264
  drawTextScreen,
@@ -284,6 +288,7 @@ export
284
288
  glCopyToContext,
285
289
  glSetAntialias,
286
290
  glDraw,
291
+ glDrawUntextured,
287
292
  glDrawPointsTransform,
288
293
  glDrawOutlineTransform,
289
294
  glDrawPoints,
@@ -6,6 +6,7 @@
6
6
  * - File saving (text, canvas, data URLs)
7
7
  * - Native share dialog support
8
8
  * - Local storage save data management
9
+ * - Gradient noise (1D and 2D)
9
10
  * @namespace Utilities
10
11
  */
11
12
 
@@ -209,4 +210,46 @@ function writeSaveData(saveName, saveData)
209
210
  {
210
211
  ASSERT(isStringLike(saveName), 'saveData requires saveName string');
211
212
  localStorage[saveName] = JSON.stringify(saveData);
213
+ }
214
+
215
+ ///////////////////////////////////////////////////////////////////////////////
216
+
217
+ // Deterministic well-distributed hash of an integer lattice index to [0, 1).
218
+ // Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
219
+ function noiseHash(i)
220
+ {
221
+ let h = (i | 0) ^ 0x9e3779b9;
222
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
223
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
224
+ h ^= h >>> 16;
225
+ return (h >>> 0) / 2**32;
226
+ }
227
+
228
+ /** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
229
+ * Integer inputs land on deterministic lattice values; non-integer inputs
230
+ * are interpolated with smoothStep for C1 continuity.
231
+ * @param {number} x
232
+ * @return {number}
233
+ * @memberof Utilities */
234
+ function noise1D(x)
235
+ {
236
+ const i = floor(x);
237
+ return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
238
+ }
239
+
240
+ /** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
241
+ * @param {number} x
242
+ * @param {number} y
243
+ * @return {number}
244
+ * @memberof Utilities */
245
+ function noise2D(x, y)
246
+ {
247
+ const ix = floor(x), iy = floor(y);
248
+ const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
249
+ // large prime decorrelates neighboring rows
250
+ const h = (a, b) => noiseHash(a + b * 374761393);
251
+ return lerp(
252
+ lerp(h(ix, iy ), h(ix + 1, iy ), fx),
253
+ lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
254
+ fy);
212
255
  }
@@ -505,7 +505,8 @@ function glFlush()
505
505
  glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
506
506
  else
507
507
  glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
508
- drawCount += glBatchCount;
508
+ ++drawCount;
509
+ primitiveCount += glBatchCount;
509
510
  glBatchCount = 0;
510
511
  }
511
512
  glBatchAdditive = glAdditive;
@@ -566,6 +567,67 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
566
567
  glPositionData[offset++] = angle;
567
568
  }
568
569
 
570
+ /** Add an untextured rect to the gl draw list
571
+ * Picks the optimal path: if already in poly mode, emits a tristrip rect
572
+ * so it batches with surrounding polys; otherwise uses the instanced path
573
+ * with uvs and rgba zeroed so the color falls through the additive slot.
574
+ * @param {number} x
575
+ * @param {number} y
576
+ * @param {number} sizeX
577
+ * @param {number} sizeY
578
+ * @param {number} angle
579
+ * @param {number} rgba - color as 32-bit integer
580
+ * @memberof WebGL */
581
+ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
582
+ {
583
+ if (glPolyMode)
584
+ {
585
+ // batch with surrounding polys as a 4-vertex tristrip rect
586
+ const vertCount = 6; // 4 corners + 2 degenerate verts
587
+ if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
588
+ glFlush();
589
+
590
+ // compute rotated corners in world space (matches glDrawPointsTransform rotation)
591
+ const hx = sizeX*.5, hy = sizeY*.5;
592
+ const c = cos(angle), s = sin(angle);
593
+ const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
594
+ const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
595
+ const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
596
+ const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
597
+ const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
598
+
599
+ // write tristrip with leading/trailing degenerate verts
600
+ let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
601
+ glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
602
+ glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
603
+ glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
604
+ glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
605
+ glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
606
+ glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
607
+ glBatchCount += vertCount;
608
+ return;
609
+ }
610
+
611
+ // instanced path: zero uvs and rgba so the texture contribution is killed,
612
+ // then carry the real color in the additive slot
613
+ if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
614
+ glFlush();
615
+ glSetInstancedMode();
616
+
617
+ let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
618
+ glPositionData[offset++] = x;
619
+ glPositionData[offset++] = y;
620
+ glPositionData[offset++] = sizeX;
621
+ glPositionData[offset++] = sizeY;
622
+ glPositionData[offset++] = 0;
623
+ glPositionData[offset++] = 0;
624
+ glPositionData[offset++] = 0;
625
+ glPositionData[offset++] = 0;
626
+ glColorData[offset++] = 0;
627
+ glColorData[offset++] = rgba;
628
+ glPositionData[offset++] = angle;
629
+ }
630
+
569
631
  /** Transform and add a polygon to the gl draw list
570
632
  * @param {Array<Vector2>} points - Array of Vector2 points
571
633
  * @param {number} rgba - Color of the polygon as a 32-bit integer