This post is a continuation on my post from last week on Visualizing Matrix Transformations with gganimate. Both posts are largely inspired by Grant Sanderson’s beautiful video series The Essence of Linear Algebra and wanting to continue messing around with Thomas Lin Peterson’s fantastic gganimate package in R.

As with the last post, I’ll describe trying to (very loosely) recreate a *small* part of the visualizations showing the geometry of matrix multiplication and changing basis vectors (using `gganimate`

in R). (Once again, just in the 2x2 case.)

If you are *really* interested in building visualizations like the ones shown on 3Blue1Brown, you should check-out the associated manim project on github.

# Topics to cover

I had two major sections in the Appendix of last week’s post:

- “Multiple matrix transformations”
- “Potential improvements” (where I mostly describe limitations around visualizing rotations)

This post expands on these topics.

`animatrixr`

and multiple matrix transformations

Sanderson discusses the value in sometimes decomposing a matrix transformation and thinking of its parts sequentially. I created a **toy** package `animatrixr`

for building chained matrix transformations that can then be animated using `gganimate`

^{1}.

The function `animatrixr::add_transformation()`

lets you chain together matrix transformations with R’s pipe operator `%>%`

.

For example, let’s consider three matrix transformations: horizontal sheer –> vertical sheer –> reflection across x-axis:

```
library(dplyr)
if (!requireNamespace("animatrixr")) devtools::install_github('brshallo/animatrixr')
library(animatrixr)
```

```
sheer_horizontal <- tribble(~ x, ~ y,
1, 0.5,
0, 1) %>%
as.matrix()
sheer_vertical <- tribble(~ x, ~ y,
1, 0,
0.5, 1) %>%
as.matrix()
reflect_x <- tribble(~ x, ~ y,
1, 0,
0, -1) %>%
as.matrix()
```

Now let’s visualize the transformations being applied sequentially:

```
matrix(c(1,0,0,1), nrow = 2) %>%
add_transformation(sheer_horizontal) %>%
add_transformation(sheer_vertical) %>%
add_transformation(reflect_x,
seq_fun = animatrixr::seq_matrix_l,
n_frames = 40) %>%
animate_matrix(datasaurus = TRUE)
```

`add_transformation()`

explicitly creates in-between frames for a given transformation. The `seq_fun`

argument allows you to define the interpolation method, for example whether the coordinates should (during the animation) follow a linear path (default) or the angle of a rotation.

It would be nice to add-in functionality where the final transformation object could then be added to layers of a ggplot (though I’ve done nothing towards this except add an argument in `animatrixr::animate_matrix()`

for displaying the datasauRus).

(Warning: `animatrixr`

is severely limited, as discussed in the Appendix and in package documentation. However you can find it at the “brshallo/animatrixr” repo on my github page.)

# Visualizing rotations

The `seq_fun`

argument within `add_transformation()`

specifies frames in-between the start and end states after a matrix transformation. By default it uses `animatrixr::seq_matrix_l`

which changes in-between coordinates linearly (as does `gganimate`

^{2}).

Let’s look at a rotation where the in-between coordinates are interpolated linearly:

```
rotate_90 <- tribble(~ x, ~ y,
cos(pi / 2), -sin(pi / 2),
sin(pi / 2), cos(pi / 2)) %>%
as.matrix()
matrix(c(1,0,0,1), nrow = 2) %>%
add_transformation(rotate_90) %>%
animate_matrix(datasaurus = TRUE)
```

Linear interpolation makes the rotation transformation appear scrunched during the animation (from how we intuitively think of a rotation) as the coordinate points take a straight line path to their positions after applying the transformation^{3}.

To make the in-between coordinates instead follow the angle of rotation we could change the `seq_fun`

from `animatrixr::seq_matrix_l`

to `animatrixr::seq_matrix_lp`

.

```
matrix(c(1,0,0,1), nrow = 2) %>%
add_transformation(rotate_90, seq_fun = animatrixr::seq_matrix_lp) %>%
animate_matrix(datasaurus = TRUE)
```

During the rotation portion of the animation `gganimate`

is still tweening images linearly, however the frames `add_transformation()`

creates are now following along the angle of rotation of the transformation. Hence the animation ends-up approximating a curved path.

However, `seq_matrix_lp()`

needs improvement and was just set-up to work for toy examples – it really only looks ‘right’ if doing rotations off of \[ \left(\begin{array}{cc} 1 & 0\\0 & 1 \end{array}\right)\] See Showing rotations in the Appendix for additional detail on how this is set-up and the various limitations with `animatrixr`

.

Happy animatrixing!

```
# animatrixr::rotation_matrix() is helper function for creating matrix
# transformations of rotations
matrix(c(1,0,0,1), nrow = 2) %>%
add_transformation(animatrixr::rotation_matrix(pi / 2),
seq_fun = animatrixr::seq_matrix_lp) %>%
add_transformation(matrix(c(1, 0.5, 0, 1), nrow = 2)) %>%
add_transformation(matrix(c(1, 0, 0, -1), nrow = 2)) %>%
animate_matrix(datasaurus = TRUE)
```

# Appendix

## Using `animatrixr`

?

