HTML5/CSS3 3D雷达扫描动画

本文作者html5tricks,转载请注明出处

之前我们分享过一款纯CSS3雷达扫描模拟动画,看起来十分炫酷。这次我们分享的另外一款雷达动画更加让人震撼,它是基于HTML5和CSS3实现,它的一大特点是3D立体的视觉效果,鼠标点击雷达后将会展现一张3D立体地图,并且对地图上指定的几个地点进行坐标详细信息描述。

html5-css3-3d-radar-animation

在线演示源码下载

下面我们一起来解析一下这款炫酷的HTML5 3D雷达动画的实现过程。

HTML代码

首先HTML代码非常简单,只需在页面上展示一个div容器即可。

<div class="radar"></div>

CSS代码

这里主要利用了CSS3的旋转等动画特性,看看以下的CSS代码,主要是完成所有的动画:

@-webkit-keyframes flashing {
  0%,
  30% {
    -webkit-transform: scale(1);
            transform: scale(1);
  }
  70%,
  100% {
    -webkit-transform: scale(0);
            transform: scale(0);
  }
}
@keyframes flashing {
  0%,
  30% {
    -webkit-transform: scale(1);
            transform: scale(1);
  }
  70%,
  100% {
    -webkit-transform: scale(0);
            transform: scale(0);
  }
}
@-webkit-keyframes pulsation {
  0% {
    -webkit-transform: scale(0);
            transform: scale(0);
    border-color: red;
    background-color: rgba(255, 0, 0, 0.6);
  }
  80%,
  100% {
    -webkit-transform: scale(1);
            transform: scale(1);
    border-color: rgba(255, 0, 0, 0.2);
    background-color: rgba(255, 0, 0, 0.1);
  }
}
@keyframes pulsation {
  0% {
    -webkit-transform: scale(0);
            transform: scale(0);
    border-color: red;
    background-color: rgba(255, 0, 0, 0.6);
  }
  80%,
  100% {
    -webkit-transform: scale(1);
            transform: scale(1);
    border-color: rgba(255, 0, 0, 0.2);
    background-color: rgba(255, 0, 0, 0.1);
  }
}
@-webkit-keyframes rotation {
  from {
    -webkit-transform: rotate(0deg) translateZ(1px);
            transform: rotate(0deg) translateZ(1px);
  }
  to {
    -webkit-transform: rotate(360deg) translateZ(1px);
            transform: rotate(360deg) translateZ(1px);
  }
}
@keyframes rotation {
  from {
    -webkit-transform: rotate(0deg) translateZ(1px);
            transform: rotate(0deg) translateZ(1px);
  }
  to {
    -webkit-transform: rotate(360deg) translateZ(1px);
            transform: rotate(360deg) translateZ(1px);
  }
}
@-webkit-keyframes scale {
  from {
    -webkit-transform: scale(1);
            transform: scale(1);
  }
  to {
    -webkit-transform: scale(1.3);
            transform: scale(1.3);
  }
}
@keyframes scale {
  from {
    -webkit-transform: scale(1);
            transform: scale(1);
  }
  to {
    -webkit-transform: scale(1.3);
            transform: scale(1.3);
  }
}
@-webkit-keyframes polish {
  30%,
  100% {
    -webkit-transform: skewX(35deg) translateX(380%);
            transform: skewX(35deg) translateX(380%);
  }
}
@keyframes polish {
  30%,
  100% {
    -webkit-transform: skewX(35deg) translateX(380%);
            transform: skewX(35deg) translateX(380%);
  }
}

当然这个雷达光是有动画是不行的,还得有漂亮的外观,用以下CSS代码实现:

.radar {
  height: 40em;
  width: 48em;
  position: relative;
  cursor: pointer;
  -webkit-perspective: 62.5em;
          perspective: 62.5em;
}
.radar .radar-map-container,
.radar .risk-points,
.radar .scanning-circle canvas {
  left: 0;
  top: 0;
  position: absolute;
}
.radar .radar-map-container,
.radar .radar-map,
.radar .scanning-circle,
.radar .risk-elements-group {
  -webkit-transition: opacity 2.5s ease-out, -webkit-transform 2.5s ease-out, -webkit-transform-origin;
  transition: opacity 2.5s ease-out, -webkit-transform 2.5s ease-out, -webkit-transform-origin;
  transition: transform 2.5s ease-out, opacity 2.5s ease-out, transform-origin;
  transition: transform 2.5s ease-out, opacity 2.5s ease-out, transform-origin, -webkit-transform 2.5s ease-out, -webkit-transform-origin;
}
.radar .radar-map-container,
.radar .radar-map,
.radar .scanning-circle {
  will-change: transform;
}
.radar .radar-map-container {
  width: 400%;
  height: 400%;
  left: -150%;
  top: -150%;
  -webkit-transform: scale(0.25);
          transform: scale(0.25);
  -webkit-transform-style: preserve-3d;
          transform-style: preserve-3d;
}
.radar .radar-map {
  color: rgba(19, 182, 206, 0.4);
  background: url('http://i64.tinypic.com/5l17ut.jpg') center / contain no-repeat;
  width: 100%;
  height: 100%;
  -webkit-transform-style: preserve-3d;
          transform-style: preserve-3d;
}
.radar .radar-map.roaming {
  -webkit-transition-duration: 5s !important;
          transition-duration: 5s !important;
  -webkit-transition-timing-function: ease-in-out;
          transition-timing-function: ease-in-out;
  -webkit-transition-delay: .5s;
          transition-delay: .5s;
}
.radar .radar-map .risk-elements-group {
  opacity: 0;
  -webkit-transform-style: preserve-3d;
          transform-style: preserve-3d;
}
.radar .radar-map .risk-elements-group .red-flag,
.radar .radar-map .risk-elements-group .info-panel,
.radar .radar-map .risk-elements-group .dashed-circle {
  position: absolute;
}
.radar .radar-map .risk-elements-group .red-flag,
.radar .radar-map .risk-elements-group .dashed-circle {
  visibility: hidden;
}
.radar .radar-map .risk-elements-group .red-flag,
.radar .radar-map .risk-elements-group .info-panel {
  -webkit-transform-origin: bottom;
          transform-origin: bottom;
  -webkit-transform: rotateX(-70deg);
          transform: rotateX(-70deg);
}
.radar .radar-map .risk-elements-group .red-flag {
  width: 0.5em;
  height: 10em;
  background-color: white;
  -webkit-transform: rotateX(-70deg) rotateZ(-90deg);
          transform: rotateX(-70deg) rotateZ(-90deg);
}
.radar .radar-map .risk-elements-group .red-flag.stand-up {
  -webkit-transform: rotateX(-70deg);
          transform: rotateX(-70deg);
  visibility: visible;
}
.radar .radar-map .risk-elements-group .red-flag:before {
  content: attr(data-city);
  font-weight: bold;
  color: white;
  position: absolute;
  background-color: red;
  -webkit-clip-path: polygon(0 0, 100% 50%, 0 100%);
  -moz-clip-path: polygon(0 0, 100% 50%, 0 100%);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  width: 4.782608695652174em;
  height: 3.4782608695652173em;
  line-height: 3.4782608695652173em;
  font-size: 1.4375em;
  font-family: 微软雅黑;
  letter-spacing: 0.43478260869565216em;
  padding-left: 0.5217391304347826em;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  top: 0.43478260869565216em;
  white-space: nowrap;
}
.radar .radar-map .risk-elements-group .red-flag:after {
  content: '';
  position: absolute;
  width: 2.1875em;
  height: 0.625em;
  border-radius: 50%;
  background-color: inherit;
  top: calc(100% - 0.625em);
  left: calc(50% - 1.09375em);
}
.radar .radar-map .risk-elements-group .info-panel {
  -webkit-transform: rotateX(-70deg) translateY(-30%);
          transform: rotateX(-70deg) translateY(-30%);
  -webkit-filter: opacity(0);
  -moz-filter: opacity(0);
  filter: opacity(0);
  border: 0.0625em solid #DCDA6B;
  border-radius: 0.25em;
  background-color: rgba(245, 228, 158, 0.31);
  display: table;
  font-weight: bold;
  font-size: 2.5em;
  text-shadow: 0.025em 0.025em 0.05em black;
  padding: 0.2em 0.3em;
  font-family: 黑体;
  overflow: hidden;
}
.radar .radar-map .risk-elements-group .info-panel.showup {
  -webkit-filter: opacity(1);
  -moz-filter: opacity(1);
  filter: opacity(1);
  transition: -webkit-filter 1s linear 0.5s, -moz-filter 1s linear 0.5s, filter 1s linear 0.5s, opacity 0.5s linear;
}
.radar .radar-map .risk-elements-group .info-panel.polish:after {
  -webkit-animation: polish 5s linear 1 1s;
          animation: polish 5s linear 1 1s;
}
.radar .radar-map .risk-elements-group .info-panel .info-title {
  background-image: -webkit-gradient(linear, left top, right top, from(rgba(208, 209, 120, 0.6)), to(rgba(223, 226, 183, 0.05)));
  background-image: linear-gradient(to right, rgba(208, 209, 120, 0.6), rgba(223, 226, 183, 0.05));
  color: #FFE401;
  padding: 0 0.275em;
  border-radius: inherit;
}
.radar .radar-map .risk-elements-group .info-panel .info-content {
  color: white;
  margin: 0.25em;
  line-height: 1.3em;
}
.radar .radar-map .risk-elements-group .info-panel:after {
  content: '';
  position: absolute;
  width: 30%;
  height: 100%;
  -webkit-transform: skewX(35deg) translateX(-160%);
          transform: skewX(35deg) translateX(-160%);
  top: 0;
  background-image: -webkit-gradient(linear, left top, right top, from(transparent), color-stop(rgba(255, 255, 255, 0.3)), to(transparent));
  background-image: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.3), transparent);
}
.radar .radar-map .risk-elements-group .dashed-circle {
  width: 7.5em;
  height: 7.5em;
  border-radius: 50%;
  background: center / contain no-repeat;
  -webkit-animation: rotation 5s linear infinite;
          animation: rotation 5s linear infinite;
  -webkit-transition-property: visibility;
  transition-property: visibility;
}
.radar .radar-map .risk-elements-group .dashed-circle:after {
  content: '';
  border-radius: inherit;
  margin: auto;
  width: 15%;
  height: 15%;
  background-color: #E03636;
  -webkit-box-shadow: 0 0 0.9375em black;
          box-shadow: 0 0 0.9375em black;
  -webkit-animation: scale 1s linear infinite alternate;
          animation: scale 1s linear infinite alternate;
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}
.radar .changing-number-container {
  position: absolute;
  right: 12%;
  top: 10%;
  bottom: 0;
  margin: auto;
}
.radar .changing-number-container {
  display: table;
  color: #10ABE0;
}
.radar .changing-number-container:before,
.radar .changing-number-container:after {
  display: inline-block;
}
.radar .changing-number-container:before {
  content: '';
  background: -webkit-gradient(linear, left top, left bottom, color-stop(45%, transparent), color-stop(55%, currentColor), color-stop(55%, transparent)) center / 100% 1em;
  background: linear-gradient(to bottom, transparent 45%, currentColor 55%, transparent 55%) center / 100% 1em;
  width: 1em;
  height: 100%;
  margin-right: 0.625em;
}
.radar .changing-number-container:after {
  font-size: 0.75em;
  content: attr(data-number);
  width: 1.3333333333333333em;
  line-height: 1.3333333333333333em;
  word-break: break-all;
  letter-spacing: 1.3333333333333333em;
  vertical-align: top;
  font-weight: bold;
}
.radar .risk-points {
  z-index: 15;
}
.radar .risk-points .risk-point-group .risk-point {
  position: absolute;
  width: 0.625em;
  height: 0.625em;
  border-radius: 50%;
  -webkit-filter: blur(2px);
}
.radar .risk-points .risk-point-group .risk-point:after {
  content: '';
  display: block;
  height: 100%;
  border-radius: 50%;
  will-change: transform;
  -webkit-transform: scale(0);
          transform: scale(0);
}
.radar .risk-points .risk-point-group .risk-point.type1:after {
  -webkit-box-shadow: 0 0 0.3125em 0.3125em white inset, 0 0 0.3125em 0.5625em rgba(245, 76, 128, 0.54), 0 0 2.5625em 1.5625em rgba(222, 17, 17, 0.89);
          box-shadow: 0 0 0.3125em 0.3125em white inset, 0 0 0.3125em 0.5625em rgba(245, 76, 128, 0.54), 0 0 2.5625em 1.5625em rgba(222, 17, 17, 0.89);
}
.radar .risk-points .risk-point-group .risk-point.type2:after {
  -webkit-box-shadow: 0 0 0.3125em 0.3125em white inset, 0 0 0.3125em 0.5625em #15d8e8, 0 0 2.5625em 1.5625em rgba(44, 218, 226, 0.89);
          box-shadow: 0 0 0.3125em 0.3125em white inset, 0 0 0.3125em 0.5625em #15d8e8, 0 0 2.5625em 1.5625em rgba(44, 218, 226, 0.89);
}
.radar .risk-points .risk-point-group .risk-point.critical {
  -webkit-transform: scale(1);
          transform: scale(1);
}
.radar .risk-points .risk-point-group .risk-point.ordinary {
  -webkit-transform: scale(0.4);
          transform: scale(0.4);
}
.radar .risk-points .risk-point-group .risk-point.pulsation {
  color: red;
  -webkit-filter: none;
  width: 5em;
  height: 5em;
}
.radar .risk-points .risk-point-group .risk-point.pulsation .pulse-circle {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  border: 0.1875em solid currentColor;
  -webkit-animation: pulsation 3s linear 1;
          animation: pulsation 3s linear 1;
}
.radar .risk-points .risk-point-group .risk-point.pulsation .pulse-circle:nth-child(2) {
  -webkit-animation-delay: .8s;
          animation-delay: .8s;
}
.radar .risk-points .risk-point-group .risk-point.pulsation .pulse-circle:nth-child(3) {
  -webkit-animation-delay: 1.6s;
          animation-delay: 1.6s;
}
.radar .risk-points .risk-point-group .risk-point.pulsation:after {
  content: '';
  position: absolute;
  width: 15%;
  height: 15%;
  border-radius: 50%;
  background-color: currentColor;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
}
.radar .risk-points .risk-point-group.flashing .risk-point:after {
  -webkit-animation: flashing 2s linear 1;
          animation: flashing 2s linear 1;
}
.radar .scanning-circle {
  position: relative;
  height: 100%;
}
.radar .scanning-circle .radar-scanner {
  width: 40em;
  height: 100%;
  margin: 0 auto;
  position: relative;
  z-index: 10;
  -webkit-transform: rotate(125deg);
          transform: rotate(125deg);
}
.radar .scanning-circle .radar-scanner .inner-scanner,
.radar .scanning-circle .radar-scanner .outer-scanner {
  width: 100%;
  height: 100%;
}
.radar .scanning-circle .radar-scanner .inner-scanner {
  display: none;
  position: absolute;
  -webkit-clip-path: inset(0 0 0 50%);
  -webkit-filter: blur(30px);
  -webkit-animation: rotation 30s linear infinite;
          animation: rotation 30s linear infinite;
}
.radar .scanning-circle .radar-scanner .inner-scanner:after {
  content: '';
  -webkit-clip-path: inset(0 50% 0 0);
  display: block;
  height: 100%;
  background-color: rgba(19, 182, 206, 0.3);
  -webkit-transform: rotate(30deg);
          transform: rotate(30deg);
  border-radius: 50%;
}
.radar .scanning-circle .radar-scanner .outer-scanner {
  -webkit-clip-path: circle(20em at 50% 48%);
  -moz-clip-path: circle(20em at 50% 48%);
  clip-path: circle(20em at 50% 48%);
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  border: 0 solid white;
  border-top-width: 5px;
  border-radius: 50%;
  -webkit-box-shadow: 0 -0.1875em 0.3125em #33C9E8, 0 0 0.9375em white, 0 0.3125em 0.5em #38C1D2 inset;
          box-shadow: 0 -0.1875em 0.3125em #33C9E8, 0 0 0.9375em white, 0 0.3125em 0.5em #38C1D2 inset;
  will-change: transform;
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container {
  height: 100%;
  position: relative;
  overflow: hidden;
  border-radius: 50%;
  -webkit-transform: rotate(-35deg);
          transform: rotate(-35deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella,
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .color-sector {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  border-radius: inherit;
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella {
  -webkit-clip-path: inset(0 0 50% 50%);
  -moz-clip-path: inset(0 0 50% 50%);
  clip-path: inset(0 0 50% 50%);
  overflow: hidden;
  -webkit-filter: blur(30px);
  -moz-filter: blur(30px);
  filter: blur(30px);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector {
  -webkit-clip-path: inset(0 50% 0 0);
  -moz-clip-path: inset(0 50% 0 0);
  clip-path: inset(0 50% 0 0);
  background-color: rgba(19, 182, 206, 0.2);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(1) {
  -webkit-transform: rotate(60deg);
          transform: rotate(60deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(2) {
  -webkit-transform: rotate(54deg);
          transform: rotate(54deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(3) {
  -webkit-transform: rotate(48deg);
          transform: rotate(48deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(4) {
  -webkit-transform: rotate(42deg);
          transform: rotate(42deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(5) {
  -webkit-transform: rotate(36deg);
          transform: rotate(36deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(6) {
  -webkit-transform: rotate(30deg);
          transform: rotate(30deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(7) {
  -webkit-transform: rotate(24deg);
          transform: rotate(24deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(8) {
  -webkit-transform: rotate(18deg);
          transform: rotate(18deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(9) {
  -webkit-transform: rotate(12deg);
          transform: rotate(12deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .umbrella .color-sector:nth-child(10) {
  -webkit-transform: rotate(6deg);
          transform: rotate(6deg);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration {
  position: relative;
  height: 100%;
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .thin-border,
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .small-ellipse {
  -webkit-filter: blur(2px);
  margin-left: auto;
  margin-right: auto;
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .thin-border {
  width: 0.0625em;
  height: 50%;
  background-color: rgba(65, 191, 226, 0.5);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .small-ellipse {
  background-color: white;
  border-radius: 50%;
  position: absolute;
  left: 0;
  right: 0;
  -webkit-transform: translateZ(0);
          transform: translateZ(0);
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .small-ellipse:nth-child(2) {
  width: 0.9375em;
  height: 0.1875em;
  top: 3%;
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .small-ellipse:nth-child(3) {
  width: 1.375em;
  height: 0.25em;
  top: 17%;
}
.radar .scanning-circle .radar-scanner .outer-scanner .scanner-container .scanner-decoration .small-ellipse:nth-child(4) {
  width: 1em;
  height: 0.125em;
  top: 33%;
  opacity: 0.3;
}
.radar .scanning-circle .scanning-dashed-circle {
  -webkit-animation: rotation 2.67s linear infinite;
          animation: rotation 2.67s linear infinite;
}
.radar .scanning-circle .scanning-dashed-empty-circle {
  -webkit-animation: rotation 1.67s linear infinite;
          animation: rotation 1.67s linear infinite;
}
.radar.lying-down .radar-map-container,
.radar.lying-down .radar-map,
.radar.lying-down .scanning-circle,
.radar.lying-down .risk-elements-group {
  -webkit-transition-duration: 1s;
          transition-duration: 1s;
}
.radar.lying-down .radar-map-container {
  -webkit-transform: scale(1);
          transform: scale(1);
}
.radar.lying-down .radar-map {
  -webkit-transform: translateZ(-125em) rotateX(70deg);
          transform: translateZ(-125em) rotateX(70deg);
  color: transparent;
}
.radar.lying-down .radar-map .risk-elements-group {
  opacity: 1;
}
.radar.lying-down .radar-map .risk-elements-group .info-panel {
  opacity: .4;
}
.radar.lying-down .radar-map .risk-elements-group .red-flag {
  -webkit-transition: opacity 0.5s linear, visibility, -webkit-transform .3s linear;
  transition: opacity 0.5s linear, visibility, -webkit-transform .3s linear;
  transition: transform .3s linear, opacity 0.5s linear, visibility;
  transition: transform .3s linear, opacity 0.5s linear, visibility, -webkit-transform .3s linear;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(19) .red-flag {
  -webkit-transition-delay: 5.7s, 0s, 5.7s;
          transition-delay: 5.7s, 0s, 5.7s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(19) .dashed-circle {
  -webkit-transition-delay: 5.7s;
          transition-delay: 5.7s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(18) .red-flag {
  -webkit-transition-delay: 5.3999999999999995s, 0s, 5.3999999999999995s;
          transition-delay: 5.3999999999999995s, 0s, 5.3999999999999995s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(18) .dashed-circle {
  -webkit-transition-delay: 5.3999999999999995s;
          transition-delay: 5.3999999999999995s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(17) .red-flag {
  -webkit-transition-delay: 5.1s, 0s, 5.1s;
          transition-delay: 5.1s, 0s, 5.1s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(17) .dashed-circle {
  -webkit-transition-delay: 5.1s;
          transition-delay: 5.1s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(16) .red-flag {
  -webkit-transition-delay: 4.8s, 0s, 4.8s;
          transition-delay: 4.8s, 0s, 4.8s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(16) .dashed-circle {
  -webkit-transition-delay: 4.8s;
          transition-delay: 4.8s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(15) .red-flag {
  -webkit-transition-delay: 4.5s, 0s, 4.5s;
          transition-delay: 4.5s, 0s, 4.5s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(15) .dashed-circle {
  -webkit-transition-delay: 4.5s;
          transition-delay: 4.5s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(14) .red-flag {
  -webkit-transition-delay: 4.2s, 0s, 4.2s;
          transition-delay: 4.2s, 0s, 4.2s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(14) .dashed-circle {
  -webkit-transition-delay: 4.2s;
          transition-delay: 4.2s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(13) .red-flag {
  -webkit-transition-delay: 3.9s, 0s, 3.9s;
          transition-delay: 3.9s, 0s, 3.9s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(13) .dashed-circle {
  -webkit-transition-delay: 3.9s;
          transition-delay: 3.9s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(12) .red-flag {
  -webkit-transition-delay: 3.5999999999999996s, 0s, 3.5999999999999996s;
          transition-delay: 3.5999999999999996s, 0s, 3.5999999999999996s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(12) .dashed-circle {
  -webkit-transition-delay: 3.5999999999999996s;
          transition-delay: 3.5999999999999996s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(11) .red-flag {
  -webkit-transition-delay: 3.3s, 0s, 3.3s;
          transition-delay: 3.3s, 0s, 3.3s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(11) .dashed-circle {
  -webkit-transition-delay: 3.3s;
          transition-delay: 3.3s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(10) .red-flag {
  -webkit-transition-delay: 3s, 0s, 3s;
          transition-delay: 3s, 0s, 3s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(10) .dashed-circle {
  -webkit-transition-delay: 3s;
          transition-delay: 3s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(9) .red-flag {
  -webkit-transition-delay: 2.6999999999999997s, 0s, 2.6999999999999997s;
          transition-delay: 2.6999999999999997s, 0s, 2.6999999999999997s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(9) .dashed-circle {
  -webkit-transition-delay: 2.6999999999999997s;
          transition-delay: 2.6999999999999997s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(8) .red-flag {
  -webkit-transition-delay: 2.4s, 0s, 2.4s;
          transition-delay: 2.4s, 0s, 2.4s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(8) .dashed-circle {
  -webkit-transition-delay: 2.4s;
          transition-delay: 2.4s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(7) .red-flag {
  -webkit-transition-delay: 2.1s, 0s, 2.1s;
          transition-delay: 2.1s, 0s, 2.1s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(7) .dashed-circle {
  -webkit-transition-delay: 2.1s;
          transition-delay: 2.1s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(6) .red-flag {
  -webkit-transition-delay: 1.7999999999999998s, 0s, 1.7999999999999998s;
          transition-delay: 1.7999999999999998s, 0s, 1.7999999999999998s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(6) .dashed-circle {
  -webkit-transition-delay: 1.7999999999999998s;
          transition-delay: 1.7999999999999998s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(5) .red-flag {
  -webkit-transition-delay: 1.5s, 0s, 1.5s;
          transition-delay: 1.5s, 0s, 1.5s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(5) .dashed-circle {
  -webkit-transition-delay: 1.5s;
          transition-delay: 1.5s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(4) .red-flag {
  -webkit-transition-delay: 1.2s, 0s, 1.2s;
          transition-delay: 1.2s, 0s, 1.2s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(4) .dashed-circle {
  -webkit-transition-delay: 1.2s;
          transition-delay: 1.2s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(3) .red-flag {
  -webkit-transition-delay: 0.8999999999999999s, 0s, 0.8999999999999999s;
          transition-delay: 0.8999999999999999s, 0s, 0.8999999999999999s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(3) .dashed-circle {
  -webkit-transition-delay: 0.8999999999999999s;
          transition-delay: 0.8999999999999999s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(2) .red-flag {
  -webkit-transition-delay: 0.6s, 0s, 0.6s;
          transition-delay: 0.6s, 0s, 0.6s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(2) .dashed-circle {
  -webkit-transition-delay: 0.6s;
          transition-delay: 0.6s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(1) .red-flag {
  -webkit-transition-delay: 0.3s, 0s, 0.3s;
          transition-delay: 0.3s, 0s, 0.3s;
}
.radar.lying-down .radar-map:not(.roaming) .risk-elements-group:nth-child(1) .dashed-circle {
  -webkit-transition-delay: 0.3s;
          transition-delay: 0.3s;
}
.radar.pause-animation .inner-scanner,
.radar.pause-animation .scanning-dashed-circle,
.radar.pause-animation .scanning-dashed-empty-circle {
  -webkit-animation-play-state: paused;
          animation-play-state: paused;
}
.radar.pause-animation .changing-number-container {
  display: none;
}
.radar.pause-animation .risk-points {
  display: none;
}
.radar.pause-animation .scanning-circle {
  -webkit-transform: scale(0);
          transform: scale(0);
  opacity: 0;
}
html,
body {
  width: 100%;
  height: 100%;
}
body {
  font-size: 0.8em;
  margin: 0;
  background-color: black;
}
.radar {
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
}
#radar-switch-btn {
  position: absolute;
  left: 0;
  top: 0;
  color: white;
}
@media only screen and (max-device-width: 667px) and (-webkit-min-device-pixel-ratio: 2) {
  .radar {
    overflow: hidden;
  }
}

JavaScript代码

JS代码主要是实现向雷达div容器中注入其他元素以及3D地图中地点标注信息的切换展示,代码如下:

function Radar(radarContainer) {
    radarContainer.innerHTML = '<div class="radar-map-container">\
                                    <div class="radar-map"></div>\
                                </div>\
                                <div class="changing-number-container" data-number="1234567890456"></div>\
                                <div class="risk-points"></div>\
                                <div class="scanning-circle">\
                                    <div class="radar-scanner">\
                                        <div class="inner-scanner"></div>\
                                        <div class="outer-scanner">\
                                            <div class="scanner-container">\
                                                <div class="umbrella"></div>\
                                                <div class="scanner-decoration">\
                                                    <div class="thin-border"></div>\
                                                    <div class="small-ellipse"></div>\
                                                    <div class="small-ellipse"></div>\
                                                    <div class="small-ellipse"></div>\
                                                </div>\
                                            </div>\
                                        </div>\
                                    </div>\
                                </div>';
    
    this._container = radarContainer;
    this._containerWidth = this._container.offsetWidth;
    this._containerHeight = this._container.offsetHeight;
    this._centerX = this._containerWidth / 2;
    this._centerY = this._containerHeight / 2;
    
    // 跟风险点"蒙版"有关的变量
    this._maskCtx = (function(cw, ch) {
        var c = document.createElement('canvas');
        c.width = cw;
        c.height = ch;
        return c.getContext('2d');
    })(this._containerWidth, this._containerHeight);
    this._maskSectorDegree = 60;  // 雷达扇形所占角度
    this._maskStartDegree = 0;  // 雷达扇形开始扫描的角度
    this._scanSpeed = 2;  // 雷达扫描速度,单位为deg
    
    this._outerScanner = this._container.querySelector('.outer-scanner');  // 外层雷达扫描器
    
    this._riskPointsContainer = this._container.querySelector('.risk-points');
    
    this._allRiskPointsArr = [];  // 保存所有风险点的数组,改变雷达扫描速度时要用到这些信息
    this._tmpRiskPointsArr = [];  // 初始化时保存所有的风险点,雷达扫描过程中会遍历这个数组,并把当前扫描到的风险点从这个数组移动到保存风险点的对象中,雷达扫描玩一遍之后这个数组为空。这样做避免雷达扫描时重复遍历风险点,特别是当有很多风险点的时候
    this._riskPoints = {};  // 以扫描角度为键值保存风险点信息
    
    this._riskElements = [];  // 与风险点相关的虚线圆圈、红旗、信息面面和位置信息
    
    this._curRoamingIndex = 0;  // 当前漫游的风险点索引
    
    this._radarMap = this._container.querySelector('.radar-map');
    
    this._roamingDuration = 0;  // 两个风险点之间漫游时长
    
    this._changingNumberContainer = this._container.querySelector('.changing-number-container');  // 不断变化的数字
    this._digits_base = Math.pow(10, Math.min(this._changingNumberContainer.dataset.number.length, 16));  // 数字位数,最大为16
    
    this._mapTranslateZ = (function(container) {  // 相机漫游时拉近的距离
        var fontSize = parseInt(getComputedStyle(container).fontSize);
        return 300 / 16 * fontSize;
    })(this._container);
    
    this._scaleFactor = this._radarMap.offsetWidth / this._containerWidth;  // 为了使地图拉近之后不失真而放大的倍数
    
    this._rotateX = 70;  // 地图倒下旋转的角度
}

/* 外部调用的方法 */
Radar.prototype.init = function(options) {
    /*
        options = {
            scanSpeed: 2  // 扫描的速度,单位为deg
        }
    */
    options.scanSpeed && (this._scanSpeed = parseInt(options.scanSpeed));
    
    this._createCanvasElements();
    this._drawLineAndCircle();
    this._drawDashedCircle();
    this._drawDashedEmptyCircle();
    this._drawScannerSector();
    this._animate();
    this._initEvent();    
};

// 添加风险点
Radar.prototype.addRiskPoints = function(points) {
    if(!(points instanceof Array)) {
        points = [points];
    }
    points.forEach(this._addOneRiskPoint.bind(this));
};

// 一次设置多个风险点
Radar.prototype.setRiskPoints = function(points) {
    this._removeAllRiskPoints();
    this.addRiskPoints(points);
};

// 调整雷达扫描速度
Radar.prototype.changeScanSpeed = function(perTimeDeg) {
    perTimeDeg = parseInt(perTimeDeg);
    if(perTimeDeg == this._scanSpeed || 360 % perTimeDeg != 0) {  // 每次旋转的角度必须是360的约数,否则可能会出现跳过风险点的情况
        return;
    }
    this._riskPoints = {};
    this._tmpRiskPointsArr = this._allRiskPointsArr;
    this._scanSpeed = perTimeDeg;
};

// 雷达状态切换
Radar.prototype.roamingToggle = function() {
    this._container.classList.toggle('lying-down');
    if(this._isLyingDown()) {
        // 倒下之后停止动画绘制
        this._pauseAnimation();
    } else {
        this._radarMap.classList.remove('roaming');
        this._radarMap.style.removeProperty('transform');
    }
};

Radar.prototype.startRoaming = function() {
    this._container.classList.add('lying-down');
    this._pauseAnimation();
};

Radar.prototype.stopRoaming = function() {
    this._container.classList.remove('lying-down');
    this._radarMap.classList.remove('roaming');
    this._radarMap.style.removeProperty('transform');
};

/* 私有方法 */
Radar.prototype._addOneRiskPoint = function(options) {
    /**
        options = {
            type: 'type1', // 'type1' 或者 'type2',风险点的颜色是红色还是蓝色
            severity: 'critical',  // 风险严重程度,'ordinary'表示普通,'critical'表示严重
            coords: [134.7728099, 53.56097399999999],  // 城市的经纬度
            city: '北京',
            title: '大额预授权交易异常',
            total: 3  // 风险卡的数量
        }
    **/
    // 计算风险点屏幕坐标
    var pointCoords = this._geoCoordsToScreenCoords(options.coords),
        point_x = pointCoords[0],
        point_y = pointCoords[1];
    
    /*// 计算风险点索引
    var riskPointIndex = this._calcRiskPointIndex(point_x, point_y);
    
    if(!this._riskPoints[riskPointIndex]) {
        var riskPointGroup = document.createElement('div');  // 相同索引的风险点放在一组
        riskPointGroup.className = 'risk-point-group';
        this._riskPointsContainer.appendChild(riskPointGroup);
        this._riskPoints[riskPointIndex] = riskPointGroup;
    }*/
    
    // 创建风险点元素
    var riskPoint = document.createElement('div');
    riskPoint.className = 'risk-point ' + options.type + ' ' + options.severity;
    if(options.type == 'pulsation') {
        riskPoint.innerHTML = '<div class="pulse-circle"></div>\
                                <div class="pulse-circle"></div>\
                                <div class="pulse-circle"></div>';
    }
    //this._riskPoints[riskPointIndex].appendChild(riskPoint);
    
    // 计算并设置风险点位置
    var point_left = point_x - riskPoint.offsetWidth / 2,
        point_top = point_y - riskPoint.offsetHeight / 2;
    riskPoint.style.cssText = 'left: ' + point_left + 'px; top: ' + point_top + 'px;';
    
    var riskPointItem = {
        x: point_x,
        y: point_y,
        target: riskPoint
    };
    this._allRiskPointsArr.push(riskPointItem);
    this._tmpRiskPointsArr.push(riskPointItem);
    
    // 创建跟风险点相关的红旗、虚线圆圈和信息面板
    var elements_group = document.createElement('div');
    elements_group.className = 'risk-elements-group';
    elements_group.innerHTML = '<div class="dashed-circle"></div>\
                                <div class="red-flag" data-city="' + options.city + '"></div>\
                                <div class="info-panel">\
                                    <div class="info-title">' + options.title + '</div>\
                                    <div class="info-content">\
                                        <div>\
                                            风险发生地:<span class="risk-city">' + options.city + '</span>\
                                        </div>\
                                        <div>\
                                            风险卡片:<span class="risk-num">' + options.total + '</span>\
                                        </div>\
                                    </div>\
                                </div>';
    this._radarMap.appendChild(elements_group);
    var dashed_circle = elements_group.querySelector('.dashed-circle'),
        red_flag = elements_group.querySelector('.red-flag'),
        info_panel = elements_group.querySelector('.info-panel');
    dashed_circle.style.backgroundImage = 'url(' + this._getDashedCircleBg(dashed_circle.offsetWidth) + ')';
    
    // 计算和设置圆圈、红旗、信息面板的位置
    var dashed_circle_left = point_x * this._scaleFactor - dashed_circle.offsetWidth / 2,
        dashed_circle_top = point_y * this._scaleFactor - dashed_circle.offsetHeight / 2,
        red_flag_left = point_x * this._scaleFactor - red_flag.offsetWidth / 2,
        red_flag_top = point_y * this._scaleFactor - red_flag.offsetHeight,
        info_panel_left = point_x * this._scaleFactor - info_panel.offsetWidth / 2,
        info_panel_top = point_y * this._scaleFactor - info_panel.offsetHeight;
    dashed_circle.style.left = dashed_circle_left + 'px';
    dashed_circle.style.top = dashed_circle_top + 'px';
    if(this._isLyingDown()) {
        dashed_circle.style.visibility = 'visible';
    }
    red_flag.style.left = red_flag_left + 'px';
    red_flag.style.top = red_flag_top + 'px';
    info_panel.style.left = info_panel_left + 'px';
    info_panel.style.top = info_panel_top + 'px';
    
    // 保存与风险点相关的元素
    this._riskElements.push({
        name: options.city,
        riskPoint: riskPoint,
        dashedCircle: dashed_circle,
        redFlag: red_flag,
        infoPanel: info_panel,
        translate: [(this._containerWidth * 0.5 - point_x) * this._scaleFactor, this._calcTYByTZ(this._mapTranslateZ, dashed_circle.offsetHeight/2) - point_y * this._scaleFactor],
        rotateY: (function(riskElements) {
            // 旋转的角度正负零交替
            var base_deg = 10,
                max_deviation_deg = 5;
            var pn = (riskElements.length % 2) * 2 - 1;  // positive or negative
            var new_base_deg = Math.ceil((riskElements.length + 1) % 3 / 2) * base_deg;  // 10 10 0 10 10 0 ...
            return pn * (new_base_deg + Math.round(Math.random() * max_deviation_deg));
        })(this._riskElements),
        transformOrigin: Math.round(point_x / this._containerWidth * 100) + '%'
    });
    
    // 旋转信息面板,使信息面板转到眼前时正对镜头
    var rotate_deg = this._riskElements[this._riskElements.length - 1].rotateY;
    info_panel.style.transform = getComputedStyle(info_panel).transform + ' rotateY(' + (-rotate_deg) + 'deg)';
};

// 根据名称删除风险点
Radar.prototype._removeRiskPointByName = function(name) {
    for(var i = 0, len = this._riskElements.length; i < len; i++) {
        var curRiskElement = this._riskElements[i];
        if(curRiskElement.name == name) {
            var riskPointGroup = curRiskElement.riskPoint.parentElement,
                riskElementsGroup = curRiskElement.dashedCircle.parentElement;
            riskPointGroup.removeChild(curRiskElement.riskPoint);
            this._radarMap.removeChild(riskElementsGroup);
            this._riskElements.splice(i, 1);
            return true;
        }
    }
    return false;
};

// 删除所有风险点
Radar.prototype._removeAllRiskPoints = function() {
    var total = this._riskElements.length;
    this._radarMap.innerHTML = '';
    this._riskPointsContainer.innerHTML = '';
    this._riskPoints = {};
    this._riskElements = [];
    return total;
};

// 暂停动画
Radar.prototype._pauseAnimation = function() {
    cancelAnimationFrame(this._requestId);
    this._container.classList.add('pause-animation');
};

// 恢复动画
Radar.prototype._resumeAnimation = function() {
    this._requestId = requestAnimationFrame(this._animate.bind(this));
    this._container.classList.remove('pause-animation');
};

// 创建canvas标签
Radar.prototype._createCanvasElements = function() {
    var scanningCircleElement = this._container.querySelector('.scanning-circle');
    
    // 绘制雷达静止的线框和圆圈用到的canvas
    var canvas = document.createElement('canvas');
    canvas.width = this._containerWidth;
    canvas.height = this._containerHeight;
    scanningCircleElement.appendChild(canvas);
    this._lineAndCircleCanvas = canvas;
    
    // 绘制内部旋转的 "虚线" 圆圈用到的canvas
    this._dashedCircleCanvas = canvas.cloneNode(true);
    this._dashedCircleCanvas.className = 'scanning-dashed-circle';
    scanningCircleElement.appendChild(this._dashedCircleCanvas);
    
    // 绘制内部旋转的 "空心虚线" 圆圈用到的canvas
    this._dashedEmptyCircleCanvas = canvas.cloneNode(true);
    this._dashedEmptyCircleCanvas.className = 'scanning-dashed-empty-circle';
    scanningCircleElement.appendChild(this._dashedEmptyCircleCanvas);
};

// 地理坐标转换成屏幕坐标
Radar.prototype._geoCoordsToScreenCoords = function(geoCoords) {  // geoCoords:经纬度数组
    var china_geometry_bounds = [73.4994136, 53.56097399999999, 61.2733963, 35.40335839999999];  // 西北的经纬度和经纬度跨度
    var point_x = Math.abs(geoCoords[0] - china_geometry_bounds[0]) / china_geometry_bounds[2] * this._containerWidth,
        point_y = Math.abs(geoCoords[1] - china_geometry_bounds[1]) / china_geometry_bounds[3] * this._containerHeight;
    return [point_x, point_y];
};

// 计算风险点的索引(雷达扇形扫描到多少度时要显示这个风险点)
Radar.prototype._calcRiskPointIndex = function(point_x, point_y) {
    var point_offset_x = point_x - this._centerX,  // 与中心点的偏移
        point_offset_y = this._centerY - point_y,
        riskPointRadian = -Math.atan(point_offset_y / point_offset_x);  // 风险点在雷达扫描圆形的弧度
    if(point_offset_x < 0) {
        riskPointRadian -= Math.PI;
    } else if(point_offset_y < 0) {
        riskPointRadian -= Math.PI * 2;
    }
    var riskPointDeg = 180 / Math.PI * riskPointRadian;
    var riskPointIndex = '_deg_' + riskPointDeg.toFixed();
    return riskPointIndex;
};

// 根据地图倒下之后translateZ的大小计算风险点应该拉近到相机前面的y值,使得风险点向镜头拉近后正好在屏幕正下方
Radar.prototype._calcTYByTZ = function(tz, dashed_circle_r) {
    var p = parseInt(getComputedStyle(this._container).perspective),  // 原始视角
        pr = (p - tz) / p,  // 应用translateZ后的视角比例
        point_screen_bottom_offset = dashed_circle_r / pr,  // 风险点漫游时要平移到的位置与屏幕底部的偏移量
        tr_o_offset_y = pr * (document.body.clientHeight - this._containerHeight / 2 - this._container.getBoundingClientRect().top - point_screen_bottom_offset);  // 最后相机要拉近到的y值与transform origin中心点的偏移量
    return this._radarMap.offsetHeight / 2 + tr_o_offset_y;
};

/*Radar.prototype._calcTYByTZ = function(tz, dashed_circle_r) {
    var p = parseInt(getComputedStyle(this._container).perspective),  // 原始视角
        pr = (p - tz) / p,  // 应用translateZ后的视角比例
        tmp_tr_o_offset_y = document.body.clientHeight - this._containerHeight / 2 - this._container.getBoundingClientRect().top;  // 最后相机要拉近到的y值与transform origin中心点的偏移量
    var tr_o_offset_y = this._calcOffsetBeforeRotateX(tmp_tr_o_offset_y, this._rotateX, p-tz);
    return this._radarMap.offsetHeight / 2 + tr_o_offset_y * pr;
};*/

// 根据绕x轴旋转之后风险点的位置计算旋转之前风险点要平移到的位置
Radar.prototype._calcOffsetBeforeRotateX = function(newOffset, rotateX, oldPerspective) {  // 计算不正确
    // (newOffset / cos(rotateX)) / x = oldPerspective / (oldPerspective - x * sin(rotateX))
    var x1 = newOffset / Math.cos(Math.PI / 180 * rotateX);
    return oldPerspective * x1 / (oldPerspective + x1 * Math.sin(Math.PI / 180 * rotateX));
};

// 动画
Radar.prototype._animate = function() {
    this._rotateRiskPointMask();
    this._changeNumber();
    this._requestId = requestAnimationFrame(arguments.callee.bind(this));
};

// 变化数字
Radar.prototype._changeNumber = function() {
    var _assist_number = arguments.callee._assist_number || 0;
    if(_assist_number % 6 == 0) {
        var number = Math.round(Math.random() * this._digits_base);
        this._changingNumberContainer.dataset.number = number;
    }
    arguments.callee._assist_number = (++_assist_number) % 360;
};

// 绘制雷达静止的线框和圆圈
Radar.prototype._drawLineAndCircle = function() {
    var radius = this._containerHeight / 2,
        ctx = this._lineAndCircleCanvas.getContext('2d');
    // 最外层圆圈
    var lineWidth = 5;
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = '#0146C2';
    ctx.beginPath();
    ctx.arc(this._centerX, this._centerY, radius - lineWidth / 2, 0, Math.PI * 2);
    ctx.closePath();
    ctx.stroke();
    // 内部圆圈
    ctx.fillStyle = 'rgba(30,199,230,.5)';
    ctx.beginPath();
    ctx.arc(this._centerX, this._centerY, 3, 0, Math.PI * 2);
    ctx.closePath();
    ctx.fill();
    var totalCircle = 8;
    ctx.lineWidth = 0.5;
    ctx.strokeStyle = 'rgba(30,199,230,.5)';
    for(var i = 0; i < totalCircle - 1; i++) {
        ctx.beginPath();
        ctx.arc(this._centerX, this._centerY, radius / totalCircle * (i + 1), 0, Math.PI * 2);
        ctx.closePath();
        ctx.stroke();
    }
    // 内部直线
    var totalLines = 14;
    ctx.save();
    ctx.lineWidth = 0.3;
    ctx.translate(this._centerX, this._centerY);
    ctx.rotate(Math.PI / totalLines);
    for(var i = 0; i < totalLines; i++) {
        ctx.rotate(Math.PI * 2 / totalLines);
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(0, -radius + lineWidth);
        ctx.closePath();
        ctx.stroke();
    }
    ctx.restore();
    // 内部虚线
    ctx.save();
    ctx.setLineDash([2, 8]);
    ctx.translate(this._centerX, this._centerY);
    for(var i = 0; i < totalLines / 2; i++) {
        ctx.rotate(Math.PI * 4 / totalLines);
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(0, -radius + lineWidth);
        ctx.closePath();
        ctx.stroke();
    }
    ctx.restore();
};

// 绘制内部旋转的 "虚线" 圆圈
Radar.prototype._drawDashedCircle = function() {
    var ctx = this._dashedCircleCanvas.getContext('2d');
    
    ctx.clearRect(-this._centerX, -this._centerY, this._dashedCircleCanvas.width, this._dashedCircleCanvas.height);
    ctx.globalAlpha = 0.8;
    ctx.lineWidth = 5;
    ctx.translate(this._centerX, this._centerY);
    
    var radius = this._containerHeight / 2 / 8 * 7 - ctx.lineWidth / 2;
    
    ctx.strokeStyle = '#016FB7';
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 5);
    ctx.stroke();
    
    ctx.strokeStyle = '#23B2D8';
    ctx.rotate(Math.PI / 3);
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 6);
    ctx.stroke();
    
    ctx.strokeStyle = '#80DEF9';
    ctx.rotate(Math.PI / 3);
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 18);
    ctx.stroke();
    
    ctx.strokeStyle = '#085BAF';
    ctx.rotate(Math.PI / 4);
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 9);
    ctx.stroke();
};

// 绘制内部旋转的空心虚线圆圈
Radar.prototype._drawDashedEmptyCircle = function() {
    var ctx = this._dashedEmptyCircleCanvas.getContext('2d');
    
    ctx.clearRect(-this._centerX, -this._centerY, this._dashedEmptyCircleCanvas.width, this._dashedEmptyCircleCanvas.height);
    ctx.strokeStyle = '#5298D3';
    ctx.globalAlpha = 0.3;
    ctx.translate(this._centerX, this._centerY);
    
    var radius = this._containerHeight / 2 / 8 * 6,
        radiusDeviation = 5,  // 空心虚线的高度
        innerRadius = radius - radiusDeviation;
    
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 5);
    ctx.arc(0, 0, innerRadius, Math.PI / 5, 0, true);
    ctx.closePath();
    ctx.stroke();
    
    ctx.rotate(Math.PI / 3);
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 6);
    ctx.arc(0, 0, innerRadius, Math.PI / 6, 0, true);
    ctx.closePath();
    ctx.stroke();
    
    ctx.rotate(Math.PI / 3);
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 18);
    ctx.arc(0, 0, innerRadius, Math.PI / 18, 0, true);
    ctx.closePath();
    ctx.stroke();
    
    ctx.rotate(Math.PI / 4);
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI / 9);
    ctx.arc(0, 0, innerRadius, Math.PI / 9, 0, true);
    ctx.closePath();
    ctx.stroke();
};

// 绘制雷达扫描锥形渐变扇形
Radar.prototype._drawScannerSector = function() {
    // 创建canvas元素
    var c = document.createElement('canvas');
    c.width = c.height = this._containerHeight;
    this._outerScanner.querySelector('.umbrella').appendChild(c);
    // 绘制锥形渐变
    var ctx = c.getContext('2d');
    var sectorCnt = 10;  // 用10块扇形模拟锥形渐变
    var startDeg = -90, endDeg;
    var sectorRadius = this._containerHeight / 2;
    ctx.translate(sectorRadius, sectorRadius);
    ctx.fillStyle = 'rgba(19, 182, 206, 0.2)';
    for(var i = 0; i < sectorCnt; i++) {
        endDeg = startDeg + this._maskSectorDegree - this._maskSectorDegree / sectorCnt * i;
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(0, sectorRadius);
        ctx.arc(0, 0, sectorRadius, Math.PI / 180 * (startDeg - 180), Math.PI / 180 * endDeg);
        ctx.closePath();
        ctx.fill();
    }
};

// 旋转只显示风险点的区域
Radar.prototype._rotateRiskPointMask = function() {
    function angleToRadian(angle) {
        return Math.PI / 180 * angle;
    }
    this._maskStartDegree = this._maskStartDegree % 360;
    
    this._maskCtx.beginPath();
    this._maskCtx.moveTo(this._centerX, this._centerY);
    this._maskCtx.arc(this._centerX, this._centerY, this._containerHeight / 2, angleToRadian(this._maskStartDegree), angleToRadian(this._maskStartDegree + this._maskSectorDegree));
    this._maskCtx.lineTo(this._centerX, this._centerY);
    this._maskCtx.closePath();
    
    // 控制风险点的显示
    var riskPointIndex = '_deg_' + this._maskStartDegree;
    if(!this._riskPoints[riskPointIndex] && this._tmpRiskPointsArr.length > 0) {
        // todo: 这里先判断this._riskPoints[riskPointIndex]可能会出现不去处理this._tmpRiskPointsArr的情况,特别是当风险点在某一块区域很密集的时候,而且后面添加的风险点都会另开一组
        var that = this;
        this._tmpRiskPointsArr.forEach(function(point) {
            if(that._maskCtx.isPointInPath(point.x, point.y)) {
                // 把当前扫描到的风险点缓存起来
                if(!that._riskPoints[riskPointIndex]) {
                    var riskPointGroup = document.createElement('div');  // 相同索引的风险点放在一组
                    riskPointGroup.className = 'risk-point-group';
                    that._riskPointsContainer.appendChild(riskPointGroup);
                    that._riskPoints[riskPointIndex] = riskPointGroup;
                }
                that._riskPoints[riskPointIndex].appendChild(point.target);
                point._willDelete = true;  // 将要删除的标记
            }
        });
        
        // 遍历完之后删除已处理过的风险点
        this._tmpRiskPointsArr = this._tmpRiskPointsArr.filter(function(pointItem) {
            var flag = !pointItem._willDelete;
            delete pointItem._willDelete;
            return flag;
        });
    }
    this._riskPoints[riskPointIndex] && this._riskPoints[riskPointIndex].classList.add('flashing');
    
    // 旋转雷达扫描扇形
    this._outerScanner.style.transform = 'rotate(' + this._maskStartDegree + 'deg) translateZ(0)';
    this._maskStartDegree -= this._scanSpeed;
};

// 绘制风险点旋转的虚线圆圈背景
Radar.prototype._getDashedCircleBg = function(radius) {
    var center_x = radius / 2,
        center_y = radius / 2;
    var c = document.createElement('canvas');
    c.width = radius;
    c.height = radius;
    var ctx = c.getContext('2d');
    ctx.strokeStyle = '#EAFF00';
    ctx.shadowColor = '#EAFF00';
    ctx.shadowBlur = radius / 25;
    // 绘制内圆环
    ctx.lineWidth = radius / 50;
    ctx.beginPath();
    ctx.arc(center_x, center_y, radius/4, 0, Math.PI*2);
    ctx.stroke();
    // 绘制外层虚线圆环
    var dash_cnt = 5,  // 实心虚线点个数
        dash_ratio = 0.8;  // 空心虚线点与实心虚线点的比例
    ctx.lineWidth = radius / 10;
    var solid_dash_len = Math.PI * (radius - ctx.lineWidth) / dash_cnt / (1 + dash_ratio),
        hollow_dash_len = solid_dash_len * dash_ratio;
    ctx.setLineDash([solid_dash_len, hollow_dash_len]);
    ctx.beginPath();
    ctx.arc(center_x, center_y, radius/2-ctx.lineWidth/2, 0, Math.PI*2);
    ctx.stroke();    
    return c.toDataURL();
};

// 地图倒下之后开始显示风险点处的虚线圆圈、红旗和信息面板
Radar.prototype._showRiskElements = function() {
    this._riskElements.forEach(function(re, ri) {
        re.dashedCircle.style.visibility = 'visible';
        re.redFlag.classList.add('stand-up');
    });
};

// 相机漫游到当前风险点时让红旗消失、让信息面板出现
Radar.prototype._handleCurRiskElements = function() {
    var curRiskElements = this._riskElements[this._curRoamingIndex];
    if(curRiskElements) {
        curRiskElements.redFlag.style.opacity = 0;
        curRiskElements.dashedCircle.style.visibility = 'visible';
        curRiskElements.infoPanel.classList.add('showup');
        curRiskElements.infoPanel.classList.add('polish');

        // 设置上一个漫游的风险点的信息面板的透明度
        var lastRiskElements = this._riskElements[(this._curRoamingIndex - 1 + this._riskElements.length) % this._riskElements.length];
        if(lastRiskElements) {
            lastRiskElements.infoPanel.style.removeProperty('opacity');
        }
    }
};

// 地图回到初始状态时隐藏跟风险点相关的元素
Radar.prototype._hideRiskElements = function() {
    this._curRoamingIndex = 0;
    this._riskElements.forEach(function(re, ri) {
        re.redFlag.classList.remove('stand-up');
        re.redFlag.style.opacity = 1;
        
        re.dashedCircle.style.visibility = 'hidden';
        
        re.infoPanel.style.removeProperty('opacity');
        re.infoPanel.classList.remove('showup');
        re.infoPanel.classList.remove('polish');
    });
};

// 风险点镜头漫游
Radar.prototype._riskPointsRoaming = function() {
    if(!this._isInRoamingState()) return;
    var curRiskElements = this._riskElements[this._curRoamingIndex];
    if(!curRiskElements) return;
    var radarMapStyle = 'translateZ(' + this._mapTranslateZ + 'px) rotateY(' + curRiskElements.rotateY + 'deg) rotateX(' + this._rotateX + 'deg) translate3d(' + curRiskElements.translate[0] + 'px, ' + curRiskElements.translate[1] + 'px, 0)';
    
    //test
    //radarMapStyle = 'translateZ(' + this._mapTranslateZ + 'px) translate3d(' + curRiskElements.translate[0] + 'px, ' + curRiskElements.translate[1] + 'px, 0)';
    //test
    
    // 以上一个风险点为中心绕Y轴旋转(暂时以中心旋转)
    /*var lastRiskElements = this._riskElements[(this._curRoamingIndex - 1 + this._riskElements.length) % this._riskElements.length];
    this._radarMap.style.transformOrigin = lastRiskElements.transformOrigin;*/
    
    this._radarMap.style.transform = radarMapStyle;
    curRiskElements.infoPanel.classList.remove('polish');
    
    // 当前的信息面板设置透明度
    var roamingDuration = this._roamingDuration || parseFloat(getComputedStyle(this._radarMap).transitionDuration.split(', ')[0]);
    setTimeout(function(that) {
        if(that._isLyingDown()) {
            curRiskElements.infoPanel.style.opacity = 1;
        }
    }, roamingDuration * 0.5 * 1000, this);
};

// 雷达是否是倒下的状态
Radar.prototype._isLyingDown = function() {
    return this._container.classList.contains('lying-down');
};

// 雷达是否是漫游状态
Radar.prototype._isInRoamingState = function() {
    return this._radarMap.classList.contains('roaming');
};

// 添加点击事件
Radar.prototype._initEvent = function() {
    var that = this;
    this._container.addEventListener('click', function(e) {
        that.roamingToggle();
    }, false);
    
    this._riskPointsContainer.addEventListener('animationend', function(e) {
        e.target.parentElement.classList.remove('flashing');
    }, false);
    
    this._radarMap.addEventListener('transitionend', function(e) {
        if(e.propertyName != 'transform') return;
        if(e.target === e.currentTarget) {
            if(that._isLyingDown()) {
                // 地图在倒下的状态
                if(that._isInRoamingState()) {
                    // 相机漫游状态
                    that._handleCurRiskElements();  // 当前风险点漫游结束
                    that._curRoamingIndex = (that._curRoamingIndex + 1) % that._riskElements.length;
                    that._riskPointsRoaming();  // 下一个风险点漫游开始
                } else {
                    // 刚刚倒下状态
                    that._showRiskElements();
                }
            } else {
                that._hideRiskElements();
                that._resumeAnimation();
            }
        } else if(e.target.classList.contains('red-flag') && !e.target.parentElement.nextElementSibling) {
            // 最后一根红旗树立起来之后开始漫游
            that._radarMap.classList.add('roaming');  // 修改transition duration和transition timing-function
            setTimeout(function() {
                that._riskPointsRoaming();
            }, 1000);
        }
    }, false);
};


var radar = new Radar(document.querySelector('.radar'));
radar.init({scanSpeed: 2});  // 扫描的速度,单位为deg,必须为360的约数
radar.addRiskPoints([
    {
        type: 'type1',
        severity: 'critical',
        coords: [116.407395, 39.904211], // 北京
        city: '北京',
        title: '大额预授权交易异常',
        total: 3
    }, {
        type: 'type1',
        severity: 'ordinary',
        coords: [104.066801, 30.572816], // 成都
        city: '成都',
        title: '大额预授权交易异常',
        total: 3
    }, {
        type: 'type1',
        severity: 'ordinary',
        coords: [121.473701, 31.230416], // 上海
        city: '上海',
        title: '大额预授权交易异常',
        total: 3
    }, {
        type: 'type2',
        severity: 'ordinary',
        coords: [113.264385, 23.12911], // 广州
        city: '广州',
        title: '大额预授权交易异常',
        total: 3
    }
]);


document.querySelector('#radar-switch-btn').addEventListener('click', e => {
    radar.roamingToggle();
});

代码中注释比较详细,有兴趣的同学可以自己下载完整的源码进行研究,HTML5和CSS3真是非常的强大。

热门推荐

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>