like2d 2.11.1 → 2.12.1

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 (62) hide show
  1. package/README.md +33 -5
  2. package/assets/logo-banner-optimized.svg +15 -27
  3. package/assets/logo-banner.svg +76 -132
  4. package/assets/logo-icon.svg +33 -23
  5. package/assets/logo.svg +78 -123
  6. package/dist/engine.d.ts +2 -0
  7. package/dist/engine.js +34 -12
  8. package/dist/events.d.ts +0 -3
  9. package/dist/graphics/canvas.d.ts +15 -7
  10. package/dist/graphics/canvas.js +64 -68
  11. package/dist/graphics/graphics.d.ts +55 -40
  12. package/dist/graphics/graphics.js +100 -72
  13. package/dist/graphics/index.d.ts +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +0 -2
  16. package/dist/input/controllerdb.json +1 -1
  17. package/dist/input/gamepad-mapping.js +2 -1
  18. package/dist/input/gamepad.d.ts +1 -1
  19. package/dist/input/gamepad.js +1 -1
  20. package/dist/input/input.d.ts +2 -2
  21. package/dist/input/input.js +2 -2
  22. package/dist/input/mouse.d.ts +7 -7
  23. package/dist/input/mouse.js +7 -7
  24. package/dist/like.d.ts +48 -10
  25. package/dist/math/rect.d.ts +1 -0
  26. package/dist/math/rect.js +1 -0
  27. package/dist/math/vector2.d.ts +4 -1
  28. package/dist/math/vector2.js +3 -0
  29. package/dist/prefab-scenes/mapGamepad.d.ts +2 -3
  30. package/dist/prefab-scenes/mapGamepad.js +17 -23
  31. package/dist/prefab-scenes/startScreen.d.ts +2 -3
  32. package/dist/prefab-scenes/startScreen.js +41 -12
  33. package/dist/scene.d.ts +49 -7
  34. package/package.json +3 -2
  35. package/dist/__benchmarks__/vector2.bench.d.ts +0 -2
  36. package/dist/__benchmarks__/vector2.bench.d.ts.map +0 -1
  37. package/dist/__benchmarks__/vector2.bench.js +0 -74
  38. package/dist/audio/audio.d.ts.map +0 -1
  39. package/dist/audio/index.d.ts.map +0 -1
  40. package/dist/engine.d.ts.map +0 -1
  41. package/dist/events.d.ts.map +0 -1
  42. package/dist/graphics/canvas.d.ts.map +0 -1
  43. package/dist/graphics/graphics.d.ts.map +0 -1
  44. package/dist/graphics/image.d.ts.map +0 -1
  45. package/dist/graphics/index.d.ts.map +0 -1
  46. package/dist/index.d.ts.map +0 -1
  47. package/dist/input/gamepad-mapping.d.ts.map +0 -1
  48. package/dist/input/gamepad.d.ts.map +0 -1
  49. package/dist/input/index.d.ts.map +0 -1
  50. package/dist/input/input.d.ts.map +0 -1
  51. package/dist/input/keyboard.d.ts.map +0 -1
  52. package/dist/input/mouse.d.ts.map +0 -1
  53. package/dist/like.d.ts.map +0 -1
  54. package/dist/math/index.d.ts.map +0 -1
  55. package/dist/math/rect.d.ts.map +0 -1
  56. package/dist/math/vector2.d.ts.map +0 -1
  57. package/dist/prefab-scenes/index.d.ts.map +0 -1
  58. package/dist/prefab-scenes/mapGamepad.d.ts.map +0 -1
  59. package/dist/prefab-scenes/startScreen.d.ts.map +0 -1
  60. package/dist/scene.d.ts.map +0 -1
  61. package/dist/timer/index.d.ts.map +0 -1
  62. package/dist/timer/timer.d.ts.map +0 -1
package/assets/logo.svg CHANGED
@@ -3,12 +3,15 @@
3
3
 
