古老的机械钟表蕴含着神秘的数学原理

2020-01-14 15:59:26 浏览数 (1)

时间是一个比较抽象的概念,是物质运动、变化的持续性、顺序性的表现。正因为人们需要研究物质的运动,就必须通过一个中介者来认识和度量时间,这个中介者就是计时器,从古代的沙漏、铜壶滴漏到近代的机械钟表,再到现代的电子钟表。我今天就通过编写一个显示机械钟表时间界面的程序(界面后面的发条传动装置啥的不在今天讨论的范围)来阐述其中的数学原理。

概述

机械钟表显示界面共有三个部分,外框(一般是圆)、刻度、指针。知道了这些,画出这个时钟就分为初始化、画外框、画刻度、画数字、画指针、更新指针这 6 个过程。

下面就可以搭建一个框架了,代码如下:

代码语言:javascript复制
 class Clock(Frame):
     def __init__(self, master):
         super().__init__(master)
 
     def draw_circle(self, center, radius):
         pass
 
     def draw_scale(self):
         pass
 
     def draw_number(self):
         pass
 
     def draw_pointer(self):
         pass
 
     def update(self):
         pass
 
 
 if __name__ == '__main__':
     root = Tk()
     clock = Clock(root)
     clock.mainloop()

初始化

首先我们需要一个画布、一个存放指针的列表(为了方便以后更新指针),然后画外框(在这里是画圆)、画刻度、画数字、画指针,最后更新指针。这里顺序同样不唯一,画刻度、画指针和画数字这 3 个操作可以互换顺序。

知道这些写出构造方法就是轻而易举了,代码如下:

代码语言:javascript复制
     def __init__(self, master):
         super().__init__(master)
         self.pointers = []
         self.canvas = Canvas(width=500, height=500)
         self.draw_circle((250, 250), 150)
         self.draw_scale()
         self.draw_number()
         self.draw_pointer()
         self.canvas.pack()
         self.update()

画外框

画外框就是画圆,要想确定一个圆必须知道圆心和半径。圆心就是所谓的坐标点,半径是圆心到圆周的距离。在这里,坐标系并不是画布中间为原点,而是左上角为原点,向右是 x 轴正方向,向下是 y 轴正方向,这个很重要,后面画刻度、画数字和画指针都是依赖于这个坐标系的。可是画布对象没有画圆方法,那么就可以看一下有没有画椭圆的方法,毕竟圆是特殊的椭圆,画椭圆的方法确实有,但是感觉怪怪的,因为传入的参数并不是我们所想的中心点和长半轴和短半轴的长度,而是椭圆外切矩形的左上角顶点坐标和右下角顶点坐标,也可以是外切矩形的左下角顶点坐标和右上角顶点坐标,那么我想画圆也就必须知道其外切正方形的左上角顶点坐标和右下角顶点坐标,那么这两个点的坐标能不能通过圆心坐标和半径进行转化呢?可以,而且非常简单!左上角坐标就是(圆心横坐标-半径, 圆心纵坐标-半径),右下角坐标就是(圆心横坐标 半径, 圆心纵坐标 半径)。

知道了这些代码实现就简单了,代码如下:

代码语言:javascript复制
     def draw_circle(self, center, radius):
         self.canvas.create_oval(center[0]-radius, center[1]-radius, center[0] radius, center[1] radius)

画刻度

画刻度就是画 60 根线段,这里以 12 点为第一个刻度,顺时针画下去,直到 60 根线段全部画完,整点为粗长线,不是整点的都是细短线。因为刻度把一整个圆分成了 60 份,因为一圈是 2π,因此每一份就是 2π/60 = π/30,画一个我们需要圆周上的一点,这个点的坐标很容易求出来,就是(圆心横坐标 半径*sinθ, 圆心纵坐标-半径*cosθ),θ 是当前坐标点和圆心的向量与 12 点方向的夹角,这个坐标记为(x1, y1)。线段长度和宽度非常简单,第一次因为画的是 12 点的刻度,所以画粗长线,接着画 4 根细短线之后再画一根粗长线,然后再画 4 根细短线……以此类推,直到全部画完。既然如此,线段长度与宽度表示就非常简单了,长度, 宽度 = (粗长线的长和宽)如果 i % 5 == 0否则(细短线的长和宽),画线段知道了一个点(就是上面的(x1, y1)),还需要一个点,这个点表示起来也很简单,画出图形一个 A 型相似就解决了,如图所示(画的不够好,请见谅)。

这里设 O 点为圆心,B 点为上面所说的(x1, y1),AB 就是上面所说的线段长度,A 就是我们要表示的点。因为两个三角形相似,所以 ∠1 = ∠2 = θ,所以 A 点坐标为(x1-线段长度*sinθ, y1 线段长度*cosθ),这样只要把 A、B 两点坐标传入画布对象的画线方法就行了,最后重复上述步骤,直到 60 根线都画完。这里逻辑不唯一,还有一种方法就是以 O 点为圆心,OA 长为半径画圆,注意 OA 长会随着线段长的变化而变化,根据偏角和 OA 长(也就是半径)直接得出 A 点坐标,下面画数字我就介绍这种方法。

知道了这些代码实现就简单了,代码如下:

代码语言:javascript复制
     def draw_scale(self):
         for i in range(60):
             theta = pi/30*i
             x1 = 250 150*sin(theta)
             y1 = 250-150*cos(theta)
             length, width = (20, 2)if i % 5 == 0else(10, 1)
             x2 = x1-length*sin(theta)
             y2 = y1 length*cos(theta)
             self.canvas.create_line(x1, y1, x2, y2, width=width)

画数字

