I am trying to build a subclass that inherited from turtle.Turtle class and want to create a function to automatically draw the polygon. But I find the initial line always tilts a bit.
I don't know where's the problem.
Here's the code:
import turtle
class Polygon(turtle.Turtle):
def __init__(self, point_list):
self.point_list = point_list
def add_point(self, point):
self.point_list.append(point)
return self.point_list
def over_write_points(self, new_points):
self.point_list = new_points
return self.point_list
def perimeter(self):
length_peri = 0
for i in range(len(self.point_list)):
point1 = self.point_list[i-1]
point2 = self.point_list[i]
x1, y1 = point1
x2, y2 = point2
length = ((x1 - x2)**2 + (y1 - y2)**2)*0.5
length_peri += length
return length_peri
def area(self):
area = 0
for i in range(len(self.point_list)):
point1 = self.point_list[i-1]
point2 = self.point_list[i]
x1, y1 = point1
x2, y2 = point2
trapezoid = ((x2 - x1) * (y1 + y2)) / 2
area = area + trapezoid
area = abs(area)
return area
def bound_points(self):
unzip_list = list(zip(*self.point_list))
x_list = unzip_list[0]
y_list = unzip_list[1]
bound1 = max(x_list), min(y_list)
bound2 = max(x_list), max(y_list)
bound3 = min(x_list), max(y_list)
bound4 = min(x_list), min(y_list)
bound_points = [bound1, bound2, bound3, bound4]
return bound_points
def move_poly(self, dx, dy):
new_point_list = []
for i in self.point_list:
x = i[0] + dx
y = i[1] + dy
new_point = (x, y)
new_point_list.append(new_point)
self.point_list = new_point_list
return self.point_list
def draw_poly(self, lineColour="green", fillColour="yellow"):
start = self.point_list[-1]
turtle.pencolor(lineColour)
turtle.fillcolor(fillColour)
turtle.penup()
turtle.pendown()
turtle.begin_fill()
x, y = start
for point in self.point_list: # go through a list of points
dx, dy = point
turtle.goto(x + dx, y + dy)
turtle.end_fill()
turtle.penup()
turtle.mainloop()
return f'The polygon is finished'
test_polygon = Polygon([(50,0), (50,50), (0,50)])
print(test_polygon.add_point((0, 0)))
print(test_polygon.over_write_points([(100,0), (100,100), (0,100), (0,0)]))
print(test_polygon.perimeter())
print(test_polygon.area())
print(test_polygon.bound_points())
print(test_polygon.move_poly(-10,-10))
print(test_polygon.draw_poly())
You've several problems in your code. First, as #martineau notes (+1), you start drawing from the turtle's home position rather than the first position in your list. (And you need to close the polygon by returning back to that first position.)
The math in your perimeter() function seems wrong:
length = ((x1 - x2)**2 + (y1 - y2)**2)*0.5
That should probably be **0.5 to calculate the square root, not half. You forgot to call super() in your __init__ function.
Also, your draw_poly() is calling functions in module turtle instead of invoking methods on self. This is why I do import Screen, Turtle instead of import turtle, just to avoid this error.
Below is a rework of your code with the above and other fixes:
from turtle import Turtle
class Polygon(Turtle):
def __init__(self, point_list):
super().__init__()
self.point_list = point_list
def add_point(self, point):
self.point_list.append(point)
return self.point_list
def over_write_points(self, new_points):
self.point_list[:] = new_points # reload existing list
return self.point_list
def perimeter(self):
length_peri = 0
for i in range(len(self.point_list)):
point1 = self.point_list[i - 1]
point2 = self.point_list[i]
x1, y1 = point1
x2, y2 = point2
length = ((x1 - x2)**2 + (y1 - y2)**2)**0.5
length_peri += length
return length_peri
def area(self):
area = 0
for i in range(len(self.point_list)):
point1 = self.point_list[i - 1]
point2 = self.point_list[i]
x1, y1 = point1
x2, y2 = point2
trapezoid = ((x2 - x1) * (y1 + y2)) / 2
area += trapezoid
return abs(area)
def bound_points(self):
unzip_list = list(zip(*self.point_list))
x_list = unzip_list[0]
y_list = unzip_list[1]
bound1 = max(x_list), min(y_list)
bound2 = max(x_list), max(y_list)
bound3 = min(x_list), max(y_list)
bound4 = min(x_list), min(y_list)
return [bound1, bound2, bound3, bound4]
def move_poly(self, dx, dy):
new_point_list = []
for x, y in self.point_list:
new_point = (x + dx, y + dy)
new_point_list.append(new_point)
return self.over_write_points(new_point_list)
def draw_poly(self, lineColour='green', fillColour='yellow'):
self.pencolor(lineColour)
self.fillcolor(fillColour)
start, *remaining_points = self.point_list
self.penup()
self.goto(start)
self.pendown()
self.begin_fill()
for point in remaining_points: # go through a list of points
self.goto(point)
self.goto(start)
self.end_fill()
self.penup()
return 'The polygon is finished'
if __name__ == '__main__':
from turtle import Screen
screen = Screen()
test_polygon = Polygon([(50, 0), (50, 50), (0, 50)])
print(test_polygon.add_point((0, 0)))
print(test_polygon.over_write_points([(100, 0), (100, 100), (0, 100), (0, 0)]))
print(test_polygon.perimeter(), "pixels")
print(test_polygon.area(), "pixels squared")
print(test_polygon.bound_points())
print(test_polygon.move_poly(-10, -10))
print(test_polygon.draw_poly())
screen.exitonclick()
Related
I have a script that creates Circle GraphicsObjects and applies newton's laws to them (gravitational pull), and moves each shape by its respective x,y velocity at each time step.
My goal is to draw a line between each of these circles and to move each line along with each circle every time step.
Circle.move(dx, dy) takes two inputs, but the same method for a line should have 4 inputs for each of the end points of the line. This method does not exist as far as I can tell.
The code in my project may be hard to digest so I added a sample of something similar. I would expect the code below to move the line like the hands of a clock (i.e., one point stays in the center, and the other end of the line rotates around the center in a circle).
I attempted to create a new version of the Line object and give it the move method, but this doesn't work as expected.
from graphics import *
import math, random, time
width = 500
height = 500
win = GraphWin("Example",width,height)
resolution = 10 # steps/lines per rotation
centerX = width/2
centerY = height/2
radius1 = 300
P1 = Point(centerX,centerY)
class newLine(Line):
def move(self, dx1, dy1, dx2, dy2):
self.p1.x = self.p1.x + dx1
self.p1.y = self.p1.y + dy1
self.p2.x = self.p2.x + dx2
self.p2.y = self.p2.y + dy2
circleX = centerX+radius1*math.cos(2*math.pi*0/resolution)
circleY = centerY+radius1*math.sin(2*math.pi*0/resolution)
P2 = Point(circleX,circleY)
L1 = newLine(P1,P2)
L1.draw(win)
for i in range(resolution):
circleX2 = centerX+radius1*math.cos(2*math.pi*(i+1)/resolution)
circleY2 = centerY+radius1*math.sin(2*math.pi*(i+1)/resolution)
dx = circleX2-circleX
dy = circleY2-circleY
L1.move(0,0,dx,dy)
time.sleep(0.2)
win.getMouse()
win.close()
First idea:
Remove old line using undraw() and next create new Line and draw it.
It can be the only method - because it uses tkinter.Canvas and it can't rotate when it moves.
l1.undraw()
p2 = Point(circle_x, circle_y)
l1 = Line(p1, p2)
l1.draw(win)
Full working code
from graphics import *
import math
import random
import time # PEP8: every module in separated line
# --- constansts --- # PEP8: `UPPER_CASE_NAMES`
WIDTH = 500
HEIGHT = 500
# --- classes --- # PEP8: `CamelCaseNames`
# empty
# --- main ---- # PEP8: `lower_case_names`
# PEP8: space after comma
win = GraphWin("Example", WIDTH, HEIGHT)
resolution = 10 # steps/lines per rotation # PEP8: two spaces before #
center_x = WIDTH/2
center_y = HEIGHT/2
radius = 100
p1 = Point(center_x, center_y)
value = 0 # `2 * math.pi * 0 / resolution` gives `0`
circle_x = center_x + radius * math.cos(value)
circle_y = center_y + radius * math.sin(value)
p2 = Point(circle_x, circle_y)
l1 = Line(p1, p2)
l1.draw(win)
time.sleep(0.1)
for r in range(3): # repeat 3 times
for i in range(1, resolution+1):
value = 2 * math.pi * (i / resolution)
circle_x = center_x + radius * math.cos(value)
circle_y = center_y + radius * math.sin(value)
l1.undraw()
p2 = Point(circle_x, circle_y)
l1 = Line(p1, p2)
l1.draw(win)
time.sleep(0.1)
win.getMouse()
win.close()
PEP 8 -- Style Guide for Python Code
BTW:
You can get full path to source code
import graphics
print( graphics.__file__ )
and you can check how move() works - maybe it use the same method.
EDIT:
Digging in source code I got another idea - use Polygon which has list of points and you can move last point - but it needs to calculate dx, dy - or simpler you can replace last point.
But both methods needs ot use undraw() + draw() to refresh screen.
Teoretically update() should refresh screen but it doesn't do it.
p2 = Point(circle_x, circle_y)
l1 = Polygon(p1, p2)
l1.draw(win)
time.sleep(0.1)
for i in range(1, resolution+1):
value = 2 * math.pi * (i / resolution)
new_circle_x = center_x + radius * math.cos(value)
new_circle_y = center_y + radius * math.sin(value)
dx = new_circle_x - circle_x
dy = new_circle_y - circle_y
l1.points[-1].move(dx, dy)
circle_x = new_circle_x
circle_y = new_circle_y
# ---
# force to redraw it
l1.undraw()
l1.draw(win)
#update() # doesn't redraw it
time.sleep(0.1)
or
p2 = Point(circle_x, circle_y)
l1 = Polygon(p1, p2)
l1.draw(win)
time.sleep(0.1)
for i in range(1, resolution+1):
value = 2 * math.pi * (i / resolution)
circle_x = center_x + radius * math.cos(value)
circle_y = center_y + radius * math.sin(value)
p2 = Point(circle_x, circle_y)
l1.points[-1] = p2
# ---
# force to redraw it
l1.undraw()
l1.draw(win)
#update() # doesn't redraw it
time.sleep(0.1)
EDIT:
After digging in source code I created
class NewLine(Line):
def move(self, dx1, dy1, dx2, dy2):
self.p1.x += dx1
self.p1.y += dy1
self.p2.x += dx2
self.p2.y += dy2
canvas = self.canvas
self.undraw()
self.draw(canvas)
or
class NewLine(Line):
def move(self, dx1, dy1, dx2, dy2):
self.p1.x += dx1
self.p1.y += dy1
self.p2.x += dx2
self.p2.y += dy2
self.canvas.coords(self.id, self.p1.x, self.p1.y, self.p2.x, self.p2.y)
self.canvas.update()
#update() # it needs `from graphics import *`
In similar way it can move end point.
class NewLine(Line):
def _change(self):
#canvas = self.canvas
#self.undraw()
#self.draw(canvas)
# OR
self.canvas.coords(self.id, self.p1.x, self.p1.y, self.p2.x, self.p2.y)
self.canvas.update()
#update() # it needs `from graphics import *`
def moveP2ToXY(self, x, y):
self.p2.x = x
self.p2.y = y
self._change()
def moveP2ToPoint(self, point):
self.p2.x = point.x
self.p2.y = point.y
self._change()
def move(self, dx1, dy1, dx2, dy2):
self.p1.x += dx1
self.p1.y += dy1
self.p2.x += dx2
self.p2.y += dy2
self._change()
def moveTo(self, x1, y1, x2, y2):
self.p1.x = x1
self.p1.y = y1
self.p2.x = x2
self.p2.y = y2
self._change()
I want to place a text centrally above the line (controlled with variable distance). There is a method drawLineDescription() for this. This method takes the start and endpoints of the lines and then calculates the center point x, y. I have already worked with angle to ensure that the text is placed correctly. Unfortunately, I can't figure out how to place the text vertically over the line at every angle, i.e. depending on the rotation the variables x, y have to be able to move. How can I supplement this?
def drawLineDescription(canvas, startX, startY, endX, endY, distance):
lengthX = endX - startX
lengthY = endY - startY
x = int(startX+((lengthX)/2))
y = int(startY+((lengthY)/2))
angle = math.degrees(math.atan2(lengthY, lengthX)*-1)
angle = round(angle)
if angle < -90 or angle > 90:
angle += 180
canvas.create_text(x, y, angle=angle, font=("Arial", 12), text="exampleText")
In the end it should look like this (example of multiple lines with text - the lines never cross their text):
Example result
lengthX = endX - startX
lengthY = endY - startY
fullLength = math.sqrt(lengthX**2 + lengthY**2)
#unit direction vector
ux = lengthX / fullLength
uy = lengthY / fullLength
#unit normal
if ux < 0:
nx, ny = -uy, ux
else:
nx, ny = uy, -ux
#text center point (D at the picture)
cx = x + nx * distance
cy = y + ny * distance
#if you need start of text (S at the picture)
sx = x + nx * distance - ux * halfwidth
sy = y + ny * distance - uy * halfwidth
You can draw rotated text on tkinter if you are using tcl > 8.6 with the following instructions: canvas_item = tk.create_text, and canvas.itemconfig(canvas_item, angle=rotation_angle)
In order to achieve what you want, you need a little bit of geometry, notably the coordinates of the line segment, its mid point, an offset vector perpendicular to the segment, and the angle of the segment.
I encapsulated the arithmetic necessary to calculate the proper geometric elements in a class point, and in c class Vector. These classes are not bullet proof, but they give you a starting point for basic geometry.
I added an example of a line defined by two points, with the text placed as you need and rotated to match the direction of the line segment.
import math
import tkinter as tk
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other: 'Vector'):
return Vector(other.x + self.x, other.y + self.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __iter__(self):
yield self.x
yield self.y
def __str__(self):
return f'{self.__class__.__name__}({self.x}, {self.y})'
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(other.x + self.x, other.y + self.y)
def __sub__(self, other):
return Vector(other.x - self.x, other.y - self.y)
def scale(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def normal(self):
norm = self.norm()
return Vector(self.x / norm, self.y / norm)
def norm(self):
return math.hypot(self.x, self.y)
def perp(self):
x, y = self.normal()
return Vector(y, -x)
def angle(self):
return math.atan2(-self.y, self.x) * (180 / math.pi)
def __iter__(self):
yield self.x
yield self.y
def __str__(self):
return f'{self.__class__.__name__}({self.x}, {self.y})'
if __name__ == '__main__':
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=500)
p0, p1 = Point(100, 40), Point(200, 300)
segment = p1 - p0
mid_point = segment.scale(0.5) + p0
# canvas.create_oval(*(mid_point - Vector(2, 2)), *(Vector(2, 2) + mid_point))
line = canvas.create_line(*p0, *p1)
offset = segment.perp().scale(20)
# canvas.create_line(*mid_point, *(mid_point+offset))
txt = canvas.create_text(*(offset + mid_point), text='example')
canvas.itemconfig(txt, angle=segment.angle())
canvas.pack()
root.mainloop()
I'm attempting to create a triangle tessellation like the following in Python:
All I've gotten is Sierpensky's triangle. I assume it'd use some of the same code.
import turtle as t
import math
import colorsys
t.hideturtle()
t.speed(0)
t.tracer(0,0)
h = 0
def draw_tri(x,y,size):
global h
t.up()
t.goto(x,y)
t.seth(0)
t.down()
color = colorsys.hsv_to_rgb(h,1,1)
h += 0.1
t.color(color)
t.left(120)
t.fd(size)
t.left(120)
t.fd(size)
t.end_fill()
def draw_s(x,y,size,n):
if n == 0:
draw_tri(x,y,size)
return
draw_s(x,y,size/2,n-1)
draw_s(x+size/2,y,size/2,n-1)
draw_s(x+size/4,y+size*math.sqrt(3)/4,size/2,n-1)
draw_s(-300,-250,600,6)
t.update()
There are various approaches; the following example generates all line segments prior to directing the turtle to draw them on the canvas.
import turtle as t
import math
WIDTH, HEIGHT = 800, 800
OFFSET = -WIDTH // 2, -HEIGHT // 2
class Point:
"""convenience for point arithmetic
"""
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __iter__(self):
yield self.x
yield self.y
def get_line_segments(side_length=50):
"""calculates the coordinates of all vertices
organizes them by line segment
stores the segments in a container and returns it
"""
triangle_height = int(side_length * math.sqrt(3) / 2)
half_side = side_length // 2
p0 = Point(0, 0)
p1 = Point(0, side_length)
p2 = Point(triangle_height, half_side)
segments = []
for idx, x in enumerate(range(-triangle_height, WIDTH+1, triangle_height)):
for y in range(-side_length, HEIGHT+1, side_length):
y += half_side * (idx%2 + 1)
offset = Point(x, y)
pa, pb, pc = p0 + offset, p1 + offset,p2 + offset
segments += [[pa, pb], [pb, pc], [pc, pa]]
return segments
def draw_segment(segment):
p0, p1 = segment
p0, p1 = p0 + offset, p1 + offset
t.penup()
t.goto(p0)
t.pendown()
t.goto(p1)
def draw_tiling():
for segment in get_line_segments():
draw_segment(segment)
t.hideturtle()
t.speed(0)
t.tracer(0,0)
offset = Point(*OFFSET)
draw_tiling()
t.update()
t.exitonclick()
If you want to see how the tiling is traced, you can replace the following lines:
# t.hideturtle()
t.speed(1)
# t.tracer(0, 0)
and enlarge the canvas screen with your mouse to see the boundary of the tiling (I made it overlap the standard size of the window)
As #ReblochonMasque notes, there are multiple approaches to the problem. Here's one I worked out to use as little turtle code as possible to solve the problem:
from turtle import Screen, Turtle
TRIANGLE_SIDE = 60
TRIANGLE_HEIGHT = TRIANGLE_SIDE * 3 ** 0.5 / 2
CURSOR_SIZE = 20
screen = Screen()
width = TRIANGLE_SIDE * (screen.window_width() // TRIANGLE_SIDE)
height = TRIANGLE_HEIGHT * (screen.window_height() // TRIANGLE_HEIGHT)
diagonal = width + height
turtle = Turtle('square', visible=False)
turtle.shapesize(diagonal / CURSOR_SIZE, 1 / CURSOR_SIZE)
turtle.penup()
turtle.sety(height/2)
turtle.setheading(270)
turtle = turtle.clone()
turtle.setx(width/2)
turtle.setheading(210)
turtle = turtle.clone()
turtle.setx(-width/2)
turtle.setheading(330)
for _ in range(int(diagonal / TRIANGLE_HEIGHT)):
for turtle in screen.turtles():
turtle.forward(TRIANGLE_HEIGHT)
turtle.stamp()
screen.exitonclick()
It probably could use optimizing but it gets the job done. And it's fun to watch...
I have made this algorithm myself based on how to rotate an image.
import cv2
import math
import numpy as np
class rotation:
angle = 60.0
x = 100
y = 100
img = cv2.imread('FNAF.png',0)
width,height = img.shape
def showImage(name, self):
cv2.imshow(name, self.img)
def printWidthHeight(self):
print(self.width)
print(self.height)
def rotateImage(self):
ForwardMap = np.zeros((self.width,self.height),dtype="uint8")
BackwardMap = np.zeros((self.width,self.height),dtype="uint8")
for i in range(self.width):
for j in range(self.height):
# forward mapping
for_x = (i - self.x) * math.cos(self.angle*(math.pi/180)) - (j - self.y) * math.sin(self.angle*(math.pi/180)) + self.x
for_y = (i - self.x) * math.sin(self.angle*(math.pi/180)) + (j - self.y) * math.cos(self.angle*(math.pi/180)) + self.x
for_x = int(for_x)
for_y = int(for_y)
# backward mapping should change the forward mapping to the original image
back_x = (i - self.x) * math.cos(self.angle*(math.pi/180)) + (j - self.y) * math.sin(self.angle*(math.pi/180)) + self.x
back_y = -(i - self.x) * math.sin(self.angle*(math.pi/180)) + (j - self.y) * math.cos(self.angle*(math.pi/180)) + self.x
back_x = int(back_x)
back_y = int(back_y)
if for_x in range(self.width) and for_y in range(self.height):
ForwardMap[i, j] = self.img[for_x, for_y]
else:
pass
if back_x in range(self.width) and back_y in range(self.height):
BackwardMap[i, j] = self.img[back_x, back_y]
else:
pass
cv2.imshow('Forward Mapping', ForwardMap)
cv2.imshow('Backward Mapping', BackwardMap)
def demo():
rotation.showImage('normal', rotation)
rotation.printWidthHeight(rotation)
rotation.rotateImage(rotation)
cv2.waitKey(0)
cv2.destroyAllWindows
if __name__ == '__main__':
demo()
My problem is now that I want to make the rotated pictures (both for forward and backwards mapping) fit JUST right so there is no unnecessary use of space. Can anybody help me with this? Much appreciated.
If you have any suggestions to optimize my code, feel free to comment on that as well.
Your original image has 4 corners, coordinates: (0,0), (w-1, 0), (0,h-1) and (w-1, h-1). Since your transformation is affine, why not transform these coordinates into destination coordinates?
(0,0) → (x1, y1)
(w-1, 0) → (x2, y2)
(0, h-1) → (x3, y3)
(w-1, h-1) → (x4, y4)
Your destination image size is then:
width = max(x1, x2, x3, x4) - min(x1, x2, x3, x4)
height = max(y1, y2, y3, y4) - min(y1, y2, y3, y4)
I'm trying to port to Python the "Controlled Circle Packing with Processing" algorithm that I found here:
http://www.codeplastic.com/2017/09/09/controlled-circle-packing-with-processing/?replytocom=22#respond
For now my goal is just to make it work, before I tweak it for my own needs. This question is not about the best way to do circle packing.
So far here is what I have:
#!/usr/bin/python
# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
from random import uniform
class Ball:
def __init__(self, x, y, radius):
self.r = radius
self.acceleration = np.array([0, 0])
self.velocity = np.array([uniform(0, 1),
uniform(0, 1)])
self.position = np.array([x, y])
#property
def x(self):
return self.position[0]
#property
def y(self):
return self.position[1]
def applyForce(self, force):
self.acceleration = np.add(self.acceleration, force)
def update(self):
self.velocity = np.add(self.velocity, self.acceleration)
self.position = np.add(self.position, self.velocity)
self.acceleration *= 0
class Pack:
def __init__(self, radius, list_balls):
self.list_balls = list_balls
self.r = radius
self.list_separate_forces = [np.array([0, 0])] * len(self.list_balls)
self.list_near_balls = [0] * len(self.list_balls)
def _normalize(self, v):
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
def run(self):
for i in range(300):
print(i)
for ball in self.list_balls:
self.checkBorders(ball)
self.checkBallPositions(ball)
self.applySeparationForcesToBall(ball)
def checkBorders(self, ball):
if (ball.x - ball.r) < - self.r or (ball.x + ball.r) > self.r:
ball.velocity[0] *= -1
ball.update()
if (ball.y - ball.r) < -self.r or (ball.y + ball.r) > self.r:
ball.velocity[1] *= -1
ball.update()
def checkBallPositions(self, ball):
list_neighbours = [e for e in self.list_balls if e is not ball]
for neighbour in list_neighbours:
d = self._distanceBalls(ball, neighbour)
if d < (ball.r + neighbour.r):
return
ball.velocity[0] = 0
ball.velocity[1] = 0
def getSeparationForce(self, c1, c2):
steer = np.array([0, 0])
d = self._distanceBalls(c1, c2)
if d > 0 and d < (c1.r + c2.r):
diff = np.subtract(c1.position, c2.position)
diff = self._normalize(diff)
diff = np.divide(diff, d)
steer = np.add(steer, diff)
return steer
def _distanceBalls(self, c1, c2):
x1, y1 = c1.x, c1.y
x2, y2 = c2.x, c2.y
dist = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
return dist
def applySeparationForcesToBall(self, ball):
i = self.list_balls.index(ball)
list_neighbours = [e for e in self.list_balls if e is not ball]
for neighbour in list_neighbours:
j = self.list_balls.index(neighbour)
forceij = self.getSeparationForce(ball, neighbour)
if np.linalg.norm(forceij) > 0:
self.list_separate_forces[i] = np.add(self.list_separate_forces[i], forceij)
self.list_separate_forces[j] = np.subtract(self.list_separate_forces[j], forceij)
self.list_near_balls[i] += 1
self.list_near_balls[j] += 1
if self.list_near_balls[i] > 0:
self.list_separate_forces[i] = np.divide(self.list_separate_forces[i], self.list_near_balls[i])
if np.linalg.norm(self.list_separate_forces[i]) > 0:
self.list_separate_forces[i] = self._normalize(self.list_separate_forces[i])
self.list_separate_forces[i] = np.subtract(self.list_separate_forces[i], ball.velocity)
self.list_separate_forces[i] = np.clip(self.list_separate_forces[i], a_min=0, a_max=np.array([1]))
separation = self.list_separate_forces[i]
ball.applyForce(separation)
ball.update()
list_balls = list()
for i in range(10):
b = Ball(0, 0, 7)
list_balls.append(b)
p = Pack(30, list_balls)
p.run()
plt.axes()
# Big container
circle = plt.Circle((0, 0), radius=30, fc='none', ec='k')
plt.gca().add_patch(circle)
for c in list_balls:
ball = plt.Circle((c.x, c.y), radius=c.r, picker=True, fc='none', ec='k')
plt.gca().add_patch(ball)
plt.axis('scaled')
plt.show()
The code was originally written with Processing, I did my best to use numpy instead.
I'm not quite sure of my checkBallPosition, the original author uses a count variable that looks useless to me. I also wonder why the steer vector in the original code has a dimension of 3.
So far, here is what my code yields:
The circles (I had to rename them balls to not conflict with Circle from matplotlib) overlap and don't seem to get away from each other. I don't think I'm really far but I would need a bit of help to find what's wrong with my code. Could you help me please ?
EDIT: I realize that I probably need to do several passes. Maybe the Processing package (language ?) runs the run function several times. It actually makes sense to me, this problem is very similar to molecular mechanics optimization and it's an iterative process.
My question can now be a bit more specific: it seems the checkBorders function doesn't do its job properly and doesn't rebound the circles properly. But given its simplicity, I'd say the bug is in applySeparationForcesToBall, I probably don't apply the forces correctly.
Ok after days of fiddling, I managed to do it:
Here is the complete code:
#!/usr/bin/python
# coding: utf-8
"""
http://www.codeplastic.com/2017/09/09/controlled-circle-packing-with-processing/
https://stackoverflow.com/questions/573084/how-to-calculate-bounce-angle/573206#573206
https://stackoverflow.com/questions/4613345/python-pygame-ball-collision-with-interior-of-circle
"""
import numpy as np
import matplotlib.pyplot as plt
from random import randint
from random import uniform
from matplotlib import animation
class Ball:
def __init__(self, x, y, radius):
self.r = radius
self.acceleration = np.array([0, 0])
self.velocity = np.array([uniform(0, 1),
uniform(0, 1)])
self.position = np.array([x, y])
#property
def x(self):
return self.position[0]
#property
def y(self):
return self.position[1]
def applyForce(self, force):
self.acceleration = np.add(self.acceleration, force)
def _normalize(self, v):
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
def update(self):
self.velocity = np.add(self.velocity, self.acceleration)
self.position = np.add(self.position, self.velocity)
self.acceleration *= 0
class Pack:
def __init__(self, radius, list_balls):
self.iter = 0
self.list_balls = list_balls
self.r = radius
self.list_separate_forces = [np.array([0, 0])] * len(self.list_balls)
self.list_near_balls = [0] * len(self.list_balls)
self.wait = True
def _normalize(self, v):
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
def run(self):
self.iter += 1
for ball in self.list_balls:
self.checkBorders(ball)
self.checkBallPositions(ball)
self.applySeparationForcesToBall(ball)
print(ball.position)
print("\n")
def checkBorders(self, ball):
d = np.sqrt(ball.x**2 + ball.y**2)
if d >= self.r - ball.r:
vr = self._normalize(ball.velocity) * ball.r
# P1 is collision point between circle and container
P1x = ball.x + vr[0]
P1y = ball.y + vr[1]
P1 = np.array([P1x, P1y])
# Normal vector
n_v = -1 * self._normalize(P1)
u = np.dot(ball.velocity, n_v) * n_v
w = np.subtract(ball.velocity, u)
ball.velocity = np.subtract(w, u)
ball.update()
def checkBallPositions(self, ball):
i = self.list_balls.index(ball)
# for neighbour in list_neighbours:
# ot a full loop; if we had two full loops, we'd compare every
# particle to every other particle twice over (and compare each
# particle to itself)
for neighbour in self.list_balls[i + 1:]:
d = self._distanceBalls(ball, neighbour)
if d < (ball.r + neighbour.r):
return
ball.velocity[0] = 0
ball.velocity[1] = 0
def getSeparationForce(self, c1, c2):
steer = np.array([0, 0])
d = self._distanceBalls(c1, c2)
if d > 0 and d < (c1.r + c2.r):
diff = np.subtract(c1.position, c2.position)
diff = self._normalize(diff)
diff = np.divide(diff, 1 / d**2)
steer = np.add(steer, diff)
return steer
def _distanceBalls(self, c1, c2):
x1, y1 = c1.x, c1.y
x2, y2 = c2.x, c2.y
dist = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
return dist
def applySeparationForcesToBall(self, ball):
i = self.list_balls.index(ball)
for neighbour in self.list_balls[i + 1:]:
j = self.list_balls.index(neighbour)
forceij = self.getSeparationForce(ball, neighbour)
if np.linalg.norm(forceij) > 0:
self.list_separate_forces[i] = np.add(self.list_separate_forces[i], forceij)
self.list_separate_forces[j] = np.subtract(self.list_separate_forces[j], forceij)
self.list_near_balls[i] += 1
self.list_near_balls[j] += 1
if np.linalg.norm(self.list_separate_forces[i]) > 0:
self.list_separate_forces[i] = np.subtract(self.list_separate_forces[i], ball.velocity)
if self.list_near_balls[i] > 0:
self.list_separate_forces[i] = np.divide(self.list_separate_forces[i], self.list_near_balls[i])
separation = self.list_separate_forces[i]
ball.applyForce(separation)
ball.update()
list_balls = list()
for i in range(25):
# b = Ball(randint(-15, 15), randint(-15, 15), 5)
b = Ball(0, 0, 5)
list_balls.append(b)
p = Pack(30, list_balls)
fig = plt.figure()
circle = plt.Circle((0, 0), radius=30, fc='none', ec='k')
plt.gca().add_patch(circle)
plt.axis('scaled')
plt.axes().set_xlim(-50, 50)
plt.axes().set_ylim(-50, 50)
def draw(i):
patches = []
p.run()
fig.clf()
circle = plt.Circle((0, 0), radius=30, fc='none', ec='k')
plt.gca().add_patch(circle)
plt.axis('scaled')
plt.axes().set_xlim(-50, 50)
plt.axes().set_ylim(-50, 50)
for c in list_balls:
ball = plt.Circle((c.x, c.y), radius=c.r, picker=True, fc='none', ec='k')
patches.append(plt.gca().add_patch(ball))
return patches
co = False
anim = animation.FuncAnimation(fig, draw,
frames=500, interval=2, blit=True)
# plt.show()
anim.save('line2.gif', dpi=80, writer='imagemagick')
From the original code, I modified the checkBorder function to bounce the circles properly from the edge, and changed the separation force between circles, it was too low. I know my question was a bit too vague from start, but I would have appreciated a more constructive feedback.