littlejsengine 1.24.1 → 1.24.2

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 (42) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/EDITOR.md +2 -0
  3. package/FAQ.md +13 -2
  4. package/README.md +1 -1
  5. package/REFERENCE.md +20 -6
  6. package/dist/littlejs.d.ts +77 -13
  7. package/dist/littlejs.esm.js +2395 -1939
  8. package/dist/littlejs.esm.min.js +3 -3
  9. package/dist/littlejs.js +2392 -1939
  10. package/dist/littlejs.min.js +2 -2
  11. package/dist/littlejs.release.js +2356 -1952
  12. package/package.json +1 -1
  13. package/plugins/gltf.js +62 -9
  14. package/plugins/lightSystem.js +1 -1
  15. package/plugins/math3d.js +1014 -1013
  16. package/plugins/medalSystem.js +7 -11
  17. package/plugins/particleEffects.js +2 -2
  18. package/plugins/pathFinder.js +2 -2
  19. package/plugins/pluginExport.js +1 -0
  20. package/plugins/render3d.js +131 -55
  21. package/plugins/render3dEditor.js +24 -10
  22. package/plugins/render3dExtras.js +29 -17
  23. package/plugins/render3dLevel.js +21 -12
  24. package/plugins/render3dVoxels.js +2 -2
  25. package/plugins/textureSheet.js +97 -3
  26. package/plugins/tweakables.js +1 -1
  27. package/plugins/tweenSystem.js +2 -2
  28. package/plugins/uiSystem.js +6 -2
  29. package/src/engine.js +34 -11
  30. package/src/engineAudio.js +13 -3
  31. package/src/engineDebug.js +4 -1
  32. package/src/engineDraw.js +29 -16
  33. package/src/engineEditor.js +82 -50
  34. package/src/engineExport.js +2 -0
  35. package/src/engineInput.js +17 -3
  36. package/src/engineMath.js +6 -2
  37. package/src/engineObject.js +675 -675
  38. package/src/engineParticles.js +1 -1
  39. package/src/engineSettings.js +16 -0
  40. package/src/engineTileLayer.js +85 -22
  41. package/src/engineUtilities.js +20 -8
  42. package/src/engineWebGL.js +12 -8
@@ -453,7 +453,7 @@ function editorSetOpen(open)
453
453
  // the layer edited last, when it is still there, or the collision layer
454
454
  const layers = editorLayers();
455
455
  layers.includes(editorLayer) || (editorLayer = layers.find((layer)=> layer.live?.isSolid) ??
456
- layers.filter((layer)=> !layer.isObjects).at(-1));
456
+ layers.filter((layer)=> !layer.isObjects).pop());
457
457
  layers.includes(editorObjectLayer) || (editorObjectLayer = editorLayer ? undefined :
458
458
  layers.find((layer)=> layer.isObjects)); // a level of objects alone
459
459
  editorCall('onOpen');
@@ -654,7 +654,7 @@ function editorLayerRecord(live)
654
654
  function editorTilesets(live)
