Skip to content

鼠标交互的CANVAS动画壁纸

1681字约6分钟

webgelcanvas

2022-08-23

介绍一个自己摸鱼时间摸出来的一个canvas 2.5D风格方块交互动画的实现方法。

<div class="content">
  <canvas 
      id="my3dCanvas" 
      width="750" 
      height="380">
  </canvas>
</div>
    .content{
        display: flex;
        width:750px;height:380px;
        align-items: center;
        justify-content: center;
    }
    .vp-code-demo-display{
        max-height:600px!important;
    }
     // 常数设置
    const RADIX3 = 1.73205 // 更号3常数
    const UNITLONG = 15 // 单个方块大小
    const TOPCOLOR1 = '#96eaea' // 方块配色
    const TOPCOLOR2 = '#c9f1f1'
    const LEFTCOLOR1 = '#16c2cd'
    const LEFTCOLOR2 = '#1c878e'
    const RIGHTCOLOR1 = '#2edad4'
    const RIGHTCOLOR2 = '#08beb8'
    const ROW = 15 // 渲染行数
    const COL = 20 // 渲染列数
    const GRAVITY = -10 // 重力加速度
    const POWER = 15 // 升力加速度

    var my3dCanvas = document.getElementById("my3dCanvas")
    var ctx = my3dCanvas.getContext("2d")
    var height = [] // 高度储存器
    var speed = [] // 运动速度储存器
    var touch = { x: 0, y: 0, work: false } // 被触发位置储存器

    initParam()
    var itv = setInterval(() => draw(), 50) // 计时器 0.05秒渲染一次 20帧

    function initParam() { // 初始化运动状态储存器
        for (let y = 0; y < ROW; y++) {
            height[y] = []
            speed[y] = []
            for (let x = 0; x < COL; x++) {
                height[y][x] = 0
                speed[y][x] = 0
            }
        }
    }
    function draw() { // 渲染
        calStatus() // 运算实时状态
        for(let y = 0; y < ROW; y++){
            for (let x = 0; x < COL; x++) {
                if (y/2 === Math.floor(y / 2)){
                    drawBox(x * UNITLONG * RADIX3 * 2, y * UNITLONG * 3, height[y][x])
                } else {
                    drawBox(x * UNITLONG * RADIX3 * 2 - UNITLONG * RADIX3, y * UNITLONG * 3, height[y][x])
                }
            }
        }
    }
    function calStatus() { // 计算瞬时状态
        for (let y = 0; y < ROW; y++) {
            for (let x = 0; x < COL; x++) {
                if (x === touch.x && y === touch.y && touch.work) {
                    speed[y][x] += POWER
                    touch.work = false
                } else {
                    speed[y][x] = height[y][x] === 0 ? 0 : speed[y][x] + GRAVITY
                }
                height[y][x] = height[y][x] + speed[y][x] < 0 ? 0 : height[y][x] + speed[y][x]
            }
        }            
    }


    my3dCanvas.onmousemove = e => { // 监听鼠标在画布上移动
       let pt = getPointOnCanvas(my3dCanvas, e.clientX, e.clientY)
       touch.work = true
       touch.y = Math.floor(pt.y / (3 * UNITLONG))
       if (pt.y/2 === Math.floor(pt.y / 2)) {
           touch.x = Math.floor(pt.x / (2 * RADIX3 * UNITLONG))
       } else {
           touch.x = Math.floor((pt.x + UNITLONG * RADIX3) / (2 * RADIX3 * UNITLONG))
       }
    }

    function drawBox(x, y, h) { // 画整个方体
        drawTop(x, y, h)
        drawLeft(x, y, h)
        drawRight(x, y, h)
    }
    function drawTop(x, y, h) { // 画方体顶部
        ctx.beginPath()
        let grd = ctx.createLinearGradient(x, y - h - UNITLONG, x, y - h + UNITLONG);
        grd.addColorStop(0, TOPCOLOR1)
        grd.addColorStop(1, TOPCOLOR2)
        ctx.fillStyle = grd
        ctx.strokeStyle = grd
        ctx.moveTo(x, y - h)
        ctx.lineTo(x + RADIX3 * UNITLONG, y - UNITLONG - h)
        ctx.lineTo(x + 2 * RADIX3 * UNITLONG, y - h)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
        ctx.stroke()
        ctx.fill()
        ctx.closePath()
    }

    function drawLeft(x, y, h) { // 画方体左侧
        ctx.beginPath()
        let grd = ctx.createLinearGradient(x, y, x - UNITLONG, y + 2 * UNITLONG);
        grd.addColorStop(0, LEFTCOLOR1)
        grd.addColorStop(1, LEFTCOLOR2)
        ctx.fillStyle = grd
        ctx.strokeStyle = grd
        ctx.moveTo(x, y - h)
        ctx.lineTo(x, y + UNITLONG * 2)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
        ctx.stroke()
        ctx.fill()
        ctx.closePath()
    }

    function drawRight(x, y, h) { // 画方体右侧
        ctx.beginPath()
        let grd = ctx.createLinearGradient(x + RADIX3 * UNITLONG, y + UNITLONG, x + RADIX3 * UNITLONG + UNITLONG, y
        + UNITLONG * 3);
        grd.addColorStop(0, RIGHTCOLOR1)
        grd.addColorStop(1, RIGHTCOLOR2)
        ctx.fillStyle = grd
        ctx.strokeStyle = grd
        ctx.moveTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
        ctx.lineTo(x + RADIX3 * UNITLONG * 2, y + UNITLONG * 2)
        ctx.lineTo(x + RADIX3 * UNITLONG * 2, y - h)
        ctx.stroke()
        ctx.fill()
        ctx.closePath()
    }


    function getPointOnCanvas(canvas, x, y) { // 窗口坐标转画布坐标
        const bbox = canvas.getBoundingClientRect();
        return {
            x: x - bbox.left * (canvas.width / bbox.width),
            y: y - bbox.top * (canvas.height / bbox.height)
        }
    }

