微信小程序 canvas涂鸦制作【有背景图】

本文介绍了如何在微信小程序中实现一个具备用户自定义画笔大小、颜色更换、橡皮擦、回退和预览功能的涂鸦应用,通过CanvasContext.clearRect方法处理画布操作,并详细展示了关键代码片段和实现思路。
摘要由CSDN通过智能技术生成

实现了以下基本功能:

1. 用户自定义画笔大小。

2. 更换涂鸦颜色。

3. 橡皮擦功能。

4. 回退和清空功能。

5. 保存和预览。

实现思路:

将canvas背景颜色设置成透明,底下渲染一张定位好的背景图。每次手指离开手机屏幕当作一次操作,然后保存成临时文件,用作回退。预览时先将背景图渲染上去,然后再将最后一张保存的临时文件渲染上去,就是带背景图片的涂鸦了,不需要背景图片的话将画布颜色设置成白色即可。橡皮檫功能使用CanvasContext.clearRecticon-default.png?t=N7T8https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.clearRect.html清空画布上的内容。

效果预览:

实现效果:

涂鸦小程序实现效果

实现代码:

let penType = 'drawPen';
Page({
  /**
   * 页面的初始数据
   */
  data: {
    scale: 1,
    imageList: [],
    showBars: false,
    selectSize: wx.getStorageSync('selectSize') || 5,
    selectColor: wx.getStorageSync('selectColor') || '#FFFFFF',
    colors: ["#FFFFFF", "#000000", "#ff0000", "#ffff00", "#00CC00", "#99CCFF", "#0000ff", "#ff00ff"],
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    this.setData({
      cover: options["cover"] || "/static/image/2.jpg"
    })
    this.initCanvas();
  },
  // 页面卸载 把字号选择的颜色和透明度保存
  onUnload() {
    const This = this.data;
    penType = 'drawPen';
    wx.setStorageSync('selectSize', This.selectSize);
    wx.setStorageSync('selectColor', This.selectColor);
  },
  colorChange(e) {
    const color = e.currentTarget.dataset.color;
    this.setData({
      selectColor: color
    })
    penType = 'drawPen';
  },
  sizeHandler(e) {
    const size = e.detail.value;
    this.setData({
      selectSize: size
    })
  },
  // 使用橡皮檫
  rubberHandler() {
    penType = 'clearPen';
    this.setData({
      selectColor: ""
    })
  },
  //初始化画布
  initCanvas() {
    const This = this.data;
    const query = wx.createSelectorQuery("#myCanvas");
    query.select('#myCanvas').fields({
      node: true,
      size: true,
      context: true
    }).exec(res => {
      const canvas = res[0].node;
      const context = canvas.getContext('2d');
      // 获取设备像素比
      const dpr = wx.getSystemInfoSync().pixelRatio;
      const width = res[0].width * dpr;
      const height = res[0].height * dpr;
      canvas.width = width;
      canvas.height = height;
      // 填充背景颜色
      context.fillStyle = "transparent";
      context.fillRect(0, 0, width, height);
      // 缩放
      context.scale(dpr, dpr);
      // 设置默认属性
      context.strokeStyle = This.selectColor;
      context.lineWidth = This.selectSize;
      this.setData({
        canvasElement: canvas,
        canvasContext: context,
      })
    })
  },
  // 开始
  startTouchClick(e) {
    var that = this;
    const x = e.touches[0].x;
    const y = e.touches[0].y;
    that.setData({
      oldPosition: {
        x: x,
        y: y
      },
    }, () => {
      that.setData({
        isDraw: true,
      })
    })
  },
  // 移动
  moveClick(e) {
    if (this.data.isDraw) {
      let positionItem = e.touches[0]
      if (this.data.canvasContext) {
        this.drawCanvas(positionItem, true)
      } else {
        this.initCanvas(() => {
          this.drawCanvas(positionItem, true)
        })
      }
    }
  },
  // 描绘canvas
  drawCanvas(position) {
    const ctx = this.data.canvasContext;
    const size = this.data.selectSize;
    const color = this.data.selectColor;
    const This = this.data;
    if (ctx) {
      ctx.beginPath();
      ctx.lineWidth = size;
      ctx.strokeStyle = color;
      ctx.lineCap = 'round';
      if (penType == 'clearPen') {
        const radius = size + 1;
        ctx.clearRect(position.x - (radius / 2), position.y - (radius / 2), radius, radius);
      } else {
        ctx.moveTo(This.oldPosition.x, This.oldPosition.y);
        ctx.lineTo(position.x, position.y);
        ctx.stroke()
      };
      ctx.closePath();
      this.setData({
        oldPosition: {
          x: position.x,
          y: position.y,
        }
      })
    }
  },
  //触摸结束
  endTouchClick(e) {
    this.setData({
      isDraw: false
    })
    this.saveImage();
  },
  //误触事件
  errorClick(e) {
    console.log("误触事件:", e);
  },
  // 是否展示 操作栏
  showBarsHandler() {
    this.setData({
      showBars: !this.data.showBars
    })
  },
  hideBarsHandler() {
    this.setData({
      showBars: false
    })
  },
  // 回退一步 || 重绘
  restore() {
    // 实际上的回退就是取存储的最后一张图片 渲染出来
    // 所以会有抖动 暂未想到其他方案解决
    const ctx = this.data.canvasContext;
    const canvas = this.data.canvasElement;
    const dpr = wx.getSystemInfoSync().pixelRatio;
    let imgs = this.data.imageList;
    if (!imgs || imgs.length == 0) return false;
    if (imgs.length == 1) return this.clearRect();
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // -2 是因为当前的也储存了
    const cover = imgs[imgs.length - 2];
    imgs.splice(imgs.length - 1, 1);
    let bg = canvas.createImage();
    bg.src = cover;
    bg.onload = () => {
      // 缩放【放大还原】
      ctx.scale(1 / dpr, 1 / dpr);
      ctx.drawImage(bg, 0, 0, canvas.width, canvas.height);
      // 再缩放
      ctx.scale(dpr, dpr);
    }
  },
  // 清空画布
  clearRect() {
    const ctx = this.data.canvasContext;
    const canvas = this.data.canvasElement;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    this.setData({
      imageList: []
    })
  },
  // 保存图片
  saveImage() {
    const that = this;
    wx.canvasToTempFilePath({
      canvasId: 'myCanvas',
      canvas: this.data.canvasElement,
      success: function (res) {
        that.data.imageList.push(res.tempFilePath);
      },
      fail: function (err) {}
    })
  },
  // 图片预览 这边的思路是 首先将背景图片画上去  再将最后的涂鸦展示上去
  preview() {
    const that = this;
    wx.showLoading({
      title: '打包中...',
    })
    const images = that.data.imageList;
    if (!images && images.length == 0) return false;
    const img = images[images.length - 1];
    // 将背景图片画上去
    const ctx = this.data.canvasContext;
    const canvas = this.data.canvasElement;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    const cover = this.data.cover;
    wx.getImageInfo({
      src: cover,
      success: e => {
        let realWidth = canvas.width;
        let realHeight = canvas.height;
        // 动态计算图片宽高
        if (e.width > e.height) {
          const ratio = canvas.height / e.height;
          realWidth = e.width * ratio;
        } else {
          const ratio = canvas.width / e.width;
          realHeight = e.height * ratio;
        }
        let bg = canvas.createImage();
        bg.src = cover;
        bg.onload = () => {
          const dpr = wx.getSystemInfoSync().pixelRatio;
          ctx.scale(1 / dpr, 1 / dpr);
          ctx.drawImage(bg, 0, (canvas.height - realHeight) / 2, realWidth, realHeight);
          let trajectory = canvas.createImage();
          trajectory.src = img;
          trajectory.onload = _ => {
            ctx.drawImage(trajectory, 0, 0, canvas.width, canvas.height);
            wx.canvasToTempFilePath({
              canvasId: 'myCanvas',
              canvas: that.data.canvasElement,
              success: function (res) {
                wx.previewImage({
                  urls: [res.tempFilePath],
                  showmenu: true,
                  current: res.tempFilePath,
                  complete: _ => {
                    wx.hideLoading();
                    ctx.scale(dpr, dpr);
                  }
                })
              },
              fail: function (err) {}
            })
          }
        }
      }
    })
  },
})
<view style="height: 100vh;" class="flex_column" catch:tap="hideBarsHandler">
  <!-- 涂鸦区 -->
  <view style="flex: 1; position: relative;" catch:touchstart="hideBarsHandler">
    <image src="{{cover}}" mode="aspectFit" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; width: 100%; height: 100%;" />
    <canvas style="width: 100%; height: 100%;;" id="myCanvas" canvas-id="myCanvas" type="2d" bindtouchstart="startTouchClick" bindtouchmove="moveClick" bindtouchend="endTouchClick" binderror="errorClick"></canvas>
  </view>
  <!-- 涂鸦工具区 -->
  <view style="padding: 30rpx 32rpx 50rpx;">
    <view class="space" style="padding-bottom: 20rpx; color: #FFF; font-size: 30rpx; line-height: 56rpx;">
      <view catch:tap="restore">回退</view>
      <view style="width: 30rpx;"></view>
      <view catch:tap="preview">预览</view>
      <view style="flex: 1;"></view>
      <view catch:tap="showBarsHandler">笔力</view>
      <view style="width: 30rpx;"></view>
      <view catch:tap="rubberHandler">橡皮擦</view>
      <view style="width: 30rpx;"></view>
      <view catch:tap="clearRect">清除</view>
    </view>
    <scroll-view scroll-x style="height: 90rpx;">
      <view style="white-space: nowrap;">
        <block wx:for="{{colors}}" wx:key="index">
          <view style="background-color: {{item}};" class="colorBtn {{selectColor == item && 'select'}}" catch:tap="colorChange" data-color="{{item}}"></view>
        </block>
      </view>
    </scroll-view>
  </view>