655
655
  {
656
656
  const tileInfo = live.tileInfo, image = tileInfo?.textureInfo?.image;
657
- if (!image) return [];
657
+ if (!image || tileInfo.tiles) return []; // a tile set is packed from many images, none of them a Tiled tileset
658
658
  const {x: tilewidth, y: tileheight} = tileInfo.size, padding = tileInfo.padding;
659
659
  const columns = tileInfo.columns || floor(image.width / (tilewidth + padding*2));
660
660
  const rows = floor(image.height / (tileheight + padding*2));
@@ -670,7 +670,17 @@ function editorJSONFetched(url, json)
670
670
  { json && typeof json === 'object' && editorFetchedURLs.set(json, String(url)); }
671
671
 
672
672
  // a map as Tiled JSON, everything it was loaded with kept, the tile data as the editor left it
673
- function editorMapJSON(record) { return JSON.stringify(record.map); }
673
+ function editorMapJSON(record)
674
+ {
675
+ // an object layer the editor made for a first object, every object of which was taken back, is left out
676
+ const map = record.map, made = map.layers.find((layer)=> editorMadeGroups.has(layer) && !layer.objects.length);
677
+ if (!made) return JSON.stringify(map);
678
+ return JSON.stringify({...map, layers: map.layers.filter((layer)=> layer !== made),
679
+ ...(map.nextlayerid === made.id + 1 && {nextlayerid: made.id})});
680
+ }
681
+
682
+ // the object layers the editor made in a map, for a map that had none
683
+ const editorMadeGroups = new WeakSet;
674
684
 
675
685
  // the files picked to save maps to, kept across reloads in IndexedDB by page and map, which is where a browser
676
686
  // lets a page keep them; each resolves to undefined when the store can not be used
@@ -882,7 +892,7 @@ function editorAutosave(record)
882
892
  const saves = editorSaves(), map = record.map, data = editorTileLayerData(map.layers);
883
893
  const objects = editorObjectGroups(map.layers).map((group)=> group.objects ?? []);
884
894
  const original = record.originalObjects ?? [], kept = objects.slice();
885
- while (kept.length > original.length && !kept.at(-1).length)
895
+ while (kept.length > original.length && !kept[kept.length - 1].length)
886
896
  kept.pop(); // an Objects layer the editor made, empty again, is not an edit
887
897
  const size = record.originalSize, sameSize = map.width === size.width && map.height === size.height;
888
898
  // the map as it was loaded has nothing to keep; one a Save wrote is kept until a reload shows the file has it
@@ -1223,13 +1233,15 @@ function editorObjectLayers(record)
1223
1233
  return record.objectLayers;
1224
1234
  }
1225
1235
 
1226
- // a new Tiled object layer named Objects, on top of a map
1236
+ // a new Tiled object layer named Objects, on top of a map, with an id past every layer's, in groups too
1227
1237
  function editorNewObjectGroup(map)
1228
1238
  {
1229
- const id = map.nextlayerid ?? map.layers.length + 1, group = {draworder: 'topdown', id, name: 'Objects',
1239
+ const ids = (layers)=> (layers || []).flatMap((layer)=> [(layer.id | 0) + 1, ...ids(layer.layers)]);
1240
+ const id = max(map.nextlayerid ?? 1, ...ids(map.layers)), group = {draworder: 'topdown', id, name: 'Objects',
1230
1241
  objects: [], opacity: 1, type: 'objectgroup', visible: true, x: 0, y: 0};
1231
1242
  map.nextlayerid = id + 1;
1232
1243
  map.layers.push(group);
1244
+ editorMadeGroups.add(group);
1233
1245
  return group;
1234
1246
  }
1235
1247
 
@@ -1253,12 +1265,13 @@ function editorObjectPos(record, object)
1253
1265
  return vec2(object.x / tilewidth, height - object.y / tileheight);
1254
1266
  }
1255
1267
 
1256
- // set an object's position in the map from a world position
1268
+ // set an object's position in the map from a world position, to a millionth of a pixel, so a place turned into
1269
+ // cells and back is written as it was and not a hair off
1257
1270
  function editorObjectSetPos(record, object, pos)
1258
1271
  {
1259
1272
  const {height=0, tilewidth=1, tileheight=1} = record.map;
1260
- object.x = pos.x * tilewidth;
1261
- object.y = (height - pos.y) * tileheight;
1273
+ object.x = round(pos.x * tilewidth * 1e6) / 1e6;
1274
+ object.y = round((height - pos.y) * tileheight * 1e6) / 1e6;
1262
1275
  }
1263
1276
 
1264
1277
  // called by objectLayersLoad for each object it read, the editor keeps what the game made by the object's id
@@ -1306,8 +1319,8 @@ function editorSetObjects(layer, list)
1306
1319
  if ('pos' in made && (old.x !== object.x || old.y !== object.y))
1307
1320
  made.pos = editorObjectPos(record, object);
1308
1321
  const was = objectLayersProperties(type, old), now = objectLayersProperties(type, object);
