第六章 平托图 (Pintographs)

原作:Keith Peters https://www.bit-101.com/blog/2022/11/coding-curves/

译者:池中物王二狗(sheldon)

blog: http://cnblogs.com/willian/

源码:github: https://github.com/willian12345/coding-curves

曲线艺术编程系列第 6 章

另一个可用于模拟绘制复杂曲线的物理装置叫平托图(Pintograph), 事实上我真的自己弄了一个。

我们将从一个视频开始--这个第一个按 Pintograph 字面意思在 Youtube 找上找到的视频

https://www.youtube.com/embed/1JyNLzdbcz4?feature=oembed

平托图也可以认为是谐波图的一种,但不像谐波图那样是基于钟摆原理,平托图通常是用电动机驱动的(尽管也有一些是手摇的)。一些圆盘连杆在电动机上,连杆又连接在圆盘上,笔又连接到连杆上。连杆上的圆盘尺寸、连杆长度、连接点位置你都可以自已改动,电机的相对速度与和偏移量可以创造出一大堆不同类型的曲线。

很长时间内我都不知道“pintograph”这个名字是怎么来的。最终发现是他创造了这个名词,事实上是他的女儿。它是从“缩放仪pantograph”演变而来,这些转动的轮子看起来像是一辆福特平托。阅读这里获取更多 http://www.fxmtech.com/harmonog.html

以防你对缩放仪(pantograph)不熟悉,我解释一下,这个装置是一般是用来复制绘图的。它有一些旋转的连杆。将一个轴点固定住,将指示器指向这些轴点中的其实一个点,另一头是固定的笔。 当你按原图移动指示器,另一头的笔就绘制出相同的形状。你可以用它复制同样的形状或者调整复制时的大小。这是维基百科对缩放仪(pantograph)的解释 https://en.wikipedia.org/wiki/Pantograph

模拟器

平托图模拟起来相当简单。它由两个旋转的转盘用各自的连杆连连接。在它的反方向上两根连杆连接在一起的点就是虚拟画笔的位置,

首先,我们得先模拟两个盘。它们拥有各自的 x, y 位置,半径,速度和相位。我们将两个盘都变成可视化以便让你知道到是如何运行的。另外它将会是一个非常长且复杂的公式。

我们将创建两个旋转的圆,且在圆周上显示那个连杆连接的点。

每个圆盘将用以下结构表示:

  1. disk: {
  2. x,
  3. y,
  4. radius,
  5. speed,
  6. phase,
  7. }

就像之前一样,我们不管它是一个通用对象、结构、类或其它什么的来表示。并且我假定你已经有一个函数用于创建返回这样的结构:

  1. d0 = disk(100, 200, 100, 2, 0.5)
  2. d1 = disk(400, 200, 60, 3, 0.0)

现在我们可以像下面这样设置一个动画:

  1. width = 600
  2. height = 400
  3. canvas = (width, height)
  4. t = 0
  5. d0 = disk(100, 200, 100, 2, 0.5)
  6. d1 = disk(400, 200, 60, 3, 0.0)
  7. function loop() {
  8. clearScreen()
  9. circle(d0.x, d0.y, d0.radius)
  10. stroke()
  11. x0 = circle.d0.x + cos(t * d0.speed + d0.phase) * d0.radius
  12. y0 = circle.d0.y + sin(t * d0.speed + d0.phase) * d0.radius
  13. circle(x0, y0, 4)
  14. fill()
  15. circle(d0.x, d0.y, d0.radius)
  16. stroke()
  17. x1 = circle.d1.x + cos(t * d1.speed + d1.phase) * d1.radius
  18. y1 = circle.d1.y + sin(t * d1.speed + d1.phase) * d1.radius
  19. circle(x1, y1, 4)
  20. fill()
  21. t += 0.1
  22. }

(译者注:注意原作者在伪代码中有个小错误,即下面 y0, y1 内也使用了 cos 来计算,我已将其改为 sin )

再一次强调一下,loop 是个假定的函数代表的是无限循环用于创建动画。我自己用代码实现了 loop 创建了一个帧动画,下面是我弄的 gif 图,但它也可以是一个实时动画:

它虽然不是那么丝滑,但不重要。我们有了两个不同大小、速度的圆。 代码本身并不复杂。我们先清空了屏幕,然后在两个圆的圆周位置上各自绘制一个圆。它用于连接连杆。这里应用基础的三角学:x = cos(angle) * radius, y = sin(angle) * radius, 角度是用 t * 速度 + 相位。 这样就得到了那两个点,我们用实心小圆代表。连接连杆并找到那根虚拟笔的位置

