@joystyk/render3d 0.0.0-stage → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb095c7: Add shared real-time input, pause, pace records and ghost races, plus a WebGL 2 renderer
8
+ lifecycle with context recovery and adaptive quality. Launch daily fixed-step simulations
9
+ from published configs and retain their puzzle identity through saved games.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [bb095c7]
14
+ - Updated dependencies [bb095c7]
15
+ - @joystyk/contracts@0.2.0
16
+
17
+ ## 0.1.0
18
+
19
+ Initial shared arcade kit (host policy).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joystyk contributors
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/NOTICE ADDED
@@ -0,0 +1,36 @@
1
+ Third-party material in the Joystyk library release
2
+
3
+ This notice preserves third-party attribution. Original Joystyk code in the 17
4
+ platform libraries under packages/ uses the MIT license in packages/LICENSE.
5
+ That choice does not license the private applications, games or other repository
6
+ material, and does not replace the third-party terms below.
7
+
8
+ @joystyk/contracts
9
+ ------------------
10
+ Standard Schema v1 type definitions from @standard-schema/spec 1.1.0:
11
+ https://github.com/standard-schema/standard-schema
12
+ https://standardschema.dev
13
+ Copyright (c) 2024 Colin McDonnell. MIT License.
14
+ The complete license and source/change notes accompany the distributed
15
+ dist/standard-schema.d.ts definitions. Changes: individually exported declarations
16
+ and formatting; the declarations themselves are unchanged.
17
+
18
+ @joystyk/identity and @joystyk/content
19
+ -----------------------------------
20
+ Word blocklists derived from List of Dirty, Naughty, Obscene, and Otherwise Bad
21
+ Words, copyright Shutterstock, Inc. and contributors:
22
+ https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words
23
+ Licensed under Creative Commons Attribution 4.0 International:
24
+ https://creativecommons.org/licenses/by/4.0/
25
+ Identity uses the English list at commit
26
+ 5faf2ba42d7b1c0977169ec3611df25a3c08eb13. Changes include reducing or dropping
27
+ phrases, dropping words with everyday meanings, substituting stems for inflections
28
+ and adding entries. Identity's README and content's lists/blocklist.txt retain
29
+ their source-specific provenance and changes.
30
+
31
+ @joystyk/content
32
+ ---------------
33
+ SCOWL-derived common, vocabulary and ranked word lists use SCOWL 2020.12.07.
34
+ Their complete source licenses and attribution accompany the package in
35
+ lists/SCOWL-COPYRIGHT. Word-list transformations and build provenance are described
36
+ in the package README. Preserve that copyright file when redistributing the lists.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @joystyk/render3d
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Three.js lifecycle helpers, imported only from a game's `render/` layer.
4
+
5
+ `createRenderStage({ host, build, pause, announce })` creates a strict WebGL 2 surface. The
6
+ `build` callback receives a renderer, scene and `track(resource)` disposal helper, and returns
7
+ `{ camera, draw(view, alpha, quality), resize? }`. Feed snapshots through `stage.draw`;
8
+ identical view/alpha keys are skipped. The host's CSS sizes `.render3d-canvas` to its viewport.
9
+ `stage.dispose()` releases the observer, listeners, geometries, materials, textures and context.
10
+
11
+ A lost context calls `pause`, blocks drawing and offers **Try 3D again**. Restoring or retrying
12
+ runs `build` again, so GPU resources are recreated. It never resumes the simulation.
13
+ `stage.graphics('auto' | 'low' | 'medium' | 'high')` selects adaptive or pinned quality.
14
+ Auto starts at medium and steps down after three seconds with render-time p95 over 16 ms;
15
+ it never steps up during a run. Pixel-ratio caps are 1, 1.5 and 2. The `development` option
16
+ shows p95, draw calls and triangles. Quality never changes the simulation.
17
+
18
+ `roomEnvironment`, `pickPlane` and `project` support game scenes. Existing scene classes can
19
+ use `createRenderer`, `webgl2Available` and `bindGraphicsRecovery` to share the same WebGL 2
20
+ boundary and full resource rebuild while retaining their own drawing APIs.
21
+
22
+ See host policy (Joystyk host reference) for reference-phone budgets. Desktop software
23
+ WebGL tests do not replace measurements on the reference phone.
@@ -0,0 +1,3 @@
1
+ export { createRenderStage, roomEnvironment, pickPlane, project, type SceneContext, type SceneDrawing, type RenderOptions, } from './stage.js';
2
+ export { AutoQuality, PIXEL_RATIO, type Quality, type Graphics } from './quality.js';
3
+ export { createRenderer, webgl2Available, bindGraphicsRecovery } from './lifecycle.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { AutoQuality, PIXEL_RATIO } from "./quality.js";
2
+ import { createRenderStage, pickPlane, project, roomEnvironment } from "./stage.js";
3
+ import { bindGraphicsRecovery, createRenderer, webgl2Available } from "./lifecycle.js";
4
+ export { AutoQuality, PIXEL_RATIO, bindGraphicsRecovery, createRenderStage, createRenderer, pickPlane, project, roomEnvironment, webgl2Available };
@@ -0,0 +1,13 @@
1
+ import * as THREE from 'three';
2
+ import { type Quality } from './quality.js';
3
+ /** Existing game scenes share the same strict WebGL 2 renderer boundary. */
4
+ export declare function createRenderer(options?: Omit<THREE.WebGLRendererParameters, 'canvas'> & {
5
+ canvas?: HTMLCanvasElement;
6
+ }, quality?: Quality): THREE.WebGLRenderer;
7
+ export declare function webgl2Available(): boolean;
8
+ /** Rebuilds all resources using the game's scene factory; it never resumes game time. */
9
+ export declare function bindGraphicsRecovery(host: HTMLElement, canvas: HTMLCanvasElement, o: {
10
+ pause(): void;
11
+ rebuild(): void;
12
+ announce?: (message: string) => void;
13
+ }): () => void;
@@ -0,0 +1,64 @@
1
+ import { PIXEL_RATIO } from "./quality.js";
2
+ import * as THREE from "three";
3
+ //#region src/lifecycle.ts
4
+ /** Existing game scenes share the same strict WebGL 2 renderer boundary. */
5
+ function createRenderer(options = {}, quality = "medium") {
6
+ const canvas = options.canvas ?? document.createElement("canvas");
7
+ const context = canvas.getContext("webgl2", {
8
+ antialias: options.antialias ?? true,
9
+ alpha: options.alpha ?? false,
10
+ powerPreference: options.powerPreference ?? "low-power"
11
+ });
12
+ if (context === null) throw Error("This game needs WebGL 2. Enable hardware acceleration, then try again.");
13
+ const renderer = new THREE.WebGLRenderer({
14
+ ...options,
15
+ canvas,
16
+ context
17
+ });
18
+ renderer.setPixelRatio(Math.min(devicePixelRatio || 1, PIXEL_RATIO[quality]));
19
+ renderer.domElement.setAttribute("aria-hidden", "true");
20
+ return renderer;
21
+ }
22
+ function webgl2Available() {
23
+ const context = document.createElement("canvas").getContext("webgl2");
24
+ if (context === null) return false;
25
+ context.getExtension("WEBGL_lose_context")?.loseContext();
26
+ return true;
27
+ }
28
+ /** Rebuilds all resources using the game's scene factory; it never resumes game time. */
29
+ function bindGraphicsRecovery(host, canvas, o) {
30
+ const abort = new AbortController(), { signal } = abort;
31
+ const notice = document.createElement("p"), retry = document.createElement("button");
32
+ notice.hidden = true;
33
+ retry.hidden = true;
34
+ retry.className = "retry-graphics";
35
+ retry.type = "button";
36
+ retry.textContent = "Try 3D again";
37
+ const rebuild = () => {
38
+ o.rebuild();
39
+ };
40
+ canvas.addEventListener("webglcontextlost", (e) => {
41
+ e.preventDefault();
42
+ o.pause();
43
+ if (signal.aborted) return;
44
+ notice.hidden = false;
45
+ retry.hidden = false;
46
+ if (!retry.isConnected) host.append(notice, retry);
47
+ notice.textContent = "Graphics interrupted. Try 3D again to restore the scene.";
48
+ o.announce?.(notice.textContent);
49
+ }, { signal });
50
+ canvas.addEventListener("webglcontextrestored", rebuild, { signal });
51
+ retry.addEventListener("click", (event) => {
52
+ event.stopPropagation();
53
+ rebuild();
54
+ }, { signal });
55
+ return () => {
56
+ abort.abort();
57
+ notice.remove();
58
+ retry.remove();
59
+ };
60
+ }
61
+ //#endregion
62
+ export { bindGraphicsRecovery, createRenderer, webgl2Available };
63
+
64
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["import * as THREE from 'three';\nimport { PIXEL_RATIO, type Quality } from './quality.ts';\n/** Existing game scenes share the same strict WebGL 2 renderer boundary. */\nexport function createRenderer(\n options: Omit<THREE.WebGLRendererParameters, 'canvas'> & {\n canvas?: HTMLCanvasElement;\n } = {},\n quality: Quality = 'medium',\n): THREE.WebGLRenderer {\n const canvas = options.canvas ?? document.createElement('canvas');\n const context = canvas.getContext('webgl2', {\n antialias: options.antialias ?? true,\n alpha: options.alpha ?? false,\n powerPreference: options.powerPreference ?? 'low-power',\n });\n if (context === null) throw Error('This game needs WebGL 2. Enable hardware acceleration, then try again.');\n const renderer = new THREE.WebGLRenderer({ ...options, canvas, context: context as WebGL2RenderingContext });\n renderer.setPixelRatio(Math.min(devicePixelRatio || 1, PIXEL_RATIO[quality]));\n renderer.domElement.setAttribute('aria-hidden', 'true');\n return renderer;\n}\nexport function webgl2Available(): boolean {\n const canvas = document.createElement('canvas'),\n context = canvas.getContext('webgl2');\n if (context === null) return false;\n context.getExtension('WEBGL_lose_context')?.loseContext();\n return true;\n}\n/** Rebuilds all resources using the game's scene factory; it never resumes game time. */\nexport function bindGraphicsRecovery(\n host: HTMLElement,\n canvas: HTMLCanvasElement,\n o: {\n pause(): void;\n rebuild(): void;\n announce?: (message: string) => void;\n },\n): () => void {\n const abort = new AbortController(),\n { signal } = abort;\n const notice = document.createElement('p'),\n retry = document.createElement('button');\n notice.hidden = true;\n retry.hidden = true;\n retry.className = 'retry-graphics';\n retry.type = 'button';\n retry.textContent = 'Try 3D again';\n const rebuild = (): void => {\n o.rebuild();\n };\n canvas.addEventListener(\n 'webglcontextlost',\n (e) => {\n e.preventDefault();\n o.pause();\n if (signal.aborted) return;\n notice.hidden = false;\n retry.hidden = false;\n if (!retry.isConnected) host.append(notice, retry);\n notice.textContent = 'Graphics interrupted. Try 3D again to restore the scene.';\n o.announce?.(notice.textContent);\n },\n { signal },\n );\n canvas.addEventListener('webglcontextrestored', rebuild, { signal });\n retry.addEventListener(\n 'click',\n (event) => {\n event.stopPropagation();\n rebuild();\n },\n { signal },\n );\n return () => {\n abort.abort();\n notice.remove();\n retry.remove();\n };\n}\n"],"mappings":";;;;AAGA,SAAgB,eACd,UAEI,CAAC,GACL,UAAmB,UACE;CACrB,MAAM,SAAS,QAAQ,UAAU,SAAS,cAAc,QAAQ;CAChE,MAAM,UAAU,OAAO,WAAW,UAAU;EAC1C,WAAW,QAAQ,aAAa;EAChC,OAAO,QAAQ,SAAS;EACxB,iBAAiB,QAAQ,mBAAmB;CAC9C,CAAC;CACD,IAAI,YAAY,MAAM,MAAM,MAAM,wEAAwE;CAC1G,MAAM,WAAW,IAAI,MAAM,cAAc;EAAE,GAAG;EAAS;EAAiB;CAAkC,CAAC;CAC3G,SAAS,cAAc,KAAK,IAAI,oBAAoB,GAAG,YAAY,QAAQ,CAAC;CAC5E,SAAS,WAAW,aAAa,eAAe,MAAM;CACtD,OAAO;AACT;AACA,SAAgB,kBAA2B;CACzC,MACE,UADa,SAAS,cAAc,QAC1B,CAAA,CAAO,WAAW,QAAQ;CACtC,IAAI,YAAY,MAAM,OAAO;CAC7B,QAAQ,aAAa,oBAAoB,CAAC,EAAE,YAAY;CACxD,OAAO;AACT;;AAEA,SAAgB,qBACd,MACA,QACA,GAKY;CACZ,MAAM,QAAQ,IAAI,gBAAgB,GAChC,EAAE,WAAW;CACf,MAAM,SAAS,SAAS,cAAc,GAAG,GACvC,QAAQ,SAAS,cAAc,QAAQ;CACzC,OAAO,SAAS;CAChB,MAAM,SAAS;CACf,MAAM,YAAY;CAClB,MAAM,OAAO;CACb,MAAM,cAAc;CACpB,MAAM,gBAAsB;EAC1B,EAAE,QAAQ;CACZ;CACA,OAAO,iBACL,qBACC,MAAM;EACL,EAAE,eAAe;EACjB,EAAE,MAAM;EACR,IAAI,OAAO,SAAS;EACpB,OAAO,SAAS;EAChB,MAAM,SAAS;EACf,IAAI,CAAC,MAAM,aAAa,KAAK,OAAO,QAAQ,KAAK;EACjD,OAAO,cAAc;EACrB,EAAE,WAAW,OAAO,WAAW;CACjC,GACA,EAAE,OAAO,CACX;CACA,OAAO,iBAAiB,wBAAwB,SAAS,EAAE,OAAO,CAAC;CACnE,MAAM,iBACJ,UACC,UAAU;EACT,MAAM,gBAAgB;EACtB,QAAQ;CACV,GACA,EAAE,OAAO,CACX;CACA,aAAa;EACX,MAAM,MAAM;EACZ,OAAO,OAAO;EACd,MAAM,OAAO;CACf;AACF"}
@@ -0,0 +1,11 @@
1
+ export type Quality = 'low' | 'medium' | 'high';
2
+ export type Graphics = 'auto' | Quality;
3
+ export declare const PIXEL_RATIO: Readonly<Record<Quality, number>>;
4
+ /** Measurement window and sustained-pressure timer use the caller's monotonic clock. */
5
+ export declare class AutoQuality {
6
+ #private;
7
+ get level(): Quality;
8
+ get p95(): number;
9
+ set(setting: Graphics): void;
10
+ sample(ms: number, now: number): boolean;
11
+ }
@@ -0,0 +1,44 @@
1
+ //#region src/quality.ts
2
+ var PIXEL_RATIO = {
3
+ low: 1,
4
+ medium: 1.5,
5
+ high: 2
6
+ };
7
+ /** Measurement window and sustained-pressure timer use the caller's monotonic clock. */
8
+ var AutoQuality = class {
9
+ #samples = [];
10
+ #slowSince = null;
11
+ #level = "medium";
12
+ #setting = "auto";
13
+ get level() {
14
+ return this.#setting === "auto" ? this.#level : this.#setting;
15
+ }
16
+ get p95() {
17
+ const sorted = this.#samples.map((s) => s.ms).sort((a, b) => a - b);
18
+ return sorted[Math.max(0, Math.ceil(sorted.length * .95) - 1)] ?? 0;
19
+ }
20
+ set(setting) {
21
+ this.#setting = setting;
22
+ this.#slowSince = null;
23
+ }
24
+ sample(ms, now) {
25
+ this.#samples.push({
26
+ at: now,
27
+ ms
28
+ });
29
+ this.#samples = this.#samples.filter((s) => s.at >= now - 1e3);
30
+ if (this.#setting !== "auto" || this.p95 <= 16) {
31
+ this.#slowSince = null;
32
+ return false;
33
+ }
34
+ this.#slowSince ??= now;
35
+ if (now - this.#slowSince < 3e3 || this.#level === "low") return false;
36
+ this.#level = this.#level === "high" ? "medium" : "low";
37
+ this.#slowSince = null;
38
+ return true;
39
+ }
40
+ };
41
+ //#endregion
42
+ export { AutoQuality, PIXEL_RATIO };
43
+
44
+ //# sourceMappingURL=quality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality.js","names":[],"sources":["../src/quality.ts"],"sourcesContent":["export type Quality = 'low' | 'medium' | 'high';\nexport type Graphics = 'auto' | Quality;\nexport const PIXEL_RATIO: Readonly<Record<Quality, number>> = { low: 1, medium: 1.5, high: 2 };\n/** Measurement window and sustained-pressure timer use the caller's monotonic clock. */\nexport class AutoQuality {\n #samples: {\n at: number;\n ms: number;\n }[] = [];\n #slowSince: number | null = null;\n #level: Quality = 'medium';\n #setting: Graphics = 'auto';\n get level(): Quality {\n return this.#setting === 'auto' ? this.#level : this.#setting;\n }\n get p95(): number {\n const sorted = this.#samples.map((s) => s.ms).sort((a, b) => a - b);\n return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)] ?? 0;\n }\n set(setting: Graphics): void {\n this.#setting = setting;\n this.#slowSince = null;\n }\n sample(ms: number, now: number): boolean {\n this.#samples.push({ at: now, ms });\n this.#samples = this.#samples.filter((s) => s.at >= now - 1000);\n if (this.#setting !== 'auto' || this.p95 <= 16) {\n this.#slowSince = null;\n return false;\n }\n this.#slowSince ??= now;\n if (now - this.#slowSince < 3000 || this.#level === 'low') return false;\n this.#level = this.#level === 'high' ? 'medium' : 'low';\n this.#slowSince = null;\n return true;\n }\n}\n"],"mappings":";AAEA,IAAa,cAAiD;CAAE,KAAK;CAAG,QAAQ;CAAK,MAAM;AAAE;;AAE7F,IAAa,cAAb,MAAyB;CACvB,WAGM,CAAC;CACP,aAA4B;CAC5B,SAAkB;CAClB,WAAqB;CACrB,IAAI,QAAiB;EACnB,OAAO,KAAK,aAAa,SAAS,KAAK,SAAS,KAAK;CACvD;CACA,IAAI,MAAc;EAChB,MAAM,SAAS,KAAK,SAAS,KAAK,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;EAClE,OAAO,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,SAAS,GAAI,IAAI,CAAC,MAAM;CACrE;CACA,IAAI,SAAyB;EAC3B,KAAK,WAAW;EAChB,KAAK,aAAa;CACpB;CACA,OAAO,IAAY,KAAsB;EACvC,KAAK,SAAS,KAAK;GAAE,IAAI;GAAK;EAAG,CAAC;EAClC,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,EAAE,MAAM,MAAM,GAAI;EAC9D,IAAI,KAAK,aAAa,UAAU,KAAK,OAAO,IAAI;GAC9C,KAAK,aAAa;GAClB,OAAO;EACT;EACA,KAAK,eAAe;EACpB,IAAI,MAAM,KAAK,aAAa,OAAQ,KAAK,WAAW,OAAO,OAAO;EAClE,KAAK,SAAS,KAAK,WAAW,SAAS,WAAW;EAClD,KAAK,aAAa;EAClB,OAAO;CACT;AACF"}
@@ -0,0 +1,41 @@
1
+ import * as THREE from 'three';
2
+ import { type Graphics, type Quality } from './quality.js';
3
+ export type Disposable = {
4
+ dispose(): void;
5
+ };
6
+ export type SceneContext = {
7
+ readonly renderer: THREE.WebGLRenderer;
8
+ readonly scene: THREE.Scene;
9
+ track<T extends Disposable>(resource: T): T;
10
+ };
11
+ export type SceneDrawing<View> = {
12
+ readonly camera: THREE.Camera;
13
+ draw(view: View, alpha: number, quality: Quality): void;
14
+ resize?(width: number, height: number): void;
15
+ };
16
+ export type RenderOptions<View> = {
17
+ readonly host: HTMLElement;
18
+ readonly build: (context: SceneContext) => SceneDrawing<View>;
19
+ readonly pause: () => void;
20
+ readonly announce: (message: string) => void;
21
+ readonly development?: boolean;
22
+ readonly now?: () => number;
23
+ };
24
+ export declare function roomEnvironment({ renderer, scene, track }: SceneContext): void;
25
+ export declare function createRenderStage<View>(o: RenderOptions<View>): {
26
+ readonly canvas: HTMLCanvasElement | null;
27
+ readonly available: boolean;
28
+ readonly measurement: {
29
+ p95: number;
30
+ calls: number;
31
+ triangles: number;
32
+ quality: Quality;
33
+ };
34
+ draw(view: View, alpha?: number, key?: string): void;
35
+ graphics(value: Graphics): void;
36
+ retry: () => void;
37
+ dispose(): void;
38
+ };
39
+ /** Camera-space pointer picking on a game's plane. */
40
+ export declare function pickPlane(canvas: HTMLCanvasElement, camera: THREE.Camera, x: number, y: number, plane?: THREE.Plane): THREE.Vector3 | null;
41
+ export declare function project(point: THREE.Vector3, camera: THREE.Camera): THREE.Vector3;
package/dist/stage.js ADDED
@@ -0,0 +1,187 @@
1
+ import { AutoQuality, PIXEL_RATIO } from "./quality.js";
2
+ import * as THREE from "three";
3
+ import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
4
+ //#region src/stage.ts
5
+ function roomEnvironment({ renderer, scene, track }) {
6
+ const room = new RoomEnvironment(), pmrem = new THREE.PMREMGenerator(renderer);
7
+ scene.environment = track(pmrem.fromScene(room, .04)).texture;
8
+ room.dispose();
9
+ pmrem.dispose();
10
+ }
11
+ function createRenderStage(o) {
12
+ const quality = new AutoQuality(), now = o.now ?? (() => performance.now());
13
+ let renderer = null, drawing = null;
14
+ let scene = null, canvas = null;
15
+ let resources = /* @__PURE__ */ new Set(), events = null;
16
+ let last = null, drawn = "", disposed = false, lost = false;
17
+ let width = 1, height = 1;
18
+ const notice = document.createElement("p"), retry = document.createElement("button"), overlay = document.createElement("output");
19
+ retry.type = "button";
20
+ retry.textContent = "Try 3D again";
21
+ retry.hidden = true;
22
+ overlay.hidden = !o.development;
23
+ overlay.setAttribute("aria-label", "Rendering performance");
24
+ o.host.append(notice, retry, overlay);
25
+ const release = () => {
26
+ events?.abort();
27
+ events = null;
28
+ if (scene !== null) scene.traverse((object) => {
29
+ if (object instanceof THREE.InstancedMesh) resources.add(object);
30
+ const mesh = object;
31
+ if (mesh.geometry !== void 0) resources.add(mesh.geometry);
32
+ const materials = mesh.material === void 0 ? [] : Array.isArray(mesh.material) ? mesh.material : [mesh.material];
33
+ for (const material of materials) {
34
+ resources.add(material);
35
+ for (const value of Object.values(material)) if (value instanceof THREE.Texture) resources.add(value);
36
+ }
37
+ });
38
+ for (const resource of resources) resource.dispose();
39
+ resources = /* @__PURE__ */ new Set();
40
+ renderer?.dispose();
41
+ renderer?.forceContextLoss();
42
+ renderer = null;
43
+ drawing = null;
44
+ scene = null;
45
+ canvas?.remove();
46
+ canvas = null;
47
+ };
48
+ const resize = () => {
49
+ width = Math.max(1, o.host.clientWidth);
50
+ height = Math.max(1, o.host.clientHeight || width * .7);
51
+ if (renderer !== null) renderer.shadowMap.enabled = quality.level === "high";
52
+ renderer?.setPixelRatio(Math.min(devicePixelRatio || 1, PIXEL_RATIO[quality.level]));
53
+ renderer?.setSize(width, height, false);
54
+ drawing?.resize?.(width, height);
55
+ drawn = "";
56
+ redraw();
57
+ };
58
+ const redraw = () => {
59
+ if (disposed || lost || renderer === null || drawing === null || scene === null || last === null) return;
60
+ const key = `${last.key}:${last.alpha}:${quality.level}:${width}:${height}`;
61
+ if (key === drawn) return;
62
+ const started = now();
63
+ drawing.draw(last.view, last.alpha, quality.level);
64
+ renderer.render(scene, drawing.camera);
65
+ if (quality.sample(now() - started, now())) {
66
+ renderer.shadowMap.enabled = quality.level === "high";
67
+ renderer.setPixelRatio(Math.min(devicePixelRatio || 1, PIXEL_RATIO[quality.level]));
68
+ renderer.setSize(width, height, false);
69
+ }
70
+ overlay.textContent = `p95 ${quality.p95.toFixed(1)} ms · ${renderer.info.render.calls} calls · ${renderer.info.render.triangles} triangles · ${quality.level}`;
71
+ drawn = key;
72
+ };
73
+ const rebuild = () => {
74
+ if (disposed) return;
75
+ release();
76
+ lost = false;
77
+ drawn = "";
78
+ try {
79
+ canvas = document.createElement("canvas");
80
+ const context = canvas.getContext("webgl2", {
81
+ antialias: true,
82
+ alpha: false,
83
+ powerPreference: "low-power"
84
+ });
85
+ if (context === null) throw new Error("WebGL 2 unavailable");
86
+ renderer = new THREE.WebGLRenderer({
87
+ canvas,
88
+ context,
89
+ antialias: true
90
+ });
91
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
92
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
93
+ canvas.setAttribute("aria-hidden", "true");
94
+ canvas.classList.add("render3d-canvas");
95
+ o.host.prepend(canvas);
96
+ scene = new THREE.Scene();
97
+ drawing = o.build({
98
+ renderer,
99
+ scene,
100
+ track: (resource) => {
101
+ resources.add(resource);
102
+ return resource;
103
+ }
104
+ });
105
+ events = new AbortController();
106
+ canvas.addEventListener("webglcontextlost", (e) => {
107
+ e.preventDefault();
108
+ lost = true;
109
+ o.pause();
110
+ notice.textContent = "Graphics interrupted. Your game is paused.";
111
+ retry.hidden = false;
112
+ o.announce(notice.textContent);
113
+ }, { signal: events.signal });
114
+ canvas.addEventListener("webglcontextrestored", () => {
115
+ rebuild();
116
+ o.announce("Graphics restored. Resume when ready.");
117
+ }, { signal: events.signal });
118
+ notice.textContent = "";
119
+ retry.hidden = true;
120
+ resize();
121
+ } catch {
122
+ release();
123
+ lost = true;
124
+ o.pause();
125
+ notice.textContent = "This game needs WebGL 2. Enable hardware acceleration, then try again.";
126
+ retry.hidden = false;
127
+ }
128
+ };
129
+ retry.addEventListener("click", rebuild);
130
+ const observer = new ResizeObserver(resize);
131
+ observer.observe(o.host);
132
+ rebuild();
133
+ return {
134
+ get canvas() {
135
+ return canvas;
136
+ },
137
+ get available() {
138
+ return renderer !== null && !lost;
139
+ },
140
+ get measurement() {
141
+ return {
142
+ p95: quality.p95,
143
+ calls: renderer?.info.render.calls ?? 0,
144
+ triangles: renderer?.info.render.triangles ?? 0,
145
+ quality: quality.level
146
+ };
147
+ },
148
+ draw(view, alpha = 1, key = JSON.stringify(view)) {
149
+ last = {
150
+ view,
151
+ alpha,
152
+ key
153
+ };
154
+ redraw();
155
+ },
156
+ graphics(value) {
157
+ quality.set(value);
158
+ resize();
159
+ },
160
+ retry: rebuild,
161
+ dispose() {
162
+ if (disposed) return;
163
+ disposed = true;
164
+ observer.disconnect();
165
+ release();
166
+ retry.removeEventListener("click", rebuild);
167
+ notice.remove();
168
+ retry.remove();
169
+ overlay.remove();
170
+ }
171
+ };
172
+ }
173
+ /** Camera-space pointer picking on a game's plane. */
174
+ function pickPlane(canvas, camera, x, y, plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)) {
175
+ const rect = canvas.getBoundingClientRect();
176
+ if (rect.width === 0 || rect.height === 0) return null;
177
+ const ray = new THREE.Raycaster();
178
+ ray.setFromCamera(new THREE.Vector2((x - rect.left) / rect.width * 2 - 1, 1 - (y - rect.top) / rect.height * 2), camera);
179
+ return ray.ray.intersectPlane(plane, new THREE.Vector3());
180
+ }
181
+ function project(point, camera) {
182
+ return point.clone().project(camera);
183
+ }
184
+ //#endregion
185
+ export { createRenderStage, pickPlane, project, roomEnvironment };
186
+
187
+ //# sourceMappingURL=stage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage.js","names":[],"sources":["../src/stage.ts"],"sourcesContent":["import * as THREE from 'three';\nimport { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';\nimport { AutoQuality, PIXEL_RATIO, type Graphics, type Quality } from './quality.ts';\nexport type Disposable = {\n dispose(): void;\n};\nexport type SceneContext = {\n readonly renderer: THREE.WebGLRenderer;\n readonly scene: THREE.Scene;\n track<T extends Disposable>(resource: T): T;\n};\nexport type SceneDrawing<View> = {\n readonly camera: THREE.Camera;\n draw(view: View, alpha: number, quality: Quality): void;\n resize?(width: number, height: number): void;\n};\nexport type RenderOptions<View> = {\n readonly host: HTMLElement;\n readonly build: (context: SceneContext) => SceneDrawing<View>;\n readonly pause: () => void;\n readonly announce: (message: string) => void;\n readonly development?: boolean;\n readonly now?: () => number;\n};\nexport function roomEnvironment({ renderer, scene, track }: SceneContext): void {\n const room = new RoomEnvironment(),\n pmrem = new THREE.PMREMGenerator(renderer);\n const target = track(pmrem.fromScene(room, 0.04));\n scene.environment = target.texture;\n room.dispose();\n pmrem.dispose();\n}\nexport function createRenderStage<View>(o: RenderOptions<View>) {\n const quality = new AutoQuality(),\n now = o.now ?? (() => performance.now());\n let renderer: THREE.WebGLRenderer | null = null,\n drawing: SceneDrawing<View> | null = null;\n let scene: THREE.Scene | null = null,\n canvas: HTMLCanvasElement | null = null;\n let resources = new Set<Disposable>(),\n events: AbortController | null = null;\n let last: {\n view: View;\n alpha: number;\n key: string;\n } | null = null,\n drawn = '',\n disposed = false,\n lost = false;\n let width = 1,\n height = 1;\n const notice = document.createElement('p'),\n retry = document.createElement('button'),\n overlay = document.createElement('output');\n retry.type = 'button';\n retry.textContent = 'Try 3D again';\n retry.hidden = true;\n overlay.hidden = !o.development;\n overlay.setAttribute('aria-label', 'Rendering performance');\n o.host.append(notice, retry, overlay);\n const release = (): void => {\n events?.abort();\n events = null;\n if (scene !== null)\n scene.traverse((object) => {\n if (object instanceof THREE.InstancedMesh) resources.add(object);\n const mesh = object as THREE.Mesh;\n if (mesh.geometry !== undefined) resources.add(mesh.geometry);\n const materials =\n mesh.material === undefined ? [] : Array.isArray(mesh.material) ? mesh.material : [mesh.material];\n for (const material of materials) {\n resources.add(material);\n for (const value of Object.values(material)) if (value instanceof THREE.Texture) resources.add(value);\n }\n });\n for (const resource of resources) resource.dispose();\n resources = new Set();\n renderer?.dispose();\n renderer?.forceContextLoss();\n renderer = null;\n drawing = null;\n scene = null;\n canvas?.remove();\n canvas = null;\n };\n const resize = (): void => {\n width = Math.max(1, o.host.clientWidth);\n height = Math.max(1, o.host.clientHeight || width * 0.7);\n if (renderer !== null) renderer.shadowMap.enabled = quality.level === 'high';\n renderer?.setPixelRatio(Math.min(devicePixelRatio || 1, PIXEL_RATIO[quality.level]));\n renderer?.setSize(width, height, false);\n drawing?.resize?.(width, height);\n drawn = '';\n redraw();\n };\n const redraw = (): void => {\n if (disposed || lost || renderer === null || drawing === null || scene === null || last === null) return;\n const key = `${last.key}:${last.alpha}:${quality.level}:${width}:${height}`;\n if (key === drawn) return;\n const started = now();\n drawing.draw(last.view, last.alpha, quality.level);\n renderer.render(scene, drawing.camera);\n if (quality.sample(now() - started, now())) {\n renderer.shadowMap.enabled = quality.level === 'high';\n renderer.setPixelRatio(Math.min(devicePixelRatio || 1, PIXEL_RATIO[quality.level]));\n renderer.setSize(width, height, false);\n }\n overlay.textContent = `p95 ${quality.p95.toFixed(1)} ms · ${renderer.info.render.calls} calls · ${renderer.info.render.triangles} triangles · ${quality.level}`;\n drawn = key;\n };\n const rebuild = (): void => {\n if (disposed) return;\n release();\n lost = false;\n drawn = '';\n try {\n canvas = document.createElement('canvas');\n const context = canvas.getContext('webgl2', { antialias: true, alpha: false, powerPreference: 'low-power' });\n if (context === null) throw new Error('WebGL 2 unavailable');\n renderer = new THREE.WebGLRenderer({ canvas, context, antialias: true });\n renderer.outputColorSpace = THREE.SRGBColorSpace;\n renderer.toneMapping = THREE.ACESFilmicToneMapping;\n canvas.setAttribute('aria-hidden', 'true');\n canvas.classList.add('render3d-canvas');\n o.host.prepend(canvas);\n scene = new THREE.Scene();\n drawing = o.build({\n renderer,\n scene,\n track: (resource) => {\n resources.add(resource);\n return resource;\n },\n });\n events = new AbortController();\n canvas.addEventListener(\n 'webglcontextlost',\n (e) => {\n e.preventDefault();\n lost = true;\n o.pause();\n notice.textContent = 'Graphics interrupted. Your game is paused.';\n retry.hidden = false;\n o.announce(notice.textContent);\n },\n { signal: events.signal },\n );\n canvas.addEventListener(\n 'webglcontextrestored',\n () => {\n rebuild();\n o.announce('Graphics restored. Resume when ready.');\n },\n { signal: events.signal },\n );\n notice.textContent = '';\n retry.hidden = true;\n resize();\n } catch {\n release();\n lost = true;\n o.pause();\n notice.textContent = 'This game needs WebGL 2. Enable hardware acceleration, then try again.';\n retry.hidden = false;\n }\n };\n retry.addEventListener('click', rebuild);\n const observer = new ResizeObserver(resize);\n observer.observe(o.host);\n rebuild();\n return {\n get canvas(): HTMLCanvasElement | null {\n return canvas;\n },\n get available(): boolean {\n return renderer !== null && !lost;\n },\n get measurement() {\n return {\n p95: quality.p95,\n calls: renderer?.info.render.calls ?? 0,\n triangles: renderer?.info.render.triangles ?? 0,\n quality: quality.level,\n };\n },\n draw(view: View, alpha = 1, key = JSON.stringify(view)): void {\n last = { view, alpha, key };\n redraw();\n },\n graphics(value: Graphics): void {\n quality.set(value);\n resize();\n },\n retry: rebuild,\n dispose(): void {\n if (disposed) return;\n disposed = true;\n observer.disconnect();\n release();\n retry.removeEventListener('click', rebuild);\n notice.remove();\n retry.remove();\n overlay.remove();\n },\n };\n}\n/** Camera-space pointer picking on a game's plane. */\nexport function pickPlane(\n canvas: HTMLCanvasElement,\n camera: THREE.Camera,\n x: number,\n y: number,\n plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),\n): THREE.Vector3 | null {\n const rect = canvas.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return null;\n const ray = new THREE.Raycaster();\n ray.setFromCamera(\n new THREE.Vector2(((x - rect.left) / rect.width) * 2 - 1, 1 - ((y - rect.top) / rect.height) * 2),\n camera,\n );\n return ray.ray.intersectPlane(plane, new THREE.Vector3());\n}\nexport function project(point: THREE.Vector3, camera: THREE.Camera): THREE.Vector3 {\n return point.clone().project(camera);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@joystyk/render3d",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Shared Three.js rendering, lifecycle and adaptive quality.",
5
+ "private": false,
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@types/three": "0.186.0",
16
+ "three": "0.186.1",
17
+ "@joystyk/contracts": "0.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "5.9.3",
21
+ "vite": "8.3.3",
22
+ "vitest": "4.1.11"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE",
31
+ "NOTICE",
32
+ "CHANGELOG.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=24.15.0"
36
+ },
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/joystyk-games/game-libraries.git",
41
+ "directory": "packages/render3d"
42
+ },
43
+ "homepage": "https://github.com/joystyk-games/game-libraries/tree/main/packages/render3d#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/joystyk-games/game-libraries/issues"
46
+ },
47
+ "scripts": {
48
+ "typecheck": "tsc --noEmit",
49
+ "build": "node ../../scripts/build-package.ts"
50
+ }
6
51
  }