1309
- for (const name in now)
1310
- editorSameData(was[name], now[name]) || (made[name] = now[name]);
1322
+ for (const name in now) // a method's name is left out, as objectLayersMake leaves it
1323
+ editorSameData(was[name], now[name]) || typeof made[name] === 'function' || (made[name] = now[name]);
1311
1324
  }
1312
1325
  }
1313
1326
  }
@@ -1321,7 +1334,7 @@ function editorChangeObjects(layer, change)
1321
1334
  change(after);
1322
1335
  if (editorSameData(before, after)) return false;
1323
1336
  editorSetObjects(layer, after);
1324
- const last = editorStroke?.at(-1);
1337
+ const last = editorStroke?.[editorStroke.length - 1];
1325
1338
  if (last?.objectLayer === layer)
1326
1339
  last.after = after;
1327
1340
  else
@@ -1335,15 +1348,21 @@ function editorObjectSetProperty(object, name, value, defaultValue)
1335
1348
  {
1336
1349
  if (Number.isInteger(defaultValue) && isNumber(value))
1337
1350
  value = round(value); // an integer stays one, as Tiled keeps an int
1338
- const properties = (object.properties ?? []).filter((property)=> property.name !== name);
1351
+ const list = object.properties ?? [], at = list.findIndex((property)=> property.name === name), old = list[at];
1352
+ const properties = list.filter((property)=> property.name !== name);
1339
1353
  const text = (v)=> isColor(v) ? v.toString() : isVector2(v) ? v.x + ',' + v.y : JSON.stringify(v);
1340
1354
  if (text(value) !== text(defaultValue))
1341
1355
  {
1342
1356
  const hex = isColor(value) && value.toString();
1343
- properties.push(hex ? {name, type: 'color', value: '#' + hex.slice(7, 9) + hex.slice(1, 7)} :
1357
+ const property = hex ? {name, type: 'color', value: '#' + hex.slice(7, 9) + hex.slice(1, 7)} :
1344
1358
  isVector2(value) ? {name, type: 'string', value: text(value)} :
1345
1359
  {name, type: typeof value === 'boolean' ? 'bool' : typeof value === 'string' ? 'string' :
1346
- Number.isInteger(defaultValue) ? 'int' : 'float', value});
1360
+ Number.isInteger(defaultValue) ? 'int' : 'float', value};
1361
+
1362
+ // one the map had keeps its place and its kind where the value fits it, a file stays a file, an object an object
1363
+ if (old?.type === 'file' && property.type === 'string' || old?.type === 'object' && property.type === 'int')
1364
+ property.type = old.type;
1365
+ at < 0 ? properties.push(property) : properties.splice(at, 0, property);
1347
1366
  }
1348
1367
  if (properties.length)
1349
1368
  object.properties = properties;
@@ -1429,14 +1448,15 @@ function editorObjectsIn(layer, a, b)
1429
1448
  function editorPickObject(object)
1430
1449
  { editorObjectBrush = [{type: object.type || object.class, properties: editorObjectsCopy(object.properties ?? []), offset: vec2()}]; }
1431
1450
 
1432
- // a new Tiled point object for a brush's object, at a world position, with the map's next id
1433
- function editorNewObject(record, {type, properties}, pos)
1451
+ // a new Tiled object for a brush's object, at a world position, with the map's next id: a copy of the object it was
1452
+ // copied from, its name, size, turn and the rest kept, or a point object of the brush's type and properties
1453
+ function editorNewObject(record, {type, properties, object}, pos)
1434
1454
  {
1435
- const object = {id: editorNextObjectId(record.map), name: '', type, point: true, rotation: 0, visible: true,
1436
- width: 0, height: 0, x: 0, y: 0};
1437
- properties.length && (object.properties = editorObjectsCopy(properties));
1438
- editorObjectSetPos(record, object, pos);
1439
- return object;
1455
+ const made = object ? editorObjectsCopy(object) : {id: 0, name: '', type, point: true, rotation: 0, visible: true,
1456
+ width: 0, height: 0, x: 0, y: 0, ...(properties.length && {properties: editorObjectsCopy(properties)})};
1457
+ made.id = editorNextObjectId(record.map);
1458
+ editorObjectSetPos(record, made, pos);
1459
+ return made;
1440
1460
  }
