@caperjs/core 0.2.2 → 0.2.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 +2 -0
- package/build/plugins/viewport.mjs +52 -0
- package/lib/{CaptionsPlugin-DeGyPecB.js → CaptionsPlugin-WPAcfAkx.js} +13 -12
- package/lib/CaptionsPlugin-WPAcfAkx.js.map +1 -0
- package/lib/{DataAdapter-DeI2kIxC.js → DataAdapter-DS-twowe.js} +11 -11
- package/lib/{DataAdapter-DeI2kIxC.js.map → DataAdapter-DS-twowe.js.map} +1 -1
- package/lib/{DebugRenderer-wjV0tMwL.js → DebugRenderer-D1ryrRYx.js} +2 -2
- package/lib/{DebugRenderer-wjV0tMwL.js.map → DebugRenderer-D1ryrRYx.js.map} +1 -1
- package/lib/{DevToolsPlugin-D4iWQwkO.js → DevToolsPlugin-DH7zX7HU.js} +2 -2
- package/lib/{DevToolsPlugin-D4iWQwkO.js.map → DevToolsPlugin-DH7zX7HU.js.map} +1 -1
- package/lib/{GSAPPlugin-DQTyIwcC.js → GSAPPlugin-BBtRjUqe.js} +2 -2
- package/lib/{GSAPPlugin-DQTyIwcC.js.map → GSAPPlugin-BBtRjUqe.js.map} +1 -1
- package/lib/{LayoutPlugin-vR_h-BaG.js → LayoutPlugin-Co6vcoV4.js} +4 -4
- package/lib/{LayoutPlugin-vR_h-BaG.js.map → LayoutPlugin-Co6vcoV4.js.map} +1 -1
- package/lib/{SpinePlugin-DJ-PpxFj.js → SpinePlugin-uN641Hmk.js} +2 -2
- package/lib/{SpinePlugin-DJ-PpxFj.js.map → SpinePlugin-uN641Hmk.js.map} +1 -1
- package/lib/{StatsPlugin-BYNnKYOm.js → StatsPlugin-vZQRm6wh.js} +4 -4
- package/lib/{StatsPlugin-BYNnKYOm.js.map → StatsPlugin-vZQRm6wh.js.map} +1 -1
- package/lib/{VoiceOverPlugin-BLSbq3DU.js → VoiceOverPlugin-KlZdGj1G.js} +32 -32
- package/lib/{VoiceOverPlugin-BLSbq3DU.js.map → VoiceOverPlugin-KlZdGj1G.js.map} +1 -1
- package/lib/caper.mjs +2112 -353
- package/lib/caper.mjs.map +1 -1
- package/lib/core/Application.d.ts +7 -1
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts +1 -1
- package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
- package/lib/core/interfaces/ICoreFunctions.d.ts +1 -0
- package/lib/core/interfaces/ICoreFunctions.d.ts.map +1 -1
- package/lib/display/AnimatedSprite.d.ts +1 -1
- package/lib/display/AnimatedSprite.d.ts.map +1 -1
- package/lib/display/Container.d.ts.map +1 -1
- package/lib/display/SpineAnimation.d.ts +1 -1
- package/lib/display/SpineAnimation.d.ts.map +1 -1
- package/lib/importOrder.containerFirst.test.d.ts +2 -0
- package/lib/importOrder.containerFirst.test.d.ts.map +1 -0
- package/lib/importOrder.displayFirst.test.d.ts +2 -0
- package/lib/importOrder.displayFirst.test.d.ts.map +1 -0
- package/lib/importOrder.mixinsFirst.test.d.ts +2 -0
- package/lib/importOrder.mixinsFirst.test.d.ts.map +1 -0
- package/lib/importOrder.sceneFirst.test.d.ts +2 -0
- package/lib/importOrder.sceneFirst.test.d.ts.map +1 -0
- package/lib/importOrder.toastFirst.test.d.ts +2 -0
- package/lib/importOrder.toastFirst.test.d.ts.map +1 -0
- package/lib/importOrder.uiFirst.test.d.ts +2 -0
- package/lib/importOrder.uiFirst.test.d.ts.map +1 -0
- package/lib/mixins/factory/Factory.d.ts +1 -1
- package/lib/mixins/factory/Factory.d.ts.map +1 -1
- package/lib/mixins/factory/Factory.test.d.ts +1 -0
- package/lib/mixins/factory/Factory.test.d.ts.map +1 -0
- package/lib/mixins/factory/const.d.ts +9 -2
- package/lib/mixins/factory/const.d.ts.map +1 -1
- package/lib/mixins/factory/defaults.d.ts +5 -0
- package/lib/mixins/factory/defaults.d.ts.map +1 -0
- package/lib/mixins/factory/methods.d.ts +1 -1
- package/lib/mixins/factory/methods.d.ts.map +1 -1
- package/lib/mixins/factory/props.d.ts +4 -2
- package/lib/mixins/factory/props.d.ts.map +1 -1
- package/lib/plugins/PopupManagerPlugin.d.ts +1 -1
- package/lib/plugins/PopupManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/ResizerPlugin.d.ts +29 -0
- package/lib/plugins/ResizerPlugin.d.ts.map +1 -1
- package/lib/plugins/ResizerPlugin.test.d.ts +2 -0
- package/lib/plugins/ResizerPlugin.test.d.ts.map +1 -0
- package/lib/plugins/WebEventsPlugin.d.ts +5 -0
- package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
- package/lib/plugins/WebEventsPlugin.test.d.ts +2 -0
- package/lib/plugins/WebEventsPlugin.test.d.ts.map +1 -0
- package/lib/plugins/captions/CaptionsRenderer.d.ts +1 -1
- package/lib/plugins/captions/CaptionsRenderer.d.ts.map +1 -1
- package/lib/plugins/i18nPlugin.d.ts +29 -3
- package/lib/plugins/i18nPlugin.d.ts.map +1 -1
- package/lib/plugins/i18nPlugin.test.d.ts +2 -0
- package/lib/plugins/i18nPlugin.test.d.ts.map +1 -0
- package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
- package/lib/plugins/spine/pixi-spine/require-shim.d.ts +0 -1
- package/lib/plugins/spine/pixi-spine/require-shim.d.ts.map +1 -1
- package/lib/{registries-aN0y0Xha.js → registries-BLrxALZ0.js} +1124 -2801
- package/lib/registries-BLrxALZ0.js.map +1 -0
- package/lib/ui/Button.d.ts +9 -2
- package/lib/ui/Button.d.ts.map +1 -1
- package/lib/ui/Button.test.d.ts +2 -0
- package/lib/ui/Button.test.d.ts.map +1 -0
- package/lib/ui/FlexContainer.d.ts +1 -1
- package/lib/ui/FlexContainer.d.ts.map +1 -1
- package/lib/ui/Input.d.ts +2 -2
- package/lib/ui/Input.d.ts.map +1 -1
- package/lib/ui/Joystick.d.ts +1 -1
- package/lib/ui/Joystick.d.ts.map +1 -1
- package/lib/ui/Toast.d.ts +1 -1
- package/lib/ui/Toast.d.ts.map +1 -1
- package/lib/ui/UICanvas.d.ts +11 -1
- package/lib/ui/UICanvas.d.ts.map +1 -1
- package/lib/ui/UICanvas.test.d.ts +2 -0
- package/lib/ui/UICanvas.test.d.ts.map +1 -0
- package/lib/utils/types.d.ts +1 -2
- package/lib/utils/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/Application.ts +15 -2
- package/src/core/interfaces/IApplicationOptions.ts +1 -1
- package/src/core/interfaces/ICoreFunctions.ts +1 -0
- package/src/display/AnimatedSprite.ts +2 -1
- package/src/display/Container.ts +3 -1
- package/src/display/SpineAnimation.ts +3 -2
- package/src/importOrder.containerFirst.test.ts +32 -0
- package/src/importOrder.displayFirst.test.ts +19 -0
- package/src/importOrder.mixinsFirst.test.ts +18 -0
- package/src/importOrder.sceneFirst.test.ts +21 -0
- package/src/importOrder.toastFirst.test.ts +20 -0
- package/src/importOrder.uiFirst.test.ts +24 -0
- package/src/mixins/factory/Factory.test.ts +22 -0
- package/src/mixins/factory/Factory.ts +9 -2
- package/src/mixins/factory/const.ts +18 -11
- package/src/mixins/factory/defaults.ts +28 -0
- package/src/mixins/factory/methods.ts +1 -1
- package/src/mixins/factory/props.ts +5 -3
- package/src/plugins/LookupPlugin.ts +1 -1
- package/src/plugins/PopupManagerPlugin.ts +1 -1
- package/src/plugins/ResizerPlugin.test.ts +84 -0
- package/src/plugins/ResizerPlugin.ts +59 -1
- package/src/plugins/WebEventsPlugin.test.ts +75 -0
- package/src/plugins/WebEventsPlugin.ts +15 -0
- package/src/plugins/captions/CaptionsRenderer.ts +1 -1
- package/src/plugins/i18nPlugin.test.ts +124 -0
- package/src/plugins/i18nPlugin.ts +57 -18
- package/src/plugins/input/keyboard/KeyboardContols.ts +1 -1
- package/src/plugins/input/touch/VirtualControls.ts +2 -2
- package/src/plugins/spine/pixi-spine/require-shim.ts +3 -1
- package/src/ui/Button.test.ts +236 -0
- package/src/ui/Button.ts +46 -11
- package/src/ui/FlexContainer.ts +3 -2
- package/src/ui/Input.ts +5 -2
- package/src/ui/Joystick.ts +1 -1
- package/src/ui/Toast.ts +2 -1
- package/src/ui/Toaster.ts +1 -1
- package/src/ui/UICanvas.test.ts +35 -0
- package/src/ui/UICanvas.ts +46 -8
- package/src/utils/types.ts +1 -1
- package/src/version.ts +1 -1
- package/lib/CaptionsPlugin-DeGyPecB.js.map +0 -1
- package/lib/registries-aN0y0Xha.js.map +0 -1
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { BitmapText, Graphics, HTMLText, Sprite, Text, TilingSprite } from 'pixi.js';
|
|
2
2
|
|
|
3
|
-
import { AnimatedSprite
|
|
3
|
+
import { AnimatedSprite } from '../../display/AnimatedSprite';
|
|
4
|
+
import { Container, ContainerConfigKeys } from '../../display/Container';
|
|
4
5
|
import { ParticleContainer, ParticleContainerConfigKeys } from '../../display/ParticleContainer';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '../../ui';
|
|
14
|
-
import type {
|
|
6
|
+
import { SpineAnimation } from '../../display/SpineAnimation';
|
|
7
|
+
import { Svg } from '../../display/Svg';
|
|
8
|
+
import type { ButtonConfig } from '../../ui/Button';
|
|
9
|
+
import { Button, ButtonConfigKeys } from '../../ui/Button';
|
|
10
|
+
import type { FlexContainerConfig } from '../../ui/FlexContainer';
|
|
11
|
+
import { FlexContainer, FlexContainerConfigKeys } from '../../ui/FlexContainer';
|
|
12
|
+
import type { ToastConfig } from '../../ui/Toast';
|
|
13
|
+
import type { ToasterConfig } from '../../ui/Toaster';
|
|
14
|
+
import { Toaster } from '../../ui/Toaster';
|
|
15
|
+
import type { UICanvasProps } from '../../ui/UICanvas';
|
|
16
|
+
import { UICanvas, UICanvasConfigKeys } from '../../ui/UICanvas';
|
|
15
17
|
import type { EntityId, EntityInstance, EntityProps, UIId, UIInstance, UIProps } from '../../utils';
|
|
16
18
|
import { pluck, resolvePointLike, Spine, WithRequiredProps } from '../../utils';
|
|
17
19
|
|
|
20
|
+
import { setDefaultFactoryMethods } from './defaults';
|
|
18
21
|
import type { EntityFactoryProps } from './props';
|
|
19
22
|
import {
|
|
20
23
|
AnimatedSpriteProps,
|
|
@@ -361,3 +364,7 @@ export const defaultFactoryMethods = {
|
|
|
361
364
|
return instance;
|
|
362
365
|
},
|
|
363
366
|
};
|
|
367
|
+
|
|
368
|
+
// Hand the table to the import-free registration slot the moment it exists, so
|
|
369
|
+
// `Factory()` can read it without importing this module. See ./defaults.ts.
|
|
370
|
+
setDefaultFactoryMethods(defaultFactoryMethods);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration slot for the factory method table.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately has ZERO imports. `mixins/factory/const` builds the
|
|
5
|
+
* table (and to do so imports every ui/display class), while `Factory()` — which
|
|
6
|
+
* every one of those classes extends — needs the table at construct time. A
|
|
7
|
+
* direct import either way closes the cycle and hands out an undefined binding
|
|
8
|
+
* to whichever module is mid-evaluation. Routing through this import-free slot
|
|
9
|
+
* breaks the edge: `const.ts` pushes the table in when it evaluates, `Factory`
|
|
10
|
+
* pulls it out lazily, inside the constructor.
|
|
11
|
+
*/
|
|
12
|
+
let defaults: Record<string, any> | null = null;
|
|
13
|
+
|
|
14
|
+
/** Called by `mixins/factory/const` as soon as the table is defined. */
|
|
15
|
+
export function setDefaultFactoryMethods(methods: Record<string, any>): void {
|
|
16
|
+
defaults = methods;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Read the registered table. Throws if `const.ts` has not evaluated yet. */
|
|
20
|
+
export function getDefaultFactoryMethods(): Record<string, any> {
|
|
21
|
+
if (!defaults) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"[caper] The factory method table hasn't been initialized. Import '@caperjs/core' (or its factory module) " +
|
|
24
|
+
'before deep-importing display/ui classes so the table is registered first.',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return defaults;
|
|
28
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { LayoutOptions } from '@pixi/layout';
|
|
2
2
|
import { GraphicsContext, HTMLTextStyleOptions, TextStyleOptions } from 'pixi.js';
|
|
3
|
-
import { ContainerConfig } from '../../display';
|
|
4
|
-
import { ParticleContainerConfig } from '../../display/ParticleContainer';
|
|
5
|
-
import { ButtonConfig
|
|
3
|
+
import type { ContainerConfig } from '../../display/Container';
|
|
4
|
+
import type { ParticleContainerConfig } from '../../display/ParticleContainer';
|
|
5
|
+
import type { ButtonConfig } from '../../ui/Button';
|
|
6
|
+
import type { FlexContainerConfig } from '../../ui/FlexContainer';
|
|
7
|
+
import type { UICanvasProps } from '../../ui/UICanvas';
|
|
6
8
|
import type {
|
|
7
9
|
BitmapFontFamilyAsset,
|
|
8
10
|
FontFamilyAsset,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Container as PIXIContainer } from 'pixi.js';
|
|
2
2
|
import { IPlugin, Plugin } from '.';
|
|
3
|
-
import { Container } from '../display';
|
|
3
|
+
import { Container } from '../display/Container';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Interface for the LookupPlugin providing fast container lookup functionality.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// ResizerPlugin transitively imports Application → Pixi display graph. Stub it.
|
|
4
|
+
vi.mock('../core', () => ({
|
|
5
|
+
coreFunctionRegistry: {},
|
|
6
|
+
coreSignalRegistry: {},
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
app: {
|
|
11
|
+
config: { resizeToContainer: false } as { resizeToContainer: boolean },
|
|
12
|
+
renderer: {
|
|
13
|
+
canvas: { style: {} as Record<string, string>, parentElement: null as unknown },
|
|
14
|
+
resize: (_w: number, _h: number) => {},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../core/Application', () => ({
|
|
20
|
+
Application: { getInstance: () => mocks.app },
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { ResizerPlugin } from './ResizerPlugin';
|
|
24
|
+
|
|
25
|
+
/** Shrinks the container below minWidth/minHeight so `_resize()` computes scale > 1. */
|
|
26
|
+
function useContainer(width: number, height: number) {
|
|
27
|
+
mocks.app.config.resizeToContainer = true;
|
|
28
|
+
mocks.app.renderer.canvas.parentElement = {
|
|
29
|
+
getBoundingClientRect: () => ({ width, height }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('ResizerPlugin safe area', () => {
|
|
34
|
+
let plugin: ResizerPlugin;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
mocks.app.config.resizeToContainer = false;
|
|
38
|
+
mocks.app.renderer.canvas = { style: {}, parentElement: null };
|
|
39
|
+
plugin = new ResizerPlugin();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports zero insets before the first resize', () => {
|
|
43
|
+
expect(plugin.safeArea).toEqual({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('scales measured insets into logical render units', async () => {
|
|
47
|
+
await plugin.initialize({ minWidth: 1000, minHeight: 1000 });
|
|
48
|
+
useContainer(500, 500);
|
|
49
|
+
plugin._measureSafeAreaCssPx = () => ({ top: 44, right: 0, bottom: 34, left: 12 });
|
|
50
|
+
|
|
51
|
+
plugin._resize();
|
|
52
|
+
|
|
53
|
+
expect(plugin.scale).toBe(2);
|
|
54
|
+
expect(plugin.safeArea).toEqual({ top: 88, right: 0, bottom: 68, left: 24 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('passes measured insets through unscaled at scale 1', async () => {
|
|
58
|
+
await plugin.initialize({ minWidth: 500, minHeight: 500 });
|
|
59
|
+
plugin._measureSafeAreaCssPx = () => ({ top: 44, right: 0, bottom: 34, left: 12 });
|
|
60
|
+
|
|
61
|
+
plugin._resize();
|
|
62
|
+
|
|
63
|
+
expect(plugin.scale).toBe(1);
|
|
64
|
+
expect(plugin.safeArea).toEqual({ top: 44, right: 0, bottom: 34, left: 12 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('reports zero insets when useSafeArea is off', async () => {
|
|
68
|
+
await plugin.initialize({ minWidth: 500, minHeight: 500, useSafeArea: false });
|
|
69
|
+
plugin._measureSafeAreaCssPx = () => ({ top: 44, right: 0, bottom: 34, left: 12 });
|
|
70
|
+
|
|
71
|
+
plugin._resize();
|
|
72
|
+
|
|
73
|
+
expect(plugin.safeArea).toEqual({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('measures 0 when the browser does not support env()', async () => {
|
|
77
|
+
await plugin.initialize({ minWidth: 500, minHeight: 500 });
|
|
78
|
+
|
|
79
|
+
plugin._resize();
|
|
80
|
+
|
|
81
|
+
expect(plugin.safeArea).toEqual({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
82
|
+
plugin.destroy();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Graphics } from 'pixi.js';
|
|
2
2
|
|
|
3
|
-
import { Container } from '../display';
|
|
3
|
+
import { Container } from '../display/Container';
|
|
4
4
|
import type { Size } from '../utils';
|
|
5
5
|
import type { IPlugin } from './Plugin';
|
|
6
6
|
import { Plugin } from './Plugin';
|
|
@@ -11,6 +11,8 @@ import { Plugin } from './Plugin';
|
|
|
11
11
|
export interface IResizerPlugin extends IPlugin {
|
|
12
12
|
readonly size: Size;
|
|
13
13
|
readonly scale: number;
|
|
14
|
+
/** Device safe-area insets, in logical render units (already scaled like `size`). */
|
|
15
|
+
readonly safeArea: { top: number; right: number; bottom: number; left: number };
|
|
14
16
|
resize(): Promise<Size>;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -30,6 +32,8 @@ export type ResizerPluginOptions = {
|
|
|
30
32
|
center: boolean;
|
|
31
33
|
/** Whether to draw debug information for visualizing canvas bounds */
|
|
32
34
|
debug: boolean;
|
|
35
|
+
/** Whether to measure the device safe-area insets (notch / status bar / gesture bar) */
|
|
36
|
+
useSafeArea: boolean;
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -42,6 +46,7 @@ const defaultOptions: ResizerPluginOptions = {
|
|
|
42
46
|
letterbox: false,
|
|
43
47
|
center: false,
|
|
44
48
|
debug: false,
|
|
49
|
+
useSafeArea: true,
|
|
45
50
|
};
|
|
46
51
|
|
|
47
52
|
export class ResizerPlugin extends Plugin<ResizerPluginOptions> implements IResizerPlugin {
|
|
@@ -51,6 +56,8 @@ export class ResizerPlugin extends Plugin<ResizerPluginOptions> implements IResi
|
|
|
51
56
|
private _size: Size;
|
|
52
57
|
private _scale: number;
|
|
53
58
|
private _resizeId: number | null;
|
|
59
|
+
private _safeArea = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
60
|
+
private _safeAreaProbe: HTMLDivElement | null = null;
|
|
54
61
|
|
|
55
62
|
get size(): Size {
|
|
56
63
|
return this._size;
|
|
@@ -60,6 +67,10 @@ export class ResizerPlugin extends Plugin<ResizerPluginOptions> implements IResi
|
|
|
60
67
|
return this._scale;
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
get safeArea(): { top: number; right: number; bottom: number; left: number } {
|
|
71
|
+
return this._safeArea;
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
/**
|
|
64
75
|
* Initializes the Resizer module.
|
|
65
76
|
*/
|
|
@@ -75,6 +86,39 @@ export class ResizerPlugin extends Plugin<ResizerPluginOptions> implements IResi
|
|
|
75
86
|
this.resize();
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
public destroy(): void {
|
|
90
|
+
if (this._safeAreaProbe) {
|
|
91
|
+
this._safeAreaProbe.remove();
|
|
92
|
+
this._safeAreaProbe = null;
|
|
93
|
+
}
|
|
94
|
+
super.destroy();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Measures the device safe-area insets in CSS pixels, via a hidden probe element
|
|
99
|
+
* padded with `env(safe-area-inset-*)`. The probe is created once and reused.
|
|
100
|
+
* Browsers without env() support (and happy-dom) report empty strings, which fall back to 0.
|
|
101
|
+
*/
|
|
102
|
+
_measureSafeAreaCssPx(): { top: number; right: number; bottom: number; left: number } {
|
|
103
|
+
if (!this._safeAreaProbe) {
|
|
104
|
+
const probe = document.createElement('div');
|
|
105
|
+
probe.style.cssText =
|
|
106
|
+
'position:fixed; top:0; left:0; width:0; height:0; visibility:hidden; pointer-events:none;' +
|
|
107
|
+
'padding-top:env(safe-area-inset-top, 0px); padding-right:env(safe-area-inset-right, 0px);' +
|
|
108
|
+
'padding-bottom:env(safe-area-inset-bottom, 0px); padding-left:env(safe-area-inset-left, 0px);';
|
|
109
|
+
document.body.appendChild(probe);
|
|
110
|
+
this._safeAreaProbe = probe;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const style = getComputedStyle(this._safeAreaProbe);
|
|
114
|
+
return {
|
|
115
|
+
top: parseFloat(style.paddingTop) || 0,
|
|
116
|
+
right: parseFloat(style.paddingRight) || 0,
|
|
117
|
+
bottom: parseFloat(style.paddingBottom) || 0,
|
|
118
|
+
left: parseFloat(style.paddingLeft) || 0,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
async resize(): Promise<Size> {
|
|
79
123
|
this._cancelResize!();
|
|
80
124
|
return new Promise((resolve) => {
|
|
@@ -151,6 +195,20 @@ export class ResizerPlugin extends Plugin<ResizerPluginOptions> implements IResi
|
|
|
151
195
|
|
|
152
196
|
this._scale = scale;
|
|
153
197
|
|
|
198
|
+
// Safe-area insets are measured in CSS pixels; the renderer's logical size is
|
|
199
|
+
// CSS pixels * scale, so the insets have to be scaled the same way.
|
|
200
|
+
if (this._options.useSafeArea) {
|
|
201
|
+
const insets = this._measureSafeAreaCssPx();
|
|
202
|
+
this._safeArea = {
|
|
203
|
+
top: insets.top * scale,
|
|
204
|
+
right: insets.right * scale,
|
|
205
|
+
bottom: insets.bottom * scale,
|
|
206
|
+
left: insets.left * scale,
|
|
207
|
+
};
|
|
208
|
+
} else {
|
|
209
|
+
this._safeArea = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
210
|
+
}
|
|
211
|
+
|
|
154
212
|
// Update canvas style dimensions and scroll window up to avoid issues on mobile resize
|
|
155
213
|
if (letterbox) {
|
|
156
214
|
if (canvasWidth > canvasHeight) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// WebEventsPlugin transitively imports Application → Pixi display graph. Stub it.
|
|
4
|
+
vi.mock('../core', () => ({
|
|
5
|
+
coreFunctionRegistry: {},
|
|
6
|
+
coreSignalRegistry: {},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../core/Application', () => ({
|
|
9
|
+
Application: { getInstance: () => ({ renderer: { canvas: null } }) },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { WebEventsPlugin } from './WebEventsPlugin';
|
|
13
|
+
|
|
14
|
+
type FakeVisualViewport = EventTarget & { scale: number };
|
|
15
|
+
|
|
16
|
+
function installFakeVisualViewport(): FakeVisualViewport {
|
|
17
|
+
const vv = new EventTarget() as FakeVisualViewport;
|
|
18
|
+
vv.scale = 1;
|
|
19
|
+
(window as any).visualViewport = vv;
|
|
20
|
+
return vv;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('WebEventsPlugin visualViewport resize', () => {
|
|
24
|
+
let plugin: WebEventsPlugin;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
delete (window as any).visualViewport;
|
|
28
|
+
plugin = new WebEventsPlugin();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
plugin.destroy();
|
|
33
|
+
delete (window as any).visualViewport;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('emits onResize when the visual viewport resizes at scale 1', () => {
|
|
37
|
+
const vv = installFakeVisualViewport();
|
|
38
|
+
plugin.initialize();
|
|
39
|
+
const spy = vi.fn();
|
|
40
|
+
plugin.onResize.connect(spy);
|
|
41
|
+
|
|
42
|
+
vv.dispatchEvent(new Event('resize'));
|
|
43
|
+
|
|
44
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(spy).toHaveBeenCalledWith({ width: window.innerWidth, height: window.innerHeight });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('ignores visual viewport resizes during pinch-zoom (scale !== 1)', () => {
|
|
49
|
+
const vv = installFakeVisualViewport();
|
|
50
|
+
plugin.initialize();
|
|
51
|
+
const spy = vi.fn();
|
|
52
|
+
plugin.onResize.connect(spy);
|
|
53
|
+
|
|
54
|
+
vv.scale = 2;
|
|
55
|
+
vv.dispatchEvent(new Event('resize'));
|
|
56
|
+
|
|
57
|
+
expect(spy).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('stops listening after destroy', () => {
|
|
61
|
+
const vv = installFakeVisualViewport();
|
|
62
|
+
plugin.initialize();
|
|
63
|
+
const spy = vi.fn();
|
|
64
|
+
plugin.onResize.connect(spy);
|
|
65
|
+
|
|
66
|
+
plugin.destroy();
|
|
67
|
+
vv.dispatchEvent(new Event('resize'));
|
|
68
|
+
|
|
69
|
+
expect(spy).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('initializes without error when visualViewport is unavailable', () => {
|
|
73
|
+
expect(() => plugin.initialize()).not.toThrow();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -50,6 +50,9 @@ export class WebEventsPlugin extends Plugin implements IWebEventsPlugin {
|
|
|
50
50
|
window.addEventListener('resize', this._onResize);
|
|
51
51
|
document.addEventListener('fullscreenchange', this._onResize);
|
|
52
52
|
window.addEventListener('orientationchange', this._onOrientationChanged);
|
|
53
|
+
// installed PWAs (standalone/fullscreen display modes) can miss or mistime window.resize
|
|
54
|
+
// during launch transitions; visualViewport reports the settled size
|
|
55
|
+
window.visualViewport?.addEventListener('resize', this._onVisualViewportResize);
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
public destroy() {
|
|
@@ -58,6 +61,7 @@ export class WebEventsPlugin extends Plugin implements IWebEventsPlugin {
|
|
|
58
61
|
document.removeEventListener('fullscreenchange', this._onResize);
|
|
59
62
|
window.removeEventListener('pagehide', this._onPageHide, false);
|
|
60
63
|
window.removeEventListener('pageshow', this._onPageShow, false);
|
|
64
|
+
window.visualViewport?.removeEventListener('resize', this._onVisualViewportResize);
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
protected getCoreSignals(): string[] {
|
|
@@ -85,6 +89,17 @@ export class WebEventsPlugin extends Plugin implements IWebEventsPlugin {
|
|
|
85
89
|
this.onResize.emit({ width: screenWidth, height: screenHeight });
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Called when the visual viewport resizes. Ignores pinch-zoom (scale !== 1),
|
|
94
|
+
* where the layout size hasn't actually changed.
|
|
95
|
+
*/
|
|
96
|
+
private _onVisualViewportResize(): void {
|
|
97
|
+
if ((window.visualViewport?.scale ?? 1) !== 1) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this._onResize();
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* Called when the page is hidden.
|
|
90
105
|
* Some browsers (like Safari) don't support the `visibilitychange` event, so we also listen for `pagehide`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { gsap } from 'gsap';
|
|
2
2
|
import { BitmapText, Container as PIXIContainer, Sprite, Texture } from 'pixi.js';
|
|
3
|
-
import { Container } from '../../display';
|
|
3
|
+
import { Container } from '../../display/Container';
|
|
4
4
|
import { type Size } from '../../utils';
|
|
5
5
|
import { CaptionsPlugin } from './CaptionsPlugin';
|
|
6
6
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Plugin.ts transitively imports Application → Pixi display graph. Stub it.
|
|
4
|
+
vi.mock('../core', () => ({
|
|
5
|
+
coreFunctionRegistry: {},
|
|
6
|
+
coreSignalRegistry: {},
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../core/Application', () => ({
|
|
9
|
+
Application: { getInstance: () => ({}) },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { Logger } from '../utils';
|
|
13
|
+
import type { i18nDict } from './i18nPlugin';
|
|
14
|
+
import { i18nPlugin } from './i18nPlugin';
|
|
15
|
+
|
|
16
|
+
/** Build an initialized plugin holding `dict` for the `en` locale. */
|
|
17
|
+
async function setup(dict: i18nDict) {
|
|
18
|
+
const plugin = new i18nPlugin();
|
|
19
|
+
await plugin.initialize({ defaultLocale: 'en', locales: ['en'] });
|
|
20
|
+
// `_dicts` is private and normally filled by loadLocale(); inject directly.
|
|
21
|
+
(plugin as unknown as { _dicts: Record<string, i18nDict> })._dicts = { en: dict };
|
|
22
|
+
return plugin;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Silence the expected Logger.error calls for missing keys. */
|
|
26
|
+
function muteErrors() {
|
|
27
|
+
return vi.spyOn(Logger, 'error').mockImplementation(() => undefined);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('i18nPlugin.t', () => {
|
|
35
|
+
it('returns the key itself when the key is missing', async () => {
|
|
36
|
+
const error = muteErrors();
|
|
37
|
+
const plugin = await setup({ hello: 'Hello' });
|
|
38
|
+
expect(plugin.t('nope.missing')).toBe('nope.missing');
|
|
39
|
+
expect(error).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns the key itself when no dictionary is loaded', async () => {
|
|
43
|
+
const error = muteErrors();
|
|
44
|
+
const plugin = await setup({ hello: 'Hello' });
|
|
45
|
+
expect(plugin.t('hello', undefined, 'fr')).toBe('hello');
|
|
46
|
+
expect(error).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('resolves variants to index 0 with no params', async () => {
|
|
50
|
+
const plugin = await setup({ greet: '[Hi|Hello|Yo] there' });
|
|
51
|
+
expect(plugin.t('greet')).toBe('Hi there');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('never leaves brackets in the output', async () => {
|
|
55
|
+
const plugin = await setup({ greet: '[Hi|Hello] [friend|pal]' });
|
|
56
|
+
expect(plugin.t('greet')).not.toMatch(/[[\]]/);
|
|
57
|
+
expect(plugin.t('greet', { variant: 'random' })).not.toMatch(/[[\]]/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('applies a numeric variant to every group', async () => {
|
|
61
|
+
const plugin = await setup({ greet: '[Hi|Hello] [friend|pal]' });
|
|
62
|
+
expect(plugin.t('greet', { variant: 1 })).toBe('Hello pal');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('clamps a numeric variant to each group last item', async () => {
|
|
66
|
+
const plugin = await setup({ greet: '[Hi|Hello|Yo] [friend|pal]' });
|
|
67
|
+
expect(plugin.t('greet', { variant: 5 })).toBe('Yo pal');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('picks each random variant independently', async () => {
|
|
71
|
+
const plugin = await setup({ greet: '[a|b] [a|b]' });
|
|
72
|
+
const random = vi.spyOn(Math, 'random').mockReturnValueOnce(0).mockReturnValueOnce(0.9);
|
|
73
|
+
expect(plugin.t('greet', { variant: 'random' })).toBe('a b');
|
|
74
|
+
expect(random).toHaveBeenCalledTimes(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('interpolates placeholders after variant resolution', async () => {
|
|
78
|
+
const plugin = await setup({ greet: '[Hi|Hello] {name}' });
|
|
79
|
+
expect(plugin.t('greet', { variant: 1, name: 'Ada' })).toBe('Hello Ada');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('i18nPlugin.tCount', () => {
|
|
84
|
+
const dict: i18nDict = {
|
|
85
|
+
apples: {
|
|
86
|
+
one: '{count} apple',
|
|
87
|
+
other: '{count} apples',
|
|
88
|
+
},
|
|
89
|
+
fish: {
|
|
90
|
+
other: '{count} fish',
|
|
91
|
+
},
|
|
92
|
+
guests: {
|
|
93
|
+
one: '[Just one|One] guest, {host}',
|
|
94
|
+
other: '{count} guests, {host}',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
it('resolves the one category for en', async () => {
|
|
99
|
+
const plugin = await setup(dict);
|
|
100
|
+
expect(plugin.tCount('apples', 1)).toBe('1 apple');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('resolves the other category for en', async () => {
|
|
104
|
+
const plugin = await setup(dict);
|
|
105
|
+
expect(plugin.tCount('apples', 2)).toBe('2 apples');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('falls back to .other when the category leaf is missing', async () => {
|
|
109
|
+
const plugin = await setup(dict);
|
|
110
|
+
expect(plugin.tCount('fish', 1)).toBe('1 fish');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('passes extra params through and still resolves variants', async () => {
|
|
114
|
+
const plugin = await setup(dict);
|
|
115
|
+
expect(plugin.tCount('guests', 1, { host: 'Ada' })).toBe('Just one guest, Ada');
|
|
116
|
+
expect(plugin.tCount('guests', 3, { host: 'Ada' })).toBe('3 guests, Ada');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('returns the .other key when nothing resolves', async () => {
|
|
120
|
+
muteErrors();
|
|
121
|
+
const plugin = await setup(dict);
|
|
122
|
+
expect(plugin.tCount('pears', 2)).toBe('pears.other');
|
|
123
|
+
});
|
|
124
|
+
});
|