Git Product home page Git Product logo

zoomable-svg's Introduction

ZoomableSvg

Pinch to pan-n-zoom react-native-svg components using a render prop.

Advanced Example: InfiniDraw Universal svg drawing with pan and zoom. Builds on Next.js and react-native-web for the web version, and react-native for native apps. Uses react-native-svg on native, svgs for the web, and zoomable-svg in both.

Basic ZoomableSvg Example

import React, { Component } from 'react';
import { View, StyleSheet, Dimensions, Animated } from 'react-native';
import { Svg } from 'expo';

import ZoomableSvg from 'zoomable-svg';

const { G, Circle, Path, Rect } = Svg;

const { width, height } = Dimensions.get('window');
const AnimatedRect = Animated.createAnimatedComponent(Rect);

const colors = ['red', 'green', 'blue', 'yellow', 'brown'];

class SvgRoot extends Component {
  state = {
    color: 'red',
    initAnim: new Animated.Value(0),
  };

  componentDidMount() {
    Animated.timing(
      // Animate over time
      this.state.initAnim,
      {
        toValue: 1,
        duration: 3000,
        useNativeDriver: false,
      },
    ).start();
  }

  onPress = () => {
    this.setState(({ color }) => ({
      color: colors[(colors.indexOf(color) + 1) % colors.length],
    }));
    const { onToggle } = this.props;
    if (onToggle) {
      onToggle();
    }
  };

  render() {
    const { initAnim, color } = this.state;
    let translateRectY = initAnim.interpolate({
      inputRange: [0, 1],
      outputRange: ['0', '50'],
    });
    const { transform } = this.props;
    return (
      <Svg width={width} height={height}>
        <G transform={transform}>
          <AnimatedRect
            y={translateRectY}
            x="5"
            width="90"
            height="90"
            fill="rgb(0,0,255)"
            strokeWidth="3"
            stroke="rgb(0,0,0)"
          />
          <Rect
            x="5"
            y="5"
            width="55"
            height="55"
            fill="white"
          />
          <Circle
            cx="32"
            cy="32"
            r="4.167"
            fill={color}
            onPress={this.onPress}
          />
          <Path
            d="M55.192 27.87l-5.825-1.092a17.98 17.98 0 0 0-1.392-3.37l3.37-4.928c.312-.456.248-1.142-.143-1.532l-4.155-4.156c-.39-.39-1.076-.454-1.532-.143l-4.928 3.37a18.023 18.023 0 0 0-3.473-1.42l-1.086-5.793c-.103-.543-.632-.983-1.185-.983h-5.877c-.553 0-1.082.44-1.185.983l-1.096 5.85a17.96 17.96 0 0 0-3.334 1.393l-4.866-3.33c-.456-.31-1.142-.247-1.532.144l-4.156 4.156c-.39.39-.454 1.076-.143 1.532l3.35 4.896a18.055 18.055 0 0 0-1.37 3.33L8.807 27.87c-.542.103-.982.632-.982 1.185v5.877c0 .553.44 1.082.982 1.185l5.82 1.09a18.013 18.013 0 0 0 1.4 3.4l-3.31 4.842c-.313.455-.25 1.14.142 1.53l4.155 4.157c.39.39 1.076.454 1.532.143l4.84-3.313c1.04.563 2.146 1.02 3.3 1.375l1.096 5.852c.103.542.632.982 1.185.982h5.877c.553 0 1.082-.44 1.185-.982l1.086-5.796c1.2-.354 2.354-.82 3.438-1.4l4.902 3.353c.456.313 1.142.25 1.532-.142l4.155-4.154c.39-.39.454-1.076.143-1.532l-3.335-4.874a18.016 18.016 0 0 0 1.424-3.44l5.82-1.09c.54-.104.98-.633.98-1.186v-5.877c0-.553-.44-1.082-.982-1.185zM32 42.085c-5.568 0-10.083-4.515-10.083-10.086 0-5.568 4.515-10.084 10.083-10.084 5.57 0 10.086 4.516 10.086 10.083 0 5.57-4.517 10.085-10.086 10.085z"
            fill="blue"
          />
        </G>
      </Svg>
    );
  }
}

