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)
1 Answer 1
Looks good, but the structure could be better.
For example, main
gets called by main_basis
. Please don't.
main
should be the top level function.
So let's start by renaming main
to base
(there's probably a better name) and main_basis
to main
. Last couple of lines end up like this:
if __name__ == "__main__":
main( (-2, 1),
(-1, 2),
color_func = color_bases)
So what's this doing halfway between the function definitions?
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) ]
If it's never modified after creation, make it a PSEUDOCONST and put it up top, near the rest of them.
The comment above it is a bit odd as well.
# 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) ]
In case you prefer just points
Smells like an if
to me. Variables to the rescue!
if I_WANT_POINTS_INSTEAD_OF_LINES:
points = [ [(x, y) for x in range(-WIDTH//2, WIDTH//2, UNIT ) ] for y in range(-HEIGHT//2, HEIGHT//2, UNIT) ]
else:
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) ]
The points-instead-of-lines don't really render well on my machine so you might want to check that. It also looks like you need a smaller step size when you want points, or it will be all over too soon. We should probably wrap the whole thing in a function, but it's not a requirement.
from pygame.locals import *
Doesn't look like you're actually using that, so it can be removed.
After polishing away the PEP8 errors and warnings (whitespace in the wrong places, redundant \
line continuations between brackets, lines too long, etc.), and replacing range
by the faster xrange
, the final result could look like this:
import pygame
import time
import numpy
HEIGHT = 600
WIDTH = 600
SCREEN_SIZE = (HEIGHT, WIDTH)
UNIT = 100
STEP = 0.01
I_WANT_POINTS_INSTEAD_OF_LINES = False
if I_WANT_POINTS_INSTEAD_OF_LINES:
POINTS = [[(x, y) for x in xrange(-WIDTH//2, WIDTH//2, UNIT)]
for y in xrange(-HEIGHT//2, HEIGHT//2, UNIT)]
else:
POINTS = [[(x, y) for x in xrange(-WIDTH//2, WIDTH//2)
if x % UNIT == 0 or y % UNIT == 0]
for y in xrange(-HEIGHT//2, HEIGHT//2)]
screen = pygame.display.set_mode(SCREEN_SIZE)
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)
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 base(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
# map(lambda k: float(k) * (float(percentage)) , final_coefficients)
koefficients = (a, b, c, d)
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(base_effect1, base_effect2, color_func=color_bases):
final_coefficients = base_effect1[0], base_effect2[0], \
base_effect1[1], base_effect2[1]
base(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)))
if __name__ == "__main__":
main((-2, 1),
(-1, 2),
color_func=color_bases)
Would you ever want to add functionality I'd probably put the whole thing in a class, but that's a matter of preference.
$$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\$