Canvas + Gsap 实现利萨如曲线

Implement Lissajous curve

数学上,利萨茹(Lissajous)曲线(又称利萨茹图形、李萨如图形或鲍迪奇(Bowditch)曲线)是两个沿着互相垂直方向的正弦振动的合成的轨迹。

效果预览:

peterroe.github.io/lissajous-c...

目标

我们不做过多的数学深究,我们只探寻在不同周期的正弦曲线水平和垂直叠加的图案

下面的每一个图案就是由从左到右,从上到下,周期相隔 1.6 s 的正弦曲线两两合成的。

  1. 绘制时间最短的就是左上角的圆,LCM(1.6,1.6) = 1.6s
  2. 绘制时间最长的是第七行第六列的曲线,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,还是像 PixiJsThreeJs 等强大绘制库,配合起来都非常好用顺手

最后,源码开源在了:github.com/peterroe/li...

觉得不错可以 Star 支持

相关推荐
newxtc36 分钟前
【爱给网-注册安全分析报告-无验证方式导致安全隐患】
前端·chrome·windows·安全·媒体
一个很帅的帅哥1 小时前
axios(基于Promise的HTTP客户端) 与 `async` 和 `await` 结合使用
javascript·网络·网络协议·http·async·promise·await
dream_ready2 小时前
linux安装nginx+前端部署vue项目(实际测试react项目也可以)
前端·javascript·vue.js·nginx·react·html5
编写美好前程2 小时前
ruoyi-vue若依前端是如何防止接口重复请求
前端·javascript·vue.js
flytam2 小时前
ES5 在 Web 上的现状
前端·javascript
喵喵酱仔__2 小时前
阻止冒泡事件
前端·javascript·vue.js
GISer_Jing2 小时前
前端面试CSS常见题目
前端·css·面试
某公司摸鱼前端2 小时前
如何关闭前端Chrome的debugger反调试
javascript·chrome
八了个戒2 小时前
【TypeScript入坑】什么是TypeScript?
开发语言·前端·javascript·面试·typescript
不悔哥2 小时前
vue 案例使用
前端·javascript·vue.js