当前位置: 代码网 > it编程>前端脚本>Vue.js > Vue3实现canvas画布组件自定义画板实例代码

Vue3实现canvas画布组件自定义画板实例代码

2024年10月28日 Vue.js 我要评论
代码示例:<template> <div> <div class="toolbar"> <el-color-picker v-model="

代码示例:

<template>
  <div>
    <div class="toolbar">
      <el-color-picker v-model="currentcolor" />
      <el-slider v-model="currentlinewidth" :min="1" :max="10" />
      <el-button :class="{ 'active': currenttool === 'brush' }" @click="selecttool('brush')">画笔</el-button>
      <el-button :class="{ 'active': currenttool === 'eraser' }" @click="selecttool('eraser')">橡皮擦</el-button>
      <el-slider v-if="currenttool === 'eraser'" v-model="erasersize" :min="10" :max="100" />
      <el-button :class="{ 'active': currenttool === 'rectangle' }" @click="selecttool('rectangle')">长方形</el-button>
      <el-button :class="{ 'active': currenttool === 'circle' }" @click="selecttool('circle')">圆形</el-button>
      <el-slider v-if="currenttool === 'check' || currenttool === 'cross' || currenttool === 'arrow'"
        v-model="shapesize" :min="10" :max="100" />
      <el-button :class="{ 'active': currenttool === 'check' }" @click="selecttool('check')">打√</el-button>
      <el-button :class="{ 'active': currenttool === 'cross' }" @click="selecttool('cross')">打×</el-button>
      <el-button :class="{ 'active': currenttool === 'arrow' }" @click="selecttool('arrow')">箭头</el-button>
      <el-button :class="{ 'active': currenttool === 'text' }" @click="selecttool('text')">文本</el-button>
      <el-button @click="clearcanvas">清除</el-button>
      <el-button @click="savecanvas">保存</el-button>
      <el-button @click="undo">撤销</el-button>
      <el-button @click="redo">重做</el-button>
      <el-button @click="rotatecanvas">翻转</el-button>
      <el-button @click="zoomin">放大</el-button>
      <el-button @click="zoomout">缩小</el-button>

    </div>
    <div class="canvas-container">
      <canvas ref="bgcanvas" width="800" height="600" style="position: absolute;"></canvas>
      <canvas ref="drawcanvas" width="800" height="600" style="position: absolute;"></canvas>
      <canvas ref="shapecanvas" @mousedown="handleclick" @mousemove="draw" @mouseup="stopdrawing"
        @mouseout="stopdrawing" width="800" height="600" style="position: absolute; border: 1px solid #000;"></canvas>
      <textarea v-if="currenttool === 'text'" v-model="textcontent" :style="[textstyle, { color: currentcolor }]"
        @blur="finishtextediting" @input="updatetextcontent" ref="textarea" class="text-editor"
        placeholder="请输入文本"></textarea>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onmounted, watch } from 'vue';

const props = defineprops({
  imageurl: {
    type: string,
    required: true,
  },
});

const bgcanvas = ref(null);
const drawcanvas = ref(null);
const shapecanvas = ref(null);
const bgcontext = ref(null);
const drawcontext = ref(null);
const shapecontext = ref(null);
const drawing = ref(false);
const currentcolor = ref('#e81e1e');
const currentlinewidth = ref(2);
const currenttool = ref('brush');
const erasersize = ref(20);
const history = reactive({
  undostack: [],
  redostack: [],
});
const shapes = ref([]);
const textshapes = ref([]); // 用于保存文本形状
const activeshape = ref(null);
const shapesize = ref(30);
const textcontent = ref('');
const textarea = ref(null);
const textstyle = reactive({
  position: 'absolute',
  border: '1px dashed black',
  backgroundcolor: 'rgba(255, 255, 255, 0)',
  resize: 'none',
  width: '200px',
  height: '100px',
  zindex: 10,
  display: 'none',
});
const rotation = ref(0);
const zoomfactor = ref(1); // 当前缩放比例
const rotatecanvas = () => {
  rotation.value = (rotation.value + 90) % 360;
  redrawcanvas();
};

const redrawcanvas = () => {
  clearallcanvases();
  drawimage();
  redrawshapecanvas();
};


