小程序使用画布

微信小程序新版画布(Canvas 2D)是基于 HTML5 Canvas API 的现代化实现,相比旧版 Canvas 具有更好的性能和更丰富的功能。主要特点包括:

主要特性:

1,​ 性能优化 ​

硬件加速渲染

更低的 CPU 占用

支持离屏渲染

2,​API 现代化 ​

支持标准的 Canvas API

更好的 TypeScript 支持

更直观的调用方式

3,​ 功能增强 ​

完整的 2D 绘图 API

渐变、阴影、滤镜效果

文本测量和渲染

路径和形状操作

图像合成和变换

​4,开发体验 ​

更好的错误提示

更详细的文档

兼容 Web 标准

这是一个 JS 文件示例:

// pages/canvas/canvas.js
Page({
  data: {
    // 画布上下文
    canvasContext: null,
    // 画布尺寸
    canvasWidth: 300,
    canvasHeight: 300,
    // 绘图状态
    drawing: false,
    lastX: 0,
    lastY: 0,
    // 画笔设置
    brushColor: '#000000',
    brushSize: 5,
    brushOpacity: 1,
    // 工具类型
    currentTool: 'pen', // pen, eraser, line, rect, circle, text
    // 图形属性
    fillColor: '#ff0000',
    strokeColor: '#000000',
    lineWidth: 2,
    // 文本设置
    textContent: 'Hello Canvas',
    textSize: 20,
    textFont: 'Arial',
    // 历史记录
    history: [],
    historyIndex: -1,
    // 图层管理
    layers: [],
    currentLayer: 0,
    // 滤镜效果
    filters: {
      brightness: 100,
      contrast: 100,
      saturation: 100,
      blur: 0
    },
    // 画布状态
    isSaved: false,
    saveMessage: ''
  },

  onLoad() {
    console.log('画布页面加载');
    this.initCanvas();
  },

  onReady() {
    // 页面渲染完成后初始化画布
    setTimeout(() => {
      this.setupCanvas();
    }, 100);
  },

  onUnload() {
    // 清理资源
    if (this.data.canvasContext) {
      this.data.canvasContext = null;
    }
  },

  /**
   * 初始化画布
   */
  initCanvas() {
    // 获取系统信息设置画布尺寸
    wx.getSystemInfo({
      success: (res) => {
        const width = res.windowWidth * 0.9;
        const height = width; // 正方形画布

        this.setData({
          canvasWidth: width,
          canvasHeight: height
        }, () => {
          this.createCanvasContext();
        });
      }
    });
  },

  /**
   * 创建画布上下文
   */
  createCanvasContext() {
    return new Promise((resolve, reject) => {
      const query = wx.createSelectorQuery();
      query.select('#myCanvas')
        .fields({ node: true, size: true })
        .exec((res) => {
          if (!res[0]) {
            reject(new Error('画布元素未找到'));
            return;
          }

          const canvas = res[0].node;
          const ctx = canvas.getContext('2d');

          // 设置画布分辨率
          const dpr = wx.getSystemInfoSync().pixelRatio;
          canvas.width = this.data.canvasWidth * dpr;
          canvas.height = this.data.canvasHeight * dpr;
          ctx.scale(dpr, dpr);

          this.setData({
            canvasContext: ctx,
            canvasNode: canvas
          });

          // 清空画布
          this.clearCanvas();
          resolve(ctx);
        });
    });
  },

  /**
   * 设置画布
   */
  async setupCanvas() {
    try {
      await this.createCanvasContext();
      this.drawWelcomeScreen();
      console.log('画布初始化成功');
    } catch (error) {
      console.error('画布初始化失败:', error);
      wx.showToast({
        title: '画布初始化失败',
        icon: 'none'
      });
    }
  },

  /**
   * 绘制欢迎界面
   */
  drawWelcomeScreen() {
    const ctx = this.data.canvasContext;
    if (!ctx) return;

    // 清空画布
    this.clearCanvas();

    // 绘制背景
    ctx.fillStyle = '#f0f0f0';
    ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);

    // 绘制标题
    ctx.fillStyle = '#333333';
    ctx.font = 'bold 20px Arial';
    ctx.textAlign = 'center';
    ctx.fillText('欢迎使用画板', this.data.canvasWidth / 2, 50);

    // 绘制说明文字
    ctx.font = '14px Arial';
    ctx.fillText('请选择工具开始绘画', this.data.canvasWidth / 2, 80);

    // 绘制示例图形
    this.drawSampleShapes();
  },

  /**
   * 绘制示例图形
   */
  drawSampleShapes() {
    const ctx = this.data.canvasContext;
    const centerX = this.data.canvasWidth / 2;
    const centerY = this.data.canvasHeight / 2;

    // 绘制圆形
    ctx.fillStyle = '#ff6b6b';
    ctx.beginPath();
    ctx.arc(centerX - 60, centerY, 30, 0, Math.PI * 2);
    ctx.fill();

    // 绘制矩形
    ctx.fillStyle = '#4ecdc4';
    ctx.fillRect(centerX - 15, centerY - 30, 60, 60);

    // 绘制三角形
    ctx.fillStyle = '#45b7d1';
    ctx.beginPath();
    ctx.moveTo(centerX + 45, centerY - 30);
    ctx.lineTo(centerX + 75, centerY + 30);
    ctx.lineTo(centerX + 15, centerY + 30);
    ctx.closePath();
    ctx.fill();

    this.saveState();
  },

  /**
   * 清空画布
   */
  clearCanvas() {
    const ctx = this.data.canvasContext;
    if (!ctx) return;

    ctx.clearRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);
    this.saveState();
  },

  /**
   * 保存绘图状态
   */
  saveState() {
    if (!this.data.canvasNode) return;

    // 获取画布图像数据
    const canvas = this.data.canvasNode;
    wx.canvasGetImageData({
      canvasId: 'myCanvas',
      x: 0,
      y: 0,
      width: this.data.canvasWidth,
      height: this.data.canvasHeight,
      success: (res) => {
        const state = {
          imageData: res,
          timestamp: Date.now()
        };

        // 保存到历史记录
        const newHistory = this.data.history.slice(0, this.data.historyIndex + 1);
        newHistory.push(state);

        this.setData({
          history: newHistory,
          historyIndex: newHistory.length - 1
        });
      }
    });
  },

  /**
   * 撤销操作
   */
  undo() {
    if (this.data.historyIndex <= 0) {
      wx.showToast({
        title: '无法继续撤销',
        icon: 'none'
      });
      return;
    }

    this.setData({
      historyIndex: this.data.historyIndex - 1
    });

    this.restoreState(this.data.historyIndex);
  },

  /**
   * 重做操作
   */
  redo() {
    if (this.data.historyIndex >= this.data.history.length - 1) {
      wx.showToast({
        title: '无法继续重做',
        icon: 'none'
      });
      return;
    }

    this.setData({
      historyIndex: this.data.historyIndex + 1
    });

    this.restoreState(this.data.historyIndex);
  },

  /**
   * 恢复状态
   */
  restoreState(index) {
    if (index < 0 || index >= this.data.history.length) return;

    const state = this.data.history[index];
    wx.canvasPutImageData({
      canvasId: 'myCanvas',
      x: 0,
      y: 0,
      width: this.data.canvasWidth,
      height: this.data.canvasHeight,
      data: state.imageData.data,
      success: () => {
        console.log('状态恢复成功');
      }
    });
  },

  /**
   * 触摸开始事件
   */
  onTouchStart(e) {
    if (!this.data.canvasContext) return;

    const touch = e.touches[0];
    const x = touch.x;
    const y = touch.y;

    this.setData({
      drawing: true,
      lastX: x,
      lastY: y
    });

    // 根据工具类型执行不同操作
    switch (this.data.currentTool) {
      case 'pen':
      case 'eraser':
        this.startDrawing(x, y);
        break;
      case 'line':
        this.startLine(x, y);
        break;
      case 'rect':
        this.startRect(x, y);
        break;
      case 'circle':
        this.startCircle(x, y);
        break;
      case 'text':
        this.startTextInput(x, y);
        break;
    }
  },

  /**
   * 触摸移动事件
   */
  onTouchMove(e) {
    if (!this.data.drawing || !this.data.canvasContext) return;

    const touch = e.touches[0];
    const x = touch.x;
    const y = touch.y;

    switch (this.data.currentTool) {
      case 'pen':
        this.drawLine(this.data.lastX, this.data.lastY, x, y);
        break;
      case 'eraser':
        this.erase(x, y);
        break;
      case 'line':
        this.drawTempLine(x, y);
        break;
      case 'rect':
        this.drawTempRect(x, y);
        break;
      case 'circle':
        this.drawTempCircle(x, y);
        break;
    }

    this.setData({
      lastX: x,
      lastY: y
    });
  },

  /**
   * 触摸结束事件
   */
  onTouchEnd(e) {
    if (!this.data.drawing) return;

    const touch = e.changedTouches[0];
    const x = touch.x;
    const y = touch.y;

    switch (this.data.currentTool) {
      case 'line':
        this.finishLine(x, y);
        break;
      case 'rect':
        this.finishRect(x, y);
        break;
      case 'circle':
        this.finishCircle(x, y);
        break;
    }

    this.setData({
      drawing: false
    });

    // 保存状态
    this.saveState();
  },

  /**
   * 开始绘制
   */
  startDrawing(x, y) {
    const ctx = this.data.canvasContext;
    ctx.beginPath();
    ctx.moveTo(x, y);
  },

  /**
   * 绘制线条
   */
  drawLine(x1, y1, x2, y2) {
    const ctx = this.data.canvasContext;

    if (this.data.currentTool === 'pen') {
      ctx.strokeStyle = this.data.brushColor;
      ctx.lineWidth = this.data.brushSize;
      ctx.globalAlpha = this.data.brushOpacity;
    } else {
      ctx.strokeStyle = '#ffffff'; // 橡皮擦用白色
      ctx.lineWidth = this.data.brushSize * 2;
      ctx.globalAlpha = 1;
    }

    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
  },

  /**
   * 橡皮擦功能
   */
  erase(x, y) {
    const ctx = this.data.canvasContext;

    // 使用目的地输出模式实现橡皮擦效果
    ctx.globalCompositeOperation = 'destination-out';
    ctx.strokeStyle = 'rgba(0,0,0,1)';
    ctx.lineWidth = this.data.brushSize * 2;
    ctx.lineCap = 'round';

    ctx.beginPath();
    ctx.moveTo(this.data.lastX, this.data.lastY);
    ctx.lineTo(x, y);
    ctx.stroke();

    // 恢复合成模式
    ctx.globalCompositeOperation = 'source-over';
  },

  /**
   * 开始绘制直线
   */
  startLine(x, y) {
    this.setData({
      startX: x,
      startY: y
    });
  },

  /**
   * 绘制临时直线(预览)
   */
  drawTempLine(x, y) {
    // 保存当前状态
    this.restoreState(this.data.historyIndex);

    const ctx = this.data.canvasContext;
    ctx.strokeStyle = this.data.strokeColor;
    ctx.lineWidth = this.data.lineWidth;
    ctx.setLineDash([5, 5]); // 虚线预览

    ctx.beginPath();
    ctx.moveTo(this.data.startX, this.data.startY);
    ctx.lineTo(x, y);
    ctx.stroke();

    ctx.setLineDash([]); // 恢复实线
  },

  /**
   * 完成直线绘制
   */
  finishLine(x, y) {
    const ctx = this.data.canvasContext;
    ctx.strokeStyle = this.data.strokeColor;
    ctx.lineWidth = this.data.lineWidth;
    ctx.setLineDash([]);

    ctx.beginPath();
    ctx.moveTo(this.data.startX, this.data.startY);
    ctx.lineTo(x, y);
    ctx.stroke();
  },

  /**
   * 开始绘制矩形
   */
  startRect(x, y) {
    this.setData({
      startX: x,
      startY: y
    });
  },

  /**
   * 绘制临时矩形
   */
  drawTempRect(x, y) {
    this.restoreState(this.data.historyIndex);

    const ctx = this.data.canvasContext;
    ctx.strokeStyle = this.data.strokeColor;
    ctx.lineWidth = this.data.lineWidth;
    ctx.setLineDash([5, 5]);

    const width = x - this.data.startX;
    const height = y - this.data.startY;

    ctx.strokeRect(this.data.startX, this.data.startY, width, height);
    ctx.setLineDash([]);
  },

  /**
   * 完成矩形绘制
   */
  finishRect(x, y) {
    const ctx = this.data.canvasContext;
    ctx.strokeStyle = this.data.strokeColor;
    ctx.fillStyle = this.data.fillColor;
    ctx.lineWidth = this.data.lineWidth;

    const width = x - this.data.startX;
    const height = y - this.data.startY;

    ctx.fillRect(this.data.startX, this.data.startY, width, height);
    ctx.strokeRect(this.data.startX, this.data.startY, width, height);
  },

  /**
   * 开始绘制圆形
   */
  startCircle(x, y) {
    this.setData({
      startX: x,
      startY: y
    });
  },

  /**
   * 绘制临时圆形
   */
  drawTempCircle(x, y) {
    this.restoreState(this.data.historyIndex);

    const ctx = this.data.canvasContext;
    ctx.strokeStyle = this.data.strokeColor;
    ctx.lineWidth = this.data.lineWidth;
    ctx.setLineDash([5, 5]);

    const radius = Math.sqrt(
      Math.pow(x - this.data.startX, 2) + Math.pow(y - this.data.startY, 2)
    );

    ctx.beginPath();
    ctx.arc(this.data.startX, this.data.startY, radius, 0, Math.PI * 2);
    ctx.stroke();
    ctx.setLineDash([]);
  },

  /**
   * 完成圆形绘制
   */
  finishCircle(x, y) {
    const ctx = this.data.canvasContext;
    ctx.strokeStyle = this.data.strokeColor;
    ctx.fillStyle = this.data.fillColor;
    ctx.lineWidth = this.data.lineWidth;

    const radius = Math.sqrt(
      Math.pow(x - this.data.startX, 2) + Math.pow(y - this.data.startY, 2)
    );

    ctx.beginPath();
    ctx.arc(this.data.startX, this.data.startY, radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
  },

  /**
   * 开始文本输入
   */
  startTextInput(x, y) {
    this.setData({
      textX: x,
      textY: y,
      showTextInput: true
    });
  },

  /**
   * 完成文本输入
   */
  finishTextInput() {
    if (!this.data.textContent.trim()) return;

    const ctx = this.data.canvasContext;
    ctx.fillStyle = this.data.brushColor;
    ctx.font = `${this.data.textSize}px ${this.data.textFont}`;
    ctx.textAlign = 'left';
    ctx.textBaseline = 'top';

    ctx.fillText(this.data.textContent, this.data.textX, this.data.textY);

    this.setData({
      showTextInput: false,
      textContent: 'Hello Canvas'
    });

    this.saveState();
  },

  /**
   * 改变画笔颜色
   */
  changeBrushColor(color) {
    this.setData({
      brushColor: color
    });
  },

  /**
   * 改变画笔大小
   */
  changeBrushSize(size) {
    this.setData({
      brushSize: size
    });
  },

  /**
   * 改变画笔透明度
   */
  changeBrushOpacity(opacity) {
    this.setData({
      brushOpacity: opacity / 100
    });
  },

  /**
   * 选择工具
   */
  selectTool(tool) {
    this.setData({
      currentTool: tool
    });

    const toolNames = {
      'pen': '画笔',
      'eraser': '橡皮擦',
      'line': '直线',
      'rect': '矩形',
      'circle': '圆形',
      'text': '文本'
    };

    wx.showToast({
      title: `已选择${toolNames[tool]}`,
      icon: 'none',
      duration: 1000
    });
  },

  /**
   * 应用滤镜
   */
  applyFilter() {
    const ctx = this.data.canvasContext;
    const filterString = this.generateFilterString();

    ctx.filter = filterString;
    this.redrawCanvas();
    ctx.filter = 'none'; // 重置滤镜

    this.saveState();
  },

  /**
   * 生成滤镜字符串
   */
  generateFilterString() {
    const f = this.data.filters;
    return `brightness(${f.brightness}%) contrast(${f.contrast}%) saturate(${f.saturation}%) blur(${f.blur}px)`;
  },

  /**
   * 改变滤镜值
   */
  changeFilter(type, value) {
    const newFilters = { ...this.data.filters };
    newFilters[type] = value;

    this.setData({
      filters: newFilters
    });

    this.applyFilter();
  },

  /**
   * 重绘画布
   */
  redrawCanvas() {
    // 重新绘制当前状态
    this.restoreState(this.data.historyIndex);
  },

  /**
   * 保存画布到相册
   */
  saveToPhotosAlbum() {
    return new Promise((resolve, reject) => {
      wx.canvasToTempFilePath({
        canvasId: 'myCanvas',
        success: (res) => {
          wx.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: () => {
              this.setData({
                isSaved: true,
                saveMessage: '图片已保存到相册'
              });

              wx.showToast({
                title: '保存成功',
                icon: 'success'
              });
              resolve();
            },
            fail: (err) => {
              this.handleSaveError(err);
              reject(err);
            }
          });
        },
        fail: (err) => {
          console.error('生成临时文件失败', err);
          reject(err);
        }
      });
    });
  },

  /**
   * 保存为文件
   */
  saveToFile() {
    wx.canvasToTempFilePath({
      canvasId: 'myCanvas',
      success: (res) => {
        wx.saveFile({
          tempFilePath: res.tempFilePath,
          success: (saveRes) {
            this.setData({
              isSaved: true,
              saveMessage: `文件已保存: ${saveRes.savedFilePath}`
            });

            wx.showToast({
              title: '文件保存成功',
              icon: 'success'
            });
          },
          fail: this.handleSaveError
        });
      }
    });
  },

  /**
   * 处理保存错误
   */
  handleSaveError(err) {
    console.error('保存失败', err);

    let errorMsg = '保存失败';
    if (err.errMsg.includes('auth deny')) {
      errorMsg = '没有相册访问权限';
    }

    this.setData({
      saveMessage: errorMsg
    });

    wx.showToast({
      title: errorMsg,
      icon: 'none'
    });
  },

  /**
   * 导出画布数据
   */
  exportCanvasData() {
    wx.canvasToTempFilePath({
      canvasId: 'myCanvas',
      fileType: 'png',
      quality: 1,
      success: (res) => {
        // 这里可以将临时文件路径上传到服务器或进行其他处理
        console.log('画布导出成功', res.tempFilePath);

        wx.showToast({
          title: '导出成功',
          icon: 'success'
        });

        // 可以在这里添加分享功能
        this.shareCanvasImage(res.tempFilePath);
      }
    });
  },

  /**
   * 分享画布图片
   */
  shareCanvasImage(tempFilePath) {
    wx.share({
      filePath: tempFilePath,
      success: () => {
        console.log('分享成功');
      },
      fail: (err) => {
        console.error('分享失败', err);
      }
    });
  },

  /**
   * 重置画布
   */
  resetCanvas() {
    wx.showModal({
      title: '确认重置',
      content: '确定要清空画布吗?',
      success: (res) => {
        if (res.confirm) {
          this.clearCanvas();
          this.drawWelcomeScreen();

          this.setData({
            history: [],
            historyIndex: -1
          });
        }
      }
    });
  },

  /**
   * 绘制渐变背景
   */
  drawGradientBackground() {
    const ctx = this.data.canvasContext;
    const gradient = ctx.createLinearGradient(0, 0, this.data.canvasWidth, this.data.canvasHeight);

    gradient.addColorStop(0, '#667eea');
    gradient.addColorStop(1, '#764ba2');

    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, this.data.canvasWidth, this.data.canvasHeight);

    this.saveState();
  },

  /**
   * 绘制网格背景
   */
  drawGridBackground() {
    const ctx = this.data.canvasContext;
    const size = 20;

    ctx.strokeStyle = '#e0e0e0';
    ctx.lineWidth = 0.5;

    // 绘制垂直线
    for (let x = 0; x <= this.data.canvasWidth; x += size) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, this.data.canvasHeight);
      ctx.stroke();
    }

    // 绘制水平线
    for (let y = 0; y <= this.data.canvasHeight; y += size) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(this.data.canvasWidth, y);
      ctx.stroke();
    }

    this.saveState();
  },

  /**
   * 添加水印
   */
  addWatermark() {
    const ctx = this.data.canvasContext;

    ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
    ctx.font = '12px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    // 在整个画布上添加水印文字
    for (let y = 20; y < this.data.canvasHeight; y += 50) {
      for (let x = 20; x < this.data.canvasWidth; x += 100) {
        ctx.save();
        ctx.translate(x, y);
        ctx.rotate(-0.3); // 倾斜角度
        ctx.fillText('My Canvas', 0, 0);
        ctx.restore();
      }
    }

    this.saveState();
  },

  /**
   * 获取画布统计信息
   */
  getCanvasStats() {
    if (!this.data.history.length) return null;

    return {
      totalOperations: this.data.history.length,
      currentState: this.data.historyIndex + 1,
      canvasSize: `${this.data.canvasWidth} × ${this.data.canvasHeight}`,
      lastSaved: this.data.isSaved ? '已保存' : '未保存'
    };
  },

  /**
   * 显示画布信息
   */
  showCanvasInfo() {
    const stats = this.getCanvasStats();
    if (!stats) return;

    const info = `画布信息:
操作次数: ${stats.totalOperations}
当前状态: ${stats.currentState}
画布尺寸: ${stats.canvasSize}
保存状态: ${stats.lastSaved}`;

    wx.showModal({
      title: '画布信息',
      content: info,
      showCancel: false
    });
  },

  /**
   * 页面分享
   */
  onShareAppMessage() {
    return {
      title: '我的画布作品',
      path: '/pages/canvas/canvas'
    };
  }
});

