Last active
August 2, 2020 16:25
-
-
Save PardhavMaradani/fab191c37e99cfbd92b769fc422d141d to your computer and use it in GitHub Desktop.
Blog: Polygon Stacks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# https://pardhav-m.blogspot.com/2020/06/polygon-stacks.html | |
# https://trinket.io/embed/python/17361f8ebd | |
# Polygon Stacks | |
# Polygon Stacks | |
import turtle | |
import math | |
import time | |
# screen setup | |
screen = turtle.Screen() | |
wh = screen.window_height() # window height | |
ww = 1.5 * wh # window width | |
# trinket returns the same window width and height (hence square) | |
# set it to a rectangular area | |
screen.setup(ww, wh) | |
screen_radius = wh / 2 | |
screen_radius_factor = 0.95 | |
top_clip_window = [(-ww/2, wh/2), (-ww/2, 0), (ww/2, 0), (ww/2, wh/2)] # top half | |
bottom_clip_window = [(-ww/2, 0), (-ww/2, -wh/2), (ww/2, -wh/2), (ww/2, 0)] # bottom half | |
# turtle setup | |
t = turtle.Turtle() | |
t.ht() | |
t.speed(0) | |
t.tracer(0) | |
# draw a border | |
def draw_border(): | |
t.pu() | |
t.goto(-ww/2, -wh/2) | |
t.pd() | |
t.goto(ww/2, -wh/2) | |
t.goto(ww/2, wh/2) | |
t.goto(-ww/2, wh/2) | |
t.goto(-ww/2, -wh/2) | |
# draw please wait | |
def draw_please_wait(): | |
t.pu() | |
t.goto(-ww/2 + 20, wh/2 - 40) | |
t.pd() | |
t.write("Please wait...", font=("Arial", 16, "normal")) | |
t.update() | |
time.sleep(0.001) | |
t.clear() | |
# regular polygon shape functions from https://www.mathsisfun.com/geometry/regular-polygons.html | |
def shape_radius(sides, length): | |
return length / (2 * math.sin(math.pi / sides)) | |
def shape_apothem(sides, length): | |
return shape_radius(sides, length) * math.cos(math.pi / sides) | |
def shape_length(sides, radius): | |
return 2 * radius * math.sin(math.pi / sides) | |
# get inner polygon to draw | |
# sides - number of sides | |
# cx, cy - center | |
# length - length of side | |
# xangle - angle wrt x axis | |
def get_polygon(sides, cx, cy, length, xangle): | |
radius = shape_radius(sides, length) | |
t.pu() | |
t.goto(cx, cy) | |
t.seth(270) # point down | |
t.right(math.degrees(math.pi / sides)) | |
t.fd(radius) | |
sx, sy = t.pos() # left most starting point | |
# get the rotated starting point | |
# see https://stackoverflow.com/questions/2259476/rotating-a-point-about-another-point-2d | |
sx -= cx | |
sy -= cy | |
s = math.sin(math.radians(xangle)) | |
c = math.cos(math.radians(xangle)) | |
rsx = cx + (sx * c - sy * s) | |
rsy = cy + (sx * s + sy * c) | |
t.goto(rsx, rsy) # goto rotated starting point | |
t.seth(xangle) # rotate turtle to start angle | |
# draw each side | |
polygon = [] | |
for s in range(sides): | |
polygon.append(t.pos()) | |
t.fd(length) | |
t.left(360 / sides) | |
t.goto(cx, cy) | |
t.pd() | |
return polygon | |
# fill a polygon | |
# polygon - vertices of polygon to fill | |
# color - fill color | |
# pen_size - pen size to draw outline | |
# index - polygon index | |
# checkerboard - whether to fill alternatively | |
def fill_polygon(polygon, color, pen_size): | |
# first fill white | |
t.pensize(1) | |
t.color(color) | |
for i, v in enumerate(polygon): | |
if i == 0: | |
t.pu() | |
t.goto(v) | |
t.pd() | |
t.begin_fill() | |
else: | |
t.goto(v) | |
t.end_fill() | |
# now draw outline except intersecting line on x axis | |
t.pensize(pen_size) | |
t.color('black') | |
for i, v1 in enumerate(polygon): | |
v2 = polygon[(i + 1) % len(polygon)] | |
if i == 0: | |
t.pu() | |
t.goto(v1) | |
t.pd() | |
_, y1 = v1 | |
_, y2 = v2 | |
if y1 == 0 and y2 == 0: | |
t.pu() | |
t.goto(v1) | |
t.goto(v2) | |
if y1 == 0 and y2 == 0: | |
t.pd() | |
# get intersecting polygon | |
# Code from: https://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#Python | |
# Referred from: https://stackoverflow.com/questions/37555770/control-shape-overlap-in-pythons-turtle | |
# subjectPolygon - polygon to be clipped | |
# clipPolygon - clipping window | |
def clip(subjectPolygon, clipPolygon): | |
def inside(p): | |
return(cp2[0]-cp1[0])*(p[1]-cp1[1]) > (cp2[1]-cp1[1])*(p[0]-cp1[0]) | |
def computeIntersection(): | |
dc = [ cp1[0] - cp2[0], cp1[1] - cp2[1] ] | |
dp = [ s[0] - e[0], s[1] - e[1] ] | |
n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0] | |
n2 = s[0] * e[1] - s[1] * e[0] | |
n3 = 1.0 / (dc[0] * dp[1] - dc[1] * dp[0]) | |
return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3) | |
outputList = subjectPolygon | |
cp1 = clipPolygon[-1] | |
for clipVertex in clipPolygon: | |
cp2 = clipVertex | |
inputList = outputList | |
outputList = [] | |
s = inputList[-1] | |
for subjectVertex in inputList: | |
e = subjectVertex | |
if inside(e): | |
if not inside(s): | |
outputList.append(computeIntersection()) | |
outputList.append(e) | |
elif inside(s): | |
outputList.append(computeIntersection()) | |
s = e | |
cp1 = cp2 | |
return(outputList) | |
# draw on outer polygon | |
# half - which half of the screen | |
# o_sides - outer sides | |
# o_length - outer side length | |
# i_sides - inner sides | |
# i_length - inner side length | |
# i_p_m - inner polygons multiple | |
# twists - number of twists | |
# ra - base rotate angle wrt x axis | |
# pen_size - pen size | |
# checkerboard - whether to fill alternatively | |
def draw_on_outer_polygon(half, o_sides, o_length, i_sides, i_length, i_p_m, twists, ra, pen_size, checkerboard): | |
o_radius = shape_radius(o_sides, o_length) | |
i_radius = shape_radius(i_sides, i_length) | |
t.pu() | |
t.goto(0, 0) | |
t.seth(270) # point down | |
t.right(math.degrees(math.pi / o_sides)) | |
t.fd(o_radius) | |
t.seth(0) # rotate turtle to start angle | |
# skip so we start on correct side | |
skip = 0 if half == 'top' else math.ceil(o_sides / 2) | |
for i in range(skip): | |
t.fd(o_length) | |
t.left(360 / o_sides) | |
t.pd() | |
# draw along each side of outer polygon | |
for s in range(o_sides): | |
delta = o_length / i_p_m | |
delta_angle = ((360 / i_sides) / i_p_m) * twists | |
# draw each inner polygon | |
for i in range(i_p_m): | |
t.pu() | |
t.fd(delta) | |
t.pd() | |
fillcolor = 'white' | |
if checkerboard: | |
fillcolor = 'white' if i % 2 == 0 else 'gray' | |
# save starting center | |
cx, cy = t.pos() | |
sh = t.heading() | |
# check if this polygon will be visible or needs intersection | |
visible = False | |
should_check_intersection = False | |
if half == 'top': | |
if cy > i_radius: | |
visible = True | |
elif cy >= -i_radius: | |
should_check_intersection = True | |
else: | |
if cy < -i_radius: | |
visible = True | |
elif cy <= i_radius: | |
should_check_intersection = True | |
# get the polygon only if will be drawn | |
if visible or should_check_intersection: | |
polygon = get_polygon(i_sides, cx, cy, i_length, ra + (delta_angle * i)) | |
else: | |
continue | |
# draw and fill inner polygon | |
if visible: | |
fill_polygon(polygon, fillcolor, pen_size) | |
elif should_check_intersection: | |
ip = None | |
try: | |
clip_window = top_clip_window if half == 'top' else bottom_clip_window | |
ip = clip(polygon, clip_window) | |
except (IndexError, ZeroDivisionError): | |
pass | |
if ip != None: | |
fill_polygon(ip, fillcolor, pen_size) | |
# go back to starting center | |
t.pu() | |
t.goto(cx, cy) | |
t.seth(sh) | |
t.pd() | |
t.left(360 / o_sides) | |
# draw polygonal stack | |
def draw_polygonal_stack( | |
outer_sides = 3, | |
inner_sides = 4, | |
inner_polygons_multiple = 1, | |
inner_length_factor = 1, | |
twists = 1, | |
pen_size = 1, | |
checkerboard = False, | |
animate = False, | |
step_angle = 2): | |
# calculate radius and lengths wrt screen size | |
outer_radius = (screen_radius * screen_radius_factor) / (1 + inner_length_factor * math.cos(math.pi / outer_sides)) | |
outer_length = shape_length(outer_sides, outer_radius) | |
inner_radius = shape_apothem(outer_sides, outer_length) | |
inner_length = shape_length(inner_sides, inner_radius) * inner_length_factor | |
# draw stack and animate if asked | |
ma = int(360 / inner_sides) | |
for ra in range(0, ma + step_angle, step_angle): | |
t.clear() | |
if not animate: | |
draw_please_wait() | |
draw_border() | |
# draw top half | |
draw_on_outer_polygon('top', outer_sides, outer_length, inner_sides, inner_length, inner_polygons_multiple, twists, ra, pen_size, checkerboard) | |
# draw bottom half | |
draw_on_outer_polygon('bottom', outer_sides, outer_length, inner_sides, inner_length, inner_polygons_multiple, twists, ra, pen_size, checkerboard) | |
t.update() | |
if not animate: | |
break | |
# main | |
outer_sides = 6 | |
inner_sides = 4 | |
inner_polygons_multiple = 30 | |
inner_length_factor = 0.2 | |
twists = 1 | |
pen_size = 1 | |
checkerboard = False | |
animate = False | |
step_angle = 2 | |
draw_polygonal_stack(outer_sides, inner_sides, inner_polygons_multiple, inner_length_factor, twists, pen_size, checkerboard, animate, step_angle) | |
# cover image | |
# first | |
# draw_polygonal_stack(5, 4, 40, 0.2) | |
# second | |
# draw_polygonal_stack(5, 4, 40, 0.8, 21) | |
# third | |
# draw_polygonal_stack(5, 3, 40, 1, 26, 1, True) | |
# penrose polygons | |
# triangle | |
# draw_polygonal_stack(3, 4, 60, 0.3) | |
# square | |
# draw_polygonal_stack(4, 4, 50, 0.22) | |
# pentagon | |
# draw_polygonal_stack(5, 4, 40, 0.18) | |
# rotating pentagon | |
# draw_polygonal_stack(5, 4, 40, 0.18, 1, 1, False, True, 9) | |
# polygon stack variants | |
# triangle | |
# draw_polygonal_stack(3, 3, 40, 0.5, 36) | |
# draw_polygonal_stack(3, 3, 40, 1, 19) | |
# draw_polygonal_stack(3, 3, 60, 1, 31, 1, True) | |
# square | |
# draw_polygonal_stack(4, 4, 50, 0.5, 24) | |
# draw_polygonal_stack(4, 4, 50, 1, 24) | |
# draw_polygonal_stack(4, 4, 50, 1, 26, 1, True) | |
# pentagon | |
# draw_polygonal_stack(5, 3, 40, 0.5, 13) | |
# draw_polygonal_stack(5, 3, 40, 1, 13) | |
# draw_polygonal_stack(5, 5, 40, 1, 21, 1, True) | |
# draw_polygonal_stack(5, 5, 40, 1, 21, 1, True, True, 4) | |
# rotating pentagon stack | |
# draw_polygonal_stack(5, 5, 40, 0.8, 21, 1, False, True, 8) | |
# rotatng checkerboard stack | |
# draw_polygonal_stack(5, 3, 40, 1, 19, 1, True, True, 3) | |
# rotating triangle checkerboard | |
# draw_polygonal_stack(3, 3, 60, 1, 31, 1, True, True, 10) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# https://pardhav-m.blogspot.com/2020/06/polygon-stacks.html | |
# https://trinket.io/embed/python/dc40d74c48 | |
# Polygon Stacks | |
# Circular Stacks | |
import turtle | |
import math | |
import time | |
# screen setup | |
screen = turtle.Screen() | |
wh = screen.window_height() # window height | |
ww = 1.5 * wh # window width | |
# trinket returns the same window width and height (hence square) | |
# set it to a rectangular area | |
screen.setup(ww, wh) | |
screen_radius = wh / 2 | |
screen_radius_factor = 0.95 | |
top_clip_window = [(-ww/2, wh/2), (-ww/2, 0), (ww/2, 0), (ww/2, wh/2)] # top half | |
bottom_clip_window = [(-ww/2, 0), (-ww/2, -wh/2), (ww/2, -wh/2), (ww/2, 0)] # bottom half | |
# turtle setup | |
t = turtle.Turtle() | |
t.ht() | |
t.speed(0) | |
t.tracer(0) | |
# draw a border | |
def draw_border(): | |
t.pu() | |
t.goto(-ww/2, -wh/2) | |
t.pd() | |
t.goto(ww/2, -wh/2) | |
t.goto(ww/2, wh/2) | |
t.goto(-ww/2, wh/2) | |
t.goto(-ww/2, -wh/2) | |
# draw please wait | |
def draw_please_wait(): | |
t.pu() | |
t.goto(-ww/2 + 20, wh/2 - 40) | |
t.pd() | |
t.write("Please wait...", font=("Arial", 16, "normal")) | |
t.update() | |
time.sleep(0.001) | |
t.clear() | |
# regular polygon shape functions from https://www.mathsisfun.com/geometry/regular-polygons.html | |
def shape_radius(sides, length): | |
return length / (2 * math.sin(math.pi / sides)) | |
def shape_apothem(sides, length): | |
return shape_radius(sides, length) * math.cos(math.pi / sides) | |
def shape_length(sides, radius): | |
return 2 * radius * math.sin(math.pi / sides) | |
# get inner polygon to draw | |
# sides - number of sides | |
# cx, cy - center | |
# length - length of side | |
# xangle - angle wrt x axis | |
def get_polygon(sides, cx, cy, length, xangle): | |
radius = shape_radius(sides, length) | |
t.pu() | |
t.goto(cx, cy) | |
t.seth(270) # point down | |
t.right(math.degrees(math.pi / sides)) | |
t.fd(radius) | |
sx, sy = t.pos() # left most starting point | |
# get the rotated starting point | |
# see https://stackoverflow.com/questions/2259476/rotating-a-point-about-another-point-2d | |
sx -= cx | |
sy -= cy | |
s = math.sin(math.radians(xangle)) | |
c = math.cos(math.radians(xangle)) | |
rsx = cx + (sx * c - sy * s) | |
rsy = cy + (sx * s + sy * c) | |
t.goto(rsx, rsy) # goto rotated starting point | |
t.seth(xangle) # rotate turtle to start angle | |
# draw each side | |
polygon = [] | |
for s in range(sides): | |
polygon.append(t.pos()) | |
t.fd(length) | |
t.left(360 / sides) | |
t.goto(cx, cy) | |
t.pd() | |
return polygon | |
# fill a polygon | |
# polygon - vertices of polygon to fill | |
# color - fill color | |
# pen_size - pen size to draw outline | |
def fill_polygon(polygon, color, pen_size): | |
# first fill white | |
t.pensize(1) | |
t.color(color) | |
for i, v in enumerate(polygon): | |
if i == 0: | |
t.pu() | |
t.goto(v) | |
t.pd() | |
t.begin_fill() | |
else: | |
t.goto(v) | |
t.end_fill() | |
# now draw outline except intersecting line on x axis | |
t.pensize(pen_size) | |
t.color('black') | |
for i, v1 in enumerate(polygon): | |
v2 = polygon[(i + 1) % len(polygon)] | |
if i == 0: | |
t.pu() | |
t.goto(v1) | |
t.pd() | |
_, y1 = v1 | |
_, y2 = v2 | |
if y1 == 0 and y2 == 0: | |
t.pu() | |
t.goto(v1) | |
t.goto(v2) | |
if y1 == 0 and y2 == 0: | |
t.pd() | |
# get intersecting polygon | |
# Code from: https://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#Python | |
# Referred from: https://stackoverflow.com/questions/37555770/control-shape-overlap-in-pythons-turtle | |
# subjectPolygon - polygon to be clipped | |
# clipPolygon - clipping window | |
def clip(subjectPolygon, clipPolygon): | |
def inside(p): | |
return(cp2[0]-cp1[0])*(p[1]-cp1[1]) > (cp2[1]-cp1[1])*(p[0]-cp1[0]) | |
def computeIntersection(): | |
dc = [ cp1[0] - cp2[0], cp1[1] - cp2[1] ] | |
dp = [ s[0] - e[0], s[1] - e[1] ] | |
n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0] | |
n2 = s[0] * e[1] - s[1] * e[0] | |
n3 = 1.0 / (dc[0] * dp[1] - dc[1] * dp[0]) | |
return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3) | |
outputList = subjectPolygon | |
cp1 = clipPolygon[-1] | |
for clipVertex in clipPolygon: | |
cp2 = clipVertex | |
inputList = outputList | |
outputList = [] | |
s = inputList[-1] | |
for subjectVertex in inputList: | |
e = subjectVertex | |
if inside(e): | |
if not inside(s): | |
outputList.append(computeIntersection()) | |
outputList.append(e) | |
elif inside(s): | |
outputList.append(computeIntersection()) | |
s = e | |
cp1 = cp2 | |
return(outputList) | |
# draw on outer circle | |
# half - which half of the screen | |
# o_radius - outer circle radius | |
# i_sides - inner sides | |
# i_length - inner side length | |
# i_p_m - inner polygons multiple | |
# twists - number of twists | |
# ra - base rotate angle wrt x axis | |
# pen_size - pen size | |
def draw_on_outer_circle(half, o_radius, i_sides, i_length, i_p_m, twists, ra, pen_size, checkerboard): | |
i_radius = shape_radius(i_sides, i_length) | |
t.pu() | |
t.goto(0, 0) | |
t.seth(270) # point down | |
t.fd(o_radius) | |
t.left(90) | |
# skip so we start on correct side | |
skip = 0 if half == 'top' else 1 | |
for i in range(skip): | |
t.circle(o_radius, 180) | |
t.pd() | |
n = 4 * i_p_m | |
fixed_angle = (90 / i_sides) / i_p_m | |
# draw each inner polygon | |
for i in range(n): | |
t.pu() | |
t.circle(o_radius, -(360 / n)) | |
t.pd() | |
fillcolor = 'white' | |
if checkerboard: | |
fillcolor = 'white' if i % 2 == 0 else 'gray' | |
# save starting center | |
cx, cy = t.pos() | |
sh = t.heading() | |
# check if this polygon will be visible or needs intersection | |
visible = False | |
should_check_intersection = False | |
if half == 'top': | |
if cy > i_radius: | |
visible = True | |
elif cy >= -i_radius: | |
should_check_intersection = True | |
else: | |
if cy < -i_radius: | |
visible = True | |
elif cy <= i_radius: | |
should_check_intersection = True | |
# get the polygon only if will be drawn | |
if visible or should_check_intersection: | |
ii = i | |
if half == 'bottom': | |
ii += (i_p_m * 2) | |
delta_angle = math.degrees(ii / n * 2 * math.pi) + (fixed_angle * ii * twists) | |
polygon = get_polygon(i_sides, cx, cy, i_length, -(ra + delta_angle)) | |
else: | |
continue | |
# draw and fill inner polygon | |
if visible: | |
fill_polygon(polygon, fillcolor, pen_size) | |
elif should_check_intersection: | |
ip = None | |
try: | |
clip_window = top_clip_window if half == 'top' else bottom_clip_window | |
ip = clip(polygon, clip_window) | |
except (IndexError, ZeroDivisionError): | |
pass | |
if ip != None: | |
fill_polygon(ip, fillcolor, pen_size) | |
# go back to starting center | |
t.pu() | |
t.goto(cx, cy) | |
t.seth(sh) | |
t.pd() | |
# draw circular stack | |
def draw_circular_stack( | |
inner_sides = 4, | |
inner_polygons_multiple = 1, | |
inner_length_factor = 1, | |
twists = 1, | |
pen_size = 1, | |
checkerboard = False, | |
animate = False, | |
step_angle = 2): | |
# calculate radius and lengths wrt screen size | |
outer_radius = (screen_radius * screen_radius_factor) / (1 + inner_length_factor) | |
inner_length = shape_length(inner_sides, outer_radius) * inner_length_factor | |
# draw stack and animate if asked | |
ma = int(360 / inner_sides) | |
for ra in range(0, ma + step_angle, step_angle): | |
t.clear() | |
if not animate: | |
draw_please_wait() | |
draw_border() | |
# draw top half | |
draw_on_outer_circle('top', outer_radius, inner_sides, inner_length, inner_polygons_multiple, twists, ra, pen_size, checkerboard) | |
# draw bottom half | |
draw_on_outer_circle('bottom', outer_radius, inner_sides, inner_length, inner_polygons_multiple, twists, ra, pen_size, checkerboard) | |
t.update() | |
if not animate: | |
break | |
# main | |
inner_sides = 4 | |
inner_polygons_multiple = 30 | |
inner_length_factor = 0.2 | |
twists = 6 | |
pen_size = 1 | |
checkerboard = False | |
animate = False | |
step_angle = 2 | |
draw_circular_stack(inner_sides, inner_polygons_multiple, inner_length_factor, twists, pen_size, checkerboard, animate, step_angle) | |
# circular stack variants | |
# draw_circular_stack(3, 40, 0.5, 30) | |
# draw_circular_stack(3, 40, 1, 35) | |
# draw_circular_stack(3, 40, 1, 27, 1, True) | |
# rotating circular stack | |
# draw_circular_stack(4, 30, 1, 20, 1, False, True, 2) | |
# draw_circular_stack(3, 22, 0.9, 15, 1, True, True, 6) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment