Closed DenPostika closed 2 months ago
@DenPostika When I asked you to open an issue I asked you for more details. What's exactly wrong with the approach you're taking? is it not working? is it getting pixelated? What's that you need? I do see spanish words on the svg, do you speak it?
@Glazzes Sorry, I don't speak Spanish very well.
Ok, problem:
I want to create interactive elements inside of svg like that:
<G onPress={() => Alert.alert("MORERES!")}>
<Rect x="1580" y="1710" width="80" height="30" fill="transparent" />
<Text x={1582.319} y={1726.638} fontSize={22}>
{"Moreres"}
</Text>
</G>
but we have a
<GestureHandlerRootView style={styles.root} onLayout={measureRoot}>
<Animated.View style={childStyle} onLayout={measureChild}>
<MapSvg/>
</Animated.View>
<GestureDetector gesture={composedGesture}>
<Animated.View style={detectorStyle} pointerEvents={"none"}/>
</GestureDetector>
</GestureHandlerRootView>
but gesture handler stop working (what is logical). Maybe you know some workaround how to fix it.
I've been making tests with Gesture Handler and it's actually doable, Gestures do not collide and everything touch related should work, currently I'm looking for a way to modify my existing solution, so this may take a while.
@DenPostika If you want you can try out the gist that evolved into this library, it does not have as much features as the lib but I think it may serve you well https://gist.github.com/Glazzes/357201f74fbfaddb3e933f4c258c4878
@Glazzes maybe u know how to fix jumping focal point
https://github.com/user-attachments/assets/dbd0d3c4-4d9c-42ed-b8ae-7919d16cb5db
import React, {useEffect} from 'react';
import {Image, StyleSheet, View, useWindowDimensions} from 'react-native';
import Animated, {
Easing,
cancelAnimation,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withDecay,
withTiming,
} from 'react-native-reanimated';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {MapSvg} from "@/components/navigation/MapSvg";
type PinchOptions = {
toScale: number;
fromScale: number;
origin: { x: number; y: number };
delta: { x: number; y: number };
offset: { x: number; y: number };
};
const pinchTransform = ({toScale, fromScale, delta, origin, offset}: PinchOptions) => {
'worklet';
// console.log('------------------------');
const fromPinchX = -1 * (origin.x * fromScale - origin.x);
const fromPinchY = -1 * (origin.y * fromScale - origin.y);
// console.log(`fromPinch: (${fromPinchX}, ${fromPinchY})`);
const toPinchX = -1 * (origin.x * toScale - origin.x);
const toPinchY = -1 * (origin.y * toScale - origin.y);
// console.log(`toPinch: (${toPinchX}, ${toPinchY})`);
const x = offset.x + toPinchX - fromPinchX + delta.x;
const y = offset.y + toPinchY - fromPinchY + delta.y;
// console.log('------------------------');
return {x, y};
};
const clamp = (lowerBound: number, upperBound: number, value: number) => {
'worklet';
return Math.max(lowerBound, Math.min(value, upperBound));
};
// https://api.flutter.dev/flutter/widgets/BouncingScrollPhysics/frictionFactor.html
const friction = (fraction: number) => {
'worklet';
return 0.75 * Math.pow(1 - fraction * fraction, 2);
};
const IMAGE =
'https://e0.pxfuel.com/wallpapers/322/848/desktop-wallpaper-fennec-fox-algeria-national-animal-desert-fox.jpg';
const config = {duration: 200, easing: Easing.linear};
// For a better understanding of the pinch gesture you can delete pan and tap gestures entirely if you feel like it
const ZoomableSvg = () => {
const {width, height} = useWindowDimensions();
const imageWidth = useSharedValue<number>(1890);
const imageHeight = useSharedValue<number>(2422);
const scale = useSharedValue<number>(0.2);
const scaleOffset = useSharedValue<number>(0.2);
const translateX = useSharedValue<number>(0);
const translateY = useSharedValue<number>(0);
const translateXOffset = useSharedValue<number>(0);
const translateYOffset = useSharedValue<number>(0);
const originX = useSharedValue<number>(0);
const originY = useSharedValue<number>(0);
const boundaries = useDerivedValue(() => {
const offsetX = Math.max(0, imageWidth.value * scale.value - width) / 2;
const offsetY = Math.max(0, imageHeight.value * scale.value - height) / 2;
return {x: offsetX, y: offsetY};
}, [scale, imageWidth, imageHeight, width, height]);
const isPinchActive = useSharedValue<boolean>(false);
const pinch = Gesture.Pinch()
.onStart(e => {
// console.log(`Start: focal: (${e.focalX}, ${e.focalY})`);
isPinchActive.value = true;
originX.value = e.focalX - imageWidth.value / 2;
originY.value = e.focalY - imageHeight.value / 2;
translateXOffset.value = translateX.value;
translateYOffset.value = translateY.value;
scaleOffset.value = scale.value;
})
.onUpdate(e => {
// console.log('--------------------------------------------------------------');
// console.log(`Update: focal: (${e.focalX}, ${e.focalY}), scale: (${e.scale})`);
const toScale = e.scale * scaleOffset.value;
// console.log(`toScale: (${toScale})`);
//
// console.log(`image: (${imageWidth.value}, ${imageHeight.value})`);
// console.log(`origin: (${originX.value}, ${originY.value})`);
const deltaX = e.focalX - imageWidth.value / 2 - originX.value;
const deltaY = e.focalY - imageHeight.value / 2 - originY.value;
// console.log(`delta: (${deltaX}, ${deltaY})`);
const {x: toX, y: toY} = pinchTransform({
toScale: toScale,
fromScale: scaleOffset.value,
origin: {x: originX.value, y: originY.value},
offset: {x: translateXOffset.value, y: translateYOffset.value},
delta: {x: deltaX, y: deltaY},
});
// console.log(`to point: (${toX}, ${toY})`);
const boundX = Math.max(0, imageWidth.value * toScale - width) / 2;
const boundY = Math.max(0, imageHeight.value * toScale - height) / 2;
// console.log(`bound: (${boundX}, ${boundY})`);
translateX.value = clamp(-1 * boundX, boundX, toX);
translateY.value = clamp(-1 * boundY, boundY, toY);
scale.value = toScale;
})
.onEnd(() => {
isPinchActive.value = false;
if (scale.value < 0.2) {
scale.value = withTiming(0.2);
translateX.value = withTiming(0);
translateY.value = withTiming(0);
}
});
const isWithinBoundX = useSharedValue<boolean>(true);
const isWithinBoundY = useSharedValue<boolean>(true);
const pan = Gesture.Pan()
.maxPointers(1)
.onStart(_ => {
cancelAnimation(translateX);
cancelAnimation(translateY);
translateXOffset.value = translateX.value;
translateYOffset.value = translateY.value;
})
.onChange(({translationX, translationY, changeX, changeY}) => {
const toX = translateXOffset.value + translationX;
const toY = translateYOffset.value + translationY;
const {x: boundX, y: boundY} = boundaries.value;
isWithinBoundX.value = toX >= -1 * boundX && toX <= boundX;
isWithinBoundY.value = toY >= -1 * boundY && toY <= boundY;
if (isWithinBoundX.value) {
translateX.value = clamp(-1 * boundX, boundX, toX);
} else {
if (imageWidth.value * scale.value < width) {
translateX.value = clamp(-1 * boundX, boundX, toX);
} else {
const fraction = (Math.abs(toX) - boundX) / width;
const frictionX = friction(clamp(0, 1, fraction));
translateX.value += changeX * frictionX;
}
}
if (isWithinBoundY.value) {
translateY.value = clamp(-1 * boundY, boundY, toY);
} else {
if (imageHeight.value * scale.value < height) {
translateY.value = clamp(-1 * boundY, boundY, toY);
} else {
const fraction = (Math.abs(toY) - boundY) / width;
const frictionY = friction(clamp(0, 1, fraction));
translateY.value += changeY * frictionY;
}
}
})
.onEnd(({velocityX, velocityY}) => {
const {x: boundX, y: boundY} = boundaries.value;
const toX = clamp(-1 * boundX, boundX, translateX.value);
const toY = clamp(-1 * boundY, boundY, translateY.value);
translateX.value = isWithinBoundX.value
? withDecay({velocity: velocityX / 2, clamp: [-1 * boundX, boundX]})
: withTiming(toX, config);
translateY.value = isWithinBoundY.value
? withDecay({velocity: velocityY / 2, clamp: [-1 * boundY, boundY]})
: withTiming(toY, config);
});
const doubleTap = Gesture.Tap()
.numberOfTaps(2)
.maxDuration(250)
.onStart(_ => {
translateXOffset.value = translateX.value;
translateYOffset.value = translateY.value;
})
.onEnd(e => {
if (isPinchActive.value) {
return;
}
if (scale.value > 2) {
translateX.value = withTiming(0);
translateY.value = withTiming(0);
scale.value = withTiming(1);
return;
}
const orgnX = e.x - imageWidth.value / 2;
const orgnY = e.y - imageHeight.value / 2;
const highestScreenDimension = Math.max(width, height);
const higheststImageDimension = Math.max(
imageWidth.value,
imageHeight.value,
);
const tapOrigin = width > height ? orgnX : orgnY;
const toScale =
((highestScreenDimension + Math.abs(tapOrigin)) /
higheststImageDimension) *
2;
const {x, y} = pinchTransform({
fromScale: scale.value,
toScale,
origin: {x: orgnX, y: orgnY},
offset: {x: translateXOffset.value, y: translateYOffset.value},
delta: {x: 0, y: 0},
});
const boundX = Math.max(0, (imageWidth.value * toScale - width) / 2);
const boundY = Math.max(0, (imageHeight.value * toScale - height) / 2);
translateX.value = withTiming(clamp(-boundX, boundX, x));
translateY.value = withTiming(clamp(-boundY, boundY, y));
scale.value = withTiming(toScale);
});
const animatedStyle = useAnimatedStyle(() => {
console.log(`translate: (${translateX.value}, ${translateY.value})`);
return {
width: imageWidth.value,
height: imageHeight.value,
transform: [
{translateX: translateX.value},
{translateY: translateY.value},
{scale: scale.value},
],
}
});
// useEffect(() => {
// Image.getSize(
// IMAGE,
// (w, h) => {
// const isPortrait = width < height;
// const aspectRatio = w / h;
//
// if (isPortrait) {
// imageWidth.value = width;
// imageHeight.value = width / aspectRatio;
// } else {
// imageWidth.value = height * aspectRatio;
// imageHeight.value = height;
// }
//
// scale.value = withTiming(1, config);
// translateX.value = withTiming(0, config);
// translateY.value = withTiming(0, config);
// },
// e => console.log(e),
// );
// }, [width, height]);
return (
<View style={styles.root}>
<GestureDetector gesture={Gesture.Race(pan, pinch, doubleTap)}>
<Animated.View style={animatedStyle}>
<MapSvg/>
</Animated.View>
</GestureDetector>
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#121212',
},
});
export default ZoomableSvg;
@DenPostika I've made severe changes to the gist I've sent you, give a try and let me know if it works https://gist.github.com/Glazzes/357201f74fbfaddb3e933f4c258c4878, if it works please send me a video of it working on iOS (as I don't have an iOS device), this is should be the pinch gesture implementation that makes it to the library in the next version.
onPress support has been added in version 3.0.0
Summary
Hey, I am developing interactive map for a subway:
here the link to the SVG file: https://filebin.net/1sqq6o03fjwdjl0a
I need to make elements inside of svg intractable.
Expected API
No response