1441
1461
 
1442
1462
  // place an object brush's objects at a position plus each one's offset, as one undo
@@ -1467,8 +1487,8 @@ function editorDeleteObjects()
1467
1487
  return true;
1468
1488
  }
1469
1489
 
1470
- // the selected objects into the brush, their offsets from the lowest corner of their positions, and the selection
1471
- // cleared so the next click places them
1490
+ // the selected objects into the brush, each a copy with its offset from the lowest corner of their positions, and
1491
+ // the selection cleared so the next click places them
1472
1492
  function editorCopyObjects()
1473
1493
  {
1474
1494
  const objects = editorSelectedObjects();
@@ -1476,7 +1496,7 @@ function editorCopyObjects()
1476
1496
  const positions = objects.map((object)=> editorObjectPos(editorObjectLayer.record, object));
1477
1497
  const low = vec2(min(...positions.map((p)=> p.x)), min(...positions.map((p)=> p.y)));
1478
1498
  editorObjectBrush = editorObjectClipboard = objects.map((object, i)=> ({type: object.type || object.class,
1479
- properties: editorObjectsCopy(object.properties ?? []), offset: positions[i].subtract(low)}));
1499
+ offset: positions[i].subtract(low), object: editorObjectsCopy(object)}));
1480
1500
  editorObjectSelection.clear();
1481
1501
  return true;
1482
1502
  }
@@ -1612,12 +1632,8 @@ function editorCellClamped(live, worldPos)
1612
1632
  function editorArea(a, b)
1613
1633
  { return {min: vec2(min(a.x, b.x), min(a.y, b.y)), max: vec2(max(a.x, b.x), max(a.y, b.y))}; }
1614
1634
 
1615
- // the tile info a layer draws a tile with, as TileLayer.drawTileData picks it
1616
- function editorTileInfo(live, tile)
1617
- {
1618
- const t = live.tileInfo;
1619
- return t && (t.columns ? t.frame(tile) : t.index(tile));
1620
- }
1635
+ // the tile info a layer draws a tile with, as TileLayer.drawTileData picks it, undefined past the end of its sheet
1636
+ function editorTileInfo(live, tile) { return tileLayerTileInfo(live.tileInfo, tile); }
1621
1637
 
1622
1638
  ///////////////////////////////////////////////////////////////////////////////
1623
1639
  // stamps
@@ -1848,8 +1864,8 @@ function editorClear()
1848
1864
  editorStrokeEnd();
1849
1865
  }
1850
1866
 
1851
- // the objects of a map's object layers inside a tile area, each {group, type, properties, offset}: the index of its
1852
- // object layer, and its offset from the area's bottom left corner, as a stamp keeps them
1867
+ // the objects of a map's object layers inside a tile area, each {group, type, offset, object}: the index of its
1868
+ // object layer, its offset from the area's bottom left corner, and a copy of it, as a stamp keeps them
1853
1869
  function editorAreaObjects(layer, area)
1854
1870
  {
1855
1871
  const corner = layer.live.pos.add(area.min), far = layer.live.pos.add(area.max).add(vec2(1));
@@ -1857,8 +1873,8 @@ function editorAreaObjects(layer, area)
1857
1873
  {
1858
1874
  const ids = new Set(editorObjectsIn(objectLayer, corner, far));
1859
1875
  return (objectLayer.group?.objects ?? []).filter((object)=> ids.has(object.id)).map((object)=>
1860
- ({group, type: object.type || object.class, properties: editorObjectsCopy(object.properties ?? []),
1861
- offset: editorObjectPos(layer.record, object).subtract(corner)}));
1876
+ ({group, type: object.type || object.class, offset: editorObjectPos(layer.record, object).subtract(corner),
1877
+ object: editorObjectsCopy(object)}));
1862
1878
  });
1863
1879
  }
1864
1880
 
