强撸一波动画工具,代码演示不光得有文字和图片,还得有动画

1,596 阅读4分钟

Moderate

我试着还原一下大佬的动画

我之前因然叔文章中的动画影响,心血来潮写了一个小工具,可以通过编程的方式快速的设计整个动画,这次我想试试用这个工具还原一下大帅老猿的动画。

大佬的动画效果图

分析

通过分析要还原的效果图,初步得出需要实现 4 点功能:

  1. 画线:画直线或者圆点线条。
  2. 代码逐字母敲入并且有光标。
  3. 文字渐变显示。

那么就逐个击破。

击破 “画线-画直线或者圆点线条”

分析一下

通过效果图了解,需要将指定字符通过画线框住。 那么我们就设计一个传入指定节点,然后用线框住的功能。 那么就设设计连个函数:画直线,画圆点线条和获取绘制点信息。

实现“画直线”:

函数怎么设计舒服?

我期望的是我传入起点和终点就能画一条线,我还能传入一个进度值来控制绘制的进度,能快进还能回滚,最后还能传入一些信息进行对绘制的调整,比如颜色,线条宽度。 期望定下来,那么顺着去开发。

代码如下:

drawLine({ ctx, start, end, progress, space, isAdd, strokeColor,lineWidth}) {
        ctx.strokeColor = strokeColor;
        ctx.lineWidth = lineWidth;
        let unit = space;
        let vline = end.sub(start);
        let vlineLen = vline.mag();
        let sumPart = parseInt(vlineLen / unit)
        let drawCount = parseInt(sumPart * progress);

        for (let i = 1; i < drawCount; i++) {
            let c = vline.mul(i / sumPart)
            ctx.lineTo(c.x + start.x, c.y + start.y);
        }
        if (progress == 1 && isAdd) {
            ctx.lineTo(end.x, end.y);
        }
        ctx.stroke();
    }

参数一览

  • ctx:绘制类的实例,通过他进行绘制。
  • start:起点
  • end: 终点
  • progress:进度
  • space:绘制间距
  • isAdd:是否需要在末尾补充绘制
  • strokeColor:绘制线条的颜色

实现“画圆点线条”:

通过效果图了解,需要画一条圆点组成的线条,首先大体跟画直线相同,唯独一些参数会有不同,比如传入绘制小点的半径。

函数怎么设计舒服?

跟画直线一致,我期望的是我传入起点和终点就能画一条线,我还能传入一个进度值来控制绘制的进度,能快进还能回滚,最后还能传入一些信息进行对绘制的调整,比如绘制点颜色,半径大小。 期望定下来,那么顺着去开发。

代码如下:

drawPointLine({ ctx, start, end, progress, space, radius, isAdd,fillColor }) {
        let unit = radius * 2 + space;
        let vline = end.sub(start);
        let vlineLen = vline.mag();
        let sumPart = parseInt(vlineLen / unit)
        let drawCount = parseInt(sumPart * progress);
        for (let i = 1; i < drawCount; i++) {
            let c = vline.mul(i / sumPart)
            ctx.circle(c.x + start.x, c.y + start.y, radius);
            ctx.fillColor =fillColor
            ctx.fill();
        }
        if (progress == 1 && isAdd) {
            ctx.circle(end.x, end.y, radius);
            ctx.fillColor =fillColor
            ctx.fill();
        }
    }

参数一览

  • ctx:绘制类的实例,通过他进行绘制。
  • start:起点
  • end: 终点
  • progress:进度
  • space:绘制间距
  • radius:绘制点所需要的半径
  • isAdd:是否需要在末尾补充绘制
  • fillColor:绘制点的颜色

实现“获取绘制点信息”:

函数怎么设计舒服?

我期望的是,我传入一个字符类型的节点,并传入指定字符的下标,函数就能返回给我一个围绕该字符画线的点集合信息,同时我也可以通过出入一下参数进行微调,比如画的格子宽一点,左右移动一点。 期望定下来,那么顺着去开发。

