产品经理:你能不能让词云动起来?

15,967 阅读6分钟

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

☀️ 前言

  • 事情是这样的,前段时间拿到公司的数据大屏原型图让我一天内把一整个页面做出来。
  • 简单看了看,就是一个3840 * 1840的大屏然后几个列表几个图例看起来也没有多复杂。
  • 唰!很快啊加了一会班把整个页面调整好了信心十足拿给产品经理看。
  • 产品经理皱了皱眉头:你这词云不会动啊??

🌤️ 之前的效果

  • 听到这话我发现情况不对,我寻思着这原型图的词云也看不出他要没要求我动啊,而且明明我做的是会动的呀!

🎢 关系图

  • 一开始我用的是echartsgraph关系图,这种图的特点是一开始会因为每个词的斥力会互相分开,在一开始会有一些动态效果,但是因为力引导布局会在多次迭代后才会稳定,所以到后面就不会继续运动了。

ciyun1.gif

  • 我:是吧我没骗人吧?确实是会动的。
  • 产品经理:这样效果不好,没有科技感,而且我要字体大小每个都不同的,明天要拿给客户看一版,比较急,算了你别做动的了就让他词云填满然后每个词的大小要不一样。

WPS图片编辑.png

🎠 词云图

  • 做不动词云的那不就简单了,直接使用echartswordCloud图啊,直接唰唰配置一下就好了。

image.png

  • 产品经理:客户看完了,整体还不错,但是词云这块我还是想它动起来,这样吧,你想个办法整整。

src=http___5b0988e595225.cdn.sohucs.com_images_20181108_0b031f4213f4403ca4cfca30c2b369ca.jpeg&refer=http___5b0988e595225.cdn.sohucs.jpg

🚄 自己手写

  • 对于这个词云,我一开始真的是死脑筋了,认定要用echarts来做,但实际上wordCloud官网也没有提供资料了,好像确实也没有办法让它动起来。
  • 思量片刻....等会,词云要不同大小不同颜色然后要在区域内随机移动,既然我不熟canvas,那我是不是可以用jscss来写一个2d的呢,说白了就是一个词语在一个容器内随机运动然后每个词语都动起来撒,好像能行....开干。

🚅 ToDoList

  • 准备容器和需要的配置项
  • 生成所有静态词云
  • 让词云动起来

🚈 Just Do It

  • 由于我这边的技术栈是vue 2.x的所以接下来会用vue 2.x的语法来分享,但实际上换成原生js也没有什么难度,相信大家可以接受的。

🚎 准备容器和需要的配置项

  • 首先建立一个容器来包裹我们要装的词云,我们接下来的所有操作都围绕这个容器进行。
<template>
  <div class="wordCloud" ref="wordCloud">
  </div>
</template>

image.png

  • 因为我们的词云需要有不同的颜色我们需要实现准备一个词语列表和颜色列表,再准备一个空数组来存储之后生成的词语。
...
data () {
    return {
            hotWord: ['万事如意', '事事如意 ', '万事亨通', '一帆风顺', '万事大吉', '吉祥如意', '步步高升', '步步登高', '三羊开泰', '得心应手', '财源广进', '陶未媲美', '阖家安康', '龙马精神'],
            color: [
                    '#a18cd1', '#fad0c4', '#ff8177',
                    '#fecfef', '#fda085', '#f5576c',
                    '#330867', '#30cfd0', '#38f9d7'
            ],
            wordArr: []
    };
}
...
  • 准备的这些词语都是想对现在在读文章的你说的~如果觉得我说得对的不妨读完文章后给一个 ~
  • 好了不开玩笑,现在准备工作完成了,开始生成我们的词云。

🚒 生成所有静态词云

  • 我们如果想让一个容器里面充满词语,按照正常我们切图的逻辑来说,每个词语占一个span,那么就相当于一个div里面有n(hotWord数量)个词语,也就是容器里面有对应数量的span标签即可。
  • 如果需要不同的颜色和大小,再分别对span标签分别加不同样式即可。