@@ -1946,12 +1962,13 @@ function editorSelectionDragTo(cell)
1946
1962
  from.arrayCheck(target.live.size) && editorPaint(target, from, 0);
1947
1963
  }
1948
1964
  editorPaintStamp(layer, editorSelection.min, stamp);
1965
+ // whole cells added to the places as written, as an object drag does, so they come back to the digit
1966
+ const {tilewidth=1, tileheight=1} = layer.record.map;
1949
1967
  for (const {objectLayer, ids} of drag.objects)
1950
1968
  editorChangeObjects(objectLayer, (list)=>
1951
1969
  {
1952
1970
  for (const object of list)
1953
- ids.has(object.id) && editorObjectSetPos(layer.record, object,
1954
- editorObjectPos(layer.record, object).add(delta));
1971
+ ids.has(object.id) && (object.x += delta.x * tilewidth, object.y -= delta.y * tileheight);
1955
1972
  });
1956
1973
  });
1957
1974
  editorStroke && editorRedraw(editorStroke);
@@ -2181,8 +2198,18 @@ function editorPaletteTiles(layer)
2181
2198
  const live = layer?.live, image = live?.tileInfo?.textureInfo?.image, list = levelEditor.paletteTiles;
2182
2199
  if (list && live)
2183
2200
  {
2184
- if (layer.palette?.list !== list)
2185
- layer.palette = {list, tiles: list.map((tile)=> ({tile, tileInfo: editorTileInfo(live, tile)}))};
2201
+ if (layer.palette?.list !== list || layer.palette.count !== live.tileInfo?.tiles?.length)
2202
+ layer.palette = {list, count: live.tileInfo?.tiles?.length, // a tile set fills in as it loads
2203
+ tiles: list.map((tile)=> ({tile, tileInfo: editorTileInfo(live, tile)}))
2204
+ .filter((t)=> t.tileInfo || !live.tileInfo)}; // a tile set, or a sheet, has only so many
2205
+ return layer.palette.tiles;
2206
+ }
2207
+ // a tile set lists its tiles, each a tile the palette offers
2208
+ const set = live?.tileInfo?.tiles;
2209
+ if (set)
2210
+ {
2211
+ if (layer.palette?.set !== set || layer.palette.tiles.length !== set.length)
2212
+ layer.palette = {set, tiles: set.map((tileInfo, tile)=> ({tile, tileInfo}))};
2186
2213
  return layer.palette.tiles;
2187
2214
  }
2188
2215
  if (!image) return [];
@@ -2210,7 +2237,7 @@ function editorPaletteTiles(layer)
2210
2237
  size.y).data.buffer);
2211
2238
  return pixels.every((p)=> p === pixels[0]);
2212
2239
  };
2213
- while (tiles.length > 1 && blank(tiles.at(-1)))
2240
+ while (tiles.length > 1 && blank(tiles[tiles.length - 1]))
2214
2241
  tiles.pop();
2215
2242
  }
2216
2243
  catch {}
@@ -2252,7 +2279,7 @@ function editorPaletteDrawObjects(canvas)
2252
2279
  function editorPaletteDraw(canvas, layer)
