aniview 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/Aniview.d.ts +63 -0
- package/dist/Aniview.d.ts.map +1 -0
- package/dist/Aniview.js +831 -0
- package/dist/Aniview.js.map +1 -0
- package/dist/AniviewPanel.d.ts +33 -0
- package/dist/AniviewPanel.d.ts.map +1 -0
- package/dist/AniviewPanel.js +66 -0
- package/dist/AniviewPanel.js.map +1 -0
- package/dist/GestureStressTest.d.ts +3 -0
- package/dist/GestureStressTest.d.ts.map +1 -0
- package/dist/GestureStressTest.js +125 -0
- package/dist/GestureStressTest.js.map +1 -0
- package/dist/aniviewConfig.d.ts +175 -0
- package/dist/aniviewConfig.d.ts.map +1 -0
- package/dist/aniviewConfig.js +568 -0
- package/dist/aniviewConfig.js.map +1 -0
- package/dist/aniviewProvider.d.ts +93 -0
- package/dist/aniviewProvider.d.ts.map +1 -0
- package/dist/aniviewProvider.js +229 -0
- package/dist/aniviewProvider.js.map +1 -0
- package/dist/core/AniviewLock.d.ts +16 -0
- package/dist/core/AniviewLock.d.ts.map +1 -0
- package/dist/core/AniviewLock.js +18 -0
- package/dist/core/AniviewLock.js.map +1 -0
- package/dist/core/AniviewMath.d.ts +41 -0
- package/dist/core/AniviewMath.d.ts.map +1 -0
- package/dist/core/AniviewMath.js +69 -0
- package/dist/core/AniviewMath.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/useAniview.d.ts +39 -0
- package/dist/useAniview.d.ts.map +1 -0
- package/dist/useAniview.js +32 -0
- package/dist/useAniview.js.map +1 -0
- package/dist/useAniviewContext.d.ts +156 -0
- package/dist/useAniviewContext.d.ts.map +1 -0
- package/dist/useAniviewContext.js +3 -0
- package/dist/useAniviewContext.js.map +1 -0
- package/dist/useAniviewLock.d.ts +20 -0
- package/dist/useAniviewLock.d.ts.map +1 -0
- package/dist/useAniviewLock.js +32 -0
- package/dist/useAniviewLock.js.map +1 -0
- package/package.json +60 -0
- package/src/Aniview.tsx +868 -0
- package/src/AniviewPanel.tsx +141 -0
- package/src/GestureStressTest.tsx +144 -0
- package/src/__tests__/AniviewLock.test.ts +58 -0
- package/src/__tests__/AniviewMath.test.ts +211 -0
- package/src/__tests__/AniviewSnapshot.test.tsx +85 -0
- package/src/__tests__/__snapshots__/AniviewSnapshot.test.tsx.snap +7 -0
- package/src/__tests__/aniviewConfig.test.ts +70 -0
- package/src/aniviewConfig.tsx +688 -0
- package/src/aniviewProvider.tsx +307 -0
- package/src/core/AniviewLock.ts +23 -0
- package/src/core/AniviewMath.ts +107 -0
- package/src/index.ts +6 -0
- package/src/useAniview.tsx +75 -0
- package/src/useAniviewContext.tsx +170 -0
- package/src/useAniviewLock.tsx +37 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, ViewStyle } from 'react-native';
|
|
3
|
+
import Aniview from './Aniview';
|
|
4
|
+
import { AniviewProps, AniviewFrame } from './useAniviewContext';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LAYOUT = {
|
|
7
|
+
CONTENT_BG: '#FFFFFF',
|
|
8
|
+
FRAME_INK: '#2D2D2D',
|
|
9
|
+
FRAME_WOOD: '#C19A6B',
|
|
10
|
+
BORR: 8,
|
|
11
|
+
FRAME_WIDTH: 2,
|
|
12
|
+
OUTLINE_WIDTH: 1,
|
|
13
|
+
GUTTER_H: 16
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* PANEL FRAME (Visual Only)
|
|
18
|
+
*/
|
|
19
|
+
interface PanelFrameProps {
|
|
20
|
+
width?: number | string;
|
|
21
|
+
height?: number | string;
|
|
22
|
+
backgroundColor?: string;
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
style?: ViewStyle;
|
|
25
|
+
clipContent?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const PanelFrame = ({
|
|
29
|
+
width,
|
|
30
|
+
height,
|
|
31
|
+
backgroundColor = DEFAULT_LAYOUT.CONTENT_BG,
|
|
32
|
+
children,
|
|
33
|
+
style,
|
|
34
|
+
clipContent = true
|
|
35
|
+
}: PanelFrameProps) => {
|
|
36
|
+
|
|
37
|
+
const frameWidth = DEFAULT_LAYOUT.FRAME_WIDTH;
|
|
38
|
+
const inkWidth = DEFAULT_LAYOUT.OUTLINE_WIDTH;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
style={{
|
|
43
|
+
width: width as any,
|
|
44
|
+
height: height as any,
|
|
45
|
+
borderWidth: inkWidth,
|
|
46
|
+
borderColor: DEFAULT_LAYOUT.FRAME_INK,
|
|
47
|
+
borderRadius: DEFAULT_LAYOUT.BORR,
|
|
48
|
+
backgroundColor: 'transparent',
|
|
49
|
+
overflow: clipContent ? 'hidden' : 'visible',
|
|
50
|
+
...style
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<View
|
|
54
|
+
style={{
|
|
55
|
+
flex: 1,
|
|
56
|
+
borderWidth: frameWidth,
|
|
57
|
+
borderColor: DEFAULT_LAYOUT.FRAME_WOOD,
|
|
58
|
+
backgroundColor: 'transparent',
|
|
59
|
+
overflow: clipContent ? 'hidden' : 'visible'
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<View
|
|
63
|
+
style={{
|
|
64
|
+
flex: 1,
|
|
65
|
+
backgroundColor: 'transparent',
|
|
66
|
+
borderWidth: inkWidth,
|
|
67
|
+
borderColor: DEFAULT_LAYOUT.FRAME_INK,
|
|
68
|
+
borderRadius: Math.max(0, DEFAULT_LAYOUT.BORR - frameWidth - inkWidth),
|
|
69
|
+
overflow: clipContent ? 'hidden' : 'visible'
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</View>
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ANIVIEW PANEL (Animated component)
|
|
81
|
+
*/
|
|
82
|
+
export interface AniviewPanelProps extends AniviewProps {
|
|
83
|
+
pageId: number | string;
|
|
84
|
+
width: number;
|
|
85
|
+
height: number;
|
|
86
|
+
marginTop?: number;
|
|
87
|
+
marginLeft?: number;
|
|
88
|
+
backgroundColor?: string;
|
|
89
|
+
frames?: Record<string, AniviewFrame> | AniviewFrame[];
|
|
90
|
+
style?: ViewStyle;
|
|
91
|
+
children?: React.ReactNode;
|
|
92
|
+
clipContent?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const AniviewPanel = (props: AniviewPanelProps) => {
|
|
96
|
+
const {
|
|
97
|
+
pageId,
|
|
98
|
+
width,
|
|
99
|
+
height,
|
|
100
|
+
marginTop = 0,
|
|
101
|
+
marginLeft = 0,
|
|
102
|
+
backgroundColor,
|
|
103
|
+
frames,
|
|
104
|
+
children,
|
|
105
|
+
style,
|
|
106
|
+
clipContent = true,
|
|
107
|
+
...rest
|
|
108
|
+
} = props;
|
|
109
|
+
|
|
110
|
+
const hasExternalBg = !!(style as any)?.backgroundColor ||
|
|
111
|
+
(frames && (Array.isArray(frames)
|
|
112
|
+
? frames.some(f => (f.style as any)?.backgroundColor)
|
|
113
|
+
: Object.values(frames).some(f => (f.style as any)?.backgroundColor)));
|
|
114
|
+
|
|
115
|
+
const innerBg = hasExternalBg ? 'transparent' : (backgroundColor || 'transparent');
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Aniview
|
|
119
|
+
pageId={pageId}
|
|
120
|
+
frames={frames as any}
|
|
121
|
+
style={{
|
|
122
|
+
width,
|
|
123
|
+
height,
|
|
124
|
+
marginTop,
|
|
125
|
+
marginLeft,
|
|
126
|
+
backgroundColor: backgroundColor || (style as any)?.backgroundColor,
|
|
127
|
+
...(style as any)
|
|
128
|
+
}}
|
|
129
|
+
{...rest}
|
|
130
|
+
>
|
|
131
|
+
<PanelFrame
|
|
132
|
+
width="100%"
|
|
133
|
+
height="100%"
|
|
134
|
+
backgroundColor={innerBg}
|
|
135
|
+
clipContent={clipContent}
|
|
136
|
+
>
|
|
137
|
+
{children as React.ReactNode}
|
|
138
|
+
</PanelFrame>
|
|
139
|
+
</Aniview>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { StyleSheet, View, Text } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
GestureDetector,
|
|
5
|
+
Gesture,
|
|
6
|
+
} from 'react-native-gesture-handler';
|
|
7
|
+
import Animated, {
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
withSpring,
|
|
11
|
+
runOnJS
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
|
|
14
|
+
import { useAniview } from './useAniview';
|
|
15
|
+
|
|
16
|
+
export default function GestureStressTest() {
|
|
17
|
+
const { lock } = useAniview();
|
|
18
|
+
const translateX = useSharedValue(0);
|
|
19
|
+
const translateY = useSharedValue(0);
|
|
20
|
+
const scale = useSharedValue(1);
|
|
21
|
+
const opacity = useSharedValue(1);
|
|
22
|
+
|
|
23
|
+
// LOGGING UTILITY
|
|
24
|
+
const logGesture = (name: string, state: string, x?: number, y?: number) => {
|
|
25
|
+
console.log(`[GESTURE: ${name}] State: ${state} | X: ${Math.round(x || 0)} Y: ${Math.round(y || 0)}`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const panGesture = Gesture.Pan()
|
|
29
|
+
.onStart((event: any) => {
|
|
30
|
+
runOnJS(lock)(1); // 1 = Horizontal Lock
|
|
31
|
+
runOnJS(logGesture)('PAN', 'START', event.x, event.y);
|
|
32
|
+
opacity.value = 0.8;
|
|
33
|
+
})
|
|
34
|
+
.onUpdate((event: any) => {
|
|
35
|
+
translateX.value = event.translationX;
|
|
36
|
+
translateY.value = event.translationY;
|
|
37
|
+
})
|
|
38
|
+
.onEnd((event: any) => {
|
|
39
|
+
runOnJS(lock)(0); // Unlock
|
|
40
|
+
runOnJS(logGesture)('PAN', 'END', event.x, event.y);
|
|
41
|
+
translateX.value = withSpring(0);
|
|
42
|
+
translateY.value = withSpring(0);
|
|
43
|
+
opacity.value = withSpring(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const tapGesture = Gesture.Tap()
|
|
47
|
+
.numberOfTaps(2)
|
|
48
|
+
.onEnd((event: any) => {
|
|
49
|
+
runOnJS(logGesture)('DOUBLE_TAP', 'END', event.x, event.y);
|
|
50
|
+
scale.value = withSpring(1.5, {}, () => {
|
|
51
|
+
scale.value = withSpring(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const longPressGesture = Gesture.LongPress()
|
|
56
|
+
.onEnd((event: any, success: boolean) => {
|
|
57
|
+
if (success) {
|
|
58
|
+
runOnJS(logGesture)('LONG_PRESS', 'SUCCESS', event.x, event.y);
|
|
59
|
+
opacity.value = withSpring(0.3, {}, () => {
|
|
60
|
+
opacity.value = withSpring(1);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const composed = Gesture.Simultaneous(panGesture, tapGesture, longPressGesture);
|
|
66
|
+
|
|
67
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
68
|
+
transform: [
|
|
69
|
+
{ translateX: translateX.value },
|
|
70
|
+
{ translateY: translateY.value },
|
|
71
|
+
{ scale: scale.value }
|
|
72
|
+
],
|
|
73
|
+
opacity: opacity.value
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={styles.container}>
|
|
78
|
+
<Text style={styles.title}>GESTURE ORCHESTRATION 2.0</Text>
|
|
79
|
+
|
|
80
|
+
<GestureDetector gesture={composed}>
|
|
81
|
+
<Animated.View style={[styles.box, animatedStyle]}>
|
|
82
|
+
<Text style={styles.boxText}>PAN / DOUBLE-TAP / LONG-PRESS</Text>
|
|
83
|
+
<Text style={styles.hint}>Testing Parallel Dispatch (Simultaneous)</Text>
|
|
84
|
+
</Animated.View>
|
|
85
|
+
</GestureDetector>
|
|
86
|
+
|
|
87
|
+
<View style={styles.logIndicator}>
|
|
88
|
+
<Text style={styles.logText}>Check Console for Orchestration Traces</Text>
|
|
89
|
+
</View>
|
|
90
|
+
</View>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
container: {
|
|
96
|
+
padding: 20,
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
},
|
|
100
|
+
title: {
|
|
101
|
+
color: '#fff',
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
fontWeight: 'bold',
|
|
104
|
+
marginBottom: 20,
|
|
105
|
+
opacity: 0.6,
|
|
106
|
+
},
|
|
107
|
+
box: {
|
|
108
|
+
width: 300,
|
|
109
|
+
height: 300,
|
|
110
|
+
backgroundColor: '#1c1c1e',
|
|
111
|
+
borderRadius: 30,
|
|
112
|
+
borderWidth: 1,
|
|
113
|
+
borderColor: '#38383a',
|
|
114
|
+
justifyContent: 'center',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
padding: 20,
|
|
117
|
+
elevation: 10,
|
|
118
|
+
shadowColor: '#000',
|
|
119
|
+
shadowOffset: { width: 0, height: 10 },
|
|
120
|
+
shadowOpacity: 0.5,
|
|
121
|
+
shadowRadius: 15,
|
|
122
|
+
},
|
|
123
|
+
boxText: {
|
|
124
|
+
color: '#0a84ff',
|
|
125
|
+
fontSize: 18,
|
|
126
|
+
fontWeight: '800',
|
|
127
|
+
textAlign: 'center',
|
|
128
|
+
},
|
|
129
|
+
hint: {
|
|
130
|
+
color: '#8e8e93',
|
|
131
|
+
marginTop: 10,
|
|
132
|
+
fontSize: 12,
|
|
133
|
+
},
|
|
134
|
+
logIndicator: {
|
|
135
|
+
marginTop: 30,
|
|
136
|
+
padding: 10,
|
|
137
|
+
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
138
|
+
borderRadius: 10,
|
|
139
|
+
},
|
|
140
|
+
logText: {
|
|
141
|
+
color: '#636366',
|
|
142
|
+
fontSize: 11,
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AniviewLock — Bitmask generation unit tests
|
|
3
|
+
*
|
|
4
|
+
* Enable/disable: ANIVIEW_TEST_LOCK=1 npm test
|
|
5
|
+
* Or: npx jest --testPathPattern=AniviewLock
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AniviewLock } from '../core/AniviewLock';
|
|
9
|
+
|
|
10
|
+
describe('AniviewLock.mask', () => {
|
|
11
|
+
test('empty object returns 0 (no locks)', () => {
|
|
12
|
+
expect(AniviewLock.mask({})).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('left = 1', () => {
|
|
16
|
+
expect(AniviewLock.mask({ left: true })).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('right = 2', () => {
|
|
20
|
+
expect(AniviewLock.mask({ right: true })).toBe(2);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('up = 4', () => {
|
|
24
|
+
expect(AniviewLock.mask({ up: true })).toBe(4);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('down = 8', () => {
|
|
28
|
+
expect(AniviewLock.mask({ down: true })).toBe(8);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('left + right = 3', () => {
|
|
32
|
+
expect(AniviewLock.mask({ left: true, right: true })).toBe(3);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('up + down = 12', () => {
|
|
36
|
+
expect(AniviewLock.mask({ up: true, down: true })).toBe(12);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('all directions = 15', () => {
|
|
40
|
+
expect(AniviewLock.mask({ left: true, right: true, up: true, down: true })).toBe(15);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('false values are ignored', () => {
|
|
44
|
+
expect(AniviewLock.mask({ left: false, right: true })).toBe(2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('undefined values are ignored', () => {
|
|
48
|
+
expect(AniviewLock.mask({ left: undefined, up: true })).toBe(4);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('horizontal only (left + right) blocks horizontal axis', () => {
|
|
52
|
+
const mask = AniviewLock.mask({ left: true, right: true });
|
|
53
|
+
expect(mask & 1).toBeTruthy(); // left blocked
|
|
54
|
+
expect(mask & 2).toBeTruthy(); // right blocked
|
|
55
|
+
expect(mask & 4).toBeFalsy(); // up open
|
|
56
|
+
expect(mask & 8).toBeFalsy(); // down open
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AniviewMath — Pure function unit tests
|
|
3
|
+
*
|
|
4
|
+
* Enable/disable: ANIVIEW_TEST_MATH=1 npm test
|
|
5
|
+
* Or: npx jest --testPathPattern=AniviewMath
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
pageIdToMatrixPos,
|
|
9
|
+
calculateAxisOffset,
|
|
10
|
+
getPageOffset,
|
|
11
|
+
getWorldBounds,
|
|
12
|
+
} from '../core/AniviewMath';
|
|
13
|
+
|
|
14
|
+
// ──────────────────────────────────────────────
|
|
15
|
+
// pageIdToMatrixPos
|
|
16
|
+
// ──────────────────────────────────────────────
|
|
17
|
+
describe('pageIdToMatrixPos', () => {
|
|
18
|
+
const ROW_3 = [[1, 1, 1]]; // 1 row, 3 cols
|
|
19
|
+
const GRID_2x2 = [[1, 1], [1, 1]]; // 2 rows, 2 cols
|
|
20
|
+
const SINGLE = [[1]]; // 1 page
|
|
21
|
+
|
|
22
|
+
test('maps ID 0 to (0,0) in a 1×3 layout', () => {
|
|
23
|
+
expect(pageIdToMatrixPos(0, ROW_3)).toEqual({ r: 0, c: 0 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('maps ID 2 to (0,2) in a 1×3 layout', () => {
|
|
27
|
+
expect(pageIdToMatrixPos(2, ROW_3)).toEqual({ r: 0, c: 2 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('maps ID 3 to (1,0) in a 2×2 layout', () => {
|
|
31
|
+
expect(pageIdToMatrixPos(3, GRID_2x2)).toEqual({ r: 1, c: 1 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('maps ID 2 to (1,0) in a 2×2 layout', () => {
|
|
35
|
+
expect(pageIdToMatrixPos(2, GRID_2x2)).toEqual({ r: 1, c: 0 });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('handles single-page layout', () => {
|
|
39
|
+
expect(pageIdToMatrixPos(0, SINGLE)).toEqual({ r: 0, c: 0 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('returns sentinel for negative ID', () => {
|
|
43
|
+
expect(pageIdToMatrixPos(-1, ROW_3)).toEqual({ r: 9999, c: 9999 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('returns sentinel for out-of-bounds ID', () => {
|
|
47
|
+
expect(pageIdToMatrixPos(5, ROW_3)).toEqual({ r: 9999, c: 9999 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('returns sentinel for ID exactly past max', () => {
|
|
51
|
+
// 1×3 layout → max valid ID is 2
|
|
52
|
+
expect(pageIdToMatrixPos(3, ROW_3)).toEqual({ r: 9999, c: 9999 });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// ──────────────────────────────────────────────
|
|
58
|
+
// calculateAxisOffset
|
|
59
|
+
// ──────────────────────────────────────────────
|
|
60
|
+
describe('calculateAxisOffset', () => {
|
|
61
|
+
const W = 400; // page width/height
|
|
62
|
+
|
|
63
|
+
test('returns 0 when from === to', () => {
|
|
64
|
+
expect(calculateAxisOffset(0, 0, W, [])).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('returns screenWidth for 1 step with no overlap', () => {
|
|
68
|
+
expect(calculateAxisOffset(0, 1, W, [0])).toBe(W);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('returns half screenWidth when overlap is 0.5', () => {
|
|
72
|
+
expect(calculateAxisOffset(0, 1, W, [0.5])).toBe(W * 0.5);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('returns negative offset for reverse direction', () => {
|
|
76
|
+
expect(calculateAxisOffset(1, 0, W, [0])).toBe(-W);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('accumulates correctly over 3 steps with no overlap', () => {
|
|
80
|
+
expect(calculateAxisOffset(0, 3, W, [0, 0, 0])).toBe(W * 3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('accumulates with mixed overlaps (layout)', () => {
|
|
84
|
+
// uses cols: [0.5, 0.4] for 3 columns
|
|
85
|
+
// Step 0→1: W * (1 - 0.5) = 200
|
|
86
|
+
// Step 1→2: W * (1 - 0.4) = 240
|
|
87
|
+
// Total: 440
|
|
88
|
+
expect(calculateAxisOffset(0, 2, W, [0.5, 0.4])).toBe(440);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('handles missing overlaps (treated as 0)', () => {
|
|
92
|
+
// Only 1 overlap value but crossing 2 gaps
|
|
93
|
+
// Step 0→1: W * (1 - 0.3) = 280
|
|
94
|
+
// Step 1→2: W * (1 - 0) = 400 (missing overlap → 0)
|
|
95
|
+
expect(calculateAxisOffset(0, 2, W, [0.3])).toBe(680);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('single step with full overlap (1.0) returns 0', () => {
|
|
99
|
+
expect(calculateAxisOffset(0, 1, W, [1.0])).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
// ──────────────────────────────────────────────
|
|
105
|
+
// getPageOffset
|
|
106
|
+
// ──────────────────────────────────────────────
|
|
107
|
+
describe('getPageOffset', () => {
|
|
108
|
+
const DIMS = { width: 400, height: 800 };
|
|
109
|
+
|
|
110
|
+
test('origin page returns (0, 0)', () => {
|
|
111
|
+
const layout = [[1, 1, 1]];
|
|
112
|
+
expect(getPageOffset(1, layout, DIMS, 1, [], [0, 0])).toEqual({ x: 0, y: 0 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('adjacent page with no overlap returns (screenWidth, 0)', () => {
|
|
116
|
+
const layout = [[1, 1]];
|
|
117
|
+
expect(getPageOffset(1, layout, DIMS, 0, [], [0])).toEqual({ x: 400, y: 0 });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('page below origin returns (0, screenHeight)', () => {
|
|
121
|
+
const layout = [[1], [1]];
|
|
122
|
+
expect(getPageOffset(1, layout, DIMS, 0, [0], [])).toEqual({ x: 0, y: 800 });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('page to left of origin returns negative x', () => {
|
|
126
|
+
const layout = [[1, 1, 1]];
|
|
127
|
+
expect(getPageOffset(0, layout, DIMS, 1, [], [0, 0])).toEqual({ x: -400, y: 0 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('layout: CANVAS from ROOM (overlap 0.5)', () => {
|
|
131
|
+
// Layout: [[1,1,1]], origin = 1 (ROOM), cols overlap = [0.5, 0.4]
|
|
132
|
+
const layout = [[1, 1, 1]];
|
|
133
|
+
const offset = getPageOffset(0, layout, DIMS, 1, [], [0.5, 0.4]);
|
|
134
|
+
// CANVAS (col 0) to ROOM (col 1): -W * (1 - 0.5) = -200
|
|
135
|
+
expect(offset).toEqual({ x: -200, y: 0 });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('layout: DRAWERS from ROOM (overlap 0.4)', () => {
|
|
139
|
+
const layout = [[1, 1, 1]];
|
|
140
|
+
const offset = getPageOffset(2, layout, DIMS, 1, [], [0.5, 0.4]);
|
|
141
|
+
// DRAWERS (col 2) to ROOM (col 1): W * (1 - 0.4) = 240
|
|
142
|
+
expect(offset).toEqual({ x: 240, y: 0 });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('2×2 grid: diagonal page offset', () => {
|
|
146
|
+
const layout = [[1, 1], [1, 1]];
|
|
147
|
+
// Page 3 is (1,1), origin 0 is (0,0)
|
|
148
|
+
const offset = getPageOffset(3, layout, DIMS, 0, [0], [0]);
|
|
149
|
+
expect(offset).toEqual({ x: 400, y: 800 });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// ──────────────────────────────────────────────
|
|
155
|
+
// getWorldBounds
|
|
156
|
+
// ──────────────────────────────────────────────
|
|
157
|
+
describe('getWorldBounds', () => {
|
|
158
|
+
const DIMS = { width: 400, height: 800 };
|
|
159
|
+
|
|
160
|
+
test('single page returns all zeros', () => {
|
|
161
|
+
const bounds = getWorldBounds([0], [[1]], DIMS, 0, [], []);
|
|
162
|
+
expect(bounds).toEqual({ minX: 0, maxX: 0, minY: 0, maxY: 0 });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('empty pages array returns all zeros', () => {
|
|
166
|
+
const bounds = getWorldBounds([], [[1]], DIMS, 0, [], []);
|
|
167
|
+
expect(bounds).toEqual({ minX: 0, maxX: 0, minY: 0, maxY: 0 });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('3 horizontal pages, no overlap, origin at 0', () => {
|
|
171
|
+
const layout = [[1, 1, 1]];
|
|
172
|
+
const bounds = getWorldBounds([0, 1, 2], layout, DIMS, 0, [], [0, 0]);
|
|
173
|
+
expect(bounds.minX).toBe(0);
|
|
174
|
+
expect(bounds.maxX).toBe(800); // 2 × 400
|
|
175
|
+
expect(bounds.minY).toBe(0);
|
|
176
|
+
expect(bounds.maxY).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('3 horizontal pages, origin at center', () => {
|
|
180
|
+
const layout = [[1, 1, 1]];
|
|
181
|
+
const bounds = getWorldBounds([0, 1, 2], layout, DIMS, 1, [], [0, 0]);
|
|
182
|
+
expect(bounds.minX).toBe(-400);
|
|
183
|
+
expect(bounds.maxX).toBe(400);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('layout bounds (overlaps, origin at ROOM)', () => {
|
|
187
|
+
const layout = [[1, 1, 1]];
|
|
188
|
+
const bounds = getWorldBounds([0, 1, 2], layout, DIMS, 1, [], [0.5, 0.4]);
|
|
189
|
+
// CANVAS: -200, ROOM: 0, DRAWERS: +240
|
|
190
|
+
expect(bounds.minX).toBe(-200);
|
|
191
|
+
expect(bounds.maxX).toBe(240);
|
|
192
|
+
expect(bounds.minY).toBe(0);
|
|
193
|
+
expect(bounds.maxY).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('2×2 grid spans both axes', () => {
|
|
197
|
+
const layout = [[1, 1], [1, 1]];
|
|
198
|
+
const bounds = getWorldBounds([0, 1, 2, 3], layout, DIMS, 0, [0], [0]);
|
|
199
|
+
expect(bounds.minX).toBe(0);
|
|
200
|
+
expect(bounds.maxX).toBe(400);
|
|
201
|
+
expect(bounds.minY).toBe(0);
|
|
202
|
+
expect(bounds.maxY).toBe(800);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('overlaps reduce bounds magnitude', () => {
|
|
206
|
+
const layout = [[1, 1]];
|
|
207
|
+
const noOverlap = getWorldBounds([0, 1], layout, DIMS, 0, [], [0]);
|
|
208
|
+
const withOverlap = getWorldBounds([0, 1], layout, DIMS, 0, [], [0.5]);
|
|
209
|
+
expect(withOverlap.maxX).toBe(noOverlap.maxX * 0.5);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as renderer from 'react-test-renderer';
|
|
3
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
4
|
+
|
|
5
|
+
// Mock Reanimated
|
|
6
|
+
jest.mock('react-native-reanimated', () => {
|
|
7
|
+
const Reanimated = require('react-native-reanimated/mock');
|
|
8
|
+
return Reanimated;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Mock Aniview component to verify layout logic
|
|
12
|
+
jest.mock('../Aniview', () => {
|
|
13
|
+
const { View } = require('react-native');
|
|
14
|
+
return jest.fn((props: any) => {
|
|
15
|
+
return <View testID="aniview-mock" style={props.style}>{props.children}</View>;
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
import Aniview from '../Aniview';
|
|
20
|
+
|
|
21
|
+
describe('Aniview Snapshot Suite', () => {
|
|
22
|
+
|
|
23
|
+
const TestLayout = ({ outerFlex, innerFlex, textPos }: any) => (
|
|
24
|
+
<Aniview pageId={1} style={[styles.outer, outerFlex]}>
|
|
25
|
+
<View style={[styles.inner, innerFlex]}>
|
|
26
|
+
<Text style={[styles.text, textPos]}>Main Content</Text>
|
|
27
|
+
<Aniview pageId={2} style={styles.nested}>
|
|
28
|
+
<Text>Nested Item</Text>
|
|
29
|
+
</Aniview>
|
|
30
|
+
</View>
|
|
31
|
+
</Aniview>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
it('Scenario 1: Default Center Aligned', () => {
|
|
35
|
+
const tree = renderer.create(
|
|
36
|
+
<TestLayout
|
|
37
|
+
outerFlex={{ justifyContent: 'center', alignItems: 'center' }}
|
|
38
|
+
innerFlex={{ padding: 20, backgroundColor: '#eee' }}
|
|
39
|
+
textPos={{ fontSize: 20 }}
|
|
40
|
+
/>
|
|
41
|
+
).toJSON();
|
|
42
|
+
console.log('TREE 1:', JSON.stringify(tree)); // DEBUG
|
|
43
|
+
expect(tree).toMatchSnapshot();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Scenario 2: Space-Between Flex with Absolute Text', () => {
|
|
47
|
+
const tree = renderer.create(
|
|
48
|
+
<TestLayout
|
|
49
|
+
outerFlex={{ justifyContent: 'space-between', flexDirection: 'row' }}
|
|
50
|
+
innerFlex={{ flex: 1, alignItems: 'flex-end' }}
|
|
51
|
+
textPos={{ position: 'absolute', top: 10, right: 10, color: 'blue' }}
|
|
52
|
+
/>
|
|
53
|
+
).toJSON();
|
|
54
|
+
expect(tree).toMatchSnapshot();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('Scenario 3: Column Reverse with Margin Auto', () => {
|
|
58
|
+
const tree = renderer.create(
|
|
59
|
+
<TestLayout
|
|
60
|
+
outerFlex={{ flexDirection: 'column-reverse' }}
|
|
61
|
+
innerFlex={{ marginTop: 'auto', marginBottom: 20 }}
|
|
62
|
+
textPos={{ textAlign: 'center', fontWeight: 'bold' }}
|
|
63
|
+
/>
|
|
64
|
+
).toJSON();
|
|
65
|
+
expect(tree).toMatchSnapshot();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
outer: {
|
|
71
|
+
width: '100%',
|
|
72
|
+
height: '100%',
|
|
73
|
+
},
|
|
74
|
+
inner: {
|
|
75
|
+
borderRadius: 10,
|
|
76
|
+
},
|
|
77
|
+
text: {
|
|
78
|
+
fontFamily: 'System',
|
|
79
|
+
},
|
|
80
|
+
nested: {
|
|
81
|
+
width: 100,
|
|
82
|
+
height: 50,
|
|
83
|
+
backgroundColor: 'rgba(0,0,0,0.1)',
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`Aniview Snapshot Suite Scenario 1: Default Center Aligned 1`] = `null`;
|
|
4
|
+
|
|
5
|
+
exports[`Aniview Snapshot Suite Scenario 2: Space-Between Flex with Absolute Text 1`] = `null`;
|
|
6
|
+
|
|
7
|
+
exports[`Aniview Snapshot Suite Scenario 3: Column Reverse with Margin Auto 1`] = `null`;
|