...
mounted () {
        this.init();
},
methods: {
        init () {
            this.dealSpan();
        },
        dealSpan () {
            const wordArr = [];
            this.hotWord.forEach((value) => {
                    // 根据词云数量生成span数量设置字体颜色和大小
                    const spanDom = document.createElement('span');
                    spanDom.style.position = 'relative';
                    spanDom.style.display = "inline-block";
                    spanDom.style.color = this.randomColor();
                    spanDom.style.fontSize = this.randomNumber(15, 25) + 'px';
                    spanDom.innerHTML = value;
                    this.$refs.wordCloud.appendChild(spanDom);
                    wordArr.push(spanDom);
            });
            this.wordArr = wordArr;
        },
        randomColor () {
            // 获取随机颜色
            var colorIndex = Math.floor(this.color.length * Math.random());
            return this.color[colorIndex];
        },
        randomNumber (lowerInteger, upperInteger) {
            // 获得一个包含最小值和最大值之间的随机数。
            const choices = upperInteger - lowerInteger + 1;
            return Math.floor(Math.random() * choices + lowerInteger);
        }
}
...
  • 我们对hotWord热词列表进行遍历,每当有一个词语就生成一个span标签,分别使用randomColor()randomSize()设置不同的随机颜色和大小。
  • 最后再将这些span都依次加入div容器中,那么完成后是这样的。

image.png

🚓 让词云动起来

  • 词语是添加完了,接下来我们需要让他们动起来,那么该怎么动呢,我们自然而然会想到transformtranslateXtranslateY属性,我们首先要让一个词语先动起来,接下来所有的都应用这种方式就可以了。
先动一下x轴
  • 怎么动呢?我们现在要做的是一件无限循环的事情,就是一个元素无限的移动,既然是无限,在js中用定时器可不可以实现呢?确实是可以的,但是会巨卡,万一词语一多你的电脑会爆炸,在另一方面编写动画循环的关键是要知道延迟时间多长合适,如果太长或者太短都不合适所以不用定时器。
  • 然后一不小心发现了window.requestAnimationFrame这个APIrequestAnimationFrame不需要设置时间间隔。

requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。

  • 也就是说当我们循环无限的让一个元素在x轴或者y轴移动,假设每秒向右移动10px那么它的translateX就是累加10px,每个元素都是如此那么我们需要给span元素新增一个属性来代表它的位置。
data () {
    return {
        ...
        timer: null,
        resetTime: 0
        ...
    };
}
methods: {
    init () {
            this.dealSpan();
            this.render();
    },
    dealSpan () {
            const wordArr = [];
            this.hotWord.forEach((value) => {
                    ...
                    spanDom.local = {
                            position: {
                                    x: 0,
                                    y: 0
                            }
                    };
                    ...
            });
            this.wordArr = wordArr;
    },
    render () {
            if (this.resetTime < 100) {
                    //防止“栈溢出”
                    this.resetTime = this.resetTime + 1;
                    this.timer = requestAnimationFrame(this.render.bind(this));
                    this.resetTime = 0;
            }
            this.wordFly();
    },
    wordFly () {
            this.wordArr.forEach((value) => {
                    //每次循环加1
                    value.local.position.x += 1;
                    // 给每个词云加动画过渡
                    value.style.transform = 'translateX(' + value.local.position.x + 'px)';
            });
    },
},
destroyed () {
        // 组件销毁,关闭定时执行
        cancelAnimationFrame(this.timer);
},
  • 这时候我们给每个元素加了个local属性里面有它的初始位置,每当我们执行一次requestAnimationFrame的时候它的初始位置+1,再把这个值给到translateX这样我们每次循环都相当于移动了1px,现在我们来看看效果。

ciyun2.gif

调整范围
  • 嘿!好家伙,动是动起来了,但是怎么还过头了呢?
  • 我们发现每次translateX+1了但是没有给一个停止的范围给他,所以我们需要给一个让他到容器的边缘就开始掉头的步骤。
  • 那怎么样让他掉头呢?既然我们可以让他每次往右移动1px那么我们是不是可以检测到当它的x轴位置大于这个容器的位置时x轴位置小于这个容器的位置时并且换个方向就好换个方向我们只需要用正负数来判断即可。
init () {
        this.dealSpan();
        this.initWordPos();
        this.render();
},
dealSpan () {
        const wordArr = [];
        this.hotWord.forEach((value) => {
            ...
            spanDom.local = {
                    position: {
                            // 位置
                            x: 0,
                            y: 0
                    },
                    direction: {
                            // 方向 正数往右 负数往左
                            x: 1,
                            y: 1
                    }
            };
            ...
        });
        this.wordArr = wordArr;
},
wordFly () {
        this.wordArr.forEach((value) => {
            // 设置运动方向 大于边界或者小于边界的时候换方向
            if (value.local.realPos.minx + value.local.position.x < this.ContainerSize.leftPos.x) {
                    value.local.direction.x = -value.local.direction.x;
            }
            if (value.local.realPos.maxx + value.local.position.x > this.ContainerSize.rightPos.x) {
                    value.local.direction.x = -value.local.direction.x;
            }
            //每次右移1个单位 当方向为负数时就是-1个单位也就是向左移1个单位
            value.local.position.x += 1 * value.local.direction.x;
            // 给每个词云加动画过渡
            value.style.transform = 'translateX(' + value.local.position.x + 'px)';
        });
},
initWordPos () {
        // 计算每个词的真实位置和容器的位置
        this.wordArr.forEach((value) => {
            value.local.realPos = {
                    minx: value.offsetLeft,
                    maxx: value.offsetLeft + value.offsetWidth
            };
        });
        this.ContainerSize = this.getContainerSize();
},
getContainerSize () {
        // 判断容器大小控制词云位置
        const el = this.$refs.wordCloud;
        return {
            leftPos: {
                    // 容器左侧的位置和顶部位置
                    x: el.offsetLeft,
                    y: el.offsetTop
            },
            rightPos: {
                    // 容器右侧的位置和底部位置
                    x: el.offsetLeft + el.offsetWidth,
                    y: el.offsetTop + el.offsetHeight
            }
        };
}
  • 我们一开始先用initWordPos来计算每个词语现在处于的位置并把它的位置保存起来,再使用getContainerSize获取我们的外部容器的最左侧最右侧最上最下的位置保存起来。
  • 给我们每个span添加一个属性direction 方向,当方向为负数则往左,方向为正则往右,注释我写在代码上了,大家如果不清除可以看一下。
  • 也就是说我们的词云会在容器里面反复横跳,那我们来看看效果。

ciyun3.gif

随机位移
  • 很不错,是我们想要的效果!!!
  • 当然我们每次位移不可能写死只位移1px我们要做到那种凌乱美,那就需要做一个随机位移。
  • 那怎么来做随机位移呢?可以看出我们的词语其实是在做匀速直线运动而匀速直线运动的公式大家还记得吗?
  • 如果不记得的话这边建议回去翻一下物理书~ 匀速直线运动的位移公式是 x=vt
  • 这个x就是我们需要的位移,而这个t我们就不用管了因为我上面也说了这个requestAnimationFrame会帮助我们设置时间,那我们只需要控制这个v初速度是随机的就可以了。
dealSpan () {
        const wordArr = [];
        this.hotWord.forEach((value) => {
            ...
            spanDom.local = {
                    velocity: {
                            // 每次位移初速度
                            x: -0.5 + Math.random(),
                            y: -0.5 + Math.random()
                    },
            };
            ...
        });
        this.wordArr = wordArr;
},
wordFly () {
        this.wordArr.forEach((value) => {
            ...
            //利用公式 x=vt
            value.local.position.x += value.local.velocity.x * value.local.direction.x;
            ...
        });
},
  • 我们给每个词语span元素一个初速度,这个初速度可以为- 也可以为+代表向左或者向右移动,当我们处理这个translateX的时候他就会随机处理了,现在我们来看看效果。

ciyun4.gif

完善y轴
  • 现在x轴已经按照我们想的所完成了,想让词云们上下左右都动起来那么我们需要按照x轴的方法来配一下y轴即可。
  • 由于代码长度问题我就不放出来啦,我下面会给出源码,大家有兴趣可以去下载看看~我们直接来看看成品!小卢感谢您的阅读,那我就在这里祝您ciyun5.gif
  • 至此一个简单的词云动画就完啦,具体源码我放在这里。

👋 写在最后

  • 首先感谢大家看到这里,其实很多时候不同的效果有很多不同的解决方式,不能太死板于一种插件或方法。
  • 前端世界太过奇妙,只有细心的人才能发现其乐趣,希望可以帮到有需要的人。
  • 如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛您的支持就是我更新的最大动力。
  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

🌅 往期精彩

一文搞定echarts地图轮播高亮⚡

看完还分不清重绘和重排过来捶我👊,我说的!!!

如何优雅的使用Vuepress编写组件示例(上)👈

如何优雅的使用Vuepress编写组件示例(下)👈

样式命名有多难?浅谈BEM命名规范⚡