闲来无事,弄个“纯CSS”的伪3D柱状图吧~

简介: 闲来无事,弄个“纯CSS”的伪3D柱状图吧~

前言


最近在忙公司的一个大屏项目,因为是半路参加的,只是为之前的同事修改一些样式问题,并把之前的UI切换改变成用代码编写的形式,提高一下加载效果。


在这个过程中也算是有一些比较有意思的效果,今天先弄一个 “伪3D” 的柱状图吧。


样式设计


UI 给的稿子上,通常会带有一些“细微”的样式,用来提高观赏性,但是这里为了加快速度,我们就先实现一个比较 “比较纯粹” 的柱状图。


首先上效果图:


网络异常,图片无法展示
|


我们大致拆分一下整体结构


首先是外层的划分,包含:顶部的盖子,中间反映数据值的圆柱,以及底部的底座;其中顶部和底部的样式基本一致。然后中间部分又可以分成 外部的渐变遮罩,和 内部的实际数据圆柱


然后,我们就可以着手实现了。


大家也别说为什么下面要用 Vue,主要还是为了在项目中写成组件复用,显示效果的实现还是全部用的 css 的。


圆柱的实现


圆柱的实现呢是参考了 吼吼酱 -- css3绘制3D图形——圆锥、圆柱、柱状图 一文的。


单个圆柱拆分成 3 个部分来实现,结合 dom 的 伪类 before 和 after 刚刚好可以实现,配合渐变效果 即可模拟出一个 3d 圆柱。

网络异常,图片无法展示
|

网络异常,图片无法展示
|


这里我们以我们UI图最顶上的一部分来举例。整个 Dom 结构如下:


<div class="weighted-cylinder">
  <div class="weighted-cylinder__header"></div>
</div>


配合一个 css 样式:


.weighted-cylinder {
  width: 100%;
  height: 100%;
  position: relative;
  box-sizing: border-box;
  padding: 0;
  .weighted-cylinder__header {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    z-index: 20;
    height: 10%;
    background: linear-gradient(to right, #6776f8, #4bc5fe);
    &::before,
    &::after {
      content: "";
      position: absolute;
      width: 100%;
      height: 40px;
      border-radius: 50%;
    }
    &::before {
      z-index: 3;
      top: -20px;
      background: linear-gradient(to right, #66c8ff, #92e1fe);
    }
    &::after {
      z-index: 1;
      bottom: -20px;
      background: linear-gradient(to right, #6776f8, #4bc5fe);
    }
  }


这里用了绝对定位 来方便布局哈


此时就得到了这样的一个圆柱:


网络异常,图片无法展示
|


当然,这里也需要注意一下几点


  1. 采用绝对定位除了方便布局,也是为了设置各个部分的层级,避免出现错误覆盖


  1. 单个圆柱中,我们用 before 模拟了盖子部分,after 模拟的底座部分,所以顶部盖子的层级应该是最高的,而底座则是最低的


  1. 在整体的柱状图中,为了模拟 3D 效果,中间的数值反应的圆柱在到达最大值时肯定还是被最顶上的 header 部分覆盖的,所以 header 部分的层级也是最高的


  1. 底部的元素应该可以被中间部分覆盖,所以肯定也是最低的


当然还有渐变色的处理,底座部分模拟了一部分圆柱外立面,所以需要与实际的柱面渐变配置一致;而盖子部分则应该选取同色系中的亮色,来模拟高亮


组件的设计与实现


上面介绍了单个圆柱的实现,那么整个组件的实现也就差不多了;只是在单个圆柱的基础上再实现三个圆柱,调整相互的位置即可实现。


整个组件的 dom 部分结构如下:


<div class="weighted-cylinder">
  <div class="weighted-cylinder__header"></div>
  <div class="weighted-cylinder__content">
    <div class="cylinder__content-inner" :style="computedStyle"></div>
  </div>
  <div class="weighted-cylinder__footer"></div>
</div>


其中 header、content、footer 分别是外层的盖子、中心数据展示区、底座 三个部分;因为数据显示的部分需要计算高度和定位,并且需要确定层级,所以将它放置在 content 中会更加方便。


因为作为 content 的子元素,它自己的层级调整不会影响外部 content 的整体层级,并且使用百分比高度也更加容易计算,100% 时就是 content 区域的高度


该组件接收一个小数用来确定 数据映射圆柱的高度,通过 Vue 计算属性和动态样式的方式来处理:


export default {
  name: "WeightedCylinder",
  props: {
    data: {
      type: Number,
      default: 0.2
    }
  },
  computed: {
    computedStyle() {
      const style = { height: `${this.data * 100}%` };
      return style;
    }
  }
};


最后,则是我们的 css 实现部分


因为外层的 header 盖子与 footer 底座其实与上面说的单个元素样式一样,只需要修改定位参数即可。


而中间的 content 部分其实也只是 改变了渐变背景色的透明度,这里可以直接通过rgba完成。


.weighted-cylinder__content {
  position: absolute;
  left: 0;
  right: 0;
  top: 10%;
  bottom: 10%;
  z-index: 10;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(102, 200, 255, 0.2));
}


最后,就是 inner 数据映射圆柱了。因为高度不定(数据不确定),并且圆柱的基准位置一般也是以底部为准,所以这里通过 绝对定位inner 部分固定在content 区域的底部,然后直接调整圆柱高度。至于样式,也和上面的差不多。


.cylinder__content-inner {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 0;
  background: linear-gradient(to right, #12e49a, #2c86e1);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  &::before,
  &::after {
    content: "";
    position: absolute;
    width: 100%;
    height: 32px;
    border-radius: 50%;
  }
  &::before {
    top: -16px;
    background: linear-gradient(to right, #53d3fa, #56d9f8);
  }
  &::after {
    bottom: -16px;
    background: linear-gradient(to right, #12e49a, #2c86e1);
  }
}


这样,我们就得到了一个完整的 3D 柱状图了。


网络异常,图片无法展示
|


扩展


当然,作为一个柱状图,很多时候我们要 在内部显示这个柱状图反应的哪个类型的数据,具体数值是多少,所以我们可以在内部增加两个标签用来显示这两个信息,并且 添加一个插槽来显示开发者需要自定义的内容


当然,既然是 可能会显示 其他信息,那么数据类型和数据数值的显示应该也可以控制,这时就需要再添加对应的控制参数了。


最终我们的 dom 结构如下:


<div class="weighted-cylinder">
  <div class="weighted-cylinder__header"></div>
  <div class="weighted-cylinder__content">
    <div class="cylinder__content-inner" :style="computedStyle">
      <div v-if="showData" class="cylinder__content-data">{{ data }}</div>
      <div v-if="showTitle" class="cylinder__content-title">{{ title }}</div>
      <slot></slot>
    </div>
  </div>
  <div class="weighted-cylinder__footer"></div>
</div>


然后定义了相关的 props 配置和计算属性:


props: {
  data: {
    type: Number,
    default: 0.2
  },
  title: {
    type: String,
    default: "权重"
  },
  showData: {
    type: Boolean,
    default: true
  },
  showTitle: {
    type: Boolean,
    default: true
  },
  reverse: {
    type: Boolean,
    default: false
  }
},
computed: {
  computedStyle() {
    const style = { height: `${this.data * 100}%` };
    if (this.reverse) {
      style.flexDirection = "column-reverse";
    }
    return style;
  }
}


这里还增加了一个 reverse 配置,用来控制 title 与 data 数值的顺序(谁在上谁在下)。这样我们还需要设置相应的样式:


.cylinder__content-inner {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.cylinder__content-data,
.cylinder__content-title {
  z-index: 100;
}
.cylinder__content-data {
  font-size: 20px;
  font-weight: bold;
}
.cylinder__content-title {
  font-size: 16px;
}


最终我们就得到了想要的效果:


image.png


Markup:

<div id="app">
    <div class="demos-content">
      <weighted-cylinder :data="nums[0]" title="权重" />
    </div>
    <div class="demos-content">
      <weighted-cylinder :data="nums[1]" title="占比" :format="(val) => (val * 100).toFixed(2)" title="占比" unit="%"  />
    </div>
    <div class="demos-content">
      <weighted-cylinder :data="nums[2]" title="哈哈" reverse />
    </div>
    <div class="demos-content">
      <weighted-cylinder :data="nums[3]" title="哈哈">
        <div style="z-index: 10">这是 slot</div>
      </weighted-cylinder>
    </div>
</div>


Style:

#app {
  width: 100vw;
  height: 100vh;
  display: flex;
}
.demos-content {
  margin: 5vh auto;
  width: 10vw;
  height: 24vw;
}
.weighted-cylinder {
  width: 100%;
  height: 100%;
  position: relative;
  box-sizing: border-box;
  padding: 0;
  .weighted-cylinder__header,
  .weighted-cylinder__content,
  .weighted-cylinder__footer,
  .weighted-cylinder__luminous {
    position: absolute;
    left: 0;
    right: 0;
  }
  .weighted-cylinder__header,
  .weighted-cylinder__footer {
    height: 10%;
    background: linear-gradient(to right, #6776f8, #4bc5fe);
    &::before,
    &::after {
      content: "";
      position: absolute;
      width: 100%;
      height: 40px;
      border-radius: 50%;
    }
    &::before {
      z-index: 3;
      top: -20px;
      background: linear-gradient(to right, #66c8ff, #92e1fe);
    }
    &::after {
      z-index: -1;
      bottom: -20px;
      background: linear-gradient(to right, #6776f8, #4bc5fe);
    }
  }
  .weighted-cylinder__header {
    top: 0;
    z-index: 20;
    //box-shadow: 0 0 8px 0 #92e1fe;
    filter: drop-shadow(0 0 8px #92e1fe);
    &::before {
      //box-shadow: 0 0 8px 0 #92e1fe;
      filter: drop-shadow(0 0 8px #92e1fe);
    }
  }
  .weighted-cylinder__footer {
    bottom: 0;
    z-index: 2;
    //box-shadow: 0 0 16px 0 #4bc5fe;
    filter: drop-shadow(0 0 16px #4bc5fe);
    &::after {
      //box-shadow: 0 6px 8px 0 #4bc5fe;
      filter: drop-shadow(0 6px 8px #4bc5fe);
    }
  }
  .weighted-cylinder__content {
    top: 10%;
    bottom: 10%;
    z-index: 10;
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(102, 200, 255, 0.4));
  }
  .weighted-cylinder__luminous {
    top: 10%;
    bottom: 10%;
    z-index: 11;
    pointer-events: none;
    &::before {
      content: "";
      position: absolute;
      width: 2%;
      min-width: 2px;
      height: 5%;
      min-height: 16px;
      border-radius: 2px;
      background: linear-gradient(to bottom, rgba(39, 111, 171, 0.8), rgba(102, 255, 232, 0.52));
    }
  }
  .cylinder__content-inner {
    position: absolute;
    left: 8%;
    right: 8%;
    bottom: 0;
    background: linear-gradient(to right, #12e49a, #2c86e1);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    transition: all ease-in-out 0.2s;
    &::before,
    &::after {
      content: "";
      position: absolute;
      width: 100%;
      height: 32px;
      border-radius: 50%;
    }
    &::before {
      top: -16px;
      z-index: 10;
      background: linear-gradient(to right, #53d3fa, #56d9f8);
    }
    &::after {
      bottom: -16px;
      z-index: 1;
      background: linear-gradient(to right, #12e49a, #2c86e1);
    }
  }
  .cylinder__content-data,
  .cylinder__content-title {
    z-index: 100;
  }
  .cylinder__content-data {
    font-size: 20px;
    font-weight: bold;
  }
  .cylinder__content-title {
    font-size: 16px;
  }
}


Script:

const WeightedCylinder = Vue.component('weighted-cylinder', {
  template: `  <div class="weighted-cylinder">
    <div class="weighted-cylinder__header"></div>
    <div class="weighted-cylinder__content">
      <div class="cylinder__content-inner" :style="computedStyle">
        <div v-if="showData" class="cylinder__content-data">{{ data }}</div>
        <div v-if="showTitle" class="cylinder__content-title">{{ title }}</div>
        <slot></slot>
      </div>
    </div>
    <div class="weighted-cylinder__footer"></div>
  </div>`,
  name: "WeightedCylinder",
  props: {
    data: {
      type: Number,
      default: 0.2
    },
    max: {
      type: Number,
      default: 1
    },
    title: {
      type: String,
      default: "权重"
    },
    unit: {
      type: String,
      default: ""
    },
    format: {
      type: Function
    },
    showData: {
      type: Boolean,
      default: true
    },
    showTitle: {
      type: Boolean,
      default: true
    },
    showUnit: {
      type: Boolean,
      default: true
    },
    reverse: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    computedData() {
      let data = this.data;
      if (this.format && typeof this.format === "function") {
        data = this.format(data);
      }
      if (this.unit && this.showUnit) {
        data += this.unit;
      }
      return data;
    },
    computedStyle() {
      const style = { height: `${(this.data / this.max) * 100}%` };
      if (this.reverse) {
        style.flexDirection = "column-reverse";
      }
      return style;
    }
  }
})
const app = new Vue({
  el: "#app",
  name: "App",
  components: { "weighted-cylinder": WeightedCylinder },
  data() {
    return {
      nums: [0.67, 0.82, 0.24, 0.14]
    };
  },
  mounted() {
    this.updateData();
  },
  methods: {
    updateData() {
      this.nums = this.nums.map(() => +Math.random().toFixed(2));
      setTimeout(this.updateData, 1 * 1000);
    }
  }
})


其他扩展


看到这里我相信大家肯定还有其他更多的需求需要对这个代码进行调整,比如中间的文字大小、文字颜色等,另外也有可能需要 调整渐变色、增加box-shadow模拟发光效果等。


这些当然可以根据实际情况进行调整,希望大家有这样的或者更好的意见,也可以提出来帮助我一起改进这个组件。


目录
相关文章
|
6月前
CSS3自动旋转正方体3D特效
CSS3自动旋转正方体3D特效
53 3
CSS3自动旋转正方体3D特效
|
28天前
|
前端开发 JavaScript API
探索 CSS Houdini:轻松构建酷炫的 3D 卡片翻转动画
本文通过构建一个 3D 翻卡动画深入探讨了 CSS Houdini 的强大功能,展示了如何通过 Worklets、自定义属性、Paint API 等扩展 CSS 的能力,实现高度灵活的动画效果。文章首先介绍了 Houdini 的核心概念与 API,并通过构建一个动态星空背景、圆形进度条以及交互式 3D 翻卡动画的实际示例,展示了如何利用 CSS Houdini 赋予网页设计更多创造力。最后,还演示了如何将这种 3D 翻卡效果集成到公司网站中,提升用户体验。CSS Houdini 的创新能力为网页设计带来了前所未有的灵活性,推动了前端开发迈向新的高度。
24 0
探索 CSS Houdini:轻松构建酷炫的 3D 卡片翻转动画
|
3月前
|
前端开发
HTML+CSS动画实现动感3D卡片墙:现代Web设计的视觉盛宴
HTML+CSS动画实现动感3D卡片墙:现代Web设计的视觉盛宴
|
5月前
|
前端开发 JavaScript UED
CSS进阶-3D变换与透视效果
【6月更文挑战第15天】CSS3的3D变换和透视效果增强了网页的深度感。通过`rotateX/Y/Z`旋转和`translateZ`移动,结合`perspective`属性可创建3D空间。`perspective`定义观察者与Z轴的距离,影响元素的缩放感。常见问题包括过度失真和元素遮挡顺序,可通过调整`perspective`值和使用`z-index`解决。进阶技巧涉及层叠上下文理解和3D卡片翻转效果,通过实践与探索,设计师能更好地利用这些工具创新用户体验。
104 6
|
4月前
|
前端开发 JavaScript
【HTML+CSS+JavaScript】3d-boxes-background
【HTML+CSS+JavaScript】3d-boxes-background
33 0
|
4月前
|
前端开发 JavaScript
前端 CSS 经典:3D Hover Effect 效果
前端 CSS 经典:3D Hover Effect 效果
50 0
|
4月前
|
前端开发 JavaScript
前端 CSS 经典:3D 渐变轮播图
前端 CSS 经典:3D 渐变轮播图
104 0
|
6月前
|
前端开发
纯css实现的3D立体鸡蛋动画视觉效果
纯css实现的3D立体鸡蛋动画视觉效果
66 6
纯css实现的3D立体鸡蛋动画视觉效果
|
前端开发 测试技术 容器
【CSS】如何给自己头像打造3D动态悬浮效果?(慎入!有点难)
有设想过,当你鼠标移入头像时,展现出从圆圈或洞中探出的那种效果吗?我有类似的想法,但采用了不同的方式并添加了一些动画。我感觉非常实用,并且可以产生简洁的悬停效果,可以在您自己的头像之类的东西上使用。
【CSS】如何给自己头像打造3D动态悬浮效果?(慎入!有点难)
|
6月前
|
UED
css3 2D与3D转换
css3 2D与3D转换
73 0

热门文章

最新文章