const constraintCombinations = [
  'none',
  'dynamic',
  'static',
  'union',
  'intersect',
];

export default class App extends Component {
  state = {
    type: 1,
    constrain: true,
    constraints: {
      combine: 'dynamic',
      scaleExtent: [width / height, 5],
      translateExtent: [[0, 0], [100, 100]],
    },
  };

  onToggle = () =>
    this.setState(({ type, constraints }) => {
      const nextType = (type + 1) % constraintCombinations.length;
      return {
        type: nextType,
        constrain: nextType !== 0,
        constraints: {
          ...constraints,
          combine: constraintCombinations[nextType],
        },
      };
    });

  childProps = { onToggle: this.onToggle };

  render() {
    const { constrain, constraints } = this.state;
    return (
      <View style={styles.container}>
        <ZoomableSvg
          align="mid"
          vbWidth={100}
          vbHeight={100}
          width={width}
          height={height}
          initialTop={-20}
          initialLeft={-50}
          initialZoom={1.2}
          doubleTapThreshold={300}
          meetOrSlice="meet"
          svgRoot={SvgRoot}
          childProps={this.childProps}
          constrain={constrain ? constraints : null}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
  },
});

Drawing Example

import React, { Component } from 'react';
import {
  View,
  StyleSheet,
  Dimensions,
  PanResponder,
  TouchableOpacity,
  Text,
} from 'react-native';
import { Svg } from 'expo';

import ZoomableSvg from 'zoomable-svg';

const { G, Path, Rect } = Svg;

const { width, height } = Dimensions.get('window');

class SvgRoot extends Component {
  state = {
    paths: [],
    currentPath: null,
  };

  processTouch = (sx, sy) => {
    const { transform } = this.props;
    const { currentPath } = this.state;
    const { translateX, translateY, scaleX, scaleY } = transform;
    const x = (sx - translateX) / scaleX;
    const y = (sy - translateY) / scaleY;
    if (!currentPath) {
      this.setState({ currentPath: `M${x},${y}` });
    } else {
      this.setState({ currentPath: `${currentPath}L${x},${y}` });
    }
  };

  componentWillMount() {
    const noop = () => {};
    const yes = () => true;
    const shouldRespond = () => {
      return this.props.drawing;
    };
    this._panResponder = PanResponder.create({
      onPanResponderGrant: noop,
      onPanResponderTerminate: noop,
      onShouldBlockNativeResponder: yes,
      onMoveShouldSetPanResponder: shouldRespond,
      onStartShouldSetPanResponder: shouldRespond,
      onPanResponderTerminationRequest: shouldRespond,
      onMoveShouldSetPanResponderCapture: shouldRespond,
      onStartShouldSetPanResponderCapture: shouldRespond,
      onPanResponderMove: ({ nativeEvent: { touches } }) => {
        const { length } = touches;
        if (length === 1) {
          const [{ pageX, pageY }] = touches;
          this.processTouch(pageX, pageY);
        }
      },
      onPanResponderRelease: () => {
        this.setState(({ paths, currentPath }) => ({
          paths: [...paths, currentPath],
          currentPath: null,
        }));
      },
    });
  }

  render() {
    const { paths, currentPath } = this.state;
    const { transform } = this.props;
    return (
      <View {...this._panResponder.panHandlers}>
        <Svg width={width} height={height} style={styles.absfill}>
          <G transform={transform}>
            <Rect x="0" y="0" width="100" height="100" fill="white" />
            {paths.map(path => (
              <Path d={path} stroke="black" strokeWidth="1" fill="none" />
            ))}
          </G>
        </Svg>
        <Svg width={width} height={height} style={styles.absfill}>
          <G transform={transform}>
            {currentPath
              ? <Path
                  d={currentPath}
                  stroke="black"
                  strokeWidth="1"
                  fill="none"
                />
              : null}
          </G>
        </Svg>
      </View>
    );
  }
}

