Vue3+canvas图片编辑器(画框,打字,箭头,画笔,清屏,保存)

在这里插入图片描述

<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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值