@caperjs/core 0.2.1 → 0.2.3
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 +18 -1
- package/build/plugins/pwa.mjs +114 -8
- package/build/plugins/viewport.mjs +52 -0
- package/lib/{CaptionsPlugin-DKhbmJvH.js → CaptionsPlugin-WPAcfAkx.js} +13 -12
- package/lib/CaptionsPlugin-WPAcfAkx.js.map +1 -0
- package/lib/{DataAdapter-BZFTkFQ4.js → DataAdapter-DS-twowe.js} +11 -11
- package/lib/{DataAdapter-BZFTkFQ4.js.map → DataAdapter-DS-twowe.js.map} +1 -1
- package/lib/{DebugRenderer-C7X-nlXE.js → DebugRenderer-D1ryrRYx.js} +2 -2
- package/lib/{DebugRenderer-C7X-nlXE.js.map → DebugRenderer-D1ryrRYx.js.map} +1 -1
- package/lib/{DevToolsPlugin-P-atJ-5U.js → DevToolsPlugin-DH7zX7HU.js} +2 -2
- package/lib/{DevToolsPlugin-P-atJ-5U.js.map → DevToolsPlugin-DH7zX7HU.js.map} +1 -1
- package/lib/{GSAPPlugin-CeH9275z.js → GSAPPlugin-BBtRjUqe.js} +2 -2
- package/lib/{GSAPPlugin-CeH9275z.js.map → GSAPPlugin-BBtRjUqe.js.map} +1 -1
- package/lib/{LayoutPlugin-iZ3k6RsK.js → LayoutPlugin-Co6vcoV4.js} +4 -4
- package/lib/{LayoutPlugin-iZ3k6RsK.js.map → LayoutPlugin-Co6vcoV4.js.map} +1 -1
- package/lib/{SpinePlugin-BWiavCpO.js → SpinePlugin-uN641Hmk.js} +2 -2
- package/lib/{SpinePlugin-BWiavCpO.js.map → SpinePlugin-uN641Hmk.js.map} +1 -1
- package/lib/{StatsPlugin-CBpzDJL0.js → StatsPlugin-vZQRm6wh.js} +4 -4
- package/lib/{StatsPlugin-CBpzDJL0.js.map → StatsPlugin-vZQRm6wh.js.map} +1 -1
- package/lib/{VoiceOverPlugin-YaiKJtJ6.js → VoiceOverPlugin-KlZdGj1G.js} +32 -32
- package/lib/{VoiceOverPlugin-YaiKJtJ6.js.map → VoiceOverPlugin-KlZdGj1G.js.map} +1 -1
- package/lib/caper.mjs +2107 -353
- package/lib/caper.mjs.map +1 -1
- package/lib/core/Application.d.ts +37 -2
- package/lib/core/Application.d.ts.map +1 -1
- package/lib/core/create.d.ts +28 -1
- package/lib/core/create.d.ts.map +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/index.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-qQmLeN0b.js → registries-BLrxALZ0.js} +1138 -2800
- package/lib/registries-BLrxALZ0.js.map +1 -0
- package/lib/ui/Button.d.ts +1 -1
- package/lib/ui/Button.d.ts.map +1 -1
- 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 +66 -2
- package/src/core/create.ts +30 -1
- package/src/core/index.ts +1 -1
- 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.ts +5 -1
- 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-DKhbmJvH.js.map +0 -1
- package/lib/registries-qQmLeN0b.js.map +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Entry order guard: a `ui` module is imported BEFORE anything from `mixins`.
|
|
4
|
+
// The factory method table (`mixins/factory/const`) references every ui/display
|
|
5
|
+
// class, and every ui/display class extends a mixin — so the two sides import
|
|
6
|
+
// each other. If either side goes back through a barrel, whichever module is
|
|
7
|
+
// mid-evaluation hands out an undefined binding and the class body throws
|
|
8
|
+
// ("Factory is not a function" / "Class extends value undefined").
|
|
9
|
+
// Vitest gives each test file a fresh module registry, so this file pins the
|
|
10
|
+
// ui-first order and importOrder.mixinsFirst.test.ts pins the other one.
|
|
11
|
+
import { UICanvas } from './ui/UICanvas';
|
|
12
|
+
import { Button } from './ui/Button';
|
|
13
|
+
import { Factory } from './mixins';
|
|
14
|
+
|
|
15
|
+
describe('import order: ui first', () => {
|
|
16
|
+
it('resolves the mixins the ui classes are built from', () => {
|
|
17
|
+
expect(typeof Factory).toBe('function');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('constructs the ui classes', () => {
|
|
21
|
+
expect(typeof UICanvas).toBe('function');
|
|
22
|
+
expect(typeof Button).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Container as PIXIContainer } from 'pixi.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// Evaluating the table registers it into ./defaults; Factory() reads it lazily.
|
|
5
|
+
import './const';
|
|
6
|
+
import { Factory } from './Factory';
|
|
7
|
+
import { getDefaultFactoryMethods } from './defaults';
|
|
8
|
+
|
|
9
|
+
describe('Factory extensions', () => {
|
|
10
|
+
it('does not leak extensions into the shared default table', () => {
|
|
11
|
+
const Extended = Factory({ customThing: () => new PIXIContainer() } as any);
|
|
12
|
+
const extended = new Extended() as any;
|
|
13
|
+
expect(typeof extended.make.customThing).toBe('function');
|
|
14
|
+
|
|
15
|
+
expect('customThing' in getDefaultFactoryMethods()).toBe(false);
|
|
16
|
+
|
|
17
|
+
const Plain = Factory();
|
|
18
|
+
const plain = new Plain() as any;
|
|
19
|
+
expect(plain.make.customThing).toBeUndefined();
|
|
20
|
+
expect(typeof plain.make.container).toBe('function');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Container as PIXIContainer } from 'pixi.js';
|
|
2
|
-
|
|
2
|
+
// Type-only: `typeof defaultFactoryMethods` is erased at compile time, so this
|
|
3
|
+
// import adds no runtime edge back into the table (which imports every ui and
|
|
4
|
+
// display class, all of which extend this mixin). The value is read lazily,
|
|
5
|
+
// inside the constructor, via the import-free registration slot in ./defaults.
|
|
6
|
+
import type { defaultFactoryMethods } from './const';
|
|
7
|
+
import { getDefaultFactoryMethods } from './defaults';
|
|
3
8
|
import { createFactoryMethods } from './methods';
|
|
4
9
|
|
|
5
10
|
export interface IFactory<T extends typeof defaultFactoryMethods = typeof defaultFactoryMethods> extends PIXIContainer {
|
|
@@ -16,7 +21,9 @@ export function Factory<T extends typeof defaultFactoryMethods = typeof defaultF
|
|
|
16
21
|
|
|
17
22
|
constructor() {
|
|
18
23
|
super();
|
|
19
|
-
|
|
24
|
+
// Merge into a copy — assigning into the shared table would leak this
|
|
25
|
+
// class's extensions into every other Factory() in the app.
|
|
26
|
+
extensions = Object.assign({}, getDefaultFactoryMethods(), extensions);
|
|
20
27
|
this.make = createFactoryMethods(extensions, this, false);
|
|
21
28
|
this.add = createFactoryMethods(extensions, this, true);
|
|
22
29
|
}
|
|
@@ -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
|
+
});
|