yuuna-engine 0.5.0 → 0.6.0

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/README.md CHANGED
@@ -107,10 +107,14 @@ hand? Grab one from [`templates/`](templates):
107
107
  open it in a browser and it runs.
108
108
  - **[npm](templates/npm)** — TypeScript + a dev server with hot reload
109
109
  (via Vite), for a real local project.
110
+ - **[neutralino-desktop](templates/neutralino-desktop)** — the `npm`
111
+ template wrapped in [Neutralino](https://neutralino.js.org) to run as a
112
+ native desktop window instead of a browser tab.
110
113
 
111
114
  ```sh
112
115
  npx degit lucy-dot-exe/yuuna/templates/blank my-game
113
116
  # or: npx degit lucy-dot-exe/yuuna/templates/npm my-game
117
+ # or: npx degit lucy-dot-exe/yuuna/templates/neutralino-desktop my-game
114
118
  ```
115
119
 
116
120
  [`degit`](https://github.com/Rich-Harris/degit) copies the folder without
@@ -174,7 +174,11 @@ export type MusicEndEvent = {
174
174
  tag: "MUSIC_END";
175
175
  id: string;
176
176
  };
177
- export type GameEvent = TimeEvent | ClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent | MouseLeaveEvent | TabBlurEvent | TabFocusEvent | MusicEndEvent;
177
+ export type FullscreenChangeEvent = {
178
+ tag: "FULLSCREEN_CHANGE";
179
+ isFullscreen: boolean;
180
+ };
181
+ export type GameEvent = TimeEvent | ClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent | MouseLeaveEvent | TabBlurEvent | TabFocusEvent | MusicEndEvent | FullscreenChangeEvent;
178
182
  export type CustomGameEvent<Custom> = {
179
183
  tag: "CUSTOM";
180
184
  event: Custom;
@@ -229,6 +233,7 @@ export type RunEngineProps<State, Custom = never> = {
229
233
  width?: number;
230
234
  height?: number;
231
235
  backgroundColor?: string;
236
+ resize?: "none" | "fit" | "stretch";
232
237
  };
233
238
  camera?: (state: State) => {
234
239
  x: number;
@@ -238,6 +243,8 @@ export type RunEngineProps<State, Custom = never> = {
238
243
  };
239
244
  export type RunEngineFunction = <State, Custom = never>(props: RunEngineProps<State, Custom>) => Promise<{
240
245
  sendEvent: (event: Custom) => void;
246
+ requestFullscreen: () => Promise<void>;
247
+ exitFullscreen: () => Promise<void>;
241
248
  }>;
242
249
  export declare const keyboardKeys: readonly ["ControlLeft", "ControlRight", "AltLeft", "AltRight", "CapsLock", "End", "Delete", "Tab", "Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Numpad0", "Numpad1", "Numpad2", "Numpad3", "Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", "KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ"];
243
250
  export type KeyboardKeys = (typeof keyboardKeys)[number];
package/lib/index.cjs CHANGED
@@ -175,6 +175,31 @@ const runEngine = async (props) => {
175
175
  // Make the canvas focusable so keyboard input is scoped to it instead of
176
176
  // leaking to the rest of the page (e.g. arrow keys scrolling the window).
177
177
  canvas.tabIndex = 0;
178
+ // Resizes the *display* size only (CSS width/height) — canvas.width/
179
+ // height above stays the fixed logical resolution every renderable's
180
+ // position is already expressed in, so this never needs to touch any
181
+ // of that math. See RunEngineProps.canvas.resize for the mode semantics.
182
+ const applyResize = () => {
183
+ const mode = props.canvas?.resize;
184
+ if (mode === undefined || mode === "none") {
185
+ return;
186
+ }
187
+ if (mode === "stretch") {
188
+ canvas.style.width = `${window.innerWidth}px`;
189
+ canvas.style.height = `${window.innerHeight}px`;
190
+ return;
191
+ }
192
+ const scale = Math.min(window.innerWidth / canvas.width, window.innerHeight / canvas.height);
193
+ canvas.style.width = `${canvas.width * scale}px`;
194
+ canvas.style.height = `${canvas.height * scale}px`;
195
+ };
196
+ applyResize();
197
+ window.addEventListener("resize", applyResize);
198
+ const requestFullscreen = () => canvas.requestFullscreen();
199
+ // exitFullscreen() rejects with a TypeError if nothing is fullscreen —
200
+ // guarded into a no-op instead, so callers don't need to track that
201
+ // state themselves just to call this safely.
202
+ const exitFullscreen = () => window.document.fullscreenElement === null ? Promise.resolve() : window.document.exitFullscreen();
178
203
  let state = props.initialState;
179
204
  const events = [];
180
205
  // Lets a caller report something that happened outside the render loop
@@ -281,9 +306,11 @@ const runEngine = async (props) => {
281
306
  };
282
307
  // A newer runEngine() call started while this one was still loading
283
308
  // resources (e.g. a spritesheet) — abandon this run instead of setting
284
- // up a second, orphaned render loop alongside the newer one.
309
+ // up a second, orphaned render loop alongside the newer one. The
310
+ // fullscreen functions are no-ops here since this run never gets far
311
+ // enough to own the canvas — the newer run's are the ones that matter.
285
312
  if (runId !== latestRunId) {
286
- return { sendEvent };
313
+ return { sendEvent, requestFullscreen: () => Promise.resolve(), exitFullscreen: () => Promise.resolve() };
287
314
  }
288
315
  context.imageSmoothingEnabled = false;
289
316
  // An offscreen 1x1 canvas used only to resolve a CSS color string (a
@@ -668,6 +695,17 @@ const runEngine = async (props) => {
668
695
  events.push({ tag: window.document.hidden ? "TAB_BLUR" : "TAB_FOCUS" });
669
696
  };
670
697
  window.document.addEventListener("visibilitychange", handleVisibilityChange);
698
+ // Fullscreen is a document-level concern too, and fires for every way
699
+ // fullscreen can change — the requestFullscreen()/exitFullscreen()
700
+ // above, but also things neither of those causes directly, like the
701
+ // user pressing Esc. Re-running applyResize() here (rather than relying
702
+ // solely on the "resize" listener) covers browsers that don't also fire
703
+ // a window resize when fullscreen is toggled.
704
+ const handleFullscreenChange = () => {
705
+ events.push({ tag: "FULLSCREEN_CHANGE", isFullscreen: window.document.fullscreenElement === canvas });
706
+ applyResize();
707
+ };
708
+ window.document.addEventListener("fullscreenchange", handleFullscreenChange);
671
709
  context.imageSmoothingEnabled = false;
672
710
  // Scale is a canvas transform around the renderable's anchor, applied
673
711
  // before its type-specific drawing runs below — everything drawn under
@@ -946,8 +984,11 @@ const runEngine = async (props) => {
946
984
  // This one's on `document`, not the canvas — same reasoning as above,
947
985
  // just doubly true since `document` isn't even scoped to this canvas.
948
986
  window.document.removeEventListener("visibilitychange", handleVisibilityChange);
987
+ window.document.removeEventListener("fullscreenchange", handleFullscreenChange);
988
+ // Same pile-up risk as the canvas listeners above, but on `window`.
989
+ window.removeEventListener("resize", applyResize);
949
990
  };
950
- return { sendEvent };
991
+ return { sendEvent, requestFullscreen, exitFullscreen };
951
992
  };
952
993
 
953
994
  exports.STOP = STOP;
package/lib/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/lib/index.js CHANGED
@@ -173,6 +173,31 @@ const runEngine = async (props) => {
173
173
  // Make the canvas focusable so keyboard input is scoped to it instead of
174
174
  // leaking to the rest of the page (e.g. arrow keys scrolling the window).
175
175
  canvas.tabIndex = 0;
176
+ // Resizes the *display* size only (CSS width/height) — canvas.width/
177
+ // height above stays the fixed logical resolution every renderable's
178
+ // position is already expressed in, so this never needs to touch any
179
+ // of that math. See RunEngineProps.canvas.resize for the mode semantics.
180
+ const applyResize = () => {
181
+ const mode = props.canvas?.resize;
182
+ if (mode === undefined || mode === "none") {
183
+ return;
184
+ }
185
+ if (mode === "stretch") {
186
+ canvas.style.width = `${window.innerWidth}px`;
187
+ canvas.style.height = `${window.innerHeight}px`;
188
+ return;
189
+ }
190
+ const scale = Math.min(window.innerWidth / canvas.width, window.innerHeight / canvas.height);
191
+ canvas.style.width = `${canvas.width * scale}px`;
192
+ canvas.style.height = `${canvas.height * scale}px`;
193
+ };
194
+ applyResize();
195
+ window.addEventListener("resize", applyResize);
196
+ const requestFullscreen = () => canvas.requestFullscreen();
197
+ // exitFullscreen() rejects with a TypeError if nothing is fullscreen —
198
+ // guarded into a no-op instead, so callers don't need to track that
199
+ // state themselves just to call this safely.
200
+ const exitFullscreen = () => window.document.fullscreenElement === null ? Promise.resolve() : window.document.exitFullscreen();
176
201
  let state = props.initialState;
177
202
  const events = [];
178
203
  // Lets a caller report something that happened outside the render loop
@@ -279,9 +304,11 @@ const runEngine = async (props) => {
279
304
  };
280
305
  // A newer runEngine() call started while this one was still loading
281
306
  // resources (e.g. a spritesheet) — abandon this run instead of setting
282
- // up a second, orphaned render loop alongside the newer one.
307
+ // up a second, orphaned render loop alongside the newer one. The
308
+ // fullscreen functions are no-ops here since this run never gets far
309
+ // enough to own the canvas — the newer run's are the ones that matter.
283
310
  if (runId !== latestRunId) {
284
- return { sendEvent };
311
+ return { sendEvent, requestFullscreen: () => Promise.resolve(), exitFullscreen: () => Promise.resolve() };
285
312
  }
286
313
  context.imageSmoothingEnabled = false;
287
314
  // An offscreen 1x1 canvas used only to resolve a CSS color string (a
@@ -666,6 +693,17 @@ const runEngine = async (props) => {
666
693
  events.push({ tag: window.document.hidden ? "TAB_BLUR" : "TAB_FOCUS" });
667
694
  };
668
695
  window.document.addEventListener("visibilitychange", handleVisibilityChange);
696
+ // Fullscreen is a document-level concern too, and fires for every way
697
+ // fullscreen can change — the requestFullscreen()/exitFullscreen()
698
+ // above, but also things neither of those causes directly, like the
699
+ // user pressing Esc. Re-running applyResize() here (rather than relying
700
+ // solely on the "resize" listener) covers browsers that don't also fire
701
+ // a window resize when fullscreen is toggled.
702
+ const handleFullscreenChange = () => {
703
+ events.push({ tag: "FULLSCREEN_CHANGE", isFullscreen: window.document.fullscreenElement === canvas });
704
+ applyResize();
705
+ };
706
+ window.document.addEventListener("fullscreenchange", handleFullscreenChange);
669
707
  context.imageSmoothingEnabled = false;
670
708
  // Scale is a canvas transform around the renderable's anchor, applied
671
709
  // before its type-specific drawing runs below — everything drawn under
@@ -944,8 +982,11 @@ const runEngine = async (props) => {
944
982
  // This one's on `document`, not the canvas — same reasoning as above,
945
983
  // just doubly true since `document` isn't even scoped to this canvas.
946
984
  window.document.removeEventListener("visibilitychange", handleVisibilityChange);
985
+ window.document.removeEventListener("fullscreenchange", handleFullscreenChange);
986
+ // Same pile-up risk as the canvas listeners above, but on `window`.
987
+ window.removeEventListener("resize", applyResize);
947
988
  };
948
- return { sendEvent };
989
+ return { sendEvent, requestFullscreen, exitFullscreen };
949
990
  };
950
991
 
951
992
  export { STOP, runEngine };
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuuna-engine",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A lightweight, state-machine-based TypeScript game engine for quick prototypes. Runs directly in the browser.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",