2253
2280
  {
2254
2281
  const tiles = editorPaletteTiles(layer), cell = editorPaletteCell, columns = editorPaletteColumns;
2255
- const image = layer?.live.tileInfo?.textureInfo?.image, slots = tiles.length + 1;
2282
+ const slots = tiles.length + 1;
2256
2283
  canvas.style.display = tiles.length ? '' : 'none';
2257
2284
  canvas.width = columns * cell;
2258
2285
  canvas.height = ceil(slots / columns) * cell;
@@ -2267,10 +2294,13 @@ function editorPaletteDraw(canvas, layer)
2267
2294
  context.moveTo(8, 8), context.lineTo(cell - 8, cell - 8);
2268
2295
  context.moveTo(cell - 8, 8), context.lineTo(8, cell - 8);
2269
2296
  context.stroke();
2270
- tiles.forEach(({tileInfo: {pos, size}}, i)=>
2297
+ tiles.forEach(({tileInfo}, i)=>
2271
2298
  {
2272
- const slot = i + 1, x = slot % columns * cell, y = (slot / columns | 0) * cell;
2273
- context.drawImage(image, pos.x, pos.y, size.x, size.y, x + 2, y + 2, cell - 4, cell - 4);
2299
+ if (!tileInfo) return; // headless, a layer with no sheet
2300
+ const {pos, size, textureInfo} = tileInfo;
2301
+ // each from its own sheet, a tile set's may be on several
2302
+ const slot = i + 1, x = slot % columns * cell, y = (slot / columns | 0) * cell, image = textureInfo?.image;
2303
+ image && context.drawImage(image, pos.x, pos.y, size.x, size.y, x + 2, y + 2, cell - 4, cell - 4);
2274
2304
  });
2275
2305
 
2276
2306
  // the brush's slot outlined, when it is one tile the palette shows or Erase
@@ -2563,7 +2593,7 @@ function editorPropertiesUpdate(box)
2563
2593
  input.type = 'number';
2564
2594
  input.step = Number.isInteger(defaultValue) ? '1' : 'any';
2565
2595
  input.value = String(value);
2566
- input.onchange = ()=> { const v = parseFloat(input.value); isNumber(v) && set(v); };
2596
+ input.onchange = ()=> { const v = parseFloat(input.value); isFinite(v) && set(v); }; // 1e999 is not one
2567
2597
  }
2568
2598
  else if (typeof defaultValue === 'string')
2569
2599
  {
@@ -2585,7 +2615,7 @@ function editorPropertiesUpdate(box)
2585
2615
  {
2586
2616
  // both checked before a vector is made of them, an emptied or half typed field shows its value again
2587
2617
  const px = parseFloat(input.value), py = parseFloat(y.value);
2588
- if (isNumber(px) && isNumber(py))
2618
+ if (isFinite(px) && isFinite(py))
2589
2619
  set(vec2(px, py));
2590
2620
  else
2591
2621
  input.value = String(value.x), y.value = String(value.y);
@@ -2858,8 +2888,9 @@ function editorRender()
2858
2888
  for (let y = y0; y < y1; ++y)
2859
2889
  {
2860
2890
  const t = editorGidToTile(source.data[x + (height - 1 - y) * width]);
2861
- if (t && live.getData(vec2(x, y)).tile === undefined)
2862
- drawTile(live.pos.add(vec2(x + .5, y + .5)), vec2(1), editorTileInfo(live, t.tile), ghost,
2891
+ const tileInfo = t && editorTileInfo(live, t.tile);
2892
+ if (tileInfo && live.getData(vec2(x, y)).tile === undefined)
2893
+ drawTile(live.pos.add(vec2(x + .5, y + .5)), vec2(1), tileInfo, ghost,
2863
2894
  t.direction * PI/2, t.mirror, undefined, glEnable, false);
2864
2895
  }
2865
2896
 
@@ -2898,8 +2929,9 @@ function editorRender()
2898
2929
  {
2899
2930
  const gid = grid[x + y * w], t = editorGidToTile(gid);
2900
2931
  const center = live.pos.add(editorHover).add(vec2(x + .5, y + .5));
2901
- if (t)
2902
- drawTile(center, vec2(1), editorTileInfo(live, t.tile), hsl(0, 0, 1, .7), t.direction * PI/2, t.mirror, undefined, glEnable, false);
2932
+ const tileInfo = t && editorTileInfo(live, t.tile);
2933
+ if (tileInfo)
2934
+ drawTile(center, vec2(1), tileInfo, hsl(0, 0, 1, .7), t.direction * PI/2, t.mirror, undefined, glEnable, false);
2903
2935
  else if (gid === 0)
2904
2936
  drawRect(center, vec2(1), hsl(0, 1, .5, .3), undefined, undefined, false);
2905
2937
  }
@@ -113,6 +113,7 @@ export
113
113
  touchGamepadVibration,
114
114
  vibrateEnable,
115
115
  soundEnable,
116
+ soundIgnoreSilentSwitch,
116
117
  soundVolume,
117
118
  soundDefaultRange,
118
119
  soundDefaultTaper,
@@ -175,6 +176,7 @@ export
175
176
  setTouchGamepadVibration,
176
177
  setVibrateEnable,
177
178
  setSoundEnable,
179
+ setSoundIgnoreSilentSwitch,
178
180
  setSoundVolume,
179
181
  setSoundDefaultRange,
180
182
  setSoundDefaultTaper,
@@ -586,6 +586,9 @@ function inputInit()
586
586
  if (soundEnable && !headlessMode && audioContext && !audioIsRunning())
587
587
  audioContext.resume();
588
588
 
589
+ // a press in the bars around a letterboxed canvas is not one on its edge; a drag out of it still moves
590
+ if (!inCanvas(e.x, e.y))
591
+ return;
589
592
  inputData[0][e.button] = 3;
590
593
 
591
594
  const mousePosScreenLast = mousePosScreen;
@@ -739,8 +742,8 @@ function inputInit()
739
742
  }
740
743
  else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
741
744
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
742
- else if (pressTouch)
743
- inputData[0][button] = 3;
745
+ else if (pressTouch && inCanvas(pressTouch.clientX, pressTouch.clientY))
746
+ inputData[0][button] = 3; // a tap in the bars around the canvas is not a press
744
747
  // the finger left after a pinch moves the mouse from where it is, with no jump and no press
745
748
  inputTouchIdentifier = pinching ? undefined : touch.identifier;
746
749
  }
@@ -771,6 +774,13 @@ function inputInit()
771
774
 
772
775
  }
773
776
 
777
+ // is a point of the window on the canvas
778
+ function inCanvas(x, y)
779
+ {
780
+ const rect = mainCanvas.getBoundingClientRect();
781
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
782
+ }
783
+
774
784
  // convert a mouse or touch event position to screen space
775
785
  function mouseEventToScreen(mousePos)
776
786
  {
@@ -941,7 +951,7 @@ function inputUpdate()
941
951
  const centered = gamepadAxisCentered[i] ?? (gamepadAxisCentered[i] = []);
942
952
  const readAxis = (j)=>
943
953
  {
944
- const v = gamepad.axes[j];
954
+ const v = isFinite(gamepad.axes[j]) ? gamepad.axes[j] : 0; // a broken axis reads as centered
945
955
  if (isStandard && j < 4)
946
956
  return v; // spec guarantees axes 0-3 are the two sticks
947
957
  if (!gamepadAxisFilterEnable)
@@ -973,6 +983,10 @@ function inputUpdate()
973
983
  if (button.pressed && (!button.value || button.value > .9))
974
984
  hadInput = true;
975
985
  }
986
+
987
+ // a different gamepad in the same slot with fewer buttons or sticks holds none of the last one's
988
+ data.length = buttonsLast.length = gamepad.buttons.length;
989
+ sticks.length = gamepad.axes.length >> 1;
976
990
 
977
991
  // set new primary gamepad if current is not connected
978
992
  if (hadInput)
package/src/engineMath.js CHANGED
@@ -200,11 +200,15 @@ function smoothStep(percent)
200
200
  * @memberof Math */
201
201
  function isPowerOfTwo(value) { return value > 0 && value % 1 === 0 && 2**round(log2(value)) === value; } // any size, not only 32 bits
202
202
 
203
- /** Returns the nearest power of two not less than the value
203
+ /** Returns the nearest whole power of two not less than the value, 1 for a value of 1 or less
204
204
  * @param {number} value
205
205
  * @return {number}
206
206
  * @memberof Math */
207
- function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
207
+ function nearestPowerOfTwo(value)
208
+ {
209
+ const power = 2**ceil(log2(max(value, 1)));
210
+ return power < value ? power * 2 : power; // log2 of a value just above a power can round down to it
211
+ }
208
212
 
209
213
  /** Returns true if two axis aligned bounding boxes are overlapping
210
214
  * this can be used for simple collision detection between objects