getPointFromCodeNode({node, letterIndex,unit,customOffsetX=0}) {
        let condeStr = node.getComponent(cc.RichText)
        let codeLen = condeStr.string.length
        let width = node.width;
        let height = node.height;
        let nodeY = node.y;
        let nodeX = node.x;
        let unitX = unit||width / codeLen
        let unitY = height / 2;
        let pointArr = [];
        let centerPoint = {
            x: nodeX + letterIndex * unitX - unitX / 2,
            y: nodeY
        }
        let offsetCommon = {
            oX: 5,
            oY: -5
        }
        let offsetArr = [
            { oX: -unitX / 2, oY: -unitY },
            { oX: -unitX / 2, oY: unitY },
            { oX: unitX / 2, oY: unitY },
            { oX: unitX / 2, oY: -unitY },
        ]
        for (let i = 0; i < 4; i++) {
            let offset = offsetArr[i];
            let pointTemp = cc.v2(centerPoint.x + offset.oX + offsetCommon.oX+customOffsetX, centerPoint.y + offset.oY + offsetCommon.oY);
            pointArr.push(pointTemp)
        }
        return pointArr
    }

这就完了

既然都已经有了画直线和画点线,那未来很有可能画浪线,画曲线,画...各种线,不在上层写一个函数进行统一管理,那代码岂不是很混乱。

那么就开发一个统一处理函数handleDrawLine,职能就是将上面传入的信息进行统一处理,整合管理多个绘制基础函数(如画线,画点线),对各个绘制环节进行灵活调配,对外尽可能暴露接口,不暴露实现,使用只需要关注主要的关键参数即可,如起始点信息,绘制线条进度。

代码如下:

handleDrawLine(data) {
        const { ctx, pointArr, progress ,drawType=DRAW_TYPE.TYPE_DEFAULT,isLoop=true} = data;
        ctx.clear();
        //根据点集合,得出向量集合
        let vlineArr = [];
        let pointArrLen = pointArr.length;
        pointArr.forEach((item, index) => {
            let start = item;
            let end;
            if (index === pointArrLen - 1) {
                end = pointArr[0];
            } else {
                end = pointArr[index + 1]
            }
            vlineArr.push(end.sub(start))
        });
        //如果不是循环的,那么就把最后自动闭合那条线去掉。
        if(!isLoop){
            vlineArr = vlineArr.slice(0,vlineArr.length-1);
        }
        //得出所有向量长度集合
        let vlineMagArr = vlineArr.map((item) => {
            return item.mag()
        });
        // 得出总长度
        let totalMag = vlineMagArr.reduce((total, current) => {
            return total + current
        }, 0)
        let basePoint = [pointArr[0]];
        let targetLength = 0;
        vlineMagArr.forEach((itemMag, index) => {
            targetLength += itemMag;
            if (targetLength / totalMag < progress) {
                basePoint.push(pointArr[index + 1])
            }
        })
        let processBaseLine = (params)=>{
            const {type,point,index} = params;
            if (type === DRAW_TYPE.TYPE_CIRCLE) {
                this.drawPointLine({ ...data, start: basePoint[index - 1], end: point, progress: 1, isAdd: true })
            } else if (drawType === DRAW_TYPE.TYPE_DEFAULT) {
                ctx.lineTo(point.x, point.y)
            }
        }
        let processDrawLine = (params)=>{
            const {type} = params;
            if (type === DRAW_TYPE.TYPE_CIRCLE) {
                this.drawPointLine({ ...data, ...targetPoint, progress: progressTemp });
            } else if (drawType === DRAW_TYPE.TYPE_DEFAULT) {
                this.drawLine({ ...data, ...targetPoint, progress: progressTemp });
            }
        }
         // 根据基本点,画固定线条
        basePoint.forEach((point, index) => {
            if (index === 0) {
                ctx.moveTo(point.x, point.y);
            } else {
                processBaseLine({type:drawType,point,index})
            }
        })
        //画动态线条根据目标点
        let targetPointIndex = basePoint.length;

        let targetPoint;
        if (targetPointIndex < pointArr.length) {
            targetPoint = {
                start: pointArr[targetPointIndex - 1],
                end: pointArr[targetPointIndex]
            }
        } else {
            targetPoint = {
                start: pointArr[targetPointIndex - 1],
                end: pointArr[0]
            }
        }
        let progressTemp = (progress * totalMag - vlineMagArr.slice(0, targetPointIndex - 1).reduce((total, current) => {
            return total + current
        }, 0)) / vlineMagArr[targetPointIndex - 1];
        processDrawLine({ ...data, ...targetPoint, progress: progressTemp,type:drawType })
    }