4
4
  <svg
5
5
  width="100mm"
6
- height="105mm"
7
- viewBox="0 0 100 105"
6
+ height="100mm"
7
+ viewBox="0 0 100 100"
8
8
  version="1.1"
9
9
  id="svg1"
10
10
  inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
11
11
  sodipodi:docname="logo.svg"
12
+ inkscape:export-filename="logo.png"
13
+ inkscape:export-xdpi="203.2"
14
+ inkscape:export-ydpi="203.2"
12
15
  xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
13
16
  xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
14
17
  xmlns="http://www.w3.org/2000/svg"
@@ -24,15 +27,16 @@
24
27
  inkscape:deskcolor="#d1d1d1"
25
28
  inkscape:document-units="mm"
26
29
  showgrid="false"
27
- inkscape:zoom="1.3509661"
28
- inkscape:cx="196.89613"
29
- inkscape:cy="207.62919"
30
+ inkscape:zoom="1.7263173"
31
+ inkscape:cx="202.16446"
32
+ inkscape:cy="214.03945"
30
33
  inkscape:window-width="1864"
31
34
  inkscape:window-height="1163"
32
35
  inkscape:window-x="0"
33
36
  inkscape:window-y="0"
34
37
  inkscape:window-maximized="1"
35
- inkscape:current-layer="layer1">
38
+ inkscape:current-layer="layer1"
39
+ showguides="true">
36
40
  <inkscape:grid
37
41
  id="grid1"
38
42
  units="mm"
@@ -55,134 +59,85 @@
55
59
  inkscape:groupmode="layer"
56
60
  id="layer1">
57
61
  <rect
58
- style="fill:#e48080;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
59
- id="rect6"
60
- width="80"
61
- height="83.543648"
62
- x="10.000003"
63
- y="14.404639" />
62
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
63
+ id="rect8"
64
+ width="67.87429"
65
+ height="22.082184"
66
+ x="16.43322"
67
+ y="69.388191"
68
+ ry="3.2184362" />
69
+ <circle
70
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999999;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
71
+ id="path9"
72
+ cx="49.649223"
73
+ cy="49.477631"
74
+ r="36.714138" />
64
75
  <path
65
76
  id="rect3"
66
- style="fill:#80c3e4;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
67
- d="m 49.999951,4.1791571 -24.73596,24.7359569 -0.01292,0.0129 a 17.500002,17.500002 0 0 0 0,24.74888 17.500002,17.500002 0 0 0 22.243603,2.07688 17.500002,17.500002 0 0 1 0.0052,0.42271 17.500002,17.500002 0 0 1 -17.500224,17.50022 h 40.000139 a 17.500002,17.500002 0 0 1 -17.499707,-17.50022 17.500002,17.500002 0 0 1 0.0052,-0.42271 17.500002,17.500002 0 0 0 22.243597,-2.0774 17.500002,17.500002 0 0 0 -5.1e-4,-24.74887 z" />
77
+ style="fill:#ba2b2b;fill-opacity:1;stroke:#ffcf42;stroke-width:0.5;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
78
+ d="M 49.999942,7.2545716 28.60704,28.647471 l -0.01117,0.01116 a 15.134882,15.134882 0 0 0 0,21.404075 15.134882,15.134882 0 0 0 19.237387,1.796191 15.134882,15.134882 0 0 1 0.0045,0.365581 15.134882,15.134882 0 0 1 -15.135074,15.13507 h 34.594132 a 15.134882,15.134882 0 0 1 -15.134627,-15.13507 15.134882,15.134882 0 0 1 0.0045,-0.365581 15.134882,15.134882 0 0 0 19.237382,-1.79664 15.134882,15.134882 0 0 0 -4.42e-4,-21.404067 z" />
68
79
  <circle
69
- style="fill:none;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
80
+ style="fill:none;stroke:#ffcf42;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
70
81
  id="path1"
