@ankhorage/zora-game 0.6.2 → 0.7.1
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/CHANGELOG.md +13 -0
- package/README.md +1 -2
- package/dist/ZORA_GAME_COMPONENT_META.d.ts +212 -0
- package/dist/ZORA_GAME_COMPONENT_META.d.ts.map +1 -1
- package/dist/ZORA_GAME_PLUGIN.d.ts +212 -0
- package/dist/ZORA_GAME_PLUGIN.d.ts.map +1 -1
- package/dist/ZORA_PLUGIN_METADATA.d.ts +212 -0
- package/dist/ZORA_PLUGIN_METADATA.d.ts.map +1 -1
- package/dist/features/game-presentation/adapters/inbound/GameEntity.d.ts +1 -1
- package/dist/features/game-presentation/adapters/inbound/GameEntity.d.ts.map +1 -1
- package/dist/features/game-presentation/adapters/inbound/GameEntity.js +15 -28
- package/dist/features/game-presentation/adapters/inbound/GameEntity.js.map +1 -1
- package/dist/features/game-presentation/meta/gameEntityMeta.d.ts +212 -0
- package/dist/features/game-presentation/meta/gameEntityMeta.d.ts.map +1 -1
- package/dist/features/game-presentation/meta/gameEntityMeta.js +114 -0
- package/dist/features/game-presentation/meta/gameEntityMeta.js.map +1 -1
- package/dist/features/game-presentation/public.d.ts +1 -1
- package/dist/features/game-presentation/public.d.ts.map +1 -1
- package/dist/features/game-presentation/public.js.map +1 -1
- package/dist/features/game-presentation/utils/resolveGameEntityMotionPlan.d.ts +15 -0
- package/dist/features/game-presentation/utils/resolveGameEntityMotionPlan.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/resolveGameEntityMotionPlan.js +31 -0
- package/dist/features/game-presentation/utils/resolveGameEntityMotionPlan.js.map +1 -0
- package/dist/features/game-presentation/utils/useAnimatedGameEntityValue.d.ts +5 -0
- package/dist/features/game-presentation/utils/useAnimatedGameEntityValue.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/useAnimatedGameEntityValue.js +33 -0
- package/dist/features/game-presentation/utils/useAnimatedGameEntityValue.js.map +1 -0
- package/dist/features/game-presentation/utils/useGameEntityAnimatedStyle.d.ts +16 -0
- package/dist/features/game-presentation/utils/useGameEntityAnimatedStyle.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/useGameEntityAnimatedStyle.js +52 -0
- package/dist/features/game-presentation/utils/useGameEntityAnimatedStyle.js.map +1 -0
- package/dist/features/game-presentation/utils/useGameEntityMeasurement.d.ts +5 -0
- package/dist/features/game-presentation/utils/useGameEntityMeasurement.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/useGameEntityMeasurement.js +14 -0
- package/dist/features/game-presentation/utils/useGameEntityMeasurement.js.map +1 -0
- package/dist/features/game-presentation/utils/useGameEntityMotionProgress.d.ts +5 -0
- package/dist/features/game-presentation/utils/useGameEntityMotionProgress.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/useGameEntityMotionProgress.js +87 -0
- package/dist/features/game-presentation/utils/useGameEntityMotionProgress.js.map +1 -0
- package/dist/features/game-presentation/utils/useGameReducedMotion.d.ts +3 -0
- package/dist/features/game-presentation/utils/useGameReducedMotion.d.ts.map +1 -0
- package/dist/features/game-presentation/utils/useGameReducedMotion.js +13 -0
- package/dist/features/game-presentation/utils/useGameReducedMotion.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/gamePresentation.d.ts +18 -1
- package/dist/types/gamePresentation.d.ts.map +1 -1
- package/dist/types/gamePresentation.js.map +1 -1
- package/package.json +4 -4
- package/src/ZORA_PLUGIN_METADATA.test.ts +17 -0
- package/src/features/game-presentation/adapters/inbound/GameEntity.tsx +19 -69
- package/src/features/game-presentation/meta/gameEntityMeta.ts +115 -0
- package/src/features/game-presentation/public.ts +2 -0
- package/src/features/game-presentation/utils/resolveGameEntityMotionPlan.test.ts +63 -0
- package/src/features/game-presentation/utils/resolveGameEntityMotionPlan.ts +53 -0
- package/src/features/game-presentation/utils/useAnimatedGameEntityValue.ts +41 -0
- package/src/features/game-presentation/utils/useGameEntityAnimatedStyle.ts +87 -0
- package/src/features/game-presentation/utils/useGameEntityMeasurement.ts +19 -0
- package/src/features/game-presentation/utils/useGameEntityMotionProgress.ts +114 -0
- package/src/features/game-presentation/utils/useGameReducedMotion.ts +15 -0
- package/src/index.ts +2 -0
- package/src/types/gamePresentation.ts +19 -1
|
@@ -2,6 +2,23 @@ import type { GameDefinition, GameInput, GameOutput } from '@ankhorage/game';
|
|
|
2
2
|
import type React from 'react';
|
|
3
3
|
type GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
4
4
|
export type GamePointerEvents = 'auto' | 'box-none' | 'box-only' | 'none';
|
|
5
|
+
export type GameEntityEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
6
|
+
export interface GameEntityMotionProps {
|
|
7
|
+
readonly transitionDurationMs?: number;
|
|
8
|
+
readonly transitionEasing?: GameEntityEasing;
|
|
9
|
+
readonly motionOffsetX?: number;
|
|
10
|
+
readonly motionOffsetY?: number;
|
|
11
|
+
readonly motionOpacityDelta?: number;
|
|
12
|
+
readonly motionScaleDelta?: number;
|
|
13
|
+
readonly motionRotationDelta?: number;
|
|
14
|
+
readonly motionDurationMs?: number;
|
|
15
|
+
readonly motionDelayMs?: number;
|
|
16
|
+
readonly motionEasing?: GameEntityEasing;
|
|
17
|
+
readonly motionRepeat?: boolean;
|
|
18
|
+
readonly motionAlternate?: boolean;
|
|
19
|
+
readonly motionPaused?: boolean;
|
|
20
|
+
readonly motionEssential?: boolean;
|
|
21
|
+
}
|
|
5
22
|
export type GameOverlayPlacement = 'fill' | 'center' | 'top' | 'bottom' | GameOverlayCornerPlacement;
|
|
6
23
|
export interface GameFieldProps {
|
|
7
24
|
readonly children?: React.ReactNode;
|
|
@@ -11,7 +28,7 @@ export interface GameFieldProps {
|
|
|
11
28
|
readonly accessibilityLabel?: string;
|
|
12
29
|
readonly testID?: string;
|
|
13
30
|
}
|
|
14
|
-
export interface GameEntityProps {
|
|
31
|
+
export interface GameEntityProps extends GameEntityMotionProps {
|
|
15
32
|
readonly children?: React.ReactNode;
|
|
16
33
|
readonly x?: number;
|
|
17
34
|
readonly y?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gamePresentation.d.ts","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,0BAA0B,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;AAE5F,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;AAC1E,MAAM,MAAM,oBAAoB,GAC9B,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"gamePresentation.d.ts","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gamePresentation.js","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"","sourcesContent":["import type { GameDefinition, GameInput, GameOutput } from '@ankhorage/game';\nimport type React from 'react';\n\ntype GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\nexport type GamePointerEvents = 'auto' | 'box-none' | 'box-only' | 'none';\nexport type GameOverlayPlacement =\n 'fill' | 'center' | 'top' | 'bottom' | GameOverlayCornerPlacement;\n\nexport interface GameFieldProps {\n readonly children?: React.ReactNode;\n readonly aspectRatio?: number;\n readonly minHeight?: number;\n readonly clip?: boolean;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameEntityProps {\n readonly children?: React.ReactNode;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly opacity?: number;\n readonly scale?: number;\n readonly rotation?: number;\n readonly zIndex?: number;\n readonly hidden?: boolean;\n readonly pointerEvents?: GamePointerEvents;\n readonly measurementId?: string;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameKeyboardBinding {\n readonly key: string;\n readonly eventType?: string;\n readonly entityId?: string;\n readonly preventDefault?: boolean;\n}\n\nexport interface GameInputZoneProps {\n readonly eventType: string;\n readonly entityId?: string;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly zIndex?: number;\n readonly enabled?: boolean;\n readonly continuous?: boolean;\n readonly keyboardBindings?: readonly GameKeyboardBinding[];\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameMeasurementProbeProps {\n readonly sourceId: string;\n readonly targetId: string;\n readonly eventType?: string;\n readonly entityId?: string;\n readonly delayMs?: number;\n readonly enabled?: boolean;\n}\n\nexport interface GameOverlayProps {\n readonly children?: React.ReactNode;\n readonly placement?: GameOverlayPlacement;\n readonly blocking?: boolean;\n readonly padding?: number;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameProps extends GameFieldProps {\n readonly definition: GameDefinition;\n readonly input?: GameInput;\n readonly seed?: number;\n readonly resetKey?: string;\n readonly autoAdvanceTime?: boolean;\n readonly onOutput?: (output: GameOutput) => void;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gamePresentation.js","sourceRoot":"","sources":["../../src/types/gamePresentation.ts"],"names":[],"mappings":"","sourcesContent":["import type { GameDefinition, GameInput, GameOutput } from '@ankhorage/game';\nimport type React from 'react';\n\ntype GameOverlayCornerPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n\nexport type GamePointerEvents = 'auto' | 'box-none' | 'box-only' | 'none';\nexport type GameEntityEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';\n\nexport interface GameEntityMotionProps {\n readonly transitionDurationMs?: number;\n readonly transitionEasing?: GameEntityEasing;\n readonly motionOffsetX?: number;\n readonly motionOffsetY?: number;\n readonly motionOpacityDelta?: number;\n readonly motionScaleDelta?: number;\n readonly motionRotationDelta?: number;\n readonly motionDurationMs?: number;\n readonly motionDelayMs?: number;\n readonly motionEasing?: GameEntityEasing;\n readonly motionRepeat?: boolean;\n readonly motionAlternate?: boolean;\n readonly motionPaused?: boolean;\n readonly motionEssential?: boolean;\n}\nexport type GameOverlayPlacement =\n 'fill' | 'center' | 'top' | 'bottom' | GameOverlayCornerPlacement;\n\nexport interface GameFieldProps {\n readonly children?: React.ReactNode;\n readonly aspectRatio?: number;\n readonly minHeight?: number;\n readonly clip?: boolean;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameEntityProps extends GameEntityMotionProps {\n readonly children?: React.ReactNode;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly opacity?: number;\n readonly scale?: number;\n readonly rotation?: number;\n readonly zIndex?: number;\n readonly hidden?: boolean;\n readonly pointerEvents?: GamePointerEvents;\n readonly measurementId?: string;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameKeyboardBinding {\n readonly key: string;\n readonly eventType?: string;\n readonly entityId?: string;\n readonly preventDefault?: boolean;\n}\n\nexport interface GameInputZoneProps {\n readonly eventType: string;\n readonly entityId?: string;\n readonly x?: number;\n readonly y?: number;\n readonly width?: number;\n readonly height?: number;\n readonly zIndex?: number;\n readonly enabled?: boolean;\n readonly continuous?: boolean;\n readonly keyboardBindings?: readonly GameKeyboardBinding[];\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameMeasurementProbeProps {\n readonly sourceId: string;\n readonly targetId: string;\n readonly eventType?: string;\n readonly entityId?: string;\n readonly delayMs?: number;\n readonly enabled?: boolean;\n}\n\nexport interface GameOverlayProps {\n readonly children?: React.ReactNode;\n readonly placement?: GameOverlayPlacement;\n readonly blocking?: boolean;\n readonly padding?: number;\n readonly accessibilityLabel?: string;\n readonly testID?: string;\n}\n\nexport interface GameProps extends GameFieldProps {\n readonly definition: GameDefinition;\n readonly input?: GameInput;\n readonly seed?: number;\n readonly resetKey?: string;\n readonly autoAdvanceTime?: boolean;\n readonly onOutput?: (output: GameOutput) => void;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora-game",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Generic game presentation primitives for React Native and React Native Web apps built on ZORA.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora-game#readme",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"node": ">=24"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@ankhorage/zora": ">=20.0.
|
|
60
|
+
"@ankhorage/zora": ">=20.0.2",
|
|
61
61
|
"react": "19.2.3",
|
|
62
62
|
"react-native": "0.86.x"
|
|
63
63
|
},
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@ankhorage/devtools": "^1.15.3",
|
|
82
82
|
"@ankhorage/paradox": "^0.1.26",
|
|
83
|
-
"@ankhorage/zora": "^20.0.
|
|
83
|
+
"@ankhorage/zora": "^20.0.2",
|
|
84
84
|
"@types/bun": "^1.4.1",
|
|
85
85
|
"@types/node": "^26.6.1",
|
|
86
86
|
"@types/react": "^19.2.18",
|
|
@@ -94,6 +94,6 @@
|
|
|
94
94
|
"packageManager": "bun@1.4.2",
|
|
95
95
|
"dependencies": {
|
|
96
96
|
"@ankhorage/game": "^0.1.1",
|
|
97
|
-
"@ankhorage/runtime": "^3.0.
|
|
97
|
+
"@ankhorage/runtime": "^3.0.2"
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -42,6 +42,23 @@ describe('ZORA game plugin metadata', () => {
|
|
|
42
42
|
expect(gameEntity.bindings?.props?.measurementId?.value.type).toBe('string');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test('exposes serializable GameEntity motion props to canonical ZORA bindings', () => {
|
|
46
|
+
const catalog = composeZoraPluginMetadata([ZORA_CORE_PLUGIN_METADATA, ZORA_PLUGIN_METADATA]);
|
|
47
|
+
const gameEntity = readComponentMeta(catalog.componentMeta.GameEntity, 'GameEntity');
|
|
48
|
+
|
|
49
|
+
expect(gameEntity.bindings?.props?.transitionDurationMs?.value.type).toBe('number');
|
|
50
|
+
expect(gameEntity.bindings?.props?.motionOffsetY?.value.type).toBe('number');
|
|
51
|
+
expect(gameEntity.bindings?.props?.motionRepeat?.value.type).toBe('boolean');
|
|
52
|
+
expect(gameEntity.props.motionEasing?.enum).toEqual([
|
|
53
|
+
'linear',
|
|
54
|
+
'ease-in',
|
|
55
|
+
'ease-out',
|
|
56
|
+
'ease-in-out',
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('ZORA game genericity metadata', () => {
|
|
45
62
|
test('keeps the generic presentation vocabulary free of product-specific concepts', () => {
|
|
46
63
|
const serialized = JSON.stringify(ZORA_PLUGIN_METADATA).toLowerCase();
|
|
47
64
|
|
|
@@ -1,87 +1,37 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { StyleSheet, View, type ViewStyle } from 'react-native';
|
|
2
|
+
import { Animated, StyleSheet, type View, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import type { GameEntityProps } from '../../../../types/gamePresentation';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { measureGameEntity } from './measureGameEntity';
|
|
5
|
+
import { useGameEntityAnimatedStyle } from '../../utils/useGameEntityAnimatedStyle';
|
|
6
|
+
import { useGameEntityMeasurement } from '../../utils/useGameEntityMeasurement';
|
|
8
7
|
|
|
9
8
|
/*** Render one generic positioned game entity without owning gameplay semantics. */
|
|
10
|
-
export function GameEntity({
|
|
11
|
-
children,
|
|
12
|
-
x = 0,
|
|
13
|
-
y = 0,
|
|
14
|
-
width,
|
|
15
|
-
height,
|
|
16
|
-
opacity = 1,
|
|
17
|
-
scale = 1,
|
|
18
|
-
rotation = 0,
|
|
19
|
-
zIndex = 0,
|
|
20
|
-
hidden = false,
|
|
21
|
-
pointerEvents = 'auto',
|
|
22
|
-
measurementId,
|
|
23
|
-
accessibilityLabel,
|
|
24
|
-
testID,
|
|
25
|
-
}: GameEntityProps) {
|
|
9
|
+
export function GameEntity(props: GameEntityProps) {
|
|
26
10
|
const elementRef = React.useRef<View | null>(null);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
React.useEffect(() => {
|
|
31
|
-
if (measurementId === undefined || registerMeasurement === undefined) return undefined;
|
|
32
|
-
return registerMeasurement(measurementId, () => measureGameEntity(elementRef.current));
|
|
33
|
-
}, [measurementId, registerMeasurement]);
|
|
11
|
+
const animatedStyle = useGameEntityAnimatedStyle(props);
|
|
12
|
+
useGameEntityMeasurement(elementRef, props.measurementId);
|
|
34
13
|
|
|
35
14
|
return (
|
|
36
|
-
<View
|
|
15
|
+
<Animated.View
|
|
37
16
|
ref={elementRef}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
pointerEvents={pointerEvents}
|
|
41
|
-
style={[
|
|
42
|
-
styles.root,
|
|
43
|
-
createEntityStyle({ x, y, width, height, opacity, scale, rotation, zIndex, hidden }),
|
|
44
|
-
]}
|
|
17
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
18
|
+
testID={props.testID}
|
|
19
|
+
pointerEvents={props.pointerEvents ?? 'auto'}
|
|
20
|
+
style={[styles.root, createEntityStaticStyle(props), animatedStyle]}
|
|
45
21
|
>
|
|
46
|
-
{children}
|
|
47
|
-
</View>
|
|
22
|
+
{props.children}
|
|
23
|
+
</Animated.View>
|
|
48
24
|
);
|
|
49
25
|
}
|
|
50
26
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
readonly y: number;
|
|
54
|
-
readonly width: number | undefined;
|
|
55
|
-
readonly height: number | undefined;
|
|
56
|
-
readonly opacity: number;
|
|
57
|
-
readonly scale: number;
|
|
58
|
-
readonly rotation: number;
|
|
59
|
-
readonly zIndex: number;
|
|
60
|
-
readonly hidden: boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/*** Convert serializable entity presentation props into a React Native view style. */
|
|
64
|
-
function createEntityStyle({
|
|
65
|
-
x,
|
|
66
|
-
y,
|
|
67
|
-
width,
|
|
68
|
-
height,
|
|
69
|
-
opacity,
|
|
70
|
-
scale,
|
|
71
|
-
rotation,
|
|
72
|
-
zIndex,
|
|
73
|
-
hidden,
|
|
74
|
-
}: GameEntityStyleInput): ViewStyle {
|
|
27
|
+
/*** Convert non-animated GameEntity presentation props into a React Native view style. */
|
|
28
|
+
function createEntityStaticStyle(props: GameEntityProps): ViewStyle {
|
|
75
29
|
return {
|
|
76
|
-
display: hidden ? 'none' : 'flex',
|
|
77
|
-
left: toGamePercentage(x),
|
|
78
|
-
opacity: Math.min(1, Math.max(0, opacity)),
|
|
30
|
+
display: props.hidden === true ? 'none' : 'flex',
|
|
79
31
|
position: 'absolute',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
...(height === undefined ? {} : { height }),
|
|
84
|
-
...(width === undefined ? {} : { width }),
|
|
32
|
+
zIndex: props.zIndex ?? 0,
|
|
33
|
+
...(props.height === undefined ? {} : { height: props.height }),
|
|
34
|
+
...(props.width === undefined ? {} : { width: props.width }),
|
|
85
35
|
};
|
|
86
36
|
}
|
|
87
37
|
|
|
@@ -23,6 +23,20 @@ export const gameEntityMeta = {
|
|
|
23
23
|
zIndex: { value: { type: 'number' }, acceptsFallback: true },
|
|
24
24
|
hidden: { value: { type: 'boolean' }, acceptsFallback: true },
|
|
25
25
|
measurementId: { value: { type: 'string' }, acceptsFallback: true },
|
|
26
|
+
transitionDurationMs: { value: { type: 'number' }, acceptsFallback: true },
|
|
27
|
+
transitionEasing: { value: { type: 'string' }, acceptsFallback: true },
|
|
28
|
+
motionOffsetX: { value: { type: 'number' }, acceptsFallback: true },
|
|
29
|
+
motionOffsetY: { value: { type: 'number' }, acceptsFallback: true },
|
|
30
|
+
motionOpacityDelta: { value: { type: 'number' }, acceptsFallback: true },
|
|
31
|
+
motionScaleDelta: { value: { type: 'number' }, acceptsFallback: true },
|
|
32
|
+
motionRotationDelta: { value: { type: 'number' }, acceptsFallback: true },
|
|
33
|
+
motionDurationMs: { value: { type: 'number' }, acceptsFallback: true },
|
|
34
|
+
motionDelayMs: { value: { type: 'number' }, acceptsFallback: true },
|
|
35
|
+
motionEasing: { value: { type: 'string' }, acceptsFallback: true },
|
|
36
|
+
motionRepeat: { value: { type: 'boolean' }, acceptsFallback: true },
|
|
37
|
+
motionAlternate: { value: { type: 'boolean' }, acceptsFallback: true },
|
|
38
|
+
motionPaused: { value: { type: 'boolean' }, acceptsFallback: true },
|
|
39
|
+
motionEssential: { value: { type: 'boolean' }, acceptsFallback: true },
|
|
26
40
|
},
|
|
27
41
|
},
|
|
28
42
|
props: {
|
|
@@ -101,6 +115,107 @@ export const gameEntityMeta = {
|
|
|
101
115
|
label: 'Measurement id',
|
|
102
116
|
authoring: { authority: 'instance' },
|
|
103
117
|
},
|
|
118
|
+
|
|
119
|
+
transitionDurationMs: {
|
|
120
|
+
type: 'number',
|
|
121
|
+
category: 'Motion',
|
|
122
|
+
label: 'Transition duration (ms)',
|
|
123
|
+
default: 0,
|
|
124
|
+
authoring: { authority: 'instance' },
|
|
125
|
+
},
|
|
126
|
+
transitionEasing: {
|
|
127
|
+
type: 'enum',
|
|
128
|
+
category: 'Motion',
|
|
129
|
+
label: 'Transition easing',
|
|
130
|
+
enum: ['linear', 'ease-in', 'ease-out', 'ease-in-out'],
|
|
131
|
+
default: 'linear',
|
|
132
|
+
authoring: { authority: 'instance' },
|
|
133
|
+
},
|
|
134
|
+
motionOffsetX: {
|
|
135
|
+
type: 'number',
|
|
136
|
+
category: 'Motion',
|
|
137
|
+
label: 'Motion X offset (%)',
|
|
138
|
+
default: 0,
|
|
139
|
+
authoring: { authority: 'instance' },
|
|
140
|
+
},
|
|
141
|
+
motionOffsetY: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
category: 'Motion',
|
|
144
|
+
label: 'Motion Y offset (%)',
|
|
145
|
+
default: 0,
|
|
146
|
+
authoring: { authority: 'instance' },
|
|
147
|
+
},
|
|
148
|
+
motionOpacityDelta: {
|
|
149
|
+
type: 'number',
|
|
150
|
+
category: 'Motion',
|
|
151
|
+
label: 'Motion opacity delta',
|
|
152
|
+
default: 0,
|
|
153
|
+
authoring: { authority: 'instance' },
|
|
154
|
+
},
|
|
155
|
+
motionScaleDelta: {
|
|
156
|
+
type: 'number',
|
|
157
|
+
category: 'Motion',
|
|
158
|
+
label: 'Motion scale delta',
|
|
159
|
+
default: 0,
|
|
160
|
+
authoring: { authority: 'instance' },
|
|
161
|
+
},
|
|
162
|
+
motionRotationDelta: {
|
|
163
|
+
type: 'number',
|
|
164
|
+
category: 'Motion',
|
|
165
|
+
label: 'Motion rotation delta',
|
|
166
|
+
default: 0,
|
|
167
|
+
authoring: { authority: 'instance' },
|
|
168
|
+
},
|
|
169
|
+
motionDurationMs: {
|
|
170
|
+
type: 'number',
|
|
171
|
+
category: 'Motion',
|
|
172
|
+
label: 'Motion duration (ms)',
|
|
173
|
+
default: 0,
|
|
174
|
+
authoring: { authority: 'instance' },
|
|
175
|
+
},
|
|
176
|
+
motionDelayMs: {
|
|
177
|
+
type: 'number',
|
|
178
|
+
category: 'Motion',
|
|
179
|
+
label: 'Motion delay (ms)',
|
|
180
|
+
default: 0,
|
|
181
|
+
authoring: { authority: 'instance' },
|
|
182
|
+
},
|
|
183
|
+
motionEasing: {
|
|
184
|
+
type: 'enum',
|
|
185
|
+
category: 'Motion',
|
|
186
|
+
label: 'Motion easing',
|
|
187
|
+
enum: ['linear', 'ease-in', 'ease-out', 'ease-in-out'],
|
|
188
|
+
default: 'linear',
|
|
189
|
+
authoring: { authority: 'instance' },
|
|
190
|
+
},
|
|
191
|
+
motionRepeat: {
|
|
192
|
+
type: 'boolean',
|
|
193
|
+
category: 'Motion',
|
|
194
|
+
label: 'Repeat motion',
|
|
195
|
+
default: false,
|
|
196
|
+
authoring: { authority: 'instance' },
|
|
197
|
+
},
|
|
198
|
+
motionAlternate: {
|
|
199
|
+
type: 'boolean',
|
|
200
|
+
category: 'Motion',
|
|
201
|
+
label: 'Alternate motion',
|
|
202
|
+
default: false,
|
|
203
|
+
authoring: { authority: 'instance' },
|
|
204
|
+
},
|
|
205
|
+
motionPaused: {
|
|
206
|
+
type: 'boolean',
|
|
207
|
+
category: 'Motion',
|
|
208
|
+
label: 'Pause motion',
|
|
209
|
+
default: false,
|
|
210
|
+
authoring: { authority: 'instance' },
|
|
211
|
+
},
|
|
212
|
+
motionEssential: {
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
category: 'Motion',
|
|
215
|
+
label: 'Essential motion',
|
|
216
|
+
default: false,
|
|
217
|
+
authoring: { authority: 'instance' },
|
|
218
|
+
},
|
|
104
219
|
accessibilityLabel: {
|
|
105
220
|
type: 'string',
|
|
106
221
|
category: 'Accessibility',
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolveGameEntityMotionPlan } from './resolveGameEntityMotionPlan';
|
|
4
|
+
|
|
5
|
+
describe('resolveGameEntityMotionPlan configuration', () => {
|
|
6
|
+
test('preserves explicit transition and repeating alternate motion', () => {
|
|
7
|
+
expect(
|
|
8
|
+
resolveGameEntityMotionPlan(
|
|
9
|
+
{
|
|
10
|
+
transitionDurationMs: 70,
|
|
11
|
+
transitionEasing: 'ease-out',
|
|
12
|
+
motionOffsetY: 8,
|
|
13
|
+
motionDurationMs: 1200,
|
|
14
|
+
motionDelayMs: 100,
|
|
15
|
+
motionEasing: 'ease-in-out',
|
|
16
|
+
motionRepeat: true,
|
|
17
|
+
motionAlternate: true,
|
|
18
|
+
},
|
|
19
|
+
false,
|
|
20
|
+
),
|
|
21
|
+
).toEqual({
|
|
22
|
+
transitionDurationMs: 70,
|
|
23
|
+
transitionEasing: 'ease-out',
|
|
24
|
+
motionDurationMs: 1200,
|
|
25
|
+
motionDelayMs: 100,
|
|
26
|
+
motionEasing: 'ease-in-out',
|
|
27
|
+
motionEnabled: true,
|
|
28
|
+
motionRepeat: true,
|
|
29
|
+
motionAlternate: true,
|
|
30
|
+
motionPaused: false,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('uses alternate only for repeating motion', () => {
|
|
35
|
+
expect(
|
|
36
|
+
resolveGameEntityMotionPlan(
|
|
37
|
+
{ motionOffsetY: 5, motionDurationMs: 100, motionAlternate: true },
|
|
38
|
+
false,
|
|
39
|
+
).motionAlternate,
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('resolveGameEntityMotionPlan reduced motion', () => {
|
|
45
|
+
test('disables decorative motion and transitions', () => {
|
|
46
|
+
const plan = resolveGameEntityMotionPlan(
|
|
47
|
+
{ transitionDurationMs: 80, motionOffsetX: 12, motionDurationMs: 500 },
|
|
48
|
+
true,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(plan.transitionDurationMs).toBe(0);
|
|
52
|
+
expect(plan.motionEnabled).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('keeps essential trajectories enabled', () => {
|
|
56
|
+
const plan = resolveGameEntityMotionPlan(
|
|
57
|
+
{ motionOffsetY: 70, motionDurationMs: 600, motionEssential: true },
|
|
58
|
+
true,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(plan.motionEnabled).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { GameEntityEasing, GameEntityMotionProps } from '../../../types/gamePresentation';
|
|
2
|
+
|
|
3
|
+
export interface GameEntityMotionPlan {
|
|
4
|
+
readonly transitionDurationMs: number;
|
|
5
|
+
readonly transitionEasing: GameEntityEasing;
|
|
6
|
+
readonly motionDurationMs: number;
|
|
7
|
+
readonly motionDelayMs: number;
|
|
8
|
+
readonly motionEasing: GameEntityEasing;
|
|
9
|
+
readonly motionEnabled: boolean;
|
|
10
|
+
readonly motionRepeat: boolean;
|
|
11
|
+
readonly motionAlternate: boolean;
|
|
12
|
+
readonly motionPaused: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*** Normalize serializable GameEntity motion props against the current reduced-motion preference. */
|
|
16
|
+
export function resolveGameEntityMotionPlan(
|
|
17
|
+
props: GameEntityMotionProps,
|
|
18
|
+
reduceMotion: boolean,
|
|
19
|
+
): GameEntityMotionPlan {
|
|
20
|
+
const essential = props.motionEssential === true;
|
|
21
|
+
const motionEnabled =
|
|
22
|
+
hasMotionOffset(props) &&
|
|
23
|
+
normalizeDuration(props.motionDurationMs) > 0 &&
|
|
24
|
+
(!reduceMotion || essential);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
transitionDurationMs: reduceMotion ? 0 : normalizeDuration(props.transitionDurationMs),
|
|
28
|
+
transitionEasing: props.transitionEasing ?? 'linear',
|
|
29
|
+
motionDurationMs: normalizeDuration(props.motionDurationMs),
|
|
30
|
+
motionDelayMs: normalizeDuration(props.motionDelayMs),
|
|
31
|
+
motionEasing: props.motionEasing ?? 'linear',
|
|
32
|
+
motionEnabled,
|
|
33
|
+
motionRepeat: props.motionRepeat === true,
|
|
34
|
+
motionAlternate: props.motionRepeat === true && props.motionAlternate === true,
|
|
35
|
+
motionPaused: props.motionPaused === true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/*** Return whether any relative presentation offset requires a motion timeline. */
|
|
40
|
+
function hasMotionOffset(props: GameEntityMotionProps): boolean {
|
|
41
|
+
return (
|
|
42
|
+
(props.motionOffsetX ?? 0) !== 0 ||
|
|
43
|
+
(props.motionOffsetY ?? 0) !== 0 ||
|
|
44
|
+
(props.motionOpacityDelta ?? 0) !== 0 ||
|
|
45
|
+
(props.motionScaleDelta ?? 0) !== 0 ||
|
|
46
|
+
(props.motionRotationDelta ?? 0) !== 0
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*** Clamp an optional duration to a finite non-negative millisecond value. */
|
|
51
|
+
function normalizeDuration(value: number | undefined): number {
|
|
52
|
+
return value === undefined || !Number.isFinite(value) ? 0 : Math.max(0, value);
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Animated, Easing } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { GameEntityEasing } from '../../../types/gamePresentation';
|
|
5
|
+
|
|
6
|
+
/*** Animate one bound numeric GameEntity presentation value toward its latest session-driven target. */
|
|
7
|
+
export function useAnimatedGameEntityValue(
|
|
8
|
+
target: number,
|
|
9
|
+
durationMs: number,
|
|
10
|
+
easing: GameEntityEasing,
|
|
11
|
+
): Animated.Value {
|
|
12
|
+
const [value] = React.useState(() => new Animated.Value(target));
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
value.stopAnimation();
|
|
16
|
+
|
|
17
|
+
if (durationMs === 0) {
|
|
18
|
+
value.setValue(target);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const animation = Animated.timing(value, {
|
|
23
|
+
toValue: target,
|
|
24
|
+
duration: durationMs,
|
|
25
|
+
easing: resolveGameEntityEasing(easing),
|
|
26
|
+
useNativeDriver: false,
|
|
27
|
+
});
|
|
28
|
+
animation.start();
|
|
29
|
+
return () => animation.stop();
|
|
30
|
+
}, [durationMs, easing, target, value]);
|
|
31
|
+
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*** Map serializable easing names onto React Native easing functions. */
|
|
36
|
+
function resolveGameEntityEasing(easing: GameEntityEasing) {
|
|
37
|
+
if (easing === 'ease-in') return Easing.in(Easing.ease);
|
|
38
|
+
if (easing === 'ease-out') return Easing.out(Easing.ease);
|
|
39
|
+
if (easing === 'ease-in-out') return Easing.inOut(Easing.ease);
|
|
40
|
+
return Easing.linear;
|
|
41
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Animated } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import type { GameEntityProps } from '../../../types/gamePresentation';
|
|
4
|
+
import { resolveGameEntityMotionPlan } from './resolveGameEntityMotionPlan';
|
|
5
|
+
import { useAnimatedGameEntityValue } from './useAnimatedGameEntityValue';
|
|
6
|
+
import { useGameEntityMotionProgress } from './useGameEntityMotionProgress';
|
|
7
|
+
import { useGameReducedMotion } from './useGameReducedMotion';
|
|
8
|
+
|
|
9
|
+
interface GameEntityAnimatedValues {
|
|
10
|
+
readonly x: Animated.Value;
|
|
11
|
+
readonly y: Animated.Value;
|
|
12
|
+
readonly opacity: Animated.Value;
|
|
13
|
+
readonly scale: Animated.Value;
|
|
14
|
+
readonly rotation: Animated.Value;
|
|
15
|
+
readonly progress: Animated.Value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*** Compose bound GameEntity values with optional relative presentation motion. */
|
|
19
|
+
export function useGameEntityAnimatedStyle(input: GameEntityProps) {
|
|
20
|
+
const reduceMotion = useGameReducedMotion();
|
|
21
|
+
const plan = resolveGameEntityMotionPlan(input, reduceMotion);
|
|
22
|
+
const values: GameEntityAnimatedValues = {
|
|
23
|
+
x: useAnimatedGameEntityValue(input.x ?? 0, plan.transitionDurationMs, plan.transitionEasing),
|
|
24
|
+
y: useAnimatedGameEntityValue(input.y ?? 0, plan.transitionDurationMs, plan.transitionEasing),
|
|
25
|
+
opacity: useAnimatedGameEntityValue(
|
|
26
|
+
input.opacity ?? 1,
|
|
27
|
+
plan.transitionDurationMs,
|
|
28
|
+
plan.transitionEasing,
|
|
29
|
+
),
|
|
30
|
+
scale: useAnimatedGameEntityValue(
|
|
31
|
+
input.scale ?? 1,
|
|
32
|
+
plan.transitionDurationMs,
|
|
33
|
+
plan.transitionEasing,
|
|
34
|
+
),
|
|
35
|
+
rotation: useAnimatedGameEntityValue(
|
|
36
|
+
input.rotation ?? 0,
|
|
37
|
+
plan.transitionDurationMs,
|
|
38
|
+
plan.transitionEasing,
|
|
39
|
+
),
|
|
40
|
+
progress: useGameEntityMotionProgress(plan),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return createGameEntityAnimatedStyle(values, input);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/*** Build one animated React Native style from base values and relative motion progress. */
|
|
47
|
+
function createGameEntityAnimatedStyle(values: GameEntityAnimatedValues, input: GameEntityProps) {
|
|
48
|
+
const left = Animated.add(values.x, Animated.multiply(values.progress, input.motionOffsetX ?? 0));
|
|
49
|
+
const top = Animated.add(values.y, Animated.multiply(values.progress, input.motionOffsetY ?? 0));
|
|
50
|
+
const opacity = Animated.add(
|
|
51
|
+
values.opacity,
|
|
52
|
+
Animated.multiply(values.progress, input.motionOpacityDelta ?? 0),
|
|
53
|
+
);
|
|
54
|
+
const scale = Animated.add(
|
|
55
|
+
values.scale,
|
|
56
|
+
Animated.multiply(values.progress, input.motionScaleDelta ?? 0),
|
|
57
|
+
);
|
|
58
|
+
const rotation = Animated.add(
|
|
59
|
+
values.rotation,
|
|
60
|
+
Animated.multiply(values.progress, input.motionRotationDelta ?? 0),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
left: left.interpolate({
|
|
65
|
+
inputRange: [-10_000, 10_000],
|
|
66
|
+
outputRange: ['-10000%', '10000%'],
|
|
67
|
+
}),
|
|
68
|
+
top: top.interpolate({
|
|
69
|
+
inputRange: [-10_000, 10_000],
|
|
70
|
+
outputRange: ['-10000%', '10000%'],
|
|
71
|
+
}),
|
|
72
|
+
opacity: opacity.interpolate({
|
|
73
|
+
inputRange: [0, 1],
|
|
74
|
+
outputRange: [0, 1],
|
|
75
|
+
extrapolate: 'clamp',
|
|
76
|
+
}),
|
|
77
|
+
transform: [
|
|
78
|
+
{ scale },
|
|
79
|
+
{
|
|
80
|
+
rotate: rotation.interpolate({
|
|
81
|
+
inputRange: [-36_000, 36_000],
|
|
82
|
+
outputRange: ['-36000deg', '36000deg'],
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { measureGameEntity } from '../adapters/inbound/measureGameEntity';
|
|
5
|
+
import { GameRuntimeContext } from '../composition/GameRuntimeContext';
|
|
6
|
+
|
|
7
|
+
/*** Register one optional measurable GameEntity view inside its local Game scope. */
|
|
8
|
+
export function useGameEntityMeasurement(
|
|
9
|
+
elementRef: React.RefObject<View | null>,
|
|
10
|
+
measurementId: string | undefined,
|
|
11
|
+
): void {
|
|
12
|
+
const runtime = React.useContext(GameRuntimeContext);
|
|
13
|
+
const registerMeasurement = runtime?.registerMeasurement;
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (measurementId === undefined || registerMeasurement === undefined) return undefined;
|
|
17
|
+
return registerMeasurement(measurementId, () => measureGameEntity(elementRef.current));
|
|
18
|
+
}, [elementRef, measurementId, registerMeasurement]);
|
|
19
|
+
}
|