更新记录
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">
画笔粗细: {{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">
画笔颜色: {{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>