71
- cx="-14.90022"
72
- cy="-73.310486"
73
- r="17.500002"
82
+ cx="-15.090682"
83
+ cy="-70.754875"
84
+ r="15.134882"
74
85
  transform="rotate(135)" />
75
86
  <circle
76
- style="fill:none;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
87
+ style="fill:none;stroke:#ffcf42;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
77
88
  id="path1-8"
78
- cx="2.5997834"
79
- cy="-55.810471"
80
- r="17.500002"
89
+ cx="0.04419899"
90
+ cy="-55.619984"
91
+ r="15.134882"
81
92
  transform="rotate(135)" />
82
93
  <circle
83
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
94
+ style="fill:none;fill-opacity:1;stroke:#ffcf42;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
84
95
  id="path1-8-2"
85
- cx="69.999847"
86
- cy="56.176464"
87
- r="17.500002" />
96
+ cx="67.29686"
97
+ cy="52.224457"
98
+ r="15.134882" />
88
99
  <circle
89
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
100
+ style="fill:none;fill-opacity:1;stroke:#ffcf42;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
90
101
  id="path1-8-2-3"
91
- cx="29.999849"
92
- cy="56.176464"
93
- r="17.500002" />
94
- <rect
95
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
96
- id="rect7"
97
- width="21.818182"
98
- height="19.209486"
99
- x="10.000003"
100
- y="78.870033" />
101
- <rect
102
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
103
- id="rect7-0"
104
- width="14.545453"
105
- height="19.209486"
106
- x="31.818186"
107
- y="78.870033" />
108
- <rect
109
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
110
- id="rect7-0-4"
111
- width="21.818182"
112
- height="19.209486"
113
- x="46.36364"
114
- y="78.870033" />
115
- <rect
116
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
117
- id="rect7-9"
118
- width="21.818182"
119
- height="19.209486"
120
- x="68.181816"
121
- y="78.870033" />
122
- <rect
123
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
124
- id="rect8"
125
- width="7.272728"
126
- height="11.408763"
127
- x="24.545458"
128
- y="78.870033" />
129
- <rect
130
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
131
- id="rect9"
132
- width="3.6363642"
133
- height="7.8007298"
134
- x="31.818186"
135
- y="86.378426" />
136
- <rect
137
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
138
- id="rect9-0"
139
- width="3.6363642"
140
- height="7.8007298"
141
- x="42.727276"
142
- y="86.378433" />
143
- <rect
144
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
145
- id="rect11"
146
- width="7.272727"
147
- height="3.6080317"
148
- x="31.818186"
149
- y="78.870033" />
150
- <rect
151
- style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
152
- id="rect11-4"
153
- width="7.272727"
154
- height="3.6080317"
155
- x="39.090912"
156
- y="78.870033" />
157
- <path
158
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
159
- d="m 68.181821,90.278798 -7.272727,-3.900366 v 0 l 7.272726,-7.508396 z"
160
- id="path11"
161
- sodipodi:nodetypes="ccccc" />
162
- <rect
163
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
164
- id="rect12-9"
165
- width="6.9679103"
166
- height="3.9003553"
167
- x="53.788776"
168
- y="94.179153" />
169
- <rect
170
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
171
- id="rect12-9-9"
172
- width="6.9679103"
173
- height="3.9003553"
174
- x="82.727272"
175
- y="82.624237" />
176
- <rect
177
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
178
- id="rect12-9-9-9"
179
- width="6.9679103"
180
- height="3.9003553"
181
- x="82.727272"
182
- y="90.278801" />
183
- <path
184
- style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999997;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
185
- d="m 53.636366,86.378432 0.15241,-7.508396 h 7.120316 z"
186
- id="path12" />
102
+ cx="32.702847"
103
+ cy="52.224457"
104
+ r="15.134882" />
105
+ <g
106
+ id="g8"
107
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-linejoin:miter;stroke-opacity:1;stroke-width:0.5;stroke-dasharray:none"
108
+ transform="translate(0,1.0583333)">
109
+ <path
110
+ id="rect2"
111
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
112
+ d="M 15.406292,74.466773 V 91.080229 H 28.446323 V 84.333871 H 22.864751 V 74.466773 Z"
113
+ sodipodi:nodetypes="ccccccc" />
114
+ <path
115
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
116
+ d="m 33.048199,75.699528 v 3.500747 h 2.341044 v 5.04483 h -2.341044 v 6.83512 h 11.48336 v -6.83512 h -2.342303 v -5.04483 h 2.342303 v -3.500747 h -5.729088 -0.01385 z"
117
+ id="path7"
118
+ sodipodi:nodetypes="ccccccccccccccc" />
119
+ <path
120
+ id="rect7-9"
121
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
122
+ d="m 69.67956,74.516469 v 16.613457 h 15.65488 v -6.019272 h -5.061194 v -2.314587 h 5.061194 v -3.24683 h -5.061194 v -2.315104 h 5.061194 v -2.717664 z"
123
+ sodipodi:nodetypes="ccccccccccccc" />
124
+ <path
125
+ id="rect5"
126
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
127
+ d="m 49.204699,73.458136 v 17.671793 h 5.69836 v -2.315107 h 5.34696 v 2.315107 h 5.69836 c 0,0 0.486363,-6.903304 -1.021347,-8.571792 -1.610295,-1.782013 -2.956804,-2.064783 -2.956804,-2.064783 l 4.122579,-5.976885 h -6.317526 l -2.974663,5.947607 -2.014865,0.01689 -0.0035,-7.022831 z"
128
+ sodipodi:nodetypes="cccccccsccccccc" />
129
+ <circle
130
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
131
+ id="path8"
132
+ cx="35.389244"
133
+ cy="71.791946"
134
+ r="2.7943003" />
135
+ <circle
136
+ style="fill:#ffcf42;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
137
+ id="path8-6"
138
+ cx="42.119633"
139
+ cy="71.791946"
140
+ r="2.7943003" />
141
+ </g>
187
142
  </g>
