微信小程序canvas自定义时间圆弧组件(点击、滑动、加减、倒计时)

项目需求

image.png

项目需求分析

image.png

以下是需求进行分析:
为了节省不必要的消耗,我们把灰色进去圆弧和有色进度圆弧以及小圆点,分两个canvas进行绘制

1:灰色圆弧进度绘制canvas

API默认起始点是从三点钟顺时针开始的,而需求起始点是从六点钟和九点钟等分点开始的,三点钟和等分点之间的夹角是135°,需要顺时针进行旋转135°的弧度值(135 * Math / 180),整个灰色圆弧的弧度长度为1.5 * Math.PI(项目需求分析的第一个图进行分析)

2:有色圆弧进度绘制canvas

a、API默认起始点是从三点钟顺时针开始的,而需求起始点是从六点钟和九点钟等分点开始的,三点钟和等分点之间的夹角是135°,需要顺时针进行旋转135°的弧度值(135 * Math / 180),画完有色圆弧进度后,需要在逆时针旋转135°的弧度值(135 * Math / 180),让起始点恢复到三点钟,为后续画小圆点做准备
b、小圆点的开始位置需要和有色圆弧进度开始位置相同,需要顺时针进行旋转135°的弧度值(135 * Math / 180),画完之后还是需要逆时针旋转135°的弧度值(135 * Math / 180),为了后续多次进行绘制
c、小圆点在每个象限的角度、坐标点的正负都不一样,我们根据API的起点在配合象限来进行判断和分析

象限 角度 坐标 以X轴计算有效角度
第四象限 [0°,90°] (x, y) angle
第三象限 (90°,180°] (-x, y) 180 - angle
第二象限 (180°,270°) (-x, -y) angle - 180
第一象限 (270°,360°] (x, -y) 360 - angle

\color{red}{angle是触摸点与圆的中心点计算得到弧度值,通过弧度值再转化为角度}\

圆弧理解示意图.png
sinA = 对边 / 斜边
cosA = 领边 / 斜边
弧度 = 角度 * Math.PI / 180
角度 = 弧度 / Math.PI * 180
两点之间的弧度值 = Math.atan2(y,x)

实现效果图


image.png

image.png

image.png

1.布局wxml

<view style="position: relative;width: 80vw;height: 80vw;border: 1rpx solid red;">
  <canvas type="2d" id="proBg" style="width: 100%;height: 100%;"></canvas>
  <canvas bindtouchstart="moveHandler" bindtouchmove="moveHandler" bindtouchend="moveEnd" type="2d" id="pro" style="position: absolute;left: 0;right: 0;top: 0;bottom: 0; width: 100%;height: 100%;"></canvas>
  <view style="position: absolute;left: 50%;top: 50%;transform: translate(-50%,-50%);">{{twoRunTimeFormat}}</view>
</view>
<view style="display: flex;flex-direction: row;margin: 20rpx 0;">
  <button bindtap="timeHandler" data-operation="{{1}}" type="default">加</button>
  <button style="margin-left: 40rpx;" bindtap="timeHandler" data-operation="{{2}}" type="default">减</button>
</view>
<view style="display: flex;flex-direction: row;margin: 20rpx 0;">
  <button bindtap="clickHandler" data-operation="{{1}}" type="default">开始</button>
  <button style="margin-left: 40rpx;" bindtap="clickHandler" data-operation="{{2}}" type="default">暂停</button>
</view>

逻辑js

// pages/arc2/index.js
const app = getApp();

