Vector Fields

A vector field is a visualization tool represented by an grid system, often 2D, that describe the behavior of vector quantities, such as velocity, force, or electric fields, at every point in a given space. Visualizing vector fields helps us understand how these quantities vary across space and can be crucial in solving various real-world problems. Visualizing vector fields using 2D grid systems is a fundamental concept in mathematics and physics, and we already encountered them in the Chapter of Cellular Automata: PathFinder.
Each tile in the system has a position in x and y. These two variables will be the input for a function. The output of that mathematical function will then be remapped into an angle between 0 to 360 degrees. The resulting angle will then be used to represent the direction in which a line emerging from the center of each tile has to go.



The parameters of the Algorithm

  • RES: int - The size of a TILE
  • DIMS: (int,int) - Total number of columns and rows that make a grid
  • SCREEN: (int,int) - Space in pixels that the grid occupies (width, height) and used to build a screen
  • RANGE: float - The rescaling factor that will determine the domain of input values in x and y to the implicit function f(x,y)
  • FORCE: int - The magnitud of a vector represented as the length of a line
  • TILE: Tile() - Basic unit of information in the system
  • GRID: [[TILE, ..., n=DIMS[0]], ..., n=DIMS[1]] - A 2D array of TILE
  • TIME: float - A third variable that enters as argument to the function f(x,y,t) representing changes in time
  • (opt)LIST_OF_PARTICLE: [Particle(), ..., n=100] - A collection of particles that will be placed on the GRID with the purpose of animation