实现

虽然画的是3d效果,但还是通过渲染绘制二维多边形去实现的。基本的核心思想是通过一个二维数组去维护小方块矩阵的位置运动状态。而每个小方块都会根据自身的当前状态,用三个平行四边形拼接出立方体的效果,这样是不是听起来就很容易了。

定义一些常量和存储器

一些常量参数在代码中手动修改后运行,运行起来的动画效果也完全不一样哦。

     // 常数设置
    const RADIX3 = 1.73205 // 更号3常数
    const UNITLONG = 20 // 单个方块大小
    const TOPCOLOR1 = '#96eaea' // 方块配色
    const TOPCOLOR2 = '#c9f1f1'
    const LEFTCOLOR1 = '#16c2cd'
    const LEFTCOLOR2 = '#1c878e'
    const RIGHTCOLOR1 = '#2edad4'
    const RIGHTCOLOR2 = '#08beb8'
    const ROW = 15 // 渲染行数
    const COL = 20 // 渲染列数
    const GRAVITY = -10 // 重力加速度
    const POWER = 15 // 升力加速度

    var height = [] // 高度储存器
    var speed = [] // 运动速度储存器
    var touch = { x: 0, y: 0, work: false } // 被触发位置储存器

初始化运动状态储存器

把方块矩阵按照行列数初始化出两个二维数组,分别记录运动高度和垂直运动速度。

    function initParam() {
        for (let y = 0; y < ROW; y++) {
            height[y] = []
            speed[y] = []
            for (let x = 0; x < COL; x++) {
                height[y][x] = 0
                speed[y][x] = 0
            }
        }
    }

封装绘制函数