This is a toy package (very hastily written). I have not put effort into thinking about making it usable for others. Also, some parts just don’t really work or aren’t set-up quite right… (as noted in the README and elsewhere in the package). But feel free to check-it out / improve it / make something better! Let me know if you do!

This has been a fun dabble into thinking (at least surface level) about animation. Though I don’t have any plans to add onto this (or write any more posts on this topic). If I do add anything, it will most likely just be cleaning-up the decomposition methods in the `seq_matrix*()`

functions. But no plans^{4}

## Notes on seq functions

Below are additional notes on the `animatrixr::seq_matrix*`

functions. They need some work, but here is a description of how they are currently set-up.

### Showing rotations

To animate the rotation of a transformation, `add_transformation(m = matrix(c(0, 1, -1, 0), nrow = 2), seq_fun = seq_matrix_lp)`

explicitly creates in-between frames on the path the points would follow if they were instead following polar coordinates along the angle of rotation. In the next few sections I’ll discuss the process for doing this (again, this is not necessarily an ideal set-up).

Given any 2x2 matrix:

\[ \left(\begin{array}{cc} a & b\\ c & d \end{array}\right)\]

you can use the equation `atan2(c, a)`

to extract the angle of rotation from the matrix^{5} and then create a sequence from the starting angle of rotation to the final angle of rotation.

For example, if my start angle is \(0^\circ\), and final angle of rotation is at \(38^\circ\) and I have 20 frames, then my sequence would be:

\[0^\circ, 2^\circ, ... 38^\circ\]

A rotation matrix is defined as:

\[ \left(\begin{array}{cc} cos(\theta) & -sin(\theta)\\ sin(\theta) & cos(\theta) \end{array}\right)\]

Hence I can convert my sequence of angles into a sequence of matrices that define the rotations applied for each explicit in-between frame.

\[ \left(\begin{array}{cc} cos(0^\circ) & -sin(0^\circ)\\ sin(0^\circ) & cos(0^\circ) \end{array}\right), \left(\begin{array}{cc} cos(2^\circ) & -sin(2^\circ)\\ sin(2^\circ) & cos(2^\circ) \end{array}\right)... \left(\begin{array}{cc} cos(28^\circ) & -sin(28^\circ)\\ sin(28^\circ) & cos(28^\circ) \end{array}\right) \]

`seq_matrix_lp`

applied on non-standard unit basis vectors

If you input a matrix transformation into `seq_matrix_lp`

that is not a pure rotation from the unit vectors it will decompose the matrix into a *rotation* component and *other* component^{6}, the *other* component creates a sequence of matrices that have the in-between frames interpolated linearly. The sequence of *rotation* and *other* matrices are then recomposed to provide the final sequence.

This approach means that non-pure rotations on the unit vectors, etc. will not really look like rotations. I would need to factor in other components (e.g. scale) to improve this.

### Show rotation first

Beyond `seq_matrip_l()`

and `seq_matrix_lp()`

, I made another seq_matrix* function: `seq_matrix_rotate_first`

which (like `seq_matrix_lp`

) also decomposes a matrix into rotation and other components. Rather than interpolating these separately and then recomposing them (as `seq_matrix_lp`

does) `seq_matrix_rotate_first`

works by interpolating them separately and then applying the decomposed sequences sequentially – so the entire rotation component of the transformation will be animated and then the ‘other’ component will be animated (this makes for twice as many frames when there is a ‘rotation’ and ‘other’ component in the transformation matrix).

I.e. starting from our identity matrix and applying a single matrix transformation, it will automatically decompose this and animate the decomposed parts in two steps, \(I\) –> \(R\) and then from \(R\) –> \(M\). Below is an example of the animation for the transformation matrix: \[ \left(\begin{array}{cc} 0 & -1\\1 & -0.5 \end{array}\right)\] (which could be decomposed into a rotation and a sheer part).

```
transformation_matrix <- sheer_vertical %*% animatrixr::rotation_matrix(pi/4)
matrix(c(1,0,0,1), nrow = 2) %>%
add_transformation(transformation_matrix, seq_fun = seq_matrix_rotate_first) %>%
animate_matrix(datasaurus = TRUE)
```

There are (especially) a lot of problems with this function currently and I don’t recommend using it e.g.

- only works (at all correctly) if starting from standard unit vectors (hence cannot really be combined into a chain of matrix transformations)
- rotation component extracted will vary depending on what ‘other’ is within M E.g. if M = {rotation}{vertical sheer} vs. M = {rotation}{horizontal sheer} – rotation component will look different
- I defaulted the amount of frames given to the rotation component to be the same as the amount of frames given to other component. If the size of the rotation is small relative to the other part of the transformation (or vice versa) the timing will feel slow/jumpy.

Provides a cleaner approach for doing this compared to the clunky method I walked through in my post last week.↩︎

All visualizations from last week used this linear interpolation method.↩︎

I discuss this at more length in my previous post – see the sub-section in the “Appendix”, “Problem of squeezing during rotation”.↩︎

However I also hadn’t planned on writing a follow-up post… so who knows…↩︎

To find the ‘other’ component of a matrix transformation… say

*M*represents the overall matrix transformation, in Showing rotations I described how to calculate*R*(the rotation component), hence to calculate*A*, ‘other’, I do:\[AR = M\] \[ARR^{-1} = MR^{-1}\] \[A = MR^{-1}\]↩︎