@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.
Files changed (42) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +28 -28
  3. package/dist/{assets.d.ts → assets.d.mts} +6 -3
  4. package/dist/assets.mjs +39 -0
  5. package/dist/assets.mjs.map +1 -0
  6. package/dist/canvas.d.mts +36 -0
  7. package/dist/canvas.mjs +50 -0
  8. package/dist/canvas.mjs.map +1 -0
  9. package/dist/input/{keyboard.d.ts → keyboard.d.mts} +9 -6
  10. package/dist/input/keyboard.mjs +61 -0
  11. package/dist/input/keyboard.mjs.map +1 -0
  12. package/dist/input/{pointer.d.ts → pointer.d.mts} +12 -9
  13. package/dist/input/pointer.mjs +105 -0
  14. package/dist/input/pointer.mjs.map +1 -0
  15. package/dist/update.d.mts +37 -0
  16. package/dist/update.mjs +82 -0
  17. package/dist/update.mjs.map +1 -0
  18. package/dist/validate.d.mts +16 -0
  19. package/dist/validate.mjs +55 -0
  20. package/dist/validate.mjs.map +1 -0
  21. package/package.json +56 -44
  22. package/dist/assets.d.ts.map +0 -1
  23. package/dist/assets.js +0 -36
  24. package/dist/assets.js.map +0 -1
  25. package/dist/canvas.d.ts +0 -31
  26. package/dist/canvas.d.ts.map +0 -1
  27. package/dist/canvas.js +0 -45
  28. package/dist/canvas.js.map +0 -1
  29. package/dist/input/keyboard.d.ts.map +0 -1
  30. package/dist/input/keyboard.js +0 -48
  31. package/dist/input/keyboard.js.map +0 -1
  32. package/dist/input/pointer.d.ts.map +0 -1
  33. package/dist/input/pointer.js +0 -91
  34. package/dist/input/pointer.js.map +0 -1
  35. package/dist/update.d.ts +0 -34
  36. package/dist/update.d.ts.map +0 -1
  37. package/dist/update.js +0 -77
  38. package/dist/update.js.map +0 -1
  39. package/dist/validate.d.ts +0 -13
  40. package/dist/validate.d.ts.map +0 -1
  41. package/dist/validate.js +0 -61
  42. 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
- export declare function loadImage(src: string): Promise<HTMLImageElement>;
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
- export declare function tintImage(source: HTMLImageElement, color: string): HTMLCanvasElement;
17
- //# sourceMappingURL=assets.d.ts.map
17
+ declare function tintImage(source: HTMLImageElement, color: string): HTMLCanvasElement;
18
+ //#endregion
19
+ export { loadImage, tintImage };
20
+ //# sourceMappingURL=assets.d.mts.map
@@ -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
@@ -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
- export declare function initKeyboard(): () => void;
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
- export declare function isDown(code: string): boolean;
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
- export declare function wasPressed(code: string): boolean;
14
+ declare function wasPressed(code: string): boolean;
14
15
  /** Advances the per-frame pressed state. Called once per frame by {@link startLoop}. */
15
- export declare function clearFrameKeyboard(): void;
16
+ declare function clearFrameKeyboard(): void;
16
17
  /** Clears the per-frame pressed state immediately. */
17
- export declare function flushKeyboard(): void;
18
- //# sourceMappingURL=keyboard.d.ts.map
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
- export declare function initPointer(canvas: HTMLCanvasElement): () => void;
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
- export declare function isPointerDown(button?: number): boolean;
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
- export declare function wasPointerClicked(button?: number): boolean;
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
- export declare function wasPointerReleased(button?: number): boolean;
19
+ declare function wasPointerReleased(button?: number): boolean;
19
20
  /** Returns the pointer's current X position in canvas pixel coordinates. */
20
- export declare function pointerX(): number;
21
+ declare function pointerX(): number;
21
22
  /** Returns the pointer's current Y position in canvas pixel coordinates. */
22
- export declare function pointerY(): number;
23
+ declare function pointerY(): number;
23
24
  /** Advances the per-frame click and release state. Called once per frame by {@link startLoop}. */
24
- export declare function clearFramePointer(): void;
25
+ declare function clearFramePointer(): void;
25
26
  /** Clears the per-frame click and release state immediately. */
26
- export declare function flushPointer(): void;
27
- //# sourceMappingURL=pointer.d.ts.map
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
@@ -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