from tkinter import *
from random import randint, choice
from time import clock, sleep
# TODO: further tweaks
# 1. Add goals for the boids to move toward (DONE - BoidGroup.target)
# 2. Add wind or current that "blows" the boids around
# 3. Have boids tend towards a place; travel through waypoints
# 4. Limit (or unlimit) a boid's speed (DONE - BoidAgent.max_speed)
# 5. Set bounds for boids (DONE - BoidGUI.force_wall & .bounce_wall)
# 6. Allow boids to "perch" on the ground at random.
# TODO: anti-flocking behaviour
# 1. Get the boid group to scatter from each other; add more rules
# 2. Send the boids away from certain areas; danger or obstacles
# 3. Introduce predators that boids will always run from
# TODO: some other details
# 1. Boids need to "see" each other
# 2. Unseen boids should be ignored
# 3. Refer to the original algorithm
# 4. http://www.red3d.com/cwr/boids/
# 5. The timing engine needs redesign (DONE - based on pt.QT.run)
# 6. Change updating system to that used by QuizMe
################################################################################
# Here are various program settings.
USE_WINDOW = False # Display program in window.
FULLSCREEN = True # Go fullscreen when executed.
SCR_SAVER = False # Turn screensaver mode on or off.
COME_BACK = -1 # The program can automatically "restart."
# if < 0: Exit program immediately
# if = 0: Disable exiting program
# if > 0: Come back after X seconds
TITLE = 'BOIDs' # Title to show in windowed mode.
WIDTH = 800 # Width for window to display in.
HEIGHT = 600 # Height to display in window mode.
BACKGROUND = '#000' # Background color for the screen.
BOIDS = 10 # Number of boids to show in a group.
# BoidGUI and BoidAgent have settings too.
################################################################################
def main():
# Create the opening window for the program.
NoDefaultRoot()
root = Tk()
assert not (USE_WINDOW and FULLSCREEN), \
'Only Window or Fullscreen may be used.'
# Define the closing event handler.
if COME_BACK < 0:
def close(event=None):
root.destroy()
else:
def close(event=None):
if COME_BACK:
root.withdraw()
sleep(COME_BACK)
for child in root.children.values():
if isinstance(child, BoidGUI):
child.last_time += COME_BACK
root.deiconify()
# Create window based on settings.
if USE_WINDOW:
root.resizable(False, False)
root.title(TITLE)
width = WIDTH
height = HEIGHT
position = ''
elif FULLSCREEN:
root.overrideredirect(True)
if not SCR_SAVER:
root.bind_all('', close)
width = root.winfo_screenwidth()
height = root.winfo_screenheight()
position = '+0+0'
else:
raise ValueError('Cannot determine window type to use.')
# Configure the root window as needed.
root.protocol('WM_DELETE_WINDOW', close)
if SCR_SAVER:
assert COME_BACK, 'Screen may not be locked as screensaver.'
root.bind_all('', close)
root.bind_all('', close)
root.geometry('{0}x{1}{2}'.format(width, height, position))
# Create the application object that handles the GUI.
app = BoidGUI(root, width, height, BACKGROUND, BOIDS)
app.grid()
root.mainloop()
################################################################################
# This function parses color strings.
def parse_color(string):
assert len(string) == 7 and string[0] == '#', 'Not Color String!'
number = []
for index in range(1, len(string) - 1, 2):
number.append(int(string[index:index+2], 16))
return tuple(number)
# This function interpolates between two colors.
def interpolate(lower, upper, bias):
A = 1 - bias
R = round(lower[0] * A + upper[0] * bias)
G = round(lower[1] * A + upper[1] * bias)
B = round(lower[2] * A + upper[2] * bias)
return R, G, B
################################################################################
class BoidGUI(Canvas):
# Drawing Options
BAL_NOT_VEC = True # Draw balls (True) or vectors (False).
RANDOM_BACK = False # Replace background with flashing colors?
RANDOM_BALL = False # Replace balls with flashing colors?
DRAW_TARGET = True # Show line from groups to their targets?
# Wall Settings
WALL_BOUNCE = False # Bouncy wall if true; force wall if false.
WALL_MARGIN = 50 # Pixels from edge of screen for boundary.
WALL_FORCE = 100 # Force applied to balls outside boundary.
# Random Parameters
MAX_FPS = 100 # Maximum frame per second for display.
GROUPS = 2 # Number of groups to have displayed on the GUI.
# Target Settings
TARGET_FORCE = 500 # Force exerted by the targets on the boid groups.
TRIG_DIST = 100 # Distance to target where target gets changed.
MINI_DIST = 200 # Target must be this far away when recreated.
# Boid Settings
MAX_SPEED = 400 # Maximum speed for boids (pixels per second).
MAX_SIZE = 15 # Largest radius a boid is allowed to have.
MIN_SIZE = 10 # Smallest radius a boid may be built with.
# Color Variables
PALETTE_MODE = True # Palette mode if true; random mode if false.
COLORS = '#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#FF00FF'
PALETTE = []
for x in range(16):
for y in range(16):
for z in range(16):
color = '#{:X}{:X}{:X}'.format(x, y, z)
PALETTE.append(color)
# Check the settings up above for errors.
assert MINI_DIST > TRIG_DIST, 'Targets must be set beyond trigger point.'
assert MAX_SIZE > MIN_SIZE, 'A minimum may not be larger than maximum.'
assert len(COLORS) > GROUPS, 'There must be more colors than groups.'
def __init__(self, master, width, height, background, boids):
# Initialize the Canvas object.
cursor = 'none' if SCR_SAVER else ''
super().__init__(master, width=width, height=height, cursor=cursor,
background=background, highlightthickness=0)
self.width = width
self.height = height
self.background = background
# Create colors for the balls.
self.create_ball_palette(boids)
# Build the boid control system.
self.build_boids(boids)
# Build loop for frame updating.
self.last_time = clock()
self.time_diff = 1 / self.MAX_FPS
self.after(1000 // self.MAX_FPS, self.update_screen)
def create_ball_palette(self, size):
# The last color is not used.
size += 1
# Turn the colors into (R, G, B) tuples.
colors = list(map(parse_color, self.COLORS))
self.BALL_PALETTE = []
for index in range(len(colors)):
# Extract color bounds.
lower = colors[index]
upper = colors[(index + 1) % len(colors)]
palette = []
# Interpolate colors between the bounds.
for bias in range(size):
R, G, B = interpolate(lower, upper, bias / size)
palette.append('#{0:02X}{1:02X}{2:02X}'.format(R, G, B))
# Add the new palette to the choice list.
self.BALL_PALETTE.append(palette)
def build_boids(self, boids):
# Build various boid simulation groups.
self.groups = []
for group in range(self.GROUPS):
group = BoidGroup()
group.palette = choice(self.BALL_PALETTE)
self.BALL_PALETTE.remove(group.palette)
# Create a new boid for current group.
for boid, color in zip(range(boids), group.palette):
# Place the boid somewhere on screen.
x = randint(0, self.width)
y = randint(0, self.height)
position = Vector2(x, y)
# Give it a random velocity (within 400).
velocity = Polar2(randint(1, self.MAX_SPEED), randint(1, 360))
# Create a random size for the ball.
size = randint(self.MIN_SIZE, self.MAX_SIZE)
assert size != 2, 'This is an oddly shaped ball.'
# Create a boid (with a maximum speed of 400).
boid = BoidAgent(position, velocity, size, self.MAX_SPEED)
# Add a color attribute from COLORS list.
if self.PALETTE_MODE:
boid.color = color
else:
boid.color = choice(self.COLORS)
group.add_boid(boid)
# Add some mutators to this group.
if self.WALL_BOUNCE:
group.add_control(self.bounce_wall)
else:
group.add_control(self.force_wall)
group.add_control(self.motivate)
# Add a random target attribute to the group.
x = randint(self.WALL_MARGIN, self.width - self.WALL_MARGIN)
y = randint(self.WALL_MARGIN, self.height - self.WALL_MARGIN)
group.target = Vector2(x, y)
self.groups.append(group)
def motivate(self, group, boid, seconds):
# What direction should this boid move in?
vector = (group.target - boid.position).unit()
# Adjust velocity according to force and scale.
boid.velocity += vector * self.TARGET_FORCE * seconds
def check_target(self):
for group in self.groups:
# Is the center of the group within (100) pixels of target?
if (group.center - group.target).magnitude <= self.TRIG_DIST:
# Adjust target to be over (200) pixels away.
while (group.center - group.target).magnitude <= self.MINI_DIST:
minimum = self.WALL_MARGIN
width = self.width - minimum
height = self.height - minimum
x = randint(minimum, width)
y = randint(minimum, height)
group.target = Vector2(x, y)
# Change the ball colors if they are not random.
if not self.RANDOM_BALL:
if self.PALETTE_MODE:
palette = choice(self.BALL_PALETTE)
self.BALL_PALETTE.remove(palette)
self.BALL_PALETTE.append(group.palette)
# Assign colors from new palette.
for boid, color in zip(group.boids, palette):
boid.color = color
group.palette = palette
else:
# Assign a random color from palette.
for boid in group.boids:
boid.color = choice(self.COLORS)
def force_wall(self, group, boid, seconds):
# Left and Right walls.
if boid.position.x < self.WALL_MARGIN:
boid.velocity.x += self.WALL_FORCE * seconds
elif boid.position.x > self.width - self.WALL_FORCE:
boid.velocity.x -= self.WALL_FORCE * seconds
# Upper and Lower walls.
if boid.position.y < self.WALL_MARGIN:
boid.velocity.y += self.WALL_FORCE * seconds
elif boid.position.y > self.height - self.WALL_FORCE:
boid.velocity.y -= self.WALL_FORCE * seconds
def bounce_wall(self, group, boid, seconds):
# Left and Right walls.
if boid.position.x < self.WALL_MARGIN:
if boid.velocity.x < 0:
boid.velocity.x *= -1
elif boid.position.x > self.width - self.WALL_MARGIN:
if boid.velocity.x > 0:
boid.velocity.x *= -1
# Upper and Lower walls.
if boid.position.y < self.WALL_MARGIN:
if boid.velocity.y < 0:
boid.velocity.y *= -1
elif boid.position.y > self.height - self.WALL_MARGIN:
if boid.velocity.y > 0:
boid.velocity.y *= -1
def update_screen(self):
# Clear the screen.
self.delete(ALL)
for group in self.groups:
# Draw the group's target if enabled.
if self.DRAW_TARGET:
center = group.center
target = group.target
self.create_line(center.x, center.y, target.x, target.y,
fill=choice(self.PALETTE), width=3)
# Draw all boids in the current group.
for boid in group.boids:
# Select correct fill color for drawing.
fill = choice(self.PALETTE) if self.RANDOM_BALL else boid.color
if self.BAL_NOT_VEC:
# Draw a ball (oval).
x1 = boid.position.x - boid.radius
y1 = boid.position.y - boid.radius
x2 = boid.position.x + boid.radius
y2 = boid.position.y + boid.radius
self.create_oval((x1, y1, x2, y2), fill=fill)
else:
# Draw a direction pointer.
start = boid.position
end = boid.velocity.unit() * (boid.radius * 3) + start
self.create_line(start.x, start.y, end.x, end.y,
fill=fill, width=3)
# Randomize the background color if enabled.
if self.RANDOM_BACK:
self['background'] = choice(self.PALETTE)
# Update all group targets as needed.
self.check_target()
# Run through the updating routines on the groups.
time = clock()
delta = time - self.last_time
for group in self.groups:
group.run_controls(delta)
group.update_velocity()
group.update_position(delta)
self.last_time = time
# Schedule for the next run of this method.
plus = time + self.time_diff
over = plus % self.time_diff
diff = plus - time - over
self.after(round(diff * 1000), self.update_screen)
import _tkinter # Properly set the GUI's update rate.
_tkinter.setbusywaitinterval(1000 // BoidGUI.MAX_FPS)
################################################################################
# This is where groups and world objects should live.
class BoidWorld:
pass
################################################################################
class BoidGroup:
# Simple collection for managing boid agents.
def __init__(self):
self.__boids = []
self.__flag = False
self.__controls = []
self.__good_center = False
self.__prop_center = Vector2(0, 0)
self.__good_vector = False
self.__prop_vector = Vector2(0, 0)
def add_boid(self, boid):
self.__boids.append(boid)
def update_velocity(self):
assert not self.__flag, 'Position must be updated first.'
self.__flag = True
for boid in self.__boids:
boid.update_velocity(self, self.__boids)
self.__good_vector = False
def update_position(self, seconds):
assert self.__flag, 'Velocity must be updated first.'
self.__flag = False
for boid in self.__boids:
boid.update_position(seconds)
self.__good_center = False
def add_control(self, control):
self.__controls.append(control)
def run_controls(self, seconds):
for control in self.__controls:
for boid in self.__boids:
control(self, boid, seconds)
@property
def boids(self):
for boid in self.__boids:
yield boid
@property
def center(self):
if self.__good_center == False:
self.__prop_center = Vector2(0, 0)
for boid in self.__boids:
self.__prop_center += boid.position
self.__prop_center /= len(self.__boids)
self.__good_center = True
return self.__prop_center
@property
def vector(self):
if self.__good_vector == False:
self.__prop_vector = Vector2(0, 0)
for boid in self.__boids:
self.__prop_vector += boid.velocity
self.__prop_vector /= len(self.__boids)
self.__good_vector = True
return self.__prop_vector
################################################################################
class BoidAgent:
# Implements all three boid rules.
RULE_1_SCALE = 100 # Scale the clumping factor.
RULE_2_SCALE = 3 # Scale the avoiding factor.
RULE_2_SPACE = 1 # Avoid when inside of space.
RULE_3_SCALE = 100 # Scale the schooling factor.
def __init__(self, position, velocity, radius, max_speed):
self.position = position
self.velocity = velocity
self.__update = Vector2(0, 0)
self.radius = radius
self.max_speed = max_speed
def update_velocity(self, group, boids):
# Filter self out of boids.
others = [boid for boid in boids if boid is not self]
# Run through the boid rules.
vector_1 = self.__rule_1(others)
# vector_1 = (group.center - self.position) / 100
vector_2 = self.__rule_2(others)
vector_3 = self.__rule_3(others)
# vector_3 = (group.vector - self.velocity) / 100
# Save the results.
self.__update = vector_1 + vector_2 + vector_3
def update_position(self, seconds):
# Update to new velocity.
self.velocity += self.__update
# Limit the velocity as needed.
if self.velocity.magnitude > self.max_speed:
self.velocity /= self.velocity.magnitude / self.max_speed
# Update our position variable.
self.position += self.velocity * seconds
def __rule_1(self, boids):
# Simulate the clumping factor.
vector = Vector2(0, 0)
for boid in boids:
vector += boid.position
vector /= len(boids)
return (vector - self.position) / self.RULE_1_SCALE
def __rule_2(self, boids):
# Simulate the avoiding factor.
vector = Vector2(0, 0)
for boid in boids:
delta = (boid.position - self.position).magnitude
space = (boid.radius + self.radius) * (self.RULE_2_SPACE + 1)
if delta < space:
vector += (self.position - boid.position)
return vector / self.RULE_2_SCALE
def __rule_3(self, boids):
# Simulate the schooling factor.
vector = Vector2(0, 0)
weight = 0
for boid in boids:
r2 = boid.radius ** 2
vector += boid.velocity * r2
weight += r2
vector /= len(boids) * weight
return (vector - self.velocity) / self.RULE_3_SCALE
################################################################################
from math import *
################################################################################
def Polar2(magnitude, degrees):
x = magnitude * sin(radians(degrees))
y = magnitude * cos(radians(degrees))
return Vector2(x, y)
################################################################################
class Vector2:
# See all the nice vector operations above?
# The following class implements those instructions.
__slots__ = 'x', 'y'
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return 'Vector2({!r}, {!r})'.format(self.x, self.y)
def polar_repr(self):
x, y = self.x, self.y
magnitude = hypot(x, y)
angle = degrees(atan2(x, y)) % 360
return 'Polar2({!r}, {!r})'.format(magnitude, angle)
# Rich Comparison Methods
def __lt__(self, obj):
if isinstance(obj, Vector2):
x1, y1, x2, y2 = self.x, self.y, obj.x, obj.y
return x1 * x1 + y1 * y1 < x2 * x2 + y2 * y2
return hypot(self.x, self.y) < obj
def __le__(self, obj):
if isinstance(obj, Vector2):
x1, y1, x2, y2 = self.x, self.y, obj.x, obj.y
return x1 * x1 + y1 * y1 <= x2 * x2 + y2 * y2
return hypot(self.x, self.y) <= obj
def __eq__(self, obj):
if isinstance(obj, Vector2):
return self.x == obj.x and self.y == obj.y
return hypot(self.x, self.y) == obj
def __ne__(self, obj):
if isinstance(obj, Vector2):
return self.x != obj.x or self.y != obj.y
return hypot(self.x, self.y) != obj
def __gt__(self, obj):
if isinstance(obj, Vector2):
x1, y1, x2, y2 = self.x, self.y, obj.x, obj.y
return x1 * x1 + y1 * y1 > x2 * x2 + y2 * y2
return hypot(self.x, self.y) > obj
def __ge__(self, obj):
if isinstance(obj, Vector2):
x1, y1, x2, y2 = self.x, self.y, obj.x, obj.y
return x1 * x1 + y1 * y1 >= x2 * x2 + y2 * y2
return hypot(self.x, self.y) >= obj
# Boolean Operation
def __bool__(self):
return self.x != 0 or self.y != 0
# Container Methods
def __len__(self):
return 2
def __getitem__(self, index):
return (self.x, self.y)[index]
def __setitem__(self, index, value):
temp = [self.x, self.y]
temp[index] = value
self.x, self.y = temp
def __iter__(self):
yield self.x
yield self.y
def __reversed__(self):
yield self.y
yield self.x
def __contains__(self, obj):
return obj in (self.x, self.y)
# Binary Arithmetic Operations
def __add__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x + obj.x, self.y + obj.y)
return Vector2(self.x + obj, self.y + obj)
def __sub__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x - obj.x, self.y - obj.y)
return Vector2(self.x - obj, self.y - obj)
def __mul__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x * obj.x, self.y * obj.y)
return Vector2(self.x * obj, self.y * obj)
def __truediv__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x / obj.x, self.y / obj.y)
return Vector2(self.x / obj, self.y / obj)
def __floordiv__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x // obj.x, self.y // obj.y)
return Vector2(self.x // obj, self.y // obj)
def __mod__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x % obj.x, self.y % obj.y)
return Vector2(self.x % obj, self.y % obj)
def __divmod__(self, obj):
if isinstance(obj, Vector2):
return (Vector2(self.x // obj.x, self.y // obj.y),
Vector2(self.x % obj.x, self.y % obj.y))
return (Vector2(self.x // obj, self.y // obj),
Vector2(self.x % obj, self.y % obj))
def __pow__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x ** obj.x, self.y ** obj.y)
return Vector2(self.x ** obj, self.y ** obj)
def __lshift__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x << obj.x, self.y << obj.y)
return Vector2(self.x << obj, self.y << obj)
def __rshift__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x >> obj.x, self.y >> obj.y)
return Vector2(self.x >> obj, self.y >> obj)
def __and__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x & obj.x, self.y & obj.y)
return Vector2(self.x & obj, self.y & obj)
def __xor__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x ^ obj.x, self.y ^ obj.y)
return Vector2(self.x ^ obj, self.y ^ obj)
def __or__(self, obj):
if isinstance(obj, Vector2):
return Vector2(self.x | obj.x, self.y | obj.y)
return Vector2(self.x | obj, self.y | obj)
# Binary Arithmetic Operations (with reflected operands)
def __radd__(self, obj):
return Vector2(obj + self.x, obj + self.y)
def __rsub__(self, obj):
return Vector2(obj - self.x, obj - self.y)
def __rmul__(self, obj):
return Vector2(obj * self.x, obj * self.y)
def __rtruediv__(self, obj):
return Vector2(obj / self.x, obj / self.y)
def __rfloordiv__(self, obj):
return Vector2(obj // self.x, obj // self.y)
def __rmod__(self, obj):
return Vector2(obj % self.x, obj % self.y)
def __rdivmod__(self, obj):
return (Vector2(obj // self.x, obj // self.y),
Vector2(obj % self.x, obj % self.y))
def __rpow__(self, obj):
return Vector2(obj ** self.x, obj ** self.y)
def __rlshift__(self, obj):
return Vector2(obj << self.x, obj << self.y)
def __rrshift__(self, obj):
return Vector2(obj >> self.x, obj >> self.y)
def __rand__(self, obj):
return Vector2(obj & self.x, obj & self.y)
def __rxor__(self, obj):
return Vector2(obj ^ self.x, obj ^ self.y)
def __ror__(self, obj):
return Vector2(obj | self.x, obj | self.y)
# Augmented Arithmetic Assignments
def __iadd__(self, obj):
if isinstance(obj, Vector2):
self.x += obj.x
self.y += obj.y
else:
self.x += obj
self.y += obj
return self
def __isub__(self, obj):
if isinstance(obj, Vector2):
self.x -= obj.x
self.y -= obj.y
else:
self.x -= obj
self.y -= obj
return self
def __imul__(self, obj):
if isinstance(obj, Vector2):
self.x *= obj.x
self.y *= obj.y
else:
self.x *= obj
self.y *= obj
return self
def __itruediv__(self, obj):
if isinstance(obj, Vector2):
self.x /= obj.x
self.y /= obj.y
else:
self.x /= obj
self.y /= obj
return self
def __ifloordiv__(self, obj):
if isinstance(obj, Vector2):
self.x //= obj.x
self.y //= obj.y
else:
self.x //= obj
self.y //= obj
return self
def __imod__(self, obj):
if isinstance(obj, Vector2):
self.x %= obj.x
self.y %= obj.y
else:
self.x %= obj
self.y %= obj
return self
def __ipow__(self, obj):
if isinstance(obj, Vector2):
self.x **= obj.x
self.y **= obj.y
else:
self.x **= obj
self.y **= obj
return self
def __ilshift__(self, obj):
if isinstance(obj, Vector2):
self.x <<= obj.x
self.y <<= obj.y
else:
self.x <<= obj
self.y <<= obj
return self
def __irshift__(self, obj):
if isinstance(obj, Vector2):
self.x >>= obj.x
self.y >>= obj.y
else:
self.x >>= obj
self.y >>= obj
return self
def __iand__(self, obj):
if isinstance(obj, Vector2):
self.x &= obj.x
self.y &= obj.y
else:
self.x &= obj
self.y &= obj
return self
def __ixor__(self, obj):
if isinstance(obj, Vector2):
self.x ^= obj.x
self.y ^= obj.y
else:
self.x ^= obj
self.y ^= obj
return self
def __ior__(self, obj):
if isinstance(obj, Vector2):
self.x |= obj.x
self.y |= obj.y
else:
self.x |= obj
self.y |= obj
return self
# Unary Arithmetic Operations
def __pos__(self):
return Vector2(+self.x, +self.y)
def __neg__(self):
return Vector2(-self.x, -self.y)
def __invert__(self):
return Vector2(~self.x, ~self.y)
def __abs__(self):
return Vector2(abs(self.x), abs(self.y))
# Virtual "magnitude" Attribute
def __fg_ma(self):
return hypot(self.x, self.y)
def __fs_ma(self, value):
x, y = self.x, self.y
temp = value / hypot(x, y)
self.x, self.y = x * temp, y * temp
magnitude = property(__fg_ma, __fs_ma, doc='Virtual "magnitude" Attribute')
# Virtual "direction" Attribute
def __fg_di(self):
return atan2(self.y, self.x)
def __fs_di(self, value):
temp = hypot(self.x, self.y)
self.x, self.y = cos(value) * temp, sin(value) * temp
direction = property(__fg_di, __fs_di, doc='Virtual "direction" Attribute')
# Virtual "degrees" Attribute
def __fg_de(self):
return degrees(atan2(self.x, self.y)) % 360
def __fs_de(self, value):
temp = hypot(self.x, self.y)
self.x, self.y = sin(radians(value)) * temp, cos(radians(value)) * temp
degrees = property(__fg_de, __fs_de, doc='Virtual "degrees" Attribute')
# Virtual "xy" Attribute
def __fg_xy(self):
return self.x, self.y
def __fs_xy(self, value):
self.x, self.y = value
xy = property(__fg_xy, __fs_xy, doc='Virtual "xy" Attribute')
# Virtual "yx" Attribute
def __fg_yx(self):
return self.y, self.x
def __fs_yx(self, value):
self.y, self.x = value
yx = property(__fg_yx, __fs_yx, doc='Virtual "yx" Attribute')
# Unit Vector Operations
def unit_vector(self):
x, y = self.x, self.y
temp = hypot(x, y)
return Vector2(x / temp, y / temp)
def normalize(self):
x, y = self.x, self.y
temp = hypot(x, y)
self.x, self.y = x / temp, y / temp
return self
# Vector Multiplication Operations
def dot_product(self, vec):
return self.x * vec.x + self.y * vec.y
def cross_product(self, vec):
return self.x * vec.y - self.y * vec.x
# Geometric And Physical Reflections
def reflect(self, vec):
x1, y1, x2, y2 = self.x, self.y, vec.x, vec.y
temp = 2 * (x1 * x2 + y1 * y2) / (x2 * x2 + y2 * y2)
return Vector2(x2 * temp - x1, y2 * temp - y1)
def bounce(self, vec):
x1, y1, x2, y2 = self.x, self.y, +vec.y, -vec.x
temp = 2 * (x1 * x2 + y1 * y2) / (x2 * x2 + y2 * y2)
return Vector2(x2 * temp - x1, y2 * temp - y1)
# Standard Vector Operations
def project(self, vec):
x, y = vec.x, vec.y
temp = (self.x * x + self.y * y) / (x * x + y * y)
return Vector2(x * temp, y * temp)
def rotate(self, vec):
x1, y1, x2, y2 = self.x, self.y, vec.x, vec.y
return Vector2(x1 * x2 + y1 * y2, y1 * x2 - x1 * y2)
def interpolate(self, vec, bias):
a = 1 - bias
return Vector2(self.x * a + vec.x * bias, self.y * a + vec.y * bias)
# Other Useful Methods
def near(self, vec, dist):
x, y = self.x, self.y
return x * x + y * y <= dist * dist
def perpendicular(self):
return Vector2(+self.y, -self.x)
def subset(self, vec1, vec2):
x1, x2 = vec1.x, vec2.x
if x1 <= x2:
if x1 <= self.x <= x2:
y1, y2 = vec1.y, vec2.y
if y1 <= y2:
return y1 <= self.y <= y2
return y2 <= self.y <= y1
else:
if x2 <= self.x <= x1:
y1, y2 = vec1.y, vec2.y
if y1 <= y2:
return y1 <= self.y <= y2
return y2 <= self.y <= y1
return False
# Synonymous Definitions
copy = __pos__
inverse = __neg__
unit = unit_vector
dot = dot_product
cross = cross_product
lerp = interpolate
perp = perpendicular
################################################################################
# If this code is run directly,
# run the program's entry point.
if __name__ == '__main__':
main()