<template>
<div>
<!-- Canvas -->
<canvas ref="canvas" width="500" height="500" @mousedown="handleMouseDown" @mousemove="handleMouseMove"
@mouseup="handleMouseUp" style="border: 1px solid #ccc; cursor: crosshair; position: relative;">
</canvas>
<!-- 按钮 -->
<button @click="setMode('select')">画框</button>
<button @click="setMode('text')">打字</button>
<button @click="setMode('arrow')">箭头</button>
<button @click="setMode('draw')">画笔</button>
<button @click="clearCanvas">清屏</button>
<button @click="saveImage">保存</button>
<!-- <label>画笔颜色:</label>
<input type="color" v-model="penColor">
<label>画笔粗细:</label>
<input type="range" min="1" max="20" v-model.number="penSize"> -->
<!-- 图片上传 -->
<input type="file" accept="image/*" @change="handleImageUpload" />
<!-- 可拖动的文本输入区 -->
<textarea v-if="isTextEditing" ref="textInput" @blur="finishTextEditing"
@keydown.enter.exact.prevent="finishTextEditing" :style="textAreaStyle" class="transparent"
style="position: absolute; z-index: 10; border: 2px dashed #ff0000;"></textarea>
</div>
</template>
<script setup>
import { ref, onMounted, computed, reactive, nextTick } from 'vue';
// import imgtwo from './assets/001.PNG'
const canvas = ref(null);
const ctx = ref(null);
let image = ref(null);
let mode = ref('none'); // select / text / none
let selections = ref([]); // 存储所有矩形框
let texts = ref([]); // 存储所有文字内容
let isDrawingRect = ref(false);
let rectStartPos = reactive({ x: 0, y: 0 });
let rectEndPos = reactive({ x: 0, y: 0 });
let isTextEditing = ref(false);
let textPosition = reactive({ x: 0, y: 0 });
let textInput = ref(null)
let imgtwo = 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
//画笔
let isDrawing = ref(false); // 新增:是否正在画笔绘制
let strokes = ref([]); // 存储所有画笔轨迹,每个轨迹是一个点数组
let currentStroke = ref([]); // 当前正在画的笔画
let penColor = ref('#0000ff'); // 画笔颜色
let penSize = ref(3); // 画笔粗细
//箭头
let isDrawingArrow = ref(false); // 新增:是否正在绘制箭头
let arrowStartPos = reactive({ x: 0, y: 0 }); // 箭头起点位置
let arrowEndPos = reactive({ x: 0, y: 0 }); // 箭头终点位置
let arrows = ref([]); // 存储所有箭头
function clearCanvas() {
// 清空所有绘制数据
selections.value = [];
texts.value = [];
strokes.value = [];
arrows.value = [];
currentStroke.value = [];
isDrawingRect.value = false;
isDrawing.value = false;
isDrawingArrow.value = false;
// 如果有背景图片,重绘背景;否则只清空 canvas
redraw();
}
onMounted(() => {
ctx.value = canvas.value.getContext('2d');
const img = new Image();
img.onload = () => {
image.value = img;
// 设置 canvas 宽高为图片原始尺寸
canvas.value.width = img.width;
canvas.value.height = img.height;
redraw();
};
// 使用导入的图片资源
img.src = imgtwo;
});
function setMode(newMode) {
// 清除其他模式的状态
isDrawingRect.value = false;
isDrawing.value = false;
isDrawingArrow.value = false;
currentStroke.value = [];
if (mode.value !== newMode) {
mode.value = newMode;
} else {
mode.value = 'none'; // 再点一次切换回 none
}
}
function handleImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
image.value = img;
redraw();
};
img.src = URL.createObjectURL(file);
}
function handleMouseDown(e) {
const rect = canvas.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (mode.value === 'select') {
//已有的矩形框逻辑
rectStartPos.x = x;
rectStartPos.y = y;
isDrawingRect.value = true;
} else if (mode.value === 'text' && !isTextEditing.value) {
//已有的文本输入逻辑
startTextEditing(e);
} else if (mode.value === 'draw') {
//已有的自由绘制逻辑
isDrawing.value = true;
currentStroke.value = [{ x, y }];
} else if (mode.value === 'arrow') {
isDrawingArrow.value = true;
arrowStartPos.x = x;
arrowStartPos.y = y;
}
}
function handleMouseMove(e) {
const rect = canvas.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (mode.value === 'select' && isDrawingRect.value) {
rectEndPos.x = x;
rectEndPos.y = y;
redraw();
} else if (mode.value === 'draw' && isDrawing.value) {
currentStroke.value.push({ x, y });
redraw();
} else if (mode.value === 'arrow' && isDrawingArrow.value) {
arrowEndPos.x = x;
arrowEndPos.y = y;
redraw();
}
}
function handleMouseUp(e) {
const rect = canvas.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (mode.value === 'select' && isDrawingRect.value) {
rectEndPos.x = x;
rectEndPos.y = y;
selections.value.push({
startX: rectStartPos.x,
startY: rectStartPos.y,
endX: rectEndPos.x,
endY: rectEndPos.y,
});
isDrawingRect.value = false;
redraw();
}
if (mode.value === 'draw' && isDrawing.value) {
currentStroke.value.push({ x, y });
strokes.value.push({
points: [...currentStroke.value],
color: penColor.value,
size: penSize.value,
});
currentStroke.value = [];
isDrawing.value = false;
}
if (mode.value === 'arrow' && isDrawingArrow.value) {
arrowEndPos.x = x;
arrowEndPos.y = y;
arrows.value.push({
startX: arrowStartPos.x,
startY: arrowStartPos.y,
endX: arrowEndPos.x,
endY: arrowEndPos.y,
});
isDrawingArrow.value = false;
redraw();
}
}
function startTextEditing(e) {
if (mode.value === 'text') {
console.log('开始打字');
}
const rect = canvas.value.getBoundingClientRect();
textPosition.x = e.clientX - rect.left;
textPosition.y = e.clientY - rect.top;
isTextEditing.value = true;
// nextTick(() => {
// textInput.value.focus(); // 自动聚焦到文本输入框
// });
}
function finishTextEditing(e) {
const textContent = e.target.value.trim();
if (textContent.length > 0) {
texts.value.push({ x: textPosition.x, y: textPosition.y, content: textContent });
}
isTextEditing.value = false;
redraw();
}
const textAreaStyle = computed(() => ({
left: `${textPosition.x}px`,
top: `${textPosition.y}px`,
// 可以适当调整这些值来改善文本框的定位
// transform: 'translate(-50%, -0%)', // 根据需要调整,使得点击位置位于文本框的底部中央
}));
function redraw() {
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
if (image.value) {
ctx.value.drawImage(image.value, 0, 0, canvas.value.width, canvas.value.height);
}
// 绘制矩形框
selections.value.forEach(selection => {
ctx.value.strokeStyle = '#fff';
ctx.value.strokeRect(
selection.startX,
selection.startY,
selection.endX - selection.startX,
selection.endY - selection.startY
);
});
// 绘制文字
texts.value.forEach(txt => {
ctx.value.font = '20px Arial';
ctx.value.fillStyle = '#ff0000';
ctx.value.fillText(txt.content, txt.x, txt.y);
});
// 绘制画笔轨迹
strokes.value.forEach(stroke => {
ctx.value.beginPath();
ctx.value.moveTo(stroke.points[0].x, stroke.points[0].y);
for (let i = 1; i < stroke.points.length; i++) {
ctx.value.lineTo(stroke.points[i].x, stroke.points[i].y);
}
ctx.value.strokeStyle = stroke.color;
ctx.value.lineWidth = stroke.size;
ctx.value.lineCap = 'round';
ctx.value.stroke();
});
// 绘制当前正在画的笔画(如果有的话)
if (currentStroke.value.length > 1) {
ctx.value.beginPath();
ctx.value.moveTo(currentStroke.value[0].x, currentStroke.value[0].y);
for (let i = 1; i < currentStroke.value.length; i++) {
ctx.value.lineTo(currentStroke.value[i].x, currentStroke.value[i].y);
}
ctx.value.strokeStyle = penColor.value;
ctx.value.lineWidth = penSize.value;
ctx.value.lineCap = 'round';
ctx.value.stroke();
}
// 绘制虚线矩形
if (isDrawingRect.value && mode.value === 'select') {
ctx.value.setLineDash([5, 3]);
ctx.value.strokeStyle = '#0000ff';
ctx.value.strokeRect(
rectStartPos.x,
rectStartPos.y,
rectEndPos.x - rectStartPos.x,
rectEndPos.y - rectStartPos.y
);
ctx.value.setLineDash([]);
}
// 绘制箭头
arrows.value.forEach(arrow => {
drawArrow(ctx.value, arrow.startX, arrow.startY, arrow.endX, arrow.endY);
});
// 如果当前正在绘制箭头,则绘制临时箭头
if (isDrawingArrow.value && mode.value === 'arrow') {
drawArrow(ctx.value, arrowStartPos.x, arrowStartPos.y, arrowEndPos.x, arrowEndPos.y);
}
}
function drawArrow(ctx, fromx, fromy, tox, toy) {
const headlen = 10; // 箭头头部长度
const angle = Math.atan2(toy - fromy, tox - fromx);
ctx.beginPath();
ctx.moveTo(fromx, fromy);
ctx.lineTo(tox, toy);
ctx.strokeStyle = '#ff0000'; // 设置箭头颜色
ctx.lineWidth = 2; // 设置线条宽度
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tox, toy);
ctx.lineTo(tox - headlen * Math.cos(angle - Math.PI / 6), toy - headlen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(tox - headlen * Math.cos(angle + Math.PI / 6), toy - headlen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#ff0000';
ctx.fill();
}
function saveImage() {
//保存成base64
// 获取 canvas 数据为 Base64 格式的 Data URL
const base64URL = canvas.value.toDataURL('image/png'); // 默认是 png,也可以指定 'image/jpeg'
// 打印查看结果(调试用)
console.log('Base64 图片数据:', base64URL);
return
// 1. 将 canvas 转换为 Blob 对象(即图片二进制)
canvas.value.toBlob(async function (blob) {
if (!blob) {
alert('生成图片失败');
return;
}
// 2. 创建 FormData 并附加文件
const formData = new FormData();
formData.append('image', blob, 'annotation.png'); // 可以改名为 .jpg 或根据需求命名
console.log('即将上传的图片:', formData);
// 3. 发送到后端
}, 'image/png'); // 可改为 'image/jpeg' 等格式
}
</script>
<style scoped>
canvas {
margin-top: 10px;
}
.transparent {
background-color: rgba(0, 0, 0, 0.1);
resize: none;
outline: none;
color: red;
}
</style>
Vue3+canvas图片编辑器(画框,打字,箭头,画笔,清屏,保存)
于 2025-07-07 16:21:37 首次发布