接下来事情变的更加数学化了。我们需要两个连接杆。每个连接杆都应该各自连接到一个旋转的点上。然后它们的另一端彼此相连。像下面草图画的那样:

两个圆盘。通过两个圆盘的半径,旋转和位置,这 p0 和 p1 也就知道了。这就是我们上面做的。 我们也可以直接定义连杆长度。它们现定义成等长, 它当然也可以不等长。 我们把它命名成 a0 和 a1.(我知道它们现在草图中看起来像 90 和 91 sorry!), 我们需要计算是的两个连杆连接点的位置。

p0 和 p1 之间的距离我们通过勾股定理很容易计算出来。我们把它命名为 d 。在图中用虚线表示。

现在我们拥有了一个三角形,每一边分别为 a0, a1 和 d。

这里有个三角法则叫“余弦定理”能帮我们。如果你知道三角形的三条边, a, b 和 c, 那么我们就可以计算得到三个角的角度。通常被写成下面这样:

  1. c = sqrt(a*a + b*b - 2*a*b*cos(y))

y 是 c 边的对角。所以如果你知道两边的长度和它们的夹角,那么你就能算出它夹角的对边长度。尽管它非常有用,但我们这里暂时用不到。

我们可以重新整理公式,让未知道的变量在一边,另一边则是可计算的公式。

我们需要知道三个角中的其中一个角用于计算虚拟笔的位置。下面是我用余弦定理公式手动计算的代数结果公式。

在我们例子中,三边在余弦定理公式中对应关系如下:

  1. a = a0
  2. b = d
  3. c = a1

应用公式后,我们就可以得到 角 p1-p0-pen 的夹角了.

我们也能用 atan2 得到 p0 到 p1 的角度。两者相关就得到了 p0 到 pen 的角度。

再次,展示下我画的草图

通过余弦定理得到 大角 p1 到 p0 到 pen 的夹角 我们把它命名为 p1_p0_pen. 通过 atan2 计算出的小的那个角命名为 p0toP1。相减后角的值,就指向虚拟笔的位置。我相信有很多种方式可以计算出这个角, 也许比我这种方式更好, 但我这种方式可以正常运行。你可以继续简化它,但我用详细的步骤用于说明,希望对你有帮助。

下面是代码部分:

  1. width = 600
  2. height = 600
  3. canvas = (width, height)
  4. t = 0
  5. d0 = disk(150, 450, 100, 2, 0.5)
  6. d1 = disk(450, 450, 60, 3, 0.0)
  7. function loop() {
  8. clearScreen()
  9. circle(d0.x, d0.y, d0.radius)
  10. stroke()
  11. x0 = d0.x + cos(t * d0.speed + d0.phase) * d0.radius
  12. y0 = d0.y + sin(t * d0.speed + d0.phase) * d0.radius
  13. // (译者注:左侧大圆圆周上的小圆)
  14. circle(x0, y0, 4)
  15. fill()
  16. // (译者注:原作者此处 circle(d0.x, d0.y, d0.radius); 书写错误 ,应为 circle(d1.x, d1.y, d1.radius))
  17. circle(d1.x, d1.y, d1.radius)
  18. stroke()
  19. x1 = d1.x + cos(t * d1.speed + d1.phase) * d1.radius
  20. y1 = d1.y + sin(t * d1.speed + d1.phase) * d1.radius
  21. circle(x1, y1, 4)
  22. fill()
  23. // 连杆长度
  24. a0 = 350
  25. a1 = 350
  26. // p0 和 p1 距离
  27. dx = x1 - x0
  28. dy = y1 - y0
  29. d = sqrt(dx * dx + dy * dy)
  30. // 找到两个关键角相减
  31. p1_p0_pen = acos((a1 * a1 - a0 * a0 - d * d) / (-2 * a0 * d))
  32. p0toP1 = atan2(y1 - y0, x1 - x0)
  33. angle = p0toP1 - p1_p0_pen
  34. // 计算得到虚拟笔的位置
  35. //(译者注:左侧大圆圆周上的小圆位置和上面的angle 计算出虚拟笔的位置)
  36. pX = x0 + cos(angle) * a0
  37. pY = y0 + sin(angle) * a0
  38. // 绘制连杆
  39. moveTo(x0, y0)
  40. lineTo(pX, pY)
  41. lineTo(x1, y1)
  42. stroke()
  43. t += 0.1
  44. }

按上面的代码正确编码后你应该可以渲染出如下的结果:

再次提醒,这只是不完整循环的 gif 图,仅用于演示如何运行。代码中每一步都有注释。希望对你有帮助。 注意我也改了 canvas 的宽高并且将圆盘位置调整到了底部腾出空间用于显示完整的连杆。

有一点需要注意,要确保连杆足够的长保证它们能彼此相连。在真实世界中,如果它们太短,可能会卡住或弄坏电动机。 在我们的模拟中,在反余弦函数计算 acos 可能会得到 NaN (not a number)错误,而你可能还摸不着头脑。我这是经验之谈。

绘制曲线

最后让我们看看画出个啥。在此,我把动画禁掉并且也不绘制圆和连杆 。我将追踪每次迭代虚拟笔的路径用于绘制一个长循环的具有利萨茹特征的曲线。

  1. width = 800
  2. height = 600
  3. canvas = (width, height)
  4. function render() {
  5. t = 0
  6. d0 = disk(250, 550, 141, 2.741, 0.5)
  7. d1 = disk(650, 550, 190, 0.793, 0.0)
  8. // the length of the arms
  9. a0 = 400
  10. a1 = 400
  11. for (i = 0; i < 50000; i++) {
  12. x0 = d0.x + cos(t * d0.speed + d0.phase) * d0.radius
  13. y0 = d0.y + sin(t * d0.speed + d0.phase) * d0.radius
  14. x1 = d1.x + cos(t * d1.speed + d1.phase) * d1.radius
  15. y1 = d1.y + sin(t * d1.speed + d1.phase) * d1.radius
  16. // get the distance between p0 and p1
  17. dx = x1 - x0
  18. dy = y1 - y0
  19. d = sqrt(dx * dx + dy * dy)
  20. // find the two key angles and subtract them
  21. p1_p0_pen = acos((a1 * a1 - a0 * a0 - d * d) / (-2 * a0 * d))
  22. p0toP1 = atan2(y1 - y0, x1 - x0)
  23. angle = p0toP1 - p1_p0_pen
  24. // find the pen point
  25. pX = x0 + cos(angle) * a0
  26. pY = y0 + sin(angle) * a0
  27. lineTo(pX, pY)
  28. t += 0.01
  29. }
  30. stroke()
  31. }

我留了足够多的空间让你自己去优化代码,少年加油!尽管代码看起来乱糟糟的,但绘制结果它应该会像下图:

这里你可以尝试修改代码中的许多东西用于体验。这就是一个超简易的平托图,所以上面的图看起来大多都有点糙。但你可以依据这些原则创建各种更复杂的设备模拟。这个网站拥有许多 demo 相信可以启发你的思路:

https://michaldudak.github.io/pintograph/demo/

一个盘叠一个盘, 旋转的平托图,三个转盘的平托图,等等。

如果你想开始尝试,你只需要在 youtube 搜索 “harmonograph”, “pintograph” and “drawing machines” 等关键字,它会提供无限的点子给你----你可以用来编码或弄个真实的设备。在我看来最有趣的那一个例子是一个转台上自身慢慢旋转在一张纸上绘制出曲线。

总结

利萨茹曲线和相关的模拟装置讨论在这里就结束了。至少暂时就这样了。下一章我们将回归到比较基础的标准几何曲线。

本章 Javascript 源码 https://github.com/willian12345/coding-curves/blob/main/examples/ch06


博客园: http://cnblogs.com/willian/

github: https://github.com/willian12345/

