当前位置: 首页 > article >正文

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>

http://www.kler.cn/a/400948.html

相关文章:

  • SD模型微调之Textual Inversion和Embedding fine-tuning
  • TDSQL 免密码登录
  • 【gitlab】gitlabrunner部署
  • LeetCode 2816.翻倍以链表形式表示的数字
  • Docker: ubuntu系统下Docker的安装
  • Quartus+Nios II for eclipse问题合集
  • EUV光刻胶行业全面且深入的分析
  • 集群聊天服务器(7)数据模块
  • sql server index FORCESEEK
  • 微搭低代码入门05循环
  • 【Redis_Day4】内部编码和单线程模型
  • 汽车资讯新纪元:Spring Boot技术引领
  • 深入解析大带宽服务器:性能优势与选择指南
  • 力扣.259 较小的三数之和
  • Redis 高并发缓存架构实战与性能优化
  • 从零开始仿抖音做一个APP(首页顶部T标签添加页面关联)
  • Android15之解决:Dex checksum does not match for dex:framework.jar问题(二百三十九)
  • ---usb 摄像头的Linux 下查询的命令
  • Go语言教程(一看就会)
  • 高级java每日一道面试题-2024年11月08日-RabbitMQ篇-RabbitMQ有哪些重要的角色?有哪些重要的组件?
  • 用AI来写SQL:让ChatGPT成为你的数据库助手
  • Spring Boot汽车资讯:科技与汽车的新融合
  • Spring Boot开箱即用可插拔实现过程演练与原理剖析
  • 【golang-技巧】-线上死锁问题排查-by pprof
  • React Native 全栈开发实战班 - 原生功能集成之权限管理
  • Qt 和 WPF(Windows Presentation Foundation)