vue球形词云旋转的实现,基于这篇文章 https://blog.csdn.net/weixin_43951323/article/details/120903651
如果不需要连线,可到这篇文章中拷贝即可
若需要各词云之间的连线,我增加了一些相关逻辑
- 固定容器的大小,此处为600*600
- 使用svg来画线(line),只需要找出每条线段的两个点坐标
- 优化 this.radius 的计算,保证每个元素的 left 和 top 值都为正(方便线条的点定位),由2修改为2.5或更大
- 优化定时器为 requestAnimationFrame,减少重排重绘
- 画线时通过文档碎片,避免频繁的 Dom 操作
效果如下
以下是完整代码
<template>
<section class="cloud-bed">
<div class="cloud-box">
<span
v-for="(item, index) in dataList"
:key="index"
@click="getDataInfo(item)"
>
{{ item.name }}
</span>
<!-- svg容器 -->
<svg width="600" height="600" xmlns="http://www.w3.org/2000/svg" version="1.1"></svg>
</div>
</section>
</template>
<script>
export default {
name: "word-cloud",
data() {
return {
timer: 30, // 球体转动速率
radius: 0, // 词云球体面积大小
dtr: Math.PI/180, //鼠标滑过球体转动速度
active: false, // 默认加载是否开启转动
lasta: 0, // 上下转动
lastb: 0.5, // 左右转动
distr: true,
tspeed: 0, // 鼠标移动上去时球体转动
mouseX: 0,
mouseY: 0,
tagAttrList: [],
tagContent: null,
cloudContent: null,
sinA: '',
cosA: '',
sinB: '',
cosB: '',
sinC: '',
cosC: '',
dataList: [
{
name: '测试1',
value: '1'
},
{
name: '测试2',
value: '2'
},
{
name: '测试3',
value: '3'
},
{
name: '测试4',
value: '4'
},
{
name: '测试5',
value: '5'
},
{
name: '测试6',
value: '6'
},
{
name: '测试7',
value: '7'
},
{
name: '测试8',
value: '8'
},
{
name: '测试9',
value: '9'
},
{
name: '测试10',
value: '10'
},
],
// animationId
animationId: undefined,
// 如果需要控制requestAnimationFrame的频率,需要自己定义两个变量用于比较
nowTime: 0,
lastTime: 0
}
},
mounted () {
this.$nextTick(() => {
this.initWordCloud()
})
},
beforeDestroy () {
// clearInterval(this.timer)
// 销毁动画
this.animationDestroy()
},
methods:{
// 获取点击文本信息
getDataInfo (item) {
console.log(item, 'item')
},
initWordCloud () {
// 销毁动画(list变化时可再次调用该事件,用于刷新动画)
this.animationDestroy()
this.cloudContent = document.querySelector('.cloud-box');
this.radius = this.cloudContent && this.cloudContent.offsetWidth / 2.5
this.tagContent = this.cloudContent.getElementsByTagName('span');
for (let i = 0; i < this.tagContent.length; i++) {
let tagObj = {};
tagObj.offsetWidth = this.tagContent[i].offsetWidth;
tagObj.offsetHeight = this.tagContent[i].offsetHeight;
this.tagAttrList.push(tagObj);
}
this.sineCosine(0, 0, 0);
this.positionAll();
this.cloudContent.onmouseover = () => {
this.active=true;
};
this.cloudContent.onmouseout = () => {
this.active=false;
};
this.cloudContent.onmousemove = (ev) => {
let oEvent = window.event || ev;
this.mouseX = oEvent.clientX - (this.cloudContent.offsetLeft + this.cloudContent.offsetWidth/2);
this.mouseY = oEvent.clientY - (this.cloudContent.offsetTop + this.cloudContent.offsetHeight/2);
this.mouseX/= 5;
this.mouseY/= 5;
};
// setInterval(this.update, this.timer);
this.animationUpdate()
},
// 通过 requestAnimationFrame 执行动画
animationUpdate () {
this.nowTime = Date.now()
if (this.nowTime - this.lastTime > 10) {
this.lastTime = this.nowTime
this.update()
}
this.animationId = window.requestAnimationFrame(this.animationUpdate)
},
// 销毁动画,状态重置
animationDestroy () {
this.animationId && window.cancelAnimationFrame(this.animationId)
this.animationId = null
// 所有与构建动画相关的变量重置
this.timer = 40
this.radius = 50
this.dtr = Math.PI / 180
this.active = false
this.lasta = 0
this.lastb = 0.5
this.distr = true
this.tspeed = 0
this.mouseX = 0
this.mouseY = 0
this.tagAttrList = []
this.tagContent = null
this.cloudContent = null
this.sinA = ''
this.cosA = ''
this.sinB = ''
this.cosB = ''
this.sinC = ''
this.cosC = ''
this.nowTime = 0
this.lastTime = 0
},
positionAll () {
let phi = 0;
let theta = 0;
let max = this.tagAttrList.length;
let aTmp = [];
let oFragment = document.createDocumentFragment();
//随机排序
for (let i=0; i < this.tagContent.length; i++) {
aTmp.push(this.tagContent[i]);
}
aTmp.sort(() => {
return Math.random() < 0.5 ? 1 : -1;
});
for (let i = 0; i < aTmp.length; i++) {
oFragment.appendChild(aTmp[i]);
}
this.cloudContent.appendChild(oFragment);
for(let i = 1; i < max + 1; i++){
if (this.distr) {
phi = Math.acos(-1 + (2 * i - 1) / max);
theta = Math.sqrt(max * Math.PI) * phi;
} else {
phi = Math.random() * (Math.PI);
theta = Math.random() * (2 * Math.PI);
}
//坐标变换
this.tagAttrList[i-1].cx = this.radius * Math.cos(theta) * Math.sin(phi);
this.tagAttrList[i-1].cy = this.radius * Math.sin(theta) * Math.sin(phi);
this.tagAttrList[i-1].cz = this.radius * Math.cos(phi);
this.tagContent[i-1].style.left = this.tagAttrList[i-1].cx + this.cloudContent.offsetWidth / 2 - this.tagAttrList[i-1].offsetWidth / 2 + 'px';
this.tagContent[i-1].style.top = this.tagAttrList[i-1].cy + this.cloudContent.offsetHeight / 2 - this.tagAttrList[i-1].offsetHeight / 2 + 'px';
}
},
update () {
let angleBasicA;
let angleBasicB;
if (this.active) {
angleBasicA = (-Math.min(Math.max(-this.mouseY, -200 ), 200) / this.radius) * this.tspeed;
angleBasicB = (Math.min(Math.max(-this.mouseX, -200 ), 200) / this.radius) * this.tspeed;
} else {
angleBasicA = this.lasta * 0.98;
angleBasicB = this.lastb * 0.98;
}
//默认转动是后是否需要停下
// lasta=a;
// lastb=b;
// if(Math.abs(a)<=0.01 && Math.abs(b)<=0.01)
// {
// return;
// }
this.sineCosine(angleBasicA, angleBasicB, 0);
for(let j = 0; j < this.tagAttrList.length; j++) {
let rx1 = this.tagAttrList[j].cx;
let ry1 = this.tagAttrList[j].cy * this.cosA + this.tagAttrList[j].cz * (-this.sinA);
let rz1 = this.tagAttrList[j].cy * this.sinA + this.tagAttrList[j].cz * this.cosA;
let rx2 = rx1 * this.cosB + rz1 * this.sinB;
let ry2 = ry1;
let rz2 = rx1 * (-this.sinB) + rz1 * this.cosB;
let rx3 = rx2 * this.cosC + ry2 * (-this.sinC);
let ry3 = rx2 * this.sinC + ry2 * this.cosC;
let rz3 = rz2;
this.tagAttrList[j].cx = rx3;
this.tagAttrList[j].cy = ry3;
this.tagAttrList[j].cz = rz3;
let per = 350 / (350 + rz3);
this.tagAttrList[j].x = rx3 * per - 2;
this.tagAttrList[j].y = ry3 * per;
this.tagAttrList[j].scale = per;
this.tagAttrList[j].alpha = per;
this.tagAttrList[j].alpha = (this.tagAttrList[j].alpha - 0.6) * (10/6);
}
this.doPosition();
this.depthSort();
this.setLinePosition()
},
doPosition() {
let len = this.cloudContent.offsetWidth/2;
let height = this.cloudContent.offsetHeight/2;
for (let i=0;i < this.tagAttrList.length;i++) {
this.tagContent[i].style.left = this.tagAttrList[i].cx + len - this.tagAttrList[i].offsetWidth/2 + 'px';
this.tagContent[i].style.top = this.tagAttrList[i].cy + height - this.tagAttrList[i].offsetHeight/2 + 'px';
this.tagContent[i].style.fontSize = Math.ceil(12 * this.tagAttrList[i].scale/2) + 8 + 'px';
this.tagContent[i].style.filter = "alpha(opacity="+100 * this.tagAttrList[i].alpha+")";
this.tagContent[i].style.opacity = this.tagAttrList[i].alpha;
}
},
depthSort(){
let aTmp = [];
for (let i = 0; i < this.tagContent.length; i++) {
aTmp.push(this.tagContent[i]);
}
aTmp.sort((item1, item2) => item2.cz - item1.cz);
for (let i = 0; i < aTmp.length; i++) {
aTmp[i].style.zIndex=i;
}
},
// 计算线的位置
setLinePosition () {
// 获取svg
const oSvg = document.getElementsByTagName('svg')[0]
// 清空svg
oSvg.innerHTML = ''
// 创建文档碎片
let oFrag = document.createDocumentFragment()
// 属性列表(取坐标准备)
let attrList = this.tagContent
// 节点个数
const len = attrList.length
// 外部控制循环次数
for(let i = 0; i < len - 1; i++) {
// 内部控制每次循环链接的次数
for (let j = 0; j < len - i - 1; j++) {
// 创建线条
let oLine = document.createElementNS("http://www.w3.org/2000/svg", 'line')
// 第一个点
oLine.setAttribute('x1', attrList[i].style.left)
oLine.setAttribute('y1', attrList[i].style.top)
// 第二个点
oLine.setAttribute('x2', attrList[len - j - 1].style.left)
oLine.setAttribute('y2', attrList[len - j - 1].style.top)
// 设置线条颜色
oLine.setAttribute('stroke','#bbb')
// 文档碎片插入线条
oFrag.appendChild(oLine)
}
}
// 插入svg中
oSvg.appendChild(oFrag)
},
sineCosine (a, b, c) {
this.sinA = Math.sin(a * this.dtr);
this.cosA = Math.cos(a * this.dtr);
this.sinB = Math.sin(b * this.dtr);
this.cosB = Math.cos(b * this.dtr);
this.sinC = Math.sin(c * this.dtr);
this.cosC = Math.cos(c * this.dtr);
}
}
};
</script>
<style scoped lang="scss">
.cloud-bed {
width: 600px;
height: 600px;
margin: auto;
.cloud-box{
position:relative;
margin:20px auto 0px;
width: 100%;
height: 100%;
background: #00000000;
span{
position: absolute;
padding: 3px 6px;
top: 0px;
font-weight: bold;
text-decoration:none;
left:0px;
background-image: linear-gradient(to bottom, red, #fff);
background-clip: text;
color: transparent;
}
}
}
</style>