@starweb-libs/engine 0.0.1 → 0.0.3
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.
- package/LICENSE +21 -21
- package/README.md +28 -28
- package/dist/{assets.d.ts → assets.d.mts} +6 -3
- package/dist/assets.mjs +39 -0
- package/dist/assets.mjs.map +1 -0
- package/dist/canvas.d.mts +36 -0
- package/dist/canvas.mjs +50 -0
- package/dist/canvas.mjs.map +1 -0
- package/dist/input/{keyboard.d.ts → keyboard.d.mts} +9 -6
- package/dist/input/keyboard.mjs +61 -0
- package/dist/input/keyboard.mjs.map +1 -0
- package/dist/input/{pointer.d.ts → pointer.d.mts} +12 -9
- package/dist/input/pointer.mjs +105 -0
- package/dist/input/pointer.mjs.map +1 -0
- package/dist/update.d.mts +37 -0
- package/dist/update.mjs +82 -0
- package/dist/update.mjs.map +1 -0
- package/dist/validate.d.mts +16 -0
- package/dist/validate.mjs +55 -0
- package/dist/validate.mjs.map +1 -0
- package/package.json +56 -44
- package/dist/assets.d.ts.map +0 -1
- package/dist/assets.js +0 -36
- package/dist/assets.js.map +0 -1
- package/dist/canvas.d.ts +0 -31
- package/dist/canvas.d.ts.map +0 -1
- package/dist/canvas.js +0 -45
- package/dist/canvas.js.map +0 -1
- package/dist/input/keyboard.d.ts.map +0 -1
- package/dist/input/keyboard.js +0 -48
- package/dist/input/keyboard.js.map +0 -1
- package/dist/input/pointer.d.ts.map +0 -1
- package/dist/input/pointer.js +0 -91
- package/dist/input/pointer.js.map +0 -1
- package/dist/update.d.ts +0 -34
- package/dist/update.d.ts.map +0 -1
- package/dist/update.js +0 -77
- package/dist/update.js.map +0 -1
- package/dist/validate.d.ts +0 -13
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -61
- package/dist/validate.js.map +0 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Starweb Libraries, Mason L'Etoile
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Starweb Libraries, Mason L'Etoile
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
# Starweb Engine
|
|
2
|
-
|
|
3
|
-
A lightweight 2D game engine for the browser, built with TypeScript and the Web Audio / Canvas APIs.
|
|
4
|
-
|
|
5
|
-
## Tech Stack
|
|
6
|
-
<p align="left">
|
|
7
|
-
<img height="35" src="https://img.shields.io/badge/TypeScript-%23007ACC?logo=typescript&logoColor=white&style=for-the-badge"/>
|
|
8
|
-
<img height="35" src="https://img.shields.io/badge/Web%20Audio%20API-black?logo=webaudio&logoColor=white&style=for-the-badge"/>
|
|
9
|
-
<img height="35" src="https://img.shields.io/badge/Canvas%20API-black?logo=html5&logoColor=white&style=for-the-badge"/>
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
## Modules
|
|
13
|
-
| Module | Description |
|
|
14
|
-
| ------ | ----------- |
|
|
15
|
-
| `canvas` | Fullscreen canvas setup with resize handling |
|
|
16
|
-
| `update` | Fixed or variable timestep game loop |
|
|
17
|
-
| `assets` | Image loading and tinting |
|
|
18
|
-
| `input/keyboard` | Per-frame keyboard state |
|
|
19
|
-
| `input/pointer` | Per-frame pointer/mouse state with canvas scaling |
|
|
20
|
-
| `validate` | JSON validation helpers and error collector |
|
|
21
|
-
|
|
22
|
-
## Installation
|
|
23
|
-
```bash
|
|
24
|
-
npm install github:starweb-libs/engine
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## License
|
|
28
|
-
MIT License - see [LICENSE](./LICENSE) for details.
|
|
1
|
+
# Starweb Engine
|
|
2
|
+
|
|
3
|
+
A lightweight 2D game engine for the browser, built with TypeScript and the Web Audio / Canvas APIs.
|
|
4
|
+
|
|
5
|
+
## Tech Stack
|
|
6
|
+
<p align="left">
|
|
7
|
+
<img height="35" src="https://img.shields.io/badge/TypeScript-%23007ACC?logo=typescript&logoColor=white&style=for-the-badge"/>
|
|
8
|
+
<img height="35" src="https://img.shields.io/badge/Web%20Audio%20API-black?logo=webaudio&logoColor=white&style=for-the-badge"/>
|
|
9
|
+
<img height="35" src="https://img.shields.io/badge/Canvas%20API-black?logo=html5&logoColor=white&style=for-the-badge"/>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
## Modules
|
|
13
|
+
| Module | Description |
|
|
14
|
+
| ------ | ----------- |
|
|
15
|
+
| `canvas` | Fullscreen canvas setup with resize handling |
|
|
16
|
+
| `update` | Fixed or variable timestep game loop |
|
|
17
|
+
| `assets` | Image loading and tinting |
|
|
18
|
+
| `input/keyboard` | Per-frame keyboard state |
|
|
19
|
+
| `input/pointer` | Per-frame pointer/mouse state with canvas scaling |
|
|
20
|
+
| `validate` | JSON validation helpers and error collector |
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
```bash
|
|
24
|
+
npm install github:starweb-libs/engine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
MIT License - see [LICENSE](./LICENSE) for details.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
//#region src/assets.d.ts
|
|
1
2
|
/** Loads an image from the given Path or URL.
|
|
2
3
|
*
|
|
3
4
|
* @param src - Path or URL of the image to load.
|
|
4
5
|
* @returns A promise that resolves with the loaded {@link HTMLImageElement}.
|
|
5
6
|
* @throws {Error} If the image fails to load.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
+
declare function loadImage(src: string): Promise<HTMLImageElement>;
|
|
8
9
|
/** Returns a new offscreen canvas with the source image tinted by the given colour.
|
|
9
10
|
* Non-transparent pixels are filled with `color`; transparency is preserved.
|
|
10
11
|
*
|
|
@@ -13,5 +14,7 @@ export declare function loadImage(src: string): Promise<HTMLImageElement>;
|
|
|
13
14
|
* @returns An offscreen {@link HTMLCanvasElement} with the tinted result.
|
|
14
15
|
* @throws {Error} If a 2D context cannot be obtained.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
-
//#
|
|
17
|
+
declare function tintImage(source: HTMLImageElement, color: string): HTMLCanvasElement;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { loadImage, tintImage };
|
|
20
|
+
//# sourceMappingURL=assets.d.mts.map
|
package/dist/assets.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/assets.ts
|
|
2
|
+
/** Loads an image from the given Path or URL.
|
|
3
|
+
*
|
|
4
|
+
* @param src - Path or URL of the image to load.
|
|
5
|
+
* @returns A promise that resolves with the loaded {@link HTMLImageElement}.
|
|
6
|
+
* @throws {Error} If the image fails to load.
|
|
7
|
+
*/
|
|
8
|
+
function loadImage(src) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const img = new Image();
|
|
11
|
+
img.onload = () => resolve(img);
|
|
12
|
+
img.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load image: ${src}`));
|
|
13
|
+
img.src = src;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** Returns a new offscreen canvas with the source image tinted by the given colour.
|
|
17
|
+
* Non-transparent pixels are filled with `color`; transparency is preserved.
|
|
18
|
+
*
|
|
19
|
+
* @param source - The source image to tint.
|
|
20
|
+
* @param color - Any valid CSS colour string.
|
|
21
|
+
* @returns An offscreen {@link HTMLCanvasElement} with the tinted result.
|
|
22
|
+
* @throws {Error} If a 2D context cannot be obtained.
|
|
23
|
+
*/
|
|
24
|
+
function tintImage(source, color) {
|
|
25
|
+
const off = document.createElement("canvas");
|
|
26
|
+
off.width = source.width;
|
|
27
|
+
off.height = source.height;
|
|
28
|
+
const c = off.getContext("2d");
|
|
29
|
+
if (!c) throw new Error("Failed to get offscreen 2d context");
|
|
30
|
+
c.drawImage(source, 0, 0);
|
|
31
|
+
c.globalCompositeOperation = "source-in";
|
|
32
|
+
c.fillStyle = color;
|
|
33
|
+
c.fillRect(0, 0, off.width, off.height);
|
|
34
|
+
return off;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { loadImage, tintImage };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=assets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.mjs","names":[],"sources":["../src/assets.ts"],"sourcesContent":["/** Loads an image from the given Path or URL.\n *\n * @param src - Path or URL of the image to load.\n * @returns A promise that resolves with the loaded {@link HTMLImageElement}.\n * @throws {Error} If the image fails to load.\n */\nexport function loadImage(src: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const img = new Image();\n img.onload = () => resolve(img);\n img.onerror = () => reject(new Error(`Failed to load image: ${src}`));\n img.src = src;\n });\n}\n\n/** Returns a new offscreen canvas with the source image tinted by the given colour.\n * Non-transparent pixels are filled with `color`; transparency is preserved.\n *\n * @param source - The source image to tint.\n * @param color - Any valid CSS colour string.\n * @returns An offscreen {@link HTMLCanvasElement} with the tinted result.\n * @throws {Error} If a 2D context cannot be obtained.\n */\nexport function tintImage(\n source: HTMLImageElement,\n color: string,\n): HTMLCanvasElement {\n const off = document.createElement(\"canvas\");\n off.width = source.width;\n off.height = source.height;\n const c = off.getContext(\"2d\");\n if (!c) throw new Error(\"Failed to get offscreen 2d context\");\n\n c.drawImage(source, 0, 0);\n c.globalCompositeOperation = \"source-in\";\n c.fillStyle = color;\n c.fillRect(0, 0, off.width, off.height);\n\n return off;\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,UAAU,KAAwC;CAChE,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,MAAM,IAAI,MAAM;EACtB,IAAI,eAAgB,QAAQ,GAAG;EAC/B,IAAI,gBAAgB,uBAAO,IAAI,MAAM,yBAAyB,KAAK,CAAC;EACpE,IAAI,MAAM;CACZ,CAAC;AACH;;;;;;;;;AAUA,SAAgB,UACd,QACA,OACmB;CACnB,MAAM,MAAM,SAAS,cAAc,QAAQ;CAC3C,IAAI,QAAQ,OAAO;CACnB,IAAI,SAAS,OAAO;CACpB,MAAM,IAAI,IAAI,WAAW,IAAI;CAC7B,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,oCAAoC;CAE5D,EAAE,UAAU,QAAQ,GAAG,CAAC;CACxB,EAAE,2BAA2B;CAC7B,EAAE,YAAY;CACd,EAAE,SAAS,GAAG,GAAG,IAAI,OAAO,IAAI,MAAM;CAEtC,OAAO;AACT"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/canvas.d.ts
|
|
2
|
+
/** Options for configuring the game canvas. */
|
|
3
|
+
interface CanvasOptions {
|
|
4
|
+
/** Enables or disables `imageSmoothingEnabled` on the canvas context. Default: `true`. */
|
|
5
|
+
imageSmoothing?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** The result of {@link createGameCanvas}. */
|
|
8
|
+
interface GameCanvas {
|
|
9
|
+
/** The underlying canvas element. */
|
|
10
|
+
canvas: HTMLCanvasElement;
|
|
11
|
+
/** The 2D rendering context for the canvas. */
|
|
12
|
+
ctx: CanvasRenderingContext2D;
|
|
13
|
+
/** Canvas width and height. */
|
|
14
|
+
size: {
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height: number;
|
|
17
|
+
};
|
|
18
|
+
/** Removes the canvas from the DOM and cleans up the resize listener. */
|
|
19
|
+
destroy: () => void;
|
|
20
|
+
}
|
|
21
|
+
/** Creates a full-window canvas and appends it to `document.body`.
|
|
22
|
+
*
|
|
23
|
+
* The canvas is automatically resized to fill the window on creation,
|
|
24
|
+
* and on every subsequent `resize` event.
|
|
25
|
+
*
|
|
26
|
+
* @returns The canvas element, its 2D context, and a `destroy` function
|
|
27
|
+
* to remove the canvas and clean up the resize listener.
|
|
28
|
+
*
|
|
29
|
+
* @throws {Error} If a 2D canvas context cannot be obtained.
|
|
30
|
+
*/
|
|
31
|
+
declare function createGameCanvas({
|
|
32
|
+
imageSmoothing
|
|
33
|
+
}?: CanvasOptions): GameCanvas;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { CanvasOptions, GameCanvas, createGameCanvas };
|
|
36
|
+
//# sourceMappingURL=canvas.d.mts.map
|
package/dist/canvas.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/canvas.ts
|
|
2
|
+
/** Creates a full-window canvas and appends it to `document.body`.
|
|
3
|
+
*
|
|
4
|
+
* The canvas is automatically resized to fill the window on creation,
|
|
5
|
+
* and on every subsequent `resize` event.
|
|
6
|
+
*
|
|
7
|
+
* @returns The canvas element, its 2D context, and a `destroy` function
|
|
8
|
+
* to remove the canvas and clean up the resize listener.
|
|
9
|
+
*
|
|
10
|
+
* @throws {Error} If a 2D canvas context cannot be obtained.
|
|
11
|
+
*/
|
|
12
|
+
function createGameCanvas({ imageSmoothing = true } = {}) {
|
|
13
|
+
const canvas = document.createElement("canvas");
|
|
14
|
+
document.body.appendChild(canvas);
|
|
15
|
+
const ctx = canvas.getContext("2d");
|
|
16
|
+
if (!ctx) throw new Error("2D canvas context not found");
|
|
17
|
+
const size = {
|
|
18
|
+
width: 0,
|
|
19
|
+
height: 0
|
|
20
|
+
};
|
|
21
|
+
const onResize = () => {
|
|
22
|
+
const dpr = window.devicePixelRatio ?? 1;
|
|
23
|
+
size.width = window.innerWidth;
|
|
24
|
+
size.height = window.innerHeight;
|
|
25
|
+
canvas.width = size.width * dpr;
|
|
26
|
+
canvas.height = size.height * dpr;
|
|
27
|
+
canvas.style.width = `${size.width}px`;
|
|
28
|
+
canvas.style.height = `${size.height}px`;
|
|
29
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
30
|
+
ctx.imageSmoothingEnabled = imageSmoothing;
|
|
31
|
+
};
|
|
32
|
+
window.addEventListener("resize", onResize);
|
|
33
|
+
onResize();
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
return {
|
|
36
|
+
canvas,
|
|
37
|
+
ctx,
|
|
38
|
+
size,
|
|
39
|
+
destroy: () => {
|
|
40
|
+
if (destroyed) return;
|
|
41
|
+
destroyed = true;
|
|
42
|
+
window.removeEventListener("resize", onResize);
|
|
43
|
+
canvas.remove();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createGameCanvas };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=canvas.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas.mjs","names":[],"sources":["../src/canvas.ts"],"sourcesContent":["/** Options for configuring the game canvas. */\nexport interface CanvasOptions {\n /** Enables or disables `imageSmoothingEnabled` on the canvas context. Default: `true`. */\n imageSmoothing?: boolean;\n}\n\n/** The result of {@link createGameCanvas}. */\nexport interface GameCanvas {\n /** The underlying canvas element. */\n canvas: HTMLCanvasElement;\n /** The 2D rendering context for the canvas. */\n ctx: CanvasRenderingContext2D;\n /** Canvas width and height. */\n size: { readonly width: number; readonly height: number };\n /** Removes the canvas from the DOM and cleans up the resize listener. */\n destroy: () => void;\n}\n\n/** Creates a full-window canvas and appends it to `document.body`.\n *\n * The canvas is automatically resized to fill the window on creation,\n * and on every subsequent `resize` event.\n *\n * @returns The canvas element, its 2D context, and a `destroy` function\n * to remove the canvas and clean up the resize listener.\n *\n * @throws {Error} If a 2D canvas context cannot be obtained.\n */\nexport function createGameCanvas(\n { imageSmoothing = true }: CanvasOptions = {}\n): GameCanvas {\n const canvas = document.createElement(\"canvas\");\n document.body.appendChild(canvas);\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"2D canvas context not found\");\n\n const size = { width: 0, height: 0 };\n\n const onResize = () => {\n const dpr = window.devicePixelRatio ?? 1;\n size.width = window.innerWidth;\n size.height = window.innerHeight;\n canvas.width = size.width * dpr;\n canvas.height = size.height * dpr;\n canvas.style.width = `${size.width}px`;\n canvas.style.height = `${size.height}px`;\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.imageSmoothingEnabled = imageSmoothing;\n }\n window.addEventListener(\"resize\", onResize);\n onResize();\n\n let destroyed = false;\n return {\n canvas,\n ctx,\n size,\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n window.removeEventListener(\"resize\", onResize);\n canvas.remove();\n }\n };\n}\n"],"mappings":";;;;;;;;;;;AA4BA,SAAgB,iBACd,EAAE,iBAAiB,SAAwB,CAAC,GAChC;CACZ,MAAM,SAAS,SAAS,cAAc,QAAQ;CAC9C,SAAS,KAAK,YAAY,MAAM;CAEhC,MAAM,MAAM,OAAO,WAAW,IAAI;CAClC,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,6BAA6B;CAEvD,MAAM,OAAO;EAAE,OAAO;EAAG,QAAQ;CAAE;CAEnC,MAAM,iBAAiB;EACrB,MAAM,MAAU,OAAO,oBAAoB;EAC3C,KAAK,QAAU,OAAO;EACtB,KAAK,SAAU,OAAO;EACtB,OAAO,QAAS,KAAK,QAAS;EAC9B,OAAO,SAAS,KAAK,SAAS;EAC9B,OAAO,MAAM,QAAS,GAAG,KAAK,MAAM;EACpC,OAAO,MAAM,SAAS,GAAG,KAAK,OAAO;EACrC,IAAI,aAAa,KAAK,GAAG,GAAG,KAAK,GAAG,CAAC;EACrC,IAAI,wBAAwB;CAC9B;CACA,OAAO,iBAAiB,UAAU,QAAQ;CAC1C,SAAS;CAET,IAAI,YAAY;CAChB,OAAO;EACL;EACA;EACA;EACA,eAAe;GACb,IAAI,WAAW;GACf,YAAY;GACZ,OAAO,oBAAoB,UAAU,QAAQ;GAC7C,OAAO,OAAO;EAChB;CACF;AACF"}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
//#region src/input/keyboard.d.ts
|
|
1
2
|
/** Initializes keyboard input listeners.
|
|
2
3
|
* @returns A cleanup function that removes all listeners and clears state.
|
|
3
4
|
* @throws {Error} If already initialized.
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
+
declare function initKeyboard(): () => void;
|
|
6
7
|
/** Returns `true` if the key is currently held down.
|
|
7
8
|
* @param code - A `KeyboardEvent.code` value (e.g. `"KeyA"`, `"Space"`, `"Digit1"`).
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
+
declare function isDown(code: string): boolean;
|
|
10
11
|
/** Returns `true` if the key was pressed this frame.
|
|
11
12
|
* @param code - A `KeyboardEvent.code` value (e.g. `"KeyA"`, `"Space"`).
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
+
declare function wasPressed(code: string): boolean;
|
|
14
15
|
/** Advances the per-frame pressed state. Called once per frame by {@link startLoop}. */
|
|
15
|
-
|
|
16
|
+
declare function clearFrameKeyboard(): void;
|
|
16
17
|
/** Clears the per-frame pressed state immediately. */
|
|
17
|
-
|
|
18
|
-
//#
|
|
18
|
+
declare function flushKeyboard(): void;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { clearFrameKeyboard, flushKeyboard, initKeyboard, isDown, wasPressed };
|
|
21
|
+
//# sourceMappingURL=keyboard.d.mts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/input/keyboard.ts
|
|
2
|
+
let isKeyboardInitialized = false;
|
|
3
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4
|
+
const pressedThisFrame = /* @__PURE__ */ new Set();
|
|
5
|
+
let pressedFrame = [];
|
|
6
|
+
const onKeyDown = (e) => {
|
|
7
|
+
if (!keys.has(e.code)) pressedThisFrame.add(e.code);
|
|
8
|
+
keys.add(e.code);
|
|
9
|
+
};
|
|
10
|
+
const onKeyUp = (e) => {
|
|
11
|
+
keys.delete(e.code);
|
|
12
|
+
};
|
|
13
|
+
const onBlur = () => {
|
|
14
|
+
keys.clear();
|
|
15
|
+
pressedThisFrame.clear();
|
|
16
|
+
};
|
|
17
|
+
/** Initializes keyboard input listeners.
|
|
18
|
+
* @returns A cleanup function that removes all listeners and clears state.
|
|
19
|
+
* @throws {Error} If already initialized.
|
|
20
|
+
*/
|
|
21
|
+
function initKeyboard() {
|
|
22
|
+
if (isKeyboardInitialized) throw new Error("initKeyboard: already initialized, call cleanup first");
|
|
23
|
+
isKeyboardInitialized = true;
|
|
24
|
+
window.addEventListener("keydown", onKeyDown);
|
|
25
|
+
window.addEventListener("keyup", onKeyUp);
|
|
26
|
+
window.addEventListener("blur", onBlur);
|
|
27
|
+
return () => {
|
|
28
|
+
isKeyboardInitialized = false;
|
|
29
|
+
keys.clear();
|
|
30
|
+
pressedThisFrame.clear();
|
|
31
|
+
flushKeyboard();
|
|
32
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
33
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
34
|
+
window.removeEventListener("blur", onBlur);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Returns `true` if the key is currently held down.
|
|
38
|
+
* @param code - A `KeyboardEvent.code` value (e.g. `"KeyA"`, `"Space"`, `"Digit1"`).
|
|
39
|
+
*/
|
|
40
|
+
function isDown(code) {
|
|
41
|
+
return keys.has(code);
|
|
42
|
+
}
|
|
43
|
+
/** Returns `true` if the key was pressed this frame.
|
|
44
|
+
* @param code - A `KeyboardEvent.code` value (e.g. `"KeyA"`, `"Space"`).
|
|
45
|
+
*/
|
|
46
|
+
function wasPressed(code) {
|
|
47
|
+
return pressedFrame.includes(code);
|
|
48
|
+
}
|
|
49
|
+
/** Advances the per-frame pressed state. Called once per frame by {@link startLoop}. */
|
|
50
|
+
function clearFrameKeyboard() {
|
|
51
|
+
pressedFrame = [...pressedThisFrame];
|
|
52
|
+
pressedThisFrame.clear();
|
|
53
|
+
}
|
|
54
|
+
/** Clears the per-frame pressed state immediately. */
|
|
55
|
+
function flushKeyboard() {
|
|
56
|
+
pressedFrame = [];
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { clearFrameKeyboard, flushKeyboard, initKeyboard, isDown, wasPressed };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=keyboard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.mjs","names":[],"sources":["../../src/input/keyboard.ts"],"sourcesContent":["let isKeyboardInitialized = false;\nconst keys = new Set<string>();\nconst pressedThisFrame = new Set<string>();\nlet pressedFrame: readonly string[] = [];\n\nconst onKeyDown = (e: KeyboardEvent) => {\n if (!keys.has(e.code)) pressedThisFrame.add(e.code);\n keys.add(e.code);\n};\nconst onKeyUp = (e: KeyboardEvent) => { keys.delete(e.code); };\nconst onBlur = () => { keys.clear(); pressedThisFrame.clear(); };\n\n/** Initializes keyboard input listeners.\n * @returns A cleanup function that removes all listeners and clears state.\n * @throws {Error} If already initialized.\n */\nexport function initKeyboard(): () => void {\n if (isKeyboardInitialized) throw new Error(\"initKeyboard: already initialized, call cleanup first\");\n isKeyboardInitialized = true;\n\n window.addEventListener(\"keydown\", onKeyDown);\n window.addEventListener(\"keyup\", onKeyUp);\n window.addEventListener(\"blur\", onBlur);\n\n return () => {\n isKeyboardInitialized = false;\n keys.clear();\n pressedThisFrame.clear();\n flushKeyboard();\n window.removeEventListener(\"keydown\", onKeyDown);\n window.removeEventListener(\"keyup\", onKeyUp);\n window.removeEventListener(\"blur\", onBlur);\n }\n}\n\n/** Returns `true` if the key is currently held down.\n * @param code - A `KeyboardEvent.code` value (e.g. `\"KeyA\"`, `\"Space\"`, `\"Digit1\"`).\n */\nexport function isDown(code: string): boolean { return keys.has(code); }\n\n/** Returns `true` if the key was pressed this frame.\n * @param code - A `KeyboardEvent.code` value (e.g. `\"KeyA\"`, `\"Space\"`).\n */\nexport function wasPressed(code: string): boolean { return pressedFrame.includes(code); }\n\n/** Advances the per-frame pressed state. Called once per frame by {@link startLoop}. */\nexport function clearFrameKeyboard(): void {\n pressedFrame = [...pressedThisFrame];\n pressedThisFrame.clear();\n}\n\n/** Clears the per-frame pressed state immediately. */\nexport function flushKeyboard(): void { pressedFrame = []; }\n"],"mappings":";AAAA,IAAI,wBAAwB;AAC5B,MAAM,uBAAO,IAAI,IAAY;AAC7B,MAAM,mCAAmB,IAAI,IAAY;AACzC,IAAI,eAAkC,CAAC;AAEvC,MAAM,aAAa,MAAqB;CACtC,IAAI,CAAC,KAAK,IAAI,EAAE,IAAI,GAAG,iBAAiB,IAAI,EAAE,IAAI;CAClD,KAAK,IAAI,EAAE,IAAI;AACjB;AACA,MAAM,WAAW,MAAqB;CAAE,KAAK,OAAO,EAAE,IAAI;AAAG;AAC7D,MAAM,eAAe;CAAE,KAAK,MAAM;CAAG,iBAAiB,MAAM;AAAG;;;;;AAM/D,SAAgB,eAA2B;CACzC,IAAI,uBAAuB,MAAM,IAAI,MAAM,uDAAuD;CAClG,wBAAwB;CAExB,OAAO,iBAAiB,WAAW,SAAS;CAC5C,OAAO,iBAAiB,SAAS,OAAO;CACxC,OAAO,iBAAiB,QAAQ,MAAM;CAEtC,aAAa;EACX,wBAAwB;EACxB,KAAK,MAAM;EACX,iBAAiB,MAAM;EACvB,cAAc;EACd,OAAO,oBAAoB,WAAW,SAAS;EAC/C,OAAO,oBAAoB,SAAS,OAAO;EAC3C,OAAO,oBAAoB,QAAQ,MAAM;CAC3C;AACF;;;;AAKA,SAAgB,OAAO,MAA2B;CAAE,OAAO,KAAK,IAAI,IAAI;AAAG;;;;AAK3E,SAAgB,WAAW,MAAuB;CAAE,OAAO,aAAa,SAAS,IAAI;AAAG;;AAGxF,SAAgB,qBAA2B;CACzC,eAAe,CAAC,GAAG,gBAAgB;CACnC,iBAAiB,MAAM;AACzB;;AAGA,SAAgB,gBAAsB;CAAE,eAAe,CAAC;AAAG"}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
+
//#region src/input/pointer.d.ts
|
|
1
2
|
/** Initializes pointer input listeners and binds to the given canvas for coordinate mapping.
|
|
2
3
|
* @param canvas - The canvas element used to transform pointer coordinates.
|
|
3
4
|
* @returns A cleanup function that removes all listeners and clears state.
|
|
4
5
|
* @throws {Error} If already initialized.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
+
declare function initPointer(canvas: HTMLCanvasElement): () => void;
|
|
7
8
|
/** Returns `true` if the given pointer button is currently held down.
|
|
8
9
|
* @param button - Pointer button index. Default: `0` (primary).
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
declare function isPointerDown(button?: number): boolean;
|
|
11
12
|
/** Returns `true` if the given button was clicked this frame.
|
|
12
13
|
* @param button - Pointer button index. Default: `0` (primary).
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
declare function wasPointerClicked(button?: number): boolean;
|
|
15
16
|
/** Returns `true` if the given button was released this frame.
|
|
16
17
|
* @param button - Pointer button index. Default: `0` (primary).
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
+
declare function wasPointerReleased(button?: number): boolean;
|
|
19
20
|
/** Returns the pointer's current X position in canvas pixel coordinates. */
|
|
20
|
-
|
|
21
|
+
declare function pointerX(): number;
|
|
21
22
|
/** Returns the pointer's current Y position in canvas pixel coordinates. */
|
|
22
|
-
|
|
23
|
+
declare function pointerY(): number;
|
|
23
24
|
/** Advances the per-frame click and release state. Called once per frame by {@link startLoop}. */
|
|
24
|
-
|
|
25
|
+
declare function clearFramePointer(): void;
|
|
25
26
|
/** Clears the per-frame click and release state immediately. */
|
|
26
|
-
|
|
27
|
-
//#
|
|
27
|
+
declare function flushPointer(): void;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { clearFramePointer, flushPointer, initPointer, isPointerDown, pointerX, pointerY, wasPointerClicked, wasPointerReleased };
|
|
30
|
+
//# sourceMappingURL=pointer.d.mts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//#region src/input/pointer.ts
|
|
2
|
+
let isPointerInitialized = false;
|
|
3
|
+
const down = /* @__PURE__ */ new Set();
|
|
4
|
+
const clicked = /* @__PURE__ */ new Set();
|
|
5
|
+
const released = /* @__PURE__ */ new Set();
|
|
6
|
+
let clickedFrame = [];
|
|
7
|
+
let releasedFrame = [];
|
|
8
|
+
let canvasRef = null;
|
|
9
|
+
let posX = 0;
|
|
10
|
+
let posY = 0;
|
|
11
|
+
function clearDown() {
|
|
12
|
+
down.clear();
|
|
13
|
+
clicked.clear();
|
|
14
|
+
released.clear();
|
|
15
|
+
}
|
|
16
|
+
const updatePos = (e) => {
|
|
17
|
+
if (!canvasRef) return;
|
|
18
|
+
const rect = canvasRef.getBoundingClientRect();
|
|
19
|
+
posX = e.clientX - rect.left;
|
|
20
|
+
posY = e.clientY - rect.top;
|
|
21
|
+
};
|
|
22
|
+
const onDown = (e) => {
|
|
23
|
+
updatePos(e);
|
|
24
|
+
if (!down.has(e.button)) clicked.add(e.button);
|
|
25
|
+
down.add(e.button);
|
|
26
|
+
};
|
|
27
|
+
const onUp = (e) => {
|
|
28
|
+
updatePos(e);
|
|
29
|
+
down.delete(e.button);
|
|
30
|
+
released.add(e.button);
|
|
31
|
+
};
|
|
32
|
+
const onMove = (e) => updatePos(e);
|
|
33
|
+
const onBlur = () => clearDown();
|
|
34
|
+
const onMenu = (e) => {
|
|
35
|
+
clearDown();
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
};
|
|
38
|
+
/** Initializes pointer input listeners and binds to the given canvas for coordinate mapping.
|
|
39
|
+
* @param canvas - The canvas element used to transform pointer coordinates.
|
|
40
|
+
* @returns A cleanup function that removes all listeners and clears state.
|
|
41
|
+
* @throws {Error} If already initialized.
|
|
42
|
+
*/
|
|
43
|
+
function initPointer(canvas) {
|
|
44
|
+
if (isPointerInitialized) throw new Error("initPointer: already initialized, call cleanup first");
|
|
45
|
+
isPointerInitialized = true;
|
|
46
|
+
canvasRef = canvas;
|
|
47
|
+
window.addEventListener("pointerdown", onDown);
|
|
48
|
+
window.addEventListener("pointerup", onUp);
|
|
49
|
+
window.addEventListener("pointermove", onMove);
|
|
50
|
+
window.addEventListener("blur", onBlur);
|
|
51
|
+
window.addEventListener("contextmenu", onMenu);
|
|
52
|
+
return () => {
|
|
53
|
+
isPointerInitialized = false;
|
|
54
|
+
canvasRef = null;
|
|
55
|
+
clearDown();
|
|
56
|
+
flushPointer();
|
|
57
|
+
window.removeEventListener("pointerdown", onDown);
|
|
58
|
+
window.removeEventListener("pointerup", onUp);
|
|
59
|
+
window.removeEventListener("pointermove", onMove);
|
|
60
|
+
window.removeEventListener("blur", onBlur);
|
|
61
|
+
window.removeEventListener("contextmenu", onMenu);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Returns `true` if the given pointer button is currently held down.
|
|
65
|
+
* @param button - Pointer button index. Default: `0` (primary).
|
|
66
|
+
*/
|
|
67
|
+
function isPointerDown(button = 0) {
|
|
68
|
+
return down.has(button);
|
|
69
|
+
}
|
|
70
|
+
/** Returns `true` if the given button was clicked this frame.
|
|
71
|
+
* @param button - Pointer button index. Default: `0` (primary).
|
|
72
|
+
*/
|
|
73
|
+
function wasPointerClicked(button = 0) {
|
|
74
|
+
return clickedFrame.includes(button);
|
|
75
|
+
}
|
|
76
|
+
/** Returns `true` if the given button was released this frame.
|
|
77
|
+
* @param button - Pointer button index. Default: `0` (primary).
|
|
78
|
+
*/
|
|
79
|
+
function wasPointerReleased(button = 0) {
|
|
80
|
+
return releasedFrame.includes(button);
|
|
81
|
+
}
|
|
82
|
+
/** Returns the pointer's current X position in canvas pixel coordinates. */
|
|
83
|
+
function pointerX() {
|
|
84
|
+
return posX;
|
|
85
|
+
}
|
|
86
|
+
/** Returns the pointer's current Y position in canvas pixel coordinates. */
|
|
87
|
+
function pointerY() {
|
|
88
|
+
return posY;
|
|
89
|
+
}
|
|
90
|
+
/** Advances the per-frame click and release state. Called once per frame by {@link startLoop}. */
|
|
91
|
+
function clearFramePointer() {
|
|
92
|
+
clickedFrame = [...clicked];
|
|
93
|
+
releasedFrame = [...released];
|
|
94
|
+
clicked.clear();
|
|
95
|
+
released.clear();
|
|
96
|
+
}
|
|
97
|
+
/** Clears the per-frame click and release state immediately. */
|
|
98
|
+
function flushPointer() {
|
|
99
|
+
clickedFrame = [];
|
|
100
|
+
releasedFrame = [];
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { clearFramePointer, flushPointer, initPointer, isPointerDown, pointerX, pointerY, wasPointerClicked, wasPointerReleased };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=pointer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pointer.mjs","names":[],"sources":["../../src/input/pointer.ts"],"sourcesContent":["let isPointerInitialized = false;\nconst down = new Set<number>();\nconst clicked = new Set<number>();\nconst released = new Set<number>();\nlet clickedFrame: readonly number[] = [];\nlet releasedFrame: readonly number[] = [];\n\nlet canvasRef: HTMLCanvasElement | null = null;\nlet posX = 0;\nlet posY = 0;\n\nfunction clearDown(): void {\n down.clear();\n clicked.clear();\n released.clear();\n}\n\nconst updatePos = (e: PointerEvent) => {\n if (!canvasRef) return;\n const rect = canvasRef.getBoundingClientRect();\n posX = e.clientX - rect.left;\n posY = e.clientY - rect.top;\n};\n\nconst onDown = (e: PointerEvent) => {\n updatePos(e);\n if (!down.has(e.button)) clicked.add(e.button);\n down.add(e.button);\n};\nconst onUp = (e: PointerEvent) => {\n updatePos(e);\n down.delete(e.button);\n released.add(e.button);\n};\nconst onMove = (e: PointerEvent) => updatePos(e);\nconst onBlur = () => clearDown();\nconst onMenu = (e: MouseEvent) => {\n clearDown();\n e.preventDefault();\n}\n\n/** Initializes pointer input listeners and binds to the given canvas for coordinate mapping.\n * @param canvas - The canvas element used to transform pointer coordinates.\n * @returns A cleanup function that removes all listeners and clears state.\n * @throws {Error} If already initialized.\n */\nexport function initPointer(canvas: HTMLCanvasElement): () => void {\n if (isPointerInitialized) throw new Error(\"initPointer: already initialized, call cleanup first\");\n isPointerInitialized = true;\n\n canvasRef = canvas;\n\n window.addEventListener(\"pointerdown\", onDown);\n window.addEventListener(\"pointerup\", onUp);\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"blur\", onBlur);\n window.addEventListener(\"contextmenu\", onMenu);\n\n return () => {\n isPointerInitialized = false;\n canvasRef = null;\n clearDown();\n flushPointer();\n window.removeEventListener(\"pointerdown\", onDown);\n window.removeEventListener(\"pointerup\", onUp);\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"blur\", onBlur);\n window.removeEventListener(\"contextmenu\", onMenu);\n };\n}\n\n/** Returns `true` if the given pointer button is currently held down.\n * @param button - Pointer button index. Default: `0` (primary).\n */\nexport function isPointerDown(button = 0): boolean { return down.has(button); }\n\n/** Returns `true` if the given button was clicked this frame.\n * @param button - Pointer button index. Default: `0` (primary).\n */\nexport function wasPointerClicked(button = 0): boolean { return clickedFrame.includes(button); }\n\n/** Returns `true` if the given button was released this frame.\n * @param button - Pointer button index. Default: `0` (primary).\n */\nexport function wasPointerReleased(button = 0): boolean { return releasedFrame.includes(button); }\n\n/** Returns the pointer's current X position in canvas pixel coordinates. */\nexport function pointerX(): number { return posX; }\n\n/** Returns the pointer's current Y position in canvas pixel coordinates. */\nexport function pointerY(): number { return posY; }\n\n/** Advances the per-frame click and release state. Called once per frame by {@link startLoop}. */\nexport function clearFramePointer(): void {\n clickedFrame = [...clicked];\n releasedFrame = [...released];\n clicked.clear();\n released.clear();\n}\n\n/** Clears the per-frame click and release state immediately. */\nexport function flushPointer(): void { clickedFrame = []; releasedFrame = []; }\n"],"mappings":";AAAA,IAAI,uBAAuB;AAC3B,MAAM,uBAAW,IAAI,IAAY;AACjC,MAAM,0BAAW,IAAI,IAAY;AACjC,MAAM,2BAAW,IAAI,IAAY;AACjC,IAAI,eAAmC,CAAC;AACxC,IAAI,gBAAmC,CAAC;AAExC,IAAI,YAAsC;AAC1C,IAAI,OAAO;AACX,IAAI,OAAO;AAEX,SAAS,YAAkB;CACzB,KAAK,MAAM;CACX,QAAQ,MAAM;CACd,SAAS,MAAM;AACjB;AAEA,MAAM,aAAa,MAAoB;CACrC,IAAI,CAAC,WAAW;CAChB,MAAM,OAAO,UAAU,sBAAsB;CAC7C,OAAO,EAAE,UAAU,KAAK;CACxB,OAAO,EAAE,UAAU,KAAK;AAC1B;AAEA,MAAM,UAAU,MAAoB;CAClC,UAAU,CAAC;CACX,IAAI,CAAC,KAAK,IAAI,EAAE,MAAM,GAAG,QAAQ,IAAI,EAAE,MAAM;CAC7C,KAAK,IAAI,EAAE,MAAM;AACnB;AACA,MAAM,QAAQ,MAAoB;CAChC,UAAU,CAAC;CACX,KAAK,OAAO,EAAE,MAAM;CACpB,SAAS,IAAI,EAAE,MAAM;AACvB;AACA,MAAM,UAAU,MAAoB,UAAU,CAAC;AAC/C,MAAM,eAAe,UAAU;AAC/B,MAAM,UAAU,MAAkB;CAChC,UAAU;CACV,EAAE,eAAe;AACnB;;;;;;AAOA,SAAgB,YAAY,QAAuC;CACjE,IAAI,sBAAsB,MAAM,IAAI,MAAM,sDAAsD;CAChG,uBAAuB;CAEvB,YAAY;CAEZ,OAAO,iBAAiB,eAAe,MAAM;CAC7C,OAAO,iBAAiB,aAAa,IAAI;CACzC,OAAO,iBAAiB,eAAe,MAAM;CAC7C,OAAO,iBAAiB,QAAQ,MAAM;CACtC,OAAO,iBAAiB,eAAe,MAAM;CAE7C,aAAa;EACX,uBAAuB;EACvB,YAAY;EACZ,UAAU;EACV,aAAa;EACb,OAAO,oBAAoB,eAAe,MAAM;EAChD,OAAO,oBAAoB,aAAa,IAAI;EAC5C,OAAO,oBAAoB,eAAe,MAAM;EAChD,OAAO,oBAAoB,QAAQ,MAAM;EACzC,OAAO,oBAAoB,eAAe,MAAM;CAClD;AACF;;;;AAKA,SAAgB,cAAc,SAAS,GAAiB;CAAE,OAAO,KAAK,IAAI,MAAM;AAAG;;;;AAKnF,SAAgB,kBAAkB,SAAS,GAAa;CAAE,OAAO,aAAa,SAAS,MAAM;AAAG;;;;AAKhG,SAAgB,mBAAmB,SAAS,GAAY;CAAE,OAAO,cAAc,SAAS,MAAM;AAAG;;AAGjG,SAAgB,WAAmB;CAAE,OAAO;AAAM;;AAGlD,SAAgB,WAAmB;CAAE,OAAO;AAAM;;AAGlD,SAAgB,oBAA0B;CACxC,eAAgB,CAAC,GAAG,OAAO;CAC3B,gBAAgB,CAAC,GAAG,QAAQ;CAC5B,QAAQ,MAAM;CACd,SAAS,MAAM;AACjB;;AAGA,SAAgB,eAAqB;CAAE,eAAe,CAAC;CAAG,gBAAgB,CAAC;AAAG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/update.d.ts
|
|
2
|
+
/** Options for configuring the game loop. */
|
|
3
|
+
interface LoopOptions {
|
|
4
|
+
/** Fixed timestep in **ms**, or "variable" for frame-rate dependent updates. */
|
|
5
|
+
tickRate: number | "variable";
|
|
6
|
+
/** Maximum elapsed time per frame in **ms**. */
|
|
7
|
+
maxDelta: number;
|
|
8
|
+
/** Automatically pause the loop when the page is hidden. */
|
|
9
|
+
pauseOnHidden: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Handle returned by {@link startLoop} to control the running game loop. */
|
|
12
|
+
interface LoopHandle {
|
|
13
|
+
/** Permanently stops the loop and cancels the animation frame. */
|
|
14
|
+
stop(): void;
|
|
15
|
+
/** Pauses update and render calls without cancelling the animation frame. */
|
|
16
|
+
pause(): void;
|
|
17
|
+
/** Resumes a paused loop. */
|
|
18
|
+
resume(): void;
|
|
19
|
+
}
|
|
20
|
+
/** Starts a game loop using `requestAnimationFrame`.
|
|
21
|
+
*
|
|
22
|
+
* When `tickRate` is a number, uses a fixed timestep accumulator
|
|
23
|
+
* so `update` is always called with a consistent delta.
|
|
24
|
+
* When `"variable"`, `update` receives the raw frame delta.
|
|
25
|
+
*
|
|
26
|
+
* @param update - Called each tick with the delta time in **ms**.
|
|
27
|
+
* @param render - Called once per frame after all update ticks.
|
|
28
|
+
* @param options - Loop configuration. See {@link LoopOptions}.
|
|
29
|
+
* @returns A {@link LoopHandle} to stop, pause, or resume the loop.
|
|
30
|
+
*
|
|
31
|
+
* @throws {RangeError} If `tickRate` is not `"variable"` or a positive finite number.
|
|
32
|
+
* @throws {RangeError} If `maxDelta` is not a positive finite number.
|
|
33
|
+
*/
|
|
34
|
+
declare function startLoop(update: (/** **Milliseconds** */dt: number) => void, render: () => void, options: LoopOptions): LoopHandle;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { LoopHandle, LoopOptions, startLoop };
|
|
37
|
+
//# sourceMappingURL=update.d.mts.map
|
package/dist/update.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { clearFrameKeyboard } from "./input/keyboard.mjs";
|
|
2
|
+
import { clearFramePointer } from "./input/pointer.mjs";
|
|
3
|
+
//#region src/update.ts
|
|
4
|
+
/** Starts a game loop using `requestAnimationFrame`.
|
|
5
|
+
*
|
|
6
|
+
* When `tickRate` is a number, uses a fixed timestep accumulator
|
|
7
|
+
* so `update` is always called with a consistent delta.
|
|
8
|
+
* When `"variable"`, `update` receives the raw frame delta.
|
|
9
|
+
*
|
|
10
|
+
* @param update - Called each tick with the delta time in **ms**.
|
|
11
|
+
* @param render - Called once per frame after all update ticks.
|
|
12
|
+
* @param options - Loop configuration. See {@link LoopOptions}.
|
|
13
|
+
* @returns A {@link LoopHandle} to stop, pause, or resume the loop.
|
|
14
|
+
*
|
|
15
|
+
* @throws {RangeError} If `tickRate` is not `"variable"` or a positive finite number.
|
|
16
|
+
* @throws {RangeError} If `maxDelta` is not a positive finite number.
|
|
17
|
+
*/
|
|
18
|
+
function startLoop(update, render, options) {
|
|
19
|
+
const { tickRate, maxDelta, pauseOnHidden } = options;
|
|
20
|
+
if (typeof tickRate === "number" && (!Number.isFinite(tickRate) || tickRate <= 0)) throw new RangeError(`startLoop: tickRate must be "variable" or a positive finite number, got ${tickRate}`);
|
|
21
|
+
if (!Number.isFinite(maxDelta) || maxDelta <= 0) throw new RangeError(`startLoop: maxDelta must be a positive finite number, got ${maxDelta}`);
|
|
22
|
+
let reqId = null;
|
|
23
|
+
let accumulator = 0;
|
|
24
|
+
let lastTime = performance.now();
|
|
25
|
+
let paused = false;
|
|
26
|
+
let visibilityCleanup = null;
|
|
27
|
+
if (pauseOnHidden) {
|
|
28
|
+
const onVisibility = () => {
|
|
29
|
+
paused = document.hidden;
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
32
|
+
visibilityCleanup = () => document.removeEventListener("visibilitychange", onVisibility);
|
|
33
|
+
}
|
|
34
|
+
function frame(nowMs) {
|
|
35
|
+
if (paused) {
|
|
36
|
+
lastTime = nowMs;
|
|
37
|
+
reqId = requestAnimationFrame(frame);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const elapsed = Math.min(nowMs - lastTime, maxDelta);
|
|
41
|
+
lastTime = nowMs;
|
|
42
|
+
if (tickRate === "variable") {
|
|
43
|
+
clearFrameKeyboard();
|
|
44
|
+
clearFramePointer();
|
|
45
|
+
update(elapsed);
|
|
46
|
+
} else {
|
|
47
|
+
accumulator += elapsed;
|
|
48
|
+
let ticked = false;
|
|
49
|
+
while (accumulator >= tickRate) {
|
|
50
|
+
if (!ticked) {
|
|
51
|
+
clearFrameKeyboard();
|
|
52
|
+
clearFramePointer();
|
|
53
|
+
ticked = true;
|
|
54
|
+
}
|
|
55
|
+
update(tickRate);
|
|
56
|
+
accumulator -= tickRate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
render();
|
|
60
|
+
reqId = requestAnimationFrame(frame);
|
|
61
|
+
}
|
|
62
|
+
reqId = requestAnimationFrame(frame);
|
|
63
|
+
return {
|
|
64
|
+
stop: () => {
|
|
65
|
+
if (reqId !== null) {
|
|
66
|
+
cancelAnimationFrame(reqId);
|
|
67
|
+
reqId = null;
|
|
68
|
+
}
|
|
69
|
+
visibilityCleanup?.();
|
|
70
|
+
},
|
|
71
|
+
pause: () => {
|
|
72
|
+
paused = true;
|
|
73
|
+
},
|
|
74
|
+
resume: () => {
|
|
75
|
+
paused = false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { startLoop };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=update.mjs.map
|