canvas在组件中循环画图时图片闪烁

起因 && bug 复现

有一个页面,要用 canvas 画出背景图片和文字,并且定时刷新页面。

这个图分三层, 第一层画红色和蓝色的矩形框,第二层画背景图,第三层写文字。

 image

原来的代码中我直接把canvasContext.fillRectcanvasContext.drawImagecacanvasContextnvas.fillText 放在一个函数中,循环多少次就画多少次。

这种写法直接放在页面中时,页面中的背景图片并不会闪烁。

但后来我优化了代码,把 canvas 画图部分挪到了组件中,然后在上级页面中调用 canvas 的组件页面。每秒更新数据,也就是每秒 canvas 重绘。

新版 canvas (type="2d") 获取 canvas 节点的方式变了: uniapp + 微信小程序:新版 canvas 常用 api 及注意事项

或许是子页面与主页面的通讯增加了性能开销,也可能是wx.createSelectorQuery()this.createSelectorQuery() 开销不同,总之这个页面开始闪烁,每次刷新都只能随机显示一张背景图。

 image

分析

为了判断是哪一部分性能消耗大,我注释了 canvasContext.drawImage 部分,发现刷新时页面不闪烁了。由此判定是 背景图片 的问题。

 image

经过进一步排查,this.createSelectorQuery() 性能消耗没有我想的大,问题还在别处。

this.createSelectorQuery()
  .select(`#oil${id}`)
  .node(({ node: canvas }) => {
    console.log("canvas: ", canvas);
  })
  .exec();

进一步排查,问题的确在图片上,但不是我以为的 canvasContext.drawImage 而在加载图片资源上。

因为在 canvas 中,不能直接使用 canvasContext.drawImage(imageUrl),而要先用 canvasContext.createImage(); 创建一个图片实例,再修改 bgImg.src ,然后才能使用 canvasContext.drawImage

const bgImg = canvas.createImage();
bgImg.src = "/static/hcbgimg.png";
​
const canvasContext = canvas.getContext("2d");
canvasContext.drawImage(bgImg, 0, 0, bgimgWidth, bgimgHeight);

在原先代码中,每次循环都重新创建图片实例,也就是每次都调用 canvas.createImage() ,所以页面闪烁。

解决办法

  1. 创建 canvas 实例和 canvasContext.draw分开,创建 canvas 实例只运行一次,每次更新数据后 canvas 实例不重新调用。

  2. 把创建图片实例放在创建第一个 canvas 实例之后,也只调用一次

  3. bgImg 缓存起来,以后每一次 canvasContext.drawImage(bgImg) 都调用这个缓存的对象

组件代码(部分)

<template >
  <view>
    <view v-for= "item in tankList" >
      <canvas  type= "2d" :id="'oil' + item.id"  class= "canvas" ></canvas >
    </view>
  </view>
</template ><script >
import {  converIntToRgb,  getSysInfo }  from "/utils/utils.js"; 
​
export  default { 
  props: { 
    tankList: { 
      type:  Array, 
      default: [], 
    },
  },
  data() { 
    return {
      // 油罐图片的宽高
      bgimgWidth: 0,
      bgimgHeight: 0,
​
      context: [],
      canvasList: [],
      bgImg: null,
    };
  },
  mounted() {
    const screenWidth = getSysInfo();
    this.bgimgWidth = screenWidth * 0.9;
    this.bgimgHeight = screenWidth * 0.4;
  },
  watch: {
    tankList(newValue, oldValue) {
      // 第一次进入页面,等有数据了再画图
      if (oldValue.length == 0 && newValue.length != 0) {
        this.createContexts(this.bgimgWidth, this.bgimgHeight); //每次进来的时候先画一次
      }
​
      // if 防止没有context还要画图报错
      if (this.context.length != 0) {
        this.refreshCanvas(this.bgimgWidth, this.bgimgHeight);
      }
    },
  },
  methods: {
    // 这个页面只运行一次
    createContexts() {
      // ❗❗❗ 新版canvas需要消除锯齿
      const windowInfo = wx.getWindowInfo();
      const availableWidth = windowInfo.windowWidth;
      const dpr = windowInfo.pixelRatio; //设备像素比
// 1. 给 context[] 赋值(在这个页面只创建一次)
      this.tankList.forEach((tank, index) => {
        this.createSelectorQuery()
          .select(`#oil${tank.id}`)
          .node(({ node: canvas }) => {
            console.log("canvas: ", canvas);
            // 获取到第一个canvas节点时创建图片对象,以免后续加载图片太慢
            if (!this.bgImg) {
              this.bgImg = canvas.createImage();
              this.bgImg.src = "/static/bgimg.png";
            }
​
            this.context[index] = canvas.getContext("2d");
​
            canvas.width = availableWidth * dpr;
            canvas.height = availableWidth * 0.4 * dpr; //按照下面css的尺寸比例
            this.context[index].scale(dpr, dpr); //必须有
this.canvasList[index] = canvas; //存储全局变量,其她函数中也可以使用已存储的canvas实例
            if (index === this.tankList.length - 1) {
              //全部建立成功后再画图,调用一遍,防止等待时间过长
              setTimeout(() => {
                this.refreshCanvas(this.bgimgWidth, this.bgimgHeight);
              }, 100);
            }
          })
          .exec();
      });
    },
    refreshCanvas(bgimgWidth, bgimgHeight) {
      const context = this.context; // 为了下面画canvas时少写一个 this
// 2. 设置不同油罐油品的颜色
      let oilColors = []; //存储不同油罐油品的颜色
      this.tankList.forEach((tank) => {
        oilColors.push(converIntToRgb(tank.oilColor));
      });
​
      this.tankList.forEach((tank, index) => {
        // // ❗❗❗ 每次画新的之前先清空画布(包括图片)
        context[index].clearRect(0, 0, bgimgWidth, bgimgHeight);
​
        // // 3. 画油和水的矩形块,然后画背景图片
        const rectStartPointX = bgimgWidth * 0.15;
        const rectWidth = bgimgWidth * 0.1;
        let yOil = bgimgHeight * (1 - (0.9 * tank.oilRatio) / 100); // 一次性数据,只在传参时用一次,不用定义数组
        let yWater = bgimgHeight * (1 - (0.9 * tank.waterRatio) / 100); // 一次性数据,只在传参时用一次,不用定义数组
        // 3.1 画油位矩形
        context[index].fillStyle = oilColors[index];
        context[index].fillRect(rectStartPointX, yOil, rectWidth, bgimgHeight);
        // // 3.3 新版canvas画背景图片,将图片绘制到 canvas 上
        this.context[index].drawImage(
          this.bgImg,
          0,
          0,
          bgimgWidth,
          bgimgHeight
        );
​
        // 4. 写详细信息(放外面会被图片挡住,因为图片加载较慢)
        // 4.1 设置文字颜色:在线黑色,离线红色
        const textColor = tank.connect ? "black" : "red";
        context[index].fillStyle = textColor;
        // 4.2 写罐号 1
        context[index].font = "20px sans-serif";
        context[index].fillText(tank.id, bgimgWidth * 0.06, bgimgHeight * 0.6);
      });
    },
  },
};
</script><style scoped>
/* .canvas样式不需要需改!! */
.canvas {
  /* border: 1rpx solid black; */
  width: 750rpx;
  height: 300rpx;
  margin-top: 30rpx;
  margin-left: 37rpx;
  margin-right: 37rpx;
}
</style>

 

posted @ 2025-12-01 15:56  sunshine233  阅读(15)  评论(0)    收藏  举报