更新记录

1.0.0(2025-06-12) 下载此版本

  • 支持手指绘制路径标注(涂鸦/涂抹)
  • 支持双指缩放和拖动图片
  • 支持撤销、清空、导出绘制结果
  • 支持原图尺寸导出,方便 AI 图像修复等场景
  • 微信小程序下自动切换虚拟 Canvas 显示模式,避免绘图偏移问题

平台兼容性

uni-app(4.65)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - - - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - -

图片涂抹标注组件(Image Painter)

一个支持触控缩放、拖动、涂抹、标注的图片标记组件,适用于涂鸦、图片修复、水印标记等场景,兼容微信小程序平台下 canvas 缩放绘制异常的特殊处理。


📌 功能特性

  • 支持手指绘制路径标注(涂鸦/涂抹)
  • 支持双指缩放和拖动图片
  • 支持撤销、清空、导出绘制结果
  • 支持原图尺寸导出,方便 AI 图像修复等场景
  • 微信小程序下自动切换虚拟 Canvas 显示模式,避免绘图偏移问题

📦 安装方式

在插件市场导入即可使用

属性说明(Props)

属性名 类型 默认值 说明
src String - 要绘制的图片地址
mode String 'aspectFit' 图片适配模式,支持 'aspectFit''scaleToFill'
allowDraw Boolean true 是否允许绘图操作
drawColor String 'rgba(128, 0, 128, 1)' 绘图的颜色
brushSize Number 5 画笔粗细,单位为 px
exportOriginSize Boolean true 导出图片时是否使用原图尺寸,设为 false 则使用容器尺寸
平台 支持情况
H5 ✅ 支持
微信小程序 ✅ 虚拟 Canvas 适配处理
App/Vue3 ✅ 支持安卓,IOS未测试

注意事项

  • ⚠️ 微信小程序兼容性说明: 小程序中的 canvas 在缩放或位移后可能出现绘制偏移问题,为此组件自动在小程序环境中启用「虚拟 Canvas 显示」模式,仅用于预览,实际绘制仍保留在主 canvas 中,确保导出结果正确。
  • 🎯 绘图过程基于组件容器相对尺寸绘制,导出时会自动按比例映射到目标尺寸(原图或容器尺寸),无需手动换算。
  • 💾 exportImage(mode) 方法支持导出两种模式:
    • mode = 'merged':底图和绘制内容合并导出(用于生成最终图像)
    • mode = 'mask':仅导出绘制内容,底图透明(适用于遮罩、掩码场景)
  • 🖍 建议绘图结束后主动调用 exportImage() 进行保存,避免刷新页面导致绘图记录丢失。
  • 📱 组件默认禁用页面滚动(touch-action: none),如需嵌入复杂页面,请合理设置容器样式避免冲突。
  • exportOriginSize在用户遮罩、掩码场景建议选择false,后端进行等比放大,避免高清图生成遮罩文件耗时较长。

📄 使用示例(完整代码参考)

<template>
  <view class="root">
    <view class="image-handler">
      <view v-if="!imageUrl" class="upload-area" @click="uploadImage">
        <view class="upload-desc">点击上传图片</view>
      </view>
      <ImagePainter ref="painterRef" class="image-painter" v-if="imageUrl && !exportUrl" :src="imageUrl" mode="aspectFit" :allowDraw="true"
                    :brushSize="brushSize" :drawColor="color" />
      <image v-if="exportUrl" :src="exportUrl" mode="aspectFit"></image>
    </view>
    <view class="draw-action" v-if="imageUrl && !exportUrl">
      <view class="draw-brush">
        <view class="draw-brush-size">
          <view class="draw-brush-size-desc">
            画笔粗细:&nbsp;{{brushSize}}
          </view>
          <slider v-model:value="brushSize" :min="1" :max="50" @change="e => brushSize = e.detail.value">
          </slider>
        </view>
        <view class="draw-brush-color">
          <view class="draw-brush-color-desc">
            画笔颜色:&nbsp;{{color}}
          </view>
          <view v-for="(c, index) in presetColors" :key="index" class="color-box"
                :class="{ active: c === color }" :style="{ backgroundColor: c }" @click="color = c"></view>
        </view>
      </view>
      <view class="draw-buttons">
        <view class="draw-button" @click="undo">撤回</view>
        <view class="draw-button" @click="clear">清空</view>
      </view>
      <view class="export-button" @click="exportImage">导出图片</view>
      <view class="export-button" @click="exportMask">导出MASK</view>
    </view>
    <view class="clear-button" v-if="exportUrl" @click="clearImage">清空图片</view>
  </view>