const constraints = {
  combine: 'dynamic',
  scaleExtent: [width / height, 5],
  translateExtent: [[0, 0], [100, 100]],
};

export default class App extends Component {
  state = {
    drawing: false,
  };

  toggleDrawing = () => {
    this.setState(({ drawing }) => ({
      drawing: !drawing,
    }));
  };

  render() {
    const { drawing } = this.state;
    return (
      <View style={[styles.container, styles.absfill]}>
        <ZoomableSvg
          align="mid"
          vbWidth={100}
          vbHeight={100}
          width={width}
          height={height}
          initialTop={0}
          initialLeft={0}
          initialZoom={1}
          doubleTapThreshold={300}
          meetOrSlice="meet"
          svgRoot={SvgRoot}
          lock={drawing}
          childProps={this.state}
          constrain={constraints}
        />
        <TouchableOpacity onPress={this.toggleDrawing} style={styles.button}>
          <Text>{drawing ? 'Move' : 'Draw'}</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
  },
  absfill: {
    position: 'absolute',
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
  },
  button: {
    position: 'absolute',
    bottom: 10,
    right: 10,
  },
});

zoomable-svg's People

Contributors

msand avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

zoomable-svg's Issues

how to use FlatList with zoomable-svg?

Hi, i implemented ZoomableSvg with SVG and it works great Android and iOS, but when i wrap the implementation with FlatList it is not work as expected...
can you please provide an example of implementation with flatlist?

Example doesn't seem to work

I'm really interested in using this lib but I can''t seem to get the example working using Expo or in my own app.

The Expo viewer on the web or my phone just crashes and my app is throwing the following error: undefined is not an object (evaluating 'Component.propTypes')

This tracks back to line 10 which is: const AnimatedRect = Animated.createAnimatedComponent(Rect);

Thoughts?

Issue with draw event panHandelers

Hello,

Thanks for building this good and simple package.

There are two things I am looking to do with my react-native app.

First, I would like my Svg Rect to have static borders, so that the Rect is always at least as big as the max width of the Svg View. There could be a built in way to do this already, but since there is not much documentation it's taking a while to figure it out just with the source code.

Second, I need elements on my Svg to be clickable, while also using a pinch zoom. For example, user pinch zooms a rectangle, taps to place a piece, then zooms back out. All with a seamless flow. I see that this is nearly achievable with the "Drawing Example", but I don't want the user to need to toggle on and off all the time - bad UX.

I'm open to fixing these with a PR, but it would be helpful to have some direction.

Any recommendations?

onPress of Circle in Basic ZoomableSvg Example doesn't work

Hi, thank you for your excellent work. I try the Basic ZoomableSvg Example , it seem work fine. But when I press the Circle,it don't work. While I remove ZoomableSvg , Circle is onPress work.

React Native : 0.56.0
react-native-svg : 6.5.3
zoomable-svg : 5.0.1

I am sorry that my English is not good.

Any way to pass properties to SVGROOT?

Hi, In my app I need to pass some variable data to be drawn.

Example:

<Ellipse cy={this.state.y} cx={this.state.x} />

But I am unable to do this since svgRoot does not inherit properties from parent component.
Moreover, I want svgRoot to execute render on state change, but, as written above, I can't pass any state to it, hence this component is not reactive.

Is there any workaround for my issue?

Re-render the SVG after the zoom-gesture is released

This might sound odd, but is it possible to re-render the SVG after the zoom-gesture is released? This would help a lot with complex SVG-trees considering that the tree is updating 24/7 while you're performing pan-gestures.

What I had in mind was that while you're zooming in the SVG, it'll have to be a bit blurry despite zooming in. However, once the gesture is released, the SVG will have to update.

Thoughts? Possible to do this?

feat(rotation): consider adding rotation angle as well

Hi! ๐Ÿ‘‹

