8
\$\begingroup\$

Coordinates

Given a 2d space, we can fix a coordinate system to talk about transformations numerically.

enter image description here

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).

enter image description here

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 is the result of a transformation with determinant = 0 and dimension of kernel = dimension of image = 1

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}$$

enter image description here

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)
\$\endgroup\$
  • \$\begingroup\$ I fixed some of the MathJax notation (some should be inline math) and made your matrices actual matrices. You should have a look at the $$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\$ – Graipher Jan 10 '18 at 15:03
  • 1
    \$\begingroup\$ @Graipher Thanks! It looks super good now :) \$\endgroup\$ – Caridorc Jan 10 '18 at 17:11
3
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.