const clearallcanvases = () => {
  drawcontext.value.clearrect(0, 0, drawcanvas.value.width, drawcanvas.value.height);
  shapecontext.value.clearrect(0, 0, shapecanvas.value.width, shapecanvas.value.height);
};

const savestate = () => {
  try {
    const state = {
      draw: drawcanvas.value.todataurl(),
      shapes: shapes.value.map(shape => ({ ...shape })),
      textshapes: textshapes.value.map(text => ({ ...text })),
      rotation: rotation.value,
    };
    history.undostack.push(state);
    history.redostack = [];
  } catch (e) {
    console.error('cannot save canvas state:', e);
  }
};


const restorestate = (state) => {
  drawcontext.value.clearrect(0, 0, drawcanvas.value.width, drawcanvas.value.height);
  shapecontext.value.clearrect(0, 0, shapecanvas.value.width, shapecanvas.value.height);

  const img = new image();
  img.src = state.draw;
  img.onload = () => {
    drawcontext.value.drawimage(img, 0, 0);
    shapes.value = state.shapes;
    textshapes.value = state.textshapes;
    rotation.value = state.rotation;
    redrawcanvas();  // 调用自定义的重绘函数以应用旋转
  };
};


const handleclick = (event) => {
  const { offsetx, offsety } = event;

  if (currenttool.value === 'text') {
    textstyle.left = `${offsetx}px`;
    textstyle.top = `${offsety}px`;
    textstyle.display = 'block';
    textarea.value.focus();
  } else if (['check', 'cross'].includes(currenttool.value)) {
    const shape = {
      type: currenttool.value,
      color: currentcolor.value,
      linewidth: currentlinewidth.value,
      startx: offsetx,
      starty: offsety,
      width: shapesize.value,
      height: shapesize.value,
    };
    shapes.value.push(shape);
    drawshape(shape);
    savestate();
  } else {
    startdrawing(event);
  }
};

const startdrawing = (event) => {
  drawing.value = true;
  const { offsetx, offsety } = event;

  if (currenttool.value === 'brush') {
    drawcontext.value.linewidth = currentlinewidth.value;
    drawcontext.value.strokestyle = currentcolor.value;
    drawcontext.value.beginpath();
    drawcontext.value.moveto(offsetx, offsety);
  } else if (['arrow', 'rectangle', 'circle'].includes(currenttool.value)) {
    const shape = {
      type: currenttool.value,
      color: currentcolor.value,
      linewidth: currentlinewidth.value,
      startx: offsetx,
      starty: offsety,
      width: 0,
      height: 0,
    };
    shapes.value.push(shape);
    activeshape.value = shape;
  }
};

const draw = (event) => {
  if (!drawing.value) return;

  const { offsetx, offsety } = event;

  if (currenttool.value === 'brush') {
    drawcontext.value.lineto(offsetx, offsety);
    drawcontext.value.stroke();
  } else if (currenttool.value === 'eraser') {
    const x = offsetx - erasersize.value / 2;
    const y = offsety - erasersize.value / 2;
    drawcontext.value.clearrect(x, y, erasersize.value, erasersize.value);
  } else if (['rectangle', 'circle', 'arrow'].includes(currenttool.value)) {
    if (activeshape.value) {
      activeshape.value.width = offsetx - activeshape.value.startx;
      activeshape.value.height = offsety - activeshape.value.starty;
      redrawshapecanvas();
    }
  }
};

const stopdrawing = () => {
  if (drawing.value) {
    if (currenttool.value === 'brush') {
      drawcontext.value.closepath();
    }
    savestate();
    drawing.value = false;
    activeshape.value = null;
  }
};

const clearcanvas = () => {
  drawcontext.value.clearrect(0, 0, drawcanvas.value.width, drawcanvas.value.height);
  shapecontext.value.clearrect(0, 0, shapecanvas.value.width, shapecanvas.value.height);
  shapes.value = [];
  textshapes.value = [];
  savestate();
};

