@xsolla/xui-b2c-glass-cube 0.0.0-stage → 0.226.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 +23 -2
- package/capabilities-BM5ntaGo.d.mts +84 -0
- package/capabilities-BM5ntaGo.d.ts +84 -0
- package/chunk-H563G5UY.mjs +267 -0
- package/chunk-H563G5UY.mjs.map +1 -0
- package/chunk-U3XM7FPH.js +267 -0
- package/chunk-U3XM7FPH.js.map +1 -0
- package/glass-cube-scene-5CKNLPJ5.mjs +2191 -0
- package/glass-cube-scene-5CKNLPJ5.mjs.map +1 -0
- package/glass-cube-scene-HTE4ZEME.js +2191 -0
- package/glass-cube-scene-HTE4ZEME.js.map +1 -0
- package/index.d.mts +11 -0
- package/index.d.ts +11 -0
- package/index.js +11 -0
- package/index.js.map +1 -0
- package/index.mjs +11 -0
- package/index.mjs.map +1 -0
- package/package.json +48 -3
- package/react/index.d.mts +19 -0
- package/react/index.d.ts +19 -0
- package/react/index.js +99 -0
- package/react/index.js.map +1 -0
- package/react/index.mjs +99 -0
- package/react/index.mjs.map +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xsolla/xui-b2c-glass-cube
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WebGL animation of Sol sealing an item into a glass cube. Framework-agnostic
|
|
4
|
+
core on three.js (`mountGlassCube`) plus a thin React wrapper; falls back to
|
|
5
|
+
the plain item picture without WebGL2 or with `prefers-reduced-motion`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @xsolla/xui-b2c-glass-cube three
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { mountGlassCube } from "@xsolla/xui-b2c-glass-cube";
|
|
13
|
+
|
|
14
|
+
const cube = mountGlassCube(hostEl, { item: imageUrl, size: 200 });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { GlassCube } from "@xsolla/xui-b2c-glass-cube/react";
|
|
19
|
+
|
|
20
|
+
<GlassCube item={imageUrl} size={200} onComplete={showNextStep} />;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Full API, assets, fallback behaviour:
|
|
24
|
+
[docs/api/components/b2c-glass-cube.md](../../../docs/api/components/b2c-glass-cube.md).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** URLs of the binary assets the scene loads at runtime. */
|
|
2
|
+
interface GlassCubeAssets {
|
|
3
|
+
/** Glass cube model (`glass-cube.glb`). */
|
|
4
|
+
readonly cubeModelUrl: string;
|
|
5
|
+
/** Sol, the printer, as a rigged model (`sol.glb`). */
|
|
6
|
+
readonly printerModelUrl: string;
|
|
7
|
+
/** Studio light map Sol was rendered in (`studio.hdr`). */
|
|
8
|
+
readonly environmentUrl: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Served from the toolkit CDN bucket (`cdn-assets/xsolla-ui-toolkit-prod/glass-cube/v1`).
|
|
12
|
+
* Bump `?v=` whenever a file changes in place — the CDN caches aggressively.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_GLASS_CUBE_ASSETS: GlassCubeAssets;
|
|
15
|
+
|
|
16
|
+
/** The ID printed on the cube's face, e.g. `{ series: "XLA", number: "-000-006" }`. */
|
|
17
|
+
interface GlassCubeLabel {
|
|
18
|
+
readonly series: string;
|
|
19
|
+
readonly number: string;
|
|
20
|
+
}
|
|
21
|
+
interface GlassCubeOptions {
|
|
22
|
+
/** URL of the picture sealed inside the cube. */
|
|
23
|
+
readonly item: string;
|
|
24
|
+
/**
|
|
25
|
+
* Printed instead when `item` cannot be used as a WebGL texture — typically a
|
|
26
|
+
* partner CDN that serves the picture without CORS, which a plain `<img>`
|
|
27
|
+
* would still show. Serve it from your own origin. Without one such an item
|
|
28
|
+
* drops the 3D cube and falls back to the picture.
|
|
29
|
+
*/
|
|
30
|
+
readonly fallbackItem?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Cube size in CSS pixels. The root element is sized to fit the whole print
|
|
33
|
+
* (`size × 2.5`, Sol flies in from outside the cube) and never exceeds the
|
|
34
|
+
* host. Without one the cube follows the host: its width divided by 2.5.
|
|
35
|
+
*/
|
|
36
|
+
readonly size?: number;
|
|
37
|
+
/** `true` shows the cube already sealed, with no print. Defaults to `false`. */
|
|
38
|
+
readonly sealed?: boolean;
|
|
39
|
+
/** Whether Sol is in the shot. Defaults to `!sealed`; `false` skips loading Sol. */
|
|
40
|
+
readonly printer?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Start the print as soon as the scene is ready. Defaults to `true`; with
|
|
43
|
+
* `false` the cube waits for `play()`. Ignored for a sealed cube.
|
|
44
|
+
*/
|
|
45
|
+
readonly autoPlay?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Tag on the cube's face; without one the cube carries no ID. A string is
|
|
48
|
+
* split at its first dash: `"XLA-000-006"` → `{ series: "XLA", number: "-000-006" }`.
|
|
49
|
+
*/
|
|
50
|
+
readonly label?: string | GlassCubeLabel;
|
|
51
|
+
/** Overrides for the model / light-map URLs; the toolkit CDN by default. */
|
|
52
|
+
readonly assets?: Partial<GlassCubeAssets>;
|
|
53
|
+
/**
|
|
54
|
+
* The cube is sealed. Fires after every print (so again on each `play()`),
|
|
55
|
+
* and once right away when there is nothing to play: a `sealed` cube or the
|
|
56
|
+
* static fallback.
|
|
57
|
+
*/
|
|
58
|
+
onComplete?(): void;
|
|
59
|
+
}
|
|
60
|
+
interface GlassCubeHandle {
|
|
61
|
+
/**
|
|
62
|
+
* Starts (or restarts) the print. Before the scene is ready the call is
|
|
63
|
+
* remembered and the print starts once it is. No-op for a sealed cube or
|
|
64
|
+
* the fallback.
|
|
65
|
+
*/
|
|
66
|
+
play(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Swaps the picture inside the cube without rebuilding the scene. Resolves
|
|
69
|
+
* once the new picture is up; only the latest call lands.
|
|
70
|
+
*/
|
|
71
|
+
setItem(src: string): Promise<void>;
|
|
72
|
+
/** Changes the cube size in CSS pixels; `undefined` makes it follow the host again. */
|
|
73
|
+
setSize(px?: number): void;
|
|
74
|
+
/** Removes everything from the host and releases GPU resources. */
|
|
75
|
+
dispose(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `false` means the print cannot play: show the item sealed right away instead
|
|
80
|
+
* of never showing it. Checked once, before the scene chunk is requested.
|
|
81
|
+
*/
|
|
82
|
+
declare function canPlayGlassCube(win?: Window | undefined): boolean;
|
|
83
|
+
|
|
84
|
+
export { DEFAULT_GLASS_CUBE_ASSETS as D, type GlassCubeOptions as G, type GlassCubeHandle as a, type GlassCubeAssets as b, type GlassCubeLabel as c, canPlayGlassCube as d };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** URLs of the binary assets the scene loads at runtime. */
|
|
2
|
+
interface GlassCubeAssets {
|
|
3
|
+
/** Glass cube model (`glass-cube.glb`). */
|
|
4
|
+
readonly cubeModelUrl: string;
|
|
5
|
+
/** Sol, the printer, as a rigged model (`sol.glb`). */
|
|
6
|
+
readonly printerModelUrl: string;
|
|
7
|
+
/** Studio light map Sol was rendered in (`studio.hdr`). */
|
|
8
|
+
readonly environmentUrl: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Served from the toolkit CDN bucket (`cdn-assets/xsolla-ui-toolkit-prod/glass-cube/v1`).
|
|
12
|
+
* Bump `?v=` whenever a file changes in place — the CDN caches aggressively.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_GLASS_CUBE_ASSETS: GlassCubeAssets;
|
|
15
|
+
|
|
16
|
+
/** The ID printed on the cube's face, e.g. `{ series: "XLA", number: "-000-006" }`. */
|
|
17
|
+
interface GlassCubeLabel {
|
|
18
|
+
readonly series: string;
|
|
19
|
+
readonly number: string;
|
|
20
|
+
}
|
|
21
|
+
interface GlassCubeOptions {
|
|
22
|
+
/** URL of the picture sealed inside the cube. */
|
|
23
|
+
readonly item: string;
|
|
24
|
+
/**
|
|
25
|
+
* Printed instead when `item` cannot be used as a WebGL texture — typically a
|
|
26
|
+
* partner CDN that serves the picture without CORS, which a plain `<img>`
|
|
27
|
+
* would still show. Serve it from your own origin. Without one such an item
|
|
28
|
+
* drops the 3D cube and falls back to the picture.
|
|
29
|
+
*/
|
|
30
|
+
readonly fallbackItem?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Cube size in CSS pixels. The root element is sized to fit the whole print
|
|
33
|
+
* (`size × 2.5`, Sol flies in from outside the cube) and never exceeds the
|
|
34
|
+
* host. Without one the cube follows the host: its width divided by 2.5.
|
|
35
|
+
*/
|
|
36
|
+
readonly size?: number;
|
|
37
|
+
/** `true` shows the cube already sealed, with no print. Defaults to `false`. */
|
|
38
|
+
readonly sealed?: boolean;
|
|
39
|
+
/** Whether Sol is in the shot. Defaults to `!sealed`; `false` skips loading Sol. */
|
|
40
|
+
readonly printer?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Start the print as soon as the scene is ready. Defaults to `true`; with
|
|
43
|
+
* `false` the cube waits for `play()`. Ignored for a sealed cube.
|
|
44
|
+
*/
|
|
45
|
+
readonly autoPlay?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Tag on the cube's face; without one the cube carries no ID. A string is
|
|
48
|
+
* split at its first dash: `"XLA-000-006"` → `{ series: "XLA", number: "-000-006" }`.
|
|
49
|
+
*/
|
|
50
|
+
readonly label?: string | GlassCubeLabel;
|
|
51
|
+
/** Overrides for the model / light-map URLs; the toolkit CDN by default. */
|
|
52
|
+
readonly assets?: Partial<GlassCubeAssets>;
|
|
53
|
+
/**
|
|
54
|
+
* The cube is sealed. Fires after every print (so again on each `play()`),
|
|
55
|
+
* and once right away when there is nothing to play: a `sealed` cube or the
|
|
56
|
+
* static fallback.
|
|
57
|
+
*/
|
|
58
|
+
onComplete?(): void;
|
|
59
|
+
}
|
|
60
|
+
interface GlassCubeHandle {
|
|
61
|
+
/**
|
|
62
|
+
* Starts (or restarts) the print. Before the scene is ready the call is
|
|
63
|
+
* remembered and the print starts once it is. No-op for a sealed cube or
|
|
64
|
+
* the fallback.
|
|
65
|
+
*/
|
|
66
|
+
play(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Swaps the picture inside the cube without rebuilding the scene. Resolves
|
|
69
|
+
* once the new picture is up; only the latest call lands.
|
|
70
|
+
*/
|
|
71
|
+
setItem(src: string): Promise<void>;
|
|
72
|
+
/** Changes the cube size in CSS pixels; `undefined` makes it follow the host again. */
|
|
73
|
+
setSize(px?: number): void;
|
|
74
|
+
/** Removes everything from the host and releases GPU resources. */
|
|
75
|
+
dispose(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `false` means the print cannot play: show the item sealed right away instead
|
|
80
|
+
* of never showing it. Checked once, before the scene chunk is requested.
|
|
81
|
+
*/
|
|
82
|
+
declare function canPlayGlassCube(win?: Window | undefined): boolean;
|
|
83
|
+
|
|
84
|
+
export { DEFAULT_GLASS_CUBE_ASSETS as D, type GlassCubeOptions as G, type GlassCubeHandle as a, type GlassCubeAssets as b, type GlassCubeLabel as c, canPlayGlassCube as d };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/assets.ts
|
|
2
|
+
var CDN_BASE = "https://cdn.xsolla.net/xsolla-ui-toolkit-prod/glass-cube/v1";
|
|
3
|
+
var DEFAULT_GLASS_CUBE_ASSETS = {
|
|
4
|
+
cubeModelUrl: `${CDN_BASE}/glass-cube.glb?v=1`,
|
|
5
|
+
printerModelUrl: `${CDN_BASE}/sol.glb?v=1`,
|
|
6
|
+
environmentUrl: `${CDN_BASE}/studio.hdr?v=1`
|
|
7
|
+
};
|
|
8
|
+
function resolveGlassCubeAssets(overrides) {
|
|
9
|
+
return { ...DEFAULT_GLASS_CUBE_ASSETS, ...overrides };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/capabilities.ts
|
|
13
|
+
var webgl2Support = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
function defaultWindow() {
|
|
15
|
+
return typeof window === "undefined" ? void 0 : window;
|
|
16
|
+
}
|
|
17
|
+
function hasWebgl2(win = defaultWindow()) {
|
|
18
|
+
if (!win) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const cached = webgl2Support.get(win);
|
|
22
|
+
if (cached !== void 0) {
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
let supported = false;
|
|
26
|
+
try {
|
|
27
|
+
const gl = win.document.createElement("canvas").getContext("webgl2");
|
|
28
|
+
gl?.getExtension("WEBGL_lose_context")?.loseContext();
|
|
29
|
+
supported = gl !== null && gl !== void 0;
|
|
30
|
+
} catch {
|
|
31
|
+
supported = false;
|
|
32
|
+
}
|
|
33
|
+
webgl2Support.set(win, supported);
|
|
34
|
+
return supported;
|
|
35
|
+
}
|
|
36
|
+
function prefersReducedMotion(win = defaultWindow()) {
|
|
37
|
+
return win?.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
|
38
|
+
}
|
|
39
|
+
function canPlayGlassCube(win = defaultWindow()) {
|
|
40
|
+
return !prefersReducedMotion(win) && hasWebgl2(win);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/label.ts
|
|
44
|
+
function normalizeGlassCubeLabel(label) {
|
|
45
|
+
if (label === void 0) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
if (typeof label !== "string") {
|
|
49
|
+
return label;
|
|
50
|
+
}
|
|
51
|
+
const trimmed = label.trim();
|
|
52
|
+
if (trimmed === "") {
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
const dash = trimmed.indexOf("-");
|
|
56
|
+
if (dash <= 0) {
|
|
57
|
+
return { series: trimmed, number: "" };
|
|
58
|
+
}
|
|
59
|
+
return { series: trimmed.slice(0, dash), number: trimmed.slice(dash) };
|
|
60
|
+
}
|
|
61
|
+
function glassCubeLabelKey(label) {
|
|
62
|
+
return label ? `${label.series}\0${label.number}` : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/layout.ts
|
|
66
|
+
var STAGE_OVERSCAN = 2.5;
|
|
67
|
+
var CLASS_ROOT = "xui-glass-cube";
|
|
68
|
+
var CLASS_STAGE = `${CLASS_ROOT}__stage`;
|
|
69
|
+
var CLASS_OVERSCAN = `${CLASS_ROOT}__overscan`;
|
|
70
|
+
var CLASS_FALLBACK = `${CLASS_ROOT}__fallback`;
|
|
71
|
+
var FALLBACK_INSET = "15%";
|
|
72
|
+
var FALLBACK_SIZE = "70%";
|
|
73
|
+
function assign(element, style) {
|
|
74
|
+
Object.assign(element.style, style);
|
|
75
|
+
}
|
|
76
|
+
function createGlassCubeLayout(host, size) {
|
|
77
|
+
const document = host.ownerDocument;
|
|
78
|
+
const root = document.createElement("div");
|
|
79
|
+
root.className = CLASS_ROOT;
|
|
80
|
+
root.setAttribute("aria-hidden", "true");
|
|
81
|
+
assign(root, {
|
|
82
|
+
position: "relative",
|
|
83
|
+
display: "flex",
|
|
84
|
+
justifyContent: "center",
|
|
85
|
+
// Never wider than the host, so a narrow column shrinks the whole print.
|
|
86
|
+
maxWidth: "100%"
|
|
87
|
+
});
|
|
88
|
+
const stage = document.createElement("div");
|
|
89
|
+
stage.className = CLASS_STAGE;
|
|
90
|
+
assign(stage, {
|
|
91
|
+
position: "relative",
|
|
92
|
+
flexShrink: "0",
|
|
93
|
+
width: `${100 / STAGE_OVERSCAN}%`
|
|
94
|
+
});
|
|
95
|
+
const square = document.createElement("div");
|
|
96
|
+
assign(square, { paddingTop: "100%" });
|
|
97
|
+
stage.appendChild(square);
|
|
98
|
+
const overscan = document.createElement("div");
|
|
99
|
+
overscan.className = CLASS_OVERSCAN;
|
|
100
|
+
assign(overscan, {
|
|
101
|
+
position: "absolute",
|
|
102
|
+
top: "50%",
|
|
103
|
+
left: "50%",
|
|
104
|
+
width: "100%",
|
|
105
|
+
height: `${STAGE_OVERSCAN * 100}%`,
|
|
106
|
+
pointerEvents: "none",
|
|
107
|
+
transform: "translate(-50%, -50%)"
|
|
108
|
+
});
|
|
109
|
+
root.append(stage, overscan);
|
|
110
|
+
host.appendChild(root);
|
|
111
|
+
let fallback = null;
|
|
112
|
+
const layout = {
|
|
113
|
+
root,
|
|
114
|
+
stage,
|
|
115
|
+
overscan,
|
|
116
|
+
setSize(px) {
|
|
117
|
+
root.style.width = px === void 0 ? "100%" : `${px * STAGE_OVERSCAN}px`;
|
|
118
|
+
},
|
|
119
|
+
showFallback(src) {
|
|
120
|
+
overscan.remove();
|
|
121
|
+
if (!fallback) {
|
|
122
|
+
fallback = document.createElement("img");
|
|
123
|
+
fallback.className = CLASS_FALLBACK;
|
|
124
|
+
fallback.alt = "";
|
|
125
|
+
assign(fallback, {
|
|
126
|
+
position: "absolute",
|
|
127
|
+
top: FALLBACK_INSET,
|
|
128
|
+
left: FALLBACK_INSET,
|
|
129
|
+
width: FALLBACK_SIZE,
|
|
130
|
+
height: FALLBACK_SIZE,
|
|
131
|
+
objectFit: "contain"
|
|
132
|
+
});
|
|
133
|
+
stage.appendChild(fallback);
|
|
134
|
+
}
|
|
135
|
+
fallback.src = src;
|
|
136
|
+
},
|
|
137
|
+
setFallbackSrc(src) {
|
|
138
|
+
if (fallback) {
|
|
139
|
+
fallback.src = src;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
dispose() {
|
|
143
|
+
root.remove();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
layout.setSize(size);
|
|
147
|
+
return layout;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/mount.ts
|
|
151
|
+
function mountGlassCube(host, options) {
|
|
152
|
+
const sealed = options.sealed ?? false;
|
|
153
|
+
const printer = options.printer ?? !sealed;
|
|
154
|
+
const layout = createGlassCubeLayout(host, options.size);
|
|
155
|
+
let item = options.item;
|
|
156
|
+
let scene = null;
|
|
157
|
+
let isDisposed = false;
|
|
158
|
+
let isFallback = false;
|
|
159
|
+
let hasCompleted = false;
|
|
160
|
+
let playPending = !sealed && (options.autoPlay ?? true);
|
|
161
|
+
const complete = () => {
|
|
162
|
+
if (isDisposed) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
hasCompleted = true;
|
|
166
|
+
options.onComplete?.();
|
|
167
|
+
};
|
|
168
|
+
const fallBack = () => {
|
|
169
|
+
if (isDisposed || isFallback) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
isFallback = true;
|
|
173
|
+
scene?.dispose();
|
|
174
|
+
scene = null;
|
|
175
|
+
layout.showFallback(item);
|
|
176
|
+
if (!hasCompleted) {
|
|
177
|
+
complete();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const start = async () => {
|
|
181
|
+
try {
|
|
182
|
+
const { GlassCubeScene } = await import("./glass-cube-scene-5CKNLPJ5.mjs");
|
|
183
|
+
const created = await GlassCubeScene.create({
|
|
184
|
+
container: layout.overscan,
|
|
185
|
+
stage: layout.stage,
|
|
186
|
+
item: options.item,
|
|
187
|
+
fallbackItem: options.fallbackItem,
|
|
188
|
+
sealed,
|
|
189
|
+
printer,
|
|
190
|
+
label: normalizeGlassCubeLabel(options.label),
|
|
191
|
+
assets: resolveGlassCubeAssets(options.assets),
|
|
192
|
+
onComplete: complete,
|
|
193
|
+
onContextLost: fallBack
|
|
194
|
+
});
|
|
195
|
+
if (item !== options.item) {
|
|
196
|
+
await created.setItem(item);
|
|
197
|
+
}
|
|
198
|
+
if (isDisposed || isFallback) {
|
|
199
|
+
created.dispose();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
scene = created;
|
|
203
|
+
if (sealed) {
|
|
204
|
+
complete();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (playPending) {
|
|
208
|
+
playPending = false;
|
|
209
|
+
created.play();
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
fallBack();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
let ready;
|
|
216
|
+
if (canPlayGlassCube(host.ownerDocument.defaultView ?? void 0)) {
|
|
217
|
+
ready = start();
|
|
218
|
+
} else {
|
|
219
|
+
ready = Promise.resolve();
|
|
220
|
+
fallBack();
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
play() {
|
|
224
|
+
if (sealed || isFallback || isDisposed) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (scene) {
|
|
228
|
+
scene.play();
|
|
229
|
+
} else {
|
|
230
|
+
playPending = true;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
async setItem(src) {
|
|
234
|
+
item = src;
|
|
235
|
+
if (isFallback) {
|
|
236
|
+
layout.setFallbackSrc(src);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await ready;
|
|
240
|
+
if (isDisposed || isFallback) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
await scene?.setItem(src);
|
|
244
|
+
},
|
|
245
|
+
setSize(px) {
|
|
246
|
+
layout.setSize(px);
|
|
247
|
+
},
|
|
248
|
+
dispose() {
|
|
249
|
+
if (isDisposed) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
isDisposed = true;
|
|
253
|
+
scene?.dispose();
|
|
254
|
+
scene = null;
|
|
255
|
+
layout.dispose();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export {
|
|
261
|
+
DEFAULT_GLASS_CUBE_ASSETS,
|
|
262
|
+
canPlayGlassCube,
|
|
263
|
+
normalizeGlassCubeLabel,
|
|
264
|
+
glassCubeLabelKey,
|
|
265
|
+
mountGlassCube
|
|
266
|
+
};
|
|
267
|
+
//# sourceMappingURL=chunk-H563G5UY.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/assets.ts","../src/capabilities.ts","../src/label.ts","../src/layout.ts","../src/mount.ts"],"sourcesContent":["/** URLs of the binary assets the scene loads at runtime. */\nexport interface GlassCubeAssets {\n /** Glass cube model (`glass-cube.glb`). */\n readonly cubeModelUrl: string;\n /** Sol, the printer, as a rigged model (`sol.glb`). */\n readonly printerModelUrl: string;\n /** Studio light map Sol was rendered in (`studio.hdr`). */\n readonly environmentUrl: string;\n}\n\nconst CDN_BASE = \"https://cdn.xsolla.net/xsolla-ui-toolkit-prod/glass-cube/v1\";\n\n/**\n * Served from the toolkit CDN bucket (`cdn-assets/xsolla-ui-toolkit-prod/glass-cube/v1`).\n * Bump `?v=` whenever a file changes in place — the CDN caches aggressively.\n */\nexport const DEFAULT_GLASS_CUBE_ASSETS: GlassCubeAssets = {\n cubeModelUrl: `${CDN_BASE}/glass-cube.glb?v=1`,\n printerModelUrl: `${CDN_BASE}/sol.glb?v=1`,\n environmentUrl: `${CDN_BASE}/studio.hdr?v=1`,\n};\n\nexport function resolveGlassCubeAssets(\n overrides?: Partial<GlassCubeAssets>\n): GlassCubeAssets {\n return { ...DEFAULT_GLASS_CUBE_ASSETS, ...overrides };\n}\n","/**\n * Deliberately free of `three` imports: these probes run before deciding whether\n * to download the WebGL chunk at all.\n */\nconst webgl2Support = new WeakMap<Window, boolean>();\n\nfunction defaultWindow(): Window | undefined {\n return typeof window === \"undefined\" ? undefined : window;\n}\n\nexport function hasWebgl2(win: Window | undefined = defaultWindow()): boolean {\n if (!win) {\n return false;\n }\n const cached = webgl2Support.get(win);\n if (cached !== undefined) {\n return cached;\n }\n let supported = false;\n try {\n const gl = win.document.createElement(\"canvas\").getContext(\"webgl2\");\n gl?.getExtension(\"WEBGL_lose_context\")?.loseContext();\n supported = gl !== null && gl !== undefined;\n } catch {\n supported = false;\n }\n webgl2Support.set(win, supported);\n return supported;\n}\n\nexport function prefersReducedMotion(\n win: Window | undefined = defaultWindow()\n): boolean {\n return (\n win?.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches ?? false\n );\n}\n\n/**\n * `false` means the print cannot play: show the item sealed right away instead\n * of never showing it. Checked once, before the scene chunk is requested.\n */\nexport function canPlayGlassCube(\n win: Window | undefined = defaultWindow()\n): boolean {\n return !prefersReducedMotion(win) && hasWebgl2(win);\n}\n","import type { GlassCubeLabel } from \"./types\";\n\n/**\n * Accepts the tag the way consumers usually hold it — one string such as\n * `\"XLA-000-006\"` — and splits it at the first dash into the series and the\n * number the cube face prints on separate lines. An object passes through;\n * an empty string means no tag.\n */\nexport function normalizeGlassCubeLabel(\n label: string | GlassCubeLabel | undefined\n): GlassCubeLabel | undefined {\n if (label === undefined) {\n return undefined;\n }\n if (typeof label !== \"string\") {\n return label;\n }\n const trimmed = label.trim();\n if (trimmed === \"\") {\n return undefined;\n }\n const dash = trimmed.indexOf(\"-\");\n if (dash <= 0) {\n return { series: trimmed, number: \"\" };\n }\n return { series: trimmed.slice(0, dash), number: trimmed.slice(dash) };\n}\n\n/** Stable key for comparing labels by value (React deps, memoisation). */\nexport function glassCubeLabelKey(label: GlassCubeLabel | undefined): string {\n return label ? `${label.series}\\u0000${label.number}` : \"\";\n}\n","/**\n * DOM the cube lives in. Mirrors the pay-station `x-status-cube` template:\n *\n * root (flex, centred) as wide as the print: `size × 2.5`, capped by the host\n * ├─ stage square the printed cube is fitted to; its width is the cube size\n * │ └─ img static fallback, shown instead of the canvas\n * └─ overscan 2.5× the stage, centred on it; Sol flies in from outside the cube\n * └─ canvas (appended by the scene)\n *\n * Everything is inline-styled so consumers need no stylesheet; the class names\n * are hooks for anyone who wants to override.\n */\n\n/** Sol flies in from outside the cube frame — the canvas is this much bigger than the cube. */\nexport const STAGE_OVERSCAN = 2.5;\n\nconst CLASS_ROOT = \"xui-glass-cube\";\nconst CLASS_STAGE = `${CLASS_ROOT}__stage`;\nconst CLASS_OVERSCAN = `${CLASS_ROOT}__overscan`;\nconst CLASS_FALLBACK = `${CLASS_ROOT}__fallback`;\n\n/** The fallback picture is inset the way the item sits inside the printed glass. */\nconst FALLBACK_INSET = \"15%\";\nconst FALLBACK_SIZE = \"70%\";\n\nexport interface GlassCubeLayout {\n readonly root: HTMLElement;\n readonly stage: HTMLElement;\n readonly overscan: HTMLElement;\n setSize(px: number | undefined): void;\n showFallback(src: string): void;\n setFallbackSrc(src: string): void;\n dispose(): void;\n}\n\nfunction assign(\n element: HTMLElement,\n style: Partial<CSSStyleDeclaration>\n): void {\n Object.assign(element.style, style);\n}\n\nexport function createGlassCubeLayout(\n host: HTMLElement,\n size: number | undefined\n): GlassCubeLayout {\n const document = host.ownerDocument;\n\n const root = document.createElement(\"div\");\n root.className = CLASS_ROOT;\n root.setAttribute(\"aria-hidden\", \"true\");\n assign(root, {\n position: \"relative\",\n display: \"flex\",\n justifyContent: \"center\",\n // Never wider than the host, so a narrow column shrinks the whole print.\n maxWidth: \"100%\",\n });\n\n const stage = document.createElement(\"div\");\n stage.className = CLASS_STAGE;\n assign(stage, {\n position: \"relative\",\n flexShrink: \"0\",\n width: `${100 / STAGE_OVERSCAN}%`,\n });\n // Keeps the stage square without `aspect-ratio`.\n const square = document.createElement(\"div\");\n assign(square, { paddingTop: \"100%\" });\n stage.appendChild(square);\n\n const overscan = document.createElement(\"div\");\n overscan.className = CLASS_OVERSCAN;\n assign(overscan, {\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n width: \"100%\",\n height: `${STAGE_OVERSCAN * 100}%`,\n pointerEvents: \"none\",\n transform: \"translate(-50%, -50%)\",\n });\n\n root.append(stage, overscan);\n host.appendChild(root);\n\n let fallback: HTMLImageElement | null = null;\n\n const layout: GlassCubeLayout = {\n root,\n stage,\n overscan,\n setSize(px) {\n // The stage is a fixed fraction of the root, so sizing the root to the\n // whole print makes the stage exactly `px` wide — no wrapper needed.\n root.style.width = px === undefined ? \"100%\" : `${px * STAGE_OVERSCAN}px`;\n },\n showFallback(src) {\n overscan.remove();\n if (!fallback) {\n fallback = document.createElement(\"img\");\n fallback.className = CLASS_FALLBACK;\n fallback.alt = \"\";\n assign(fallback, {\n position: \"absolute\",\n top: FALLBACK_INSET,\n left: FALLBACK_INSET,\n width: FALLBACK_SIZE,\n height: FALLBACK_SIZE,\n objectFit: \"contain\",\n });\n stage.appendChild(fallback);\n }\n fallback.src = src;\n },\n setFallbackSrc(src) {\n if (fallback) {\n fallback.src = src;\n }\n },\n dispose() {\n root.remove();\n },\n };\n\n layout.setSize(size);\n\n return layout;\n}\n","import { resolveGlassCubeAssets } from \"./assets\";\nimport { canPlayGlassCube } from \"./capabilities\";\nimport { normalizeGlassCubeLabel } from \"./label\";\nimport { createGlassCubeLayout } from \"./layout\";\nimport type { GlassCubeScene } from \"./scene/scene/glass-cube-scene\";\nimport type { GlassCubeHandle, GlassCubeOptions } from \"./types\";\n\n/**\n * Builds the cube inside `host`. The WebGL scene (and `three` with it) is\n * requested only when the print can actually play; otherwise, and whenever the\n * scene fails to load or loses its context, the item is shown as a plain picture.\n */\nexport function mountGlassCube(\n host: HTMLElement,\n options: GlassCubeOptions\n): GlassCubeHandle {\n const sealed = options.sealed ?? false;\n const printer = options.printer ?? !sealed;\n const layout = createGlassCubeLayout(host, options.size);\n\n let item = options.item;\n let scene: GlassCubeScene | null = null;\n let isDisposed = false;\n let isFallback = false;\n /** The cube has been sealed at least once — the fallback must not announce it again. */\n let hasCompleted = false;\n /** A print is wanted as soon as the scene is ready. */\n let playPending = !sealed && (options.autoPlay ?? true);\n\n const complete = (): void => {\n if (isDisposed) {\n return;\n }\n hasCompleted = true;\n options.onComplete?.();\n };\n\n const fallBack = (): void => {\n if (isDisposed || isFallback) {\n return;\n }\n isFallback = true;\n scene?.dispose();\n scene = null;\n layout.showFallback(item);\n if (!hasCompleted) {\n complete();\n }\n };\n\n const start = async (): Promise<void> => {\n try {\n const { GlassCubeScene } = await import(\"./scene/scene/glass-cube-scene\");\n const created = await GlassCubeScene.create({\n container: layout.overscan,\n stage: layout.stage,\n item: options.item,\n fallbackItem: options.fallbackItem,\n sealed,\n printer,\n label: normalizeGlassCubeLabel(options.label),\n assets: resolveGlassCubeAssets(options.assets),\n onComplete: complete,\n onContextLost: fallBack,\n });\n // The picture may have been swapped while the assets were loading.\n if (item !== options.item) {\n await created.setItem(item);\n }\n if (isDisposed || isFallback) {\n created.dispose();\n return;\n }\n // Published only now: `play` / `setItem` queue up behind `ready` until then.\n scene = created;\n if (sealed) {\n complete();\n return;\n }\n if (playPending) {\n playPending = false;\n created.play();\n }\n } catch {\n fallBack();\n }\n };\n\n let ready: Promise<void>;\n if (canPlayGlassCube(host.ownerDocument.defaultView ?? undefined)) {\n ready = start();\n } else {\n ready = Promise.resolve();\n fallBack();\n }\n\n return {\n play() {\n if (sealed || isFallback || isDisposed) {\n return;\n }\n if (scene) {\n scene.play();\n } else {\n playPending = true;\n }\n },\n async setItem(src) {\n item = src;\n if (isFallback) {\n layout.setFallbackSrc(src);\n return;\n }\n await ready;\n if (isDisposed || isFallback) {\n return;\n }\n // Already printed by `start` when the swap happened during loading; the\n // scene skips a repeat of its current URL.\n await scene?.setItem(src);\n },\n setSize(px) {\n layout.setSize(px);\n },\n dispose() {\n if (isDisposed) {\n return;\n }\n isDisposed = true;\n scene?.dispose();\n scene = null;\n layout.dispose();\n },\n };\n}\n"],"mappings":"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","names":[]}
|