"""
(C) Steven Byrnes, 2014-2016. This code is released under the MIT license
http://opensource.org/licenses/MIT
This code runs in Python 2.7 or 3.3. It requires imagemagick to be installed;
that's how it assembles images into animated GIFs.
"""
from __future__ import division
import pygame as pg
from numpy import cos, pi, sin, linspace
import subprocess, os
directory_now = os.path.dirname(os.path.realpath(__file__))
frames_in_anim = 100
animation_loop_seconds = 5 #time in seconds for animation to loop one cycle
bgcolor = (255,255,255) #background is white
ecolor = (0,0,0) #electrons are black
wire_color = (200,200,200) # wire color is light gray
arrow_color = (140,0,0)
# pygame draws pixel-art, not smoothed. Therefore I am drawing it
# bigger, then smoothly shrinking it down
img_height = 180
img_width = 900
final_height = 60
final_width = 300
# ~23 megapixel limit for wikipedia animated gifs
assert final_height * final_width * frames_in_anim < 22e6
#transmission line thickness, and y-coordinate of the top of each wire
tl_thickness = 27
tl_top_y = 40
tl_bot_y = img_height - tl_top_y - tl_thickness + 2
wavelength = 1.1 * img_width
e_radius = 4
# dimensions of triangular arrow head (this is for the longest arrows; it's
# scaled down when the arrow is too small)
arrowhead_base = 9
arrowhead_height = 15
# width of the arrow line
arrow_width = 6
# number of electrons spread out over the transmission line (top plus bottom)
num_electrons = 100
# max_e_displacement is defined here as a multiple of the total electron path length
# (roughly twice the width of the image, because we're adding top + bottom)
max_e_displacement = 1/60
num_arrows = 20
max_arrow_halflength = 22
def tup_round(tup):
"""round each element of a tuple to nearest integer"""
return tuple(int(round(x)) for x in tup)
def draw_arrow(surf, x, tail_y, head_y):
"""
draw a vertical arrow. Coordinates do not need to be integers
"""
# calculate dimensions of the triangle; it's scaled down for short arrows
if abs(head_y - tail_y) >= 1.5 * arrowhead_height:
h = arrowhead_height
b = arrowhead_base
else:
h = abs(head_y - tail_y) / 1.5
b = arrowhead_base * h / arrowhead_height
if tail_y < head_y:
# downward arrow
triangle = [tup_round((x, head_y)),
tup_round((x - b, head_y - h)),
tup_round((x + b, head_y - h))]
triangle_middle_y = head_y - h/2
else:
# upward arrow
triangle = [tup_round((x, head_y)),
tup_round((x - b, head_y + h)),
tup_round((x + b, head_y + h))]
triangle_middle_y = head_y + h/2
pg.draw.line(surf, arrow_color, tup_round((x, tail_y)), tup_round((x, triangle_middle_y)), arrow_width)
pg.draw.polygon(surf, arrow_color, triangle, 0)
def e_path(param, phase_top_left):
"""
as param goes 0 to 1, this returns {'pos': (x, y), 'phase':phi},
where (x,y) is the coordinates of the corresponding point on the electron
dot path, and phi is the phase for an electron at that point on the path.
phase_top_left is phase of the left side of the top wire.
"""
# d is a vertical offset between the electrons and the wires
d = tl_thickness - e_radius - 2
# pad is how far to extend the transmission line beyond the image borders
# (since those electrons may enter the image a bit)
pad = 36
path_length = 2*(img_width + 2*pad)
howfar = param * path_length
# move right across top transmission line
if howfar <= path_length / 2:
x = howfar - pad
y = tl_top_y + d
phase = phase_top_left + 2 * pi * x / wavelength
return {'pos':(x,y), 'phase':phase}
# ...then move left across the bottom transmission line
x = path_length - howfar - pad
y = tl_bot_y + tl_thickness - d
phase = phase_top_left + 2 * pi * x / wavelength
return {'pos':(x,y), 'phase':phase}
def main():
#Make and save a drawing for each frame
filename_list = [os.path.join(directory_now, 'temp' + str(n) + '.png')
for n in range(frames_in_anim)]
for frame in range(frames_in_anim):
phase_top_left = -2 * pi * frame / frames_in_anim
#initialize surface
surf = pg.Surface((img_width,img_height))
surf.fill(bgcolor);
#draw transmission line
pg.draw.rect(surf, wire_color, [0, tl_top_y, img_width, tl_thickness])
pg.draw.rect(surf, wire_color, [0, tl_bot_y, img_width, tl_thickness])
#draw electrons. Remember, "param" is an abstract coordinate that goes
#from 0 to 1 as the electron position goes right across the top wire
#then left across the bottom wire
equilibrium_params = linspace(0, 1, num=num_electrons)
phases = [e_path(a, phase_top_left)['phase'] for a in equilibrium_params]
now_params = [equilibrium_params[i] + sin(phases[i]) * max_e_displacement
for i in range(num_electrons)]
coords = [e_path(a, phase_top_left)['pos'] for a in now_params]
for coord in coords:
pg.draw.circle(surf, ecolor, tup_round(coord), e_radius)
#draw arrows
arrow_params = linspace(0, 0.5, num=num_arrows)
for i in range(len(arrow_params)):
a = arrow_params[i]
arrow_x = e_path(a, phase_top_left)['pos'][0]
arrow_phase = e_path(a, phase_top_left)['phase']
head_y = img_height/2 + max_arrow_halflength * cos(arrow_phase)
tail_y = img_height/2 - max_arrow_halflength * cos(arrow_phase)
draw_arrow(surf, arrow_x, tail_y, head_y)
#shrink the surface to its final size, and save it
shrunk_surface = pg.transform.smoothscale(surf, (final_width, final_height))
pg.image.save(shrunk_surface, filename_list[frame])
seconds_per_frame = animation_loop_seconds / frames_in_anim
frame_delay = str(int(seconds_per_frame * 100))
# Use the "convert" command (part of ImageMagick) to build the animation
command_list = ['convert', '-delay', frame_delay, '-loop', '0'] + filename_list + ['anim.gif']
subprocess.call(command_list, cwd=directory_now)
# Earlier, we saved an image file for each frame of the animation. Now
# that the animation is assembled, we can (optionally) delete those files
if True:
for filename in filename_list:
os.remove(filename)
main()