188
143
  </svg>
package/dist/engine.d.ts CHANGED
@@ -17,11 +17,13 @@ export declare class Engine {
17
17
  private isRunning;
18
18
  private lastTime;
19
19
  private abort;
20
+ private sceneStack;
20
21
  /**
21
22
  * The Like interface providing access to all engine subsystems.
22
23
  */
23
24
  readonly like: Like;
24
25
  constructor(container: HTMLElement);
26
+ private refreshScene;
25
27
  private dispatch;
26
28
  /**
27
29
  * Start the game loop.
package/dist/engine.js CHANGED
@@ -42,6 +42,12 @@ export class Engine {
42
42
  writable: true,
43
43
  value: new AbortController()
44
44
  });
45
+ Object.defineProperty(this, "sceneStack", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: []
50
+ });
45
51
  /**
46
52
  * The Like interface providing access to all engine subsystems.
47
53
  */
@@ -53,16 +59,13 @@ export class Engine {
53
59
  });
54
60
  this.canvas = document.createElement('canvas');
55
61
  const canvas = new Canvas(this.canvas, this.dispatch.bind(this), this.abort.signal);
56
- this.canvas.addEventListener("like:updateRenderTarget", (event) => {
57
- this.like.gfx.setContext(event.detail.target.getContext("2d"));
58
- });
59
62
  this.container.appendChild(this.canvas);
60
63
  const props = {
61
64
  canvas: this.canvas,
62
65
  dispatch: this.dispatch.bind(this),
63
66
  abort: this.abort.signal,
64
67
  };
65
- const gfx = new Graphics(this.canvas.getContext('2d'));
68
+ const gfx = new Graphics(canvas.getContext());
66
69
  const audio = new Audio();
67
70
  const timer = new Timer(props);
68
71
  const keyboard = new Keyboard(props);
