@codexo/exojs-react 0.14.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/esm/ExoCanvas.d.ts +43 -0
- package/dist/esm/ExoCanvas.js +36 -0
- package/dist/esm/ExoCanvas.js.map +1 -0
- package/dist/esm/ExoContext.d.ts +15 -0
- package/dist/esm/ExoContext.js +23 -0
- package/dist/esm/ExoContext.js.map +1 -0
- package/dist/esm/Scenes.d.ts +53 -0
- package/dist/esm/Scenes.js +95 -0
- package/dist/esm/Scenes.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/useExoApp.d.ts +17 -0
- package/dist/esm/useExoApp.js +27 -0
- package/dist/esm/useExoApp.js.map +1 -0
- package/dist/esm/useExoApplication.d.ts +57 -0
- package/dist/esm/useExoApplication.js +103 -0
- package/dist/esm/useExoApplication.js.map +1 -0
- package/dist/esm/useScene.d.ts +29 -0
- package/dist/esm/useScene.js +67 -0
- package/dist/esm/useScene.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codexo
|
|
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
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @codexo/exojs-react
|
|
2
|
+
|
|
3
|
+
React 18 / 19 bindings for [ExoJS](https://exojs.dev) — mount an ExoJS
|
|
4
|
+
`Application` into your React tree, drive scenes declaratively, and overlay React
|
|
5
|
+
HUD on the canvas.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @codexo/exojs @codexo/exojs-react react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@codexo/exojs` and `react` (>= 18) are peer dependencies; `react-dom` is an
|
|
14
|
+
optional peer. The package ships pre-built ESM (`dist/esm`) with type
|
|
15
|
+
declarations and works on both `@types/react` 18 and 19.
|
|
16
|
+
|
|
17
|
+
## Two layers, pick what you need
|
|
18
|
+
|
|
19
|
+
This package is intentionally layered:
|
|
20
|
+
|
|
21
|
+
- **`useExoApplication` — headless.** Creates and owns the `Application`, binds
|
|
22
|
+
it to a `<canvas>` you render yourself. No DOM, no wrapper, no styling
|
|
23
|
+
opinions — full control.
|
|
24
|
+
- **`<ExoCanvas>` — batteries-included.** Renders a positioned wrapper `<div>` +
|
|
25
|
+
a React-managed `<canvas>` and provides the app via context, so HUD overlays
|
|
26
|
+
work out of the box.
|
|
27
|
+
|
|
28
|
+
## Quick start — `<ExoCanvas>`
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { ExoCanvas, Scenes, Scene, useExoApp } from '@codexo/exojs-react';
|
|
32
|
+
import { TitleScene, GameScene } from './scenes';
|
|
33
|
+
|
|
34
|
+
function Game() {
|
|
35
|
+
return (
|
|
36
|
+
<ExoCanvas
|
|
37
|
+
options={{ canvas: { width: 1280, height: 720 }, clearColor: someColor }}
|
|
38
|
+
style={{ width: 1280, height: 720 }}
|
|
39
|
+
>
|
|
40
|
+
<Scenes active="game" transition={{ type: 'fade', duration: 0.3 }}>
|
|
41
|
+
<Scene name="title" component={TitleScene} />
|
|
42
|
+
<Scene name="game" component={GameScene}>
|
|
43
|
+
<Hud /> {/* absolutely-positioned React overlay, over the canvas */}
|
|
44
|
+
</Scene>
|
|
45
|
+
</Scenes>
|
|
46
|
+
</ExoCanvas>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Hud() {
|
|
51
|
+
const app = useExoApp();
|
|
52
|
+
return <div style={{ position: 'absolute', top: 8, left: 8 }}>FPS overlay…</div>;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Layout props (`style`, `className`, …) apply to the **wrapper**; size it to
|
|
57
|
+
drive `'fill'`/`'letterbox'` sizing. Style the canvas itself via `canvasProps`.
|
|
58
|
+
|
|
59
|
+
## Quick start — headless hook (full control)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { useExoApplication } from '@codexo/exojs-react';
|
|
63
|
+
|
|
64
|
+
function Game() {
|
|
65
|
+
const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });
|
|
66
|
+
// Render the canvas however and wherever you want.
|
|
67
|
+
return <canvas ref={canvasRef} className="my-canvas" />;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
| Export | Kind | Purpose |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `ExoCanvas` | component | Batteries-included canvas host (wrapper div + canvas + context). |
|
|
76
|
+
| `useExoApplication(options?, onReady?)` | hook | Headless: owns the `Application`, returns `{ app, canvasRef }`. |
|
|
77
|
+
| `useExoApp()` | hook | The `Application` from the nearest `<ExoCanvas>`/provider. Throws if absent. |
|
|
78
|
+
| `useExoContext()` | hook | Like `useExoApp` but returns `Application \| null` (no throw). |
|
|
79
|
+
| `ExoContext` | context | The underlying context (advanced / testing). |
|
|
80
|
+
| `useScene(SceneClass, deps?)` | hook | Instantiate + activate a single scene; returns it once live. |
|
|
81
|
+
| `Scenes` / `Scene` | components | Declarative scene switch over the one-active-scene model. |
|
|
82
|
+
| `useActiveScene()` | hook | The active scene instance from the nearest `<Scenes>`. |
|
|
83
|
+
|
|
84
|
+
### Reactivity model
|
|
85
|
+
|
|
86
|
+
The `Application` is recreated only when an **identity** option changes — the
|
|
87
|
+
render `backend` (WebGL2 ↔ WebGPU cannot be hot-swapped). Other supported options
|
|
88
|
+
are applied **live**:
|
|
89
|
+
|
|
90
|
+
- `canvas.width` / `canvas.height` → `app.resize(...)`
|
|
91
|
+
- `canvas.sizingMode` → `app.sizingMode`
|
|
92
|
+
- `clearColor` → `app.clearColor`
|
|
93
|
+
|
|
94
|
+
Options without a live setter (`canvas.pixelRatio`, `seed`, `extensions`, …) are
|
|
95
|
+
captured at creation; change the `backend` or remount to apply them.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT © Codexo
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Application } from '@codexo/exojs';
|
|
2
|
+
import { type CanvasHTMLAttributes, type HTMLAttributes, type ReactElement } from 'react';
|
|
3
|
+
import { type ExoApplicationOptions } from './useExoApplication';
|
|
4
|
+
export interface ExoCanvasProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Options forwarded to the ExoJS {@link Application}. Pass
|
|
7
|
+
* `canvas.width`/`height`/`sizingMode`/etc.; most options are captured at
|
|
8
|
+
* creation, but `canvas.width`/`height`, `canvas.sizingMode` and `clearColor`
|
|
9
|
+
* are applied live (see {@link useExoApplication}).
|
|
10
|
+
*/
|
|
11
|
+
options?: ExoApplicationOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Called once each time the {@link Application} is (re)created. The backend
|
|
14
|
+
* (WebGL2 / WebGPU) is not yet initialized at this point — that happens when
|
|
15
|
+
* the first {@link import('./useScene').useScene} child calls `app.start()`.
|
|
16
|
+
*/
|
|
17
|
+
onReady?: (app: Application) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Props forwarded to the inner `<canvas>` (e.g. its own `style`/`className`).
|
|
20
|
+
* `ref`, `width` and `height` are managed by the engine and cannot be set.
|
|
21
|
+
*/
|
|
22
|
+
canvasProps?: Omit<CanvasHTMLAttributes<HTMLCanvasElement>, 'ref' | 'width' | 'height'>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Batteries-included canvas host. Renders a **positioned wrapper `<div>`**
|
|
26
|
+
* containing a React-managed `<canvas>` bound to an ExoJS {@link Application},
|
|
27
|
+
* and provides the app to descendant hooks via context. Because the wrapper is
|
|
28
|
+
* `position: relative`, absolutely-positioned `children` (HUD overlays,
|
|
29
|
+
* {@link import('./Scenes').Scenes}) sit over the canvas out of the box.
|
|
30
|
+
*
|
|
31
|
+
* Layout props (`style`, `className`, …) apply to the **wrapper**; size it to
|
|
32
|
+
* size the canvas in `'fill'`/`'letterbox'` modes. Use {@link canvasProps} to
|
|
33
|
+
* style the canvas itself. For full control with no wrapper element, use the
|
|
34
|
+
* headless {@link useExoApplication} hook directly.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <ExoCanvas options={{ canvas: { width: 800, height: 600 } }} style={{ width: 800, height: 600 }}>
|
|
39
|
+
* <Hud /> // absolutely-positioned overlay; works because the wrapper is relative
|
|
40
|
+
* </ExoCanvas>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function ExoCanvas({ options, onReady, canvasProps, children, style, ...divProps }: ExoCanvasProps): ReactElement;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import 'react';
|
|
3
|
+
import { ExoContext } from './ExoContext.js';
|
|
4
|
+
import { useExoApplication } from './useExoApplication.js';
|
|
5
|
+
|
|
6
|
+
/** Default canvas style: block layout avoids the inline-element baseline gap. */
|
|
7
|
+
const defaultCanvasStyle = { display: 'block' };
|
|
8
|
+
/**
|
|
9
|
+
* Batteries-included canvas host. Renders a **positioned wrapper `<div>`**
|
|
10
|
+
* containing a React-managed `<canvas>` bound to an ExoJS {@link Application},
|
|
11
|
+
* and provides the app to descendant hooks via context. Because the wrapper is
|
|
12
|
+
* `position: relative`, absolutely-positioned `children` (HUD overlays,
|
|
13
|
+
* {@link import('./Scenes').Scenes}) sit over the canvas out of the box.
|
|
14
|
+
*
|
|
15
|
+
* Layout props (`style`, `className`, …) apply to the **wrapper**; size it to
|
|
16
|
+
* size the canvas in `'fill'`/`'letterbox'` modes. Use {@link canvasProps} to
|
|
17
|
+
* style the canvas itself. For full control with no wrapper element, use the
|
|
18
|
+
* headless {@link useExoApplication} hook directly.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ExoCanvas options={{ canvas: { width: 800, height: 600 } }} style={{ width: 800, height: 600 }}>
|
|
23
|
+
* <Hud /> // absolutely-positioned overlay; works because the wrapper is relative
|
|
24
|
+
* </ExoCanvas>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
function ExoCanvas({ options, onReady, canvasProps, children, style, ...divProps }) {
|
|
28
|
+
const { app, canvasRef } = useExoApplication(options, onReady);
|
|
29
|
+
const { style: canvasStyle, ...restCanvasProps } = canvasProps ?? {};
|
|
30
|
+
const wrapperStyle = { position: 'relative', ...style };
|
|
31
|
+
const mergedCanvasStyle = { ...defaultCanvasStyle, ...canvasStyle };
|
|
32
|
+
return (jsx(ExoContext.Provider, { value: app, children: jsxs("div", { style: wrapperStyle, ...divProps, children: [jsx("canvas", { ref: canvasRef, style: mergedCanvasStyle, ...restCanvasProps }), app !== null && children] }) }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { ExoCanvas };
|
|
36
|
+
//# sourceMappingURL=ExoCanvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExoCanvas.js","sources":["../../../src/ExoCanvas.tsx"],"sourcesContent":[null],"names":["_jsx","_jsxs"],"mappings":";;;;;AAMA;AACA,MAAM,kBAAkB,GAAkB,EAAE,OAAO,EAAE,OAAO,EAAE;AAuB9D;;;;;;;;;;;;;;;;;;AAkBG;SACa,SAAS,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,QAAQ,EAAkB,EAAA;AACvG,IAAA,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC;AAE9D,IAAA,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,eAAe,EAAE,GAAG,WAAW,IAAI,EAAE;IACpE,MAAM,YAAY,GAAkB,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE;IACtE,MAAM,iBAAiB,GAAkB,EAAE,GAAG,kBAAkB,EAAE,GAAG,WAAW,EAAE;AAElF,IAAA,QACEA,GAAA,CAAC,UAAU,CAAC,QAAQ,IAAC,KAAK,EAAE,GAAG,EAAA,QAAA,EAC7BC,cAAK,KAAK,EAAE,YAAY,EAAA,GAAM,QAAQ,EAAA,QAAA,EAAA,CACpCD,GAAA,CAAA,QAAA,EAAA,EAAQ,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,KAAM,eAAe,EAAA,CAAI,EACxE,GAAG,KAAK,IAAI,IAAI,QAAQ,CAAA,EAAA,CACrB,EAAA,CACc;AAE1B;;;;"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Application } from '@codexo/exojs';
|
|
2
|
+
/**
|
|
3
|
+
* Internal React context that carries the active {@link Application} instance
|
|
4
|
+
* created by {@link ExoCanvas}. Consumers should use the {@link useExoApp}
|
|
5
|
+
* hook rather than reading this context directly; the context object is
|
|
6
|
+
* exported for advanced use (e.g. testing, custom providers).
|
|
7
|
+
*/
|
|
8
|
+
export declare const ExoContext: import("react").Context<Application | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Returns the nearest {@link Application} from the React tree, or `null`
|
|
11
|
+
* when called outside of an {@link ExoCanvas}. Prefer {@link useExoApp} for
|
|
12
|
+
* component-level use — it throws an actionable error instead of returning
|
|
13
|
+
* null.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useExoContext(): Application | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal React context that carries the active {@link Application} instance
|
|
5
|
+
* created by {@link ExoCanvas}. Consumers should use the {@link useExoApp}
|
|
6
|
+
* hook rather than reading this context directly; the context object is
|
|
7
|
+
* exported for advanced use (e.g. testing, custom providers).
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
10
|
+
const ExoContext = createContext(null);
|
|
11
|
+
ExoContext.displayName = 'ExoContext';
|
|
12
|
+
/**
|
|
13
|
+
* Returns the nearest {@link Application} from the React tree, or `null`
|
|
14
|
+
* when called outside of an {@link ExoCanvas}. Prefer {@link useExoApp} for
|
|
15
|
+
* component-level use — it throws an actionable error instead of returning
|
|
16
|
+
* null.
|
|
17
|
+
*/
|
|
18
|
+
function useExoContext() {
|
|
19
|
+
return useContext(ExoContext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { ExoContext, useExoContext };
|
|
23
|
+
//# sourceMappingURL=ExoContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExoContext.js","sources":["../../../src/ExoContext.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAGA;;;;;AAKG;AACH;MACa,UAAU,GAAG,aAAa,CAAqB,IAAI;AAChE,UAAU,CAAC,WAAW,GAAG,YAAY;AAErC;;;;;AAKG;SACa,aAAa,GAAA;AAC3B,IAAA,OAAO,UAAU,CAAC,UAAU,CAAC;AAC/B;;;;"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Scene as ExoScene, type SceneTransition } from '@codexo/exojs';
|
|
2
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the currently-active scene instance from the nearest {@link Scenes},
|
|
5
|
+
* or `null` while none is live. Useful for HUD/overlay components that need to
|
|
6
|
+
* read scene state.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useActiveScene<T extends ExoScene = ExoScene>(): T | null;
|
|
9
|
+
/** Props for a {@link Scene} declaration. */
|
|
10
|
+
export interface SceneProps {
|
|
11
|
+
/** Unique name used to select this scene via {@link ScenesProps.active}. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** Scene class to instantiate when this scene becomes active. */
|
|
14
|
+
readonly component: new () => ExoScene;
|
|
15
|
+
/** React overlay (HUD) rendered only while this scene is active. */
|
|
16
|
+
readonly children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Declares one scene inside a {@link Scenes} switch. Renders nothing on its own —
|
|
20
|
+
* {@link Scenes} reads its props and renders its {@link SceneProps.children} only
|
|
21
|
+
* while the scene is active.
|
|
22
|
+
*/
|
|
23
|
+
export declare function Scene(_props: SceneProps): ReactElement | null;
|
|
24
|
+
/** Props for the {@link Scenes} switch. */
|
|
25
|
+
export interface ScenesProps {
|
|
26
|
+
/** Name of the active {@link Scene}. Changing it switches scenes. */
|
|
27
|
+
readonly active: string;
|
|
28
|
+
/** Optional transition (e.g. a fade) applied when switching scenes. */
|
|
29
|
+
readonly transition?: SceneTransition;
|
|
30
|
+
/** {@link Scene} declarations. */
|
|
31
|
+
readonly children?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Declarative scene switch over the one-active-scene model. Renders a set of
|
|
35
|
+
* {@link Scene} declarations and activates the one whose `name` equals `active`
|
|
36
|
+
* via `app.start()` (first activation) or `app.scene.setScene()` (subsequent
|
|
37
|
+
* switches, with the optional `transition`). The active scene's React children
|
|
38
|
+
* (HUD overlay) render alongside, and can read the instance via
|
|
39
|
+
* {@link useActiveScene}.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <ExoCanvas>
|
|
44
|
+
* <Scenes active={screen} transition={{ type: 'fade', duration: 0.3 }}>
|
|
45
|
+
* <Scene name="title" component={TitleScene} />
|
|
46
|
+
* <Scene name="game" component={GameScene}>
|
|
47
|
+
* <Hud />
|
|
48
|
+
* </Scene>
|
|
49
|
+
* </Scenes>
|
|
50
|
+
* </ExoCanvas>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function Scenes({ active, transition, children }: ScenesProps): ReactElement;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { ApplicationStatus } from '@codexo/exojs';
|
|
3
|
+
import { createContext, useState, useMemo, Children, isValidElement, useEffect, useContext } from 'react';
|
|
4
|
+
import { useExoApp } from './useExoApp.js';
|
|
5
|
+
|
|
6
|
+
/** Carries the active {@link ExoScene} instance to descendants (HUD overlays). */
|
|
7
|
+
const ActiveSceneContext = createContext(null);
|
|
8
|
+
ActiveSceneContext.displayName = 'ExoActiveScene';
|
|
9
|
+
/**
|
|
10
|
+
* Returns the currently-active scene instance from the nearest {@link Scenes},
|
|
11
|
+
* or `null` while none is live. Useful for HUD/overlay components that need to
|
|
12
|
+
* read scene state.
|
|
13
|
+
*/
|
|
14
|
+
function useActiveScene() {
|
|
15
|
+
return useContext(ActiveSceneContext);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Declares one scene inside a {@link Scenes} switch. Renders nothing on its own —
|
|
19
|
+
* {@link Scenes} reads its props and renders its {@link SceneProps.children} only
|
|
20
|
+
* while the scene is active.
|
|
21
|
+
*/
|
|
22
|
+
function Scene(_props) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Declarative scene switch over the one-active-scene model. Renders a set of
|
|
27
|
+
* {@link Scene} declarations and activates the one whose `name` equals `active`
|
|
28
|
+
* via `app.start()` (first activation) or `app.scene.setScene()` (subsequent
|
|
29
|
+
* switches, with the optional `transition`). The active scene's React children
|
|
30
|
+
* (HUD overlay) render alongside, and can read the instance via
|
|
31
|
+
* {@link useActiveScene}.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <ExoCanvas>
|
|
36
|
+
* <Scenes active={screen} transition={{ type: 'fade', duration: 0.3 }}>
|
|
37
|
+
* <Scene name="title" component={TitleScene} />
|
|
38
|
+
* <Scene name="game" component={GameScene}>
|
|
39
|
+
* <Hud />
|
|
40
|
+
* </Scene>
|
|
41
|
+
* </Scenes>
|
|
42
|
+
* </ExoCanvas>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function Scenes({ active, transition, children }) {
|
|
46
|
+
const app = useExoApp();
|
|
47
|
+
const [instance, setInstance] = useState(null);
|
|
48
|
+
// Collect the <Scene> declarations from children (keyed by name).
|
|
49
|
+
const registry = useMemo(() => {
|
|
50
|
+
const map = new Map();
|
|
51
|
+
Children.forEach(children, child => {
|
|
52
|
+
if (isValidElement(child) && child.type === Scene) {
|
|
53
|
+
const props = child.props;
|
|
54
|
+
map.set(props.name, props);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return map;
|
|
58
|
+
}, [children]);
|
|
59
|
+
const entry = registry.get(active);
|
|
60
|
+
const SceneClass = entry?.component ?? null;
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (SceneClass === null) {
|
|
63
|
+
setInstance(null);
|
|
64
|
+
void app.scene.setScene(null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
const scene = new SceneClass();
|
|
69
|
+
const apply = async () => {
|
|
70
|
+
if (app.status === ApplicationStatus.Stopped) {
|
|
71
|
+
// First activation initializes the backend and starts the frame loop;
|
|
72
|
+
// transitions only apply to subsequent switches.
|
|
73
|
+
await app.start(scene);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await app.scene.setScene(scene, transition !== undefined ? { transition } : {});
|
|
77
|
+
}
|
|
78
|
+
if (!cancelled) {
|
|
79
|
+
setInstance(scene);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
void apply();
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
setInstance(null);
|
|
86
|
+
};
|
|
87
|
+
// Re-activate when the active name changes. SceneClass/transition derive
|
|
88
|
+
// from `active`; keying on app + active avoids re-instantiating each render.
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
}, [app, active]);
|
|
91
|
+
return (jsx(ActiveSceneContext.Provider, { value: instance, children: instance !== null && entry?.children }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { Scene, Scenes, useActiveScene };
|
|
95
|
+
//# sourceMappingURL=Scenes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Scenes.js","sources":["../../../src/Scenes.tsx"],"sourcesContent":[null],"names":["_jsx"],"mappings":";;;;;AAeA;AACA,MAAM,kBAAkB,GAAG,aAAa,CAAkB,IAAI,CAAC;AAC/D,kBAAkB,CAAC,WAAW,GAAG,gBAAgB;AAEjD;;;;AAIG;SACa,cAAc,GAAA;AAC5B,IAAA,OAAO,UAAU,CAAC,kBAAkB,CAAa;AACnD;AAYA;;;;AAIG;AACG,SAAU,KAAK,CAAC,MAAkB,EAAA;AACtC,IAAA,OAAO,IAAI;AACb;AAYA;;;;;;;;;;;;;;;;;;;AAmBG;AACG,SAAU,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAe,EAAA;AAClE,IAAA,MAAM,GAAG,GAAG,SAAS,EAAE;IACvB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAkB,IAAI,CAAC;;AAG/D,IAAA,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAK;AAC5B,QAAA,MAAM,GAAG,GAAG,IAAI,GAAG,EAAsB;AACzC,QAAA,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,IAAG;YACjC,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE;AACjD,gBAAA,MAAM,KAAK,GAAG,KAAK,CAAC,KAAmB;gBACvC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC;YAC5B;AACF,QAAA,CAAC,CAAC;AACF,QAAA,OAAO,GAAG;AACZ,IAAA,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IAEd,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;AAClC,IAAA,MAAM,UAAU,GAAG,KAAK,EAAE,SAAS,IAAI,IAAI;IAE3C,SAAS,CAAC,MAAK;AACb,QAAA,IAAI,UAAU,KAAK,IAAI,EAAE;YACvB,WAAW,CAAC,IAAI,CAAC;YACjB,KAAK,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC7B;QACF;QAEA,IAAI,SAAS,GAAG,KAAK;AACrB,QAAA,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE;AAE9B,QAAA,MAAM,KAAK,GAAG,YAA0B;YACtC,IAAI,GAAG,CAAC,MAAM,KAAK,iBAAiB,CAAC,OAAO,EAAE;;;AAG5C,gBAAA,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC;YACxB;iBAAO;gBACL,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,KAAK,SAAS,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;YACjF;YACA,IAAI,CAAC,SAAS,EAAE;gBACd,WAAW,CAAC,KAAK,CAAC;YACpB;AACF,QAAA,CAAC;QAED,KAAK,KAAK,EAAE;AAEZ,QAAA,OAAO,MAAK;YACV,SAAS,GAAG,IAAI;YAChB,WAAW,CAAC,IAAI,CAAC;AACnB,QAAA,CAAC;;;;AAIH,IAAA,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAEjB,IAAA,QACEA,GAAA,CAAC,kBAAkB,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,QAAQ,YACzC,QAAQ,KAAK,IAAI,IAAI,KAAK,EAAE,QAAQ,EAAA,CACT;AAElC;;;;"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { ExoCanvasProps } from './ExoCanvas';
|
|
2
|
+
export { ExoCanvas } from './ExoCanvas';
|
|
3
|
+
export { ExoContext, useExoContext } from './ExoContext';
|
|
4
|
+
export type { SceneProps, ScenesProps } from './Scenes';
|
|
5
|
+
export { Scene, Scenes, useActiveScene } from './Scenes';
|
|
6
|
+
export { useExoApp } from './useExoApp';
|
|
7
|
+
export type { ExoApplicationOptions, UseExoApplicationResult } from './useExoApplication';
|
|
8
|
+
export { useExoApplication } from './useExoApplication';
|
|
9
|
+
export { useScene } from './useScene';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ExoCanvas } from './ExoCanvas.js';
|
|
2
|
+
export { ExoContext, useExoContext } from './ExoContext.js';
|
|
3
|
+
export { Scene, Scenes, useActiveScene } from './Scenes.js';
|
|
4
|
+
export { useExoApp } from './useExoApp.js';
|
|
5
|
+
export { useExoApplication } from './useExoApplication.js';
|
|
6
|
+
export { useScene } from './useScene.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Application } from '@codexo/exojs';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the {@link Application} instance from the nearest {@link ExoCanvas}
|
|
4
|
+
* ancestor. Throws an informative error when called outside of an
|
|
5
|
+
* `<ExoCanvas>` tree.
|
|
6
|
+
*
|
|
7
|
+
* @throws {Error} When no `<ExoCanvas>` ancestor is present.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function HudOverlay() {
|
|
12
|
+
* const app = useExoApp();
|
|
13
|
+
* return <span>Frame: {app.frameCount}</span>;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function useExoApp(): Application;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useExoContext } from './ExoContext.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the {@link Application} instance from the nearest {@link ExoCanvas}
|
|
5
|
+
* ancestor. Throws an informative error when called outside of an
|
|
6
|
+
* `<ExoCanvas>` tree.
|
|
7
|
+
*
|
|
8
|
+
* @throws {Error} When no `<ExoCanvas>` ancestor is present.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function HudOverlay() {
|
|
13
|
+
* const app = useExoApp();
|
|
14
|
+
* return <span>Frame: {app.frameCount}</span>;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function useExoApp() {
|
|
19
|
+
const app = useExoContext();
|
|
20
|
+
if (app === null) {
|
|
21
|
+
throw new Error('useExoApp must be used inside an <ExoCanvas> component.');
|
|
22
|
+
}
|
|
23
|
+
return app;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { useExoApp };
|
|
27
|
+
//# sourceMappingURL=useExoApp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExoApp.js","sources":["../../../src/useExoApp.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAIA;;;;;;;;;;;;;;AAcG;SACa,SAAS,GAAA;AACvB,IAAA,MAAM,GAAG,GAAG,aAAa,EAAE;AAE3B,IAAA,IAAI,GAAG,KAAK,IAAI,EAAE;AAChB,QAAA,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC;IAC5E;AAEA,IAAA,OAAO,GAAG;AACZ;;;;"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Application, type ApplicationOptions, type CanvasApplicationOptions } from '@codexo/exojs';
|
|
2
|
+
import { type Ref } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link useExoApplication} / {@link import('./ExoCanvas').ExoCanvas}.
|
|
5
|
+
*
|
|
6
|
+
* Same as {@link ApplicationOptions} but the `canvas.element` and `canvas.mount`
|
|
7
|
+
* fields are managed for you (the Application binds to the canvas the hook
|
|
8
|
+
* references), so they are omitted. You may still pass
|
|
9
|
+
* `canvas.width`/`height`/`sizingMode`/etc.
|
|
10
|
+
*/
|
|
11
|
+
export type ExoApplicationOptions = Omit<ApplicationOptions, 'canvas'> & {
|
|
12
|
+
readonly canvas?: Omit<CanvasApplicationOptions, 'element' | 'mount'>;
|
|
13
|
+
};
|
|
14
|
+
/** Return value of {@link useExoApplication}. */
|
|
15
|
+
export interface UseExoApplicationResult {
|
|
16
|
+
/** The Application instance, or `null` until it has been created. */
|
|
17
|
+
readonly app: Application | null;
|
|
18
|
+
/**
|
|
19
|
+
* Attach this to the `<canvas>` element the Application should bind to. Typed
|
|
20
|
+
* as `Ref` (not `RefObject`) so the same code type-checks against both
|
|
21
|
+
* `@types/react` 18 and 19, whose `useRef`/`RefObject` nullability differ.
|
|
22
|
+
*/
|
|
23
|
+
readonly canvasRef: Ref<HTMLCanvasElement>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates and owns an ExoJS {@link Application}, binding it to a `<canvas>` you
|
|
27
|
+
* render yourself and attach the returned `canvasRef` to. The hook renders no
|
|
28
|
+
* DOM of its own — you keep full control over the canvas element, its container,
|
|
29
|
+
* and its styling.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function Game() {
|
|
33
|
+
* const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });
|
|
34
|
+
* return <canvas ref={canvasRef} className="game" />;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* **Reactivity model.** The Application is recreated only when an *identity*
|
|
39
|
+
* option changes — currently the render `backend` (you cannot hot-swap WebGL2 ↔
|
|
40
|
+
* WebGPU). All other supported options are applied *live* without tearing the
|
|
41
|
+
* app down:
|
|
42
|
+
*
|
|
43
|
+
* - `canvas.width` / `canvas.height` → `app.resize(...)`
|
|
44
|
+
* - `canvas.sizingMode` → `app.sizingMode`
|
|
45
|
+
* - `clearColor` → `app.clearColor`
|
|
46
|
+
*
|
|
47
|
+
* Options without a live setter (e.g. `canvas.pixelRatio`, `seed`, `extensions`)
|
|
48
|
+
* are captured at creation; change the `backend` or remount to apply them.
|
|
49
|
+
*
|
|
50
|
+
* Styling note: with the default `'fixed'` sizing mode the engine never touches
|
|
51
|
+
* the canvas CSS, so you may style it freely. The `'fit'`/`'shrink'`/`'letterbox'`
|
|
52
|
+
* modes manage `canvas.style` themselves — don't fight them with a `style` prop.
|
|
53
|
+
*
|
|
54
|
+
* @param options - Application options (the canvas element is the one you render).
|
|
55
|
+
* @param onReady - Called once each time an Application is created.
|
|
56
|
+
*/
|
|
57
|
+
export declare function useExoApplication(options?: ExoApplicationOptions, onReady?: (app: Application) => void): UseExoApplicationResult;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Application } from '@codexo/exojs';
|
|
2
|
+
import { useRef, useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Stable string key for the colour so the sync effect can depend on its value. */
|
|
5
|
+
function colorKey(color) {
|
|
6
|
+
return color === undefined ? undefined : `${color.r},${color.g},${color.b},${color.a}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates and owns an ExoJS {@link Application}, binding it to a `<canvas>` you
|
|
10
|
+
* render yourself and attach the returned `canvasRef` to. The hook renders no
|
|
11
|
+
* DOM of its own — you keep full control over the canvas element, its container,
|
|
12
|
+
* and its styling.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function Game() {
|
|
16
|
+
* const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });
|
|
17
|
+
* return <canvas ref={canvasRef} className="game" />;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* **Reactivity model.** The Application is recreated only when an *identity*
|
|
22
|
+
* option changes — currently the render `backend` (you cannot hot-swap WebGL2 ↔
|
|
23
|
+
* WebGPU). All other supported options are applied *live* without tearing the
|
|
24
|
+
* app down:
|
|
25
|
+
*
|
|
26
|
+
* - `canvas.width` / `canvas.height` → `app.resize(...)`
|
|
27
|
+
* - `canvas.sizingMode` → `app.sizingMode`
|
|
28
|
+
* - `clearColor` → `app.clearColor`
|
|
29
|
+
*
|
|
30
|
+
* Options without a live setter (e.g. `canvas.pixelRatio`, `seed`, `extensions`)
|
|
31
|
+
* are captured at creation; change the `backend` or remount to apply them.
|
|
32
|
+
*
|
|
33
|
+
* Styling note: with the default `'fixed'` sizing mode the engine never touches
|
|
34
|
+
* the canvas CSS, so you may style it freely. The `'fit'`/`'shrink'`/`'letterbox'`
|
|
35
|
+
* modes manage `canvas.style` themselves — don't fight them with a `style` prop.
|
|
36
|
+
*
|
|
37
|
+
* @param options - Application options (the canvas element is the one you render).
|
|
38
|
+
* @param onReady - Called once each time an Application is created.
|
|
39
|
+
*/
|
|
40
|
+
function useExoApplication(options, onReady) {
|
|
41
|
+
const canvasRef = useRef(null);
|
|
42
|
+
const [app, setApp] = useState(null);
|
|
43
|
+
// Latest onReady without retriggering the lifecycle effect. Updated in an
|
|
44
|
+
// effect (not during render) so the ref-write happens after commit.
|
|
45
|
+
const onReadyRef = useRef(onReady);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
onReadyRef.current = onReady;
|
|
48
|
+
});
|
|
49
|
+
// Identity: only the backend type forces a full recreation.
|
|
50
|
+
const backendKey = options?.backend?.type ?? 'auto';
|
|
51
|
+
// ── Lifecycle: create on mount / recreate on backend change ───────────────
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const canvas = canvasRef.current;
|
|
54
|
+
if (!canvas) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Bind to the React-rendered canvas. The engine never removes a canvas it
|
|
58
|
+
// did not create (Application.destroy leaves it in the DOM), so React stays
|
|
59
|
+
// the sole owner of the element's lifecycle.
|
|
60
|
+
const application = new Application({
|
|
61
|
+
...options,
|
|
62
|
+
canvas: { ...options?.canvas, element: canvas },
|
|
63
|
+
});
|
|
64
|
+
setApp(application);
|
|
65
|
+
onReadyRef.current?.(application);
|
|
66
|
+
return () => {
|
|
67
|
+
application.destroy();
|
|
68
|
+
setApp(null);
|
|
69
|
+
};
|
|
70
|
+
// Recreate only when the backend identity changes; live options are synced
|
|
71
|
+
// by the effects below. `options` is intentionally read at (re)create time.
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [backendKey]);
|
|
74
|
+
// ── Live sync: size ───────────────────────────────────────────────────────
|
|
75
|
+
const width = options?.canvas?.width;
|
|
76
|
+
const height = options?.canvas?.height;
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (app !== null && width !== undefined && height !== undefined) {
|
|
79
|
+
app.resize(width, height);
|
|
80
|
+
}
|
|
81
|
+
}, [app, width, height]);
|
|
82
|
+
// ── Live sync: sizing mode ────────────────────────────────────────────────
|
|
83
|
+
const sizingMode = options?.canvas?.sizingMode;
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (app !== null && sizingMode !== undefined) {
|
|
86
|
+
app.sizingMode = sizingMode;
|
|
87
|
+
}
|
|
88
|
+
}, [app, sizingMode]);
|
|
89
|
+
// ── Live sync: clear colour ───────────────────────────────────────────────
|
|
90
|
+
const clearColor = options?.clearColor;
|
|
91
|
+
const clearKey = colorKey(clearColor);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (app !== null && clearColor !== undefined) {
|
|
94
|
+
app.clearColor = clearColor;
|
|
95
|
+
}
|
|
96
|
+
// clearColor identity is unstable; depend on its value key instead.
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [app, clearKey]);
|
|
99
|
+
return { app, canvasRef };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { useExoApplication };
|
|
103
|
+
//# sourceMappingURL=useExoApplication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExoApplication.js","sources":["../../../src/useExoApplication.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AA2BA;AACA,SAAS,QAAQ,CAAC,KAAwB,EAAA;IACxC,OAAO,KAAK,KAAK,SAAS,GAAG,SAAS,GAAG,CAAA,EAAG,KAAK,CAAC,CAAC,CAAA,CAAA,EAAI,KAAK,CAAC,CAAC,CAAA,CAAA,EAAI,KAAK,CAAC,CAAC,CAAA,CAAA,EAAI,KAAK,CAAC,CAAC,CAAA,CAAE;AACxF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;AACG,SAAU,iBAAiB,CAC/B,OAA+B,EAC/B,OAAoC,EAAA;AAEpC,IAAA,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC;IACjD,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAqB,IAAI,CAAC;;;AAIxD,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC;IAClC,SAAS,CAAC,MAAK;AACb,QAAA,UAAU,CAAC,OAAO,GAAG,OAAO;AAC9B,IAAA,CAAC,CAAC;;IAGF,MAAM,UAAU,GAAG,OAAO,EAAE,OAAO,EAAE,IAAI,IAAI,MAAM;;IAGnD,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO;QAChC,IAAI,CAAC,MAAM,EAAE;YACX;QACF;;;;AAKA,QAAA,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC;AAClC,YAAA,GAAG,OAAO;YACV,MAAM,EAAE,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;AAChD,SAAA,CAAC;QAEF,MAAM,CAAC,WAAW,CAAC;AACnB,QAAA,UAAU,CAAC,OAAO,GAAG,WAAW,CAAC;AAEjC,QAAA,OAAO,MAAK;YACV,WAAW,CAAC,OAAO,EAAE;YACrB,MAAM,CAAC,IAAI,CAAC;AACd,QAAA,CAAC;;;;AAIH,IAAA,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;;AAGhB,IAAA,MAAM,KAAK,GAAG,OAAO,EAAE,MAAM,EAAE,KAAK;AACpC,IAAA,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM;IACtC,SAAS,CAAC,MAAK;AACb,QAAA,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE;AAC/D,YAAA,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC;QAC3B;IACF,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;;AAGxB,IAAA,MAAM,UAAU,GAAG,OAAO,EAAE,MAAM,EAAE,UAAU;IAC9C,SAAS,CAAC,MAAK;QACb,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU,KAAK,SAAS,EAAE;AAC5C,YAAA,GAAG,CAAC,UAAU,GAAG,UAAU;QAC7B;AACF,IAAA,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;;AAGrB,IAAA,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU;AACtC,IAAA,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC;IACrC,SAAS,CAAC,MAAK;QACb,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU,KAAK,SAAS,EAAE;AAC5C,YAAA,GAAG,CAAC,UAAU,GAAG,UAAU;QAC7B;;;AAGF,IAAA,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAEnB,IAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE;AAC3B;;;;"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Scene } from '@codexo/exojs';
|
|
2
|
+
import { type DependencyList } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an instance of `SceneClass`, activates it on the ExoJS
|
|
5
|
+
* {@link Application}, and returns it once the scene is live.
|
|
6
|
+
*
|
|
7
|
+
* On first call (engine not yet started) this hook calls `app.start(scene)`,
|
|
8
|
+
* which initializes the render backend and begins the per-frame loop. On
|
|
9
|
+
* subsequent dep-change remounts it calls `app.scene.setScene(scene)` to
|
|
10
|
+
* switch scenes without restarting the engine.
|
|
11
|
+
*
|
|
12
|
+
* The scene is cleared (`setScene(null)`) when the component unmounts or
|
|
13
|
+
* when `deps` change — mirroring `useEffect` semantics.
|
|
14
|
+
*
|
|
15
|
+
* @param SceneClass - Constructor for the scene to instantiate.
|
|
16
|
+
* @param deps - Extra deps that trigger scene replacement when changed, in
|
|
17
|
+
* addition to the stable `app` reference (same semantics as `useEffect`).
|
|
18
|
+
* @returns The active scene instance, or `null` while it is loading.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* function GameScreen() {
|
|
23
|
+
* const scene = useScene(MyGameScene);
|
|
24
|
+
* if (!scene) return null;
|
|
25
|
+
* return <ScoreHud scene={scene} />;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function useScene<T extends Scene>(SceneClass: new () => T, deps?: DependencyList): T | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ApplicationStatus } from '@codexo/exojs';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useExoApp } from './useExoApp.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an instance of `SceneClass`, activates it on the ExoJS
|
|
7
|
+
* {@link Application}, and returns it once the scene is live.
|
|
8
|
+
*
|
|
9
|
+
* On first call (engine not yet started) this hook calls `app.start(scene)`,
|
|
10
|
+
* which initializes the render backend and begins the per-frame loop. On
|
|
11
|
+
* subsequent dep-change remounts it calls `app.scene.setScene(scene)` to
|
|
12
|
+
* switch scenes without restarting the engine.
|
|
13
|
+
*
|
|
14
|
+
* The scene is cleared (`setScene(null)`) when the component unmounts or
|
|
15
|
+
* when `deps` change — mirroring `useEffect` semantics.
|
|
16
|
+
*
|
|
17
|
+
* @param SceneClass - Constructor for the scene to instantiate.
|
|
18
|
+
* @param deps - Extra deps that trigger scene replacement when changed, in
|
|
19
|
+
* addition to the stable `app` reference (same semantics as `useEffect`).
|
|
20
|
+
* @returns The active scene instance, or `null` while it is loading.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* function GameScreen() {
|
|
25
|
+
* const scene = useScene(MyGameScene);
|
|
26
|
+
* if (!scene) return null;
|
|
27
|
+
* return <ScoreHud scene={scene} />;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
32
|
+
function useScene(SceneClass, deps = []) {
|
|
33
|
+
const app = useExoApp();
|
|
34
|
+
const [scene, setScene] = useState(null);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
let cancelled = false;
|
|
37
|
+
const s = new SceneClass();
|
|
38
|
+
const apply = async () => {
|
|
39
|
+
if (app.status === ApplicationStatus.Stopped) {
|
|
40
|
+
// First activation — initialize the backend and start the frame loop.
|
|
41
|
+
await app.start(s);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Engine already running — switch scenes without restarting.
|
|
45
|
+
await app.scene.setScene(s);
|
|
46
|
+
}
|
|
47
|
+
if (!cancelled) {
|
|
48
|
+
setScene(s);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
void apply();
|
|
52
|
+
return () => {
|
|
53
|
+
cancelled = true;
|
|
54
|
+
setScene(null);
|
|
55
|
+
// Best-effort scene clear; the Application.destroy() called by
|
|
56
|
+
// ExoCanvas cleanup will also handle any remaining active scene.
|
|
57
|
+
void app.scene.setScene(null);
|
|
58
|
+
};
|
|
59
|
+
// SceneClass is intentionally excluded from deps: a new class reference
|
|
60
|
+
// (e.g. inline arrow class) on every render would recreate the scene
|
|
61
|
+
// each frame. Pass an explicit deps array to react to changes.
|
|
62
|
+
}, [app, ...deps]);
|
|
63
|
+
return scene;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { useScene };
|
|
67
|
+
//# sourceMappingURL=useScene.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useScene.js","sources":["../../../src/useScene.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;AAKA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACH;SACgB,QAAQ,CAAkB,UAAuB,EAAE,OAAuB,EAAE,EAAA;AAC1F,IAAA,MAAM,GAAG,GAAG,SAAS,EAAE;IACvB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAW,IAAI,CAAC;IAElD,SAAS,CAAC,MAAK;QACb,IAAI,SAAS,GAAG,KAAK;AACrB,QAAA,MAAM,CAAC,GAAG,IAAI,UAAU,EAAE;AAE1B,QAAA,MAAM,KAAK,GAAG,YAA0B;YACtC,IAAI,GAAG,CAAC,MAAM,KAAK,iBAAiB,CAAC,OAAO,EAAE;;AAE5C,gBAAA,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;YACpB;iBAAO;;gBAEL,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC7B;YAEA,IAAI,CAAC,SAAS,EAAE;gBACd,QAAQ,CAAC,CAAC,CAAC;YACb;AACF,QAAA,CAAC;QAED,KAAK,KAAK,EAAE;AAEZ,QAAA,OAAO,MAAK;YACV,SAAS,GAAG,IAAI;YAChB,QAAQ,CAAC,IAAI,CAAC;;;YAGd,KAAK,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;AAC/B,QAAA,CAAC;;;;IAIH,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAElB,IAAA,OAAO,KAAK;AACd;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codexo/exojs-react",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "React integration for ExoJS.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Exoridus/ExoJS.git",
|
|
8
|
+
"directory": "packages/exojs-react"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/esm/index.js",
|
|
12
|
+
"module": "./dist/esm/index.js",
|
|
13
|
+
"types": "./dist/esm/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/esm/index.d.ts",
|
|
17
|
+
"import": "./dist/esm/index.js",
|
|
18
|
+
"default": "./dist/esm/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/esm/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@codexo/exojs": "0.14.x",
|
|
29
|
+
"react": ">=18.0.0",
|
|
30
|
+
"react-dom": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"react-dom": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^18.0.0",
|
|
39
|
+
"@codexo/exojs": "0.14.0",
|
|
40
|
+
"@codexo/exojs-config": "0.0.0"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsx ../../node_modules/rollup/dist/bin/rollup -c --environment EXOJS_ENV:production",
|
|
48
|
+
"build:dev": "tsx ../../node_modules/rollup/dist/bin/rollup -c --environment EXOJS_ENV:development",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\""
|
|
51
|
+
}
|
|
52
|
+
}
|