You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The img32 transform convention is reversed with regard to SVG #82
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:
procedurematrix_addLocalRotate( var matrix: TMatrixD; angle: double);
var mat: TMatrixD;
begin
mat := identityMatrix;
MatrixRotate( mat, NullPointD, angle);
matrix := matrixMultiply( matrix, mat);
end;
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.
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.
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))
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.
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.
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.
procedurePrependTransform( const GM: TMatrixD; var matrix: TMatrixD); inline;
begin matrixMultiply( matrix, GM); end; // SVG: prepend global matrix: CTM := GM * CTMprocedureAppendTransform( 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():
procedureAppendRotate( 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:
trCtm := trCtmA; // starting with most global
AppendTransform( trCtm, trB); // trCtm := trAB = tr( A * B) = (trB * trA)
AppendTransform( trCtm, trC); // trCtm := trABC = tr( AB * C) = (trC * trAB)
Old sequence, less readable, but the same result:
SVG: Start with local D, then prepend GLOBAL transforms:
trCtm := (( trD * trC) * trB) * trCtmA; stepwise:
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.
Repository owner locked and limited conversation to collaborators on Aug 3, 2024
Activity
AngusJohnson commentedon Jul 13, 2024
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 ...
will scale the path horizontally before rotating, and will be rendered (in Image32 and in browsers) exactly like the following image ...
tomwiel commentedon Jul 13, 2024
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 commentedon Jul 14, 2024
Too bad, you did🤣.
But I'm coming to the conclusion that I've got matrix multiplication back to front.
tomwiel commentedon Jul 15, 2024
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 commentedon Jul 23, 2024
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 commentedon Jul 25, 2024
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.
Or simpler than this wrapping, just rename both matrixMultiply-functions and reverse their params.
Then later add specific functions using AppendTransform():
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:
Old sequence, less readable, but the same result:
SVG: Start with local D, then prepend GLOBAL transforms:
trCtm := (( trD * trC) * trB) * trCtmA; stepwise:
AngusJohnson commentedon Jul 26, 2024
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.