6 - 序列帧:Sprite


很多时候动画的每帧之间, 不仅仅是变形属性的差异, 更重要的是内容(素材)的变化。一串动画, 往往是通过工具做好的一串序列帧。

以下我们两张构成序列帧的图片。按照一定的时间间隔, 交替展示这两张图片, 就形成了小鸟挥舞翅膀飞的动画。

在HTML里, 可以用引入gif图片展示动图; 在画布里, 利用 Ticker 加上切分帧间隔, 也可以实现类似的效果。完整代码和演示可见于 jsfiddle

创建 Sprite 类

Sprite 是一个展现序列动画的类, 只需要实现以下功能: 一份关于每一帧的全部状态的数据合集; 每秒钟更新多少帧。我们把这些数据作为构造函数参数 config 传入。然后, 在 Sprite 内部注册 Ticker 事件, 每帧检查是否需要切换到下一帧。如果需要, 就把当前帧指向下一帧的数据, 并在这一帧结束的时候重绘。

var Sprite = function (config) {
    this.x = 0
    this.y = 0

    this.regX = 0
    this.regY = 0

    this.image = null
    this.frameRate = config.frameRate
    this.frameInterval = Math.ceil(60 / this.frameRate)

    //  总的帧数
    this.totalFrames = config.frames.length
    //  当前帧数
    this.currentFrame = 0

    var updateFrame = tickCount => {
        if(tickCount % this.frameInterval == 0) {
            this.currentFrame = (this.currentFrame + 1) % this.totalFrames
            var frame = config.frames[this.currentFrame]
            this.image = frame.image
            this.regX = frame.regX
            this.regY = frame.regY
        }
    }
    updateFrame(0)
    Ticker.on(updateFrame)

    this.canMove = true

    this.draw = function () {
        ctx.drawImage(this.image, this.x - this.regX, this.y - this.regY)
    }

    this.move = function () {
        if (!this.canMove) {
            return
        }

        this.x += this.vx
        this.y += this.vy

        if (this.x <= this.regX || this.x >= canvas.width - this.regX) {
           this.vx *= -1
        }
        if (this.y <= this.regY || this.y >= canvas.height - this.regY) {
            this.vy *= -1
        }
    }
}

...

function start() {
    var config = {
        frameRate: 6,
        frames: [
            {regX:52, regY:44, image:null},
            {regX:48, regY:40, image:null}
        ]
    }

    config.frames.forEach((frame, index) => {
        frame.image = imgList[index]
    })

    var sp = new Sprite(config)
    sp.x = canvas.width >> 1
    sp.y = canvas.height >> 1
    children.push(sp)
}

在上面的 config 中, 每一帧数据包含了: regX, regY, image。regX/regY 是之前就在工具中定好位置, 否则两帧动画之间可能会不连贯。用FLASH可以很方便的制作2D动画并导出成需要的各种格式。

上下浮动

为了让飞的效果更好一些, 给 Sprite 加入一种上下晃动的效果。这里给它加入两个属性 offsetX, offsetY 作为额外的偏移, 并利用正弦变换, 每帧计算偏移量, 并最终应用到 draw() 方法中。

定义属性 r 作为浮动的半径, 定义属性 deg 作为相位角度(单位是度), 定义 vDeg 作为相位角度的增速(单位是度)。代码如下:

    this.deg = 0
    this.vDeg = 4
    this.r = 30
    this.offsetX = 0
    this.offsetY = 0

    this.draw = function () {
        ctx.drawImage(this.image, this.x - this.regX + this.offsetX, this.y - this.regY + this.offsetY)
    }

    this.move = function () {
        if (!this.canMove) {
            return
        }

        this.x += this.vx
        this.y += this.vy

        this.deg += this.vDeg
        var rad = Math.PI * this.deg / 180
        // this.offsetX = this.r * Math.cos(rad)
        this.offsetY = this.r * Math.sin(rad)
    }

点击交互

最后, 加入一点点交互: 点击舞台, 让小鸟飞到点击的位置。通过小鸟当前的位置和点击的位置, 实时计算小鸟的 vx, vy。

计算从A点到B点的矢量的倾斜度, 通常使用 Math.atan2 以得到值域在[-PI, PI] 之间的弧度值。然后, 把矢量的速度分解为水平/垂直分量并赋值给小鸟, 使得小鸟以恒定的速度飞向点击位置。

然而还有一个问题: 需要判定小鸟是否飞到了目标点。简单的每帧判断坐标点是否等于目标位置坐标是不行的, 因为运动过程中坐标数值是小数, 在每一帧可能都不会严格等于目标位置坐标。

于是采用了距离计算的方式, 每一帧记录与目标位置之间的距离, 如果这个距离比上一次记录的距离要大,说明刚刚飞过目标点, 此时把小鸟停在目标点并移除速度即可。


//  点击时计算速度
canvas.addEventListener('click', (e) => {
    var rect = canvas.getBoundingClientRect()
    var x = e.clientX - rect.left   //  px
    var y = e.clientY - rect.top    //  px

    //  location
    x *= canvas.width / canvas.offsetWidth
    y *= canvas.height / canvas.offsetHeight

    var rad = Math.atan2(y - bird.y, x - bird.x)
    bird.vx = birdVelocity * Math.cos(rad)
    bird.vy = birdVelocity * Math.sin(rad)
    bird.dist = 0
    tarX = x
    tarY = y
})

//  移动 & 计算记录
Ticker.on(() => {
    children.forEach(child => {
        child.move()
        var dist = Math.sqrt(Math.pow(tarX - child.x, 2) + Math.pow(tarY - child.y, 2))
        if (child.dist && child.dist < dist){
            child.vx = 0
            child.vy = 0
            child.x = tarX
            child.y = tarY
        }

        child.dist = dist
    })
})

由以上的演示, 可以看出帧动画实际上是比较不方便的。所幸有很多第三方缓动库, 可以快速实现不错的运动效果。

完整代码和演示可见于 jsfiddle