@energy8platform/platform-core 0.30.10 → 0.31.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 +1 -1
- package/dist/index.cjs.js +246 -29
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +180 -2
- package/dist/index.esm.js +241 -30
- package/dist/index.esm.js.map +1 -1
- package/dist/loading.cjs.js +246 -29
- package/dist/loading.cjs.js.map +1 -1
- package/dist/loading.d.ts +180 -2
- package/dist/loading.esm.js +241 -30
- package/dist/loading.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/loading/ExternalOverlay.ts +249 -0
- package/src/loading/index.ts +20 -1
- package/src/loading/variants/voidmoon.ts +38 -29
- package/src/types.ts +55 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A loading overlay the GAME owns, covering the gap the engine cannot: from the
|
|
3
|
+
* browser's first paint until the engine's own loading screen is on screen.
|
|
4
|
+
*
|
|
5
|
+
* The case this exists for is Artube. Their platform ships a branded loader
|
|
6
|
+
* whose markup a Vite plugin injects into `index.html`, so it is painted before
|
|
7
|
+
* the game bundle has even been fetched — earlier than any engine code can
|
|
8
|
+
* possibly run. That window (bundle download → Pixi init → SDK handshake) is
|
|
9
|
+
* otherwise a blank page.
|
|
10
|
+
*
|
|
11
|
+
* ── Where it stops ──────────────────────────────────────────────────────────
|
|
12
|
+
* It stops once BOTH are true: the engine's own loading screen has painted its
|
|
13
|
+
* first frame, and the overlay has had its guaranteed time on screen
|
|
14
|
+
* ({@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}, `loading.externalOverlayMinDisplayTime`).
|
|
15
|
+
* The gap alone is often only a few hundred milliseconds, which is not long
|
|
16
|
+
* enough for a partner's branding to register — let alone for their two-phase
|
|
17
|
+
* screen to reach its second phase.
|
|
18
|
+
*
|
|
19
|
+
* From there the player gets the engine's brand, the engine's progress bar and
|
|
20
|
+
* the engine's tap-to-start — exactly as on every other target. The external
|
|
21
|
+
* overlay covers the gap; it does not replace the loading screen, and it has no
|
|
22
|
+
* say in the tap gate: by then the player is looking at OUR screen.
|
|
23
|
+
*
|
|
24
|
+
* (This inverts an earlier design in which the external overlay REPLACED the
|
|
25
|
+
* CSS preloader for the whole boot. Handing over at the first frame keeps the
|
|
26
|
+
* game's own loading identity on every platform and leaves the built-in
|
|
27
|
+
* preloader's code path untouched — `CSSPreloader.ts` knows nothing about any
|
|
28
|
+
* of this, so non-Artube targets cannot be affected by a change here.)
|
|
29
|
+
*
|
|
30
|
+
* ── Structural, not nominal ─────────────────────────────────────────────────
|
|
31
|
+
* Nothing in `@energy8platform` names Artube's controller. The game passes an
|
|
32
|
+
* instance; we describe the shape (`ExternalLoadingOverlay`). Artube's vendored
|
|
33
|
+
* `LoaderViewController` (`@energy8platform/artube-bridge/loader`) satisfies it
|
|
34
|
+
* with no adapter.
|
|
35
|
+
*
|
|
36
|
+
* ── Module-level state ──────────────────────────────────────────────────────
|
|
37
|
+
* Singleton, like the CSS preloader next door, and for the same reason: the
|
|
38
|
+
* three call sites (boot, hand-over, failure path) are in different files and
|
|
39
|
+
* there is exactly one loading screen per page.
|
|
40
|
+
*/
|
|
41
|
+
import type { ExternalLoadingOverlay } from '../types';
|
|
42
|
+
|
|
43
|
+
let adopted: ExternalLoadingOverlay | null = null;
|
|
44
|
+
/** Last value pushed, so a repeated milestone cannot walk the bar backwards. */
|
|
45
|
+
let lastProgress = 0;
|
|
46
|
+
/** When the overlay was adopted — the origin the minimum display time is measured from. */
|
|
47
|
+
let adoptedAt = 0;
|
|
48
|
+
/** How long this overlay is owed on screen; see {@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}. */
|
|
49
|
+
let minDisplayMs = 0;
|
|
50
|
+
/** When the first progress went out, i.e. when their phase crossfade started. 0 = not yet. */
|
|
51
|
+
let crossfadeStartedAt = 0;
|
|
52
|
+
/** The one pending timer: the hand-over waiting out the floor below. */
|
|
53
|
+
let holdTimer: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
+
let holdResolve: (() => void) | null = null;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How long a game-supplied overlay is guaranteed on screen, measured from
|
|
58
|
+
* adoption (which is the first thing the boot does, so in practice from the
|
|
59
|
+
* page's first paint).
|
|
60
|
+
*
|
|
61
|
+
* Without a floor the overlay lives exactly as long as the gap it covers, and
|
|
62
|
+
* that gap is short: a warm local boot handed over at ~500ms and dismissed
|
|
63
|
+
* their screen at ~840ms. A partner's branding that flashes past in under a
|
|
64
|
+
* second has not been shown. 1500ms is the user's number, and it is also the
|
|
65
|
+
* smallest one that fits their two-phase screen whole — a 500ms crossfade into
|
|
66
|
+
* the branded phase plus time to read it — with room for the 300ms dismissal
|
|
67
|
+
* fade on top.
|
|
68
|
+
*
|
|
69
|
+
* Overridable per game: `loading.externalOverlayMinDisplayTime`.
|
|
70
|
+
*/
|
|
71
|
+
export const DEFAULT_EXTERNAL_MIN_DISPLAY_MS = 1500;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Artube's partner→branded crossfade, `transition: opacity 0.5s ease` in the
|
|
75
|
+
* markup their Vite plugin injects (vendored verbatim in
|
|
76
|
+
* `@energy8platform/artube-server/vite`). It is triggered by the first
|
|
77
|
+
* `updateProgress` above zero.
|
|
78
|
+
*
|
|
79
|
+
* The floor below is extended to cover it because a crossfade that starts and
|
|
80
|
+
* then gets cut is worse than one that never starts: the player sees a
|
|
81
|
+
* half-formed gradient and a bar that never fills — the three-screens-at-once
|
|
82
|
+
* artefact this integration hit once and rejected (evidence:
|
|
83
|
+
* `rejected-immediate-progress-01-Lartube_P_C.png`). Extending the floor is
|
|
84
|
+
* bounded by construction: only the FIRST progress starts it, and every
|
|
85
|
+
* progress value comes from a boot milestone that precedes the hand-over.
|
|
86
|
+
*/
|
|
87
|
+
const PHASE_CROSSFADE_MS = 500;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Call into the game's overlay without letting it break the boot. A third-party
|
|
91
|
+
* controller is outside our control (Artube's throws if its markup is missing,
|
|
92
|
+
* for one), and none of these calls is worth failing a game over — least of all
|
|
93
|
+
* `hideLoader`, where a throw escaping the teardown path is exactly how an
|
|
94
|
+
* overlay ends up stranded on screen forever.
|
|
95
|
+
*/
|
|
96
|
+
function guard(method: string, fn: () => void): void {
|
|
97
|
+
try {
|
|
98
|
+
fn();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn(`[GameEngine] loading overlay ${method}() threw`, err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether a game-supplied overlay is currently on screen and owned by us. */
|
|
105
|
+
export function hasExternalOverlay(): boolean {
|
|
106
|
+
return adopted !== null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Take ownership of the game's overlay: from here on the engine is responsible
|
|
111
|
+
* for taking it down, including when the boot fails.
|
|
112
|
+
*
|
|
113
|
+
* Called BEFORE anything in the boot that can throw — notably before the
|
|
114
|
+
* container selector is resolved. Until the engine has adopted it, nothing can
|
|
115
|
+
* dismiss it, and Artube's is already on screen from `index.html`.
|
|
116
|
+
*
|
|
117
|
+
* `showLoader()` is Artube's "reveal the progress affordance"; it does not
|
|
118
|
+
* switch their two-phase screen to the branded phase — only progress does, see
|
|
119
|
+
* {@link advanceExternalOverlay}.
|
|
120
|
+
*
|
|
121
|
+
* @param minDisplayMillis how long the overlay is owed on screen before the
|
|
122
|
+
* hand-over may dismiss it (`loading.externalOverlayMinDisplayTime`); defaults
|
|
123
|
+
* to {@link DEFAULT_EXTERNAL_MIN_DISPLAY_MS}. Adoption is where the clock
|
|
124
|
+
* starts, so it is taken here rather than read at the hand-over.
|
|
125
|
+
*/
|
|
126
|
+
export function adoptExternalOverlay(
|
|
127
|
+
overlay: ExternalLoadingOverlay,
|
|
128
|
+
minDisplayMillis?: number,
|
|
129
|
+
): void {
|
|
130
|
+
if (adopted) return; // idempotent: boot step 0 and step 2 both reach here
|
|
131
|
+
adopted = overlay;
|
|
132
|
+
lastProgress = 0;
|
|
133
|
+
crossfadeStartedAt = 0;
|
|
134
|
+
minDisplayMs =
|
|
135
|
+
typeof minDisplayMillis === 'number' && Number.isFinite(minDisplayMillis)
|
|
136
|
+
? Math.max(0, minDisplayMillis)
|
|
137
|
+
: DEFAULT_EXTERNAL_MIN_DISPLAY_MS;
|
|
138
|
+
adoptedAt = Date.now();
|
|
139
|
+
guard('showLoader', () => overlay.showLoader());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Report boot progress, as a 0..1 fraction, to a game-supplied overlay.
|
|
144
|
+
*
|
|
145
|
+
* NOT the asset-loading progress the built-in preloader shows — that belongs to
|
|
146
|
+
* the engine's own loading screen, which by then has taken over. This is the
|
|
147
|
+
* pre-first-frame boot: bundle up, Pixi up, SDK handshake done.
|
|
148
|
+
*
|
|
149
|
+
* Sending it is deliberate, not decorative: Artube's loader only crossfades to
|
|
150
|
+
* its BRANDED phase on the first value above zero, so without progress the
|
|
151
|
+
* player never sees the half of their screen the integration exists to show.
|
|
152
|
+
* Values go out as they arrive — the guarantee that the crossfade has room to
|
|
153
|
+
* finish is the dismissal floor ({@link externalOverlayHold}), not a delay
|
|
154
|
+
* here. An earlier design held the first value back for 800ms instead; the
|
|
155
|
+
* floor makes that redundant, and one timer on this overlay is the whole point.
|
|
156
|
+
*
|
|
157
|
+
* Values are clamped to 0..1, kept monotonic, and converted to the PERCENTAGE
|
|
158
|
+
* (0–100) that `ILoaderViewController.updateProgress` documents.
|
|
159
|
+
*/
|
|
160
|
+
export function advanceExternalOverlay(fraction: number): void {
|
|
161
|
+
const overlay = adopted;
|
|
162
|
+
if (!overlay) return;
|
|
163
|
+
const clamped = Number.isFinite(fraction) ? Math.max(0, Math.min(1, fraction)) : 0;
|
|
164
|
+
if (clamped <= lastProgress) return;
|
|
165
|
+
lastProgress = clamped;
|
|
166
|
+
// The first non-zero value is what starts their partner→branded crossfade, so
|
|
167
|
+
// it is also what the floor has to cover.
|
|
168
|
+
if (clamped > 0 && crossfadeStartedAt === 0) crossfadeStartedAt = Date.now();
|
|
169
|
+
guard('updateProgress', () => overlay.updateProgress(clamped * 100));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Milliseconds still owed to the overlay before the hand-over may dismiss it:
|
|
174
|
+
* the minimum display time, extended when needed so a phase crossfade that has
|
|
175
|
+
* started can finish. Zero once both are satisfied — and zero when there is no
|
|
176
|
+
* overlay, so nothing on a normal target ever waits.
|
|
177
|
+
*/
|
|
178
|
+
function remainingHoldMs(): number {
|
|
179
|
+
if (!adopted) return 0;
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const floor = Math.max(
|
|
182
|
+
adoptedAt + minDisplayMs,
|
|
183
|
+
crossfadeStartedAt === 0 ? 0 : crossfadeStartedAt + PHASE_CROSSFADE_MS,
|
|
184
|
+
);
|
|
185
|
+
return Math.max(0, floor - now);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wait until the overlay has had its guaranteed time on screen. The hand-over
|
|
190
|
+
* awaits this BEFORE it mounts the engine's own loading screen, so the two
|
|
191
|
+
* timelines do not overlap: their screen is whole and undisturbed for the whole
|
|
192
|
+
* window, then ours mounts, paints, and only then takes over.
|
|
193
|
+
*
|
|
194
|
+
* Resolves immediately when no overlay is adopted (every non-Artube target) or
|
|
195
|
+
* when the floor has already passed (any boot slower than it, which is the
|
|
196
|
+
* normal case in production — this costs nothing there).
|
|
197
|
+
*
|
|
198
|
+
* NOT part of the failure path on purpose: {@link releaseExternalOverlay} is
|
|
199
|
+
* synchronous and immediate, so a boot that throws takes the overlay down at
|
|
200
|
+
* once instead of stranding the player behind a courtesy delay. A release also
|
|
201
|
+
* settles a wait already in progress.
|
|
202
|
+
*/
|
|
203
|
+
export function externalOverlayHold(): Promise<void> {
|
|
204
|
+
// Reaching here means the boot is done and the only thing left is the wait, so the bar is
|
|
205
|
+
// finished too. Without this it freezes at the last boot milestone (0.85 live) and their screen
|
|
206
|
+
// is taken away with the bar still short — a bar that never fills, which is the same artefact in
|
|
207
|
+
// a different costume. Only when a bar is actually on screen: if no progress ever went out the
|
|
208
|
+
// player is still on the first phase, and starting a crossfade at the hand-over is precisely
|
|
209
|
+
// what must not happen.
|
|
210
|
+
if (crossfadeStartedAt !== 0) advanceExternalOverlay(1);
|
|
211
|
+
const remaining = remainingHoldMs();
|
|
212
|
+
if (remaining <= 0) return Promise.resolve();
|
|
213
|
+
return new Promise<void>((resolve) => {
|
|
214
|
+
const finish = () => {
|
|
215
|
+
if (holdTimer !== null) clearTimeout(holdTimer);
|
|
216
|
+
holdTimer = null;
|
|
217
|
+
holdResolve = null;
|
|
218
|
+
resolve();
|
|
219
|
+
};
|
|
220
|
+
holdResolve = finish;
|
|
221
|
+
holdTimer = setTimeout(finish, remaining);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Dismiss the overlay and forget it. Idempotent — this is reached both on the
|
|
227
|
+
* normal hand-over and defensively from the failure paths, and a second
|
|
228
|
+
* `hideLoader()` must not fire.
|
|
229
|
+
*
|
|
230
|
+
* @returns whether this call is the one that dismissed it. Callers that can run
|
|
231
|
+
* BEFORE adoption (`createSlotGame`'s `fatal`, which refuses some launches
|
|
232
|
+
* before `GameApplication` exists) use the `false` to fall back to hiding the
|
|
233
|
+
* game's overlay directly.
|
|
234
|
+
*/
|
|
235
|
+
export function releaseExternalOverlay(): boolean {
|
|
236
|
+
// Settle a hand-over that is still waiting out the floor, even if there is
|
|
237
|
+
// nothing to release. This is the failure path: a boot that threw must not
|
|
238
|
+
// leave a timer holding the process open (Node-side tests) or a promise that
|
|
239
|
+
// resolves into a teardown that already happened.
|
|
240
|
+
if (holdResolve) holdResolve();
|
|
241
|
+
|
|
242
|
+
const overlay = adopted;
|
|
243
|
+
if (!overlay) return false;
|
|
244
|
+
adopted = null;
|
|
245
|
+
lastProgress = 0;
|
|
246
|
+
crossfadeStartedAt = 0;
|
|
247
|
+
guard('hideLoader', () => overlay.hideLoader());
|
|
248
|
+
return true;
|
|
249
|
+
}
|
package/src/loading/index.ts
CHANGED
|
@@ -4,6 +4,19 @@ export {
|
|
|
4
4
|
waitCSSPreloaderTap,
|
|
5
5
|
removeCSSPreloader,
|
|
6
6
|
} from './CSSPreloader';
|
|
7
|
+
/**
|
|
8
|
+
* A game-supplied overlay covering the gap before the engine's own loading
|
|
9
|
+
* screen paints (Artube's branded loader). Deliberately NOT part of
|
|
10
|
+
* `CSSPreloader.ts`: the built-in preloader must have no branch for it.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
adoptExternalOverlay,
|
|
14
|
+
advanceExternalOverlay,
|
|
15
|
+
externalOverlayHold,
|
|
16
|
+
releaseExternalOverlay,
|
|
17
|
+
hasExternalOverlay,
|
|
18
|
+
DEFAULT_EXTERNAL_MIN_DISPLAY_MS,
|
|
19
|
+
} from './ExternalOverlay';
|
|
7
20
|
export { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from './logo';
|
|
8
21
|
export { VARIANTS, DEFAULT_VARIANT_NAME } from './variants';
|
|
9
22
|
export type {
|
|
@@ -11,4 +24,10 @@ export type {
|
|
|
11
24
|
PreloaderVariantHandle,
|
|
12
25
|
PreloaderVariantName,
|
|
13
26
|
} from './variants';
|
|
14
|
-
export type {
|
|
27
|
+
export type {
|
|
28
|
+
LoadingScreenConfig,
|
|
29
|
+
ExternalLoadingOverlay,
|
|
30
|
+
AssetManifest,
|
|
31
|
+
AssetBundle,
|
|
32
|
+
AssetEntry,
|
|
33
|
+
} from '../types';
|
|
@@ -4,41 +4,50 @@ import type { PreloaderVariant, PreloaderVariantHandle } from './types';
|
|
|
4
4
|
const RECT_ID = 'ge-vm-loader-rect';
|
|
5
5
|
const TEXT_ID = 'ge-vm-loader-text';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
const
|
|
7
|
+
/** Left edge (SVG units) of the loader bar — aligned with the "V" of the wordmark. */
|
|
8
|
+
const LOADER_BAR_X = 249.79;
|
|
9
|
+
|
|
10
|
+
/** Max width (SVG units) of the voidmoon loader bar fill. Spans the wordmark: "V" → end of the final "N". */
|
|
11
|
+
const LOADER_BAR_MAX_WIDTH = 519;
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* voidmoon logo — the official lockup, embedded verbatim as SVG outlines:
|
|
15
|
+
* the crescent-moon-and-spark mark on the left, the "VOIDMOON" wordmark on the
|
|
16
|
+
* right, all in #f5f5f5 (filled *and* stroked, exactly as exported — the stroke
|
|
17
|
+
* is what gives the thin glyphs their weight). The export's `.st0` / `.st1`
|
|
18
|
+
* classes are inlined as presentation attributes on two wrapper groups so the
|
|
19
|
+
* logo carries no global CSS into the host page.
|
|
20
|
+
*
|
|
21
|
+
* The loader bar + status text are added beneath the lockup in the same
|
|
22
|
+
* viewBox space; the viewBox is taller than the artwork (140.2) to make room.
|
|
16
23
|
*/
|
|
17
|
-
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="
|
|
24
|
+
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 769.28 215" class="ge-vm-logo-svg" style="overflow:visible" role="img">
|
|
18
25
|
<title>voidmoon</title>
|
|
19
|
-
<g
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
<g fill="#f5f5f5" stroke="#f5f5f5" stroke-miterlimit="10">
|
|
27
|
+
<polygon points="274.76 98.02 249.79 48.07 256.03 48.07 274.76 85.54 293.49 48.07 299.74 48.07 274.76 98.02"/>
|
|
28
|
+
<path d="M325.51,70.2h6.24c1.12-8.22,7.63-14.65,15.83-15.85v-6.24c-11.99,1.24-20.85,10.36-22.07,22.1ZM353.82,48.1v6.24c9.03,1.32,15.48,9.27,15.48,18.67,0,10.33-8.4,18.73-18.73,18.73-9.33,0-17.41-6.36-18.82-15.3h-6.24c1.49,12.45,11.94,21.55,25.06,21.55,14.14,0,24.97-11.14,24.97-24.97,0-12.92-8.88-23.58-21.72-24.91Z"/>
|
|
29
|
+
<rect x="400.39" y="48.08" width="6.24" height="49.95"/>
|
|
30
|
+
<path d="M437.62,54.32l-6.14-6.24h19.14c13.15,0,23.81,10.85,23.81,24.22v.63c0,13.91-10.99,25.1-24.54,25.1-9.62,0-18.41,0-18.41,0l6.14-6.24h12.27c10.17,0,18.41-8.39,18.41-18.73h0c0-10.34-8.24-18.73-18.41-18.73h-12.27Z"/>
|
|
31
|
+
<polygon points="499.59 98.02 505.83 98.02 505.85 63.24 524.56 79.29 543.29 64.35 543.29 98.02 549.54 98.02 549.54 48.07 524.62 70.2 499.59 48.07 499.59 98.02"/>
|
|
32
|
+
<polygon points="725.07 68.91 731.32 74.56 731.32 98.02 725.07 98.02 725.07 68.91"/>
|
|
33
|
+
<polygon points="768.78 48.07 768.78 98.01 762.53 92.38 753.31 84.06 753.31 84.05 725.07 58.51 725.07 48.1 731.32 53.73 762.53 81.87 762.53 48.07 768.78 48.07"/>
|
|
34
|
+
<path d="M602.55,48.03v6.24c8.22,1.12,14.65,7.63,15.85,15.83h6.24c-1.24-11.99-10.36-20.85-22.1-22.07ZM624.65,76.34h-6.24c-1.32,9.03-9.27,15.48-18.67,15.48-10.33,0-18.73-8.4-18.73-18.73,0-9.33,6.36-17.41,15.3-18.82v-6.24c-12.45,1.49-21.55,11.94-21.55,25.06,0,14.14,11.14,24.97,24.97,24.97,12.92,0,23.58-8.88,24.91-21.72Z"/>
|
|
35
|
+
<path d="M699.92,75.89h-6.24c-1.12,8.22-7.63,14.65-15.83,15.85v6.24c11.99-1.24,20.85-10.36,22.07-22.1ZM671.6,97.99v-6.24c-9.03-1.32-15.48-9.27-15.48-18.67,0-10.33,8.4-18.73,18.73-18.73,9.33,0,17.41,6.36,18.82,15.3h6.24c-1.49-12.45-11.94-21.55-25.06-21.55-14.14,0-24.97,11.14-24.97,24.97,0,12.92,8.88,23.58,21.72,24.91Z"/>
|
|
36
|
+
</g>
|
|
37
|
+
<g fill="#f5f5f5" stroke="#f5f5f5" stroke-miterlimit="10" stroke-width="2">
|
|
38
|
+
<path d="M199.39,73.47h.01c-1.76,36.6-31.98,65.73-69.01,65.73s-67.52-29.39-69.04-66.2c-.9-.53-1.53-1.47-1.63-2.57l-22.61,2.67c-1.75.2-3.12,1.6-3.3,3.35l-2.47,24.86-3.11-24.95c-.21-1.7-1.56-3.06-3.28-3.25L.12,70.11l25.29-1.97c1.83-.14,3.28-1.57,3.46-3.38l2.47-25.88,2.47,25.88c.18,1.81,1.63,3.24,3.45,3.38l22.45,1.74c.06-1.14.7-2.13,1.64-2.68C62.87,30.38,93.2,1,130.39,1s67.25,29.12,69.01,65.72h0s-.01,0-.01,0c-1.74-30.76-27.24-55.15-58.43-55.15s-58.52,26.2-58.52,58.52,26.2,58.52,58.52,58.52,56.69-24.4,58.43-55.15Z"/>
|
|
39
|
+
<path d="M202.84,70.1c0,1.86-1.51,3.38-3.37,3.38-.02,0-.05,0-.07,0,.02-.25.02-.49.03-.74-.01.25-.03.49-.04.74-1.83-.04-3.3-1.53-3.3-3.37s1.47-3.34,3.3-3.37c.01.25.03.49.04.74-.01-.25-.01-.49-.03-.74.02,0,.05,0,.07,0,1.86,0,3.37,1.51,3.37,3.38Z"/>
|
|
40
|
+
<line x1="232.26" y1="70.1" x2="202.84" y2="70.1"/>
|
|
41
|
+
<line x1="196.09" y1="70.1" x2="166.58" y2="70.1"/>
|
|
33
42
|
</g>
|
|
34
43
|
|
|
35
|
-
<rect x="
|
|
44
|
+
<rect x="${LOADER_BAR_X}" y="170" width="${LOADER_BAR_MAX_WIDTH}" height="6.5" rx="3.25" fill="rgba(255,255,255,0.12)"/>
|
|
36
45
|
<clipPath id="vm-loader-clip">
|
|
37
|
-
<rect id="${RECT_ID}" x="
|
|
46
|
+
<rect id="${RECT_ID}" x="${LOADER_BAR_X}" y="170" width="0" height="6.5" rx="3.25" class="ge-vm-clip-rect"/>
|
|
38
47
|
</clipPath>
|
|
39
|
-
<rect x="
|
|
48
|
+
<rect x="${LOADER_BAR_X}" y="170" width="${LOADER_BAR_MAX_WIDTH}" height="6.5" rx="3.25" fill="#9D63FE" clip-path="url(#vm-loader-clip)"/>
|
|
40
49
|
|
|
41
|
-
<text id="${TEXT_ID}" x="
|
|
50
|
+
<text id="${TEXT_ID}" x="${LOADER_BAR_X + LOADER_BAR_MAX_WIDTH / 2}" y="205" text-anchor="middle" class="ge-vm-text">Loading...</text>
|
|
42
51
|
</svg>`;
|
|
43
52
|
|
|
44
53
|
export const voidmoonVariant: PreloaderVariant = {
|
|
@@ -72,7 +81,7 @@ export const voidmoonVariant: PreloaderVariant = {
|
|
|
72
81
|
|
|
73
82
|
@keyframes ge-vm-fill {
|
|
74
83
|
0% { width: 0; }
|
|
75
|
-
50% { width:
|
|
84
|
+
50% { width: 519px; }
|
|
76
85
|
100% { width: 0; }
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -84,9 +93,9 @@ export const voidmoonVariant: PreloaderVariant = {
|
|
|
84
93
|
.ge-vm-text {
|
|
85
94
|
fill: rgba(255, 255, 255, 0.6);
|
|
86
95
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
87
|
-
font-size:
|
|
96
|
+
font-size: 14px;
|
|
88
97
|
font-weight: 600;
|
|
89
|
-
letter-spacing:
|
|
98
|
+
letter-spacing: 2.2px;
|
|
90
99
|
animation: ge-vm-pulse 1.5s ease-in-out infinite;
|
|
91
100
|
}
|
|
92
101
|
|
package/src/types.ts
CHANGED
|
@@ -44,7 +44,62 @@ export interface AssetManifest {
|
|
|
44
44
|
// Used by the Energy8 CSS preloader (in this package) and by
|
|
45
45
|
// engine-specific loading scenes (in @energy8platform/game-engine etc.).
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A loading overlay the GAME owns, covering the gap BEFORE the engine's own loading screen exists:
|
|
49
|
+
* from the browser's first paint to the first frame the engine paints. It does not replace the
|
|
50
|
+
* loading screen — see `LoadingScreenConfig.externalOverlay`.
|
|
51
|
+
*
|
|
52
|
+
* Described structurally on purpose. The implementation this exists for is Artube's
|
|
53
|
+
* `LoaderViewController` (vendored at `@energy8platform/artube-bridge/loader`), whose markup their
|
|
54
|
+
* Vite plugin injects into `index.html` so the overlay is painted before our bundle is even
|
|
55
|
+
* fetched. Naming that type here would tie every consumer of platform-core to the Artube packages;
|
|
56
|
+
* the shape is satisfied with no adapter, and nothing in `@energy8platform/platform-core` knows
|
|
57
|
+
* Artube exists.
|
|
58
|
+
*
|
|
59
|
+
* The engine's use of it, in order:
|
|
60
|
+
* - `showLoader()` once, at the very start of boot, before anything that can throw;
|
|
61
|
+
* - `updateProgress(0..100)` at boot milestones (Pixi up, SDK handshake done, subsystems up) —
|
|
62
|
+
* real progress through the gap, not the asset loading that follows;
|
|
63
|
+
* - `hideLoader()` the moment the engine's loading screen has painted its first frame — AND on
|
|
64
|
+
* every failure path, so a boot that throws can never leave the overlay on screen forever.
|
|
65
|
+
*/
|
|
66
|
+
export interface ExternalLoadingOverlay {
|
|
67
|
+
/** Reveal the overlay / its progress affordance. Called once, at the start of boot. */
|
|
68
|
+
showLoader(): void;
|
|
69
|
+
/** Loading progress as a PERCENTAGE, 0–100 (what `ILoaderViewController` expects — the engine's
|
|
70
|
+
* internal 0..1 fraction is converted at the seam). */
|
|
71
|
+
updateProgress(value: number): void;
|
|
72
|
+
/** Dismiss the overlay. Called once; must tolerate being called after a failed boot. */
|
|
73
|
+
hideLoader(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
export interface LoadingScreenConfig {
|
|
77
|
+
/**
|
|
78
|
+
* A game-supplied loading overlay covering ONLY the gap before the engine's own loading screen
|
|
79
|
+
* paints — Artube's branded loader is the case this exists for. It is not a replacement: once the
|
|
80
|
+
* loading screen has painted its first frame, the overlay is dismissed and the rest of the boot
|
|
81
|
+
* (brand, progress bar, tap-to-start, minimum display time) is exactly what it is on every other
|
|
82
|
+
* target. Every other option in this object therefore still applies.
|
|
83
|
+
*
|
|
84
|
+
* The two never stack visibly: the built-in preloader is mounted only at the hand-over, and it
|
|
85
|
+
* covers the external overlay (z-index) from the frame it appears in, so there is no gap, no
|
|
86
|
+
* flash and no bare background at the seam.
|
|
87
|
+
*/
|
|
88
|
+
externalOverlay?: ExternalLoadingOverlay;
|
|
89
|
+
/**
|
|
90
|
+
* How long {@link externalOverlay} is guaranteed on screen, in ms, measured from the boot's very
|
|
91
|
+
* first step. Default 1500.
|
|
92
|
+
*
|
|
93
|
+
* The gap this overlay covers is short — a warm boot hands over in a few hundred milliseconds —
|
|
94
|
+
* and a partner's branding that flashes past in under a second has not been shown. The floor also
|
|
95
|
+
* gives a two-phase overlay (Artube's) room to reach its second phase and settle there instead of
|
|
96
|
+
* being cut mid-crossfade. It costs a slow boot nothing: the hand-over happens later than this
|
|
97
|
+
* anyway.
|
|
98
|
+
*
|
|
99
|
+
* Distinct from {@link minDisplayTime}, which is the minimum for the engine's OWN loading screen
|
|
100
|
+
* and is measured from the hand-over. A game that sets both is asking for the sum.
|
|
101
|
+
*/
|
|
102
|
+
externalOverlayMinDisplayTime?: number;
|
|
48
103
|
/**
|
|
49
104
|
* Which visual identity the CSS preloader renders. Defaults to `'energy8'`;
|
|
50
105
|
* an unknown value falls back to the default. Ignored when `cssPreloaderHTML`
|