vue3绘制画图工具
需求:在图片上显示一个透明层图片,并在图片上绘制矩形、圆形、文字,可以撤销、还原。呈现效果如下
代码如下:
vue3 + setup
<template>
<div class="panduUp_right">
<canvas
ref="canvas"
:width="canvasWidth"
:height="canvasHeight"
@mousedown="startDrawing"
@mousemove="drawCanvas"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
></canvas>
<!-- 文字输入框 -->
<div v-if="currentTool === 'text'">
<input id="text" v-model="textInput" type="text" autofocus autocomplete="off" ref="input" @keyup.enter="handleTextBlur" @blur="handleTextBlur"/>
</div>
</div>
<div>
<button class="btn" @click="undo">撤销</button>
<button class="btn" @click="selectTool('text')">标注</button>
<button class="btn" @click="selectTool('rectangle')">矩形</button>
<button class="btn" @click="selectTool('circle')">圆形</button>
<button class="btn" @click="redo">还原</button>
<button class="btn" @click="saveImage">保存</button>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import clockUrl from '/src/assets/clock.png'
import imagePath from '/src/assets/v2_rvxbui.png'
// 绘制工具==================================================
// 画布配置
// 画布宽高,根据自己图片大小调整数值
// const canvasWidth = 1230;
// const canvasWidth = 950;
const canvasWidth = 1200;
const canvasHeight = 670;
// const canvasHeight = 500;
const canvas = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
// 工具设置
const currentTool = ref<string>('circle'); // 当前选择的工具
const textInput = ref<string>(''); // 文本输入框的内容
const input = ref()
const inputX = ref(0)
const inputY = ref(0)
// 绘制状态
const drawing = ref(false); // 是否正在绘制
const startX = ref(0);
const startY = ref(0);
const imgData = ref(null)
// 历史记录
const history = ref<any[]>([]);//当前绘制数据的list
const historyStep = ref<any[]>([]);//历史操作步骤list
// 图片URL
const imageUrl = ref<string>(imagePath); // 背景图片
const overlayImageUrl = ref<string>(clockUrl); // 透明图片(叠加图)
// 在组件被挂载之后调用
onMounted(()=>{
// 初始化画布
initializeCanvas()
console.log('历史记录',JSON.stringify(history))
})
// 初始化画布
const initializeCanvas = () => {
console.log('初始化画布', canvas.value);
if (canvas.value) {
ctx.value = canvas.value.getContext('2d');
ctx.strokeStyle = '#FF0000'; // 设置线条颜色为红色
ctx.lineWidth = 2; // 设置线条宽度为2px
if (ctx.value) {
loadImage();
} else {
console.log('--------Failed to get canvas context------------');
}
}
}
// 加载背景图片
const loadImage = () => {
if (ctx.value && imageUrl.value) {
console.log('加载背景图片', imageUrl.value);
const img = new Image();
img.src = imageUrl.value;
//img.src = overlayImageUrl.value
img.onload = () => {
let width = canvas.value.width;
let height = canvas.value.height;
console.log('width:', width, 'height:', height);
//ctx.value?.drawImage(img, 0, 0, canvasWidth, canvasHeight); // 绘制背景图片
ctx.value?.drawImage(img, 0, 0, width, height); // 绘制背景图片
loadOverlayImage()
};
}
}
// 绘制透明叠加图片(固定叠加)
const loadOverlayImage = () => {
if (ctx.value && overlayImageUrl.value) {
const overlayImg = new Image();
overlayImg.src = overlayImageUrl.value;
console.log('加载叠加图片', overlayImg.src);
overlayImg.onload = () => {
let width = canvas.value.width;
let height = canvas.value.height;
let dx = (width - height)/2
console.log('覆盖层', 'width',width, 'height',height,'dx', dx)
ctx.value?.drawImage(overlayImg, dx, 0, height, height);
imgData.value = ctx.value?.getImageData(0,0, width,height)
}
}
}
// 选择工具
const selectTool = (tool: string) => {
console.log('选择工具-1', tool)
currentTool.value = tool;
if (tool !== 'text') {
textInput.value = ''; // 清空文字输入框
}
//如果是绘制了文字,操作动作,没有触发:光标失去焦点、回车键盘事件,而是直接选择了其他工具,这时把数据存入历史步骤list
if (history.value && history.value.length > 0) {
let last = history.value[history.value.length - 1]
if (last.tools == 'text' && last && (last != undefined)) {
historyStep.value.push(last)
history.value = []
}
}
}
// 开始绘制
const startDrawing = (event: MouseEvent) => {
console.log('开始绘制')
if (!ctx.value) return;
const rect = canvas.value!.getBoundingClientRect();
startX.value = event.clientX - rect.left;
startY.value = event.clientY - rect.top;
drawing.value = true;
if (currentTool.value === 'text') {
handleTextBlur()
//绘制文字
let text = input.value
text.style.fontSize = 20 + "px";
text.style.color = '#E53E30';
text.style.left = event.offsetX + canvas.value.offsetLeft - 20 + "px";
text.style.top = event.offsetY + canvas.value.offsetTop - 10 + "px";
text.style.zIndex = 9999;
console.log('input', text)
text.style.display = "block";
inputX.value = event.offsetX - 20;
inputY.value = event.offsetY + 10;
}
}
const handleTextBlur = () => {
console.log('input blur 事件-1')
let text = input.value;
let textVal = textInput.value
if(text && textVal){
console.log('input blur 事件-2')
ctx.value.font = '20px Arial'
ctx.value.fillStyle = '#E53E30';
ctx.value.fillText(textVal, inputX.value, inputY.value);
let json = {tools: 'text', text: textVal, x: inputX.value, y: inputY.value}
console.log('text-history-1', json)
history.value.push(json)
console.log('text-history-2', JSON.stringify(history))
}
textInput.value = "";
}
// 绘制实时效果
const drawCanvas = (event: MouseEvent) => {
//console.log('绘制-鼠标移动时调用-1')
//console.log('绘制-drawing', drawing.value)
// console.log('绘制-ctx', ctx.value)
if (!drawing.value || !ctx.value) return;
console.log('绘制-鼠标移动时调用-2')
const rect = canvas.value!.getBoundingClientRect();
const currentX = event.clientX - rect.left;
const currentY = event.clientY - rect.top;
ctx.value.strokeStyle = '#FF0000'; // 设置线条颜色为红色
ctx.value.lineWidth = 2; // 设置线条宽度为2px
// 当前绘制
redrawCanvas()
ctx.value.beginPath();
let tools = currentTool.value
console.log('当前绘制--tools--', tools)
if (tools === 'circle') {
console.log('当前绘制--圆形')
const radius = Math.sqrt(Math.pow(currentX - startX.value, 2) + Math.pow(currentY - startY.value, 2));
//(method) arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void
let end = Math.PI * 2
ctx.value.arc(startX.value, startY.value, radius, 0, end);
ctx.value.stroke();
let json = {tools: tools, x: startX.value, y: startY.value, radius: radius, start: 0, end: end}
history.value.push(json)
} else if (tools === 'rectangle') {
console.log('当前绘制--矩形')
const width = currentX - startX.value;
const height = currentY - startY.value;
//(method) rect(x: number, y: number, w: number, h: number): void
ctx.value.rect(startX.value, startY.value, width, height);
ctx.value.stroke();
let json = {tools: tools, x: startX.value, y: startY.value, w: width, h: height}
history.value.push(json)
}
}
// 停止绘制
const stopDrawing = () => {
drawing.value = false;
let last = history.value[history.value.length - 1]
console.log('停止绘制', JSON.stringify(last))
if (last && (last != undefined)) {
historyStep.value.push(last)
}
// 清空 history 数组
history.value = [];
}
// 撤销操作
const undo = () => {
console.log('撤销-click-1')
if(!historyStep.value || (historyStep.value == undefined)){
return
}
console.log('撤销-click-2')
historyStep.value.pop()
redrawCanvas()
}
// 还原操作
const redo = () => {
console.log('还原-click-1')
history.value = []
historyStep.value = []
redrawCanvas()
}
// 重新绘制历史记录
const redrawCanvas = () => {
console.log('重新绘制历史记录-0')
if (!ctx.value) return;
console.log('重新绘制历史记录-1')
ctx.value.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.value!.putImageData(imgData.value, 0, 0)
if(!historyStep.value || (historyStep.value == undefined)){
return
}
// 之前绘制的记录,画到画布上
console.log('历史记录', historyStep.value)
let len = historyStep.value.length
console.log('historyStep-length', len)
if (len == 0){
return
}
historyStep.value.forEach((item, index) => {
console.log('历史item-index', index, JSON.stringify(item))
let tools = item.tools
console.log('历史item-tools', tools)
ctx.value.beginPath();
if (tools === 'circle') {
console.log('历史item-绘制--圆形')
//(method) arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void
ctx.value.arc(item.x, item.y, item.radius, item.start, item.end);
ctx.value.stroke();
} else if (tools === 'rectangle') {
console.log('历史item-绘制--矩形')
//(method) rect(x: number, y: number, w: number, h: number): void
ctx.value.rect(item.x, item.y, item.w, item.h);
ctx.value.stroke();
} else if (tools === 'text') {
console.log('历史item-绘制--文字')
//(method) fillText(text: string, x: number, y: number, maxWidth?: number): void
ctx.value.fillText(item.text, item.x, item.y);
}
ctx.value!.closePath();
})
}
// 保存绘制后的图片(不包含透明图片)
const saveImage = () => {
console.log('保存绘制后的图片-1')
if (canvas.value) {
const dataUrl = canvas.value.toDataURL(); // 获取画布的图像数据URL
const link = document.createElement('a');
link.href = dataUrl;
link.download = 'canvas_image.png';
link.click(); // 自动触发下载
}
}
</script>
<style lang="css">
.panduUp_right{
width: 1000px;
height: 800px;
}
.btn{
width: 200px;
height: 40px;
margin-right: 50px;
}
#text {
position: absolute;
z-index: 9999;
resize: none;
outline: none;
border: 1px dashed #FBB47B;
overflow: hidden;
background: transparent;
line-height: 30px;
display: none;
}
</style>