# MODULES
import pygame
import math
import random 
# DD
RES = 16
DIMS = (40, 40)
SCREEN = (RES * DIMS[0], RES * DIMS[1])
display = pygame.display.set_mode(SCREEN)
RANGE = 3
FORCE = (RES//4) * 3

# DD. TILE
# tile = Tile()
# interp. the basic unit of information in the program. A square in the grid


class Tile:
    def __init__(self, c, r):
        self.c = c
        self.r = r
        self.x = self.c * RES
        self.y = self.r * RES
        self.rect = pygame.Rect(self.x, self.y, RES, RES)
        self.color = "#1e1e1e"
        # attr. related to angle
        self.center = (self.x + RES//2, self.y + RES//2)
        self.dx = 0
        self.dy = 0

    def drawTile(self):
        pygame.draw.rect(display, self.color, self.rect)

    def drawLine(self):
        pygame.draw.line(display, "red", self.center, (self.center[0]+self.dx,self.center[1]+self.dy), 1)


# DD. TILEROW
# tileRow = [TILE, ..., n=DIMS[0]]
# interp. a row of TILE, each tile is next to the other
tileRow = []
for c in range(DIMS[0]):
    # create
    tile = Tile(c, 0)
    # append
    tileRow.append(tile)

# TEMPLATE FOR TILEROW
# for tile in tileRow:
#   ... tile

# DD. GRID
# grid = [TILEROW, ..., n = DIMS[1]]
# interp. a 2D array of TILE
grid = []
for r in range(DIMS[1]):
    tileRow = []
    for c in range(DIMS[0]):
        # create
        tile = Tile(c, r)
        # append
        tileRow.append(tile)
    grid.append(tileRow)

# TEMPLATE FOR GRID
# for tileRow in grid:
#   for tile in tileRow:
#       ... tile

# DD. TIME
# t = float
# interp. a third dimension that shapes the grid by modifying the function f(x,y,t) to animate the grid
t = 0.0

# DD. PARTICLE
# particle = Particle()
# interp. a particle that travels the vector field
class Particle:
    def __init__(self,x,y):
        self.x = x 
        self.y = y 
        self.rect = pygame.Rect(self.x, self.y, 5,5)
        self.color = "blue"
        self.dx = 0
        self.dy = 0
    
    def draw(self):
        self.rect.center = self.x, self.y
        self.x += self.dx
        self.y += self.dy
        pygame.draw.circle(display,self.color,(self.x, self.y),5)

# DD. LIST_OF_PARTICLE
# lop = [PARTICLE, ..., n=100]
# interp. a collection of PARTICLE that are updated every frame
lop = []
for i in range(100):
    particle = Particle(random.randint(0,SCREEN[0]),random.randint(0,SCREEN[1]))
    lop.append(particle)

# TEMPLATE FOR LIST_OF_PARTICLE
# for particle in lop:
#   ... particle

The Algorithm

  • For each TILE in GRID:
    • Remap TILE c and TILE r into the new values x and y in the range (-RANGE,RANGE)
    • Obtain the value of the function f(x,y,t) with x,y,t. Then remap from 0-360 to get an ANGLE
    • Calculate new vector (dx,dy) using the ANGLE and the formulas cosine(angle) and sine(angle), adding the position of TILE as an offset in x and y
  • Make t slightly bigger

Let's implement this algorithm using Python!
# MODULES
    import pygame
    import math
    import random 
    # DD
    RES = 16
    DIMS = (40, 40)
    SCREEN = (RES * DIMS[0], RES * DIMS[1])
    display = pygame.display.set_mode(SCREEN)
    RANGE = 0.5
    FORCE = (RES//4) * 3
    
    # DD. TILE
    # tile = Tile()
    # interp. the basic unit of information in the program. A square in the grid
    
    
    class Tile:
        def __init__(self, c, r):
            self.c = c
            self.r = r
            self.x = self.c * RES
            self.y = self.r * RES
            self.rect = pygame.Rect(self.x, self.y, RES, RES)
            self.color = "#1e1e1e"
            # attr. related to angle
            self.center = (self.x + RES//2, self.y + RES//2)
            self.dx = 0
            self.dy = 0
    
        def drawTile(self):
            pygame.draw.rect(display, self.color, self.rect)
    
        def drawLine(self):
            pygame.draw.line(display, "red", self.center, (self.center[0]+self.dx,self.center[1]+self.dy), 1)
    
    
    # DD. TILEROW
    # tileRow = [TILE, ..., n=DIMS[0]]
    # interp. a row of TILE, each tile is next to the other
    tileRow = []
    for c in range(DIMS[0]):
        # create
        tile = Tile(c, 0)
        # append
        tileRow.append(tile)
    
    # TEMPLATE FOR TILEROW
    # for tile in tileRow:
    #   ... tile
    
    # DD. GRID
    # grid = [TILEROW, ..., n = DIMS[1]]
    # interp. a 2D array of TILE
    grid = []
    for r in range(DIMS[1]):
        tileRow = []
        for c in range(DIMS[0]):
            # create
            tile = Tile(c, r)
            # append
            tileRow.append(tile)
        grid.append(tileRow)
    
    # TEMPLATE FOR GRID
    # for tileRow in grid:
    #   for tile in tileRow:
    #       ... tile
    
    # DD. TIME
    # t = float
    # interp. a third dimension that shapes the grid by modifying the function f(x,y,t) to animate the grid
    t = 0.0
    
    # DD. PARTICLE
    # particle = Particle()
    # interp. a particle that travels the vector field
    class Particle:
        def __init__(self,x,y):
            self.x = x 
            self.y = y 
            self.rect = pygame.Rect(self.x, self.y, 5,5)
            self.color = "blue"
            self.dx = 0
            self.dy = 0
        
        def draw(self):
            self.rect.center = self.x, self.y
            self.x += self.dx
            self.y += self.dy
            pygame.draw.circle(display,self.color,(self.x, self.y),5)
    
    # DD. LIST_OF_PARTICLE
    # lop = [PARTICLE, ..., n=100]
    # interp. a collection of PARTICLE that are updated every frame
    lop = []
    for i in range(100):
        particle = Particle(random.randint(0,SCREEN[0]),random.randint(0,SCREEN[1]))
        lop.append(particle)
    
    # TEMPLATE FOR LIST_OF_PARTICLE
    # for particle in lop:
    #   ... particle
    
    # CODE
    
    
    def draw():
        display.fill("green")
        for tileRow in grid:
            for tile in tileRow:
                tile.drawTile()
        for tileRow in grid:
            for tile in tileRow:
                tile.drawLine()
        for particle in lop:
            particle.draw()
        pygame.display.flip()
    
    # FD. remap()
    # Signature: float, float, float, float, float -> float
    # purp. rescale a given value
    
    
    def remap(value, from1, to1, from2, to2):
        return (value - from1) / (to1 - from1) * (to2 - from2) + from2
    
    
    def fn0(tile):
        x = remap(tile.x, 0, SCREEN[0], -RANGE, RANGE)
        y = remap(tile.y, 0, SCREEN[1], -RANGE, RANGE)
        value = math.sin(x**2) + math.sin(y**2) + math.sin(t**2)
        return value
    
    
    def update():
        global t
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
    
        for tileRow in grid:
            for tile in tileRow:
                angle = remap(fn0(tile),0,5,0,359)
                tile.dx = (math.cos(angle) * FORCE)
                tile.dy = (math.sin(angle) * FORCE)
    
    
        for particle in lop:
            for tileRow in grid:
                for tile in tileRow:
                    if tile.rect.colliderect(particle.rect):
                        particle.dx = tile.dx
                        particle.dy = tile.dy
    
        t += 0.001
    
    
    while True:
        draw()
        update()