分别封装绘制顶部,左侧,右侧三个平行四边形的函数,再合起来调用一遍,就得到了绘制单个立方体的方法。

    function drawBox(x, y, h) { // 画整个方体
        drawTop(x, y, h)
        drawLeft(x, y, h)
        drawRight(x, y, h)
    }
    function drawTop(x, y, h) { // 画方体顶部
        ctx.beginPath()
        let grd = ctx.createLinearGradient(x, y - h - UNITLONG, x, y - h + UNITLONG);
        grd.addColorStop(0, TOPCOLOR1)
        grd.addColorStop(1, TOPCOLOR2)
        ctx.fillStyle = grd
        ctx.strokeStyle = grd
        ctx.moveTo(x, y - h)
        ctx.lineTo(x + RADIX3 * UNITLONG, y - UNITLONG - h)
        ctx.lineTo(x + 2 * RADIX3 * UNITLONG, y - h)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
        ctx.stroke()
        ctx.fill()
        ctx.closePath()
    }

    function drawLeft(x, y, h) { // 画方体左侧
        ctx.beginPath()
        let grd = ctx.createLinearGradient(x, y, x - UNITLONG, y + 2 * UNITLONG);
        grd.addColorStop(0, LEFTCOLOR1)
        grd.addColorStop(1, LEFTCOLOR2)
        ctx.fillStyle = grd
        ctx.strokeStyle = grd
        ctx.moveTo(x, y - h)
        ctx.lineTo(x, y + UNITLONG * 2)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
        ctx.stroke()
        ctx.fill()
        ctx.closePath()
    }

    function drawRight(x, y, h) { // 画方体右侧
        ctx.beginPath()
        let grd = ctx.createLinearGradient(x + RADIX3 * UNITLONG, y + UNITLONG, x + RADIX3 * UNITLONG + UNITLONG, y
        + UNITLONG * 3);
        grd.addColorStop(0, RIGHTCOLOR1)
        grd.addColorStop(1, RIGHTCOLOR2)
        ctx.fillStyle = grd
        ctx.strokeStyle = grd
        ctx.moveTo(x + RADIX3 * UNITLONG, y + UNITLONG - h)
        ctx.lineTo(x + RADIX3 * UNITLONG, y + UNITLONG * 3)
        ctx.lineTo(x + RADIX3 * UNITLONG * 2, y + UNITLONG * 2)
        ctx.lineTo(x + RADIX3 * UNITLONG * 2, y - h)
        ctx.stroke()
        ctx.fill()
        ctx.closePath()
    }

实时运算与渲染

这一模块由3个函数构成: 监听onmousemove事件:鼠标移动对方块产生向上的加速度,把运动状态写入到速度存储器中。 计算瞬时状态:从存储器读取当前位置和速度,计算下一帧的位置和速度。 draw方法:计算完成下一帧的位置信息后,调用drawBox绘制下一帧的画面。

    function draw() { // 渲染一帧画面
        calStatus() // 运算实时状态
        for(let y = 0; y < ROW; y++){
            for (let x = 0; x < COL; x++) {
                if (y/2 === Math.floor(y / 2)){
                    drawBox(x * UNITLONG * RADIX3 * 2, y * UNITLONG * 3, height[y][x])
                } else {
                    drawBox(x * UNITLONG * RADIX3 * 2 - UNITLONG * RADIX3, y * UNITLONG * 3, height[y][x])
                }
            }
        }
    }
    
    function calStatus() { // 计算瞬时状态
        for (let y = 0; y < ROW; y++) {
            for (let x = 0; x < COL; x++) {
                if (x === touch.x && y === touch.y && touch.work) {
                    speed[y][x] += POWER
                    touch.work = false
                } else {
                    speed[y][x] = height[y][x] === 0 ? 0 : speed[y][x] + GRAVITY
                }
                height[y][x] = height[y][x] + speed[y][x] < 0 ? 0 : height[y][x] + speed[y][x]
            }
        }            
    }


    my3dCanvas.onmousemove = e => { // 监听鼠标在画布上移动
       let pt = getPointOnCanvas(my3dCanvas, e.clientX, e.clientY)
       touch.work = true
       touch.y = Math.floor(pt.y / (3 * UNITLONG))
       if (pt.y/2 === Math.floor(pt.y / 2)) {
           touch.x = Math.floor(pt.x / (2 * RADIX3 * UNITLONG))
       } else {
           touch.x = Math.floor((pt.x + UNITLONG * RADIX3) / (2 * RADIX3 * UNITLONG))
       }
    }

开始动起来!

调用初始化! 用计数器循环调用draw方法绘制画面,成功啦!

    initParam()
    var itv = setInterval(() => draw(), 50) // 计时器 0.05秒渲染一次 20帧

有时候一开始也并不知道自己要做成什么效果,一步一步慢慢完善出来,最后才发现,摸掉的时间已经太久啦。