我试着还原一下大佬的动画
我之前因然叔文章中的动画影响,心血来潮写了一个小工具,可以通过编程的方式快速的设计整个动画,这次我想试试用这个工具还原一下大帅老猿的动画。
大佬的动画效果图
分析
通过分析要还原的效果图,初步得出需要实现 4 点功能:
- 画线:画直线或者圆点线条。
- 代码逐字母敲入并且有光标。
- 文字渐变显示。
那么就逐个击破。
击破 “画线-画直线或者圆点线条”
分析一下
通过效果图了解,需要将指定字符通过画线框住。 那么我们就设计一个传入指定节点,然后用线框住的功能。 那么就设设计连个函数:画直线,画圆点线条和获取绘制点信息。
实现“画直线”:
函数怎么设计舒服?
我期望的是我传入起点和终点就能画一条线,我还能传入一个进度值来控制绘制的进度,能快进还能回滚,最后还能传入一些信息进行对绘制的调整,比如颜色,线条宽度。 期望定下来,那么顺着去开发。
代码如下:
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;
},
},
];
别忘了光标是需要闪烁的
用原生生命周期update
和entity
个体的生命周期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
的书,也就是略懂皮毛,勉强能通过面向百度
编程,有效地收集到了完成该功能的相关信息,好让该功能实现,不过这是一个相当值得深入探寻的领域,我很感兴趣,但话说回来,我对知识的态度还是比较实在的,够用就行,一言蔽之就是“随遇而刻”,随着当下境遇,进行刻意练习,等未来或许有机会有需要我可能会深入且系统的去研习。
实现的效果
完整效果图
结语
目前工具还在不断完善,离我心中所想还有一段距离,未来的一段时间还会继续打磨,尽快做出一个可以试用的版本,如果有前辈和同学有需要我还原的可以跟我讲,我会尽可能的安排上(安排不上可能只是没时间弄,哈哈)