绘制一个时钟
目标
本文的目标是制作一个时钟,每秒钟时针和分针都会进行非线性运动,并且能够持续循环,这也可以算是一种仿真。
具体的运动如下:
- 时针每 1 秒为周期旋转 45°,并且每个周期内的速度非线性,先快后慢;
- 分针每 1 秒为周期旋转 360°,同样以非线性速度运动。
绘制时钟
时针和分针
如何画指针
以绘制时针为例,我们可以通过绘制一个长方形来实现。首先,将时针长方形的长度设置为参数 len_hour
;由于时针和分针的宽度相同,都设置为参数 radius
。通过添加一个 polygon 对象,我们可以绘制一个长方形。
local hand_minute = scene.addobj("polygon", {
vertices = {-radius, 0, 0, -radius, len_minute, 0, radius, len_minute, 0, radius, 0, 0},
size = 0,
color = 'white'
})
分针同理,只需将 len_hour
替换为 len_minute
即可。
local hand_hour = scene.addobj("polygon", {
vertices = {-radius, 0, 0, -radius, len_hour, 0, radius, len_hour, 0, radius, 0, 0},
size = 0,
color = 'white'
})
背景板和转轴
时钟的背景板和时针的转轴都是圆形,而 MicroCity 中没有提供绘制圆形的函数。但是我们可以通过绘制多边形来模拟圆形。
-- circle
function get_circle_vertices(radius)
local vertices_circle = {}
local segment = 360
local radius_step = math.pi * 2 / segment
print()
for i = 1, segment do
local k = 3 * (i - 1)
vertices_circle[k + 1], vertices_circle[k + 2], vertices_circle[k + 3] = radius * math.cos(i * radius_step),
radius * math.sin(i * radius_step), 0
end
return vertices_circle
end
这个函数的思路是,将圆分成指定的份数(通过参数 segment
设置),然后通过三角函数计算出每个点的坐标。这样就可以得到一个近似圆形的多边形。
然后我们可以通过这个函数绘制背景板和转轴,转轴已经通过 radius
参数控制其半径大小;背景板通过 radius_bg
参数控制大小。
-- draw background circle
local bg_circle = scene.addobj("polygon", {
vertices = get_circle_vertices(radius_bg),
size = 0,
color = '#fec300'
})
bg_circle:setpos(0, 0, -1)
-- draw pointer core circle
local core_circle = scene.addobj("polygon", {
vertices = get_circle_vertices(radius),
size = 0,
color = 'white'
})
让指针运动
运动函数
我们首先确定时针的运动函数。为了实现时针的非线性运动,我们可以使用三角函数。通过适当的函数变换,我们可以使得时针在每个周期的开始和结束时速度为 0,而在周期中间时速度逐渐加快然后再减慢。
这个函数的基本表达式为:
而分针每个周期的运动速度正好是时针的 8 倍,因此分针的运动函数只需在时针运动函数的基础上修改三角函数的参数即可。
函数转换
如果 x 表示为时针真实走过的弧度,将上面的运动函数积分可以得到时针 x-t 关系的函数:
同理,分针的运动函数为:
设置运动
在 MicroCity 中,通过 setrot()
函数设置时针的旋转角度。由于旋转方向是顺时针,而正方向为逆时针,因此需要对角度进行取反。
得到时针和分针的弧度位置代码如下:
function x_hour(x)
return -(x - math.sin(x * 8) / 8)
end
function x_minute(x)
return -(x - math.sin(x))
end
根据上图可知,时针和分针的周期分别为 和 。为了与我们构建的目标相对应,我们需要将时间周期(1s)分别缩放为 和 ,实现与目标周期相对应。
可以在具体调用时进行调整(方便适应未来修改为不同的周期):
local speed = math.pi * 2
-- set rotation
hand_hour:setrot(0, 0, x_hour(t / 8 * speed))
hand_minute:setrot(0, 0, x_minute(t * speed))
与现实时间对应
现在时针和分针已经可以按照给定的输入实现非线性的速度运动了,但是这个运动并没有与现实时间对应起来。
我们可以通过 os.clock()
函数获取当前的时间,并通过记录这个值计算出每次调用这个函数的时间差,以此令仿真时间 t
与真实世界时间相对应。
因此,循环部分可以这样写:
local t = 0
local last_time = os.clock()
while scene.render() do
-- align with real world time
t = t + (os.clock() - last_time)
last_time = os.clock()
-- set rotation
hand_hour:setrot(0, 0, x_hour(t / 8 * speed))
hand_minute:setrot(0, 0, x_minute(t * speed))
print()
print('t =', math.floor(t))
end
如果想要检验时针与分针是否真的在1秒时完成一次周期,可以在循环最后通过如下代码检验:
-- 检验1圈为2*pi
if t>=1 then
break
end
效果和代码
效果
最终实现的仿真效果如下:
代码
scene.setenv({
camtype = 'ortho'
})
-- definitions
local radius = 1
local len_minute = 8
local len_hour = 4
local radius_bg = 12
-- shapes
local hand_minute = scene.addobj("polygon", {
vertices = {-radius, 0, 0, -radius, len_minute, 0, radius, len_minute, 0, radius, 0, 0},
size = 0,
color = 'white'
})
local hand_hour = scene.addobj("polygon", {
vertices = {-radius, 0, 0, -radius, len_hour, 0, radius, len_hour, 0, radius, 0, 0},
size = 0,
color = 'white'
})
-- circle
function get_circle_vertices(radius)
local vertices_circle = {}
local segment = 360
local radius_step = math.pi * 2 / segment
print()
for i = 1, segment do
local k = 3 * (i - 1)
vertices_circle[k + 1], vertices_circle[k + 2], vertices_circle[k + 3] = radius * math.cos(i * radius_step),
radius * math.sin(i * radius_step), 0
end
return vertices_circle
end
-- draw background circle
local bg_circle = scene.addobj("polygon", {
vertices = get_circle_vertices(radius_bg),
size = 0,
color = '#fec300'
})
bg_circle:setpos(0, 0, -1)
-- draw pointer core circle
local core_circle = scene.addobj("polygon", {
vertices = get_circle_vertices(radius),
size = 0,
color = 'white'
})
function x_hour(x)
return -(x - math.sin(x * 8) / 8)
end
function x_minute(x)
return -(x - math.sin(x))
end
local t = 0
local speed = math.pi * 2
local last_time = os.clock()
while scene.render() do
-- align with real world time
t = t + (os.clock() - last_time)
last_time = os.clock()
-- set rotation
hand_hour:setrot(0, 0, x_hour(t / 8 * speed))
hand_minute:setrot(0, 0, x_minute(t * speed))
print()
print('t=', math.floor(t))
-- 检验1圈为2*pi
-- if t>=1 then
-- break
-- end
end
scene.render()