Firstly, thanks for your work on this project! ๐Ÿ™‚

Today I used patch-package to patch [email protected] for the project I'm working on.

Here is the diff that solved my problem:

diff --git a/node_modules/zoomable-svg/index.js b/node_modules/zoomable-svg/index.js
index 1b3f5e2..1667658 100644
--- a/node_modules/zoomable-svg/index.js
+++ b/node_modules/zoomable-svg/index.js
@@ -238,6 +238,7 @@ function getDerivedStateFromProps(props, state) {
 }
 
 function getZoomTransform({
+  angle,
   left,
   top,
   zoom,
@@ -247,6 +248,7 @@ function getZoomTransform({
   translateY,
 }) {
   return {
+    angle,
     translateX: left + zoom * translateX,
     translateY: top + zoom * translateY,
     scaleX: zoom * scaleX,
@@ -258,6 +260,7 @@ class ZoomableSvg extends Component {
   constructor(props) {
     super();
     this.state = getDerivedStateFromProps(props, {
+      angle: props.angle || 0,
       zoom: props.initialZoom || 1,
       left: props.initialLeft || 0,
       top: props.initialTop || 0,
@@ -430,7 +433,7 @@ class ZoomableSvg extends Component {
         initialTop: top,
         initialLeft: left,
         initialZoom: zoom,
-        initialDistance: distance,
+        initialDistance: distance
       });
     } else {
       const {
@@ -451,10 +454,14 @@ class ZoomableSvg extends Component {
       const top = (initialTop + dy - y) * touchZoom + y;
       const zoom = initialZoom * touchZoom;
 
+      let angle = (Math.atan2(y2-y1, x2-x1) * 180) / Math.PI 
+      console.log("??????tempAngle", angle)
+
       const nextState = {
         zoom,
         left,
         top,
+        angle
       };
 
       this.setState(constrain ? this.constrainExtent(nextState) : nextState);
@@ -473,7 +480,7 @@ class ZoomableSvg extends Component {
         initialY: y,
       });
     } else {
-      const { initialX, initialY, initialLeft, initialTop, zoom } = this.state;
+      const { initialX, initialY, initialLeft, initialTop, zoom, angle } = this.state;
       const { constrain } = this.props;
 
       const dx = x - initialX;
@@ -483,6 +490,7 @@ class ZoomableSvg extends Component {
         left: initialLeft + dx,
         top: initialTop + dy,
         zoom,
+        angle
       };
 
       this.setState(constrain ? this.constrainExtent(nextState) : nextState);
@@ -494,6 +502,7 @@ class ZoomableSvg extends Component {
       top: initialTop,
       left: initialLeft,
       zoom: initialZoom,
+      angle
     } = this.state;
     const { constrain } = this.props;
 
@@ -505,6 +514,7 @@ class ZoomableSvg extends Component {
       zoom,
       left,
       top,
+      angle
     };
 
     this.setState(constrain ? this.constrainExtent(nextState) : nextState);

This issue body was partially generated by patch-package.

onPress causing Image flicker

Almost appears to be triggering a re-render of the component which causes the Image to flicker. This doesn't happen with react-native-svg and only seems to occur when wrapped with the ZoomableSvg component.

Alt Text

<ZoomableSvg
           align="mid"
           width={width}
           height={height}
           vbWidth={100}
           vbHeight={100}
           initialTop={0}
           initialLeft={0}
           initialZoom={1}
           meetOrSlice="meet"
           constrain={{
             combine: 'dynamic',
             scaleExtent: [1, 2],
             translateExtent: [[-10, -10], [110, 110]],
           }}
           svgRoot={({ transform }) => (
             <>
               <Svg
                 style={styles.absfill}
                 width={width}
                 height={height}
                 preserveAspectRatio="xMinYMin meet"
               >
                 <G transform={transform}>
                   <Image
                     width="100"
                     height="100"
                     preserveAspectRatio="none"
                     href={{ uri: diagram.image }}
                   />
                 </G>
               </Svg>
               <Svg
                 style={styles.absfill}
                 width={width}
                 height={height}
                 preserveAspectRatio="xMinYMin meet">
                 <G transform={transform}>
                   {diagram.polygons.map((polygon) => (
                     <Polygon
                       key={polygon.$id}
                       points={polygon.points.toString()}
                       fill="none"
                       stroke={activeSegment === segment.$id ? `red` : 'none'}
                       strokeWidth="1"
                       onPress={() => setActiveSegment(segment.$id)}
                     />
                   ))}
                 </G>
               </Svg>
             </>
           )} />

missing dependencies for react-native?

I've implemented zoomable-svg in a react native project, but compilation fails due to missing react-native-web and react-dom dependencies.

i see the platform check at the top of index.js, but it doesn't seem to be working correctly?

I'm using RN 0.59.9

Zooming midpoint

Hello,
I would like to ask, is it possible to configure the ZoomableSvg component so that the zoom is always the same for each direction and the zoom point is in the center of the image view?
Thank you for any answer.
Regards,
Marcin

Example: How to set zoom focus point?

When running on Android Emulator, the focus point of the zoom is not in the middle, but on the left side of the screen. Any ideas why? :) I can post screenshots, but the code is the same as in the example.

how to keep transform translateX and translateY to be the same on max scale (scaleX, scaley)

Hi,
i try to keep the translateX and translateY to be the same when reaching the max zoom when scaleX or scaley === 3.
how can we do that?

but i also want it to be able to drag

Object {
  "scaleX": 3,
  "scaleY": 3,
  "translateX": -528.1742063998316, - > this keep changing when user keep doing zoom in
  "translateY": -173.31884437708027   - > this keep changing when user keep doing zoom in
}

tnx

Need some help with minZoom, maxZoom and few other things

Hey,

First of all great component ๐Ÿ‘

I had some questions:

  1. Is it possible to set min and max zoom?
  2. Can we set not to translateX (left and right) more than 10px offset. For example if we have svg with size 50x100 I want to avoid the user to be able to move svg to the left more than 10px offset (is the same for the right position and is the same for translateY.
  3. Do you have any plans to add double tap to zoom?
  4. Can we set for example the initial scale (scale={1.5}) and the initial position of the svg like {top: 20, left: 50} and this should be the initial state of the svg.

Hope you will understand what I mean :) if not please let me know and I will try to explain more.

Thanks in advance :)

Documentation?

Hi

I'm trying to use your library but some properties are not clear for me what they do.
I did read software-mansion/react-native-svg#374 where some properties are explained but I don't understand them all.

  • vbWidth/vbHeight, i know that vb stands for viewbox but not sure how these values are translated to the canvas. I tried to edit them but not sure what to expect
  • meetOrSlice: not sure what both options do
  • constraints - combine: 'dynamic', // and all other properties here
  • constraints - scaleExtent
  • constraints - translateExtent

Could you elaborate a bit more on what the above properties do?

Thanks in advance!

Add Type definition

It would be great if you add a type definition file for supporting typescript projects.

Thanks.

doubleTapThreshold prop breaks touch events on SVG.

When the doubleTapThreshold prop is set, touch events never get passed through to the underlying SVG.

shouldRespond returns a truthy value when doubleTapThreshold is set, causing the view to always set the pan responder.

        !lock &&
        (evt.nativeEvent.touches.length === 2 ||
          dx * dx + dy * dy >= moveThreshold ||
          doubleTapThreshold)

In my case I removed doubleTapThreshold from ZoomableSvg props.

If this is by design, could you add to the homepage docs?

Thanks for your great work @msand, saved me many hours!

Reset transform?

Hello again,

I have created a canvas where a certain object is displayed. Users can zoom in, scale, etc. I have a button that when pressed, it changes the canvas to its initial position.

Any idea how I could achieve this? I tried resetting the transform object but without luck.

Thanks in advance!

Kind regards

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.