疫情可视化part3
前言
此系列已完结,共3部分:
之前在part2中说的添加自定义主题配色已经开发完成了,除此之外我还添加了一些的3d特效。
成果链接
- 浏览链接:http://xisite.top/original/bigScreen/28/index.html#/
- 项目链接:https://gitee.com/xi1213/covid19-visualization
具体效果
最后出来的效果还是蛮炫的。添加与修改
- 设置中添加主题定制,预设3种主题。
- 主题定制支持取色器取色。
- 添加3d球体的环形动态特效。
主题预设
由于项目一开始就使用的dataV,并且有计划的提取出了颜色值,所以定制主题便不是很麻烦,直接提取改变dataV的color属性即可,其他部分直接使用vue的动态样式绑定。设置配置
先在设置组件SetDrawer中预先设置好需要使用的配置信息,包括预设的主题,然后使用vue3的onBeforeMount在挂载组件之前将包含颜色的配置信息用sessionStorage.setItem(“config”, JSON.stringify(setData.value))先存储到浏览器本地,这样可以防止刷新页面时配置信息丢失重置。
值得注意的是,需要在存入配置信息前判断缓存中是否已经存在配置信息,若有,则直接使用缓存配置,若没有则存入预先在代码中写好配置。//系统配置 function sysConfig() { setData.value.sysVer = PK.version//获取系统版本号 process.env.NODE_ENV == "development" ? setData.value.dataType = dataTypeList[0] ://开发环境使用离线数据 setData.value.dataType = dataTypeList[1];//生产环境使用在线数据 let ss = sessionStorage.getItem("config");//获取缓存配置 //缓存中有配置取出配置,无则使用初始配置 if (ss) { let cuVer = setData.value.sysVer,//当前版本号 ssVer = JSON.parse(ss).sysVer,//缓存版本号 isUpDate = null;//是否更新缓存 //当前版本号与缓存版本号若不等清除缓存使用当前配置,否则使用缓存配置 (cuVer !== ssVer) ? (isUpDate = true) : isUpDate = false; isUpDate ? ( (sessionStorage.removeItem("config")),//清除缓存 (sessionStorage.setItem("config", JSON.stringify(setData.value)))//设置缓存配置 ) : (setData.value = JSON.parse(ss)); } else { sessionStorage.setItem("config", JSON.stringify(setData.value));//设置缓存配置 } };
加载配置
在页面加载瞬间就需要获取颜色值的组件中使用onMounted与JSON.parse(sessionStorage.getItem(“config”) )获取到上一步在缓存中存下的配置信息,从配置信息中获取到颜色值,最后在利用该值将dom渲染出来,dataV的部分支持使用:color=”[‘#fff’, ‘#aaa’]”来动态改变颜色,其中数组中第一个值为主色,第二个为副色; 非dataV的部分直接使用vue动态绑定样式的语法:style=“{color:#fff}”来修改配色。
切换配置
切换主题时直接在设置组件中改变当前使用的配色值然后刷新页面即可,因为我在切换3d球体颜色时偷了一个懒,正常流程是获取球体mesh,改变材质的color值,然后更新的。我直接刷新了浏览器(又不是不能用(*  ̄︿ ̄)),重建了场景、相机、球体等。
预设主题
我预设了三对主色、副色的颜色值。为了看起来更和谐,建议使用同一颜色的深色与浅色来搭配。
v-for即可渲染出切换按钮。
取色器
原生input实现
我也是在无意中发现input的type居然是支持color的。可以直接原生实现取色器,这样就可以用颜色吸管获取屏幕中的任何颜色了:
<input id="colorInp" style="height: 0px;opacity: 0;width: 0px;margin: 0px;padding: 0px;" type="color" />
绑定事件
要使用自己的按钮点击打开取色器,可以直接将input的高宽赋为0,不透明度赋为0,将其隐藏后,再使用按钮绑定事件打开取色器即可:
<el-button class="main-color" :color="setData.sysColor[0]" @click="changeColor(0)">主色</el-button>
function changeColor(type: Number) {
(document.getElementById("colorInp") as any).click();//手动点击取色器
colorType = type;//改变颜色类型
};
环形特效
//创建环
function createRings() {
createEquatorSolidRing(earthSize + 20);//创建赤道实线环
createEquatorFlyline(earthSize + 30);//创建赤道飞线环
createEquatorDottedLineRing(earthSize + 35);//创建赤道虚线环
createSpikes(earthSize + 40);//创建赤道尖刺
createUpDownRing(earthSize - 50, earthSize - 40);//创建南北极环
createExpandRing();//创建爆炸环
createSphereGlow();//创建球体光晕
};
接下来我们详细分析一下。
赤道实线环
- 该环使用RingGeometry几何体实现,参数分别为:内半径、外半径、分段数。
- 刚创建出来的环形是平行于屏幕的,需要改变环mesh的rotation属性,绕x轴旋转90度即可。
//创建赤道实线环 function createEquatorSolidRing(r: any) { //创建里层的环 let ringGeometry = new THREE.RingGeometry(r - 2, r + 2, 100); let ringMaterial = new THREE.MeshBasicMaterial({ color: dvColor.value[0], opacity: .3, side: THREE.DoubleSide, fog: true, depthWrite: false, blending: THREE.AdditiveBlending, }); let ringMesh = new THREE.Mesh(ringGeometry, ringMaterial); ringMesh.rotation.x = 90 * Math.PI / 180; earthGroup.add(ringMesh); };
赤道飞线环
即为赤道第二靠近球体的环。 - 仔细观察会发现,它其实并不是一个环,而是一条弧线再在环绕球体运动。
- 实现该弧线的时候发现,webGL的线条由于渲染器的限制,并不能设置宽度,但可以通过three.meshline(这是一个three插件,three不自带,需要npm i three.meshline安装)中的MeshLineMaterial来实现宽度的设置,修改MeshLineMaterial中的lineWidth属性值即可。
- dashArray为弧线段数量的倒数(0.5即为2条,0.3即为3条),dashRatio为线段的不可见部分与可见部分的比例。
- 几何体使用了BufferGeometry,设置setFromPoint,提取THREE.Path绘制出的arc的数据,改变几何体顶点属性即可。
- arc参数依次为:弧线中心x与y值、弧线半径、起始角、终止角、是否顺时针方向创建弧线(默认false)。
- 最后创建完成后同样需要rotation.x改变角度。
//创建赤道飞线 function createEquatorFlyline(r: any) { const geometry = new THREE.BufferGeometry(); const path = new THREE.Path(); path.arc(0, 0, r, 0, Math.PI * 2); const points = path.getPoints(100);//切割段数 geometry.setFromPoints(points); const line = new MeshLine(); // 设置几何体 line.setGeometry(geometry) const material = new MeshLineMaterial({ color: dvColor.value[0], lineWidth: 1, // 线条的宽度 dashArray: .5, // 该数值倒数为线段数量 dashRatio: .5, // 不可见与可见比例 transparent: true, // 设置透明度 }) flylineMesh = new THREE.Mesh(line.geometry, material); flylineMesh.rotation.x = 90 * Math.PI / 180; earthGroup.add(flylineMesh); };
赤道虚线环
即为赤道第三靠近球体的白色虚线环。 - 该环是由50个小白点组成,几何体使用了BufferGeometry,材质使用了PointsMaterial,组使用了Points。
- 几何体中需要使用Math.cos与sin改变点单位向量的xyz值,然后将位置列表positions使用Float32BufferAttribute(值得注意的是Float32Attribute已被删除弃用)设置position属性至ringPointGeometry中。
- 材质中记得设置transparent: false,与size尺寸。
- 最后将几何体与材质添加到点的组中,将点组添加到球体组中即可。
//创建赤道虚线环 function createEquatorDottedLineRing(r: any) { const positions = []; let ringPointGeometry = new THREE.BufferGeometry(); //环形点几何体 let pointNum = 50;//点的数量 let ringPointAngle = (2 * Math.PI) / pointNum; //环形点角度 for (let o = 0; o < 500; o++) { let n = new THREE.Vector3(); //点的向量 n.x = r * Math.cos(ringPointAngle * o); //计算点的角度 n.y = 0; n.z = r * Math.sin(ringPointAngle * o); positions.push(n.x, n.y, n.z); } ringPointGeometry.setAttribute( "position", new THREE.Float32BufferAttribute(positions, 3) );//设置位置属性 let ringPointMaterial = new THREE.PointsMaterial({ //环形点材质 size: 3, // color: dvColor.value[0], transparent: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false, }); dotLineRingMesh = new THREE.Points( ringPointGeometry, ringPointMaterial ); dotLineRingMesh.name = "赤道虚线"; earthGroup.add(dotLineRingMesh); };
赤道尖刺环
即为赤道环中类似钟表刻度的环形。 - 该环使用LineBasicMaterial材质、BufferGeometry几何体、LineSegments组。
- 材质中由于webGL限制不能使用linewidth,始终宽度为1。
- 几何体顶点的处理类似创建虚线环,循环改变每个尖刺的xyz向量即可。
- 若要改变指定尖刺的长度只需使用multiplyScalar向量与标量相乘。
- 最后将材质与几何体添加到组中即可。
//创建赤道尖刺 function createSpikes(spikeRadius: any) { let spikesVerticesArray = []; let spikesObject = new THREE.Group(); //创建尖刺的组 spikesObject.name = "赤道尖刺"; earthGroup.add(spikesObject); //将尖刺组添加到旋转组中 //创建尖刺 let spikeNum = 400;//尖刺数量 let o = (2 * Math.PI) / spikeNum; for (let s = 0; s < spikeNum; s++) { let r = new THREE.Vector3(); r.x = spikeRadius * Math.cos(o * s); r.y = 0; r.z = spikeRadius * Math.sin(o * s); r.normalize();//归一化,将该向量转化为向量单位 r.multiplyScalar(spikeRadius); let i = r.clone(); //克隆r至i (s % 10 == 1) ? i.multiplyScalar(1.1) : i.multiplyScalar(1.05);//每10个计算一次向量与标量相乘 spikesVerticesArray.push(r); //将向量存入尖刺顶点列表 spikesVerticesArray.push(i); } let n = new Float32Array(3 * spikesVerticesArray.length); //创建顶点数组 for (let s = 0; s < spikesVerticesArray.length; s++) { n[3 * s] = spikesVerticesArray[s].x;//给顶点数组设置坐标 n[3 * s + 1] = spikesVerticesArray[s].y; n[3 * s + 2] = spikesVerticesArray[s].z; } //尖刺材质 let spikesMaterial = new THREE.LineBasicMaterial({ // linewidth: 1,//webgl渲染器限制,不能设置宽度,始终为1(three.meshline插件可解决) // color: "#fff", color: dvColor.value[0], transparent: true, opacity: .5 }); let spikesBufferGeometry = new THREE.BufferGeometry(); //创建尖刺几何体 spikesBufferGeometry.setAttribute( "position", new THREE.BufferAttribute(n, 3) ); //添加位置属性 let spikesMesh = new THREE.LineSegments( spikesBufferGeometry, spikesMaterial ); spikesObject.add(spikesMesh); //将网格放进组 };
爆炸环
即为赤道最外面一层环,它会不断的放大渐变,形成类似爆炸冲击波一样的效果。具体原理是这样的。 - 先直接用MeshBasicMaterial材质中的map加载一张透明环形贴图,记得设置transparent、side、depthWrite、blending属性。
- 再使用PlaneGeometry几何体添加一个平面矩形,其具体参数为:矩形宽,矩形高、宽分段数、高分段数。
- 然后将几何体与材质添加到组中,完成后的平面也是平行于屏幕的,记得设置rotation.x的值,使之垂直于屏幕。
- 最后要让环产生动画需要结合gsap(这是最健全的web动画库之一,生成动画十分方便)的fromTo方法。fromTo中第一个参数为产生动画的对象,第二个参数为动画开始状态,第三个参数为动画结束状态(其中包含动画时长duration)。
- 这时你会发现自己的动画只会动一次,其实只需将createExpandRingAnimation添加到requestAnimationFrame动画请求帧使用的方法中使其一直render渲染即可。
//创建渐变环 function createExpandRing() { let ringMaterial = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(ringImg), color: new THREE.Color(dvColor.value[0]),//颜色 transparent: true, opacity: 1, side: THREE.DoubleSide, fog: true, depthWrite: false, blending: THREE.AdditiveBlending, }); let ringGeometry = new THREE.PlaneGeometry(earthSize * 2, earthSize * 2, 10, 10); expandRingMesh = new THREE.Mesh(ringGeometry, ringMaterial); expandRingMesh.name = "放大环"; expandRingMesh.rotation.x = 90 * Math.PI / 180; earthGroup.add(expandRingMesh); }; //创建渐变环动画 function createExpandRingAnimation() { gsap.isTweening(expandRingMesh.scale) ||//环动画 (gsap.fromTo( expandRingMesh.scale,//缩放渐变 { x: 1, y: 1, }, { x: 2.7, y: 2.7, duration: 1.5 } ), gsap.fromTo( expandRingMesh.material,//材质的透明度渐变 { opacity: 1, }, { opacity: 0, duration: 1.5 } )) };
南北极环
如图,即为球体上下的双层环形。其生成方法与赤道实线环一样,不同之处是需要改变position.y值,使之移动到球体南北极。//创建上下环 function createUpDownRing(r1: any, r2: any) { let ringsObject = new THREE.Group(); //创建环的组 ringsObject.name = "南北极环"; earthGroup.add(ringsObject); //将环添加到场景中 //创建内环 let a = new THREE.RingGeometry(r1, r1 - 2, 100); //圆环几何体(内半径,外半径,分段数) let ringsOuterMaterial = new THREE.MeshBasicMaterial({ color: dvColor.value[0], transparent: true, opacity: .3, side: THREE.DoubleSide, fog: true, depthWrite: false, blending: THREE.AdditiveBlending, }); let o = new THREE.Mesh(a, ringsOuterMaterial); o.rotation.x = 90 * Math.PI / 180; //设置旋转 let r = o.clone(); //克隆外环网格o至r o.position.y = 95; //设置位置 r.position.y = -95; ringsObject.add(o); ringsObject.add(r); //创建外环 let t = new THREE.RingGeometry(r2, r2 - 2, 100); let ringsInnerMaterial = new THREE.MeshBasicMaterial({ color: dvColor.value[0], transparent: true, opacity: .3, side: THREE.DoubleSide, fog: true, depthWrite: false, blending: THREE.AdditiveBlending, }); let i = new THREE.Mesh(t, ringsInnerMaterial); i.rotation.x = 90 * Math.PI / 180; let n = i.clone(); i.position.y = 100; n.position.y = -100; ringsObject.add(i); ringsObject.add(n); };
球体光晕
即为球体外部的一层光晕。 - 因为光晕是需要一直平行于屏幕的,所以我们这里直接采用SpriteMaterial材质,将其添加到Sprite中,Sprite精灵的特性就是可以一直正对相机。
- 生成材质时需要添加光晕的透明贴图,同时设置属性:blending、depthWrite、transparent、side。
//创建球体发光环 function createSphereGlow() { //SpriteMaterial材质始终朝向平面 let glowMaterial = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(earthGlowImg), color: new THREE.Color(dvColor.value[0]),//颜色 transparent: true, opacity: 1, side: THREE.DoubleSide, fog: true, depthWrite: false, blending: THREE.AdditiveBlending, }) let glowSprite = new THREE.Sprite(glowMaterial); glowSprite.scale.set(earthSize * 3.2, earthSize * 3.2, 1); //点大小 earthGroup.add(glowSprite); };
结语
成都的12月份好冷啊ヽ(≧□≦)ノ,手指头开始造反不听使唤了,项目到这里差不多该是告一段落了,本项目仅作为我学习webgl与可视化结合使用的一个demo,项目是完全开源了的,有想使用的可以直接在我的gitee上clone,链接在本文开头(不要忘记star啊大哥们!)。