littlejsengine 1.18.7 → 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.
@@ -103,7 +103,7 @@ function inputClear()
103
103
  * @memberof Input */
104
104
  function keyIsDown(key, device=0)
105
105
  {
106
- ASSERT(isString(key), 'key must be a number or string');
106
+ ASSERT(isStringLike(key), 'key must be a number or string');
107
107
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
108
108
  return !!(inputData[device]?.[key] & 1);
109
109
  }
@@ -115,7 +115,7 @@ function keyIsDown(key, device=0)
115
115
  * @memberof Input */
116
116
  function keyWasPressed(key, device=0)
117
117
  {
118
- ASSERT(isString(key), 'key must be a number or string');
118
+ ASSERT(isStringLike(key), 'key must be a number or string');
119
119
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
120
120
  return !!(inputData[device]?.[key] & 2);
121
121
  }
@@ -127,7 +127,7 @@ function keyWasPressed(key, device=0)
127
127
  * @memberof Input */
128
128
  function keyWasReleased(key, device=0)
129
129
  {
130
- ASSERT(isString(key), 'key must be a number or string');
130
+ ASSERT(isStringLike(key), 'key must be a number or string');
131
131
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
132
132
  return !!(inputData[device]?.[key] & 4);
133
133
  }
@@ -141,10 +141,10 @@ function keyWasReleased(key, device=0)
141
141
  * @memberof Input */
142
142
  function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
143
143
  {
144
- ASSERT(isString(up), 'up key must be a string');
145
- ASSERT(isString(down), 'down key must be a string');
146
- ASSERT(isString(left), 'left key must be a string');
147
- ASSERT(isString(right), 'right key must be a string');
144
+ ASSERT(isStringLike(up), 'up key must be a string');
145
+ ASSERT(isStringLike(down), 'down key must be a string');
146
+ ASSERT(isStringLike(left), 'left key must be a string');
147
+ ASSERT(isStringLike(right), 'right key must be a string');
148
148
  const k = (key)=> keyIsDown(key) ? 1 : 0;
149
149
  return vec2(k(right) - k(left), k(up) - k(down));
150
150
  }
package/src/engineMath.js CHANGED
@@ -292,13 +292,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
292
292
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
293
293
 
294
294
  /**
295
- * Check if object can be converted to a string (has a toString method)
295
+ * Check if a value is stringifiable i.e. it has a toString that returns
296
+ * a string. Use this for ASSERTs and inputs that will be coerced to text;
297
+ * use `typeof x === 'string'` inline if you need strict-string semantics.
296
298
  * - Returns true for strings, numbers, and most objects
297
299
  * - Returns false for null and undefined
298
300
  * @param {any} s
299
301
  * @return {boolean}
300
302
  * @memberof Math */
301
- function isString(s) { return s != null && typeof s?.toString() === 'string'; }
303
+ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
302
304
 
303
305
  /**
304
306
  * Check if object is an array
@@ -1042,7 +1044,7 @@ class Color
1042
1044
  * @return {Color} */
1043
1045
  setHex(hex)
1044
1046
  {
1045
- ASSERT(isString(hex), 'Color hex code must be a string');
1047
+ ASSERT(isStringLike(hex), 'Color hex code must be a string');
1046
1048
  ASSERT(hex[0] === '#', 'Color hex code must start with #');
1047
1049
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1048
1050
 
@@ -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
 
@@ -155,8 +156,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
155
156
  * @memberof Utilities */
156
157
  function saveDataURL(url, filename='download', revokeTime)
157
158
  {
158
- ASSERT(isString(url), 'saveDataURL requires url string');
159
- ASSERT(isString(filename), 'saveDataURL requires filename string');
159
+ ASSERT(isStringLike(url), 'saveDataURL requires url string');
160
+ ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
160
161
 
161
162
  // create link for saving screenshots
162
163
  const link = document.createElement('a');
@@ -174,8 +175,8 @@ function saveDataURL(url, filename='download', revokeTime)
174
175
  * @memberof Utilities */
175
176
  function shareURL(title, url, callback)
176
177
  {
177
- ASSERT(isString(title), 'shareURL requires title string');
178
- ASSERT(isString(url), 'shareURL requires url string');
178
+ ASSERT(isStringLike(title), 'shareURL requires title string');
179
+ ASSERT(isStringLike(url), 'shareURL requires url string');
179
180
  navigator.share?.({title, url}).then(()=>callback?.());
180
181
  }
181
182
 
@@ -188,7 +189,7 @@ function shareURL(title, url, callback)
188
189
  * @memberof Utilities */
189
190
  function readSaveData(saveName, defaultSaveData)
190
191
  {
191
- ASSERT(isString(saveName), 'loadData requires saveName string');
192
+ ASSERT(isStringLike(saveName), 'loadData requires saveName string');
192
193
 
193
194
  // replace undefined values with defaults; tolerate corrupt JSON
194
195
  const data = localStorage[saveName];
@@ -207,6 +208,48 @@ function readSaveData(saveName, defaultSaveData)
207
208
  * @memberof Utilities */
208
209
  function writeSaveData(saveName, saveData)
209
210
  {
210
- ASSERT(isString(saveName), 'saveData requires saveName string');
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