Implement Lissajous curve
数学上,利萨茹(Lissajous)曲线(又称利萨茹图形、李萨如图形或鲍迪奇(Bowditch)曲线)是两个沿着互相垂直方向的正弦振动的合成的轨迹。
效果预览:
peterroe.github.io/lissajous-c...
目标
我们不做过多的数学深究,我们只探寻在不同周期的正弦曲线水平和垂直叠加的图案
下面的每一个图案就是由从左到右,从上到下,周期相隔 1.6 s
的正弦曲线两两合成的。
- 绘制时间最短的就是左上角的圆,
LCM(1.6,1.6) = 1.6s
- 绘制时间最长的是第七行第六列的曲线,
LCM(11.2,9.2) = 67.2s
所用技术
Canvas 是 HTML5 中的一个元素,它提供了一种使用 JavaScript 和 HTML 绘制图形的方法,可以通过 JavaScript 来动态绘制图形等。它是一个类似画板的区域,可以用来渲染图像,创建动画等。
我们需要用 Canvas 进行轨迹的绘制。
GSAP(GreenSock Animation Platform)是一个用于创建高性能、流畅动画的JavaScript库。它是一个强大的动画引擎,支持在Web开发中创建各种动画效果,包括但不限于轨迹动画、缓动效果、逐帧动画等
我们需要用 GSAP 得到轨迹点的补充动画
得到轨迹点
在 Canvas 中的坐标是 x 轴是向右的,y 轴是向下的。我们设置初始点的坐标为 (0, 1)
gsap.timeline 用于创建一个时间线,我们给出一个圆的剩余三个关键点的坐标,指挥 gsap 应如何过渡这些数据
ts
const coordinate = { // 初始点的坐标
x: 0,
y: 1
}
gsap.timeline({
defaults: { // 设置从一个关键点过渡到另一个关键点的时间都是 0.4 s,这里统一定义
duration: 0.4,
}
}).to(coordinate, { x: 1, ease: 'sine.in' })
.to(coordinate, { x: 2, ease: 'sine.out' })
.to(coordinate, { x: 1, ease: 'sine.in' })
.to(coordinate, { x: 0, ease: 'sine.out' })
.repeat(-1) // 循环绘制
gsap.timeline({
defaults: {
duration: 0.4,
}
}).to(coordinate, { y: 2, ease: 'sine.out' })
.to(coordinate, { y: 1, ease: 'sine.in' })
.to(coordinate, { y: 0, ease: 'sine.out' })
.to(coordinate, { y: 1, ease: 'sine.in' })
.repeat(-1)
设置 ease 也是关键的一步, gsap 内置了 sin(e) 的变化曲线。默认的曲线是 line,所以如果不设置的话,画出来的会是一个绕中心旋转了 45 度的正方形(菱形)
然后 gsap 会不断地改变 coordinate 的数据。我们就得到了轨迹点的补充动画
画圆
有了不断变化的轨迹点的数据,在 Canvas 上画轨迹就轻而易举了
ts
const canvas = document.querySelector("canvas")
const ctx = canvas.value.getContext('2d')
const draw = (ctx: CanvasRenderingContext2D) => {
const left = Math.round(coordinate.x * 40) // 乘 40 放大轨迹
const top = Math.round(coordinate.y * 40)
ctx.beginPath();
// 通过不断绘制小圆点的形式绘制出轨迹
ctx.arc(left + 110 * i + 30, top + 110 * j + 30, 1.5, 0, 2 * Math.PI)
ctx.fillStyle = '#000000';
ctx.fill(); // 填充颜色
requestAnimationFrame(() => draw(ctx))
}
draw(ctx)
我们就能看到通过轨迹绘制出一个圆环的效果,这很棒,接下来我们将更近一步
不同周期的轨迹点
我们需要得到不同周期的轨迹点的变化,显然我们需要一个数组来储存记录,这很简单:
ts
let arr = new Array(7).fill(0).map(it => ({
x: 0,
y: 1,
}))
再让 gsap 过渡他们,给他们不同的周期(duration)
ts
arr.forEach((it, i) => {
gsap.timeline({
defaults: {
duration: 0.4 * (i + 1), // 不同的周期
}
}).to(it, { x: 1, ease: 'sine.in' })
.to(it, { x: 2, ease: 'sine.out' })
.to(it, { x: 1, ease: 'sine.in' })
.to(it, { x: 0, ease: 'sine.out' })
.repeat(-1)
gsap.timeline({
defaults: {
duration: 0.4 * (i + 1),
}
}).to(it, { y: 2, ease: 'sine.out' })
.to(it, { y: 1, ease: 'sine.in' })
.to(it, { y: 0, ease: 'sine.out' })
.to(it, { y: 1, ease: 'sine.in' })
.repeat(-1)
})
接下来, Canvas 把所有的这些点进行绘制,一些说明:
110 * i / 110 * j
是为了让不同的轨迹相对画布偏移,不要重叠在一起30
则是为了让绘制的轨迹稍微远离画布边缘,不重要
ts
const draw = (ctx: CanvasRenderingContext2D) => {
for(let i = 0; i < 7; i++) {
const x = Math.round(arr[i].x * 40)
const y = Math.round(arr[j].y * 40)
ctx.beginPath();
ctx.arc(x + 110 * i + 30, y + 110 * j + 30, 1.5, 0, 2 * Math.PI)
ctx.fillStyle = '#000000';
ctx.fill(); // 填充颜色
}
requestAnimationFrame(() => draw(ctx))
}
这样,我们看到的效果就和最开始展示的例子相差不多了
添加颜色
我选取了红橙黄绿蓝靛紫正好七种颜色,来绘制不同坐标的颜色
ts
const colors = ['#ec6866', '#f2873d', '#f3c430', '#52d67a', '#609bf4', '#7a82f1', '#b67cf5']
而且为了让我们的颜色也有合成的效果,更好地配合我们的正弦合成,让 ChatGPT 帮我们写一个 HEX 颜色合成的方法:
ts
function blendColors(color1, color2) {
// 将 HEX 转换为 RGB
const rgb1 = parseInt(color1.slice(1), 16);
const r1 = (rgb1 >> 16) & 255;
const g1 = (rgb1 >> 8) & 255;
const b1 = rgb1 & 255;
const rgb2 = parseInt(color2.slice(1), 16);
const r2 = (rgb2 >> 16) & 255;
const g2 = (rgb2 >> 8) & 255;
const b2 = rgb2 & 255;
// 计算平均值
const blendedR = Math.round((r1 + r2) / 2);
const blendedG = Math.round((g1 + g2) / 2);
const blendedB = Math.round((b1 + b2) / 2);
// 将 RGB 转换回 HEX
const blendedColor = "#" + ((1 << 24) + (blendedR << 16) + (blendedG << 8) + blendedB).toString(16).slice(1);
return blendedColor;
}
然后,修改我们的 Canvas 绘制过程:
diff
const draw = (ctx: CanvasRenderingContext2D) => {
for(let i = 0; i < 7; i++) {
const x = Math.round(arr[i].x * 40)
const y = Math.round(arr[j].y * 40)
ctx.beginPath();
ctx.arc(x + 110 * i + 30, y + 110 * j + 30, 1.5, 0, 2 * Math.PI)
- ctx.fillStyle = '#000000';
+ ctx.fillStyle = blendColors(colors[i], colors[j]);
ctx.fill(); // 填充颜色
}
requestAnimationFrame(() => draw(ctx))
}
总结
gsap 虽然常常用于 DOM 元素的过渡,例如
ts
import gsap from "gsap";
gsap.to(".box", {
duration: 2,
x: 200,
rotation: 360,
});
但是它也可以脱离 DOM 元素创建补间动画,直接去过渡非 DOM 的 普通 JS 对象。
这就给了它配合其他库一起使用的能力,不管是上面的原生 Canvas,还是像 PixiJs 、ThreeJs 等强大绘制库,配合起来都非常好用顺手
最后,源码开源在了:github.com/peterroe/li...
觉得不错可以 Star 支持