QB64 Phoenix Edition
What am I missing here? - Printable Version

+- QB64 Phoenix Edition (https://staging.qb64phoenix.com)
+-- Forum: QB64 Rising (https://staging.qb64phoenix.com/forumdisplay.php?fid=1)
+--- Forum: Code and Stuff (https://staging.qb64phoenix.com/forumdisplay.php?fid=3)
+---- Forum: Help Me! (https://staging.qb64phoenix.com/forumdisplay.php?fid=10)
+---- Thread: What am I missing here? (/showthread.php?tid=868)



What am I missing here? - TerryRitchie - 09-08-2022

Over the past week I've been on a trigonometry quest to better understand the math needed for degree, radian, and vector math for my tutorial. The previous tutorial was lacking in this area and I'm changing that.

I've been using a routine written by Galleon back in 2009 to rotate a sprite using trig and _MAPTRIANGLE. I thought I would see if I could rewrite it using new commands that have come out since then like _HYPOT, _ATAN2, _D2R, etc..

The code below is my attempt at this, and it works. But still not nearly as efficient as Galleon's. I have marked Galleon's lines of code and my lines of code with REM statements, ' ******* RITCHIE and ' ******* GALLEON.

I've stared and stared at his lines 59, 60, 70, and 71 (Galleon's calculations) and I'll be damned if I can make sense of them. Could someone please explain to me how his lines of code achieve the same thing I'm doing? What am I missing here?

Code: (Select All)
'** SPRITE ROTATION

DIM Img AS LONG
DIM RotImg AS LONG

Img = _NEWIMAGE(50, 100, 32)
_DEST Img
CLS
LINE (0, 0)-(49, 99), , BF
_DEST 0

SCREEN _NEWIMAGE(640, 480, 32)
CLS

_PUTIMAGE (100, 100), Img ' original image

RotateImage 30, Img, RotImg

_PUTIMAGE (200, 100), RotImg ' rotated image




SUB RotateImage (Degree AS SINGLE, InImg AS LONG, OutImg AS LONG)

    DIM px(3) AS INTEGER
    DIM py(3) AS INTEGER
    DIM Left AS INTEGER
    DIM Right AS INTEGER
    DIM Top AS INTEGER
    DIM Bottom AS INTEGER
    DIM v AS INTEGER
    DIM RotWidth AS INTEGER
    DIM RotHeight AS INTEGER
    DIM Xoffset AS INTEGER
    DIM Yoffset AS INTEGER

    DIM Rotate AS SINGLE '     ******* RITCHIE
    DIM NewRadian AS SINGLE '  ******* RITCHIE
    DIM Distance AS SINGLE '   ******* RITCHIE

    DIM COSr AS SINGLE '       ******* GALLEON
    DIM SINr AS SINGLE '       ******* GALLEON
    DIM x AS SINGLE '          ******* GALLEON
    DIM y AS SINGLE '          ******* GALLEON

    IF OutImg THEN _FREEIMAGE OutImg
    px(0) = -_WIDTH(InImg) / 2 '                                                 -x,-y ------------------- x,-y
    py(0) = -_HEIGHT(InImg) / 2 '                   Create points around (0,0)    p(0) |                 | p(3)
    px(1) = px(0) '                                 that macth the size of the         |                 |
    py(1) = _HEIGHT(InImg) / 2 '                    original image. This creates       |        .        |
    px(2) = _WIDTH(InImg) / 2 '                     four vector quantities to          |       0,0       |
    py(2) = py(1) '                                 work with.                         |                 |
    px(3) = px(2) '                                                               p(1) |                 | p(2)
    py(3) = py(0) '                                                               -x,y ------------------- x,y

    'Rotate = _D2R(Degree) ' ******* RITCHIE                        convert to radian rotation

    SINr = SIN(-Degree / 57.2957795131) ' ******* GALLEON
    COSr = COS(-Degree / 57.2957795131) ' ******* GALLEON

    DO '                                            cycle through vectors

        'Distance = _HYPOT(px(v), py(v)) '           ******* RITCHIE  get distance to vector
        'NewRadian = _ATAN2(py(v), px(v)) + Rotate ' ******* RITCHIE convert vector to radian then add rotation
        'px(v) = COS(NewRadian) * Distance '         ******* RITCHIE convert radian to vector with correct distance
        'py(v) = SIN(NewRadian) * Distance '         ******* RITCHIE


        x = px(v) * COSr + SINr * py(v) ' ******* GALLEON
        y = py(v) * COSr - px(v) * SINr ' ******* GALLEON
        px(v) = x '                       ******* GALLEON
        py(v) = y '                       ******* GALLEON


        IF px(v) < Left THEN Left = px(v) '         keep track of new image size
        IF px(v) > Right THEN Right = px(v)
        IF py(v) < Top THEN Top = py(v)
        IF py(v) > Bottom THEN Bottom = py(v)
        v = v + 1 '                                 increment vector counter
    LOOP UNTIL v = 4 '                              leave when all vectors processed
    RotWidth = Right - Left + 1 '                   calculate width of rotated image
    RotHeight = Bottom - Top + 1 '                  calculate height of rotated image
    Xoffset = RotWidth / 2 '                        place (0,0) in upper left corner of rotated image
    Yoffset = RotHeight / 2
    v = 0 '                                         reset corner counter
    DO '                                            cycle through rotated image coordinates
        px(v) = px(v) + Xoffset '                   move image coordinates so (0,0) in upper left corner
        py(v) = py(v) + Yoffset
        v = v + 1 '                                 increment corner counter
    LOOP UNTIL v = 4 '                              leave when all four corners of image moved
    OutImg = _NEWIMAGE(RotWidth, RotHeight, 32) '   create rotated image canvas
    _MAPTRIANGLE (0, 0)-(0, _HEIGHT(InImg) - 1)-(_WIDTH(InImg) - 1, _HEIGHT(InImg) - 1), InImg TO(px(0), py(0))-(px(1), py(1))-(px(2), py(2)), OutImg
    _MAPTRIANGLE (0, 0)-(_WIDTH(InImg) - 1, 0)-(_WIDTH(InImg) - 1, _HEIGHT(InImg) - 1), InImg TO(px(0), py(0))-(px(3), py(3))-(px(2), py(2)), OutImg

END SUB



RE: What am I missing here? - luke - 09-08-2022

The 57.3 division factor is 180 / pi, a change from degrees to radians.

Rob's code first defines four characteristic points, the corners. If we can work out how to transform them, we can just redraw the triangles that connect them and the whole image will be rotated.

Each of the four points is a 2D (x, y) vector. It is a well known result from linear algebra that a vector can be rotated through an angle a by multiplying it by the rotation matrix:
Code: (Select All)
.-                -   -     -                  -     -
| cos(a)   -sin(a) | |x| = |x cos(a) - y sin(a) | = |x'|
| sin(a)    cos(a) | |y|   |x sin(a) + y cos(a) |   |y'|
.-                -   -     -                  -     -
So we have x' = x cos(a) - y sin(a), y' = x sin(a) + y cos(a).

Rob's SINr and COSr are sin(a) and cos(a) respectively. These lines:
Code: (Select All)
        x = px(v) * COSr + SINr * py(v) ' ******* GALLEON
        y = py(v) * COSr - px(v) * SINr ' ******* GALLEON
then apply the formulas above with the caveat that x & y are swapped. This is just a matter of convention, the original matrix could have been written differently (I think it's just a matter of which way your rotation is going).


RE: What am I missing here? - TerryRitchie - 09-08-2022

(09-08-2022, 01:40 PM)luke Wrote: The 57.3 division factor is 180 / pi, a change from degrees to radians.

Rob's code first defines four characteristic points, the corners. If we can work out how to transform them, we can just redraw the triangles that connect them and the whole image will be rotated.

Each of the four points is a 2D (x, y) vector. It is a well known result from linear algebra that a vector can be rotated through an angle a by multiplying it by the rotation matrix:
Code: (Select All)
.-                -   -     -                  -     -
| cos(a)   -sin(a) | |x| = |x cos(a) - y sin(a) | = |x'|
| sin(a)    cos(a) | |y|   |x sin(a) + y cos(a) |   |y'|
.-                -   -     -                  -     -
So we have x' = x cos(a) - y sin(a), y' = x sin(a) + y cos(a).

Rob's SINr and COSr are sin(a) and cos(a) respectively. These lines:
Code: (Select All)
        x = px(v) * COSr + SINr * py(v) ' ******* GALLEON
        y = py(v) * COSr - px(v) * SINr ' ******* GALLEON
then apply the formulas above with the caveat that x & y are swapped. This is just a matter of convention, the original matrix could have been written differently (I think it's just a matter of which way your rotation is going).

It's a matrix then. Thank you for pointing that out. I just read the Wikipedia page on the 2D rotation matrix and now it makes a bit more sense. Yes, you're right about rotation direction. Use a positive angle for counter-clockwise rotation and a negative angle (as Galleon did) for clockwise rotation according to the Wikipedia article.

So it would appear I need to read up on matrix math now and play around with this matrix to get a better understanding.

Thanks for the explanation Luke.


RE: What am I missing here? - bplus - 09-08-2022

Here image rotation from the standpoint of point rotations, might be easier to understand?
Code: (Select All)
_Title "Rotate Point" 'b+ 2022-09-08

Screen _NewImage(800, 600, 32)

rotateCenterX = _Width / 2
rotateCenterY = _Height / 2

' test rotate point about the middle of the screen  start point at 300, 100 and rotate 1/12 circle = 30 degree or pi/12 radians
x = 300: y = 100
Circle (x, y), 1, &HFF0000FF

For a = _Pi(2 / 12) To _Pi(2 - 1 / 6) Step _Pi(1 / 6)
    Cls
    Print "rotated"; _R2D(a); " degrees,  press any to continue demo"
    Circle (x, y), 1, &HFF0000FF
    drawRotate x, y, rotateCenterX, rotateCenterY, a
    Sleep
Next

' OK make a rectangle image  50x100
Line (0, 0)-(49, 99), &HFFFFFF00, BF
rect& = _NewImage(50, 100, 32)
_PutImage , 0, rect&, (0, 0)-(49, 99)
_PutImage (_Width - _Width(rect&), 0), rect&, 0
_PutImage (0, _Height - _Height(rect&)), rect&, 0
_PutImage (_Width - _Width(rect&), _Height - _Height(rect&)), rect&, 0
For a = 0 To _Pi(2) - .001 Step _Pi(1 / 6)
    Cls
    rotateImage rotateCenterX, rotateCenterY, rect&, a, 0
    Print "Rotated"; _R2D(a)
    Sleep
Next

Sub rotateImage (aboutX, aboutY, Img&, radianRot, dest&)
    Dim px(1 To 4), py(1 To 4), rotx(1 To 4), roty(1 To 4)
    w = _Width(Img&) / 2 ' these are 1/2 widths and heights for faster calc
    h = _Height(Img&) / 2
    px(1) = aboutX - w: py(1) = aboutY - h
    px(2) = aboutX - w: py(2) = aboutY + h
    px(3) = aboutX + w: py(3) = aboutY + h
    px(4) = aboutX + w: py(4) = aboutY - h
    For i = 1 To 4
        Rotate px(i), py(i), aboutX, aboutY, radianRot, rotx(i), roty(i)
    Next
    w = _Width(Img&) - 1 'for source coordinates
    h = _Height(Img&) - 1
    _MapTriangle (0, 0)-(0, h)-(w, h), Img& To(rotx(1), roty(1))-(rotx(2), roty(2))-(rotx(3), roty(3)), dest&
    _MapTriangle (0, 0)-(w, 0)-(w, h), Img& To(rotx(1), roty(1))-(rotx(4), roty(4))-(rotx(3), roty(3)), dest&
End Sub

Sub drawRotate (x, y, aboutX, aboutY, rotRA)
    radius = _Hypot(y - aboutY, x - aboutX)
    angle = _Atan2(y - aboutY, x - aboutX)
    rotA = angle + rotRA
    'rotated x, y point
    rotX = aboutX + radius * Cos(rotA)
    rotY = aboutY + radius * Sin(rotA)
    Circle (rotX, rotY), 1, &HFFFFFF00
End Sub

' rotate point x,y about point aboutX, aboutY, Radian Angles, (output) rotX, (output) rotY
Sub Rotate (x, y, aboutX, aboutY, rotRA, rotX, rotY)
    radius = _Hypot(y - aboutY, x - aboutX)
    angle = _Atan2(y - aboutY, x - aboutX)
    rotA = angle + rotRA
    'rotated x, y point
    rotX = aboutX + radius * Cos(rotA)
    rotY = aboutY + radius * Sin(rotA)
End Sub

Edit: fixed error in the 2 _MapTriangle lines.


RE: What am I missing here? - bplus - 09-08-2022

OK fixed.

We were just 2 steps away from Classic RotoZoom. Incorporate the RotatePoint code into the RotateAndZoomImage sub and add a scale for the Destination points to grow or shrink the radius:
Code: (Select All)
_Title "Rotate and Zoom from Point perspective" 'b+ 2022-09-08

Screen _NewImage(800, 600, 32)

rotateCenterX = _Width / 2
rotateCenterY = _Height / 2

' OK make a rectangle image  50x100 and rotate it around the center of the screen in 30 degree increments
Line (0, 0)-(49, 99), &HFFFFFF00, BF
rect& = _NewImage(50, 100, 32)
_PutImage , 0, rect&, (0, 0)-(49, 99)
scale = .5
For a = 0 To _Pi(2) - .001 Step _Pi(1 / 12)
    Cls
    RotateAndZoomImage rotateCenterX, rotateCenterY, rect&, scale, a, 0
    Print "Rotated"; _R2D(a); "  scaled at:"; scale
    scale = scale + .1
    Sleep
Next

Sub RotateAndZoomImage (aboutX As Long, aboutY As Long, Img&, xyScale, radianRot, dest&)
    Dim px(1 To 4), py(1 To 4), rotx(1 To 4), roty(1 To 4)
    w = _Width(Img&) / 2 ' these are 1/2 widths and heights for faster calc of destination coordinates
    h = _Height(Img&) / 2
    px(1) = aboutX - w: py(1) = aboutY - h
    px(2) = aboutX - w: py(2) = aboutY + h
    px(3) = aboutX + w: py(3) = aboutY + h
    px(4) = aboutX + w: py(4) = aboutY - h
    radius = _Hypot(py(1) - aboutY, px(1) - aboutX) ' radius is all the same
    For i = 1 To 4
        angle = _Atan2(py(i) - aboutY, px(i) - aboutX)
        rotA = angle + radianRot
        'rotated x, y point
        rotx(i) = aboutX + xyScale * radius * Cos(rotA)
        roty(i) = aboutY + xyScale * radius * Sin(rotA)
    Next
    w = _Width(Img&) - 1 'for source coordinates
    h = _Height(Img&) - 1
    _MapTriangle (0, 0)-(0, h)-(w, h), Img& To(rotx(1), roty(1))-(rotx(2), roty(2))-(rotx(3), roty(3)), dest&
    _MapTriangle (0, 0)-(w, 0)-(w, h), Img& To(rotx(1), roty(1))-(rotx(4), roty(4))-(rotx(3), roty(3)), dest&
End Sub



RE: What am I missing here? - bplus - 09-09-2022

Here I write up the Alternate RotateAndZoomImage sub with more comments and test it out with demo:

Feature Sub:
Code: (Select All)
Sub RotateAndZoomImage (aboutX, aboutY, Img&, xyScale, radianRot, dest&)
    Dim px(1 To 4), py(1 To 4), rotx(1 To 4), roty(1 To 4)

    ' aboutX, aboutY is the centerPoint for the destination image
    ' Img& is image handle from drawn _newImage or _LoadImage
    ' xyScale is multiplier to shrink or grow image projection from Map Triangle
    ' RadianRot is the angle in radians to turn the image from 0 rotation clockwise when positive
    ' dest& is where you want the projected image drawn 0 is the screen

    ' This part sets up our Destination points for _MapTriangle, 4 points around a Center X, Y point
    w = _Width(Img&) / 2 ' these are 1/2 widths and heights for faster calc of destination coordinates
    h = _Height(Img&) / 2
    ' the 4 points are the central point +- half the width and or height
    px(1) = aboutX - w: py(1) = aboutY - h
    px(2) = aboutX - w: py(2) = aboutY + h
    px(3) = aboutX + w: py(3) = aboutY + h
    px(4) = aboutX + w: py(4) = aboutY - h

    ' the "radius" of 4 points from the center will be same for square or rectangle image
    radius = _Hypot(py(1) - aboutY, px(1) - aboutX) ' radius is all the same

    '  the 4 projection points needs to be rotated
    For i = 1 To 4
        angle = _Atan2(py(i) - aboutY, px(i) - aboutX) ' the angle the point is before rotation
        rotA = angle + radianRot ' add the rotation to angle
        'rotated x, y point
        rotx(i) = aboutX + xyScale * radius * Cos(rotA)
        roty(i) = aboutY + xyScale * radius * Sin(rotA)
    Next

    ' this w, h concerns the Triangle coordinates for the Source _MapTriangle points
    w = _Width(Img&) - 1 'for source coordinates
    h = _Height(Img&) - 1
    _MapTriangle (0, 0)-(0, h)-(w, h), Img& To(rotx(1), roty(1))-(rotx(2), roty(2))-(rotx(3), roty(3)), dest&
    _MapTriangle (0, 0)-(w, 0)-(w, h), Img& To(rotx(1), roty(1))-(rotx(4), roty(4))-(rotx(3), roty(3)), dest&
End Sub



Code: (Select All)
_Title "Test Alternate RotoZoom Derived From Point Rotation" ' b+ 2022-09-09
Screen _NewImage(600, 350, 32)

' from demo code for Lander

' ===========================================   make background snapshot
Color , _RGB32(30, 30, 60)
snapBack& = _NewImage(_Width, _Height, 32)
Cls
DrawTerrain 100, 25, &HFF332211
DrawTerrain 150, 20, &HFF443322
DrawTerrain 200, 15, &HFF554433
DrawTerrain 250, 10, &HFF665544
DrawTerrain 300, 5, &HFF776655
_PutImage , 0, snapBack&

' ========================================== make a spaceship sprite
ship& = _NewImage(61, 31, 32) ' ship is 60 x 30 drawn in top left hand corner
' need black backgrounf for ship
Color , &HFF000000 '= black background
Cls
drawShip 30, 15, &HFF00FF88
_PutImage , 0, ship&, (0, 0)-(61, 31) ' <<<< upper left corner of screen!!!
_ClearColor &HFF000000, ship& ' <<<  make the background black of ship transparent

' ============================================= now for test of Alternate RotoZoom
sx = 0 ' from left edge to right and back
dx = 5 ' 270 / 5 = 54 loops to go from start to mid screen with max tilt there at pi(.25)
tilt = 0
dt = _Pi(.25 / 54)
scale = 1
ds = 1 / 54 ' want to double scale at 54 loops
Do
    _PutImage , snapBack&, 0 ' back to screen
    'rotozoom workes from image center, add 30 for middle of ship  15 add to y keeps ship lower
    RotateAndZoomImage sx + 30, 175 + 15, ship&, scale, tilt, 0 ' ship to screen at destination x, y

    ' update x, scale and tilt
    sx = sx + dx
    If sx > _Width - 60 Then
        sx = _Width - 60: dx = -dx
        scale = 1: tilt = 0
    ElseIf sx < 0 Then
        sx = 0: dx = -dx
        scale = 1: tilt = 0
    ElseIf Abs(sx - 270) < .01 And dx > 0 Then
        dt = -dt: ds = -ds
    ElseIf Abs(sx - 270) < .01 And dx < 0 Then
        dt = -dt: ds = -ds
    End If
    scale = scale + ds
    tilt = tilt + dt
    Locate 1, 1
    Print "Press escape to quit..."
    _Display 'no flicker
    _Limit 20 ' max 20 loops a second
Loop Until _KeyDown(27)


Sub drawShip (x, y, colr As _Unsigned Long) 'shipType     collisions same as circle x, y radius = 30
    Static ls
    Dim light As Long, r As Long, g As Long, b As Long
    r = _Red32(colr): g = _Green32(colr): b = _Blue32(colr)
    fellipse x, y, 6, 15, _RGB32(r, g - 120, b - 100)
    fellipse x, y, 18, 11, _RGB32(r, g - 60, b - 50)
    fellipse x, y, 30, 7, _RGB32(r, g, b)
    For light = 0 To 5
        fcirc x - 30 + 11 * light + ls, y, 1, _RGB32(ls * 50, ls * 50, ls * 50)
    Next
    ls = ls + 1
    If ls > 5 Then ls = 0
End Sub

' ======== helper subs for drawShip that you can use for other things specially fcirc = fill_circle  x, y, radius, color

Sub fellipse (CX As Long, CY As Long, xr As Long, yr As Long, C As _Unsigned Long)
    If xr = 0 Or yr = 0 Then Exit Sub
    Dim h2 As _Integer64, w2 As _Integer64, h2w2 As _Integer64
    Dim x As Long, y As Long
    w2 = xr * xr: h2 = yr * yr: h2w2 = h2 * w2
    Line (CX - xr, CY)-(CX + xr, CY), C, BF
    Do While y < yr
        y = y + 1
        x = Sqr((h2w2 - y * y * w2) \ h2)
        Line (CX - x, CY + y)-(CX + x, CY + y), C, BF
        Line (CX - x, CY - y)-(CX + x, CY - y), C, BF
    Loop
End Sub

Sub fcirc (x As Long, y As Long, R As Long, C As _Unsigned Long) 'vince version  fill circle x, y, radius, color
    Dim x0 As Long, y0 As Long, e As Long
    x0 = R: y0 = 0: e = 0
    Do While y0 < x0
        If e <= 0 Then
            y0 = y0 + 1
            Line (x - x0, y + y0)-(x + x0, y + y0), C, BF
            Line (x - x0, y - y0)-(x + x0, y - y0), C, BF
            e = e + 2 * y0
        Else
            Line (x - y0, y - x0)-(x + y0, y - x0), C, BF
            Line (x - y0, y + x0)-(x + y0, y + x0), C, BF
            x0 = x0 - 1: e = e - 2 * x0
        End If
    Loop
    Line (x - R, y)-(x + R, y), C, BF
End Sub

Sub DrawTerrain (h, modN, c As _Unsigned Long) ' modN for ruggedness the higher the less smooth
    For x = 0 To _Width
        If x Mod modN = 0 Then ' adjust mod number for ruggedness the higher the number the more jagged
            If h < 350 - modN And h > 50 + modN Then
                dy = Rnd * 20 - 10
            ElseIf h >= 350 - modN Then
                dy = Rnd * -10
            ElseIf h <= 50 + modN Then
                dy = Rnd * 10
            End If
        End If
        h = h + .1 * dy
        Line (x, _Height)-(x, h), c
    Next
End Sub

Sub RotateAndZoomImage (aboutX, aboutY, Img&, xyScale, radianRot, dest&)
    Dim px(1 To 4), py(1 To 4), rotx(1 To 4), roty(1 To 4)

    ' aboutX, aboutY is the centerPoint for the destination image
    ' Img& is image handle from drawn _newImage or _LoadImage
    ' xyScale is multiplier to shrink or grow image projection from Map Triangle
    ' RadianRot is the angle in radians to turn the image from 0 rotation clockwise when positive
    ' dest& is where you want the projected image drawn 0 is the screen

    ' This part sets up our Destination points for _MapTriangle, 4 points around a Center X, Y point
    w = _Width(Img&) / 2 ' these are 1/2 widths and heights for faster calc of destination coordinates
    h = _Height(Img&) / 2
    ' the 4 points are the central point +- half the width and or height
    px(1) = aboutX - w: py(1) = aboutY - h
    px(2) = aboutX - w: py(2) = aboutY + h
    px(3) = aboutX + w: py(3) = aboutY + h
    px(4) = aboutX + w: py(4) = aboutY - h

    ' the "radius" of 4 points from the center will be same for square or rectangle image
    radius = _Hypot(py(1) - aboutY, px(1) - aboutX) ' radius is all the same

    '  the 4 projection points needs to be rotated
    For i = 1 To 4
        angle = _Atan2(py(i) - aboutY, px(i) - aboutX) ' the angle the point is before rotation
        rotA = angle + radianRot ' add the rotation to angle
        'rotated x, y point
        rotx(i) = aboutX + xyScale * radius * Cos(rotA)
        roty(i) = aboutY + xyScale * radius * Sin(rotA)
    Next

    ' this w, h concerns the Triangle coordinates for the Source _MapTriangle Points
    w = _Width(Img&) - 1 'for source coordinates
    h = _Height(Img&) - 1
    _MapTriangle (0, 0)-(0, h)-(w, h), Img& To(rotx(1), roty(1))-(rotx(2), roty(2))-(rotx(3), roty(3)), dest&
    _MapTriangle (0, 0)-(w, 0)-(w, h), Img& To(rotx(1), roty(1))-(rotx(4), roty(4))-(rotx(3), roty(3)), dest&
End Sub

So important to understand Rotozoom, so really cool to be able to derive it on your own with a little guidance from Galleon's example.

EDIT to fix spelling