Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(): skewing controls #8380

Merged
merged 68 commits into from
Oct 28, 2022
Merged

fix(): skewing controls #8380

merged 68 commits into from
Oct 28, 2022

Conversation

ShaMan123
Copy link
Contributor

@ShaMan123 ShaMan123 commented Oct 17, 2022

Motivation

Skewing is buggy in some cases .
#8344 has introduced changes that break the skewing controls.

Description

Existing logic is brilliant! It used the distance between the pointer and the origin point to determine the size of the target's axis (twice the diff). Then it extracted skewing as follows:

$$ \displaylines{ x' = x + y \cdot shear_x \\ shear_x = \tan(skew_x) \\ skew_x = \arctan(\frac{x'-x}{y}) } $$

Apparently the problem lied in the calculation of width (x) when calculating the skewed height (y) when skewing y

Problems with the previous impl:

  • skewing in a group - qrDecompose seems to mess everything up (I think)
  • skewing an object with skewing set on the counter axis - fixed in https://github.com/fabricjs/fabric.js/compare/fix-skewing-controls2 (thanks to @luizzappa pointing out that skewX depends on skewY but not the other way round), still buggy inside group
  • It assumes the change to size is linear to the change in pointer, which was correct until we fixed stroke bbox calculations. I can't think of a decent way around this unless we don't calculate the projections while skewing. This led me to seek a solution that compares against the last skewing state.

