Math help needed: 2D transformation matrix

This is a screenshot next to a picture of a color measurement device app I am developing.
There are thee random reference points on the scan template, visible in the screenshot as the blue one to the upper left (selected Ref. point A) and the two black squares.

User can take the instrument’s arm and collect the “real” coordinate values as the template can be tilted or translated.
And this is where I need help. I would need to calculate a transformation matrix based on the vectors AB/BC or AB/AC and their measured values, and then apply this to the color patches to recalculate their “live” positions.
But the tutorials I found don’t nail that point. Any hints how I can achieve this in Xojo?

Thanks a lot!

OK, I found a JavaScript application I could use. But its usage troubles me. It is not compiled into a node.js, but a collection of source files. Would be nice to have it run in JavaScriptEngineMBS.
Any hints how to set it up? @Christian_Schmitz ?

It shouldn’t be hard to convert the relevant parts into Xojo. You can use an array to hold the matrix values.

I thought so too. Structures would also do.
And some of the code looks pretty straightforward. For other pieces I had to realise I don’t understand Javascript. I mean, what is something like this supposed to do?

export function fromTriangles (t1, t2) {
  // point p = first point of the triangle
  const px1 = t1[0].x != null ? t1[0].x : t1[0][0]

This one?

Its doing this:

If t1[0].x is not null
     px1 = t1[0].x
else
     px1 = t1[0][0]

As long as you keep things simple and don’t allow values to be nil (null) then you can simplify the code.

I would also not get bogged down with converting stuff you don’t need.

!= is <>
? x:y is IIf

i made a few matrix methods in xojo but later i continue in c#
because the matrix library is more complex and ready to use there.

if you want to rotate around a point x,y you move all to 0,0
means translate vectors via subtract, turn it via rotation matrix and push them the same distance back.
you need a angle in radians, and a distance in compare to scale something.

Xojo
Vec3 Class

Public Shared Function Point(x As Double, y As Double, z As Double) As Vec3
  Return New Vec3(x,y,z,1.0)
  
End Function


Public Shared Function Vector(x As Double, y As Double, z As Double) As Vec3
  Return New Vec3(x,y,z,0.0)
  
End Function


Public Shared Function Add(a1 as Vec3, a2 as Vec3) As Vec3
  
  Return New Vec3(a1.x + a2.x, a1.y + a2.y, a1.z + a2.z, a1.w + a2.w)
  
  
End Function


Public Shared Function Subtract(a1 as Vec3, a2 as Vec3) As Vec3
  
  Return New Vec3(a1.x - a2.x, a1.y - a2.y, a1.z - a2.z, a1.w - a2.w)
  
  
End Function


Public Shared Function Negating(a as Vec3) As Vec3
  
  Return New Vec3(-a.x, -a.y, -a.z, -a.w)
  
  
End Function


Public Shared Function MultiplyingByScalar(a As Vec3, scalar As Double) As Vec3
  
  Return New Vec3(a.x * scalar, a.y * scalar, a.z * scalar, a.w * scalar)
  
  
End Function


Public Shared Function DividingByScalar(a As Vec3, scalar As Double) As Vec3
  
  Return New Vec3(a.x / scalar, a.y / scalar, a.z / scalar, a.w / scalar)
  
  
End Function


Public Shared Function Magnitute(a as Vec3) As Double
  
  Return Sqrt((a.x * a.x) + (a.y * a.y) + (a.z * a.z) + (a.w * a.w))
  
  
End Function


Public Shared Function Normalize(a As Vec3) As Vec3
  Var magnitute As Double = Magnitute(a)
  
  If magnitute = 0.0 Then
    Return New Vec3(0.0, 0.0, 0.0, 0.0)
  Else
    Return New Vec3(a.x / magnitute, a.y / magnitute, a.z / magnitute, a.w / magnitute)
  End If
  
End Function


Public Shared Function DotProduct(a as Vec3, b as Vec3) As Double
  
  'auch als Scalar Produkt genannt
  
  Return (a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w)
  
  
End Function


Public Shared Function CrossProduct(a as Vec3, b as Vec3) As Vec3
  
  Return Vec3.Vector(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x)
  
  
  
End Function


Public Shared Function Length(a as Vec3) As Double
  
  Return Magnitute(a)
  
  
End Function


Public Shared Function Reflect(v As Vec3, n As Vec3, bounce As Double) As Vec3
  
  'https://www.3dkingdoms.com/weekly/weekly.php?a=2
  
  ' Vnew = bounce * ( -2*(V dot N)*N + V )
  
  ' where bounce=0.0 means no bounce, and bounce=1.0 means no loss of speed.
  
  Var dotproduct As Double = DotProduct(v, n)
  
  Return Vec3.Add(Vec3.MultiplyingByScalar(Vec3.MultiplyingByScalar(n, -2.0 * dotproduct), bounce),v)
  
  
  
End Function


Public Sub Constructor()
  Self.x = 0.0
  Self.y = 0.0
  Self.z = 0.0
  Self.w = 0.0
  
  
End Sub


Public Sub Constructor(x As Double, y As Double, z As Double, w As Double)
  Self.x = x
  Self.y = y
  Self.z = z
  Self.w = w 
  
End Sub


Public Sub Debug()
  System.DebugLog "X = " + x.ToString + " Y = " + y.ToString + " Z = " + z.ToString  
End Sub


Public Property x As Double = 0


Public Property y As Double = 0


Public Property w As Double = 0


Public Property z As Double = 0

Xojo
Ray Class

Private Sub Constructor()
  
End Sub


Public Sub Constructor(origin As Vec3, direction As Vec3)
  
  Self.Origin = Vec3.Point(origin.x, origin.y, origin.z) 'a point
  Self.Direction = Vec3.Vector(direction.x, direction.y, direction.z) 'a vector
  
End Sub


Public Property Origin As Vec3


Public Property Direction As Vec3


Public Shared Function Position(ray As Ray, t As Double) As Vec3
  
  ' t = time
  
  Var totalDistance As Vec3 = Vec3.MultiplyingByScalar(ray.Direction, t)
  
  Return Vec3.Add(ray.Origin, totalDistance)
  
End Function


Public Shared Function Transform(ray As Ray, m(, ) As Double) As Ray
  
  Var ray2 As New Ray(Vec3.Point(0,0,0),Vec3.Vector(0,0,0))
  
  ray2.Origin = MatrixMultiplyTuple(m, ray.Origin)
  ray2.Direction = MatrixMultiplyTuple(m, ray.Direction)
  
  Return ray2
  
End Function

Xojo
MatrixModule

Public Function Matrix4x4() As double(,)
  'Row, Column
  '0-3, 0-3
  
  Var m(3,3) As Double
  
  Return m
  
  'eine 3x5 Matrix hat 3 Reihen und 5 Spalten
End Function

Public Function MatrixCustomized(rows As Integer, columns As Integer) As double(,)
  'Row, Column
  
  Var m(-1, -1) As Double
  m.ResizeTo(rows-1, columns-1) 
  
  Return m
  
  'eine 3x5 Matrix hat 3 Reihen und 5 Spalten
End Function


Public Sub MatrixDebug(m(, ) As Double)
  System.DebugLog "Debug Matrix"
  For row As Integer = 0 To 3
    Var rowStr As String = "row (" + row.ToString + ")"
    For col As Integer = 0 To 3
      rowStr = rowStr + " , " + Str(m(row,col))
    Next
    System.DebugLog rowStr
  Next
  
  
End Sub




Public Function MatrixEquality(a(, ) As Double, b(, ) As Double) As Boolean
  
  'Gleichheit prüfen
  
  Var equality As Boolean = True
  
  For row As Integer = 0 To 3
    For col As Integer = 0 To 3
      If Equal(a(row, col), b(row, col)) Then
      Else
        equality = False
        Exit
      End If
    Next
    If equality = False Then Exit
  Next
  
  Return equality 
End Function


Public Sub MatrixIdentity(m(, ) As Double)
  For row As Integer = 0 To 3
    For col As Integer = 0 To 3
      m(row,col) = 0.0
    Next
  Next
  
  'diagonal 1
  m(0, 0) = 1.0
  m(1, 1) = 1.0
  m(2, 2) = 1.0
  m(3, 3) = 1.0
  
End Sub




Public Function MatrixMultiply(a(, ) As Double, b(, ) As Double) As Double(,)
  Var c(3,3) As Double
  
  For row As Integer = 0 To 3
    For col As Integer = 0 To 3
      c(row,col) = a(row, 0) * b(0, col) + a(row, 1) * b(1, col) + a(row, 2) * b(2, col) + a(row, 3) * b(3, col) 
    Next
  Next
  
  Return c
  
End Function


Public Function MatrixMultiplyTuple(m(, ) As Double, v As Vec3) As Vec3
  
  Var vout As New Vec3
  
  ' immer eine Reihe der Matrix mit einer Spalte des Tuple
  
  vout.x = Vec3.DotProduct(New vec3(m(0,0),m(0,1),m(0,2),m(0,3)), v)
  vout.y = Vec3.DotProduct(New vec3(m(1,0),m(1,1),m(1,2),m(1,3)), v)
  vout.z = Vec3.DotProduct(New vec3(m(2,0),m(2,1),m(2,2),m(2,3)), v)
  vout.w = Vec3.DotProduct(New vec3(m(3,0),m(3,1),m(3,2),m(3,3)), v)
  
  Return vout
  
End Function


Public Function MatrixRotationX(radians As Double) As Double(,)
  
  ' drehen um die X Achse
  
  Var m(-1,-1) As Double = Matrix4x4()
  
  MatrixIdentity(m)
  
  m(1,1)=Cos(radians)
  m(1,2)=-Sin(radians)
  
  m(2,1)=Sin(radians)
  m(2,2)=Cos(radians)
  
  Return m
End Function


Public Function MatrixRotationY(radians As Double) As Double(,)
  
  ' drehen um die Y Achse
  
  Var m(-1,-1) As Double = Matrix4x4()
  
  MatrixIdentity(m)
  
  m(0,0)=Cos(radians)
  m(0,2)=Sin(radians)
  
  m(2,0)=-Sin(radians)
  m(2,2)= Cos(radians)
  
  Return m
End Function


Public Function MatrixRotationZ(radians As Double) As Double(,)
  
  ' drehen um die Z Achse
  
  Var m(-1,-1) As Double = Matrix4x4()
  
  MatrixIdentity(m)
  
  m(0,0)= Cos(radians)
  m(0,1)=-Sin(radians)
  
  m(1,0)= Sin(radians)
  m(1,1)= Cos(radians)
  
  Return m
End Function


Public Function MatrixScaling(x As Double, y As Double, z As Double) As double(,)
  
  ' zum scalieren
  
  Var m(-1,-1) As Double = Matrix4x4()
  
  MatrixIdentity(m)
  
  m(0,0)=x
  m(1,1)=y
  m(2,2)=z
  
  Return m
End Function


Public Function MatrixShearing(xy As Double, xz As Double, yx As Double, yz As Double, zx As Double, zy As Double) As Double(,)
  Var m(-1,-1) As Double = Matrix4x4()
  
  MatrixIdentity(m)
  
  m(0,1)= xy
  m(0,2)= xz
  
  m(1,0)= yx
  m(1,2)= yz
  
  m(2,0)= zx
  m(2,1)= zy
  
  Return m
End Function





Public Function MatrixTranslation(x As Double, y As Double, z As Double) As double(,)
  
  ' zum verschieben
  
  Var m(-1,-1) As Double = Matrix4x4()
  
  MatrixIdentity(m)
  
  m(0,3)=x
  m(1,3)=y
  m(2,3)=z
  
  Return m
End Function


Public Function MatrixTranspose(a(, ) As Double) As Double(,)
  
  'rows into columns and columns into rows
  
  Var m(3,3) As Double
  
  For row As Integer = 0 To 3
    For col As Integer = 0 To 3
      m(col, row) = a(row, col)
    Next
  Next
  
  Return m 
End Function

Protected Property EPSILON As Double = 0.00001


Public Function Clamp(x As double, _min As Double, _max As Double) As Double
  If (x < _min) Then Return _min
  If (x > _max) Then Return _max
  Return x
  
End Function


Public Function DegreesToRadians(degrees As Double) As Double
  
  Return degrees * Math.PI / 180.0
End Function


Public Function Equal(a As Double, b As Double) As Boolean
  If Abs(a-b)<EPSILON Then
    Return True
  Else
    Return False
  End If

End Function



Math Module

Public Property PI As Double = 3.14159265359

Thanks a lot, all!
I started to rewrite the JS library in Xojo (it has exactly the features I need, plus a few more) and am partially successful. Some unit tests perform like expected, some others fail and I don’t see the reason currently.
Maybe it lies in the weird parameter handling for a Matrix depending on

declare module 'transformation-matrix' {
  type Matrix = {
    a: number;
    b: number;
    c: number;
    d: number;
    e: number;
    f: number;
  };

Which I read as a structure with the double parameters in alphabetical order.

Many functions export this like here, with a weird mixture of double parameters:

export function shear (shx, shy) {
  return {
    a: 1,
    c: shx,
    e: 0,
    b: shy,
    d: 1,
    f: 0
  }

Is that order of importance – will it set {1, shx, 0, shy, 1, 0} or do I have to take the parameter names into account, writing {1, shy, shx, 1, 0,0}?

i think this a: is like a property name.

Thanks again. Turned out it was me, comparing double values too exactly. Now only combined transforms do not work. Here I must be getting something wrong about the recursion the Javascript developer uses for chained matrix multiplies.

Still have the combined transform error, but besides that everything else looks nice. Thanks a lot again!
Think I’ll add a graphic display to see the results in 2D – let me know if someone should need this stuff in pure Xojo too.