34

I have just built a relatively complex image editing UI in React Native.

The experience is intended to be quite similar to Instagram, and has pinch-to-zoom, pan, and rotate functionality.

Transformations are stored as data, eg:

transformation: {
  bottomBoundary: 1989,
  leftBoundary: 410,
  rightBoundary: 1634,
  topBoundary: 765,
  rotation: 0,
},

Wherein topBoundary and bottomBoundary are both offsets from the top of the image, and leftBoundary and rightBoundary are both offsets from the left of the image.

When rotating, because React Native uses the centre of objects as a transform origin, images need to be offset when in the 90° or 270° orientations, so that they are still "sticky" to the top/left corner and can be offset:

calculateRotationOffset.js

export default function (width, height) {
  const transform = [
    { rotate: `${this.rotation}deg` },
  ]

  /*
    RN rotates around centre point, so we need to
    manually offset the rotation to stick the image
    to the top left corner so that our offsets will
    work.
  */
  if (this.rotation === 90) {
    transform.push(
      { translateX: -((width - height) / 2) },
      { translateY: -((width - height) / 2) },
    )
  } else if (this.rotation === 270) {
    transform.push(
      { translateX: ((width - height) / 2) },
      { translateY: ((width - height) / 2) },
    )
  }
  return transform
}

Something about my rotation and zoom implementation are not working together.

Here is the complete snack example..

To reproduce the bug:

  1. Rotate the image once
  2. Pinch to zoom in or out
  3. Rotate the image again

The image will now be flipped and offset incorrectly.

There are a minimum of three transformations happening sequentially here to reproduce the bug, so I have not been able to track down the issue.

This question will definitely be bountied if necessary, and I might also offer a monetary reward. Thanks!

Update: After a few suggestions, it seems the best way to solve this problem is via matrix transformations, and then somehow convert this matrix back to styling that RN can represent onscreen. The matrix transformations need to support pan, rotate, and zoom.

j_d
  • 2,818
  • 9
  • 50
  • 91
  • 2
    This sounds like a common problem that occurs when you represent a transform with separate subtransforms that should not be separated. Do you have the option to directly set a matrix and combine different matrices? Btw, I don't know the react framework. This will most likely make this much easier. For example, the offsets must also rotate when you rotate the image. This will be done implicitly with a single matrix and appropriate operators. – Nico Schertler Apr 24 '18 at 18:32
  • 1
    @NicoSchertler I suppose that would be an option, but the reason I am doing things this way is to bounce transformed images from the master image, given the transform data, as easily as possible. Specifically, using the [Cloudinary API](https://cloudinary.com/documentation/image_transformations). If you have an idea for a completely rewritten solution, please contact me via PM. – j_d Apr 25 '18 at 06:54
  • 1
    These kinds of transforms are order dependent so check your code if you are using the same order of transforms as intended (while editing) but as Nico pointed out using cumulative single 3x3 uniform matrix is much much easier and foul proof than this. for more info take a look at this: [Understanding 4x4 homogenous transform matrices](https://stackoverflow.com/a/28084380/2521214) they are 3D so yours would not have Z axis and Z coordinate (3x3 matrix) dropping (`Xz,Yz,Zx,Zy,Zz` and one of the zeros in `Z?` collumn ) – Spektre Apr 29 '18 at 09:24
  • 1
    @Spektre Yes, I've seen this mentioned several times now, but I fail to see how this is easier. – j_d Apr 30 '18 at 08:03
  • 1
    instead of handling of open ordered list of transform commands that affect each other you apply an or remember only single 3x3 matrix no matter what. no `if` conditions what so ever – Spektre Apr 30 '18 at 15:07
  • @Spektre Yes, that side of it is clear. How would this matrix then be converted into styling that RN can understand? There's no free card, the logic has to be written at some point in time... – j_d May 01 '18 at 07:06
  • @IsaacHinman the resulting rotation translation and scale can be extracted directly from the matrix extracting the 2 axis vectors and origin ... size of each axis vector gives you scale, rotation in 2D is simple usage of `atan2` on X axis and origin is the translation ... – Spektre May 01 '18 at 14:03
  • @Spektre Are you interested in creating a demo solution for the bounty? – j_d May 01 '18 at 16:59
  • @IsaacHinman I do not code in environment you tag so not really but this [Decompose 2D Transformation Matrix](https://stackoverflow.com/a/45160616/2521214) might help – Spektre May 01 '18 at 17:10
  • @Spektre If you are available for hire please PM me. – j_d May 02 '18 at 08:06
  • @IsaacHinman my hire-able programing time for next 10-15 years is already booked out. I code simple things here as a start up and or distraction before doing the "hard" stuff. – Spektre May 02 '18 at 08:12
  • @Spektre Understood. Thank you for your time, I will re-read your previous comments and attempt to understand. – j_d May 02 '18 at 08:17
  • Did you do unit tests on individual transformations, then on combination of transformations ? I'm certain there should be already such a library. Did you try working from https://github.com/kiddkai/react-native-gestures ? – Soleil May 03 '18 at 10:08
  • I was able to build you app and make it run. Did you make any progress ? Do you have a repo ? – Soleil May 03 '18 at 11:48
  • @Soleil What exactly are you asking? I want to refactor the app (as seen in the Expo snack) to use a simpler matrix transformation system. No, there is no progress and no repo. – j_d May 03 '18 at 12:25
  • @IsaacHinman I couldn't reproduce your bug, everything is fine. However, you loose pan after first pinch and it doesn't come back. No problem when we rotate. – Soleil May 03 '18 at 14:10
  • Couldn't reproduce the bug but you need `this.pinching = false` in `handleResponderEnd` or you won't be able to pan after pinching anymore. – Anthony De Smet Jun 09 '18 at 11:35
  • @j_d just worth noting that React Native already supports `matrix` as a transform type, so there's no need to convert back - https://github.com/facebook/react-native/blob/8553e1acc4195479190971cc7a3ffaa0ed37a5e0/Libraries/StyleSheet/StyleSheetTypes.js#L491-L512 – levi Dec 05 '19 at 10:25

1 Answers1

1

I'd replace the bottomBoundary etc. with a decomposed transformation matrix (rotation, scale, translation) and use multitouch in the UI. When sliding a single finger over the image, you can update the translation component of the matrix, and with 2 fingers transform it so that the image appears to stick to the fingers. If you want to constrain rotation to 90 degree increments, you can snap it when the user lets go. This allows you to move freely and intuitively within the image without requiring the current rotation buttons.

Storing the matrix always in decomposed form avoids having to decompose or normalize it to avoid accumulating numerical inaccuracy (skew and changing aspect ratio).

Creating a new DecomposedMatrix class would help structuring the code. The key is to handle transforming it roughly like so:

scaleBy(scale) {
    this.scale *= scale;
    this.x *= scale;
    this.y *= scale;
}
jjrv
  • 4,211
  • 2
  • 40
  • 54
  • 1
    This is way too general to be an answer, and simply repeats things already stated in comments on the original post. – j_d May 03 '18 at 08:22
  • 2
    @IsaacHinman it completely changes the UI, the internal logic and reverses the idea whether you should use a transformation matrix that needs conversion to React Native format. So it doesn't restate things, but takes a completely new approach. I was about to also add sample code, but just changed my mind about that... – jjrv May 03 '18 at 08:24
  • 1
    @jjrv Then perhaps I read the answer before you were finished writing it. Suit yourself! – j_d May 03 '18 at 08:27