Glazzes / react-native-zoom-toolkit

Zoom anything you want! Most complete pinch to zoom utilities for React Native
https://glazzes.github.io/react-native-zoom-toolkit/
MIT License
192 stars 11 forks source link

[Feature]: RotateGesture #72

Closed itsnyx closed 1 month ago

itsnyx commented 1 month ago

Summary

Hello, thanks for the great work on library

can we have a rotate gesture as well ? like in instagram stories when you add an image you basically can do everything zoom-pan-rotate at the same time ! im looking for a library that can do all of these at once, but none have the rotate gesture but i saw a function that can rotate image , which means i have to create another gesture handler for this which im not sure it will work

i really need help on this thanks

Expected API

No response

Glazzes commented 1 month ago

Hello @itsnyx and thank you, I've never used Instragram in life if you could attach a video I could have a better undestanding of your needs.

itsnyx commented 1 month ago

@Glazzes thanks for the fast response here is a video as you can see image only moves and rotates and zooms with double finger touching it but texts has this + that it can move with only 1 finger touching it https://github.com/user-attachments/assets/8d6a8642-f048-4ada-a3ac-ca5d2ce21db0

https://github.com/user-attachments/assets/b2fc8fb9-ea17-4377-bcd3-f0993e2e7fec

Glazzes commented 1 month ago

I see, what you send me looks like some sort of canvas where you can make your own collages or something like that, is this your use case? If not please describe it in more detail.

itsnyx commented 1 month ago

@Glazzes basically yes there is a parent view that you can add items to it (floating position: 'absolute') and each item can rotate zoom pan with double finger gesture i can also share project in a meeting if you dm me in telegram

Glazzes commented 1 month ago

Well, there are some problems to look out, none of my components are designed to have this level of freedom as they're constrained to their particular use case.

Talking of wrapping a rotation gesture over ResumableZoom, these problems may arrise:

Not everything are bad news tho as it can be achieved with little effort from my part, at this point in time I don't frame this as feature for the library due to it's niche nature, maybe if more people display interest in this feature it will make it to the lib.

So as a temporary or definitive solution I can't tell, I can offer you a modification from the gist that gave birth to this library with those features you need, of course keep in mind such script is would be nothing but the gestures part, any other modifications such as event handlers would be a job for you if needed.

If thats ok for you let me know.

itsnyx commented 1 month ago

@Glazzes thanks. yes this is a little bit niche but i think this is a limitation from react-native-gesture handler .


i also have another problem in video below i have shown that i have a horizontal flat list which each item is a simple

<View >
        <Image
          style={{
            width: '100%',
            height: height - 200,
            resizeMode: 'contain',
            // backgroundColor: 'transparent',
          }}
          source={{uri: item.file}}
        />
      </View>

and you can see in first half of the video everything works fine for the flat list swipe but after i change the child elements to :

       <ResumableZoom
        maxScale={resolution}
        onPanStart={test => {
          console.log('onPanStart', test);
        }}>
        <Image style={{...imageSize}} source={{uri: item.file}} />
      </ResumableZoom>

the flat list stops responding because ResumableZoom is getting the gesture events

https://github.com/user-attachments/assets/de262647-0811-43b1-a6b6-4882a4811665

what i think is correct is that when zoom scale is 1 the pan gesture should be disabled so if it's on a horizontal list, the list can do it's swipe

Glazzes commented 1 month ago

@itsnyx ResumableZoom is not meant to be used within scrollable components because both scroll and zoom features must be tightly integrated with each other, therefore that's why I created Gallery component.

itsnyx commented 1 month ago

@Glazzes my bad yes the galley component is what i need !

so should i close this issue or let it be open so if some one else want's this could reply i also try to work on this and will update if i found a proper way!

Glazzes commented 1 month ago

@itsnyx It should remain open in case more people find themselves in the need of something this specific, nonetheless I'd post the base gist with the modifications mentioned (best I can achieve, I can't promise perfect work) here, you and maybe someone else may find good use of it.

Glazzes commented 1 month ago

@itsnyx So I put some ideas of mine of the test and I can't tell whether I achieved a close enough feeling. You can give it a try and let me know how it works for you, remember it's more of a proof of concept rather than same to be shipped to the lib.

Copy and paste the following snippet into a RN app's App.tsx file and run it.

