From 7be39f598cf093d9d12c1ab8d0fa46bdd93ba8cb Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 17 May 2024 15:15:30 +0200 Subject: [PATCH 01/19] Unify white-ish backgrounds, reorder examples --- example/App.tsx | 64 +++++++++++----------- example/src/ListWithHeader/Header.tsx | 5 +- example/src/basic/bouncing/index.tsx | 1 - example/src/basic/panResponder/index.tsx | 1 - example/src/common.tsx | 4 ++ example/src/empty/EmptyExample.tsx | 4 +- example/src/new_api/bottom_sheet/index.tsx | 1 - example/src/new_api/chat_heads/index.tsx | 1 - example/src/release_tests/combo/index.tsx | 1 - example/src/showcase/chatHeads/index.tsx | 1 - 10 files changed, 41 insertions(+), 42 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index 376256ee73..16552e1e7f 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -65,6 +65,7 @@ import VelocityTest from './src/new_api/velocityTest'; import EmptyExample from './src/empty/EmptyExample'; import RectButtonBorders from './src/release_tests/rectButton'; import { ListWithHeader } from './src/ListWithHeader'; +import { COLORS } from 'src/common'; interface Example { name: string; @@ -80,6 +81,31 @@ const EXAMPLES: ExamplesSection[] = [ sectionTitle: 'Empty', data: [{ name: 'Empty Example', component: EmptyExample }], }, + { + sectionTitle: 'New api', + data: [ + { name: 'Ball', component: ReanimatedSimple }, + { name: 'Velocity test', component: VelocityTest }, + { name: 'Camera', component: Camera }, + { name: 'Transformations', component: Transformations }, + { name: 'Overlap parents', component: OverlapParents }, + { name: 'Overlap siblings', component: OverlapSiblings }, + { name: 'Bottom Sheet', component: BottomSheetNewApi }, + { name: 'Calculator', component: Calculator }, + { name: 'Chat Heads', component: ChatHeadsNewApi }, + { name: 'Drag and drop', component: DragNDrop }, + { name: 'Hover', component: Hover }, + { name: 'Hoverable icons', component: HoverableIcons }, + { + name: 'Horizontal Drawer (Reanimated 2 & RNGH 2)', + component: BetterHorizontalDrawer, + }, + { + name: 'Manual gestures', + component: ManualGestures, + }, + ], + }, { sectionTitle: 'Basic examples', data: [ @@ -139,34 +165,6 @@ const EXAMPLES: ExamplesSection[] = [ { name: 'RectButton (borders)', component: RectButtonBorders }, ], }, - { - sectionTitle: 'New api', - data: [ - { - name: 'Simple interaction with Reanimated', - component: ReanimatedSimple, - }, - { name: 'Hover', component: Hover }, - { name: 'Hoverable icons', component: HoverableIcons }, - { name: 'Camera', component: Camera }, - { name: 'Velocity test', component: VelocityTest }, - { name: 'Transformations', component: Transformations }, - { name: 'Overlap parents', component: OverlapParents }, - { name: 'Overlap siblings', component: OverlapSiblings }, - { name: 'Calculator', component: Calculator }, - { name: 'Bottom Sheet', component: BottomSheetNewApi }, - { name: 'Chat Heads', component: ChatHeadsNewApi }, - { name: 'Drag and drop', component: DragNDrop }, - { - name: 'Horizontal Drawer (Reanimated 2 & RNGH 2)', - component: BetterHorizontalDrawer, - }, - { - name: 'Manual gestures', - component: ManualGestures, - }, - ], - }, ]; type RootStackParamList = { @@ -188,9 +186,10 @@ export default function App() { cardStyle: { // It's important to set height for the screen, without it scroll doesn't work on web platform. height: Dimensions.get('window').height, + backgroundColor: '#fff', }, headerStyle: { - backgroundColor: '#f8f9ff', + backgroundColor: COLORS.offWhite, }, }}> { } } +export const COLORS = { + offWhite: '#f8f9ff', +}; + const LOREM_IPSUM = ` Curabitur accumsan sit amet massa quis cursus. Fusce sollicitudin nunc nisl, quis efficitur quam tristique eget. Ut non erat molestie, ullamcorper turpis nec, euismod neque. Praesent aliquam risus ultricies, cursus mi consectetur, bibendum lorem. Nunc eleifend consectetur metus quis pulvinar. In vitae lacus eu nibh tincidunt sagittis ut id lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque sagittis mauris rhoncus, maximus justo in, consequat dolor. Pellentesque ornare laoreet est vulputate vestibulum. Aliquam sit amet metus lorem. diff --git a/example/src/empty/EmptyExample.tsx b/example/src/empty/EmptyExample.tsx index 28aa248fab..900460d6a6 100644 --- a/example/src/empty/EmptyExample.tsx +++ b/example/src/empty/EmptyExample.tsx @@ -4,7 +4,8 @@ import { StyleSheet, Text, View } from 'react-native'; export default function EmptyExample() { return ( - Hello World! + 😞 + It's so empty here ); } @@ -14,6 +15,5 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#F5FCFF', }, }); diff --git a/example/src/new_api/bottom_sheet/index.tsx b/example/src/new_api/bottom_sheet/index.tsx index a6ed9062ff..1218963588 100644 --- a/example/src/new_api/bottom_sheet/index.tsx +++ b/example/src/new_api/bottom_sheet/index.tsx @@ -142,7 +142,6 @@ function Example() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#eef', }, header: { height: HEADER_HEIGTH, diff --git a/example/src/new_api/chat_heads/index.tsx b/example/src/new_api/chat_heads/index.tsx index 502ccda4a9..1f59b90358 100644 --- a/example/src/new_api/chat_heads/index.tsx +++ b/example/src/new_api/chat_heads/index.tsx @@ -200,7 +200,6 @@ const IMAGE_SIZE = 80; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#F5FCFF', }, box: { position: 'absolute', diff --git a/example/src/release_tests/combo/index.tsx b/example/src/release_tests/combo/index.tsx index 4bd98010ae..bcb782087e 100644 --- a/example/src/release_tests/combo/index.tsx +++ b/example/src/release_tests/combo/index.tsx @@ -225,7 +225,6 @@ export const ComboWithRNScroll = () => ; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#F5FCFF', }, rectButton: { flex: 1, diff --git a/example/src/showcase/chatHeads/index.tsx b/example/src/showcase/chatHeads/index.tsx index 523ae4cbb2..0292e9e4cf 100644 --- a/example/src/showcase/chatHeads/index.tsx +++ b/example/src/showcase/chatHeads/index.tsx @@ -213,7 +213,6 @@ const BOX_SIZE = 80; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#F5FCFF', }, box: { position: 'absolute', From f5b2eef24687720f77e621a27e3e5d66aec73b2e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 17 May 2024 16:00:54 +0200 Subject: [PATCH 02/19] Unify examples --- example/App.tsx | 10 +- example/src/new_api/overlap/index.tsx | 132 ++++++++++++++++++ example/src/new_api/overlap_parent/index.tsx | 56 -------- .../src/new_api/overlap_siblings/index.tsx | 58 -------- example/src/new_api/reanimated/index.tsx | 68 --------- example/src/new_api/velocityTest/index.tsx | 19 ++- 6 files changed, 151 insertions(+), 192 deletions(-) create mode 100644 example/src/new_api/overlap/index.tsx delete mode 100644 example/src/new_api/overlap_parent/index.tsx delete mode 100644 example/src/new_api/overlap_siblings/index.tsx delete mode 100644 example/src/new_api/reanimated/index.tsx diff --git a/example/App.tsx b/example/App.tsx index 16552e1e7f..09a8be0a43 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -47,11 +47,9 @@ import PagerAndDrawer from './src/basic/pagerAndDrawer'; import ForceTouch from './src/basic/forcetouch'; import Fling from './src/basic/fling'; -import ReanimatedSimple from './src/new_api/reanimated'; import Camera from './src/new_api/camera'; import Transformations from './src/new_api/transformations'; -import OverlapParents from './src/new_api/overlap_parent'; -import OverlapSiblings from './src/new_api/overlap_siblings'; +import Overlap from './src/new_api/overlap'; import Calculator from './src/new_api/calculator'; import BottomSheetNewApi from './src/new_api/bottom_sheet'; import ChatHeadsNewApi from './src/new_api/chat_heads'; @@ -84,12 +82,10 @@ const EXAMPLES: ExamplesSection[] = [ { sectionTitle: 'New api', data: [ - { name: 'Ball', component: ReanimatedSimple }, - { name: 'Velocity test', component: VelocityTest }, + { name: 'Ball with velocity', component: VelocityTest }, { name: 'Camera', component: Camera }, { name: 'Transformations', component: Transformations }, - { name: 'Overlap parents', component: OverlapParents }, - { name: 'Overlap siblings', component: OverlapSiblings }, + { name: 'Overlap', component: Overlap }, { name: 'Bottom Sheet', component: BottomSheetNewApi }, { name: 'Calculator', component: Calculator }, { name: 'Chat Heads', component: ChatHeadsNewApi }, diff --git a/example/src/new_api/overlap/index.tsx b/example/src/new_api/overlap/index.tsx new file mode 100644 index 0000000000..35684e0c2b --- /dev/null +++ b/example/src/new_api/overlap/index.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { StyleSheet, View, Text } from 'react-native'; +import { GestureDetector, Gesture } from 'react-native-gesture-handler'; + +function Box(props: { + color: string; + overlap?: boolean; + children?: React.ReactNode; + elevated: boolean; +}) { + return ( + + {props.children} + + ); +} + +function OverlapSiblings() { + const [elevated, setElevated] = React.useState(''); + + const tapRed = Gesture.Tap() + .onEnd((_e, success) => { + if (success) { + setElevated('red'); + } + }) + .runOnJS(true); + + const tapGreen = Gesture.Tap() + .onEnd((_e, success) => { + if (success) { + setElevated('green'); + } + }) + .runOnJS(true); + + return ( + + Overlap Siblings + + + + + + + + + + ); +} + +function OverlapParents() { + const [elevated, setElevated] = React.useState(''); + + const tapRed = Gesture.Tap() + .onEnd((e, success) => { + if (success) { + setElevated('red'); + } + }) + .runOnJS(true); + + const tapGreen = Gesture.Tap() + .onEnd((e, success) => { + if (success) { + setElevated('green'); + } + }) + .runOnJS(true); + return ( + + Overlap Child + + + + + + + + + + + ); +} + +export default function Example() { + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + row: { + padding: 30, + alignItems: 'center', + height: 225, + marginTop: 48, + }, + box: { + width: 150, + height: 150, + }, + overlap: { + position: 'absolute', + left: 75, + top: 75, + }, + text: { + fontSize: 24, + margin: 4, + }, + elevated: { + zIndex: 10, + elevation: 16, + shadowColor: 'black', + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.5, + shadowRadius: 8, + }, +}); diff --git a/example/src/new_api/overlap_parent/index.tsx b/example/src/new_api/overlap_parent/index.tsx deleted file mode 100644 index a6ce40c57c..0000000000 --- a/example/src/new_api/overlap_parent/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { GestureDetector, Gesture } from 'react-native-gesture-handler'; - -function Box(props: { - color: string; - overlap?: boolean; - children?: React.ReactNode; -}) { - const gesture = Gesture.Tap().onEnd((_e, success) => { - if (success) { - console.log(props.color); - } - }); - - return ( - - - {props.children} - - - ); -} - -export default function Example() { - return ( - - - - - - ); -} - -const styles = StyleSheet.create({ - home: { - width: '100%', - height: '100%', - alignSelf: 'center', - backgroundColor: 'plum', - }, - box: { - width: 150, - height: 150, - }, - overlap: { - position: 'absolute', - left: 75, - top: 75, - }, -}); diff --git a/example/src/new_api/overlap_siblings/index.tsx b/example/src/new_api/overlap_siblings/index.tsx deleted file mode 100644 index c364912701..0000000000 --- a/example/src/new_api/overlap_siblings/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { GestureDetector, Gesture } from 'react-native-gesture-handler'; - -function Box(props: { color: string; overlap?: boolean }) { - return ( - - ); -} - -export default function Example() { - const tapRed = Gesture.Tap().onEnd((_e, success) => { - if (success) { - console.log('red'); - } - }); - - const tapGreen = Gesture.Tap().onEnd((_e, success) => { - if (success) { - console.log('green'); - } - }); - - return ( - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - home: { - width: '100%', - height: '100%', - alignSelf: 'center', - backgroundColor: 'plum', - }, - box: { - width: 150, - height: 150, - }, - overlap: { - position: 'absolute', - left: 75, - top: 75, - }, -}); diff --git a/example/src/new_api/reanimated/index.tsx b/example/src/new_api/reanimated/index.tsx deleted file mode 100644 index dd3773729d..0000000000 --- a/example/src/new_api/reanimated/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { GestureDetector, Gesture } from 'react-native-gesture-handler'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withSpring, -} from 'react-native-reanimated'; - -function Ball() { - const isPressed = useSharedValue(false); - const offset = useSharedValue({ x: 0, y: 0 }); - - const animatedStyles = useAnimatedStyle(() => { - return { - transform: [ - { translateX: offset.value.x }, - { translateY: offset.value.y }, - { scale: withSpring(isPressed.value ? 1.2 : 1) }, - ], - backgroundColor: isPressed.value ? 'yellow' : 'blue', - }; - }); - - const gesture = Gesture.Pan() - .onBegin(() => { - 'worklet'; - isPressed.value = true; - }) - .onChange((e) => { - 'worklet'; - offset.value = { - x: e.changeX + offset.value.x, - y: e.changeY + offset.value.y, - }; - }) - .onFinalize(() => { - 'worklet'; - isPressed.value = false; - }); - - return ( - - - - ); -} - -export default function Example() { - return ( - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - ball: { - width: 100, - height: 100, - borderRadius: 100, - backgroundColor: 'blue', - alignSelf: 'center', - }, -}); diff --git a/example/src/new_api/velocityTest/index.tsx b/example/src/new_api/velocityTest/index.tsx index 719daa0680..687708d732 100644 --- a/example/src/new_api/velocityTest/index.tsx +++ b/example/src/new_api/velocityTest/index.tsx @@ -16,13 +16,18 @@ export default function App() { const aref = useAnimatedRef(); const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); + const isPressed = useSharedValue(false); const pan = Gesture.Pan() + .onBegin(() => { + isPressed.value = true; + }) .onChange((event) => { offsetX.value += event.changeX; offsetY.value += event.changeY; }) .onFinalize((event) => { + isPressed.value = false; // If we can't get view size, just ignore it. Half of the view will be // able to go outside the screen const size = measure(aref) ?? { width: 0, height: 0 }; @@ -30,6 +35,8 @@ export default function App() { offsetX.value = withDecay({ velocity: event.velocityX, clamp: [-size.width / 2 + BOX_SIZE / 2, size.width / 2 - BOX_SIZE / 2], + rubberBandEffect: true, + rubberBandFactor: 0.75, }); offsetY.value = withDecay({ @@ -38,11 +45,18 @@ export default function App() { -size.height / 2 + BOX_SIZE / 2, size.height / 2 - BOX_SIZE / 2, ], + rubberBandEffect: true, + rubberBandFactor: 0.75, }); }); const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ translateX: offsetX.value }, { translateY: offsetY.value }], + transform: [ + { translateX: offsetX.value }, + { translateY: offsetY.value }, + { scale: isPressed.value ? 1.2 : 1 }, + ], + backgroundColor: isPressed.value ? '#FFD61E' : '#001A72', })); return ( @@ -64,8 +78,7 @@ const styles = StyleSheet.create({ box: { width: BOX_SIZE, height: BOX_SIZE, - backgroundColor: '#001A72', - borderRadius: 20, + borderRadius: BOX_SIZE / 2, //@ts-expect-error cursor: 'grab', }, From 767c7244dd6053d345b5b528949086abf008cbe3 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 20 May 2024 08:38:36 +0200 Subject: [PATCH 03/19] Ignore unused variable --- example/src/new_api/overlap/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/new_api/overlap/index.tsx b/example/src/new_api/overlap/index.tsx index 35684e0c2b..ef85c78e54 100644 --- a/example/src/new_api/overlap/index.tsx +++ b/example/src/new_api/overlap/index.tsx @@ -59,7 +59,7 @@ function OverlapParents() { const [elevated, setElevated] = React.useState(''); const tapRed = Gesture.Tap() - .onEnd((e, success) => { + .onEnd((_e, success) => { if (success) { setElevated('red'); } @@ -67,7 +67,7 @@ function OverlapParents() { .runOnJS(true); const tapGreen = Gesture.Tap() - .onEnd((e, success) => { + .onEnd((_e, success) => { if (success) { setElevated('green'); } From a274018b0d61cd2e10538129302ffaf6c2cae3c3 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 20 May 2024 15:00:58 +0200 Subject: [PATCH 04/19] Update transformations --- example/src/new_api/transformations/index.tsx | 244 ++++++++++++++++-- 1 file changed, 223 insertions(+), 21 deletions(-) diff --git a/example/src/new_api/transformations/index.tsx b/example/src/new_api/transformations/index.tsx index c4c2d21339..753e5a5c4c 100644 --- a/example/src/new_api/transformations/index.tsx +++ b/example/src/new_api/transformations/index.tsx @@ -1,44 +1,248 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { + measure, + useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; import { GestureDetector, Gesture } from 'react-native-gesture-handler'; +function identity4() { + 'worklet'; + return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; +} + +function multiply4(a: number[], b: number[]) { + 'worklet'; + return [ + a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12], + a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13], + a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14], + a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15], + a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12], + a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13], + a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14], + a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15], + a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12], + a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13], + a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14], + a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15], + a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12], + a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13], + a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14], + a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15], + ]; +} + +function scale4(sx: number, sy: number, sz: number) { + 'worklet'; + return [sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1]; +} + +function translate4(tx: number, ty: number, tz: number) { + 'worklet'; + return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1]; +} + +function rotate4(rad: number, x: number, y: number, z: number) { + 'worklet'; + const len = Math.hypot(x, y, z); + const c = Math.cos(rad); + const s = Math.sin(rad); + const t = 1 - c; + x /= len; + y /= len; + z /= len; + return [ + t * x * x + c, + t * x * y - s * z, + t * x * z + s * y, + 0, + t * x * y + s * z, + t * y * y + c, + t * y * z - s * x, + 0, + t * x * z - s * y, + t * y * z + s * x, + t * z * z + c, + 0, + 0, + 0, + 0, + 1, + ]; +} + +function invert2(m: number[]) { + 'worklet'; + const a = m[0]; + const b = m[1]; + const c = m[2]; + const d = m[3]; + const det = a * d - b * c; + + return [d / det, -b / det, -c / det, a / det]; +} + +function toTransformedCoords( + point: { x: number; y: number }, + matrix: number[] +) { + 'worklet'; + const m2 = [matrix[0], matrix[1], matrix[4], matrix[5]]; + const inv = invert2(m2); + const x = point.x; + const y = point.y; + const newX = inv[0] * x + inv[2] * y; + const newY = inv[1] * x + inv[3] * y; + + return { x: newX, y: newY }; +} + +function createMatrix( + translation: { x: number; y: number }, + scale: number, + rotation: number, + origin: { x: number; y: number } +) { + 'worklet'; + let matrix = identity4(); + + if (scale !== 1) { + matrix = multiply4(matrix, translate4(origin.x, origin.y, 0)); + matrix = multiply4(matrix, scale4(scale, scale, 1)); + matrix = multiply4(matrix, translate4(-origin.x, -origin.y, 0)); + } + if (rotation !== 0) { + matrix = multiply4(matrix, translate4(origin.x, origin.y, 0)); + matrix = multiply4(matrix, rotate4(-rotation, 0, 0, 1)); + matrix = multiply4(matrix, translate4(-origin.x, -origin.y, 0)); + } + + if (translation.x !== 0 || translation.y !== 0) { + matrix = multiply4(matrix, translate4(translation.x, translation.y, 0)); + } + + return matrix; +} + +function applyTransformations( + translation: { x: number; y: number }, + scale: number, + rotation: number, + origin: { x: number; y: number }, + matrix: number[] +) { + 'worklet'; + const translationInViewCoords = toTransformedCoords(translation, matrix); + const transform = createMatrix( + translationInViewCoords, + scale, + rotation, + origin + ); + return multiply4(transform, matrix); +} + function Photo() { - const translationX = useSharedValue(0); - const translationY = useSharedValue(0); + const animatedRef = useAnimatedRef(); + const translation = useSharedValue({ x: 0, y: 0 }); + const origin = useSharedValue({ x: 0, y: 0 }); const scale = useSharedValue(1); const rotation = useSharedValue(0); + const transform = useSharedValue(identity4()); + const style = useAnimatedStyle(() => { + const matrix = applyTransformations( + translation.value, + scale.value, + rotation.value, + origin.value, + transform.value + ); + return { - transform: [ - { translateX: translationX.value }, - { translateY: translationY.value }, - { scale: scale.value }, - { rotateZ: `${rotation.value}rad` }, - ], + transform: [{ matrix: matrix }], }; }); - const rotationGesture = Gesture.Rotation().onChange((e) => { - 'worklet'; - rotation.value += e.rotationChange; - }); + const rotationGesture = Gesture.Rotation() + .onStart((e) => { + const measured = measure(animatedRef)!; + origin.value = { + x: -(e.anchorX - measured.width / 2), + y: -(e.anchorY - measured.height / 2), + }; + }) + .onChange((e) => { + 'worklet'; + rotation.value += e.rotationChange; + }) + .onEnd(() => { + 'worklet'; + transform.value = applyTransformations( + translation.value, + scale.value, + rotation.value, + origin.value, + transform.value + ); - const scaleGesture = Gesture.Pinch().onChange((e) => { - 'worklet'; - scale.value *= e.scaleChange; - }); + rotation.value = 0; + translation.value = { x: 0, y: 0 }; + scale.value = 1; + }); + + const scaleGesture = Gesture.Pinch() + .onStart((e) => { + const measured = measure(animatedRef)!; + origin.value = { + x: -(e.focalX - measured.width / 2), + y: -(e.focalY - measured.height / 2), + }; + }) + .onChange((e) => { + 'worklet'; + scale.value *= e.scaleChange; + }) + .onEnd(() => { + 'worklet'; + transform.value = applyTransformations( + translation.value, + scale.value, + rotation.value, + origin.value, + transform.value + ); + rotation.value = 0; + translation.value = { x: 0, y: 0 }; + scale.value = 1; + }); const panGesture = Gesture.Pan() .averageTouches(true) .onChange((e) => { 'worklet'; - translationX.value += e.changeX; - translationY.value += e.changeY; + translation.value = { + x: translation.value.x + e.changeX, + y: translation.value.y + e.changeY, + }; + }) + .onEnd(() => { + 'worklet'; + transform.value = applyTransformations( + translation.value, + scale.value, + rotation.value, + origin.value, + transform.value + ); + + rotation.value = 0; + translation.value = { x: 0, y: 0 }; + scale.value = 1; }); const doubleTapGesture = Gesture.Tap() @@ -59,7 +263,7 @@ function Photo() { return ( - + ); } @@ -77,12 +281,10 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', alignSelf: 'center', - backgroundColor: 'plum', }, button: { width: 200, height: 200, backgroundColor: 'green', - alignSelf: 'center', }, }); From 9dcd1311a5802f16ff4419439683baa72211cc1a Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 20 May 2024 15:30:09 +0200 Subject: [PATCH 05/19] Fix logic --- example/src/new_api/transformations/index.tsx | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/example/src/new_api/transformations/index.tsx b/example/src/new_api/transformations/index.tsx index 753e5a5c4c..141256241e 100644 --- a/example/src/new_api/transformations/index.tsx +++ b/example/src/new_api/transformations/index.tsx @@ -1,12 +1,11 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { - measure, - useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; import { GestureDetector, Gesture } from 'react-native-gesture-handler'; +import { useState } from 'react'; function identity4() { 'worklet'; @@ -146,11 +145,13 @@ function applyTransformations( } function Photo() { - const animatedRef = useAnimatedRef(); + const [size, setSize] = useState({ width: 0, height: 0 }); const translation = useSharedValue({ x: 0, y: 0 }); const origin = useSharedValue({ x: 0, y: 0 }); const scale = useSharedValue(1); const rotation = useSharedValue(0); + const isRotating = useSharedValue(false); + const isScaling = useSharedValue(false); const transform = useSharedValue(identity4()); @@ -170,11 +171,13 @@ function Photo() { const rotationGesture = Gesture.Rotation() .onStart((e) => { - const measured = measure(animatedRef)!; - origin.value = { - x: -(e.anchorX - measured.width / 2), - y: -(e.anchorY - measured.height / 2), - }; + if (!isRotating.value && !isScaling.value) { + origin.value = { + x: -(e.anchorX - size.width / 2), + y: -(e.anchorY - size.height / 2), + }; + } + isRotating.value = true; }) .onChange((e) => { 'worklet'; @@ -193,15 +196,18 @@ function Photo() { rotation.value = 0; translation.value = { x: 0, y: 0 }; scale.value = 1; + isRotating.value = false; }); const scaleGesture = Gesture.Pinch() .onStart((e) => { - const measured = measure(animatedRef)!; - origin.value = { - x: -(e.focalX - measured.width / 2), - y: -(e.focalY - measured.height / 2), - }; + if (!isRotating.value && !isScaling.value) { + origin.value = { + x: -(e.focalX - size.width / 2), + y: -(e.focalY - size.height / 2), + }; + } + isScaling.value = true; }) .onChange((e) => { 'worklet'; @@ -219,6 +225,7 @@ function Photo() { rotation.value = 0; translation.value = { x: 0, y: 0 }; scale.value = 1; + isScaling.value = false; }); const panGesture = Gesture.Pan() @@ -263,7 +270,15 @@ function Photo() { return ( - + { + setSize({ + width: nativeEvent.layout.width, + height: nativeEvent.layout.height, + }); + }} + style={[styles.button, style]} + /> ); } From aac0b4ec626dd0877e171c9c4445a59559175833 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 20 May 2024 15:40:22 +0200 Subject: [PATCH 06/19] Use GH logo --- example/src/new_api/transformations/index.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/example/src/new_api/transformations/index.tsx b/example/src/new_api/transformations/index.tsx index 141256241e..507cef40fe 100644 --- a/example/src/new_api/transformations/index.tsx +++ b/example/src/new_api/transformations/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet, View, Image } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, @@ -144,6 +144,8 @@ function applyTransformations( return multiply4(transform, matrix); } +const SIGNET = require('../../ListWithHeader/signet.png'); + function Photo() { const [size, setSize] = useState({ width: 0, height: 0 }); const translation = useSharedValue({ x: 0, y: 0 }); @@ -277,8 +279,9 @@ function Photo() { height: nativeEvent.layout.height, }); }} - style={[styles.button, style]} - /> + style={[styles.container, style]}> + + ); } @@ -293,13 +296,25 @@ export default function Example() { const styles = StyleSheet.create({ home: { - width: '100%', - height: '100%', - alignSelf: 'center', + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + container: { + width: 240, + height: 240, + backgroundColor: '#eef0ff', + padding: 16, + elevation: 8, + borderRadius: 48, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, }, - button: { - width: 200, - height: 200, - backgroundColor: 'green', + image: { + flex: 1, + width: null, + height: null, }, }); From faffba25218019561088cf07a12f4ef3cfae08cb Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 20 May 2024 16:44:07 +0200 Subject: [PATCH 07/19] Update colors --- example/src/new_api/overlap/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/src/new_api/overlap/index.tsx b/example/src/new_api/overlap/index.tsx index ef85c78e54..ffe5f2bd55 100644 --- a/example/src/new_api/overlap/index.tsx +++ b/example/src/new_api/overlap/index.tsx @@ -45,10 +45,10 @@ function OverlapSiblings() { Overlap Siblings - + - + @@ -78,9 +78,9 @@ function OverlapParents() { Overlap Child - + - + From 54c5ab125010b41a978ec2596dfeb1d446de6852 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 20 May 2024 19:33:58 +0200 Subject: [PATCH 08/19] Update camera sample --- example/app.json | 12 +- example/package.json | 2 + .../src/new_api/camera/AnimatedCameraView.tsx | 80 +++ example/src/new_api/camera/index.tsx | 456 +++++++----------- example/yarn.lock | 91 ++++ 5 files changed, 362 insertions(+), 279 deletions(-) create mode 100644 example/src/new_api/camera/AnimatedCameraView.tsx diff --git a/example/app.json b/example/app.json index 208b86a338..8d690c5394 100644 --- a/example/app.json +++ b/example/app.json @@ -26,6 +26,16 @@ }, "web": { "favicon": "./assets/favicon.png" - } + }, + "plugins": [ + [ + "expo-camera", + { + "cameraPermission": "Allow RNGH example to access your camera", + "microphonePermission": "Allow RNGH example to access your microphone", + "recordAudioAndroid": true + } + ] + ] } } diff --git a/example/package.json b/example/package.json index 48d6d9a83a..698227d62c 100644 --- a/example/package.json +++ b/example/package.json @@ -23,6 +23,7 @@ "@react-navigation/native": "^6.1.17", "@react-navigation/stack": "^6.3.29", "expo": "^51.0.0", + "expo-camera": "~15.0.9", "hoist-non-react-statics": "^3.3.2", "invariant": "^2.2.4", "patch-package": "^6.5.1", @@ -34,6 +35,7 @@ "react-native-reanimated": "3.10.0", "react-native-safe-area-context": "4.10.1", "react-native-screens": "3.31.1", + "react-native-svg": "15.2.0", "react-native-web": "~0.19.10" }, "devDependencies": { diff --git a/example/src/new_api/camera/AnimatedCameraView.tsx b/example/src/new_api/camera/AnimatedCameraView.tsx new file mode 100644 index 0000000000..7ae9a48d72 --- /dev/null +++ b/example/src/new_api/camera/AnimatedCameraView.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { CameraView, useCameraPermissions } from 'expo-camera'; +import { Button, StyleSheet, Text, View } from 'react-native'; +import Animated, { + SharedValue, + useAnimatedProps, +} from 'react-native-reanimated'; + +const AnimatedCameraView = Animated.createAnimatedComponent(CameraView); + +interface CameraProps { + facing: 'front' | 'back'; + zoom: SharedValue; +} + +const Camera = React.forwardRef( + (props: CameraProps, ref: React.Ref) => { + const [permission, requestPermission] = useCameraPermissions(); + + const animatedProps = useAnimatedProps(() => { + return { + zoom: props.zoom.value - 1, + }; + }); + + if (!permission) { + return ; + } + + if (!permission.granted) { + return ( + + + We need your permission to show the camera + +