@caperjs/core 0.7.2 → 0.7.4
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/build/index.mjs +1 -1
- package/build/internal/buildFlags.mjs +63 -18
- package/build/internal/schema.mjs +53 -0
- package/build/plugins/pwa.mjs +13 -7
- package/cli/doctor.mjs +53 -2
- package/cli/doctor.test.mjs +80 -1
- package/cli/native.mjs +126 -15
- package/cli/native.test.mjs +219 -3
- package/extras/llms.txt +35 -3
- package/lib/{CaptionsPlugin-COrofpHN.js → CaptionsPlugin-CAgQBcA1.js} +7 -7
- package/lib/{CaptionsPlugin-COrofpHN.js.map → CaptionsPlugin-CAgQBcA1.js.map} +1 -1
- package/lib/{DataAdapter-T-22-_D1.js → DataAdapter-CbyqWMo-.js} +5 -5
- package/lib/{DataAdapter-T-22-_D1.js.map → DataAdapter-CbyqWMo-.js.map} +1 -1
- package/lib/{DebugRenderer-ZN692Pbg.js → DebugRenderer-Bdkpyyt6.js} +2 -2
- package/lib/{DebugRenderer-ZN692Pbg.js.map → DebugRenderer-Bdkpyyt6.js.map} +1 -1
- package/lib/{DevToolsPlugin-CQXFoj1T.js → DevToolsPlugin-Cq5HHN9K.js} +2 -2
- package/lib/{DevToolsPlugin-CQXFoj1T.js.map → DevToolsPlugin-Cq5HHN9K.js.map} +1 -1
- package/lib/{GSAPPlugin-CNkncyXS.js → GSAPPlugin-D-ujhVYo.js} +2 -2
- package/lib/{GSAPPlugin-CNkncyXS.js.map → GSAPPlugin-D-ujhVYo.js.map} +1 -1
- package/lib/{LayoutPlugin-BuJgr9bd.js → LayoutPlugin-DqbwBm-X.js} +4 -4
- package/lib/{LayoutPlugin-BuJgr9bd.js.map → LayoutPlugin-DqbwBm-X.js.map} +1 -1
- package/lib/{ScreenDebugPlugin-kz9Mr6j1.js → ScreenDebugPlugin-CyT8UxEa.js} +3 -3
- package/lib/{ScreenDebugPlugin-kz9Mr6j1.js.map → ScreenDebugPlugin-CyT8UxEa.js.map} +1 -1
- package/lib/{SpinePlugin-DDzD-Qxd.js → SpinePlugin-DyXJyA3x.js} +2 -2
- package/lib/{SpinePlugin-DDzD-Qxd.js.map → SpinePlugin-DyXJyA3x.js.map} +1 -1
- package/lib/{StatsPlugin-tyFGzVjy.js → StatsPlugin-CEhgzWby.js} +4 -4
- package/lib/{StatsPlugin-tyFGzVjy.js.map → StatsPlugin-CEhgzWby.js.map} +1 -1
- package/lib/{VoiceOverPlugin-rTNz_9eb.js → VoiceOverPlugin-CSpNZjpJ.js} +2 -2
- package/lib/{VoiceOverPlugin-rTNz_9eb.js.map → VoiceOverPlugin-CSpNZjpJ.js.map} +1 -1
- package/lib/caper.mjs +126 -129
- package/lib/caper.mjs.map +1 -1
- package/lib/{const-Cj14a5U2.js → const-Bca54QyV.js} +235 -227
- package/lib/{const-Cj14a5U2.js.map → const-Bca54QyV.js.map} +1 -1
- package/lib/core/Application.d.ts +4 -0
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts +21 -0
- package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
- package/lib/plugins/OrientationPlugin.d.ts +57 -0
- package/lib/plugins/OrientationPlugin.d.ts.map +1 -0
- package/lib/plugins/OrientationPlugin.test.d.ts +2 -0
- package/lib/plugins/OrientationPlugin.test.d.ts.map +1 -0
- package/lib/plugins/defaults.d.ts.map +1 -1
- package/lib/plugins/index.d.ts +1 -0
- package/lib/plugins/index.d.ts.map +1 -1
- package/lib/{registries-DmNGVLAi.js → registries-C1nPl8B3.js} +593 -502
- package/lib/registries-C1nPl8B3.js.map +1 -0
- package/lib/ui/UICanvas.d.ts +10 -2
- package/lib/ui/UICanvas.d.ts.map +1 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/orientation.d.ts +44 -0
- package/lib/utils/orientation.test.d.ts +2 -0
- package/lib/utils/orientation.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/Application.ts +10 -0
- package/src/core/interfaces/IApplicationOptions.ts +21 -0
- package/src/plugins/OrientationPlugin.test.ts +252 -0
- package/src/plugins/OrientationPlugin.ts +178 -0
- package/src/plugins/defaults.ts +6 -0
- package/src/plugins/index.ts +1 -0
- package/src/ui/UICanvas.test.ts +23 -1
- package/src/ui/UICanvas.ts +22 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/orientation.d.ts +44 -0
- package/src/utils/orientation.js +37 -0
- package/src/utils/orientation.test.ts +30 -0
- package/src/version.ts +1 -1
- package/lib/registries-DmNGVLAi.js.map +0 -1
package/lib/ui/UICanvas.d.ts
CHANGED
|
@@ -29,17 +29,23 @@ export type UICanvasConfig = {
|
|
|
29
29
|
padding: Padding;
|
|
30
30
|
size: Size;
|
|
31
31
|
useAppSize: boolean;
|
|
32
|
-
useSafeArea:
|
|
32
|
+
useSafeArea: UICanvasSafeArea;
|
|
33
33
|
layout?: Omit<LayoutOptions, 'target'> | null | boolean;
|
|
34
34
|
autoLayoutChildren?: boolean;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Which device safe-area insets the canvas pads by: `true` (all four, the default), `false`
|
|
38
|
+
* (none), or per edge, where an edge left out stays on. `{ top: false }` lets top-row UI sit
|
|
39
|
+
* in the camera strip of a punch-hole phone while the bottom still clears the gesture bar.
|
|
40
|
+
*/
|
|
41
|
+
export type UICanvasSafeArea = boolean | Partial<Record<keyof Padding, boolean>>;
|
|
36
42
|
export declare const UICanvasConfigKeys: (keyof UICanvasConfig)[];
|
|
37
43
|
export type UICanvasProps = {
|
|
38
44
|
debug: boolean;
|
|
39
45
|
padding: Partial<Padding> | PointLike;
|
|
40
46
|
size?: SizeLike;
|
|
41
47
|
useAppSize?: boolean;
|
|
42
|
-
useSafeArea?:
|
|
48
|
+
useSafeArea?: UICanvasSafeArea;
|
|
43
49
|
layout?: Omit<LayoutOptions, 'target'> | null | boolean;
|
|
44
50
|
autoLayoutChildren?: boolean;
|
|
45
51
|
};
|
|
@@ -49,6 +55,8 @@ export type UICanvasProps = {
|
|
|
49
55
|
* re-derived on every resize without compounding.
|
|
50
56
|
*/
|
|
51
57
|
export declare function computeEffectivePadding(padding: Padding, safeArea: Padding): Padding;
|
|
58
|
+
/** The safe-area insets `useSafeArea` keeps: all, none, or the edges not set to `false`. */
|
|
59
|
+
export declare function resolveSafeArea(safeArea: Padding, useSafeArea: UICanvasSafeArea): Padding;
|
|
52
60
|
declare const _UICanvas: (new () => import('..').IFactory<{
|
|
53
61
|
existing: <TEntity>(entity: TEntity, props?: Partial<import('..').ExistingProps>) => TEntity;
|
|
54
62
|
container: (props?: Partial<import('..').ContainerProps> | undefined) => Container;
|
package/lib/ui/UICanvas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UICanvas.d.ts","sourceRoot":"","sources":["../../src/ui/UICanvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,IAAI,aAAa,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAElG,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAarF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,UAAU,GACV,YAAY,GACZ,KAAK,GACL,cAAc,GACd,aAAa,GACb,eAAe,GACf,QAAQ,GACR,UAAU,GACV,aAAa,GACb,aAAa,GACb,MAAM,GACN,WAAW,GACX,cAAc,GACd,cAAc,GACd,OAAO,GACP,QAAQ,CAAC;AAEb,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;sCAEsC;AACtC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;kEACkE;AAClE,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAQpF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"UICanvas.d.ts","sourceRoot":"","sources":["../../src/ui/UICanvas.ts"],"names":[],"mappings":"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"}
|
package/lib/utils/index.d.ts
CHANGED
package/lib/utils/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,OAAO,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,OAAO,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The orientation a game is locked to. */
|
|
2
|
+
export type OrientationLock = 'portrait' | 'landscape';
|
|
3
|
+
|
|
4
|
+
/** `orientation.overlay` in `caper.config.ts`: restyle or replace the web "rotate your device" overlay. */
|
|
5
|
+
export interface OrientationOverlayOptions {
|
|
6
|
+
/** Default: "Rotate your device" (portrait) / "Turn your device sideways" (landscape). */
|
|
7
|
+
text?: string;
|
|
8
|
+
/** CSS background. Default `#000`. */
|
|
9
|
+
background?: string;
|
|
10
|
+
/** CSS text color. Default `#fff`. */
|
|
11
|
+
color?: string;
|
|
12
|
+
/** CSS font family. Default: the system font. */
|
|
13
|
+
fontFamily?: string;
|
|
14
|
+
/** Class(es) added to the overlay so a game can style it with its own CSS. */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Build the whole overlay yourself; replaces the default element. Called once, when first shown. */
|
|
17
|
+
element?: () => HTMLElement;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `orientation` in `caper.config.ts`: a lock, or `{ lock, overlay }`. The string
|
|
22
|
+
* form is shorthand for `{ lock }` with the default overlay. `overlay: false`
|
|
23
|
+
* turns off the web overlay and its pause.
|
|
24
|
+
*/
|
|
25
|
+
export type OrientationConfig =
|
|
26
|
+
| OrientationLock
|
|
27
|
+
| {
|
|
28
|
+
lock: OrientationLock;
|
|
29
|
+
overlay?: false | OrientationOverlayOptions;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** `orientation` normalized by `resolveOrientation`. */
|
|
33
|
+
export interface ResolvedOrientation {
|
|
34
|
+
lock: OrientationLock;
|
|
35
|
+
/** `false` (no overlay, no pause) or the overlay options (`{}` for the default look). */
|
|
36
|
+
overlay: false | OrientationOverlayOptions;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `orientation` as `{ lock, overlay }`, or `null` when unset. The string form is
|
|
41
|
+
* shorthand for `{ lock }`; a missing `overlay` means the default overlay (`{}`).
|
|
42
|
+
* Throws a `TypeError` on anything else.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveOrientation(orientation: unknown): ResolvedOrientation | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orientation.test.d.ts","sourceRoot":"","sources":["../../src/utils/orientation.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
package/src/core/Application.ts
CHANGED
|
@@ -65,6 +65,7 @@ import { type IDevToolsPlugin } from '../plugins/DevToolsPlugin';
|
|
|
65
65
|
import { IFullScreenPlugin } from '../plugins/FullScreenPlugin';
|
|
66
66
|
import { type IGSAPPlugin } from '../plugins/GSAPPlugin';
|
|
67
67
|
import { ILookupPlugin } from '../plugins/LookupPlugin';
|
|
68
|
+
import type { IOrientationPlugin } from '../plugins/OrientationPlugin';
|
|
68
69
|
import { ITimerPlugin } from '../plugins/TimerPlugin';
|
|
69
70
|
import { shouldEnableScreenDebug } from '../plugins/screenDebugFlag';
|
|
70
71
|
import { Signal } from '../signals';
|
|
@@ -210,6 +211,7 @@ export class Application extends PIXIPApplication implements IApplication {
|
|
|
210
211
|
protected _focusManager: IFocusManagerPlugin;
|
|
211
212
|
protected _popupManager: IPopupManagerPlugin;
|
|
212
213
|
protected _timerPlugin: ITimerPlugin;
|
|
214
|
+
protected _orientationPlugin: IOrientationPlugin;
|
|
213
215
|
protected _audioManager: IAudioManagerPlugin;
|
|
214
216
|
protected _voiceoverPlugin: IVoiceOverPlugin;
|
|
215
217
|
protected _captionsPlugin: ICaptionsPlugin;
|
|
@@ -560,6 +562,14 @@ export class Application extends PIXIPApplication implements IApplication {
|
|
|
560
562
|
return this._timerPlugin;
|
|
561
563
|
}
|
|
562
564
|
|
|
565
|
+
/** The "rotate your device" guard for `orientation` in caper.config.ts: `mismatched`, `onMismatchChanged`. */
|
|
566
|
+
public get orientation(): IOrientationPlugin {
|
|
567
|
+
if (!this._orientationPlugin) {
|
|
568
|
+
this._orientationPlugin = this.getPlugin<IOrientationPlugin>('orientation');
|
|
569
|
+
}
|
|
570
|
+
return this._orientationPlugin;
|
|
571
|
+
}
|
|
572
|
+
|
|
563
573
|
public get audio(): IAudioManagerPlugin {
|
|
564
574
|
if (!this._audioManager) {
|
|
565
575
|
this._audioManager = this.getPlugin<IAudioManagerPlugin>('audio');
|
|
@@ -23,6 +23,7 @@ import type { TextStyle } from '../../mixins/factory/props';
|
|
|
23
23
|
import type { CaptionsOptions } from '../../plugins/captions';
|
|
24
24
|
import { GSAPPluginOptions } from '../../plugins/GSAPPlugin';
|
|
25
25
|
import type { IDataAdapterOptions } from '../../plugins/DataAdapter';
|
|
26
|
+
import type { OrientationConfig } from '../../utils/orientation';
|
|
26
27
|
import type { PluginConfig } from '../config';
|
|
27
28
|
import { IApplication } from './IApplication';
|
|
28
29
|
|
|
@@ -49,6 +50,26 @@ export interface IApplicationOptions extends ApplicationOptions {
|
|
|
49
50
|
* @default false
|
|
50
51
|
*/
|
|
51
52
|
useWasm?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Lock the game to one orientation on every target. Unset means no lock.
|
|
55
|
+
* `'portrait'` / `'landscape'` is shorthand for `{ lock: 'portrait' }` etc.
|
|
56
|
+
*
|
|
57
|
+
* - `caper native android` sets `android:screenOrientation` on the
|
|
58
|
+
* MainActivity (`portrait`, or `sensorLandscape` for landscape).
|
|
59
|
+
* - `caper native init` opens a 450x800 desktop window for portrait
|
|
60
|
+
* (1280x720 otherwise).
|
|
61
|
+
* - The PWA web manifest's `orientation` defaults to the lock.
|
|
62
|
+
* - At runtime, on a mobile browser (never desktop, never Tauri), the
|
|
63
|
+
* `orientation` plugin shows a "rotate your device" overlay and pauses the
|
|
64
|
+
* game while the device is held the wrong way. `overlay` restyles it
|
|
65
|
+
* (`text` / `background` / `color` / `fontFamily`), adds a `className`, or
|
|
66
|
+
* replaces it (`element`); `overlay: false` turns off the overlay and its
|
|
67
|
+
* pause, and `app.orientation.onMismatchChanged` still fires.
|
|
68
|
+
*
|
|
69
|
+
* The build and the CLI read the lock with an AST parse, so write the lock
|
|
70
|
+
* (the string, or `lock` in the object) as a string literal.
|
|
71
|
+
*/
|
|
72
|
+
orientation?: OrientationConfig;
|
|
52
73
|
/**
|
|
53
74
|
* Enable the `window.Caper.automation[id]` facade for this app regardless of
|
|
54
75
|
* environment. Automation is also auto-enabled in dev or when
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const h = vi.hoisted(() => ({
|
|
4
|
+
platform: { isMobile: true, isTouch: true, isTauri: false },
|
|
5
|
+
app: null as any,
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../core', () => ({ coreFunctionRegistry: {}, coreSignalRegistry: {} }));
|
|
9
|
+
vi.mock('../core/Application', () => ({
|
|
10
|
+
Application: { getInstance: () => h.app },
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('../utils/platform', () => ({
|
|
13
|
+
get isMobile() {
|
|
14
|
+
return h.platform.isMobile;
|
|
15
|
+
},
|
|
16
|
+
get isTouch() {
|
|
17
|
+
return h.platform.isTouch;
|
|
18
|
+
},
|
|
19
|
+
get isTauri() {
|
|
20
|
+
return h.platform.isTauri;
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { Signal } from '../signals';
|
|
25
|
+
import { OrientationPlugin } from './OrientationPlugin';
|
|
26
|
+
|
|
27
|
+
/** A `matchMedia('(orientation: portrait)')` whose answer the test flips. */
|
|
28
|
+
function mockMatchMedia(portrait: boolean) {
|
|
29
|
+
const listeners = new Set<(event: { matches: boolean }) => void>();
|
|
30
|
+
const query = {
|
|
31
|
+
matches: portrait,
|
|
32
|
+
media: '(orientation: portrait)',
|
|
33
|
+
addEventListener: vi.fn((_type: string, fn: any) => listeners.add(fn)),
|
|
34
|
+
removeEventListener: vi.fn((_type: string, fn: any) => listeners.delete(fn)),
|
|
35
|
+
};
|
|
36
|
+
window.matchMedia = vi.fn(() => query) as any;
|
|
37
|
+
return {
|
|
38
|
+
query,
|
|
39
|
+
listeners,
|
|
40
|
+
rotate(toPortrait: boolean) {
|
|
41
|
+
query.matches = toPortrait;
|
|
42
|
+
for (const fn of [...listeners]) fn({ matches: toPortrait });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeApp(config: Record<string, unknown>) {
|
|
48
|
+
const app = {
|
|
49
|
+
config,
|
|
50
|
+
paused: false,
|
|
51
|
+
pause: vi.fn(() => {
|
|
52
|
+
app.paused = true;
|
|
53
|
+
}),
|
|
54
|
+
resume: vi.fn(() => {
|
|
55
|
+
app.paused = false;
|
|
56
|
+
}),
|
|
57
|
+
fullScreen: { onFullScreenChange: new Signal<(isFullscreen: boolean) => void>() },
|
|
58
|
+
};
|
|
59
|
+
return app;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const overlay = () => document.querySelector<HTMLElement>('[data-caper-orientation-overlay]');
|
|
63
|
+
|
|
64
|
+
describe('OrientationPlugin', () => {
|
|
65
|
+
let plugin: OrientationPlugin;
|
|
66
|
+
const originalMatchMedia = window.matchMedia;
|
|
67
|
+
|
|
68
|
+
async function start(config: Record<string, unknown>) {
|
|
69
|
+
h.app = makeApp(config);
|
|
70
|
+
plugin = new OrientationPlugin();
|
|
71
|
+
// The config key `orientation` is the plugin id, so its value arrives as the options.
|
|
72
|
+
plugin.initialize(config.orientation as any);
|
|
73
|
+
await plugin.postInitialize(h.app);
|
|
74
|
+
return h.app;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
h.platform = { isMobile: true, isTouch: true, isTauri: false };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
plugin?.destroy();
|
|
83
|
+
window.matchMedia = originalMatchMedia;
|
|
84
|
+
document.querySelectorAll('[data-caper-orientation-overlay]').forEach((node) => node.remove());
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('shows the overlay and pauses when the device is held the wrong way, from the start', async () => {
|
|
88
|
+
mockMatchMedia(false);
|
|
89
|
+
const app = await start({ orientation: 'portrait' });
|
|
90
|
+
|
|
91
|
+
expect(plugin.mismatched).toBe(true);
|
|
92
|
+
expect(app.pause).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(overlay()?.textContent).toBe('Rotate your device');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('hides the overlay and resumes when the device turns the right way', async () => {
|
|
97
|
+
const media = mockMatchMedia(true);
|
|
98
|
+
const app = await start({ orientation: { lock: 'landscape' } });
|
|
99
|
+
expect(plugin.orientation).toBe('landscape');
|
|
100
|
+
expect(overlay()?.textContent).toBe('Turn your device sideways');
|
|
101
|
+
const changes: boolean[] = [];
|
|
102
|
+
plugin.onMismatchChanged.connect((mismatched) => changes.push(mismatched));
|
|
103
|
+
|
|
104
|
+
media.rotate(false);
|
|
105
|
+
|
|
106
|
+
expect(plugin.mismatched).toBe(false);
|
|
107
|
+
expect(overlay()).toBeNull();
|
|
108
|
+
expect(app.resume).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(app.paused).toBe(false);
|
|
110
|
+
|
|
111
|
+
media.rotate(true);
|
|
112
|
+
expect(overlay()).not.toBeNull();
|
|
113
|
+
expect(changes).toEqual([false, true]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does nothing while the orientation matches', async () => {
|
|
117
|
+
mockMatchMedia(true);
|
|
118
|
+
const app = await start({ orientation: 'portrait' });
|
|
119
|
+
expect(plugin.active).toBe(true);
|
|
120
|
+
expect(plugin.mismatched).toBe(false);
|
|
121
|
+
expect(overlay()).toBeNull();
|
|
122
|
+
expect(app.pause).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not resume a game someone else paused', async () => {
|
|
126
|
+
const media = mockMatchMedia(true);
|
|
127
|
+
h.app = makeApp({ orientation: 'portrait' });
|
|
128
|
+
h.app.paused = true;
|
|
129
|
+
plugin = new OrientationPlugin();
|
|
130
|
+
plugin.initialize('portrait');
|
|
131
|
+
await plugin.postInitialize(h.app);
|
|
132
|
+
|
|
133
|
+
media.rotate(false);
|
|
134
|
+
expect(overlay()).not.toBeNull();
|
|
135
|
+
expect(h.app.pause).not.toHaveBeenCalled();
|
|
136
|
+
|
|
137
|
+
media.rotate(true);
|
|
138
|
+
expect(overlay()).toBeNull();
|
|
139
|
+
expect(h.app.resume).not.toHaveBeenCalled();
|
|
140
|
+
expect(h.app.paused).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('with overlay: false shows no overlay and does not pause, but still signals', async () => {
|
|
144
|
+
const media = mockMatchMedia(true);
|
|
145
|
+
const app = await start({ orientation: { lock: 'portrait', overlay: false } });
|
|
146
|
+
const changes: boolean[] = [];
|
|
147
|
+
plugin.onMismatchChanged.connect((mismatched) => changes.push(mismatched));
|
|
148
|
+
|
|
149
|
+
media.rotate(false);
|
|
150
|
+
|
|
151
|
+
expect(plugin.mismatched).toBe(true);
|
|
152
|
+
expect(changes).toEqual([true]);
|
|
153
|
+
expect(overlay()).toBeNull();
|
|
154
|
+
expect(app.pause).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('is inert when orientation is unset', async () => {
|
|
158
|
+
mockMatchMedia(false);
|
|
159
|
+
const app = await start({});
|
|
160
|
+
expect(plugin.active).toBe(false);
|
|
161
|
+
expect(window.matchMedia).not.toHaveBeenCalled();
|
|
162
|
+
expect(overlay()).toBeNull();
|
|
163
|
+
expect(app.pause).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('is inert on a desktop browser', async () => {
|
|
167
|
+
h.platform = { isMobile: false, isTouch: false, isTauri: false };
|
|
168
|
+
mockMatchMedia(false);
|
|
169
|
+
const app = await start({ orientation: 'portrait' });
|
|
170
|
+
expect(plugin.active).toBe(false);
|
|
171
|
+
expect(overlay()).toBeNull();
|
|
172
|
+
expect(app.pause).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('is inert on a touchscreen laptop, which is not a phone or tablet', async () => {
|
|
176
|
+
h.platform = { isMobile: false, isTouch: true, isTauri: false };
|
|
177
|
+
mockMatchMedia(false);
|
|
178
|
+
const app = await start({ orientation: 'portrait' });
|
|
179
|
+
expect(plugin.active).toBe(false);
|
|
180
|
+
expect(overlay()).toBeNull();
|
|
181
|
+
expect(app.pause).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('is inert inside Tauri, where the manifest locks the orientation', async () => {
|
|
185
|
+
h.platform = { isMobile: true, isTouch: true, isTauri: true };
|
|
186
|
+
mockMatchMedia(false);
|
|
187
|
+
const app = await start({ orientation: 'portrait' });
|
|
188
|
+
expect(plugin.active).toBe(false);
|
|
189
|
+
expect(overlay()).toBeNull();
|
|
190
|
+
expect(app.pause).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('applies custom text, colors, font and className to the default element', async () => {
|
|
194
|
+
mockMatchMedia(false);
|
|
195
|
+
await start({
|
|
196
|
+
orientation: {
|
|
197
|
+
lock: 'portrait',
|
|
198
|
+
overlay: { text: 'Hold it upright', background: 'rgb(10, 20, 30)', color: 'rgb(1, 2, 3)', fontFamily: 'Parlour', className: 'my-rotate' },
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const el = overlay()!;
|
|
203
|
+
expect(el.textContent).toBe('Hold it upright');
|
|
204
|
+
expect(el.style.background).toContain('rgb(10, 20, 30)');
|
|
205
|
+
expect(el.style.color).toBe('rgb(1, 2, 3)');
|
|
206
|
+
expect(el.style.fontFamily).toContain('Parlour');
|
|
207
|
+
expect(el.classList.contains('my-rotate')).toBe(true);
|
|
208
|
+
expect(el.style.position).toBe('fixed');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('uses a custom element in place of the default one', async () => {
|
|
212
|
+
mockMatchMedia(false);
|
|
213
|
+
const custom = document.createElement('section');
|
|
214
|
+
custom.textContent = 'custom';
|
|
215
|
+
const element = vi.fn(() => custom);
|
|
216
|
+
await start({ orientation: { lock: 'portrait', overlay: { element } } });
|
|
217
|
+
|
|
218
|
+
expect(custom.isConnected).toBe(true);
|
|
219
|
+
expect(custom.dataset.caperOrientationOverlay).toBeDefined();
|
|
220
|
+
expect(custom.textContent).toBe('custom');
|
|
221
|
+
expect(element).toHaveBeenCalledTimes(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('locks the screen orientation on entering fullscreen, swallowing a rejection', async () => {
|
|
225
|
+
mockMatchMedia(true);
|
|
226
|
+
const lock = vi.fn(() => Promise.reject(new Error('not supported')));
|
|
227
|
+
Object.defineProperty(screen, 'orientation', { configurable: true, value: { lock } });
|
|
228
|
+
try {
|
|
229
|
+
const app = await start({ orientation: 'landscape' });
|
|
230
|
+
app.fullScreen.onFullScreenChange.emit(true);
|
|
231
|
+
expect(lock).toHaveBeenCalledWith('landscape');
|
|
232
|
+
app.fullScreen.onFullScreenChange.emit(false);
|
|
233
|
+
expect(lock).toHaveBeenCalledTimes(1);
|
|
234
|
+
await Promise.resolve();
|
|
235
|
+
} finally {
|
|
236
|
+
delete (screen as any).orientation;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('cleans up on destroy: removes the overlay, the listener, and resumes what it paused', async () => {
|
|
241
|
+
const media = mockMatchMedia(false);
|
|
242
|
+
const app = await start({ orientation: 'portrait' });
|
|
243
|
+
expect(overlay()).not.toBeNull();
|
|
244
|
+
|
|
245
|
+
plugin.destroy();
|
|
246
|
+
|
|
247
|
+
expect(overlay()).toBeNull();
|
|
248
|
+
expect(media.listeners.size).toBe(0);
|
|
249
|
+
expect(app.resume).toHaveBeenCalledTimes(1);
|
|
250
|
+
expect(app.fullScreen.onFullScreenChange.hasConnections()).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { IApplication } from '../core/interfaces/IApplication';
|
|
2
|
+
import { Signal } from '../signals';
|
|
3
|
+
import { isMobile, isTauri } from '../utils/platform';
|
|
4
|
+
import type { OrientationConfig, OrientationLock as Orientation, OrientationOverlayOptions } from '../utils/orientation';
|
|
5
|
+
import { resolveOrientation } from '../utils/orientation';
|
|
6
|
+
import type { IPlugin } from './Plugin';
|
|
7
|
+
import { Plugin } from './Plugin';
|
|
8
|
+
|
|
9
|
+
export interface IOrientationPlugin extends IPlugin {
|
|
10
|
+
/** The lock from `caper.config.ts`'s `orientation`, or `undefined` when unset. */
|
|
11
|
+
readonly orientation: Orientation | undefined;
|
|
12
|
+
/** Whether the plugin is watching at all: orientation set, a touch/mobile device, not Tauri. */
|
|
13
|
+
readonly active: boolean;
|
|
14
|
+
/** Whether the device is currently held the wrong way. */
|
|
15
|
+
readonly mismatched: boolean;
|
|
16
|
+
/** Fires when `mismatched` changes. Still fires with `orientation: { lock, overlay: false }`. */
|
|
17
|
+
onMismatchChanged: Signal<(mismatched: boolean) => void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ORIENTATION_OVERLAY_TEXT: Readonly<Record<Orientation, string>> = Object.freeze({
|
|
21
|
+
portrait: 'Rotate your device',
|
|
22
|
+
landscape: 'Turn your device sideways',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const OVERLAY_ATTRIBUTE = 'data-caper-orientation-overlay';
|
|
26
|
+
|
|
27
|
+
type LockableOrientation = ScreenOrientation & { lock?: (orientation: string) => Promise<void> };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The web "rotate your device" guard for `orientation` in `caper.config.ts`.
|
|
31
|
+
*
|
|
32
|
+
* Only on a touch/mobile browser outside Tauri (native apps are locked by their
|
|
33
|
+
* manifests; desktop browsers never see it). While the device is held the
|
|
34
|
+
* wrong way it shows a full-screen DOM overlay and pauses the app, then hides
|
|
35
|
+
* it and resumes when the device turns back. It only resumes a pause it made
|
|
36
|
+
* itself. Entering fullscreen also asks `screen.orientation.lock` for the
|
|
37
|
+
* orientation (works on Android Chrome; a rejection is ignored).
|
|
38
|
+
*/
|
|
39
|
+
export class OrientationPlugin extends Plugin implements IOrientationPlugin {
|
|
40
|
+
public readonly id = 'orientation';
|
|
41
|
+
|
|
42
|
+
public onMismatchChanged: Signal<(mismatched: boolean) => void> = new Signal<(mismatched: boolean) => void>();
|
|
43
|
+
|
|
44
|
+
private _orientation: Orientation | undefined;
|
|
45
|
+
private _overlayOptions: OrientationOverlayOptions | false = {};
|
|
46
|
+
private _active = false;
|
|
47
|
+
private _mismatched = false;
|
|
48
|
+
/** Whether *this* plugin is the one that paused the app. */
|
|
49
|
+
private _didPause = false;
|
|
50
|
+
private _query: MediaQueryList | null = null;
|
|
51
|
+
private _overlay: HTMLElement | null = null;
|
|
52
|
+
|
|
53
|
+
public get orientation(): Orientation | undefined {
|
|
54
|
+
return this._orientation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public get active(): boolean {
|
|
58
|
+
return this._active;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public get mismatched(): boolean {
|
|
62
|
+
return this._mismatched;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `options` is `caper.config.ts`'s `orientation` itself: the config key is
|
|
67
|
+
* this plugin's id, so `loadPlugin` hands the value over as the options.
|
|
68
|
+
*/
|
|
69
|
+
public initialize(options?: OrientationConfig | unknown): void {
|
|
70
|
+
const resolved = resolveOrientation(options);
|
|
71
|
+
this._orientation = resolved?.lock;
|
|
72
|
+
this._overlayOptions = resolved?.overlay ?? {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The work happens in postInitialize: pausing reaches into the audio and timer
|
|
76
|
+
// plugins, which register after this one.
|
|
77
|
+
public postInitialize(_app?: IApplication): void {
|
|
78
|
+
if (!this._orientation) return;
|
|
79
|
+
|
|
80
|
+
if (!isMobile || isTauri) return;
|
|
81
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
82
|
+
|
|
83
|
+
this._active = true;
|
|
84
|
+
this._query = window.matchMedia('(orientation: portrait)');
|
|
85
|
+
this.listen(this._query, 'change', this._check);
|
|
86
|
+
this.addDisposer(this._teardown);
|
|
87
|
+
|
|
88
|
+
const onFullScreenChange = this.app.fullScreen?.onFullScreenChange;
|
|
89
|
+
if (onFullScreenChange) {
|
|
90
|
+
this.addSignalConnection(onFullScreenChange.connect(this._onFullScreenChange));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._check();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private _check(): void {
|
|
97
|
+
if (!this._query || !this._orientation) return;
|
|
98
|
+
const mismatched = this._query.matches !== (this._orientation === 'portrait');
|
|
99
|
+
if (mismatched === this._mismatched) return;
|
|
100
|
+
this._mismatched = mismatched;
|
|
101
|
+
|
|
102
|
+
if (this._overlayOptions !== false) {
|
|
103
|
+
if (mismatched) {
|
|
104
|
+
this._showOverlay();
|
|
105
|
+
this._pause();
|
|
106
|
+
} else {
|
|
107
|
+
this._hideOverlay();
|
|
108
|
+
this._resume();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.onMismatchChanged.emit(mismatched);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private _onFullScreenChange(isFullscreen: boolean): void {
|
|
116
|
+
if (!isFullscreen || typeof screen === 'undefined') return;
|
|
117
|
+
const screenOrientation = screen.orientation as LockableOrientation | undefined;
|
|
118
|
+
try {
|
|
119
|
+
void screenOrientation?.lock?.(this._orientation === 'landscape' ? 'landscape' : 'portrait')?.catch?.(() => {});
|
|
120
|
+
} catch {
|
|
121
|
+
// No lock on this browser (iOS); the overlay still covers it.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _showOverlay(): void {
|
|
126
|
+
if (typeof document === 'undefined') return;
|
|
127
|
+
if (!this._overlay) this._overlay = this._createOverlay();
|
|
128
|
+
if (!this._overlay.isConnected) document.body.appendChild(this._overlay);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private _hideOverlay(): void {
|
|
132
|
+
this._overlay?.remove();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private _createOverlay(): HTMLElement {
|
|
136
|
+
const options = this._overlayOptions || {};
|
|
137
|
+
let el: HTMLElement;
|
|
138
|
+
if (options.element) {
|
|
139
|
+
el = options.element();
|
|
140
|
+
} else {
|
|
141
|
+
el = document.createElement('div');
|
|
142
|
+
el.textContent = options.text ?? ORIENTATION_OVERLAY_TEXT[this._orientation!];
|
|
143
|
+
el.setAttribute('role', 'alert');
|
|
144
|
+
el.style.cssText =
|
|
145
|
+
'position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;' +
|
|
146
|
+
'padding:24px;box-sizing:border-box;text-align:center;font-size:24px;line-height:1.3;font-weight:600;' +
|
|
147
|
+
'touch-action:none;user-select:none;-webkit-user-select:none;pointer-events:auto;';
|
|
148
|
+
el.style.background = options.background ?? '#000';
|
|
149
|
+
el.style.color = options.color ?? '#fff';
|
|
150
|
+
el.style.fontFamily = options.fontFamily ?? 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
151
|
+
}
|
|
152
|
+
el.setAttribute(OVERLAY_ATTRIBUTE, '');
|
|
153
|
+
if (options.className) el.classList.add(...options.className.split(/\s+/).filter(Boolean));
|
|
154
|
+
return el;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Pause the app, but only if nothing else already did, and remember that we did. */
|
|
158
|
+
private _pause(): void {
|
|
159
|
+
if (this.app.paused) return;
|
|
160
|
+
this.app.pause();
|
|
161
|
+
this._didPause = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resume the app, but only if this plugin is the one that paused it. */
|
|
165
|
+
private _resume(): void {
|
|
166
|
+
if (!this._didPause) return;
|
|
167
|
+
this._didPause = false;
|
|
168
|
+
this.app.resume();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private _teardown(): void {
|
|
172
|
+
this._hideOverlay();
|
|
173
|
+
this._overlay = null;
|
|
174
|
+
this._resume();
|
|
175
|
+
this._query = null;
|
|
176
|
+
this._active = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/src/plugins/defaults.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AssetsPlugin } from './AssetsPlugin';
|
|
|
3
3
|
import { FullScreenPlugin } from './FullScreenPlugin';
|
|
4
4
|
import { KeyboardPlugin } from './KeyboardPlugin';
|
|
5
5
|
import { LookupPlugin } from './LookupPlugin';
|
|
6
|
+
import { OrientationPlugin } from './OrientationPlugin';
|
|
6
7
|
import type { IPlugin } from './Plugin';
|
|
7
8
|
import { PopupManagerPlugin } from './PopupManagerPlugin';
|
|
8
9
|
import { ResizerPlugin } from './ResizerPlugin';
|
|
@@ -98,4 +99,9 @@ export const defaultPlugins: ImportList<IPlugin> = [
|
|
|
98
99
|
module: TimerPlugin,
|
|
99
100
|
namedExport: 'TimerPlugin',
|
|
100
101
|
},
|
|
102
|
+
{
|
|
103
|
+
id: 'orientation',
|
|
104
|
+
module: OrientationPlugin,
|
|
105
|
+
namedExport: 'OrientationPlugin',
|
|
106
|
+
},
|
|
101
107
|
] as const;
|
package/src/plugins/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './i18nPlugin';
|
|
|
10
10
|
export * from './input';
|
|
11
11
|
export * from './KeyboardPlugin';
|
|
12
12
|
export * from './LookupPlugin';
|
|
13
|
+
export * from './OrientationPlugin';
|
|
13
14
|
export * from './Plugin';
|
|
14
15
|
export * from './PopupManagerPlugin';
|
|
15
16
|
export * from './ResizerPlugin';
|