click here to display the code ```jsx import React from "react"; import { Image, LayoutChangeEvent, StyleSheet, useWindowDimensions, View, } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { Gesture, GestureDetector, gestureHandlerRootHOC, } from "react-native-gesture-handler"; type Vector = { x: number; y: number }; type PinchOptions = { toScale: number; fromScale: number; origin: Vector; }; const pinchTransform = ({ toScale, fromScale, origin }: PinchOptions) => { "worklet"; const fromPinchX = -1 * (origin.x * fromScale - origin.x); const fromPinchY = -1 * (origin.y * fromScale - origin.y); const toPinchX = -1 * (origin.x * toScale - origin.x); const toPinchY = -1 * (origin.y * toScale - origin.y); const x = toPinchX - fromPinchX; const y = toPinchY - fromPinchY; return { x, y }; }; const rotate2D = (vector: Vector, angle: number): Vector => { "worklet"; const x1 = vector.x * Math.cos(angle) - vector.y * Math.sin(angle); const y1 = vector.x * Math.sin(angle) + vector.y * Math.cos(angle); return { x: x1, y: y1 }; }; const useVector = (x: number, y: number) => { const x1 = useSharedValue(x); const y1 = useSharedValue(y); return { x: x1, y: y1 }; }; const useSizeVector = (x: number, y: number) => { const width = useSharedValue(x); const height = useSharedValue(y); return { width, height }; }; const TAU = Math.PI * 2; const MIN_SCALE = 1; const MAX_SCALE = 4; const Sticker: React.FC> = ({ children }) => { const childSize = useSizeVector(0, 0); const measureChild = (e: LayoutChangeEvent) => { childSize.width.value = e.nativeEvent.layout.width; childSize.height.value = e.nativeEvent.layout.height; }; const translate = useVector(0, 0); const offset = useVector(0, 0); const focal = useVector(0, 0); // Focal point relative to the event const initialFocal = useVector(0, 0); const currentFocal = useVector(0, 0); const scale = useSharedValue(MIN_SCALE); const scaleOffset = useSharedValue(MIN_SCALE); const angle = useSharedValue(Math.PI / 4); const angleOffset = useSharedValue(0); const panGesture = Gesture.Pan() .maxPointers(1) .onStart((e) => { offset.x.value = translate.x.value; offset.y.value = translate.y.value; }) .onUpdate((e) => { translate.x.value = e.translationX + offset.x.value; translate.y.value = e.translationY + offset.y.value; }); const pinchGesture = Gesture.Pinch() .onTouchesMove((e) => { if (e.numberOfTouches !== 2) return; const one = e.allTouches[0]!; const two = e.allTouches[1]!; currentFocal.x.value = (one.absoluteX + two.absoluteX) / 2; currentFocal.y.value = (one.absoluteY + two.absoluteY) / 2; }) .onStart((e) => { initialFocal.x.value = currentFocal.x.value; initialFocal.y.value = currentFocal.y.value; offset.x.value = translate.x.value; offset.y.value = translate.y.value; focal.x.value = e.focalX - childSize.width.value / 2; focal.y.value = e.focalY - childSize.height.value / 2; scaleOffset.value = scale.value; console.log(e.focalX, e.focalY); }) .onUpdate((e) => { const toScale = e.scale * scaleOffset.value; const deltaX = currentFocal.x.value - initialFocal.x.value; const deltaY = currentFocal.y.value - initialFocal.y.value; const { x, y } = pinchTransform({ toScale, fromScale: scaleOffset.value, origin: { x: focal.x.value, y: focal.y.value }, }); const r = rotate2D({ x, y }, angle.value); translate.x.value = offset.x.value + deltaX + r.x; translate.y.value = offset.y.value + deltaY + r.y; scale.value = toScale; }); const doubleTapGesture = Gesture.Tap() .numberOfTaps(2) .maxDuration(250) .onEnd((e) => { const toScale = scale.value >= MAX_SCALE ? 1 : MAX_SCALE; const originX = e.x - childSize.width.value / 2; const originY = e.y - childSize.height.value / 2; const { x, y } = pinchTransform({ toScale, fromScale: scale.value, origin: { x: originX, y: originY }, }); const r = rotate2D({ x, y }, angle.value); translate.x.value = withTiming(translate.x.value + r.x); translate.y.value = withTiming(translate.y.value + r.y); scale.value = withTiming(toScale); }); const rotationGesture = Gesture.Rotation() .onStart(() => { angleOffset.value = angle.value; }) .onUpdate((e) => { angle.value = (e.rotation + angleOffset.value + TAU) % TAU; }); const animatedStyles = useAnimatedStyle(() => { return { transform: [ { translateX: translate.x.value }, { translateY: translate.y.value }, { scale: scale.value }, { rotate: `${angle.value}rad` }, ], }; }, [translate]); const composedGesture = Gesture.Race( Gesture.Simultaneous(pinchGesture, rotationGesture), panGesture, doubleTapGesture, ); return ( {children} ); }; const IMAGE = "https://winstonandporter.com/cdn/shop/articles/dalmatian-g6a4523f4a_1280_1024x1024.jpg?v=1688553037"; // Resolution of the image above const resolution = { width: 682, height: 1024 }; const App = () => { const { width } = useWindowDimensions(); const imageWidth = width * 0.6; const imageHeight = imageWidth / (resolution.width / resolution.height); return ( ); }; const styles = StyleSheet.create({ root: { flex: 1, justifyContent: "center", alignItems: "center", }, }); export default gestureHandlerRootHOC(App); ```
itsnyx commented 1 month ago

@Glazzes that's is exactly what i need you are a champ !!

usually doubleTapGesture is not needed in sticker component because whole component moves on the screen .

itsnyx commented 1 month ago

@Glazzes also here in

     <Gallery
        ref={ref}
        onPanStart={() => (hideProgress.value = 0)}
        onGestureEnd={() => (hideProgress.value = 1)}
        onUpdate={onUpdate}
        data={item.files}
        keyExtractor={keyExtractor}
        renderItem={renderItem}
        onTap={onTap}
        customTransition={transition}
      />

can you add velocity for pan gesture in onUpdate method?

Glazzes commented 1 month ago

@itsnyx No problem, I'm glad it works and second do not overflow this issue with unrelated stuff to the rotation gesture.

About that velocity parameter, I can't, onUpdate is used for all gestures and not all gestures have a velocity parameter, velocity does not make part of the transformation state or it's relevant to it.

itsnyx commented 1 month ago

@Glazzes ok then i will close this issue. and will investigate on gallery methods.

btw what have you done is huge ! thanks alot💟