import math
from math import pi
from math import cos as cos
from math import sin as sin
from math import sqrt as mS
import matplotlib
from matplotlib.animation import FuncAnimation as FA
from matplotlib.patches import Circle as mpC
from matplotlib.patches import Polygon as mpP
from matplotlib.patches import Rectangle as mpR
import matplotlib.pyplot as plt
import numpy as np
colors = ["#FDE725FF",
"#7AD151FF",
"#22A884FF",
"#2A788EFF",
"#414487FF",
"#440154FF"]
#Clear axes.
def clearAx(ax):
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["left"].set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
return(ax)
#Rotate unit square counterclockwise about origin.
def squareRot(a) :
return(mpR((-0.5, -0.5),
height = 1,
width = 1,
edgecolor = colors[2],
facecolor = 'w',
angle = a,
rotation_point = 'center',
lw = 2,
zorder = 1))
def bigPoint(xy, c, z) :
return(mpC(xy = xy,
radius = 0.03,
ec = c,
fc = c,
zorder = z))
#Return list of points.
def periodicPath(v) :
path = []
for i in range(361) :
#Horizontally back and forth.
if v == 2:
y = 0
if i < 180 :
x = 1/2 - i/180
else :
x = -3/2 + i/180
#Equilateral Triangle.
elif v == 3:
if i < 120 :
x = 1/2 - i/160
y = mS(3) * (i) / 480
elif i < 240 :
x = -1/4
y = mS(3) * (180-i) / 240
else :
x = -7/4 + i/160
y = -mS(3) * (360-i) / 480
#Square.
elif v == 4 :
if i < 90 :
x = 1/2 - i/180
y = i/180
elif i < 180 :
x = 1/2 - i/180
y = 1 - i/180
elif i < 270 :
x = -3/2 + i/180
y = 1 - i/180
else :
x = -3/2 + i/180
y = -2 + i/180
else :
#Constant values for pentagons.
px = 0.5*cos(72*pi/180)
py = 0.5*sin(72*pi/180)
qx = -0.5*cos(36*pi/180)
qy = 0.5*sin(36*pi/180)
#Pentagon :
if v == 5.1 :
if i < 72 :
x = 1/2 - (1/2-px)*i/72
y = py*i/72
elif i < 144 :
x = px - (px-qx)*(i-72)/72
y = py - (py-qy)*(i-72)/72
elif i < 216 :
x = qx
y = qy - (2*qy)*(i-144)/72
elif i < 288 :
x = qx - (qx-px)*(i-216)/72
y = -qy - (-qy+py)*(i-216)/72
else :
x = px - (px - 1/2)*(i-288)/72
y = -py - (-py)*(i-288)/72
#Pentagram :
if v == 5.2 :
if i < 72 :
x = 1/2 - (1/2-qx)*i/72
y = qy*i/72
elif i < 144 :
x = qx - (qx-px)*(i-72)/72
y = qy - (qy+py)*(i-72)/72
elif i < 216 :
x = px
y = -py - (-2*py)*(i-144)/72
elif i < 288 :
x = px - (px-qx)*(i-216)/72
y = py - (py+qy)*(i-216)/72
else :
x = qx - (qx - 1/2)*(i-288)/72
y = -qy - (-qy)*(i-288)/72
path.append((x, y))
return(path)
def textHelper(ax, y, text) :
return(ax.text(1.05, y, text, fontsize = 30, c = colors[5], ha = 'center', va = 'center'))
#5 decimal places for alignment.
def numericString(n) :
n = str(round(n, 5))
l = len(n)
n += '0'*(7-l)
return(n)
#Visual!
def rotatingSquare(v) :
fig = plt.figure(figsize = (9, 6))
ax = fig.add_subplot(xlim = (-0.707, 1.414),
ylim = (-0.707, 0.707))
fig.subplots_adjust(left = 0.02, bottom = 0.02, right = 0.98, top = 0.98)
#Remove axes and ticks.
ax = clearAx(ax)
#v specific.
path = periodicPath(v)
#Total Distance.
if v == 2 :
TD = 2
elif v == 3 :
TD = 3 * mS(3) / 2
elif v == 4 :
TD = 2 * mS(2)
elif v == 5.1 :
TD = 5 * mS(10-2*mS(5)) / 4 #https://xgeometry.com/formulas/pentagon
elif v == 5.2 :
TD = 5/2 * mS((5+mS(5)) / 2) #https://xgeometry.com/formulas/pentagon
#Six artists:
#Rotating square and point of contact.
#Ball and ball path.
#Rotation and Distance values.
#Square.
square = squareRot(0)
contact = bigPoint((0, 0.5), colors[2], 2)
#Ball.
ball = bigPoint((0, 0.5), colors[5], 3)
ball_path = mpP([path[0]], ec = colors[5], ls = ':', lw = 2, alpha = 0.5, fill = False)
#Rotation.
textHelper(ax, 0.4, 'Rotation :')
rotation = textHelper(ax, 0.25, '0°')
#Distance.
textHelper(ax, -0.1, 'Distance')
textHelper(ax, -0.25, 'Traveled :')
distance = textHelper(ax, -0.4, '0.00000')
def init():
ax.add_patch(square)
ax.add_patch(contact)
ax.add_patch(ball)
ax.add_patch(ball_path)
return square, contact, ball, ball_path
def animate(i) :
j = i
#Pentagram is faster!
if v == 5.2 :
j = 2*i
#Rotate square and contact point 1° counterclockwise.
square.set_angle(j+90)
contact.set_center((0.5*cos(pi*j/180), 0.5*sin(pi*j/180)))
#Move to next point on path, continue to draw path.
ball.set_center(path[i])
ball_path.set_xy(path[0:(i+1)] + list(reversed(path[0:(i+1)])))
#Update rotation and distance.
rotation.set_text(str(j) + '°')
distance.set_text(numericString(TD*i/360))
return square, contact, ball, ball_path, rotation, distance
#Run animation.
anim = FA(fig, animate, init_func = init, frames = 361, interval = 20, blit = True)
#Save animation.
anim.save('2025.02.07_Fiddler_EC_Path_' + str(v) + '.mp4');
rotatingSquare(2)
rotatingSquare(3)
rotatingSquare(5.1)
rotatingSquare(5.2)