AngusJohnson / Image32

A 2D graphics library written in Delphi Pascal
Boost Software License 1.0
127 stars 31 forks source link

The img32 transform convention is reversed with regard to SVG #82

Closed AngusJohnson closed 1 month ago

AngusJohnson commented 1 month ago
          I see, and this is because the img32 transform convention is reversed to SVG sequence:

Your code (matrixRotate(); matrixScale()) is equivalent with: svgScale(); svgRotate(); SVG adds systems from left (parent) to right (child) to produce the same current matrix, Your example corresponds to my extraction method (M = Z2 Q). So Z2 would deliver the userdefined scalefactor (sy = 1), Extraction method (M = Q Z) delivers the visual (sy = 2), which is Ok as well.

svgRotate() with current code would be:

procedure matrix_addLocalRotate( var matrix: TMatrixD; angle: double);
var mat: TMatrixD;
begin
  mat := identityMatrix;
  MatrixRotate( mat, NullPointD, angle);
  matrix := matrixMultiply( matrix, mat);
end;

Originally posted by @tomwiel in https://github.com/AngusJohnson/Image32/issues/70#issuecomment-2222686931

AngusJohnson commented 1 month ago

What has confused me is that compound transformations in SVG images are processed in reverse order (see comments in Example 5 here), even though the visual appareance[sic] is as if processed left to right.

And this still isn't making sense to me since ...

this SVG ...

<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.2" width="200px" height="200px"
  viewBox="-50 -100 150 100" xmlns="http://www.w3.org/2000/svg">  
  <path transform="rotate(90) scale(4,1)" 
  fill="none" stroke="#AA0000" stroke-width="5" d=" M -30 -30 L -15,-30, -15,30, -30,30" />
</svg>

will scale the path horizontally before rotating, and will be rendered (in Image32 and in browsers) exactly like the following image ...

<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.2" width="200px" height="200px"
  viewBox="-50 -100 150 100" xmlns="http://www.w3.org/2000/svg">  
  <path transform="matrix(0, 4, -1, 0, 0, 0)" 
  fill="none" stroke="#AA0000" stroke-width="5" d=" M -30 -30 L -15,-30, -15,30, -30,30" />
</svg>

test

tomwiel commented 1 month ago

I swear, this comment is not intended to add confusions:)

SVG: The System-transformation (matrix-building (CTM = A B C ... )) goes from left to right (postmultiply), but the Point-transformation (getting global (visual) output from local input) goes oppositely, from right (C) to left (A), mostly done in a single step: globalPoint := CTM localPoint, but can also be seen as: globalPoint := A (B (C * localPoint)),

Image32: Fortunately there is no real bug (but rather inconvenience), because image32 is producing the correct result. The reason: Postmultiplying of nontransposed matrices (SVG matrices) is the same as Premultiplying of transposed matrices (img32 matrices). TMatrixD is already transposed by definition (3 rows, 2 columns, as known from MS APIs).

MatrixMultiply( img32GlobalMatrix, img32LocalMatrix) implements premultiply by transposed local matrix (TMatrixD): result := img32LocalMatrix img32GlobalMatrix; // which is equivalent to: result := transpose( svgGlobalMatrix svgLocalMatrix); // postmultiply by (nontransposed) local matrix result := transpose( transpose( img32GlobalMatrix) transpose( img32LocalMatrix) ); The equations with transpose() are just to show the equivalence, while the implementation is only the first line ( img32LocalMatrix img32GlobalMatrix).

matrix_addLocalRotate( img32CurrentMatrix, angle) implements premultiply by transposed local matrix (TMatrixD): img32CurrentMatrix := img32RotateMatrix img32CurrentMatrix; // which is equivalent to: img32CurrentMatrix := transpose( svgCurrentMatrix svgRotateMatrix); // postmultiply by (nontransposed) local matrix

So image32 could have some functions (matrix_addLocalRotate() and alike) to abstract away the internal implementation: These function calls are in SVG sequence (postmultiply), but the internal implementation is premultiplication of transposed matrices.

AngusJohnson commented 1 month ago

I swear, this comment is not intended to add confusions:)

Too bad, you did🤣. But I'm coming to the conclusion that I've got matrix multiplication back to front.

tomwiel commented 1 month ago

What is missing in my first comment, longer expressions:

