曲线艺术编程 coding curves 第五章 谐波图形(谐振图形) HARMONOGRAPHS
原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/
译者:池中物王二狗(sheldon)
曲线艺术编程系列第 5 章
这一篇幅建立在对第四章利萨茹曲线的讨论之上。事实上谐波图形并不是一类曲线,它是一种用于绘制(模拟)利萨茹曲线的装置。当我说一个装置时,我的意思就是真实物理世界的一个设备,它由绳子、链条、杠杆、笔、沙瓶、钟摆或其它机械结构组成用于创建这些曲线。
真谐波图形
我第一次碰到谐波图形是在波士顿的一个科技馆中。那是在我小时候众多旅行中的一次。它是一个类似钟摆,装满沙子的容器,沙子漏出来形成一条轨迹。这个视频中展示的并不完全像我当时看到的,但基本差不多。直到多年后我才知道它叫 谐波图形 harmonograph
(译者注:如果打不开视频这里我截了个图,我小时候从未见过,不知道你大伙有没有见过类似的东西,反正我是第一次见)
https://www.youtube.com/watch?v=uPbzhxYTioM
这个视频值得观看用于深入探讨利萨茹图形。 通过一个杠杆,来回摆动所花费的时间就称为周期,我们说过的 frequency 频率放到后面再讨论。在视频的4分12秒处,视频作者解释了这个钟摆装置为什么 可以拥有两个不同周期在它各自轴上。这就是为啥最后形成的图形看起来像利萨茹曲线- 因为它本来就是! 如果各自轴周期相同,则它可能会创建一个圆形,椭圆形,螺旋。 技术上讲这些仍然也是利萨茹图形的一种,但我们感兴趣的是此篇中的谐波图。
这里是另一个版本使用笔和纸
源自 https://en.wikipedia.org/wiki/Harmonograph
在这个例子中,笔不动,纸做周期摆动。但完成的是一样的事情。
这里有另一个视频与此类似的的装置,它则纸盒,线,和胶带做成!
这是纯粹的利萨茹曲线与机械的,基于摆动的谐波图形关键不同点。 钟摆慢慢失能,摆动经过的距离越来越小。直到停摆在绘制的中心点。
而这种类型的谐波图形可以产生一些有趣的图形,你甚至可以用两个钟摆制作出更复杂的。
下面是一个相关的例子:
此处,纸和笔都安装在钟摆顶部,配重在桌底摆动。所以它们自动能产生复杂的曲线。
这个视频就是一个非常魔幻的双钟摆,展示了它可以创建出惊人的图形。也演示了不少特征。
https://www.youtube.com/watch?v=_PdGcl1Ugl0&t=110s
模拟谐振
鉴于咱是一个聚焦于编程的网站,我就不解释如何建一个物理的谐振装置了。 但这些装置肯定遵循物理规则,并且物理规则的公式已被我们知晓。我们可以用这些公式直接创建虚拟的谐波图形。
我们先从单钟摆谐振版本入手,但开始之前我们先回顾一下我们的利萨茹公式:
x = A * sin(a * t + d)
y = B * sin(b * t)
A 和 B 是波在自各轴上的振幅, a 和 b 是频率。 d 用于让 x 和 y 脱离它们的相位。t 是时间参数。 最初我们说 t 会是 0 至 2 * PI 区别,但后面我们会看到它会无穷增长。
为了改动到模拟谐振, 我们要认识到各自轴它们由自己的相位, 而不是只有一个有相位,另一个...没相位? 所以我们将 d 变成两个变量 p1 和 p2。
x = A * sin(a * t + p1)
y = B * sin(b * t + p2)
这依然会是一个利萨茹曲线, 但有了多了一点点复杂的定义。 为了完全模拟谐誫, 我们先要模拟失能或者说摸拟衰减。 为了更为精确,它会以额外的乘数的形式呈现:
e-d*t
... 或者说 “e 的底的 负 d 乘以 t 次方” 指数
在代码中表现为:
pow(e, -d * t)
pow 方法在任何数学库中都会有。
所以,这都是些啥? 我们有了两个新的变量 e and d。 实现上 e 是个常量, 又名欧拉数, 约等于 2.71828。 我会让你自己去了解相关知识,但 e 确实广泛存在于各种物理公式中。当然也存在于钟摆衰减中。
到目前为止,你可能猜到 d 是那个衰减因子。 我们将 d 设为成一个相当小的值 比如 0.002 就挺不错的。 现在当 t 等于 0, 那么指数为 0, 指数函数计算结果会为 1.0 。
当 t 不断增长, 比如每个迭代增加 0.01, 指数会缓慢的负向增长。 当 t 为 0.01, 指数为 -0.00002, 指数函数结果将会衰减至 0.9999800002
100 次迭代后, t 会变为 1.0 衰减因子将会是 0.9980019987。 1000 次迭代后, 它将是 0.9801986733。 所以你可以观察到它减小的非常慢。 如果 d 增加, 那么衰减值会向0.0更快的进发。 下面展示的是应用在谐振公式内:
x = A * sin(a * t + p1) * pow(e, -d1 * t)
y = B * sin(b * t + p2) * pow(e, -d2 * t)
注意,我用了 d1 和 d2 这样你在各自轴就有它各自的振幅、频率、相位和衰减因子了。
为了让它归位,当 t 增长时上面的等式会慢慢接近 0.0, 意谓着 x 和 y 会越来越小接近 0.0, 模拟钟摆摆动至停止。 d1 和 d2 设的越大,速度就越快。
根据你自己使用的数学函数库, 也许它提供了简写。 自由需将 e 进行指数计算成为一个普遍的操作, 通常会提供一个内置函数一般叫 exp 。比如 Javascript , 你可以直接用 Math.exp(-d1 * t)
, 这和用 Math.pow(Math.E, -d1 * t)
相同,但更简短,也许更加高效。
于是我们的伪代码可以写成这样:
x = A * sin(a * t + p1) * exp(-d1 * t)
y = B * sin(b * t + p2) * exp(-d2 * t)
函数
走起!我们的函数将会是下面这个样子:
function harmonograph(cx, cy, A, B, a, b, p1, p2, d1, d2, iter) {
res = 0.01
t = 0.0
for (i = 0; t < iter; i += res) {
x = cx + sin(a * t + p1) * A * exp(-d1 * t)
y = cy + sin(b * t + p2) * B * exp(-d2 * t)
lineTo(x, y)
t += res
}
stroke()
}
现在有了一大堆参数。但你应该已经知道了它们中的大部分。我首先要介绍变量 iter. 之前我们循环一直是从 0 到 2 * PI 。 现在我们期望更多, 随着曲线的持续改变和运动范围的减小。 我们设一个非常大的值给 iter 用于模拟谐振长时间运行。在现实现世中单个谐波图绘制会花费5分钟以上。
下面是调用:
width = 800
height = 800
canvas(width, height)
A = 390
B = 390
a = 2.0
b = 2.01
p1 = 0.3
p2 = 1.7
d1 = 0.001
d2 = 0.001
iter = 100000
harmonograph(width / 2, height / 2, A, B, a, b, p1, p2, d1, d2, iter)
是的... 10 万次迭代。 也许会花个 1 到 2 秒,但你应该可以看到类似下面的结果:
(译者注:注意 这个 10 万次的迭代在我的 firefox 浏览器中感觉花了10多秒才画完,绘制过程浏览器会出现假死现象, webkit 内核的浏览器上完全显示不出来,所以需要减小 iter 值)
下面是尝试使用随机参数产生的结果:
我发现最好将 a 和 b 设为非常相近的值, 让它们变化量很小, 比如像最上面的例子我设它们为 2.0 和 2.01。 如果将值设为相关的简单比例值也很好,比如 7.5 和 2.5 它们的比值是 3 : 1。你再将其中的某个值改动一丢丢的量,它将会变的更有趣, 比如 7.5 和 2.501。 但如果是完全随机的值如 5.7 和 3.2 这会产生相当狂野的图形。
d 值决定钟摆衰减的快慢,所以较小的值将会在距离中心处产生更多的线条。 下面是将上面例子中自各衰减值设为 0.0003 后的结果:
这是衰减值设为 0.003 的结果:
钟摆衰退的很快且离中心点近的位置画了更多线条。
你可以好好尝试一下,比如给它加点颜色!
双钟摆
最后一个视频中绘制产生的图形非常有吸引力。为了实现它需要实现双钟摆模拟。你可以认为我们有一支笔给它 x,y 轴钟摆, 还有另一张纸也拥有 x, y 轴钟摆。 两者都单独运动,最终创造出复杂的曲线。你只需要计出各两个 x 轴的钟摆值加在一起给 x ,y 轴也同样做。
尽管概念上相对直白,但意味着我们需要传双倍的参数。每个 振幅,频率,相位,衰减 都需要传两遍。如果我们真这么傻白甜的做了,它可能会是像下面这样的代码实现那是相当难维护了。
// 别这么干 !!!
function harmonograph2(cx, cy, a1, a2, a3, a4, f1, f2, f3, f4, p1, p2, p3, p4, d1, d2, d3, d4, iter) {
res = 0.01
t = 0.0
for (i = 0; t < iter; i += res) {
x = cx + sin(f1 * t + p1) * a1 * exp(-d1 * t) + sin(f2 * t + p2) * a2 * exp(-d2 * t)
y = cy + sin(f3 * t + p3) * a3 * exp(-d3 * t)+ sin(f4 * t + p4) * a4 * exp(-d4 * t)
lineTo(x, y)
t += res
}
stroke()
}
我试过这样做把我整懵了根本记不住哪个变量控制哪个轴的钟摆频率,参数太高不清。 更好的做法(可能是不是最好的)是将它们安装单轴(振幅,频率,相位,衰减)的钟摆需求封装成参数形式到一个对象内,然后传给函数。
我不知道你使用的平台语言用的是类或者结构或仅仅是纯粹的普通通用对象,所以你只需要知道我们有这样个拥有4个参数的对象用于传参:
pendulum: {
amp,
freq,
phase,
damp,
}
这里别担心语法,用你所在平台的语法实现这样一个对象即可。
现在我们可以创建四个对象,它们可能命名为 penX,penY,paperX,paperY。 像这样:
penX = pendulum(90.0, 7.5, 1.57, 0.0001)
penY = pendulum(90.0, 4.0, 0.0, 0.0001)
paperX = pendulum(280.0, 1.001, 1.57, 0.0001)
paperY = pendulum(280.0, 2.0, 0.0, 0.0001)
再次提醒,别关心语法。为每个钟摆周期你需要创建一个工厂函数或一个构造函数或者类似的一个包含振幅,频率,相位,衰减对象字面量。
现在我们可以将 harmonograph2 函数改成下面这样:
function harmonograph2(cx, cy, penX, penY, papX, papY, iter) {
res = 0.01
t = 0.0
for (i = 0; t < iter; i += res) {
x = cx
+ sin(penX.freq * t + penX.phase) * penX.amp * exp(-penX.damp * t)
+ sin(papX.freq * t + papX.phase) * papX.amp * exp(-papX.damp * t)
y = cy
+ sin(penY.freq * t + penY.phase) * penY.amp * exp(-penY.damp * t)
+ sin(papY.freq * t + papY.phase) * papY.amp * exp(-papY.damp * t)
lineTo(x, y)
t += res
}
stroke()
}
这里仍然有些重复的代码,但这是个好的开始。我尽可能保证它的可读性。你当然可以将它写的更简洁,但这常常是在复杂编程中有趣的部分先概念验证再转为优雅的代码实现。我并不想在这上面深入太多。你可以自己去优化。
penX = pendulum(90.0, 7.5, 1.57, 0.0001)
penY = pendulum(90.0, 4.0, 0.0, 0.0001)
paperX = pendulum(280.0, 1.001, 1.57, 0.0001)
paperY = pendulum(280.0, 2.0, 0.0, 0.0001)
harmonograph2(width/2, height/2, penX, penY, paperX, paperY, 100000)
如果你做的没错(且我上面的代码也没写错的)的话,你应该会得到以下这个图形:
很接近了是不是?参数值并没用啥魔法。为了让图形看起来很酷,我只是随意改动测试了一下参数值。下面是更多应用别的参数值后的结果:
penX = pendulum(50.0, 17.5, 1.57, 0.0001)
penY = pendulum(50.0, 11.0, 0.5, 0.0001)
paperX = pendulum(280.0, 0.50, 1.57, 0.0007)
paperY = pendulum(280.0, 1.50, 0.0, 0.0007)
你可以多式式,它将产生无穷无尽的图形。
动画
到目前为止我们实现的都是静态图,是时候实现动画了。你可以动画实现绘制过程就像真实世界绘制的过程一样。我觉得不太有意思 ,就直接跳过这一步了。
对其它属性进行动画有趣的多。相位这个选项就不错。这里就是以相位值从 0 到 2 * PI 动态变动的一个例子。它看起来几乎成三维的了。
(译者注:原 gif 图太大42.6M ,我压缩了下 _! )
还有下面,一些衰减值在 0.001 到 0.0001 来回变动。
(译者注:gif 图太大 33.8 M ,我压缩了下 _! )
总结
这就是谐波图了。试着用程序实现它吧。你可以玩一整天。你甚至可以买些设备实现一个绘制谐波图的设备。我很期待看到你这么做。
下一章我们将聚集到另一个绘制曲线的物理设备并把它模拟出来。
本章 Javascript 源码 https://github.com/willian12345/coding-curves/tree/main/examples/ch05
博客园: http://cnblogs.com/willian/
github: https://github.com/willian12345/
曲线艺术编程 coding curves 第五章 谐波图形(谐振图形) HARMONOGRAPHS的更多相关文章
- JavaScript DOM编程艺术-学习笔记(第五章、第六章)
第五章: 1.题外话:首先大声疾呼,"js无罪",有罪的是滥用js的那些人.js的father 布兰登-艾克,当初为了应付工作,10天就赶出了这个js,事后还说人家js是c语言和s ...
- Python 编程快速上手 第五章总结
第五章 字典和结构化数据 创建数组 格式:myCat = {'size':'fat','color':'gray',disposition':'loud'} 对字典的操作 通过[ ] 访问字典的值 [ ...
- java并发编程实战:第五章----基础构建模块
委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可. 一.同步容器类 1.同步容器类的问题 同步容器类都是线程安全的,容器本身内置的复合操作能够保证原子性,但是当在其上进 ...
- Python 核心编程 课后习题 第五章
2. 操作符. (a) 写一个函数, 计算并返回两个数的乘积. (b) 写一段代码调用这个函数, 并显示它的结果. def multi(a,b): return a * b result = mult ...
- 《Java并发编程实战》第五章 同步容器类 读书笔记
一.同步容器类 1. 同步容器类的问题 线程容器类都是线程安全的.可是当在其上进行符合操作则须要而外加锁保护其安全性. 常见符合操作包括: . 迭代 . 跳转(依据指定顺序找到当前元素的下一个元素) ...
- C语言编程入门之--第五章C语言基本运算和表达式-part1
导读:程序要完成高级功能,首先要能够做到基本的加减乘除.本章从程序中变量的概念开始,结合之前学的输出函数和新介绍的输入函数制作简单人机交互程序,然后讲解最基础的加减法运算,自制简单计算器程序练手. 5 ...
- C语言编程入门之--第五章C语言基本运算和表达式-part2
5.1.4 再来一个C库函数getchar吸收回车键 回车键也是一个字符,在使用scanf的时候,输入完毕要按下回车键,这时候回车键也会被输入到stdin流中,会搞乱我们的程序. 注意:stdin是输 ...
- C语言编程入门之--第五章C语言基本运算和表达式-part3
5.3 挑几个运算符来讲 常用的运算符除了加减乘除(+-*/)外,还有如下: 注意:以下运算符之间用逗号隔开,C语言中也有逗号运算符,这里不讲逗号运算符. 1. 赋值运算符 =,+=,*= 2. 一 ...
- C语言编程入门之--第五章C语言基本运算和表达式-part4
5.3.5 和二进制极为密切的运算符 本小节的运算符需要借助二进制概念来理解. 二进制数据中,比如一个字节的数据,它的十进制为228,二进制就为11100100,如图5.11, 注意:如果不懂怎么转换 ...
- C#高级编程学习一-----------------第五章泛型
三层架构之泛型应用 概述 1.命名约定 泛型类型以T开头或就是T. 2.泛型类 2.1.创建泛型类
随机推荐
- Redis Cluster集群搭建及节点的添加、删除
系统性学习,移步IT-BLOG 一.什么是 Redis Cluster Redis 是在内存中保存数据的,而我们的电脑一般内存都不大,这也意味着 Redis 不适合存储大数据,适合存储大数据的是 Ha ...
- 面对AI的兴起,从人类发展到个人发展,普通人应当如何抉择?
这一周被各种 AI 卷的不行,从 ChatGPT 4.0 上线到百度文心一言发布会,再到微软的 Microsoft 365 Copilot. 网上有很多人.公众号吐嘈百度,而晓衡接触到的圈子还有一些不 ...
- 五月十二号java基础知识点
1.注解是代码中特殊标记,作用是告知编译器做什么事2.反射允许程序在运行状态时,对任意一个字节码获取它所有信息3.内部类是定义在类中的嵌套类4.匿名内部类是定义在类的同时创建该类的一个对象5.lamb ...
- MySQL(二)字符集、比较规则与规范
1 字符集的相关操作 MySQL8.0之前的版本,默认字符集为latin1,8.0及之后默认为utfmb3.utfmb4,如果以前的版本忘记修改默认的密码,就会出现乱码的问题. 1.1 修改步骤 修改 ...
- 香,一套逻辑轻松且智能解决PyQt中控件数值验证的问题
在PyQt开发中,时常需要对控件的值进行校验,如需要校验QCheckBox是否被选中,QLabel是否校验值是否为空等等.在复杂的业务场景下,这类控件如果数量很多,逐个校验就显得麻烦,需要一一获得控件 ...
- Java中方法的定义及注意事项
一.方法 什么是方法: 方法(method)是程序中最小的执行单元 实际开发中,什么时候用到方法: 重复的代码.具有独立功能的代码可以抽取到方法中 实际开发中,方法有什么好处: 可以提高代码的复用性 ...
- 如何通过C#/VB.NET 代码调整PDF文档的页边距
PDF边距是页面主要内容区域和页面边缘之间的距离.与Word页边距不同,PDF文档的页边距很难更改.因为Adobe没有提供操作页边距的直接方法.但是,您可以通过缩放页面内容来改变页边距.本文将介绍如何 ...
- 《流畅的Python》第二版上市了,值得入手么?
<Fluent Python>第一版在 2015 年出版,简体中文版<流畅的Python>在 2017 年出版.从那时起,它就成为了所有 Python 程序员的必读之书.如果一 ...
- 搭建一个简易框架 3秒创建一个WebApi接口
前端ajax请求数据,传递的参数都是一个json字符串,经过多次解析发现其实都是一个DataSet {"selectA1":[{"Name":"156 ...
- Prism Sample 19-NavigationParticipation
Navigation Participation,不知翻译方法,意思是对导航过程的参与,触发事件,类似离开导航目标和进入导航的回调 在VM中,增加一个接口 ,然后实现导航事件 public class ...