曲线艺术编程 coding curves 第六章 平托图 (Pintographs)的更多相关文章

  1. Python 编程快速上手 第六章总结

    第六章 字符串操作 前言 这一章节讲了关于 Python 中字符串类型的知识.与字符串有关的操作符,方法等等. 处理字符串:字符串的写入.打印.访问的知识 原始字符串 格式:r'string'作用:在 ...

  2. Python核心编程课后习题-第六章

    1. 字符串, string模块中是否有一种字符串方法或者函数可以帮我鉴定一下一个字符串是否是另一个大字符串的一部分? str1 = 'abcdefghijklmnopqrstuv' print st ...

  3. 《Java并发编程实战》第六章 任务运行 读书笔记

    一. 在线程中运行任务 无限制创建线程的不足 .线程生命周期的开销很高 .资源消耗 .稳定性 二.Executor框架 Executor基于生产者-消费者模式.提交任务的操作相当于生产者.运行任务的线 ...

  4. C语言编程入门之--第六章C语言控制语句

    导读:本章带读者理解什么是控制语句,然后逐个讲解C语言常用的控制语句,含有控制语句的代码量多起来后就要注意写代码的风格了,本章末节都是练习题,大量的练习才能掌握好控制语句的使用. 6.1 什么是控制语 ...

  5. 《Linux命令行与shell脚本编程大全》 第六章环境变量

    很多程序和脚本都通过环境变量来获取系统信息.存储临时数据和配置信息. 6.1 什么是环境变量: bash shell用一个叫环境变量(environment variable)的特性来存储有关shel ...

  6. Java编程基础篇第六章

    构造方法 一:概念: 给对象的数据(属性)进行初始化 二:特点: a.方法名与类同名(字母大小写也要一样) b.没有返回值类型 c.没有具体的返回值 return 三:构造方法重载: 方法名相同,与返 ...

  7. Java多线程编程核心技术,第六章

    1,饿汉模式/单例模式,一开始就新建一个静态变量,后面用getInstance()都是同一个变量 2,懒汉模式/单例模式,在getInstance()才会new一个对象,在第一个有了后不会继续创建 3 ...

  8. java并发编程实战:第六章----任务执行

    任务:通常是一些抽象的且离散的工作单元.大多数并发应用程序都是围绕"任务执行"来构造的,把程序的工作分给多个任务,可以简化程序的组织结构便于维护 一.在线程中执行任务 任务的独立性 ...

  9. 【java并发编程实战】第六章:线程池

    1.线程池 众所周知创建大量线程时代价是非常大的: - 线程的生命周期开销非常大:创建需要时间,导致延迟处理请求,jvm需要分配空间. - 资源消耗:线程需要占用空间,如果线程数大于可用的处理器数量, ...

  10. JavaScript DOM编程艺术-学习笔记(第五章、第六章)

    第五章: 1.题外话:首先大声疾呼,"js无罪",有罪的是滥用js的那些人.js的father 布兰登-艾克,当初为了应付工作,10天就赶出了这个js,事后还说人家js是c语言和s ...

随机推荐

  1. 《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(12)-Charles如何使用Repeat功能进行简单压力测试

    1.前言 李四:"今天好累啊,点的我手指都疼了.我一直被要求给后端接口的同事重复发送请求来调试接口." Charles:"哎呀,李四同学,你怎么能一条一条的手动发送呢 我 ...

  2. 深度学习之PyTorch实战(4)——迁移学习

    (这篇博客其实很早之前就写过了,就是自己对当前学习pytorch的一个教程学习做了一个学习笔记,一直未发现,今天整理一下,发出来与前面基础形成连载,方便初学者看,但是可能部分pytorch和torch ...

  3. 微信-JSSDK网页调用-(微信扫一扫)

    网页调用微信扫一扫接口 1.准备工作:  1.1微信浏览器 1.2微信APPID,nonceStr 2.使用方式快速预览 调用扫一扫微信接口. 1需要获取access_token 2获取 $jsapi ...

  4. panda之series结构

    eries 结构,也称 Series 序列,是 Pandas 常用的数据结构之一,它是一种类似于一维数组的结构,由一组数据值(value)和一组标签组成,其中标签与数据值之间是一一对应的关系.Seri ...

  5. python入门教程之二环境搭建

    环境搭建 1python解释器 当我们编写Python代码时,我们得到的是一个包含Python代码的以.py为扩展名的文本文件.要运行代码,就需要Python解释器去执行.py文件. 由于整个Pyth ...

  6. 四月十七日Java基础知识点

    1.默认构造方法:如果class前面有public修饰符,则默认的构造方法也会是public的.由于系统提供的默认构造方法往往不能满足需求,所以用户可以自己定义类的构造方法来满足需要,一旦用户为该类定 ...

  7. XmlSerializer 反射类型xxx时出错,反射属性xxx时出错。

    在使用XmlSerializer将类序列化成XML时出错,看到InnerException的message可以知道是这个receiver里有错误,进入这个类查看一下代码发现有重名的类 NodeId类修 ...

  8. MySQL(三)数据目录

    目录 Mysql的主要目录结构 1 数据库文件的存放路径 /var/lib/mysql/ 2 相关命令目录 /usr/bin/mysql /usr/sbin/mysql 3 配置文件目录 /usr/s ...

  9. 【Vue项目】尚品汇(六)ShopCar组件开发 购物车模块

    4 购物车 4.1 购物车商品数量控制 Detail\index.vue <div class="cartWrap"> <div class="cont ...

  10. VScode连接GPU服务器进行深度学习

    VScode连接GPU服务器进行深度学习 ​ 最近用台式机跑一些小的深度学习项目,发现越来越慢了,由于一些原因,有时候需要我进行现场作业但是我的笔记本是轻薄本(Thinkpad YYDS)不带显卡,百 ...