@@ -80,15 +83,22 @@ export class Engine {
80
83
  canvas,
81
84
  start: this.start.bind(this),
82
85
  dispose: this.dispose.bind(this),
86
+ getScene: (pos = -1) => {
87
+ return this.sceneStack.at(pos);
88
+ },
89
+ pushScene: (scene, _overlay) => {
90
+ this.sceneStack.push(scene);
91
+ this.refreshScene();
92
+ },
93
+ popScene: () => {
94
+ const s = this.sceneStack.pop();
95
+ this.refreshScene();
96
+ return s;
97
+ },
83
98
  setScene: (scene) => {
84
- if (scene) {
85
- this.like.handleEvent = (event) => sceneDispatch(scene, this.like, event);
86
- if (this.isRunning)
87
- this.dispatch("load", []);
88
- }
89
- else {
90
- this.like.handleEvent = undefined;
91
- }
99
+ const idx = Math.max(0, this.sceneStack.length - 1);
100
+ this.sceneStack[idx] = scene;
101
+ this.refreshScene();
92
102
  },
93
103
  callOwnHandlers: (event) => {
94
104
  if (event.type in this.like)
@@ -99,6 +109,18 @@ export class Engine {
99
109
  window.addEventListener('blur', () => this.dispatch('blur', ['tab']));
100
110
  this.canvas.addEventListener('focus', () => this.dispatch('focus', ['canvas']));
101
111
  }
112
+ refreshScene() {
113
+ const topScene = this.sceneStack.at(-1);
114
+ if (topScene) {
115
+ this.like.handleEvent = (event) => sceneDispatch(topScene, this.like, event);
116
+ if (this.isRunning) {
117
+ this.dispatch("load", []);
118
+ }
119
+ }
120
+ else {
121
+ this.like.handleEvent = undefined;
122
+ }
123
+ }
102
124
  dispatch(type, args) {
103
125
  const event = { type, args, timestamp: this.like.timer.getTime() };
104
126
  if (this.like.handleEvent) {
package/dist/events.d.ts CHANGED
@@ -11,9 +11,6 @@ export type LikeCanvasEventMap = HTMLElementEventMap & {
11
11
  pos: Vector2;
12
12
  delta: Vector2;
13
13
  }>;
14
- 'like:updateRenderTarget': CustomEvent<{
15
- target: HTMLCanvasElement;
16
- }>;
17
14
  'like:resizeCanvas': CustomEvent<{
18
15
  size: Vector2;
19
16
  }>;
@@ -12,21 +12,28 @@ export type CanvasSize = Vector2 | 'native';
12
12
  * A manager for the HTML canvas element, similar to `love.window`.
13
13
  *
14
14
  * Controls game size / scaling -- both native and pixelart mode via {@link Canvas.setMode}, as well as fullscreen functions.
15
+ *
16
+ * The canvas keeps two canvases: render and display. Each frame, it copies render to display before the canvas is presented.
17
+ * This allows for pixel-accurate scaling.
15
18
  */
16
19
  export declare class Canvas {
17
20
  /** The ultimately visible canvas in the browser */
18
21
  private displayCanvas;
19
22
  private dispatch;
20
23
  private abort;
21
- /** The canvas that we're drawing to with `like.gfx` functions.
22
- * If it's the same as displayCanvas, we're in native mode.
23
- * Otherwise, we're in pixelart mode, consisting of nearest -> linear scaling.
24
- */
24
+ /** The canvas that we're drawing to with `like.gfx` functions. */
25
25
  private renderCanvas;
26
26
  private resizeTimeoutId;
27
+ private isNativeMode;
27
28
  constructor(
28
29
  /** The ultimately visible canvas in the browser */
29
30
  displayCanvas: LikeCanvasElement, dispatch: Dispatcher<'resize'>, abort: AbortSignal);
31
+ /**
32
+ * Get the canvas that graphics functions render to.
33
+ * This is separate from the display canvas; it is
34
+ * not visibly exposed but rather copied each frame.
35
+ */
36
+ getContext(): CanvasRenderingContext2D;
30
37
  /** Get a unified canvas info object. */
31
38
  getMode(): {
32
39
  size: Vector2;
@@ -57,14 +64,15 @@ export declare class Canvas {
57
64
  private getDisplayPixelSize;
58
65
  /** Are we fullscreen? */
59
66
  getFullscreen(): boolean;
67
+ /** Does the canvas have focus? */
68
+ hasFocus(): boolean;
60
69
  /** Set fullscreen. */
61
70
  setFullscreen(fullscreen: boolean): void;
62
71
  /**
63
- * Called internally by the engine before
64
- * rendering a frame.
72
+ * Trigered by `like:preDraw`
65
73
  */
66
74
  private preDraw;
67
- /** Called every frame by the engine after drawing */
75
+ /** Triggered by `like:postDraw` */
68
76
  private postDraw;
69
77
  /** @returns if size was changed. */
70
78
  static setCanvasElemSize(canvas: LikeCanvasElement, newSize: Vector2): boolean;
@@ -4,6 +4,9 @@ import { Vec2 } from "../math/vector2";
4
4
  * A manager for the HTML canvas element, similar to `love.window`.
5
5
  *
6
6
  * Controls game size / scaling -- both native and pixelart mode via {@link Canvas.setMode}, as well as fullscreen functions.
7
+ *
8
+ * The canvas keeps two canvases: render and display. Each frame, it copies render to display before the canvas is presented.
9
+ * This allows for pixel-accurate scaling.
7
10
  */
8
11
  export class Canvas {
9
12
  constructor(
@@ -27,10 +30,7 @@ export class Canvas {
27
30
  writable: true,
28
31
  value: abort
29
32
  });
30
- /** The canvas that we're drawing to with `like.gfx` functions.
31
- * If it's the same as displayCanvas, we're in native mode.
32
- * Otherwise, we're in pixelart mode, consisting of nearest -> linear scaling.
33
- */
33
+ /** The canvas that we're drawing to with `like.gfx` functions. */
34
34
  Object.defineProperty(this, "renderCanvas", {
35
35
  enumerable: true,
36
36
  configurable: true,
@@ -43,42 +43,39 @@ export class Canvas {
43
43
  writable: true,
44
44
  value: 0
45
45
  });
46
+ Object.defineProperty(this, "isNativeMode", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: true
51
+ });
46
52
  displayCanvas.tabIndex = 0;
47
53
  displayCanvas.style.width = '100%';
48
54
  displayCanvas.style.height = '100%';
49
- this.renderCanvas = this.displayCanvas;
55
+ displayCanvas.style.objectFit = 'contain';
56
+ // Always create a separate render canvas
57
+ this.renderCanvas = document.createElement('canvas');
50
58
  this.setMode('native');
51
59
  /** Only the canvas can really transform the mouse to the game size.
52
60
  * This hack sends an event for the mouse module to listen to.
53
61
  */
54
62
  this.displayCanvas.addEventListener('mousemove', (ev) => {
55
- let pos;
56
- let delta;
57
63
  const rawPos = [ev.offsetX, ev.offsetY];
58
64
  const rawDelta = [ev.movementX, ev.movementY];
59
- if (this.renderCanvas == this.displayCanvas) {
60
- /* Native mode. */
61
- pos = Vec2.mul(rawPos, window.devicePixelRatio ?? 1);
62
- delta = Vec2.mul(rawDelta, window.devicePixelRatio ?? 1);
63
- }
64
- else {
65
- /* Pixelart mode. This math simulates object-fit: contain,
66
- * which preserves aspect ratio.
67
- */
68
- const csize = [
69
- this.displayCanvas.clientWidth,
70
- this.displayCanvas.clientHeight
71
- ];
72
- /* Scale of both dimensions */
73
- const scale = calcAspectFriendlyScale(this.getSize(), csize);
74
- /* Upper-left corner */
75
- const offset = Vec2.div(Vec2.sub(csize, Vec2.mul(this.getSize(), scale)), 2);
76
- pos = Vec2.div(Vec2.sub(rawPos, offset), scale);
77
- delta = Vec2.div(rawDelta, scale);
78
- /* Only handle mousemove events that are in bounds. */
79
- if (!Rect.containsPoint(this.getRect(), pos)) {
80
- return;
81
- }
65
+ /* Recreation of object-fit */
66
+ const csize = [
67
+ this.displayCanvas.clientWidth,
68
+ this.displayCanvas.clientHeight
69
+ ];
70
+ /* Scale of both dimensions */
71
+ const scale = calcAspectFriendlyScale(this.getSize(), csize);
72
+ /* Upper-left corner */
73
+ const offset = Vec2.div(Vec2.sub(csize, Vec2.mul(this.getSize(), scale)), 2);
74
+ const pos = Vec2.div(Vec2.sub(rawPos, offset), scale);
75
+ const delta = Vec2.div(rawDelta, scale);
76
+ /* Only handle mousemove events that are in bounds. */
77
+ if (!Rect.containsPoint(this.getRect(), pos)) {
78
+ return;
82
79
  }
83
80
  this.displayCanvas.dispatchEvent(new CustomEvent('like:mousemoved', {
84
81
  detail: {
@@ -90,6 +87,14 @@ export class Canvas {
90
87
  this.displayCanvas.addEventListener("like:preDraw", this.preDraw.bind(this), { signal: this.abort });
91
88
  this.displayCanvas.addEventListener("like:postDraw", this.postDraw.bind(this), { signal: this.abort });
92
89
  }
90
+ /**
91
+ * Get the canvas that graphics functions render to.
92
+ * This is separate from the display canvas; it is
93
+ * not visibly exposed but rather copied each frame.
94
+ */
95
+ getContext() {
96
+ return this.renderCanvas.getContext('2d');
97
+ }
93
98
  /** Get a unified canvas info object. */
94
99
  getMode() {
95
100
  return {
@@ -115,27 +120,13 @@ export class Canvas {
115
120
  * @param flags optional options.
116
121
  */
117
122
  setMode(size, flags = {}) {
118
- // set up sizing / render target
119
- const prevRenderCanvas = this.renderCanvas;
120
- if (size == 'native') {
121
- this.displayCanvas.style.objectFit = 'fill';
122
- this.renderCanvas = this.displayCanvas;
123
- }
124
- else {
125
- this.displayCanvas.style.objectFit = 'contain';
126
- this.renderCanvas = document.createElement('canvas');
123
+ this.isNativeMode = size === 'native';
124
+ if (size !== 'native') {
127
125
  const changed = Canvas.setCanvasElemSize(this.renderCanvas, size);
128
126
  if (changed) {
129
127
  this.dispatchResize(size);
130
128
  }
131
129
  }
132
- if (prevRenderCanvas != this.renderCanvas) {
133
- this.displayCanvas.dispatchEvent(new CustomEvent("like:updateRenderTarget", {
134
- detail: {
135
- target: this.renderCanvas,
136
- },
137
- }));
138
- }
139
130
  if ('fullscreen' in flags) {
140
131
  this.setFullscreen(flags.fullscreen);
141
132
  }
@@ -164,6 +155,10 @@ export class Canvas {
164
155
  getFullscreen() {
165
156
  return this.displayCanvas === document.fullscreenElement;
166
157
  }
158
+ /** Does the canvas have focus? */
159
+ hasFocus() {
160
+ return document.activeElement === this.displayCanvas;
161
+ }
167
162
  /** Set fullscreen. */
168
163
  setFullscreen(fullscreen) {
169
164
  if (fullscreen) {
@@ -177,49 +172,50 @@ export class Canvas {
177
172
  }
178
173
  }
179
174
  /**
180
- * Called internally by the engine before
181
- * rendering a frame.
175
+ * Trigered by `like:preDraw`
182
176
  */
183
177
  preDraw() {
184
- if (this.renderCanvas == this.displayCanvas) {
178
+ if (this.isNativeMode) {
179
+ // In native mode, renderCanvas tracks display size
185
180
  const realSize = this.getDisplayPixelSize();
186
- if ((realSize[0] != this.displayCanvas.width ||
187
- realSize[1] != this.displayCanvas.height) &&
181
+ if ((realSize[0] != this.renderCanvas.width ||
182
+ realSize[1] != this.renderCanvas.height) &&
188
183
  !this.resizeTimeoutId) {
189
184
  /** In native scaling mode, zooming and resizing the window cause us
190
185
  * to set canvas width and height every frame, which could cause
191
186
  * tons of canvas bitmap reallocations. So wait 1/4 second..
192
187
  */
193
- Canvas.setCanvasElemSize(this.displayCanvas, realSize);
188
+ Canvas.setCanvasElemSize(this.renderCanvas, realSize);
194
189
  this.dispatchResize(realSize);
195
190
  this.resizeTimeoutId = setTimeout(() => { this.resizeTimeoutId = 0; }, 250);
196
191
  }
197
192
  }
198
- this.renderCanvas.getContext('2d').resetTransform();
193
+ const ctx = this.renderCanvas.getContext('2d');
194
+ ctx.resetTransform();
195
+ // Enable smoothing in native mode, disable in pixelart mode
196
+ ctx.imageSmoothingEnabled = this.isNativeMode;
199
197
  }
200
- /** Called every frame by the engine after drawing */
198
+ /** Triggered by `like:postDraw` */
201
199
  postDraw() {
202
- if (this.renderCanvas != this.displayCanvas) {
203
- /* We're in pixelart mode,
204
- * so set output canvas size to an ideal integer scale.
205
- * No debounce: changes to integer ratio are infrequent.
206
- */
200
+ // Always blit from render canvas to display canvas
201
+ if (this.isNativeMode) {
202
+ // In native mode, display canvas matches render canvas size
203
+ Canvas.setCanvasElemSize(this.displayCanvas, this.getSize());
204
+ }
205
+ else {
206
+ // In pixelart mode, set output canvas size to an ideal integer scale
207
207
  Canvas.setCanvasElemSize(this.displayCanvas, Vec2.mul(this.getSize(), Math.round(calcAspectFriendlyScale(this.getSize(), this.getDisplayPixelSize()))));
208
- // Copy the internal canvas to the visible one.
209
- const ctx = this.displayCanvas.getContext('2d');
210
- ctx.imageSmoothingEnabled = false;
211
- ctx.drawImage(this.renderCanvas, 0, 0, this.renderCanvas.width, this.renderCanvas.height, 0, 0, this.displayCanvas.width, this.displayCanvas.height);
212
208
  }
209
+ // Copy the render canvas to the visible one
210
+ const displayCtx = this.displayCanvas.getContext('2d');
211
+ displayCtx.imageSmoothingEnabled = false;
212
+ displayCtx.drawImage(this.renderCanvas, 0, 0, this.renderCanvas.width, this.renderCanvas.height, 0, 0, this.displayCanvas.width, this.displayCanvas.height);
213
213
  }
214
214
  /** @returns if size was changed. */
215
215
  static setCanvasElemSize(canvas, newSize) {
216
- const ctx = canvas.getContext('2d');
217
- if (!ctx)
218
- return false;
219
216
  if (canvas.width === newSize[0] && canvas.height === newSize[1])
220
217
  return false;
221
- canvas.width = newSize[0];
222
- canvas.height = newSize[1];
218
+ [canvas.width, canvas.height] = newSize;
223
219
  return true;
224
220
  }
225
221
  static getCanvasElemSize(canvas) {