const savecanvas = () => {
  const link = document.createelement('a');
  link.download = 'drawing.png';

  const combinedcanvas = document.createelement('canvas');
  combinedcanvas.width = drawcanvas.value.width;
  combinedcanvas.height = drawcanvas.value.height;
  const combinedcontext = combinedcanvas.getcontext('2d');

  combinedcontext.drawimage(bgcanvas.value, 0, 0);
  combinedcontext.drawimage(drawcanvas.value, 0, 0);
  combinedcontext.drawimage(shapecanvas.value, 0, 0);

  link.href = combinedcanvas.todataurl();
  link.click();
};

const selecttool = (tool) => {
  currenttool.value = tool;
  if (tool === 'eraser') {
    updateerasercursor();
  } else {
    shapecanvas.value.style.cursor = tool === 'brush' ? 'crosshair' : 'default';
  }
};

const updateerasercursor = () => {
  const cursorurl = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="${erasersize.value}" height="${erasersize.value}" viewbox="0 0 ${erasersize.value} ${erasersize.value}"><rect x="0" y="0" width="${erasersize.value}" height="${erasersize.value}" stroke="rgba(0, 0, 0, 0.5)" stroke-width="1" fill="rgba(255, 255, 255, 0.3)" /></svg>`;
  shapecanvas.value.style.cursor = `url('${cursorurl}') ${erasersize.value / 2} ${erasersize.value / 2}, auto`;
};

const undo = () => {
  if (history.undostack.length > 0) {
    const laststate = history.undostack.pop();
    history.redostack.push({
      draw: drawcanvas.value.todataurl(),
      shapes: shapes.value.map(shape => ({ ...shape })),
      textshapes: textshapes.value.map(text => ({ ...text })),
      rotation: rotation.value,
    });
    restorestate(laststate);
  }
};

const redo = () => {
  if (history.redostack.length > 0) {
    const laststate = history.redostack.pop();
    history.undostack.push({
      draw: drawcanvas.value.todataurl(),
      shapes: shapes.value.map(shape => ({ ...shape })),
      textshapes: textshapes.value.map(text => ({ ...text })),
      rotation: rotation.value,
    });
    restorestate(laststate);
  }
};
const zoomin = () => {
  zoomfactor.value *= 1.1; // 放大10%
  redrawcanvas();
};

const zoomout = () => {
  zoomfactor.value /= 1.1; // 缩小10%
  redrawcanvas();
};

const drawimage = () => {
  if (props.imageurl) {
    const img = new image();
    img.crossorigin = 'anonymous'; // 处理跨域图片
    img.src = props.imageurl;
    img.onload = () => {
      const padding = 30; // 设置留白区域的大小
      bgcontext.value.clearrect(0, 0, bgcanvas.value.width, bgcanvas.value.height);
      bgcontext.value.save();
      bgcontext.value.translate(padding, padding); // 在绘制时增加留白
      // bgcontext.value.drawimage(img, 0, 0, bgcanvas.value.width - 2 * padding, bgcanvas.value.height - 2 * padding);
      bgcontext.value.translate(bgcanvas.value.width / 2, bgcanvas.value.height / 2);
      bgcontext.value.rotate((rotation.value * math.pi) / 180);
      bgcontext.value.scale(zoomfactor.value, zoomfactor.value); // 应用缩放
      // bgcontext.value.drawimage(img, -bgcanvas.value.width / 2, -bgcanvas.value.height / 2, bgcanvas.value.width, bgcanvas.value.height);
      bgcontext.value.drawimage(img, -bgcanvas.value.width / 2, -bgcanvas.value.height / 2, bgcanvas.value.width - 2 * padding, bgcanvas.value.height - 2 * padding);
      bgcontext.value.restore();
    };
  }
};


const redrawshapecanvas = () => {
  shapecontext.value.clearrect(0, 0, shapecanvas.value.width, shapecanvas.value.height);
  shapes.value.foreach(shape => {
    drawshape(shape);
  });
  textshapes.value.foreach(text => {
    drawshape(text);
  });
};

const drawshape = (shape) => {
  shapecontext.value.beginpath();
  shapecontext.value.strokestyle = shape.color;
  shapecontext.value.linewidth = shape.linewidth;

  if (shape.type === 'rectangle') {
    shapecontext.value.rect(shape.startx, shape.starty, shape.width, shape.height);
  } else if (shape.type === 'circle') {
    shapecontext.value.arc(shape.startx, shape.starty, math.sqrt(math.pow(shape.width, 2) + math.pow(shape.height, 2)), 0, 2 * math.pi);
  } else if (shape.type === 'check') {
    shapecontext.value.beginpath();
    shapecontext.value.moveto(shape.startx, shape.starty);
    shapecontext.value.lineto(shape.startx + shape.width, shape.starty + shape.height / 2);
    shapecontext.value.lineto(shape.startx + shape.width * 2, shape.starty - shape.height);
    shapecontext.value.stroke();
  } else if (shape.type === 'cross') {
    shapecontext.value.beginpath();
    shapecontext.value.moveto(shape.startx, shape.starty);
    shapecontext.value.lineto(shape.startx + shape.width, shape.starty + shape.height);
    shapecontext.value.moveto(shape.startx, shape.starty + shape.height);
    shapecontext.value.lineto(shape.startx + shape.width, shape.starty);
    shapecontext.value.stroke();
  } else if (shape.type === 'arrow') {
    const headlength = 10;
    const angle = math.atan2(shape.height, shape.width);
    shapecontext.value.moveto(shape.startx, shape.starty);
    shapecontext.value.lineto(shape.startx + shape.width, shape.starty + shape.height);
    shapecontext.value.lineto(shape.startx + shape.width - headlength * math.cos(angle - math.pi / 6), shape.starty + shape.height - headlength * math.sin(angle - math.pi / 6));
    shapecontext.value.moveto(shape.startx + shape.width, shape.starty + shape.height);
    shapecontext.value.lineto(shape.startx + shape.width - headlength * math.cos(angle + math.pi / 6), shape.starty + shape.height - headlength * math.sin(angle + math.pi / 6));
  } else if (shape.type === 'text') {
    shapecontext.value.fillstyle = shape.color;
    shapecontext.value.font = '16px arial';
    shapecontext.value.textalign = 'left';
    shapecontext.value.textbaseline = 'top';
    shapecontext.value.filltext(shape.content, shape.startx, shape.starty);
  }

  shapecontext.value.stroke();
};

const finishtextediting = () => {
  if (textcontent.value.trim() === '') return;
  const { offsetleft, offsettop, offsetwidth, offsetheight } = textarea.value;
  const shape = {
    type: 'text',
    content: textcontent.value,
    color: currentcolor.value,
    startx: offsetleft,
    starty: offsettop,
    width: offsetwidth,
    height: offsetheight,
  };
  textshapes.value.push(shape);
  redrawshapecanvas();
  textcontent.value = '';
  textstyle.display = 'none';
};

const updatetextcontent = () => {
  // optional: handle text content updates here if needed
};

onmounted(() => {
  bgcontext.value = bgcanvas.value.getcontext('2d');
  drawcontext.value = drawcanvas.value.getcontext('2d');
  shapecontext.value = shapecanvas.value.getcontext('2d');
  drawimage();
  savestate();
});

watch(() => props.imageurl, drawimage);
watch(() => erasersize.value, updateerasercursor);
</script>

<style scoped>
.toolbar {
  margin-bottom: 10px;
}

.canvas-container {
  position: relative;
  /* width: 800px;
  height: 600px; */
}

canvas {
  cursor: default;
  /* border: 1px solid #ccc; */
  /* padding: 20px; */
}

.text-editor {
  display: none;
  /* hidden initially */
}

.el-button.active {
  background-color: #409eff;
  color: white;
}

.text-editor {
  display: block;
  position: absolute;
  border: 2px solid #ddd;
  border-radius: 4px;
  background-color: #f9f9f9;
  font-size: 14px;
  padding: 10px;
  resize: none;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  z-index: 10;
  transition: border-color 0.3s ease;
  width: 200px;
  height: 100px;
  top: 0;
  left: 0;
}

.text-editor:focus {
  border-color: #409eff;
  outline: none;
}

.text-editor::placeholder {
  color: #888;
  font-style: italic;
}
</style>

使用

 效果图

到此这篇关于vue3实现canvas画布组件自定义画板的文章就介绍到这了,更多相关vue3 canvas画布组件自定义画板内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com