diff --git a/example/src/new_api/transformations/index.tsx b/example/src/new_api/transformations/index.tsx index c4c2d21339..6101ebfad6 100644 --- a/example/src/new_api/transformations/index.tsx +++ b/example/src/new_api/transformations/index.tsx @@ -1,44 +1,263 @@ import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet, View, Image } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; import { GestureDetector, Gesture } from 'react-native-gesture-handler'; +import { useState } from 'react'; + +// @ts-ignore it's an image +import SIGNET from '../../ListWithHeader/signet.png'; + +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 [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()); 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` }, + { translateX: matrix[12] }, + { translateY: matrix[13] }, + { scale: Math.hypot(matrix[0], matrix[1]) }, + { rotateZ: `${Math.atan2(matrix[1], matrix[0])}rad` }, ], }; }); - const rotationGesture = Gesture.Rotation().onChange((e) => { - 'worklet'; - rotation.value += e.rotationChange; - }); + const rotationGesture = Gesture.Rotation() + .onStart((e) => { + 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'; + 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; + isRotating.value = false; + }); + + const scaleGesture = Gesture.Pinch() + .onStart((e) => { + 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'; + 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; + isScaling.value = false; + }); 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 +278,16 @@ function Photo() { return ( - + { + setSize({ + width: nativeEvent.layout.width, + height: nativeEvent.layout.height, + }); + }} + style={[styles.container, style]}> + + ); } @@ -74,15 +302,24 @@ export default function Example() { const styles = StyleSheet.create({ home: { - width: '100%', - height: '100%', - alignSelf: 'center', - backgroundColor: 'plum', + 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', - alignSelf: 'center', + image: { + width: 208, + height: 208, }, });