• 技术文章 >微信小程序 >小程序开发

    手把手教你在微信小程序中使用canvas绘制天气折线图(附代码)

    青灯夜游青灯夜游2022-03-08 20:02:55转载2744
    微信小程序中如何绘制天气折线图?下面本篇文章就来给大家介绍一下在微信小程序中使用canvas绘制天气折线图的方法,以及使用三阶贝塞尔曲线拟合温度点,使之变得圆滑,曲线底部有背景色,希望对大家有所帮助!

    折线

    效果图:

    1.gif

    自定义组件 line-chart

    <canvas type="2d" id="line" class="line-class" style="width:{{width}}px;height:{{height}}px" />
    Component({
      externalClasses: ['line-class'],
      properties: {
        width: String,
        height: String,
        data: Array,
      },
      observers: {
        width() {
          // 这里监听 width 变化重绘 canvas
          // 动态传入 width 好像只能这样了..
          const query = this.createSelectorQuery();
          query
            .select('#line')
            .fields({ node: true, size: true })
            .exec(res => {
              const canvas = res[0].node;
              const ctx = canvas.getContext('2d');
              const width = res[0].width; // 画布宽度
              const height = res[0].height; // 画布高度
    
              console.log(`宽度: ${width}, 高度: ${height}`);
    
              const dpr = wx.getSystemInfoSync().pixelRatio;
              canvas.width = width * dpr;
              canvas.height = height * dpr;
              ctx.scale(dpr, dpr);
    
              // 开始绘图
              this.drawLine(ctx, width, height, this.data.data);
            });
        },
      },
      methods: {
        drawLine(ctx, width, height, data) {
          const Max = Math.max(...data);
          const Min = Math.min(...data);
    
          // 把 canvas 的宽度, 高度按一定规则平分
          const startX = width / (data.length * 2), // 起始点的横坐标 X
            baseY = height * 0.9, // 基线纵坐标 Y
            diffX = width / data.length,
            diffY = (height * 0.7) / (Max - Min); // 高度预留 0.2 写温度
    
          ctx.beginPath();
          ctx.textAlign = 'center';
          ctx.font = '13px Microsoft YaHei';
          ctx.lineWidth = 2;
          ctx.strokeStyle = '#ABDCFF';
    
          // 画折线图的线
          data.forEach((item, index) => {
            const x = startX + diffX * index,
              y = baseY - (item - Min) * diffY;
    
            ctx.fillText(`${item}°`, x, y - 10);
            ctx.lineTo(x, y);
          });
          ctx.stroke();
    
          // 画折线图背景
          ctx.lineTo(startX + (data.length - 1) * diffX, baseY); // 基线终点
          ctx.lineTo(startX, baseY); // 基线起点
          const lingrad = ctx.createLinearGradient(0, 0, 0, height * 0.7);
          lingrad.addColorStop(0, 'rgba(255,255,255,0.9)');
          lingrad.addColorStop(1, 'rgba(171,220,255,0)');
          ctx.fillStyle = lingrad;
          ctx.fill();
    
          // 画折线图上的小圆点
          ctx.beginPath();
          data.forEach((item, index) => {
            const x = startX + diffX * index,
              y = baseY - (item - Min) * diffY;
    
            ctx.moveTo(x, y);
            ctx.arc(x, y, 3, 0, 2 * Math.PI);
          });
          ctx.fillStyle = '#0396FF';
          ctx.fill();
        },
      },
    });

    data 就是温度数组,如 [1, 2, ...]

    因为不知道温度数值有多少个,因此这里的 width 动态传入

    有个小问题,就是宽度过大的话真机不会显示...

     // 获取 scroll-view 的总宽度
     wx.createSelectorQuery()
          .select('.hourly')
          .boundingClientRect(rect => {
            this.setData({
              scrollWidth: rect.right - rect.left,
            });
          })
          .exec();
    <view class="title">小时概述</view>
    <scroll-view scroll-x scroll-y class="scroll" show-scrollbar="{{false}}" enhanced="{{true}}">
        <view class="hourly">
          <view wx:for="{{time}}" wx:key="index">{{item}}</view>
        </view>
        <line-chart line-class="line" width="{{scrollWidth}}" height="100" data="{{temp}}" />
    </scroll-view>

    这里写 scroll-x 和 scroll-y,要不会出现绝对定位偏移的问题,也不知道为什么

    2.gif

    .scroll {
      position: relative;
      height: 150px;
      width: 100%;
    }
    
    .hourly {
      display: flex;
      height: 150px;
      position: absolute;
      top: 0;
    }
    
    .hourly > view {
      min-width: 3.5em;
      text-align: center;
    }
    
    .line { // 折线图绝对定位到底部
      position: absolute;
      bottom: 0;
    }

    这里使用绝对定位其实是想模拟墨迹天气这种折线图和每一天在一个块内的效果,所以 hourly 要和 scroll-view 等高,canvas 需要定位一下

    主要是不知道墨迹天气怎么实现的,只能暂时这样

    3.gif

    三阶贝塞尔曲线

    效果图

    4.gif

    emmm,好像并不怎么圆滑

    计算控制点

    首先写一个点类

    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }

    Canvas贝塞尔曲线绘制工具 (karlew.com)

    http://wx.karlew.com/canvas/bezier/

    通过上面这个网站可以知道三阶贝塞尔曲线各个参数的意义

    5.gif

    也就是使用 bezierCurveTo 的时候最后一个点是下一个点,前两个是控制点

    控制点的计算参考: 贝塞尔曲线控制点确定的方法 - 百度文库

    https://wenku.baidu.com/view/c790f8d46bec0975f565e211.html

    浓缩一下就是

    6.gif

    这里的 a 和 b 可以是任意正数

    因此定义一个计算某点的控制点 A 和 B 的方法

    /**
     * 计算当前点的贝塞尔曲线控制点
     * @param {Point} previousPoint: 前一个点
     * @param {Point} currentPoint: 当前点
     * @param {Point} nextPoint1: 下一个点
     * @param {Point} nextPoint2: 下下个点
     * @param {Number} scale: 系数
     */
    calcBezierControlPoints(
      previousPoint,
      currentPoint,
      nextPoint1,
      nextPoint2,
      scale = 0.25
    ) {
      let x = currentPoint.x + scale * (nextPoint1.x - previousPoint.x);
      let y = currentPoint.y + scale * (nextPoint1.y - previousPoint.y);
    
      const controlPointA = new Point(x, y); // 控制点 A
    
      x = nextPoint1.x - scale * (nextPoint2.x - currentPoint.x);
      y = nextPoint1.y - scale * (nextPoint2.y - currentPoint.y);
    
      const controlPointB = new Point(x, y); // 控制点 B
    
      return { controlPointA, controlPointB };
    }

    这里 scale 就是 a 和 b,不过将它们的取值相等

    但是第一个点没有 previousPoint,倒数第二个点没有 nextPoint2

    因此当点是第一个的时候,使用 currentPoint 代替 previousPoint

    当倒数第二个点的时候,使用 nextPoint1 代替 nextPoint2

    7.gif

    至于最后一个点,不需要做任何事,因为 bezierCurveTo 第三个参数就是下一个点,只需要提供坐标就能连起来,不需要计算控制点

    因此绘制三阶贝塞尔曲线的方法:

    /**
     * 绘制贝塞尔曲线
     * ctx.bezierCurveTo(控制点1, 控制点2, 当前点);
     */
    drawBezierLine(ctx, data, options) {
      const { startX, diffX, baseY, diffY, Min } = options;
    
      ctx.beginPath();
      // 先移动到第一个点
      ctx.moveTo(startX, baseY - (data[0] - Min) * diffY);
    
      data.forEach((e, i) => {
        let curPoint, prePoint, nextPoint1, nextPoint2, x, y;
    
        // 当前点
        x = startX + diffX * i;
        y = baseY - (e - Min) * diffY;
        curPoint = new Point(x, y);
    
        // 前一个点
        x = startX + diffX * (i - 1);
        y = baseY - (data[i - 1] - Min) * diffY;
        prePoint = new Point(x, y);
    
        // 下一个点
        x = startX + diffX * (i + 1);
        y = baseY - (data[i + 1] - Min) * diffY;
        nextPoint1 = new Point(x, y);
    
        // 下下个点
        x = startX + diffX * (i + 2);
        y = baseY - (data[i + 2] - Min) * diffY;
        nextPoint2 = new Point(x, y);
    
        if (i === 0) {
          // 如果是第一个点, 则前一个点用当前点代替
          prePoint = curPoint;
        } else if (i === data.length - 2) {
          // 如果是倒数第二个点, 则下下个点用下一个点代替
          nextPoint2 = nextPoint1;
        } else if (i === data.length - 1) {
          // 最后一个点直接退出
          return;
        }
    
        const { controlPointA, controlPointB } = this.calcBezierControlPoints(
          prePoint,
          curPoint,
          nextPoint1,
          nextPoint2
        );
    
        ctx.bezierCurveTo(
          controlPointA.x,
          controlPointA.y,
          controlPointB.x,
          controlPointB.y,
          nextPoint1.x,
          nextPoint1.y
        );
      });
    
      ctx.stroke();
    },

    【相关学习推荐:小程序开发教程

    以上就是手把手教你在微信小程序中使用canvas绘制天气折线图(附代码)的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    上一篇:小程序中rpx和px有什么区别 下一篇:浅析小程序中什么是behaviors?怎么创建和使用?
    20期PHP线上班

    相关文章推荐

    • 【活动】充值PHP中文网VIP即送云服务器• 微信小程序与微商小程序的区别是什么• 门店小程序和功能小程序的区别有哪些• 微信小程序和微信公众号的区别有哪些• 总结分享一些小程序开发中遇到的问题(帮忙避坑)• 什么类型的微信号可以开通门店小程序• 一个营业执照可申请几个小程序
    1/1

    PHP中文网