因为上面画刻度时我把粗长线的长度设置成 20,粗长线终点坐标所围成的圆半径就是外框圆半径-20,因此为了避免数字和刻度线出现重合,数字中心点的坐标就必须在以 O 点为圆心,以外框圆半径-30 为半径的圆上。因为有 12 个数字,一圈是 2π,因此一圈被分成 12 份,每一份 2π/12 = π/6。第一个画上去的是 12 ,顺时针方向依次画 1,2……11。

知道了这些代码实现就简单了,代码如下:

代码语言:javascript复制
     def draw_number(self):
         number = (12,) tuple(range(1, 12))
         for i in range(12):
             theta = pi/6*i
             x = 250 120*sin(theta)
             y = 250-120*cos(theta)
             self.canvas.create_text(x, y, text=str(number[i]))

画指针

画指针很简单,先获取当前时间的时分秒,然后依次计算秒针偏角、分针偏角、时针偏角。有人会想当然的认为秒针偏角就是当前时间的秒数*π/30,分针偏角就是当前时间的分钟数*π/30,时针偏角就是当前时间的小时数*π/6,这样对吗?很明显,这样做完全错了,因为秒针走过去一步,分针和时针也会向前走,只不过是很小的一步,分针走过去一步,时针也会向前走很小的一步,这一小步不能忽略。虽然没有想的那么简单,但是没有过于复杂,秒针偏角 = 秒数*π/30,分针偏角 = 分钟数*π/30 秒数*π/30/60 = 分钟数*π/30 秒数*π/1800,时针偏角 = 小时数*π/6 分钟数*π/6/60 秒数*π/6/60/60 = 小时数*π/6 分钟数*π/360 秒数*π/21600,知道了指针的指向,接下来只要有指针的长度和宽度(粗细度),我们都知道时针最短最粗,分针在中间,秒针最长最细。在这里我为了做区分,还把三个指针用三种不同的颜色画出来了。为了方便后面更新,我把三个指针存在了 self.pointers 列表里面了。

知道了这些代码实现就简单了,代码如下:

代码语言:javascript复制
     def draw_pointer(self):
         now = localtime()
         hour = now.tm_hour
         minute = now.tm_min
         second = now.tm_sec
         alpha = pi/30*second  # 秒针偏角
         beta = pi/1800*second minute*pi/30  # 分针偏角
         gamma = pi/21600*second pi/360*minute pi/6*hour  # 时针偏角
         for length, theta, color, width in ((60, gamma, 'red', 3), (100, beta, 'green', 2), (120, alpha, 'blue', 1)):
             x = 250 length*sin(theta)
             y = 250-length*cos(theta)
             self.pointers.append(self.canvas.create_line(250, 250, x, y, fill=color, width=width))

更新指针

更新指针的逻辑非常简单,设置一个死循环,死循环内先等待一秒,然后删除三个指针,接着调用 self.draw_pointer() 重新画上指针,一直循环下去,直到程序退出,为了避免程序退出时会引发 tkinter.TclError 异常,我把这个死循环直接放在 try...except TclError... 中的 try 里面。

这里没有太多的数学元素,代码实现非常简单,代码如下:

代码语言:javascript复制
     def update(self):
         try:
             while True:
                 sleep(1)
                 for i in range(len(self.pointers)):
                     self.canvas.delete(self.pointers[i])
                 self.pointers.clear()
                 self.draw_pointer()
                 super().update()
         except TclError:
             pass

最后我直接给出完整的源代码,如下所示:

代码语言:javascript复制
 from tkinter import Frame, Tk, Canvas, TclError
 from math import pi, sin, cos
 from time import localtime, sleep
 
 
 class Clock(Frame):
     def __init__(self, master):
         super().__init__(master)
         self.pointers = []
         self.canvas = Canvas(width=500, height=500)
         self.draw_circle((250, 250), 150)
         self.draw_scale()
         self.draw_number()
         self.draw_pointer()
         self.canvas.pack()
         self.update()
 
     def draw_circle(self, center, radius):
         self.canvas.create_oval(center[0]-radius, center[1]-radius, center[0] radius, center[1] radius)
 
     def draw_scale(self):
         for i in range(60):
             theta = pi/30*i
             x1 = 250 150*sin(theta)
             y1 = 250-150*cos(theta)
             length, width = (20, 2)if i % 5 == 0else(10, 1)
             x2 = x1-length*sin(theta)
             y2 = y1 length*cos(theta)
             self.canvas.create_line(x1, y1, x2, y2, width=width)
 
     def draw_number(self):
         number = (12,) tuple(range(1, 12))
         for i in range(12):
             theta = pi/6*i
             x = 250 120*sin(theta)
             y = 250-120*cos(theta)
             self.canvas.create_text(x, y, text=str(number[i]))
 
     def draw_pointer(self):
         now = localtime()
         hour = now.tm_hour
         minute = now.tm_min
         second = now.tm_sec
         alpha = pi/30*second  # 秒针偏角
         beta = pi/1800*second minute*pi/30  # 分针偏角
         gamma = pi/21600*second pi/360*minute pi/6*hour  # 时针偏角
         for length, theta, color, width in ((60, gamma, 'red', 3), (100, beta, 'green', 2), (120, alpha, 'blue', 1)):
             x = 250 length*sin(theta)
             y = 250-length*cos(theta)
             self.pointers.append(self.canvas.create_line(250, 250, x, y, fill=color, width=width))
 
     def update(self):
         try:
             while True:
                 sleep(1)
                 for i in range(len(self.pointers)):
                     self.canvas.delete(self.pointers[i])
                 self.pointers.clear()
                 self.draw_pointer()
                 super().update()
         except TclError:
             pass
 
 
 if __name__ == '__main__':
     root = Tk()
     clock = Clock(root)
     clock.mainloop()

运行结果如图所示:

0 人点赞