littlejsengine 1.18.0 → 1.18.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm test *)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,17 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v5
12
+ - uses: actions/setup-node@v5
13
+ with:
14
+ node-version: '22'
15
+ - run: npm install
16
+ - run: npm run build
17
+ - run: npm test
package/AI.md CHANGED
@@ -155,6 +155,16 @@ drawEllipse(pos, size, color) // filled ellipse
155
155
  npm run build
156
156
  ```
157
157
 
158
+ ### Testing
159
+ ```bash
160
+ npm test
161
+ ```
162
+
163
+ - Tests target `dist/littlejs.esm.js` — rebuild with `npm run build` after changing source.
164
+ - [test/setup.mjs](test/setup.mjs) stubs minimal DOM and enables headless mode. Tests shouldn't call `engineInit`, `render()`, or assume `time` advances.
165
+ - Zero test dependencies — uses Node's built-in `node --test`. Match the style in [test/](test/) when adding new ones.
166
+ - CI runs build + test on every push/PR ([.github/workflows/test.yml](.github/workflows/test.yml)).
167
+
158
168
  ### Debug features
159
169
  - Press `Esc` to toggle debug overlay
160
170
  - Number keys toggle visualizations
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ See @AI.md for instructions.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # LittleJS - The Tiny Fast JavaScript Game Engine
2
2
 
3
- <div align='center'>
3
+ <div align='center' markdown='1'>
4
4
 
5
5
  ![LittleJS Logo](examples/logo.png)
6
6
 
@@ -18,7 +18,11 @@ LittleJS is a fast, lightweight, and fully open source HTML5 game engine designe
18
18
  Its small footprint is packed with a comprehensive feature set including rendering, physics, particles, sound, and input handling.
19
19
  The code is very clean and well documented with many examples to get you started quickly.
20
20
 
21
- <div align='center'>
21
+ ### 🚀 Join the LittleJS Game Jam
22
+
23
+ *The Third Annual LittleJS Game Jam will take place From Oct 2 to Nov 2! Unleash your creativity and develop amazing games using the LittleJS game engine. 🕹️🎮 [Sign up today and get more info about the jam on itch.io!](https://itch.io/jam/littlejs-jam)*
24
+
25
+ <div align='center' markdown='1'>
22
26
 
23
27
  ## [Demos](https://killedbyapixel.github.io/LittleJS/examples) | [Docs](https://killedbyapixel.github.io/LittleJS/docs) | [Trailer](https://youtu.be/chuBzGjv7Ms) | [Discord](https://discord.gg/zb7hcGkyZe) | [FAQ](https://github.com/KilledByAPixel/LittleJS/blob/main/FAQ.md)
24
28
 
@@ -30,14 +34,15 @@ The code is very clean and well documented with many examples to get you started
30
34
 
31
35
  ### ✨ Graphics
32
36
 
33
- - Super fast WebGL2 + Canvas2D hybrid rendering system
37
+ - Blazing fast WebGL2 + Canvas2D hybrid rendering system
34
38
  - Apply [Shadertoy](https://www.shadertoy.com) style shaders for post-processing effects
35
39
  - Robust particle effect system and [effect design tool](https://killedbyapixel.github.io/LittleJS/examples/particles/)
36
40
 
37
41
  ### 🔊 Audio
38
42
 
39
43
  - Sound and music with mp3, ogg, or wave files
40
- - Use [ZzFX](https://killedbyapixel.github.io/ZzFX/) sound effect generator to play sounds without asset files
44
+ - Use [ZzFX](https://killedbyapixel.github.io/ZzFX/) sound generator to play sounds without asset files
45
+ - Spatial audio stereo panning
41
46
 
42
47
  ### 🎮 Input
43
48
 
@@ -53,13 +58,14 @@ The code is very clean and well documented with many examples to get you started
53
58
  ### 🚀 Flexibility
54
59
 
55
60
  - Compatible with all modern web browsers and mobile devices
56
- - TypeScript and Module support with example projects for both
61
+ - Full TypeScript and Module support with example projects for both
57
62
  - Great for size coding competitions like [Js13kGames](https://js13kgames.com/)
58
63
  - Open Source and [MIT licensed](https://github.com/KilledByAPixel/LittleJS/blob/main/LICENSE)
59
64
 
60
65
  ### 🛠️ Developer Tools
61
66
 
62
67
  - Live example browser with code editor
68
+ - Import level editor data from [Tiled](https://github.com/mapeditor/tiled) or other JSON
63
69
  - Debug overlay and primitive rendering system
64
70
  - Medal tracking system with [Newgrounds](https://www.newgrounds.com/) support
65
71
  - Node.js build system
@@ -74,6 +80,8 @@ To get started download the latest LittleJS package from GitHub or install via n
74
80
  - [LittleJS Quick Reference Sheet](https://github.com/KilledByAPixel/LittleJS/blob/main/reference.md) - A reference sheet to help you get started.
75
81
  - [Little JS FAQ](https://github.com/KilledByAPixel/LittleJS/blob/main/FAQ.md) - Answers to common questions about LittleJS.
76
82
  - [JS13k Branch](https://github.com/KilledByAPixel/LittleJS/tree/js13k) - For size coding events like JS13k there is a special branch that builds to a 7KB zip.
83
+ - [LittleJS GPT AI](https://chatgpt.com/g/g-67c7c080b5bc81919736bc8815836be6-make-games-with-littlejs) - Use AI to make games without writing any code!
84
+ - [LittleJS AI Info](https://github.com/KilledByAPixel/LittleJS-AI) - Experiments, games, and templates to improve LittleJS + AI workflows.
77
85
 
78
86
  ## Examples
79
87
 
@@ -82,7 +90,7 @@ LittleJS comes with a several demos both for learning and using as starter proje
82
90
  - [Example Browser](https://killedbyapixel.github.io/LittleJS/examples/) - Live example browser with all examples
83
91
  - [Breakout](https://killedbyapixel.github.io/LittleJS/examples/breakout/) - Block breaking game with post-processing effects
84
92
  - [Puzzle Game](https://killedbyapixel.github.io/LittleJS/examples/puzzle/) - Match 3 puzzle game with HD rendering and high score tracking
85
- - [Platformer](https://killedbyapixel.github.io/LittleJS/examples/platformer/) - Platformer/shooter that loads level data from [Tiled Editor](https://github.com/mapeditor/tiled)
93
+ - [Platformer](https://killedbyapixel.github.io/LittleJS/examples/platformer/) - Platformer/shooter demo that loads level data
86
94
  - [Box2D Demo](https://killedbyapixel.github.io/LittleJS/examples/box2d/) - Box2D plugin demonstration and testbed
87
95
  - [Stress Test](https://killedbyapixel.github.io/LittleJS/examples/stress/) - Sprite rendering benchmark and music system demo
88
96
  - [Particle System Designer](https://killedbyapixel.github.io/LittleJS/examples/particles/) - Particle system editor and visualizer
@@ -117,6 +125,7 @@ Here are a few of the many amazing games created with LittleJS...
117
125
  - [Bug&Bee](https://itch.io/jam/littlejs-game-jam-2025/results) - Low fi shoot em up with co-op gameplay. LittleJS Jam 1st place! by [eoinmcg](https://eoinmcg.itch.io/)
118
126
  - [Little Platformer](https://psemo.itch.io/little-platformer) - Platformer with many mechanics. LittleJS Jam 2st place! by [PSEMO](https://psemo.itch.io/), [Solita666](https://itch.io/profile/solita666), [GabrielRG](https://gabrielrg.itch.io/), [Nate](https://natesassoon.itch.io/)
119
127
  - [Rogue Pong](https://itch.io/jam/littlejs-game-jam-2025/rate/4004165) - Roguelike crossed with classic pong gameplay. LittleJS Jam 3rd place! by [webdevbrian](https://webdevbrian.itch.io/)
128
+ - [Orbitswarm](https://focaccai.itch.io/orbitswarm) - Fast paced space strategy game. by [Focaccai](https://www.focaccai.com/)
120
129
  - [LittleJS Jam 2024 Results](https://itch.io/jam/littlejs-jam-2024/results) - All the games from the first LittleJS Game Jam.
121
130
  - [LittleJS Jam 2025 Results](https://itch.io/jam/littlejs-game-jam-2025/results) - All the games from the second LittleJS Game Jam.
122
131
 
@@ -377,6 +377,12 @@ declare module "littlejsengine" {
377
377
  * @default
378
378
  * @memberof Settings */
379
379
  export let tilesPixelated: boolean;
380
+ /** Scale factor applied to the canvas backing store for native-resolution rendering.
381
+ * Pass 1 for no scaling, a number for an explicit ratio, or undefined to track devicePixelRatio each frame.
382
+ * @type {number|undefined}
383
+ * @default
384
+ * @memberof Settings */
385
+ export let canvasPixelRatio: number | undefined;
380
386
  /** Default font used for text rendering
381
387
  * @type {string}
382
388
  * @default
@@ -474,17 +480,21 @@ declare module "littlejsengine" {
474
480
  export let inputWASDEmulateDirection: boolean;
475
481
  /** True if touch gamepad should appear on mobile devices
476
482
  * - Supports left analog stick, 4 face buttons and start button (button 9)
483
+ * - setTouchGamepadButtonCount(1) to use face buttons as right analog stick
484
+ * - Analog stick buttons 10 and 11 are also activated when virtual sticks are touched
485
+
477
486
  * @type {boolean}
478
487
  * @default
479
488
  * @memberof Settings */
480
489
  export let touchGamepadEnable: boolean;
481
490
  /** True if touch gamepad should have start button in the center
491
+ * - Prevents activating if overlappng with virtual stick or buttons if they are enabled
482
492
  * - When the game is paused, any touch will press the button
483
- * - This can function as a way to pause/unpause the game
484
- * @type {boolean}
493
+ * - Set size to enable the center button
494
+ * @type {number}
485
495
  * @default
486
496
  * @memberof Settings */
487
- export let touchGamepadCenterButton: boolean;
497
+ export let touchGamepadCenterButtonSize: number;
488
498
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
489
499
  * @type {boolean}
490
500
  * @default
@@ -592,6 +602,11 @@ declare module "littlejsengine" {
592
602
  * @param {boolean} pixelated
593
603
  * @memberof Settings */
594
604
  export function setTilesPixelated(pixelated: boolean): void;
605
+ /** Set the canvas pixel ratio.
606
+ * Pass a number for an explicit ratio, or call with no argument to track devicePixelRatio each frame.
607
+ * @param {number} [pixelRatio]
608
+ * @memberof Settings */
609
+ export function setCanvasPixelRatio(pixelRatio?: number): void;
595
610
  /** Set default font used for text rendering
596
611
  * @param {string} font
597
612
  * @memberof Settings */
@@ -676,11 +691,12 @@ declare module "littlejsengine" {
676
691
  * @param {boolean} enable
677
692
  * @memberof Settings */
678
693
  export function setTouchGamepadEnable(enable: boolean): void;
679
- /** True if touch gamepad should have start button in the center
680
- * - This can function as a way to pause/unpause the game
681
- * @param {boolean} enable
694
+ /** Set if touch gamepad should have start button in the center
695
+ * - Set size to enable the center button
696
+ * - When the game is paused, any touch will press the button
697
+ * @param {number} size
682
698
  * @memberof Settings */
683
- export function setTouchGamepadCenterButton(enable: boolean): void;
699
+ export function setTouchGamepadCenterButtonSize(size: number): void;
684
700
  /** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
685
701
  * @param {number} count
686
702
  * @memberof Settings */
@@ -882,6 +898,16 @@ declare module "littlejsengine" {
882
898
  * @return {number}
883
899
  * @memberof Math */
884
900
  export function lerp(valueA: number, valueB: number, percent: number): number;
901
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
902
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
903
+ * @param {number} value
904
+ * @param {number} percentA
905
+ * @param {number} percentB
906
+ * @param {number} lerpA
907
+ * @param {number} lerpB
908
+ * @return {number}
909
+ * @memberof Math */
910
+ export function percentLerp(value: number, percentA: number, percentB: number, lerpA: number, lerpB: number): number;
885
911
  /** Applies smoothstep function to the percentage value
886
912
  * @param {number} percent
887
913
  * @return {number}
@@ -909,6 +935,20 @@ declare module "littlejsengine" {
909
935
  * @return {boolean} - True if intersecting
910
936
  * @memberof Math */
911
937
  export function isIntersecting(start: Vector2, end: Vector2, pos: Vector2, size: Vector2): boolean;
938
+ /**
939
+ * @callback LineTestFunction - Checks if a position is colliding
940
+ * @param {Vector2} pos
941
+ * @memberof Draw
942
+ */
943
+ /**
944
+ * Casts a ray and returns position of the first collision found, or undefined if none are found
945
+ * @param {Vector2} posStart
946
+ * @param {Vector2} posEnd
947
+ * @param {LineTestFunction} testFunction - Check if colliding
948
+ * @param {Vector2} [normal] - Optional vector to store the normal
949
+ * @return {Vector2|undefined} - Position of the collision or undefined if none found
950
+ * @memberof Math */
951
+ export function lineTest(posStart: Vector2, posEnd: Vector2, testFunction: LineTestFunction, normal?: Vector2): Vector2 | undefined;
912
952
  /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
913
953
  * @param {number} [frequency] - Frequency of the wave in Hz
914
954
  * @param {number} [amplitude] - Amplitude (max height) of the wave
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
35
35
  * @type {string}
36
36
  * @default
37
37
  * @memberof Engine */
38
- const engineVersion = '1.18.0';
38
+ const engineVersion = '1.18.2';
39
39
 
40
40
  /** Frames per second to update
41
41
  * @type {number}
@@ -318,12 +318,17 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
318
318
  }
319
319
  else
320
320
  {
321
+ // apply device pixel ratio for crisp rendering
322
+ const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
323
+ const viewWidth = innerWidth * dpr | 0;
324
+ const viewHeight = innerHeight * dpr | 0;
325
+
321
326
  // get main canvas size based on window size
322
- mainCanvasSize.x = min(innerWidth, canvasMaxSize.x);
323
- mainCanvasSize.y = min(innerHeight, canvasMaxSize.y);
327
+ mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
328
+ mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
324
329
 
325
330
  // responsive aspect ratio with native resolution
326
- const innerAspect = innerWidth / innerHeight;
331
+ const innerAspect = viewWidth / viewHeight;
327
332
  ASSERT(canvasMinAspect <= canvasMaxAspect);
328
333
  if (canvasMaxAspect && innerAspect > canvasMaxAspect)
329
334
  {
@@ -337,6 +342,17 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
337
342
  const h = mainCanvasSize.x / canvasMinAspect | 0;
338
343
  mainCanvasSize.y = min(h, canvasMaxSize.y);
339
344
  }
345
+
346
+ // set CSS display size so backing store renders at viewport size
347
+ const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
348
+ const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
349
+ mainCanvas.style.width = cssW;
350
+ mainCanvas.style.height = cssH;
351
+ if (glCanvas)
352
+ {
353
+ glCanvas.style.width = cssW;
354
+ glCanvas.style.height = cssH;
355
+ }
340
356
  }
341
357
 
342
358
  // clear main canvas and set size
@@ -556,8 +572,9 @@ function drawEngineLogo(t)
556
572
 
557
573
  // LittleJS Logo and Splash Screen
558
574
  const x = mainContext;
559
- const w = mainCanvas.width = innerWidth;
560
- const h = mainCanvas.height = innerHeight;
575
+ const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
576
+ const w = mainCanvas.width = innerWidth * dpr;
577
+ const h = mainCanvas.height = innerHeight * dpr;
561
578
  {
562
579
  // background
563
580
  const p3 = percent(t, 1, .8);
@@ -951,6 +968,8 @@ function debugShowErrors()
951
968
 
952
969
  function debugInit()
953
970
  {
971
+ if (showEngineVersion)
972
+ console.warn("LittleJS DEBUG build loaded. Use the release build for production.");
954
973
  }
955
974
 
956
975
  function debugUpdate()
@@ -1732,7 +1751,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1732
1751
  // get ray direction and length
1733
1752
  const dx = posEnd.x - posStart.x;
1734
1753
  const dy = posEnd.y - posStart.y;
1735
- const totalLength = hypot(dx, dy);
1754
+ const totalLength = (dx*dx + dy*dy)**.5;
1736
1755
  if (!totalLength) return;
1737
1756
 
1738
1757
  // current integer cell we are in
@@ -2836,6 +2855,13 @@ let canvasPixelated = false;
2836
2855
  * @memberof Settings */
2837
2856
  let tilesPixelated = true;
2838
2857
 
2858
+ /** Scale factor applied to the canvas backing store for native-resolution rendering.
2859
+ * Pass 1 for no scaling, a number for an explicit ratio, or undefined to track devicePixelRatio each frame.
2860
+ * @type {number|undefined}
2861
+ * @default
2862
+ * @memberof Settings */
2863
+ let canvasPixelRatio = 1;
2864
+
2839
2865
  /** Default font used for text rendering
2840
2866
  * @type {string}
2841
2867
  * @default
@@ -2977,18 +3003,22 @@ let touchInputEnable = true;
2977
3003
 
2978
3004
  /** True if touch gamepad should appear on mobile devices
2979
3005
  * - Supports left analog stick, 4 face buttons and start button (button 9)
3006
+ * - setTouchGamepadButtonCount(1) to use face buttons as right analog stick
3007
+ * - Analog stick buttons 10 and 11 are also activated when virtual sticks are touched
3008
+
2980
3009
  * @type {boolean}
2981
3010
  * @default
2982
3011
  * @memberof Settings */
2983
3012
  let touchGamepadEnable = false;
2984
3013
 
2985
3014
  /** True if touch gamepad should have start button in the center
3015
+ * - Prevents activating if overlappng with virtual stick or buttons if they are enabled
2986
3016
  * - When the game is paused, any touch will press the button
2987
- * - This can function as a way to pause/unpause the game
2988
- * @type {boolean}
3017
+ * - Set size to enable the center button
3018
+ * @type {number}
2989
3019
  * @default
2990
3020
  * @memberof Settings */
2991
- let touchGamepadCenterButton = true;
3021
+ let touchGamepadCenterButtonSize = 300;
2992
3022
 
2993
3023
  /** Number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
2994
3024
  * @type {number}
@@ -3006,7 +3036,7 @@ let touchGamepadAnalog = true;
3006
3036
  * @type {number}
3007
3037
  * @default
3008
3038
  * @memberof Settings */
3009
- let touchGamepadSize = 99;
3039
+ let touchGamepadSize = 100;
3010
3040
 
3011
3041
  /** Transparency of touch gamepad overlay
3012
3042
  * @type {number}
@@ -3148,6 +3178,12 @@ function setCanvasPixelated(pixelated)
3148
3178
  * @memberof Settings */
3149
3179
  function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
3150
3180
 
3181
+ /** Set the canvas pixel ratio.
3182
+ * Pass a number for an explicit ratio, or call with no argument to track devicePixelRatio each frame.
3183
+ * @param {number} [pixelRatio]
3184
+ * @memberof Settings */
3185
+ function setCanvasPixelRatio(pixelRatio) { canvasPixelRatio = pixelRatio; }
3186
+
3151
3187
  /** Set default font used for text rendering
3152
3188
  * @param {string} font
3153
3189
  * @memberof Settings */
@@ -3268,11 +3304,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
3268
3304
  * @memberof Settings */
3269
3305
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
3270
3306
 
3271
- /** True if touch gamepad should have start button in the center
3272
- * - This can function as a way to pause/unpause the game
3273
- * @param {boolean} enable
3307
+ /** Set if touch gamepad should have start button in the center
3308
+ * - Set size to enable the center button
3309
+ * - When the game is paused, any touch will press the button
3310
+ * @param {number} size
3274
3311
  * @memberof Settings */
3275
- function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
3312
+ function setTouchGamepadCenterButtonSize(size) { touchGamepadCenterButtonSize = size; }
3276
3313
 
3277
3314
  /** Set number of buttons on touch gamepad (0-4), if 1 also acts as right analog stick
3278
3315
  * @param {number} count
@@ -3570,10 +3607,10 @@ class EngineObject
3570
3607
  // if already was touching, try to push away
3571
3608
  const deltaPos = oldPos.subtract(o.pos);
3572
3609
  const length = deltaPos.length();
3573
- const pushAwayAccel = .001; // push away if already overlapping
3574
- const velocity = length < .01 ? randVec2(pushAwayAccel) : deltaPos.scale(pushAwayAccel/length);
3610
+ const pushAwayAccel = .001;
3611
+ const velocity = length < .001 ? vec2(0,1) : deltaPos.scale(pushAwayAccel/length);
3575
3612
  this.velocity = this.velocity.add(velocity);
3576
- if (o.mass) // push away if not fixed
3613
+ if (o.mass) // push away other object if not fixed
3577
3614
  o.velocity = o.velocity.subtract(velocity);
3578
3615
 
3579
3616
  debugPhysics && debugOverlap(this.pos, this.size, o.pos, o.size, '#f00');
@@ -3810,6 +3847,7 @@ class EngineObject
3810
3847
  child.parent = this;
3811
3848
  child.localPos = localPos.copy();
3812
3849
  child.localAngle = localAngle;
3850
+ child.updateTransforms();
3813
3851
  return child;
3814
3852
  }
3815
3853
 
@@ -4002,6 +4040,7 @@ function tile(index=new Vector2, size=tileDefaultSize, texture=0, padding=tileDe
4002
4040
  const textureInfo = typeof texture === 'number' ?
4003
4041
  textureInfos[texture] : texture;
4004
4042
  ASSERT(textureInfo instanceof TextureInfo, 'tile texture is not loaded');
4043
+ ASSERT(textureInfo.size.x > 0, 'tile texture is not loaded');
4005
4044
 
4006
4045
  // get the position of the tile
4007
4046
  const sizePaddedX = size.x + padding*2;
@@ -5364,7 +5403,7 @@ function inputInit()
5364
5403
  document.addEventListener('mouseup', onMouseUp);
5365
5404
  document.addEventListener('mousemove', onMouseMove);
5366
5405
  document.addEventListener('mouseleave', onMouseLeave);
5367
- document.addEventListener('wheel', onMouseWheel);
5406
+ document.addEventListener('wheel', onMouseWheel, { passive: false });
5368
5407
  document.addEventListener('contextmenu', onContextMenu);
5369
5408
  document.addEventListener('blur', onBlur);
5370
5409
 
@@ -5383,7 +5422,7 @@ function inputInit()
5383
5422
  }
5384
5423
 
5385
5424
  // try to prevent default browser handling of input
5386
- if (!inputPreventDefault || !document.hasFocus() || !e.cancelable) return;
5425
+ if (!inputPreventDefault || !e.cancelable || !document.hasFocus()) return;
5387
5426
 
5388
5427
  // don't break browser shortcuts
5389
5428
  if (e.ctrlKey || e.metaKey || e.altKey) return;
@@ -5442,7 +5481,7 @@ function inputInit()
5442
5481
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
5443
5482
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5444
5483
 
5445
- if (inputPreventDefault && document.hasFocus() && e.cancelable)
5484
+ if (inputPreventDefault && e.cancelable && document.hasFocus())
5446
5485
  e.preventDefault();
5447
5486
  }
5448
5487
  function onMouseUp(e)
@@ -5464,7 +5503,12 @@ function inputInit()
5464
5503
  mouseDeltaScreen = mouseDeltaScreen.add(movement);
5465
5504
  }
5466
5505
  function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
5467
- function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
5506
+ function onMouseWheel(e)
5507
+ {
5508
+ mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
5509
+ if (inputPreventDefault && e.cancelable && document.hasFocus())
5510
+ e.preventDefault(); // prevent page scrolling
5511
+ }
5468
5512
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
5469
5513
  function onBlur() { inputClear(); } // reset input when focus is lost
5470
5514
 
@@ -5514,7 +5558,7 @@ function inputInit()
5514
5558
  wasTouching = touching;
5515
5559
 
5516
5560
  // prevent default handling like copy, magnifier lens, and scrolling
5517
- if (inputPreventDefault && document.hasFocus() && e.cancelable)
5561
+ if (inputPreventDefault && e.cancelable && document.hasFocus())
5518
5562
  e.preventDefault();
5519
5563
 
5520
5564
  // must return true so the document will get focus
@@ -5535,7 +5579,7 @@ function inputInit()
5535
5579
  if (touching)
5536
5580
  {
5537
5581
  touchGamepadTimer.set();
5538
- if (touchGamepadCenterButton && !wasTouching && paused)
5582
+ if (touchGamepadCenterButtonSize && !wasTouching && paused)
5539
5583
  {
5540
5584
  // touch anywhere to press start when paused
5541
5585
  touchGamepadButtons[9] = 1;
@@ -5560,6 +5604,7 @@ function inputInit()
5560
5604
  // virtual analog stick
5561
5605
  const delta = touchPos.subtract(stickCenter);
5562
5606
  touchGamepadSticks[0] = delta.scale(2/touchGamepadSize).clampLength();
5607
+ touchGamepadButtons[10] = 1; // also press a button when touching stick
5563
5608
  }
5564
5609
  else if (buttonCenter.distance(touchPos) < touchGamepadSize)
5565
5610
  {
@@ -5568,6 +5613,7 @@ function inputInit()
5568
5613
  // virtual right analog stick
5569
5614
  const delta = touchPos.subtract(buttonCenter);
5570
5615
  touchGamepadSticks[1] = delta.scale(2/touchGamepadSize).clampLength();
5616
+ touchGamepadButtons[11] = 1; // also press a button when touching right stick
5571
5617
  }
5572
5618
  // virtual face buttons
5573
5619
  let button = buttonCenter.subtract(touchPos).direction();
@@ -5584,8 +5630,7 @@ function inputInit()
5584
5630
  if (button < touchGamepadButtonCount)
5585
5631
  touchGamepadButtons[button] = 1;
5586
5632
  }
5587
- else if (touchGamepadCenterButton &&
5588
- startCenter.distance(touchPos) < touchGamepadSize)
5633
+ else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize)
5589
5634
  {
5590
5635
  // virtual start button in center
5591
5636
  touchGamepadButtons[9] = 1;
@@ -5634,6 +5679,18 @@ function inputUpdate()
5634
5679
  // update touch gamepad if enabled
5635
5680
  if (touchGamepadEnable && isTouchDevice)
5636
5681
  {
5682
+ if (debugGamepads)
5683
+ {
5684
+ const stickCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5685
+ const buttonCenter = touchGamepadButtonCenter();
5686
+ const startCenter = mainCanvasSize.scale(.5);
5687
+
5688
+ debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
5689
+ debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
5690
+ if (touchGamepadCenterButtonSize)
5691
+ debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
5692
+ }
5693
+
5637
5694
  if (!touchGamepadTimer.isSet()) return;
5638
5695
 
5639
5696
  // read virtual analog stick
@@ -5661,7 +5718,7 @@ function inputUpdate()
5661
5718
 
5662
5719
  // read virtual gamepad buttons
5663
5720
  const data = inputData[1] ?? (inputData[1] = []);
5664
- for (let i=10; i--;)
5721
+ for (let i=12; i--;)
5665
5722
  {
5666
5723
  const wasDown = gamepadIsDown(i,0);
5667
5724
  data[i] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
@@ -5672,7 +5729,13 @@ function inputUpdate()
5672
5729
  }
5673
5730
 
5674
5731
  // return if gamepads are disabled or not supported
5675
- if (!gamepadsEnable || !navigator || !navigator.getGamepads) return;
5732
+ try {
5733
+ // protect against getGamepads disallowed security error
5734
+ if (!gamepadsEnable || !navigator?.getGamepads)
5735
+ return;
5736
+ } catch(e) {
5737
+ return;
5738
+ }
5676
5739
 
5677
5740
  // only poll gamepads when focused or in debug mode
5678
5741
  if (!debug && !document.hasFocus()) return;
@@ -13060,6 +13123,7 @@ export
13060
13123
  canvasFixedSize,
13061
13124
  canvasPixelated,
13062
13125
  tilesPixelated,
13126
+ canvasPixelRatio,
13063
13127
  fontDefault,
13064
13128
  showSplashScreen,
13065
13129
  headlessMode,
@@ -13080,7 +13144,7 @@ export
13080
13144
  gamepadDirectionEmulateStick,
13081
13145
  inputWASDEmulateDirection,
13082
13146
  touchGamepadEnable,
13083
- touchGamepadCenterButton,
13147
+ touchGamepadCenterButtonSize,
13084
13148
  touchGamepadAnalog,
13085
13149
  touchGamepadSize,
13086
13150
  touchGamepadAlpha,
@@ -13106,6 +13170,7 @@ export
13106
13170
  setCanvasFixedSize,
13107
13171
  setCanvasPixelated,
13108
13172
  setTilesPixelated,
13173
+ setCanvasPixelRatio,
13109
13174
  setFontDefault,
13110
13175
  setShowSplashScreen,
13111
13176
  setHeadlessMode,
@@ -13127,7 +13192,7 @@ export
13127
13192
  setGamepadDirectionEmulateStick,
13128
13193
  setInputWASDEmulateDirection,
13129
13194
  setTouchGamepadEnable,
13130
- setTouchGamepadCenterButton,
13195
+ setTouchGamepadCenterButtonSize,
13131
13196
  setTouchGamepadButtonCount,
13132
13197
  setTouchGamepadAnalog,
13133
13198
  setTouchGamepadSize,
@@ -13167,10 +13232,12 @@ export
13167
13232
  distanceAngle,
13168
13233
  lerpAngle,
13169
13234
  lerp,
13235
+ percentLerp,
13170
13236
  smoothStep,
13171
13237
  nearestPowerOfTwo,
13172
13238
  isOverlapping,
13173
13239
  isIntersecting,
13240
+ lineTest,
13174
13241
  oscillate,
13175
13242
 
13176
13243
  // Utilities