主要功能说明

  1. ​ 画布初始化 ​
    createCanvasContext(): 创建画布上下文

setupCanvas(): 设置画布参数

自动适配屏幕尺寸和分辨率

  1. ​ 绘图工具 ​

pen: 画笔工具,支持颜色、大小、透明度设置

eraser: 橡皮擦工具

line: 直线绘制工具

rect: 矩形绘制工具

circle: 圆形绘制工具

text: 文本输入工具

  1. ​ 绘图控制 ​

onTouchStart(): 触摸开始事件处理

onTouchMove(): 触摸移动事件处理

onTouchEnd(): 触摸结束事件处理

实时预览和最终绘制分离

  1. ​ 历史记录管理 ​

saveState(): 保存绘图状态

undo(): 撤销操作

redo(): 重做操作

restoreState(): 恢复特定状态

  1. ​ 滤镜效果 ​

brightness: 亮度调整

contrast: 对比度调整

saturation: 饱和度调整

blur: 模糊效果

实时滤镜应用

  1. ​ 文件操作 ​

saveToPhotosAlbum(): 保存到相册

saveToFile(): 保存为文件

exportCanvasData(): 导出画布数据

完善的错误处理

  1. ​ 高级功能 ​

渐变背景绘制

网格背景

水印添加

画布信息统计

分享功能

  1. ​ 工具函数 ​

颜色管理

尺寸调整

透明度控制

文本渲染

这个示例展示了新版画布的完整功能,包括基础绘图、高级特效、文件操作等,可以直接在小程序项目中使用。新版画布 API 更加现代化,性能更好,适合开发复杂的绘图应用。

通关密语:画布