</view>

<view class="bars {{showBars && 'show'}}" wx:if="{{showBars}}">
  <view class="space vertical_center">
    <view style="width: 136rpx;">字号:</view>
    <slider style="flex: 1;" value="{{selectSize}}" step="1" min="1" max="20" block-size="12" activeColor="#a88cf8" bindchange="sizeHandler" />
    <view style="width: 50rpx; text-align: right;">{{selectSize}}</view>
  </view>
</view>
page {
  height: 100vh;
  overflow: hidden;
  box-sizing: border-box;
  padding-bottom: 0 !important;
  background-color: #242424 !important;
}

.colorBtn {
  width: 50rpx;
  height: 50rpx;
  border-radius: 50%;
  margin-right: 20rpx;
  display: inline-block;
  border: 6rpx solid #242424;
}

.colorBtn.select {
  border: 6rpx solid #FFF;
}

.bars {
  right: 1rem;
  width: 400rpx;
  padding: 20rpx;
  bottom: 7.5rem;
  color: #a88cf8;
  font-size: 32rpx;
  font-weight: bold;
  position: absolute;
  border-radius: 8rpx;
  background-color: #242424;
  box-shadow: 0 0 1.5rem 0 #FFFFFF20;
}

slider {
  margin: 20rpx 20rpx 20rpx 0 !important;
}

.flex_column {
  display: flex;
  flex-direction: column !important;
}

.vertical_center {
  display: flex;
  align-items: center;
}

.space {
  display: flex;
  justify-content: space-between;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值