【组件设计】比例圆

197 阅读2分钟

比例圆

这期的需求是要做一个比例圆,首先的想到的是canvas,以此为基础画了一个比例圆,主要是处理从90°作为起点,以及弧度和角度之间的转换。话不多说,直接上代码!

<template>
  <canvas ref="ratePie" id="ratePie" width="16px" height="16px"></canvas>
</template>

<script>
export default {
  props: {
    rate: {
      type: [Number, String],
      default: 10,
    },
    percentage: {
      type: [Number, String],
      default: 100,
    }  
  },
  watch: {
    rate(v) {
      this.$nextTick(() => {
        this.pieInit();
      });
    },
  },
  mounted() {
    this.pieInit();
  },
  methods: {
    pieInit() {
      const canvas = this.$refs.ratePie;
      const ctx = canvas.getContext("2d");
      // 垂直从270度开始, 后续转动角度 = (比例值 * 360) + 270
      const deg = 270 + (parseFloat(this.rate) / 100) * 360;
      // console.log('deg', deg);
      const radius = 8//Number(this.radius);
      const x = 8//ctx.canvas.height / 2; 
      const y = 8//ctx.canvas.width / 2; 
      const color = "#0740FF";
      ctx.beginPath();
      // ctx.translate(100, 100);
      ctx.moveTo(x, y);
      ctx.arc(x, y, radius, this.getRad(270), this.getRad(deg));
      ctx.closePath();
      // ctx.lineTo(0, 0);
      // ctx.strokeStyle = color;
      // ctx.stroke();
      ctx.fillStyle = color;
      ctx.fill();
    },
    // 1° = π / 180
    // 1弧度 = 180° / π
    // 获取弧度
    getRad(deg) {
      return (Math.PI * deg) / 180;
    },
    // 获取度数
    getDeg(rad) {
      return (rad * 180) / Math.PI;
    },
    // getXY(min) {
    //   const r = this.radius;
    //   const x = Math.cos((deg * Math.PI) / 180) * r; // 已知半径和角度,求 x 轴的长度
    //   const y = Math.sin((deg * Math.PI) / 180) * r; // 已知半径和角度,求 y 轴的长度
    //   return { x, y };
    // },
  },
};
</script>

在后面提测阶段,发现canvas最外层边线失真模糊问题,研究了一下发现其实canvas本质上只是提供了一块画布,然后根据当前画布大小来绘图,但在不同屏幕上存在逻辑像素与物理像素的差别,其实就涉及到了设备像素比devicePixelRatio的概念,所展示出的图形势必会有拉伸,导致模糊。具体原因可自行百度,这边加上结局方案,原理其实就是先放大canvas画布,保证图片的展示保真度,再用物理缩小。

const canvas = document.createElement('canvas');
// 获取到屏幕倒是是几倍屏。
const getPixelRatio = function(context) {
  const backingStore = context.backingStorePixelRatio ||
    context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1;
   return (window.devicePixelRatio || 1) / backingStore;
};
 // iphone6下得到是2 
const pixelRatio = getPixelRatio(canvas);
// 设置canvas的真实宽高
canvas.width = pixelRatio * canvas.offsetWidth; // 相当于 2 * 375 = 750 
canvas.height = pixelRatio * canvas.offsetHeight;
canvas.style.width = pixelRatio * canvas.offsetWidth;
canvas.style.height = pixelRatio * canvas.offsetHeight;

// 此处绘图的时候也需要 * devicePixelRatio 和放大的 canvas 相匹配
ctx.arc(100 * pixelRatio, 100 * pixelRatio, 2 * pixelRatio, 0, 2 * Math.PI);

不过实际体验中,发现似乎并不能处理的很好,因为UI的设计稿上的比例图真的太小了(18px*18px),把我给整麻了,可能当时开发时间太紧张,所以也没用很充分的调试时间,后面就用svg重新设计了一下,简单处理了,同理主要还是从90°作为起点处理一下,此处记录一下,其实这组件应该还是能优化,原本都没打算用<circle>,但是没时间细调了= =

<template>
  <svg class="ratePie" ref="ratePie" id="ratePie" xmlns='http://www.w3.org/2000/svg' version="1.1" :style="ratePieStyle">
    <circle v-show="showType === 'circle'" ref="ratePieCircle" />
    <path v-show="showType === 'pie'" ref="ratePiePath" />
  </svg>
</template>

<script>
export default {
  props: {
    rate: {
      type: [Number, String],
      default: 10,
    },
    // 外层盒子大小
    size:{
      type: Number,
      default: 16
    }
  },
  computed: {
    ratePieStyle() {
      const style = {
        width: `${this.size}px`,
        height: `${this.size}px`,
      }
      return style;
    }
  },
  watch: {
    rate(v) {
      this.$nextTick(() => {
        this.pieInit();
      });
    },
  },
  data() {
    return {
      showType: 'pie'
    }
  },
  mounted() {
    this.pieInit();
  },
  methods: {
    draw() {
      const step = [];
      const cxy = Number(this.size / 2); // 中心 x 和 y 
      const r = cxy;
      const deg = Number(this.rate) / 100 * 360; // 转动角度
      const rad = this.getRad(deg); // 弧度
      const pointx = cxy + (cxy * Math.sin(rad)); // 结束点 x
      const pointy = cxy - (cxy * Math.cos(rad)); // 结束点 y
      step.push(`M ${cxy} ${cxy} L ${r} 0`);
      step.push(' ');
      step.push(`A ${r} ${r}, 0, ${deg > 180 ? 1 : 0}, 1, ${pointx} ${pointy}`);
      step.push(' ');
      step.push('Z');
      return step.join('');
    },
    pieInit() {
      const rate = Number(this.rate);
      if(rate <= 0) {
        this.showType = 'none';
      } else if(rate >= 100) {
        const circle = this.$refs.ratePieCircle;
        if(circle) {
          const cxy = Number(this.size / 2);
          circle.setAttribute('cx', cxy);
          circle.setAttribute('cy', cxy);
          circle.setAttribute('r', cxy);
          circle.setAttribute('stroke', 'none');
          circle.setAttribute('fill', '#0740FF');
        }
        this.showType = 'circle';
      } else {
        const path = this.$refs.ratePiePath;
        if(path) {
          path.setAttribute('stroke', 'transparent');
          path.setAttribute('stroke-width', 1);
          path.setAttribute('fill', '#0740FF');
          path.setAttribute('d', this.draw());
        }
        this.showType = 'pie';
      }
    },
  },
};
</script>