canvas进阶——实现静态图像的变形并合成动态效果

2,509 阅读5分钟

写在最前

在之前的这篇bezierMaker.js——N阶贝塞尔曲线生成器的文章中我们提到了对于高阶贝塞尔公式的绘制与生成。不过更多的童鞋看到后可能会不知道其使用场景是什么。故作者本次分享一下基于bezierMaker.js实现的将静态图片按照自定义曲线轨迹扭曲图片并合称为动态效果。

欢迎关注我的博客,不定期更新中——

效果预览

之前的描述可能不是很清楚我们直接看下效果图:

首先加载一张图:
image

然后通过bezierMaker.js提供的试验场功能来绘制一段曲线,进行图片扭曲:
image

最后拟合为动态图:

2018-01-19 12_40_32

再来一个竖直方向的扭动:

anmate

demo地址

源码地址

图像变形实现思路

  1. 绘制一条由bezierMaker.js生成的贝塞尔曲线,以此来掌握曲线各点的准确坐标值
  2. 确定扭曲方向为横向或纵向
  3. 根据该方向的基准线(图中的灰色线)来计算本次绘制的曲线与基准对比的偏移量,按照该方向每隔1px记录一个值
  4. 将图像数据按照选定方向进行切分,将一个一维数组imgData.data变为一段一段有方向的二维数组
  5. 将每段数组按照之前记录的偏移值进行移位后再拼接为一维数组
  6. 将新拼接好的数组重新赋值到imgData中

其中较为核心的实现即横向与纵向对一维图像数据的切分。其中横向相对简单,细节如下:

image

如上图所示,在原始图像的数据中的数据形式为一维数组的形式,而对其进行拆分则是一个从中不断截取与提取数据的过程。横向拆分较为简单,只需要确定每一行开始的位置即可,截取的数量就是一行的元素数。同时纵向拆分则需要多加一步,我们需要计算每一层数组中的每一个数,像上图一般拆分第每列数组时首先要遍历图的宽得到每一列的索引,再遍历图的高,通过高 ✖️宽 ✖️4 + 宽 ✖️ 4算出当前值在原数据中的位置。当拆分成功数组后,将数组依次移位,移位数为之前曲线与基准线的偏移量决定。

//pg.js
//按行拆分
bezierArr.forEach(function (obj, index) {
    if (_.imgStartY < obj.y && _.imgStartY + _.imgHeight > obj.y && type === 'row') {
    
        var diffX = parseInt(obj.x - _.baseX, 10) //计算偏移量
        var dissY = parseInt(obj.y - _.imgStartY, 10)
        var rowNum = dissY
        imgDataSlice = _.imgData.data.slice((rowNum) * _.imgWidth * 4, rowNum * _.imgWidth * 4 + _.imgWidth * 4) //按层切片
        ...
    }
})

//按列拆分
for (var i = 0; i < _.imgWidth; i++) {
    imgDataSlice = []
    for (var j = 0; j < _.imgHeight; j++) {
        var index = j * _.imgWidth * 4 + i * 4
        var sliceArr = _.imgData.data.slice(index, index + 4)
        imgDataSlice = imgDataSlice.concat(Array.from(sliceArr))
    }
    if(_.imgChangeObj[i]) {
        for (var k = 0; k < Math.abs(_.imgChangeObj[i].diffY * 4); k++) {
            imgDataSlice = _.arraymove(_.imgChangeObj[i].diffY, imgDataSlice)
        }
        for (var p = 0; p < imgDataSlice.length / 4; p++) {
            arr[p * _.imgWidth * 4 + i * 4] = imgDataSlice[p * 4]
            arr[p * _.imgWidth * 4 + i * 4 + 1] = imgDataSlice[p * 4 + 1]
            arr[p * _.imgWidth * 4 + i * 4 + 2] = imgDataSlice[p * 4 + 2]
            arr[p * _.imgWidth * 4 + i * 4 + 3] = imgDataSlice[p * 4 + 3]
        }
    }
}

核心的数组拆分移位再合并的逻辑相对分散,知道思路即可有兴趣的同学欢迎戳源码~

合并成动态效果

核心思想为从我们的原始形态到最终态的两张静态图我们已经得到了。现在我们需要做的是添加几张过渡态。在这里面有两种方式:

  • 将计算的各点偏移量进行按比例偏移,比如一共四张图合成则需要三次改变状态,那么每次将数组移位的量设定为总量的1/3,每次移位后拼出一维数组更新到一张离屏canvas中将其保存为base64,作为后续合并时的替换url
  • 计算贝塞尔曲线控制点的偏移量且进行按比例偏移。如一开始的垂直或水平的初始图控制点形成了一条直线。同时最终形态的控制点位置我们已经知道了,借此我们可以将控制点由直线到两边的过程按比例切分,依次计算各中间态控制点所形成曲线导致的偏移图像数据,导出base64,作为后续合并替换的url

作者一开始使用了第一种方式,但是有一个明显的缺陷及通过按比例直接偏移会导致拆分出来的每层的偏移每次都是相同的,那么就会出现锯齿现象。因为图像扭曲可能上一层在这一次移位的时候偏移5合适可是你仍然偏移了总量的1/3导致与下一层的图像不匹配从而出现锯齿。故重新选择了第二种方式,由重新计算各中间态图像的控制点再来移位图像数据,图像的呈现情况就改善了很多。

小结

由于操作图像数据量比较大,故在尝试demo的时候如果遇到ui卡顿那是正在计算中,并没有引入webworker之类的所以请稍等一会就会出现结果=。= PS:demo使用步骤

  • 加载图像
  • 画曲线,竖向切分请点击checkbox,同时曲线宽要大于图像的宽。横向切分数据则曲线高要大于图像,保证起终点在基准线外。描点后点击绘制
  • 计算结束后点击合成

其他canvas相关文章

最后

demo地址

源码地址

惯例po作者的博客,不定时更新中——

有问题欢迎在issues下交流。