SVG: Postmultiply by nontransposed local matrix: ABC = A B C Image32: Premultiply by transposed local matrix: trABC = trC trB trA (Image32 needs Premultiply, because is based on TMatrixD (transposed))

Reversed calling sequence with existing image32 function: trBC := matrixMultiply( trB, trC); // internally (trC trB) trABC := matrixMultiply( trA, trBC); // internally (trBC trA) in short: trABC := (trC trB) trA

SVG sequence with existing image32 function: trAB := matrixMultiply( trA, trB); // internally (trB trA) trABC := matrixMultiply( trAB, trC); // internally (trC trAB) in short: trABC := trC (trB trA)

matrixMultiply() is already SVG compliant, but not so the specific functions: MatrixRotate(), ... Currently they are prepending a new global matrix (SVG parent). Such use-case can be wanted, so these functions could remain. But appending a local matrix (SVG child) is needed more often.

AngusJohnson commented 1 month ago

matrixMultiply() is already SVG compliant, but not so the specific functions: MatrixRotate(), ...

Tom, I suspect I still haven't addressed this in my most recent commit. However I'm currently pretty tied up with other things so, for the time being, I won't be able to give this the attention it still needs. But if you can see an obvious quick fix then I'd be happy to make those changes.

tomwiel commented 1 month ago

No quick/easy fix, but a possible long-term strategy: For readability, it would make sense to abstract multiplications away, to support transform-chains which also look like SVG transforms.

procedure PrependTransform( const GM: TMatrixD; var matrix: TMatrixD); inline;
begin matrixMultiply( matrix, GM); end;  // SVG: prepend global matrix:  CTM := GM * CTM

procedure AppendTransform( var matrix: TMatrixD; const LM: TMatrixD); inline;
begin matrixMultiply2( LM, matrix); end;  // SVG: append local matrix:  CTM := CTM * LM

Or simpler than this wrapping, just rename both matrixMultiply-functions and reverse their params. Then later add specific functions using AppendTransform():

procedure AppendRotate( var matrix: TMatrixD; angle: double);
//  CTM := CTM * R;  is a subcase of current ParseTransform():
var mat: TMatrixD;
begin
  mat := identityMatrix;
  MatrixRotate( mat, NullPointD, angle);
  AppendTransform( matrix, mat); // matrixMultiply2( mat, matrix);
end;

AppendRotate() works, although this is not the most efficient, because MatrixRotate( mat, angle) is internally a PrependTransform( RotationMatrix, mat).

Beyond of this, a few places already use sequences of such PrependTransform() calls. To see SVG chains in source code, these old sequences would need to be replaced with AppendTransform() sequences, and only in a way which creates the same result.

SVG chains start with most global ctmA (current transform matrix A), which is the docking site for the local chain (BCD). SVG: ctm := ctmA B C D; // and Image32 needs to implement this: Image32: trCtm := trD trC trB trCtmA; And for this product (trCtm), two alternative implementations can exist in Image32:

SVG: start with global ctmA (shortname A), then append LOCAL transforms: trCtm := trD (trC (trB trCtmA)); stepwise: 1) trCtm := trCtmA; // starting with most global 2) AppendTransform( trCtm, trB); // trCtm := trAB = tr( A B) = (trB trA) 3) AppendTransform( trCtm, trC); // trCtm := trABC = tr( AB C) = (trC trAB) 4) AppendTransform( trCtm, trD); // trCtm := trABCD = tr( ABC D) = (trD * trABC)

Old sequence, less readable, but the same result: SVG: Start with local D, then prepend GLOBAL transforms: trCtm := (( trD trC) trB) trCtmA; stepwise: 1) trCtm := trD; // starting with most local 2) PrependTransform( trC, trCtm); // trCtm := trCD = tr( C D) = (trD trC) 3) PrependTransform( trB, trCtm); // trCtm := trBCD = tr( B CD) = (trCD trB) 4) PrependTransform( trCtmA, trCtm); // trCtm := trABCD = tr( A BCD) = (trBCD * trA)

AngusJohnson commented 1 month ago

Thanks Tom. I do appreciate the effort you've made trying to explain this to me. Unfortunately I don't currently have the resources to unpick this. My current very simplistic understanding (that could be wrong) is that while SVG transformations are parsed from parent element to nested child element, they need to be applied in reverse order (ie child element before parent element transformations). And I suspect that this is why all SVG transformations need to be handled as AppendTransforms.