我做了一个制作代码演示动画的工具,答应我以后别再熬夜做动画了

3,243 阅读5分钟

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

项目源码

做这个工具的动机

偶然间我往webpack标签中发了一篇文章,碰巧看到了然叔写的做了一夜动画,让大家十分钟搞懂Webpack,我并不了解然叔,但我觉得他很可能比我大,考虑到一位如此努力分享技术的大哥,为了奉献自己的光和热,做了一夜,那属实让人有些心疼,故我想尽我的一点力,为大佬和大家们也发出我的一份热。

我被吸引了

首先领略了一下文章,并印证了“不用想肯定比我参悟深,大佬就是大佬”这个”偏见“,但最直接吸引我并不是webpack😂,而是一束“光”,对就是大佬写了一夜的动画,我感叹动画制作的如此之精巧,表达效果竟然如此之显著,直接地,强烈地吸引了我。

行动起来

与其沉浸在喜爱和膜拜的感觉中,不如借着这份情感做点事。我心中审视了一下自己现有的能力,很快就得出了一个想法💡,我好像能整出来一个做出这种“光”的轮子,ok,说干就干。

描绘出自己的期望就成功了一半

首先我不想做成静态资源那种的,比如视频,gif之类的,这种是没有“生命”的,只能称之为“物质”,我简单用“阴”代指,我所需要是基于物质所焕发同时也可以创造物质的的“生命,心灵”,我称他为阳。说ta是“活”的有点过,就是说可以交互,可以整合再利用,随时可以通过ta创建视频,gif之类的静态资源,岂不妙哉。

基于期望我确定了实现方案

首先作为一个前端,技术面要打开,就很容易接触到形形色色的技术,那么渲染引擎就是之一。首先出于我的需求,通过渲染引擎开发这会大大的提高开发的效率,其次也可以整合进我写的脚手架“Moderate”中,是一个优解。

设计开发

我总希望用最简单的话语,描述一件事,我比较喜欢简单,那么我们就用简单的方式把我的设计讲讲。

通过鼠标或触摸滑动,推进动画进程

视频可以快进,倒退,仅仅通过拨动进度条或者滑动屏幕即可,我喜欢这种交互,那就做成这样,舒服。

设计好个体,非常关键

我希望设计出一系列独立个体,可以很好的串联起整个逻辑,它具备了基本的功能,同时又具备了扩展的能力,可互相联结,又彼此独立,目前有两个主要的个体,一个是单位个体entity,另一个是动作个体recation

entity大体应该具备以下行为:

  • 描述自身运行的周期: lenPercentstartPercent
  • 生命周期函数
    • 开始:live
    • 渲染:process
    • 结束:end
  • 可以装载其他个体的能力:entityArr
    • 组成单位为:entity
  • 执行动作的集合:recationArr
    • 组成单位为:recation,

recation大体应该具备以下行为:

  • 描述自身运行的周期:startend
  • 执行的动作:action()

那么个体设计好了,围绕个体所展开的逻辑,就顺理成章了。

代码如下:

export default cc.Class({
    extends: cc.Component,

    properties: {

        lenPercent: cc.Float,
        startPercent: cc.Float,
        isAutoStart: cc.Boolean,
        entityArr: {
            default: [],
            type: cc.Node
        }
    },

    //externalDuration:外部时间(父节点传过来的时间),由父节点决定
    //internalDuration:自己内部定的时间,有自己决定,
    //为什么要区分两个呢?由于外部应该只能确定我的播放时间,不应该决定我的播放速率,而后者应该有个体自身决定,
    //startTime和endTime:由父节点指定的开始和结束时间,(根据父节点的世界定的‘外部时间’!!!)
    //timeLine-表示时间到哪了,(根据父节点的世界定的‘外部时间’!!!)
    //totaTime-表示我在父节点应该播放的总时长,(根据父节点的世界定的‘外部时间’!!!)
    //progressValue就是通过父节点传过来的timeLine,totaTime,timeLine得出我处于的播放进度百分比
    //相应的往自己的子节点传的就得参照自己的
    ctor() {
        this.isLive = false;
        this.startTime = undefined;
        this.endTime = undefined;
        this.internalDuration = 0;//个体内部的时长
        this.externalDuration = 0;//个体相对父级的时长
        this.progressValue = 0;
        this.entryData = [];
        this.recationArr = [];
        this.startPosition = cc.v2();
        this.entityArrEx = [];
    },

    // LIFE-CYCLE CALLBACKS:
    start() {
        this.startPosition = this.node.position;
    },
    onLoad() {
        this.node.comName = this.__classname__;
        this.internalDuration = this.node.getContentSize().height;
        //防止设置的时间太长,强制设置为剩余的时长
        if (this.lenPercent + this.startPercent > 1) {
            this.lenPercent = 1 - this.startPercent;
        }
        if (this.isAutoStart) {
            this.startPercent += Math.abs((this.node.position.y / this.node.parent.getContentSize().height));
        }

    },

    onEnable() {
        let self = this;
        if (this.entityArr.length) {
            this.entityArrEx = this.entityArr.map((item, index) => {
                let entity = item.getComponent(item._name);
                if (entity.isAutoStart) {

                }
                this.entryData.push(entity.initData({
                    startTime: this.getStarTime(entity.startPercent),
                    totaTime: self.internalDuration,
                }));
                return entity;
            });
        }
    },

    //业务接口
    getStarTime(value) {
        if (value <= 1) {
            return value * this.internalDuration
        } else {
            return value
        }
    },

    initData({ totaTime, startTime }) {
        this.startTime = startTime;
        this.externalDuration = this.lenPercent <= 1 ? totaTime * this.lenPercent : this.lenPercent;
        //结束时间最大只能是父类节点结束时间
        //因为父节点结束,子节点也必须结束
        this.endTime = Math.min(totaTime, this.startTime + this.externalDuration);
        return {
            startTime: this.startTime,
            internalDuration: this.internalDuration,
            endTime: this.endTime
        }
    },

    getCurrentTime(percent) {
        return (
            this.startTime + (percent <= 1 ? this.externalDuration * percent : percent)
        );
    },

    live() {
        this.isLive = true;
    },

    calcProgress() {
        this.progressValue = (this.timeLine - this.startTime) / this.externalDuration;
    },

    calcReactionProgress({ start, end }) {
        start = (start <= 1) ? this.internalDuration * start : start;
        end = (end <= 1) ? this.internalDuration * end : end;
        return Math.min((this.progressValue * this.internalDuration - start) / (end - start), 1);
    },

    process({ timeLine }) {
        this.timeLine = timeLine;
        this.calcProgress();
        this.internalTimeLine = this.progressValue * this.internalDuration;
        let actionArr = this.recationArr.filter((item) => {
            if (item) {
                let isOk = (timeLine > this.getCurrentTime(item.start) &&
                    timeLine <= this.getCurrentTime(item.end)) ||
                    (!item.start && !item.end)
                if (isOk) {
                    item.isAction = true
                } else {
                    if (item.isAction) {
                        item.action(this.calcActionData(item, true))
                    }
                    item.isAction = false
                }
                return isOk;
            }
        });
        actionArr.forEach((item) => {
            item.action(this.calcActionData(item));
        });
    },

    update() {
        let self = this;
        this.actionEntityArr = this.entityArrEx.filter((entity) => {
            if ((self.internalTimeLine) > entity.startTime && self.internalTimeLine <= entity.endTime) {
                if (!entity.isLive) {
                    entity.live();
                }
                entity.process({
                    timeLine: self.progressValue * self.internalDuration,
                });
                return true;
            } else {
                if (entity.isLive) {
                    entity.end();
                }
            }
            return false;
        });
    },

    calcActionData(item, isEnd) {
        let params = {};
        let actionLen = (item.end - item.start) || 1;
        let progress;
        progress = Math.min((this.progressValue - item.start) / actionLen, 1);
        if (isEnd) {
            let isEndForce = window.GLOBAL.dir > 0;
            let isEndForceStart = window.GLOBAL.dir < 0;
            if (isEndForce) {
                progress = 1
            } else if (isEndForceStart) {
                progress = 0
            }
            params = {
                isEndForce: isEndForce,
                isEndForceStart: isEndForceStart
            }
        }
        params = {
            actionLen,
            progress,
            ...params,
            ...item
        }

        return params;
    },
    end() {
        this.isLive = false;
        //如果滑动非常快,并且是快进而非后退,那么就要直接强行设置反馈为结束
        // if (window.GLOBAL.dir > 0) {

        // }
        this.recationArr.forEach(item => {
            if (item.isAction) {
                item.isAction = false
                item.action(this.calcActionData(item, true))
            }
        });
    },
});

看下效果:

这是然哥的做的动画

我的版本(做的时候才发现这俩是一体的)

我的意外收获

就在我开始着手做然叔第三个动画,摸索箭头如何实现的时候,机缘巧合地我有了另一个新的灵感,我做出了这个。

“Moderate”新首页。

感谢然叔。

结语

这是我为“Moderate”写专栏的第十篇了,很有成就感,虽然不受欢迎,但整个过程我是快乐的。我遇见了好多好多热情的,才华的,可敬的coder和掘友,收获良多。最后由衷地感谢掘金营造的社区氛围,感叹相见恨晚,但一见如故,自强不息,未来可期。