实现的效果

击破问题点 “代码逐字母敲入并且有光标”

这个问题就好办多了,如果单独写一个代码敲入的函数,可能会费点劲,但别忘了,我写的这个架子中entity 个体所实现的生命周期函数配合reaction个体的动作的执行函数action,解决起这个问题来,简直 6 的不要不要的。

只需找一个个体entity,然后配置其动作列表,指定动作的运行生命周期,并在动作中根据进度来控制代码的敲入,控制光标节点位置,那么剩下的全部交给架子就 ok 了。

代码如下:

this.recationArr = [
  {
    start: this.customStart,
    end: this.customStart + 0.1,
    action: (data = {}) => {
      const { progress } = data;
      let richText = self.getComponent(cc.RichText);
      let endId = parseInt(progress * this.codeArr.length);
      let tempArr = this.codeArr.slice(0, endId);
      self.curr.setContentSize(
        cc.size(6, richText.node.getContentSize().height * 0.7)
      );
      //这段很low。。。。我也想优雅,但我感觉分析代码并进行配置颜色,好像得写个很大的功能才行。临时先这么根据字符串进行配色,毕竟字符不是很多。
      tempArr = tempArr.map((item) => {
        if (self.code.includes("methods")) {
          if ("methods".indexOf(item) > -1) {
            return (item = `<color=#1e88df>${item}</color>`);
          }
        }
        if (self.code.includes("return")) {
          if ("return".indexOf(item) > -1) {
            return (item = `<color=#AD582E>${item}</color>`);
          }
        }
        if (self.code.includes("data")) {
          if ("data".indexOf(item) > -1) {
            return (item = `<color=#5ab23c>${item}</color>`);
          }
        }
        return item;
      });
      richText.string = tempArr.join("");
      self.curr.x = self.node.x + richText.node.width;
      self.curr.y = richText.node.y - 2;
    },
  },
];

别忘了光标是需要闪烁的

用原生生命周期updateentity 个体的生命周期process相结合来解决。

update(){
        this._super();
        if(this.isStop){
            this.curr.active = false
        }else{
            if(!this.shakeFlag){
                this.shakeFlag = Date.now()
            }else{
                let now = Date.now()
                if((now - this.shakeFlag)>500){
                    this.shakeFlag = now;
                    this.curr.active = !this.curr.active;
                }
            }
        }
    },
    process(props){
        if(this._super(props)===0){
            return
        }
        if(this.isStop){
            this.curr.active = false
        }else{
            this.shakeFlag = 0;
            this.curr.active = true;
        }
    }

实现的效果

  • this.isStop:标注是否停止闪烁
  • this.shakeFlag:控制闪烁的变量

击破:文字渐变显示。。。

em~~~,这个,怎么说呢,我就是实现了功能,但是你让我说出个一二,我还真没那个功力,这里使用的是shader,相信学习过webgl的同学一定不陌生,当时我开发cesium的时候,读了半本的webgl的书,也就是略懂皮毛,勉强能通过面向百度编程,有效地收集到了完成该功能的相关信息,好让该功能实现,不过这是一个相当值得深入探寻的领域,我很感兴趣,但话说回来,我对知识的态度还是比较实在的,够用就行,一言蔽之就是“随遇而刻”,随着当下境遇,进行刻意练习,等未来或许有机会有需要我可能会深入且系统的去研习。

实现的效果

完整效果图

结语

目前工具还在不断完善,离我心中所想还有一段距离,未来的一段时间还会继续打磨,尽快做出一个可以试用的版本,如果有前辈和同学有需要我还原的可以跟我讲,我会尽可能的安排上(安排不上可能只是没时间弄,哈哈)