I have tried to come up with a different approach, relying purely on linear algebra.
If fabric move away from decomposition then we can look into that (see Gist).
This PR is intended eventually to work only with matrices, but as long as fabric relays on decomposition skewY will not work.
I think if we can live without decomposing we should (canvas API doesn't decompose), but that's a tale for another time.
Another thought I had is that in the case of skewing Y we know the value of skewX so that might be enough to alter the decomposition to return a skewY value, @asturur ?

Changes

  • Fixed the second skewing interaction - skewing is now maintained.
  • Fixed cross skewing

Gist - IGNORED, here for future use only

Calculating Shearing

We are looking for the shearing values of the transform matrix defined by the pointer offset.

$$ \displaylines{ \Delta shear= \begin{bmatrix} 1 & x \\ y & 1 \end{bmatrix}, size= \begin{bmatrix} w \\ h \end{bmatrix} \\ \Delta pointer= \begin{bmatrix} p_x \\ p_y \end{bmatrix}= \Delta size = \Delta shear \cdot size= \begin{bmatrix} 1 & x \\ y & 1 \end{bmatrix} \cdot \begin{bmatrix} w \\ h \end{bmatrix}= \begin{bmatrix} h \cdot x \\ w \cdot y \end{bmatrix} \\ \begin{cases} p_x=h \cdot x \\ p_y= w \cdot y \end{cases} \Rightarrow \begin{cases} x=\frac{p_x}{h} \\ y=\frac{p_y}{w} \end{cases} \Rightarrow \Delta shear= \begin{bmatrix} 1 & \frac{p_x}{h} \\ \frac{p_y}{w} & 1 \end{bmatrix} } $$

Calculating the new transform

The following solves for skewX, same process is done for skewY (need to account for cross axis skewing)
This logic is not used in the PR but is here in case we decide to set the transform matrix directly instead of decomposed values.

  1. First we need to calculate the transform without the existing shearing:

$$ \displaylines{ t = \begin{bmatrix} a' & 0 \\ b' & d' \end{bmatrix} \\ \begin{bmatrix} a & c \\ b & d \end{bmatrix} = \begin{bmatrix} 1 & s \\ 0 & 1 \end{bmatrix} \cdot t = \begin{bmatrix} 1 & s \\ 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} a' & 0 \\ b' & d' \end{bmatrix} = \begin{bmatrix} a' + s \cdot b' & s \cdot d' \\ b' & d' \end{bmatrix} \\ \begin{cases} a = a' + s \cdot b' \\ b = b' \\ c = s \cdot d' \\ d = d' \end{cases} \Rightarrow \begin{cases} s = \frac{c}{d} \\ t = \begin{bmatrix} a - s \cdot b & 0 \\ b & d \end{bmatrix} \end{cases} \Rightarrow t = \begin{bmatrix} a - \frac{c}{d} \cdot b & 0 \\ b & d \end{bmatrix} } $$

  1. Apply the new shearing

$$ \displaylines{ \Delta shear_x= \begin{bmatrix} 1 & \frac{p_x}{h} \\ 0 & 1 \end{bmatrix} \\ t'_x = \Delta shear_x \cdot t= \begin{bmatrix} 1 & \frac{p_x}{h} \\ 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} a - \frac{c}{d} \cdot b & 0 \\ b & d \end{bmatrix} = \begin{bmatrix} a + (\frac{p_x}{h} - \frac{c}{d}) \cdot b & \frac{p_x}{h} \cdot d \\ b & d \end{bmatrix} } $$

Doing the same for skewY results in:
WRONG - we must figure out how to extract scaleX/skewX

$$ \displaylines{ s = \frac{b}{a} \\ \Delta shear_y= \begin{bmatrix} 1 & 0 \\ \frac{p_y}{w} & 1 \end{bmatrix} \\ t = \begin{bmatrix} a & c \\ 0 & d - \frac{b}{a} \cdot c \end{bmatrix} \\ t'_y= \Delta shear_y \cdot t = \begin{bmatrix} a & c \\ \frac{p_y}{w} \cdot a & d + (\frac{p_y}{w} - \frac{b}{a}) \cdot c \end{bmatrix} } $$

  1. Another approach:

$$ \displaylines{ T = T_{translate} \cdot T_{rotate} \cdot T_{scale} \cdot T_{skewX} \cdot T_{skewY} \\ T_{no skew} = T_{translate} \cdot T_{rotate} \cdot T_{scale} = T \cdot T_{skewY} ^{-1} \cdot T_{skewX} ^{-1} \\ T_{apply Skew}(skewX', skewY') = T_{no skew} \cdot T_{skewX'} \cdot T_{skewY'} \\ } $$

I have managed to get skewX to work with both approaches. SkewY however...

TO DO

  • verify skewing one axis with existing skew value on the other
  • origin switching: change or account for size when skewing against direction to fit the mouse more accurately
  • skewing inside group

In Action

tracking mouse
multiple interactions
skewY

@ShaMan123 ShaMan123 changed the title fix() skewing controls fix(): skewing controls Oct 17, 2022
@asturur
Copy link
Member

asturur commented Oct 17, 2022

let's address the i don't trust qrDecompose first:

The fact you don't trust qrDecompose is because you don't accept that that has a narrow scope.
Given a series of transformations done in fabricjs order, or in an order that is equivalent, you obtain a matrix. qrDecompose is going to give you a set of EQUIVALENT parameters that represent that transformations if you apply them again in fabricJS order.
It can't tell you more because everything as been mashed together.
if i tell you 6, you can answer me with 5 + 1, 3 * 2, 6, 4 + 2. Are all valid answers and i can't pretend you guess the original one.
That is what qrDecompose is doing and is doing it good.

qrDecompose is a pillar of fabricJS geometry and a constrain, and we can't avoid using it, it would come at the cost of a lot of calculations when skew is involved.

Let's give the proper introduction to those bugs

Skew controls, compared to other controls, have an harsh logic.
They have visual bugs when both skewX and skewY are mixed together.
Skewing on both axis is an edge case and our brain difficultly wraps around it.

Which is this always buggy behaviour?
Let's start from the non buggy one.
Take a rectangle and skew it along one axe only, X or Y.

image

If you press down shift and start skewing, you will see that with one skew only, the mouse always follow the control and all is good, you don't ecounter surprises.

When both skew are used, you will see that the skew controls start to move at a different speed compared to the mouse cursor. this becase the calculation that are there can't reverse engineer this question:
Given an object with

  • angle A,
  • scale 4,2
  • skewX : 33
  • skewY: 12
    taken the mouse at position x,y on the control for skewY
    moving the mouse to x1 = x +3 and y1+ y+1
    what value i have to give to skewY in order for the control to be the closest to the mouse cursor but still on the line determined by the side of the bounding box where the skew control is located?

I didn't answer that question, i didn't even try after a couple of attempts and we should be limited to pay a little price in code complexity for that to work.

Probably to improve the situation one thing to look at is to determine why incremental skew at every mouse move diverge so much. Is also non linear, because some combinations of skewX and skewY are just a rotation, and that means that this skewY control is going to move along the mouse as a sort of function of the angle too.

Maybe the skew control needs to just add and remove skew to the object withtout trying to reduce the calculation at the different sizes of the bounding boxes with and without

The issue with skew controls is the predictability of the next value of skew to assign when both skew are involved

how does inkscape handle it?

Inkscape use qrDecompose more.
If i remember correctly after skewing they reset the object transform with a sort of an equivalent trasnform and kill one of the skews. In practice i think they they use the equivalent of qrDecompose MORE.

Can we see how other interactive tool solve this issue with double skewing before trying to fix the code?

I ask this because we can't pursue a bug that is generated by a UI choice without first validating that the UI choice is good.
Maybe the fix is small, maybe is complicated and a time sink, but is not a bug in the skewing logic itself but on the expectations we set on the mouse movements and boundig box reactions.
I want we look at something similar first and see how it gets handled visually

Example of things i don't want too see or talk about

  • a binary search in future skew values that will make the mouse and the control conincident
    (i ll add more if i can think of it)

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Oct 17, 2022

Great context.
Thank you! Much appreciated.
What I saw after reading the skew controls (when I opened the PR) is that as you mentioned it doesn't start from the current skew state but starts from 0.
That seems a bug to me. That is why skew jumps back/flickers once you do a mouse down or use setDimensions

a binary search in future skew values that will make the mouse and the control coincident

they use the equivalent of qrDecompose MORE.

Can't guess what these mean.
What is the equivalent of qrDecompose MORE?

how does inkscape handle it?

Could you add visuals?

@asturur
Copy link
Member

asturur commented Oct 17, 2022

i need to install inkscape and record but i think is something to compare with and better than nothing.
I don't have adobe tools but maybe they do something as well.

I think that Inkscape normalize the bounding box each transformation to reduce issues and complexity, that is what i meant with use qrDecompose more. It flattens the transformation to remove one skew factor.

a binary search in future skew values that will make the mouse and the control conincident
^^^ I meant don't start search the correct skewY value exahustively jumping from 90 deg to 0, in order to guess the correct one, because that is a solution, but i don't want to use it

@asturur
Copy link
Member

asturur commented Oct 17, 2022

Inkscape normalize the bounding box on rotation and not in skew.
They get double skewing right, meaning no JUMPS, but they also have the control desynced with the mouse.
Probably jumps are removed if we remove the dependency from the bounding box size.
We do guess for the current transformation how much skew should be added for a mouse movement and we add eithout trying to match any position.

That would probably remove weird jumps entirely

progress

progress(): position

major progress

fix(): flip

cleanup

no multiply

good state!

fix(): successive skewing

cleaup
@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Oct 18, 2022

I have done a major refactoring that will probably enable not using qrDecompose for skewing (in case one day qrDecompose will fade out and matrix will be the only thing defining object transform).
Fixed the second skewing interaction - skewing is now maintained.
I will add an explanation soon.

Please check the interaction for accuracy.
I will need help to finalize skewY

Copy link
Contributor Author

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Updated description

  • origin switching is weird IMO now that successive interactions are supported
    origin switch

  • skewY after skewX is broken seems to be FIXED
    skewY after skewX

Please review

Copy link
Contributor Author

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed tests

this.set('scaleX', scaleX);
this.set('scaleY', scaleY);
this.angle = angle;
this.skewX = skewX;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a case that you warned from
I tried calling set instead of manual assignment but since polys run a side effect this breaks.
I can set initialized=false so it won't run.

Copy link
Member

@asturur asturur Oct 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intialized false has to go away, do not make it more necessary that it is now.

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Oct 18, 2022

The fact you don't trust qrDecompose is because you don't accept that that has a narrow scope.

To me that is dangerous since I can't use it everywhere and for everything so I keep my guards up.
IMO (could be a matter of taste) remembering such details is an overkill and in general feels fragile.
I avoid impl that don't cover all usable cases. And if by chance I must I rename them as DANGEROUS_XXX

@ShaMan123 ShaMan123 requested a review from asturur October 27, 2022 23:02
Copy link
Contributor Author

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

READY after comments and extensive skewing with:

  • scaling
  • flipping
  • rotating
  • initial skewing
  • double skewing
  • vpt
  • successive skewing in all the above

@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Oct 27, 2022

seems 91c5d72 fixed #8380 (comment) (files were displayed in B units, not KB)

@asturur
Copy link
Member

asturur commented Oct 28, 2022

Ok so i tried it, and the flip behaviour is changed.
Is not the end of the world but i m trying to fix it, with few result.
If i manage i ll add a PR over thi PR or i ll let it go

You can compare a single flip target with master to see that some direction changes when skewing and it just feels innatural.

TS provided!
I started typing controls and it alerted the flip doesn't exist on `transform` so I found this bug that otherwise what have stayed hidden
@ShaMan123
Copy link
Contributor Author

ShaMan123 commented Oct 28, 2022

So funny
I just found the bug and pushed the fix and then instantly received your comment
Try now!

@ShaMan123
Copy link
Contributor Author

You can compare a single flip target with master to see that some direction changes when skewing and it just feels innatural.

If this continues please share a screen cast


const finalHandler = wrapWithFireEvent(
'skewing',
wrapWithFixedAnchor((eventData, transform, x, y) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this affects perf?

@asturur
Copy link
Member

asturur commented Oct 28, 2022

Going to retry now, i was literally adding -1 and 1 randomly around with the flipX !== flipY logic

Copy link
Contributor Author

@ShaMan123 ShaMan123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fixed now
But I have seen another related bug that exists on master as well
I think it happens only when skewing Y and when skewY * skewX < 0, then target is kind of flipped

In the video I am pulling the mr corner

Parcel.Sandbox.-.Google.Chrome.2022-10-28.11-15-18.mp4

@asturur
Copy link
Member

asturur commented Oct 28, 2022

yes now it behaves more consistently.
Any other eventual change in behaviour is irrelevant for me.

Good pr!

@asturur asturur merged commit d3e09e7 into master Oct 28, 2022
@asturur asturur deleted the fix-skewing-controls branch November 1, 2022 23:37
frankrousseau pushed a commit to cgwire/fabric.js that referenced this pull request Jan 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants