1、需求简述;
2、CSS绘制90度扇形;
3、添加分隔线绘制成一个超声波图样式的声呐图;
4、添加声呐图深水样式的背景;
5、根据声呐探测器返回数据计算鱼的位置;
6、根据声呐探测器返回数据绘制水深标尺;
7、完整代码;
8、知识点学习。
最近做一个项目有这么一个需求,根据声呐探测器返回水下鱼的位置数据,把鱼显示到扇形内,声呐数据包含总水深、n条鱼的水深二维数组(如:[[3452,8569,…],[...],[...],[...],[...]],单位毫米),随机显示到扇形相应的深度处即可。为什么是二维数组呢?因为声呐探测器有5个探头(如图1所示),返回的数据是包含了5个探头的回波数据,回波数据数组里的一个数字就代表一条鱼的深度位置,现要把1、2、3探头的数据展示到声呐图中间那格,4、5号探头数据分别显示到声呐图左右两格。
图1:声呐探头示意图
下面是最终效果图
最终效果图
看到这里的前端同学可以先暂停想想怎么实现哦,看完的同学们可以在评论区自己的方法哦,我也想借鉴一下,我觉得可能用canvas可能会更好。
这里用的是 Vue2+Less。
这是第一个难点,这里我尝试过svg的方式,但是没搞定,最后还是回到css来吧。
首先css是没办法直接画一个扇形的,因为我们的html元素都是四边形的,就算加了圆角,也只能显示个圆形。这里就要用的css高阶一点点的写法了:css clip-path属性。clip-path 用法将在文章底部总结知识点介绍。
html
<section ref="sona" class="maxbox fxbox" style="background: #ccc;">
<div class="sx_cp">
<div class="round1 path"></div>
</div>
</section>
section 标签为一个正方形,用于为扇形可以绝对定位,div class="sx_cp" 为扇形容器,div class="round1 path" 为扇形主体。
css
.fxbox {
display: flex;
align-items: center;
justify-content: center;
}
.sx_cp {
width: 100%;
height: 100%;
position: absolute;
top: 6%;
}
.round1 {
width: 140%;
height: 140%;
position: absolute;
top: -70%; // -140/2
left: -20%; // (100-140)/2
}
.path {
clip-path: polygon(0% 0%, 100% 0%, 50% 50%, 100% 100%);
border-radius: 50%;
background-color: rgba(0, 225, 239, .1);
box-shadow: inset 0 0 40px 1px rgba(1, 240, 255, 0.85);
border: rgba(1, 240, 255, 1) solid 1px;
transform: rotateZ(180deg);
}
这段代码得到一个扇形雏形效果:
扇形雏形效果
有了这个雏形,接下来就明了了,再加3个逐渐缩小的扇形就可以了。
html
<div class="sx_cp">
<div class="round1 path"></div>
<div class="round2 path"></div>
<div class="round3 path"></div>
<div class="round4 path"></div>
</div>
css
.round2 {
width: 100%;
height: 100%;
position: absolute;
top: -50%; // -100/2
left: 0%; // (100-100)/2
z-index: 2;
}
.round3 {
width: 60%;
height: 60%;
position: absolute;
top: -30%;
left: 20%; // (100-60)/2
z-index: 3;
}
.round4 {
width: 20%;
height: 20%;
position: absolute;
top: -10%;
left: 40%; // (100-80)/2
z-index: 3;
}
添加以上代码后,得到效果:
超声波效果
添加两条倾斜的虚线作为分隔线,把扇形分成三格,虚线的倾斜和定位很关键。
html
......
<div class="round4 path"></div>
<div class="line1"></div>
<div class="line2"></div>
css
.line1 {
border-left: rgba(0, 225, 239, .6) dashed 1px;
width: 0;
height: 70%;
position: absolute;
transform: rotateZ(165deg);
top: -1%;
left: 59%;
}
.line2 {
border-left: rgba(0, 225, 239, .7) dashed 1px;
width: 0;
height: 70%;
position: absolute;
transform: rotateZ(195deg);
top: -1%;
left: 41%;
}
添加后得到以下效果:
效果
从上面图可以看出,扇形最大的半径就是最外面那个圆的半径,那就直接给最外面的圆添加一个背景图,就是整个扇形超声波图的背景了。
开始裁剪圆形得到扇形时,使用transform: rotateZ(180deg)翻转了圆形,使扇形朝下,所以现在我们需要用一个水面朝下的图片作为背景图素材。
翻转后的背景图素材
css
.round1 {
background-image: url(./bg.jpg);
background-position: center center;
background-size: 100% 100%;
width: 140%;
height: 140%;
position: absolute;
top: -70%; // -140/2
left: -20%; // (100-140)/2
}
再给section标签添加个好看协调点的背景,得到以下效果:
添加背景后的效果
在vue文件的data模拟探测器回波数据:
data() {
// 回波数据
this.snData = {
depth: 4568,
temperature: 18,
echo_data: [[3299, 3299, 3200, 3399, 3299, 4400, 1230], [3333, 2222],
[1234, 3210], [1234], [3210, 1234, 5000, 5000]]
};
return {
XYArr: [], // {left, top, transform: rotateZ(0deg)}
scale: [], // 参考线刻度
hh: 0, // 水的总深度
temperature: 0, // 水温
count: 0 // 统计数
};
},
下面是计算鱼的位置的关键代码,利用了三角函数的正弦函数sinθ、余弦函数cosθ,对应的js方法:Math.sin(ag * Math.PI / 180)、Math.cos(ag * Math.PI / 180),在vue文件的methods新增方法:
/*
三角函数,获取鱼在屏幕上的显示位置
h鱼的水深度,
grid=1,2,3 鱼属于扇形的分区索引,
第一格角度 = 15~45,第二格= 0~15,第二格区分左右部分,第三格= 15~45,
*/
getXY(h, hh, grid = 2) {
let ag = 0; // 角度
let lr = 'L'; // 第二格随机区分左右, L/R
// 1.根据鱼的grid随机取扇形角度
if (grid === 1) {
ag = Math.floor(Math.random() * 15 + 25);
}
if (grid === 2) {
ag = Math.round(Math.random() * 15);
lr = ag % 2 === 0 ? 'R' : 'L';
}
if (grid === 3) {
ag = Math.floor(Math.random() * 15 + 25);
lr = 'R';
}
// 2.获取扇形相关数据
let sona = this.$refs['sona'];
let L = sona.offsetWidth / 2;
let r = $('.round1').width() / 2;
// 3.计算
let p = hh / r;
let LL = p * L; // 屏幕像素转换为毫米长度
let xy = {};
let x = LL - h * Math.sin(ag * Math.PI / 180);
let psx = x / (LL * 2);
if (lr === 'L') {
xy.left = psx * 100 + '%';
} else {
xy.left = (LL * 2 - x) / (LL * 2) * 100 + '%';
}
let y = h * Math.cos(ag * Math.PI / 180);
xy.top = y / (LL * 2) * 100 + '%';
// 4.随机取鱼图标的旋转角度
let st = -60; let end = 240;
let deg = Math.floor(Math.random() * (st - end) + end);
xy.transform = 'rotateZ(' + deg + 'deg)';
this.XYArr.push(xy);
},
// 生成鱼的位置
createFish() {
this.XYArr = []; // 清空鱼的数据
this.getScale(this.hh, this.snData.depth);
this.snData.echo_data.forEach((item, idx) => {
// idx = 0,1,3 为扇形中间格;2为左边格,4为右边格
let gird = 2;
if (idx === 2) {
gird = 1;
} else if (idx === 4) {
gird = 3;
}
item.forEach(fitem => {
if (fitem > this.hh) fitem = this.hh;
if (fitem < 600) fitem = 600;
this.getXY(fitem, this.hh, gird);
});
});
},
鱼的图标样式:
css
.fsIcon {
position: absolute;
z-index: 5;
left: 0;
top: 0;
width: 26px;
height: 10px;
background: url(./yu.png) no-repeat center center;
background-size: 100% 100%;
margin-top: -2px;
margin-left: -13px;
transform: rotateZ(40deg); // -60~240
opacity: .9;
}
html,for生成鱼图标
.......
<div class="line1"></div>
<div class="line2"></div>
<i v-for="(item, idx) in XYArr" :key="idx" class="fsIcon" :style="item"></i>
得到以下鱼的分布效果:
鱼的分布效果
最后根据探测器返回的总水深,生成参考标尺。
html
<div class="ruler">
<span v-for="item in scale" :key="item">{{ item }}米</span>
</div>
css
.ruler {
width: 0;
height: 70%;
position: absolute;
transform: rotateZ(45deg);
top: -10%;
left: 25%;
span {
white-space: nowrap;
position: absolute;
right: -100%;
z-index: 3;
font-size: 12px;
color: #eee;
&:nth-child(1) {
top: calc(100% - 20px); // 140
}
&:nth-child(2) {
top: 70%;
}
&:nth-child(3) {
top: 40%;
}
&:nth-child(4) {
top: 10%;
}
}
}
JAVAScript
// 扇形参考线刻度
getScale(old_hh, new_hh) {
if (Math.abs(new_hh - old_hh) < 200) return; // 两次深度小于0.x米不作处理
this.hh = new_hh;
this.scale = [];
for (let i = 0; i <= 3; i++) {
let item = ((new_hh - new_hh / 4 * i) / 1000).toFixed(1);
this.scale.push(item);
}
}
最终效果展示
上文的css和js代码是完整的,下面是完整的html:
<div ref="sona" class="maxbox fxbox">
<div class="sx_cp">
<div class="round1 path"></div>
<div class="round2 path"></div>
<div class="round3 path"></div>
<div class="round4 path"></div>
<div class="line1"></div>
<div class="line2"></div>
<div class="ruler">
<span v-for="item in scale" :key="item">{{ item }}米</span>
</div>
<i v-for="(item, idx) in XYArr" :key="idx" class="fsIcon" :style="item"></i>
</div>
</div>
clip-path 属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。可以指定一些特定形状。
语法:clip-source|basic-shape|margin-box|border-box|padding-box|content-box|fill-box|stroke-box|view-box|none|initial|inherit;
属性值 描述
clip-source 用 URL 表示剪切元素的路径
basic-shape 将元素裁剪为基本形状:圆形、椭圆形、多边形或插图
margin-box 使用外边距框作为引用框
border-box 使用边框作为引用框
padding-box 使用内边距框作为引用框
content-box 使用内容框作为引用框
fill-box 使用对象边界框作为引用框
stroke-box 使用笔触边界框(stroke bounding box)作为引用框
view-box 使用最近的 SVG 视口(viewport)作为引用框。
none 这是默认设置,不剪辑
initial 设置属性为默认值。
inherit 属性值从父元素继承。
其中basic-shape属性的值有几种:
polygon多边形、circle圆形、ellipse椭圆、inset插图
三角形
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
菱形
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
梯形
clip-path: polygon(20% 0%, 80% 0%, 100% 100%, 0% 100%);
平行四边形
clip-path: polygon(25% 0%, 100% 0%, 75% 100%, 0% 100%);
斜角
clip-path: polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%);
左箭头
clip-path: polygon(40% 0%, 40% 20%, 100% 20%, 100% 80%, 40% 80%, 40% 100%, 0% 50%);
右箭头
clip-path: polygon(0% 20%, 60% 20%, 60% 0%, 100% 50%, 60% 100%, 60% 80%, 0% 80%);
五角星
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
Math.sin()
sin 方法返回一个 -1 到 1 之间的数值,表示给定角度(单位:弧度)的正弦值。
Math.sin(x) //函数返回一个数值的正弦值。x为弧度
Math.sin(0); // 0
Math.sin(Math.PI / 2); // 1
Math.cos()
cos 方法返回一个 -1 到 1 之间的数值,表示角度(单位:弧度)的余弦值。
Math.cos(0); // 1
Math.cos(Math.PI); // -1
Math.cos(2 * Math.PI); // 1
Math.tan()
表示一个角的正切值。
Math.atan()
函数返回一个数值的反正切(以弧度为单位)
Math.atan(0); // 0
已知两直角边Y,X长度,求夹角角度:
180*Math.atan(Y/X)/(Math.PI)
*创作不易,转载请注明出处并附上本文链接