Coordinates
Given a 2d space, we can fix a coordinate system to talk about transformations numerically.
The red vector, that ends at (1, 0) and the green vector that ends at (0, 1) are called the canonical (conventional) basis of the space.
Linear Transformations
A transformation is a function defined from \$R^2\$ to \$R^2\$ that may take any point and translate (move) it to any other point, but in this instance I will only work with Linear Transformations (adapting the program for any transformation is easy though).
Linear Transformations are Transformations for which:
Grid lines remain parallel and evenly spaced (algebraically): $$T(ax + by) = aT(x) + bT(y)$$
The origin remains fixed:
$$T(0) = 0$$
It is easy to show (and very important in their study) that a linear transformation is fully defined by its effects on the basis vectors, that are just four numbers.
These numbers are usually put in a matrix A associated with T:
$$A=\begin{pmatrix}a&b\\c&d\end{pmatrix}$$
Such that $$T(1, 0) = (a, b) $$
$$ T(0, 1) = (c, d) $$
(Putting these results in rows or columns is just a convention).
For example this linear transformation is represented by my (by row) convention as
$$A= \begin{pmatrix} 1 & -2\\ 0.5 & 2 \end{pmatrix}$$
As you can see by noting where the first red vector lands (1, -2) and where the green second vector lands (0.5, 2)
Edge cases
An interesting rarity is if the two rows of vectors (or even columns) are linearly dependent (that is, one can be written as a number (scalar) multiplied by the other), the whole space gets "squished" into a line, or in the degenerate case [ [0,0],[0,0] ] into single point.
This shows the result of the transformation
$$A= \begin{pmatrix} 1 & -2\\ 2 & -4 \end{pmatrix}$$
Or more generally
$$A= \begin{pmatrix} 1 & -2\\ k & -2k \end{pmatrix}$$
For any real \$k\$.
This underwhelming image below is the result of the very degenerate matrix
$$A= \begin{pmatrix} 0 & 0\\ 0 & 0 \end{pmatrix}$$
My program
Now you are fully prepared to understand my program.
As you might have guessed, all the images on this post are the end result of running my animation on these example inputs I have given, but the program also animates the transformation by showing all intermediate steps in the deformation of space.
Here you can see a video demo of the program
Colors
My program gives some options for coloring:
- Just the basis
- Red the "right-er", Green the higher
- Whither the nearest to the center
All of the coloring are related to the position of the original points to make it easier to track where a point/line/region is going.
Code:
import pygame
from pygame.locals import *
import time
import numpy
WIDTH, HEIGHT = 600, 600
UNIT = 100
STEP = 0.01
screen = pygame.display.set_mode((WIDTH, HEIGHT))
def transform_point( (x, y) , a, b, c, d):
return (a*x + b*y, c*x + d*y)
def transform_point_basis( (x, y), (e1x, e1y), (e2x, e2y) ):
return transform_point( (x, y), e1x, e2x, e1y, e2y)
# In case you prefer just points, no lines
#points = [ [(x, y) for x in range(-WIDTH//2, WIDTH//2, UNIT ) ] for y in range(-HEIGHT//2, HEIGHT//2, UNIT) ]
# Builds the grid lines.
# Weird range for computer to cartesian coordinates
points = [ [(x, y) for x in range(-WIDTH//2, WIDTH//2) if x % UNIT == 0 or y % UNIT == 0] \
for y in range(-HEIGHT//2, HEIGHT//2) ]
def twod_map(f, xss):
return [ [f(item) for item in xs] for xs in xss]
def color_bases( (x,y) ):
"""
Colors the 1-st canonical base (0,1) red,
The 2-nd canonical base (1,0) green
"""
if ( distance_from_o( (x,y) ) < UNIT and y == 0 and x > 0):
return (255, 0, 0)
if ( distance_from_o( (x,y) ) < UNIT and x == 0 and y >0):
return (0, 255 ,0)
return (255, 255, 255)
def bright_by_distance( (x,y) ):
return (255 - distance_from_o( (x,y) ) // 3 % 256, \
255 - distance_from_o( (x,y) ) // 3 % 256, \
255 - distance_from_o( (x,y) ) // 3 % 256)
def color_up_right( (x,y) ):
"""
The most right a point was in the original state, the red-der it is.
The most height a point was in the original state, the green-er it is.
Does not work for size > 3*255.
"""
return (int(x + WIDTH//2)//3%255, int(-y + HEIGHT//2)//3%255, 0)
def main(final_coefficients, color_func=color_bases):
for percentage in numpy.arange(0, 1, STEP):
final_a, final_b, final_c, final_d = final_coefficients
# In identity matrix, a and d start at one and c and c start from 0
# In fact transform_point( point , 1, 0, 0, 1) = point
# So to represent the transformation a and d must start
# similar to 1 and become more and more similar to the final
a = 1 * ( (1 - percentage) ) + percentage * final_a
d = 1 * ( (1 - percentage) ) + percentage * final_d
b = percentage * final_b
c = percentage * final_c
koefficients = (a,b,c,d) #map(lambda k: float(k) * (float(percentage)) , final_coefficients)
show_points( twod_map(lambda p: transform_point(p, a,b,c,d), points), points, color_func)
# Be sure final state is precise
show_points( twod_map(lambda p: transform_point(p, *final_coefficients), points), points, color_func)
def main_basis(base_effect1, base_effect2, color_func=color_bases):
final_coefficients =base_effect1[0], base_effect2[0], base_effect1[1], base_effect2[1]
main(final_coefficients, color_func=color_func)
def distance_from_o(p):
return int ( (p[0]**2 + p[1]**2)**0.5 )
def to_cartesian( (x,y), width=WIDTH, height=HEIGHT ):
return int(x + width//2), int(-y + height//2)
def draw_basic_grid(screen, grid):
pygame.display.flip()
screen.fill( (0,0,0) )
for l in grid:
for p in l:
coords = to_cartesian(p)
screen.set_at( coords, (50, 50, 50))
def show_points(points, originals, color_func=color_bases):
draw_basic_grid(screen, originals)
# Original points are needed for coloring.
for (line, lineorig) in zip(points, originals):
for (point, original) in zip(line, lineorig):
screen.set_at( to_cartesian(point), \
color_func( (original) ))
main_basis( (-2, 1),
(-1, 2),
color_func = color_bases)
$$A= \begin{pmatrix} 1 & -2\\ 0.5 & 2 \end{pmatrix}$$
matrix, because I'm note sure it was correct before (there were some stray*
). \$\endgroup\$