</template>

<script setup>
  import {
    ref
  } from 'vue'
  import ImagePainter from '@/components/z-image-painter/z-image-painter.vue'

  const painterRef = ref(null)
  const imageUrl = ref()
  const exportUrl = ref()
  const brushSize = ref(3)
  const color = ref('#00aaff')

  // 预设颜色
  const presetColors = ref([
    '#00aaff',
    '#ff4d4f',
    '#52c41a',
    '#faad14',
    '#722ed1',
    '#000000',
    '#ffffff'
  ])

  function uploadImage() {
    uni.chooseImage({
      count: 1,
      sizeType: ["original"],
      extension: ["png", "jpg", "jpeg"],
      success(res) {
        imageUrl.value = res.tempFilePaths[0]
      }
    })
  }

  function undo() {
    painterRef.value.undo()
  }

  function clear() {
    painterRef.value.clear()
  }

  function exportImage() {
    uni.showLoading({
      title: '图片导出中...',
      mask: true
    });
    painterRef.value.exportImage('merged').then(res => {
      uni.hideLoading()
      exportUrl.value = res
    }).then(err => {
      uni.hideLoading()
    })
  }

  function exportMask() {
    uni.showLoading({
      title: '图片导出中...',
      mask: true
    });
    painterRef.value.exportImage('mask').then(res => {
      uni.hideLoading()
      exportUrl.value = res
    }).then(err => {
      uni.hideLoading()
    })
  }

  function clearImage() {
    imageUrl.value = null
    exportUrl.value = null
  }
</script>

<style scoped>
  .root {
    height: 100vh;
    width: 100%;
    display: flex;
    flex-direction: column;
    background-color: #ffffff;
  }

  .image-handler {
    height: 50%;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #fafafa;
    border-bottom: 1px solid #eee;
    position: relative;
    overflow: hidden;
  }

  .image-painter,
  image {
    max-width: 100%;
    max-height: 100%;
    border-radius: 12px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  }

  .upload-area {
    height: 100%;
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #f5f5f5;
    border: 2px dashed #ccc;
    cursor: pointer;
    border-radius: 12px;
    transition: all 0.3s ease;
  }

  .upload-area:hover {
    background-color: #e6f7ff;
    border-color: #91d5ff;
  }

  .upload-desc {
    color: #666;
    font-size: 16px;
  }

  .draw-action {
    height: 35%;
    padding: 16px 20px;
    background-color: #ffffff;
    box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.04);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }

  .draw-brush {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  .draw-brush-size-desc,
  .draw-brush-color-desc {
    font-size: 14px;
    color: #333;
    margin-bottom: 8px;
  }

  .draw-brush-size slider {
    width: 100%;
  }

  .draw-brush-color {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    align-items: center;
  }

  .color-box {
    width: 28px;
    height: 28px;
    border-radius: 6px;
    border: 2px solid transparent;
    box-sizing: border-box;
    cursor: pointer;
    transition: all 0.2s ease-in-out;
  }

  .color-box.active {
    border-color: #1890ff;
    box-shadow: 0 0 4px rgba(24, 144, 255, 0.6);
    transform: scale(1.1);
  }

  .draw-buttons {
    display: flex;
    justify-content: flex-start;
    gap: 12px;
    margin-top: 12px;
  }

  .draw-button {
    padding: 8px 16px;
    background-color: #f0f0f0;
    border-radius: 8px;
    font-size: 14px;
    color: #333;
    cursor: pointer;
    transition: all 0.2s;
    border: 1px solid #ccc;
  }

  .draw-button:hover {
    background-color: #d9f7be;
    border-color: #52c41a;
    color: #389e0d;
  }

  .export-button {
    margin-top: 12px;
    padding: 10px 16px;
    background-color: #1890ff;
    color: white;
    text-align: center;
    border-radius: 8px;
    font-size: 14px;
    cursor: pointer;
    transition: background-color 0.3s ease;
  }

  .export-button:hover {
    background-color: #40a9ff;
  }

  .clear-button {
    margin: 16px auto;
    padding: 8px 16px;
    background-color: #ff4d4f;
    color: white;
    border-radius: 8px;
    text-align: center;
    font-size: 14px;
    width: fit-content;
    cursor: pointer;
    transition: background-color 0.3s ease;
  }

  .clear-button:hover {
    background-color: #ff7875;
  }
</style>

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

暂无用户评论。

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问