前端实现图片的立体合成效果

一切源于这样一张有趣的图片,破碎的国旗在川普的身后重新组装起来:
image.png

我打算用纯js实现一下这个效果:一张图片打碎后渐进拼接的组合效果,一个寓意着和平、团结、希望的图案。

这是以中国国旗为例的效果图:
qssyl-37xix.gif

功能设计

我们要实现这样一个东西,主要考虑几下几点:

  • 如何实现立体效果
  • 如何将一张完整的图片切成一个个的小块
  • 如何处理小块的起点、终点位置并设置运动轨迹

立体效果

其实就是借助的css3属性transfrom-style:preserve-3d结合transform。通过给父容器元素设置preserve-3d激活3d效果,再进行子元素的x、y轴、z轴平移、反转实现的。

具体细节不再赘述,可以参考这篇文章

图片切块

将一张完整的图片切成一个个的小片,有一些问题需要思考:

  • 如何获取图片的原始尺寸、保证图片不会超出容器?借助imgElement.naturalWidth/naturalHeight,设置合理的比例尺,完成图片的等比例缩放
  • 切片的形状如何定义?三角形?多边形?不,必须是矩形,因为我打算用DOM节点来做形状,不想搞太复杂
  • 切片尺寸如何定义?随机生成?太复杂,必须是均分的。参数可配置即可
  • 如何实现切片呢?利用canvasCtx.drawImage

okay,细节敲定,开始撸代码!
先计算切片的剪切参数:dx、dy、dw、dh,dx、dy表示图片的剪切起点,dw、dh表示图片的剪切大小。

//计算每片的宽高,向下取整
const slicePixelXLength = Math.floor(this._containerWidth / this.columnCount)
const slicePixelYLength = Math.floor(this._containerHeight / this.rowCount)

//边界参数组
const boundaryHorizontalValues = (new Array(this.columnCount).join('-').split('-')).map((_, index) => slicePixelXLength * index);
const boundaryVerticalValues = (new Array(this.rowCount).join('-').split('-')).map((_, index) => slicePixelYLength * index)

//每个切片参数组成的数组
const sliceArr = []

boundaryVerticalValues.forEach((itemY, indexY) => {
    boundaryHorizontalValues.forEach((itemX, indexX) => {
        let dx, dy, dw, dh;
        dx = itemX;
        dy = itemY;
        dw = slicePixelXLength;
        dh = slicePixelYLength

        if (indexX === boundaryHorizontalValues.length - 1) {
            //the last one
            dw = this._containerWidth - indexX * slicePixelXLength
        }
        if (indexY === boundaryVerticalValues.length - 1) {
            //the last one
            dh = this._containerHeight - indexY * slicePixelYLength
        }

        sliceArr.push({
            dx,
            dy,
            dw,
            dh
        })
    })
})
复制代码

得到切片的参数后就可以拿到一个个的切后的图片了:

const base64Arr = [];
sliceArr.forEach(item => {
    canvas.width = item.dw * this.ratio
    canvas.height = item.dh * this.ratio
    canvasCtx.clearRect(0, 0, canvas.width, canvas.height)
    canvasCtx.drawImage(this.imgEle, item.dx / this.ratio, item.dy / this.ratio, item.dw / this.ratio, item.dh / this.ratio, 0, 0, canvas.width, canvas.height);
    base64Arr.push(canvas.toDataURL())
})
复制代码

计算、设置切块的轨迹

其实我们上面的到的 sliceArr里切片的dx,dy属性就是切片相对于原始图片的位置,自然也就是运动轨迹的终点。所以我们还需要计算起点。

其实起点的位置我们不需要太精确,只需要位于容器的最底部即可,从看不见的位置飞上来到达终点:imgBox.style.top = this._containerHeight + imgboxSize[0] / 2 +'px',这里我们取了外围容器的高度加盒子尺寸一个边的一半(其实不太精确,有时形状可能会露头,不过没啥大问题)。

上面设置的还只是2d平面的,还需要考虑3d平面的transform,:

const randomDegree = () => Math.floor(Math.random() * 360)
const randomSignal = () => Math.random() > 0.5 ? '-' : '+'

//initial position:搞个随机位置点作为起点
imgBox.style.transform = `rotateX(${randomSignal()}${randomDegree()}deg) rotateY(${randomSignal()}${randomDegree()}deg)`
复制代码

终点自不必说了,还原就行了,角度都是0。

拿一个切片为例:

const imgBox = document.createElement('div');
//设置盒子的大小
const imgboxSize = [this.sliceArr[index].dw, this.sliceArr[index].dh]
imgBox.style.width = imgboxSize[0] + 'px'
imgBox.style.height = imgboxSize[1] + 'px'
//定位
imgBox.style.position = 'absolute';
imgBox.style.transformStyle = 'preserve-3d';
//定义轨迹属性
imgBox.style.transition = 'transform 1.5s ease-in, left 1.5s ease-in, top 1.5s ease-in';
//get random degree
const randomDegree = () => Math.floor(Math.random() * 360)
const randomSignal = () => Math.random() > 0.5 ? '-' : '+'

//initial position
imgBox.style.top = this._containerHeight + imgboxSize[0] / 2 + 'px'
imgBox.style.transform = `rotateX(${randomSignal()}${randomDegree()}deg) rotateY(${randomSignal()}${randomDegree()}deg)`

//开启运动(一定要在nextTick里设置transition的终点才能触发运动)
setTimeout(() => {
    imgBox.style.left = this.sliceArr[index].dx + 'px'
    imgBox.style.top = this.sliceArr[index].dy + 'px'
    imgBox.style.transform = 'rotateX(0deg) rotateY(0deg)'
}, 100)
复制代码

最后

具体代码可以在这个仓库下找到,我将上述过程封装成了一个类里面,无需任何的样式,支持一些参数的配置,仅需几行代码加一张图片即可实现:

const imgTransformer = new ImageTransformer('#img-container')
//配置,可选
imgTransformer.config({
    containerWidth: 450,
    effect: 'grow',
    columnCount: 6,
    rowCount: 6
})
//启动:如果因某种原因导致图片跨域不能访问,你可以考虑将其转换为base64的形式
imgTransformer.initImgData(base64OrUrl, () => {
    imgTransformer.execute()
})
复制代码

来一组效果图:

20220309080801-7c011ad592.[gif-2-mp4.com].gif

距离上次发文 为战胜焦虑,我写了个App已经有近一周了,
感谢里评论区各位网友的热心建议,当时焦虑已经有了明显的改善,但最近似乎又卷土重来了,看来与焦虑的斗争注定是一场拉锯战,没有那么简单。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享