Page({
  data: {
    centerPointer: null, //canvas画布中心点
    arcRadius: 0, //圆弧线的半径
    arcWidth: 0, //圆弧线的宽度
    pixelRatio: 0, //像素
    ctx: null, //pro的ctx
    validDis: 0, //有效范围的宽度
    angleVlue: 0, //角度
    radianValue: 0, //弧度
    lastRadianValue: 0, //上次绘制的弧度值
    secondRadianValue: 0, //每秒的弧度值
    helfMinRadianValue: 0, //半分钟的弧度值
    circleRadius: 0, //小圆点的半径
    twoTime: 0, //设置时间
    twoId: null, //循环标识
    twoRunTimeFormat: '00:00', //格式化的时间字符串
    isTwoTreat: false, //是否正在治疗
  },
  onLoad(options) {
    this.initData();
  },
  initData() { //初始化一些配置参数&canvas
    const width = app.globalData.width * 0.8;
    const half = width / 2;
    this.data.centerPointer = {
      x: half,
      y: half,
    }
    this.data.arcRadius = half * 0.8;
    this.data.arcWidth = half * 0.05;
    this.data.validDis = half * 0.2;
    this.data.pixelRatio = app.globalData.pixelRatio;
    this.data.secondRadianValue = 1.5 * Math.PI / (2 * 60);
    this.data.helfMinRadianValue = 1.5 * Math.PI / (2 * 2);
    // this.data.radianValue = 0 * 60 * this.data.secondRadianValue;
    this.data.circleRadius = half * 0.065;

    //画圆弧的灰色背景
    const proBg = wx.createSelectorQuery();
    proBg.select("#proBg")
      .fields({
        node: true,
        size: true,
      }).exec(res => {
        const canvas = res[0].node;
        const ctx = canvas.getContext('2d');
        canvas.width = width * this.data.pixelRatio;
        canvas.height = width * this.data.pixelRatio;
        ctx.scale(this.data.pixelRatio, this.data.pixelRatio);
        ctx.translate(this.data.centerPointer.x, this.data.centerPointer.y); //设置中心

        //API默认是从三点钟开始,需求是从六点钟和九点钟的等分点开始的,需要顺时针旋转90°+45°=135°
        ctx.rotate(135 * Math.PI / 180);

        //画灰色背景圆弧
        ctx.beginPath();
        ctx.lineCap = 'round';
        ctx.lineWidth = this.data.arcWidth;
        ctx.strokeStyle = '#BEBEBE';
        ctx.arc(0, 0, this.data.arcRadius, 0, 1.5 * Math.PI);
        ctx.stroke();

      });

    //初始化有色圆弧进度  
    const pro = wx.createSelectorQuery();
    pro.select("#pro")
      .fields({
        node: true,
        size: true,
      })
      .exec(res => {
        const canvas = res[0].node;
        const ctx = canvas.getContext('2d');
        canvas.width = width * this.data.pixelRatio;
        canvas.height = width * this.data.pixelRatio;
        ctx.scale(this.data.pixelRatio, this.data.pixelRatio);
        ctx.translate(this.data.centerPointer.x, this.data.centerPointer.y); //设置中心点

        this.data.ctx = ctx;
        this.droPro();
      });
  },
  moveHandler(e) { //触摸点击或者滑动监听事件
    //获取触摸点
    const point = {
      x: e.touches[0].x,
      y: e.touches[0].y
    };
    //计算触摸点到中心点的距离
    const distance = Math.sqrt(Math.pow(this.data.centerPointer.x - point.x, 2) + Math.pow(this.data.centerPointer.y - point.y, 2));
    //触摸范围最大有效距离
    const maxDistance = this.data.arcRadius + this.data.validDis;
    //触摸范围最小有效距离
    const minDistance = this.data.arcRadius - this.data.validDis;
    // console.log(maxDistance,distance,minDistance);
    //判断触摸距离是否在最大和最小距离之内
    if (distance >= minDistance && distance <= maxDistance) {
      //计算弧度值 初始点顺时针旋转了135°,所以这里要减135 * Math.PI / 180的弧度值
      const radian = Math.atan2(point.y - this.data.centerPointer.y, point.x - this.data.centerPointer.x) - 135 * Math.PI / 180;
      // console.log(radian);
      if (radian < 0 && radian >= -0.785) { //特殊处理  在六点钟与六点钟和九点钟等分点范围内(90°~135°) 设置为最小弧度值0
        this.data.radianValue = 0;
        return;
      } else if (radian < -0.785 && radian > -1.57) { //特殊处理  在六点钟与五点钟和六点钟等分点范围内(45°~90°) 设置为最大弧度值1.5*Math.PI
        this.data.radianValue = this.data.helfMinRadianValue * 4;
        return;
      } else {
        this.data.radianValue = radian;
      }
      //弧度值转化为角度
      this.data.angleVlue = radian / Math.PI * 180;
      // console.log('radian', radian);
      this.droPro();
    }
  },
  moveEnd(e) { //触摸结束
    let radianValue = this.data.radianValue;
    if (radianValue === this.data.lastRadianValue) return; //如果与上次的弧度值相同,则进行拦截,减少绘制,性能消耗
    const helfMinRadianValue = this.data.helfMinRadianValue;
    //根据触摸弧度值范围来判断在那个范围,从而得出最后的弧度值和时间
    let timeValue = 0; //时间--秒
    if ((radianValue > 0 && radianValue < 0.78) || (radianValue < 0 && radianValue <= -5.1)) { // 0.5 * 60秒范围
      radianValue = helfMinRadianValue;
      timeValue = 0.5 * 60;
    } else if (radianValue > -5.1 && radianValue <= -3.9) { // 1 * 60秒范围
      radianValue = helfMinRadianValue * 2;
      timeValue = 1 * 60;
    } else if (radianValue > -3.9 && radianValue <= -2.7) { // 1.5 * 60秒范围
      radianValue = helfMinRadianValue * 3;
      timeValue = 1.5 * 60;
    } else if ((radianValue > -2.7 && radianValue <= -1.5) ||
      (radianValue == helfMinRadianValue * 4)) { // 2 * 60秒范围
      radianValue = helfMinRadianValue * 4;
      timeValue = 2 * 60;
    }
    this.data.radianValue = radianValue;
    this.data.angleVlue = radianValue / Math.PI * 180; //弧度值转化为角度
    this.data.lastRadianValue = radianValue;
    const min = Math.floor(timeValue / 60);
    const second = timeValue % 60;
    this.setData({
      twoTime: timeValue,
      twoRunTimeFormat: (min < 10 ? '0' + min : min) + ':' + (second < 10 ? '0' + second : second),
    })
    this.droPro();
  },
  droPro() { //绘制有色进度和小圆点
    if (this.data.ctx == null) return;
    //清空上次一的ctx画图
    this.data.ctx.clearRect(-this.data.centerPointer.x, -this.data.centerPointer.y,
      this.data.centerPointer.x * 2, this.data.centerPointer.y * 2);

    //圆的初始点在三点钟方向,需求是六点和九点之间等分点开始,需要顺时针旋转90°+45°=135°
    this.data.ctx.rotate(135 * Math.PI / 180);

    //画有色进度圆弧
    this.data.ctx.beginPath();
    this.data.ctx.lineCap = 'round';
    this.data.ctx.lineWidth = this.data.arcWidth;
    this.data.ctx.strokeStyle = 'green';
    this.data.ctx.arc(0, 0, this.data.arcRadius, 0, this.data.radianValue);
    this.data.ctx.stroke();

    //前面顺时针旋转了135°,要把起始点恢复到3点钟开始
    this.data.ctx.rotate(-135 * Math.PI / 180);

    let angleVlue = this.data.angleVlue;
    const arcRadius = this.data.arcRadius;
    let point = {
      x: 0,
      y: 0
    }; //小圆点的坐标
    // console.log(this.data.radianValue, angleVlue)
    //是对照X轴进行计算 --- 通过角度来判断是那个一象限,得到真正的小圆点的坐标
    if (angleVlue >= 0 && angleVlue <= 90) { //第四象限
      point = {
        x: Math.cos(angleVlue * Math.PI / 180) * arcRadius,
        y: Math.sin(angleVlue * Math.PI / 180) * arcRadius
      }
    } else if (angleVlue > 90 && angleVlue <= 180) { //第三象限
      point = {
        x: -Math.cos((180 - angleVlue) * Math.PI / 180) * arcRadius,
        y: Math.sin((180 - angleVlue) * Math.PI / 180) * arcRadius
      }
    } else if (angleVlue > 180 && angleVlue <= 270) { //第二象限
      point = {
        x: -Math.cos((angleVlue - 180) * Math.PI / 180) * arcRadius,
        y: -Math.sin((angleVlue - 180) * Math.PI / 180) * arcRadius
      }

    } else if (angleVlue > 270 && angleVlue <= 360) { //第一象限
      point = {
        x: Math.cos((360 - angleVlue) * Math.PI / 180) * arcRadius,
        y: -Math.sin((360 - angleVlue) * Math.PI / 180) * arcRadius
      }
    } else if (angleVlue < 0) { //in 第二现象 & 第一象限 & 第四象限
      point = {
        x: -Math.cos((angleVlue - 180) * Math.PI / 180) * arcRadius,
        y: -Math.sin((angleVlue - 180) * Math.PI / 180) * arcRadius
      }
    }
    //目前小圆点是从3点钟为起始点,为了和有色进度圆弧保持一致,需要顺时针旋转135°,起始点保持一致
    this.data.ctx.rotate(135 * Math.PI / 180);

    //画小圆点
    this.data.ctx.beginPath();
    this.data.ctx.fillStyle = 'red';
    this.data.ctx.arc(point.x, point.y, this.data.circleRadius, 0, 2 * Math.PI);
    this.data.ctx.fill();

    //前面顺时针旋转了135°,为了后续绘制,需要逆时针旋转135°,把起始原点恢复到3点钟开始
    this.data.ctx.rotate(-135 * Math.PI / 180);

  },
  timeHandler(e) { //加减时间监听事件
    let twoTime = this.data.twoTime;
    const operation = e.currentTarget.dataset.operation;
    switch (operation) {
      case 1: //加
        if (twoTime >= 2 * 60) {
          wx.showToast({
            title: '最大时间啦',
            mask: false,
            icon: 'none',
          })
          return;
        }
        twoTime += 30;
        const min = Math.floor(twoTime / 60);
        const second = twoTime % 60;
        const twoRunTimeFormat = (min < 10 ? '0' + min : min) + ':' + (second < 10 ? '0' + second : second);
        this.setData({
          twoTime: twoTime,
          twoRunTimeFormat: twoRunTimeFormat,
        });
        break;
      case 2: //减
        if (twoTime <= 0) {
          wx.showToast({
            title: '最小时间啦',
            mask: false,
            icon: 'none'
          })
          return;
        }
        twoTime -= 30;
        const _min = Math.floor(twoTime / 60);
        const _second = twoTime % 60;
        const _twoRunTimeFormat = (_min < 10 ? '0' + _min : _min) + ':' + (_second < 10 ? '0' + _second : _second);
        this.setData({
          twoTime: twoTime,
          twoRunTimeFormat: _twoRunTimeFormat,
        })
        break;
    }
    this.data.radianValue = twoTime * this.data.secondRadianValue;
    this.data.angleVlue = this.data.radianValue / Math.PI * 180;
    this.droPro();
  },
  clickHandler(e) { //点击事件
    const operation = e.currentTarget.dataset.operation;
    switch (operation) {
      case 1:
        let twoTime = this.data.twoTime;
        if (twoTime == 0) return;
        if (this.data.isTwoTreat) {
          wx.showToast({
            title: '正在倒计时',
            mask: false,
            icon: 'none'
          })
          return;
        }
        this.setData({
          isTwoTreat: true,
        })
        this.data.twoId = setInterval(() => {
          if (twoTime <= 0) {
            this.setData({
              twoTime: 0,
              isTwoTreat: false,
            })
            clearInterval(this.data.twoId);
            return;
          }
          twoTime--;
          console.log(twoTime);
          this.data.radianValue = twoTime * this.data.secondRadianValue;
          this.data.angleVlue = this.data.radianValue / Math.PI * 180;
          const min = Math.floor(twoTime / 60);
          const second = twoTime % 60;
          const twoRunTimeFormat = (min < 10 ? '0' + min : min) + ':' + (second < 10 ? '0' + second : second);
          this.droPro();
          this.setData({
            twoRunTimeFormat: twoRunTimeFormat,
          });
        }, 1000);
        break;
      case 2:
        clearInterval(this.data.twoId);
        this.setData({
          isTwoTreat: false,
          twoTime: 0,
          twoRunTimeFormat: '00:00',
          radianValue: 0,
          angleVlue: 0,
        })
        this.droPro();
        break;
    }
  },
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容