Setup¶

In [1]:
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

Constants & Functions¶

In [2]:
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)

Fiddler¶

In [3]:
#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');
In [4]:
rotatingSquare(2)
In [5]:
rotatingSquare(3)
In [6]:
rotatingSquare(5.1)
In [7]:
rotatingSquare(5.2)
Rohan Lewis¶

2025.02.10¶