阅卷批改——SVG涂鸦组件

简介

因为业务需要,需要写一个可以在图片上涂鸦打分的功能。在考虑了Canvas和SVG之后,决定使用SVG图片来实现(这里遇到一个坑,一开始需求是只做PC端的,需求变更后面新增了一个小程序,但是小程序不支持SVG,无奈只能用Canvas又写了一套)。

  • 最终效果图,可以随意涂鸦打分、自定义工具、拖拽、缩放等。

image.png

image.png

实现方案

一开始得到这个需求,考虑过三种实现方案:

  1. 直接使用使用DIV元素结合CSS来实现涂鸦功能,相对简单,不需要像SVG和Canvas那样处理绘图逻辑。DIV元素可以很容易地响应用户的鼠标或触摸事件,通过CSS样式可以实现一些简单的涂鸦效果。
  2. 使用Canvas来实现,画布嘛,功能强大,玩得溜的话可以做出十分炫酷的功能。对于涂鸦功能,Canvas能够提供更好的性能,特别是当需要频繁更新画面时。但是使用起来没有直接操作DOM元素方便。
  3. SVG所能够提供的功能和我们的业务需求十分贴切,它可以直接绘制多种图形,操作简便,所以最后是选用了SVG来实现需求。

SVG是一种用于描述二维图形和图形应用程序的XML标记语言,使用矢量路径来描述图形,这意味着图形由数学公式定义,可以无损缩放,并且在不同分辨率下保持清晰度。SVG适用于静态图形和图标,以及需要交互和动画效果的场景。

从以下代码可以观察到SVG图片内部是由一个个元素组成的,十分的语义化,可以直接绘制矩形、圆形、直线等,非常的方便。

<!DOCTYPE html>

<html>

<head>
  <title>SVG绘图示例</title>
</head>
<body>
  <!-- 创建SVG容器 -->
  <svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">

    <!-- 绘制一个矩形 -->
    <rect x="50" y="50" width="100" height="100" fill="blue" />


    <!-- 绘制一个圆形 -->
    <circle cx="150" cy="150" r="30" fill="red" />

    <!-- 绘制一条直线 -->
    <line x1="10" y1="10" x2="190" y2="190" stroke="black" />


    <!-- 绘制一个文本 -->
    <text x="50" y="20" font-family="Arial" font-size="14" fill="green">Hello, SVG!</text>


  </svg>
</body>
</html>

创建元素

在JavaScript中创建SVG元素时,需要使用document.createElementNS方法而不是普通的document.createElement方法,是因为SVG元素属于XML命名空间,而不是HTML命名空间。

export default function createElement<K extends keyof SVGElementTagNameMap>(
  name: K,
  brush?: Partial<Brush>
): SVGElementTagNameMap[K] {
  const el = document.createElementNS("http://www.w3.org/2000/svg", name);

  el.setAttribute("fill", brush?.fill ?? "transparent");

  if (brush?.color) el.setAttribute("stroke", brush.color);
  if (brush?.size) el.setAttribute("stroke-width", brush.size.toString());
  el.setAttribute("stroke-linecap", "round");


  if (brush?.dasharray) el.setAttribute("stroke-dasharray", brush!.dasharray);


  return el;
}

画一个矩形

原理:监听鼠标事件,在鼠标按下(mousedown)的时候记录起点的坐标,在移动(mousemove)的时候根据起点坐标和当前移动时的坐标来计算矩形的宽高,随后根据宽高来绘制矩形,就可以随着鼠标的移动画出一个矩形了。

鼠标移动的时候需要移除之前的矩形,避免重叠绘制。

image.png

  • html
<!DOCTYPE html>

<html>

  <head>
    <title>拖动绘制矩形</title>
  </head>
  <body>
    <!-- 创建SVG容器 -->
    <svg
      id="svgContainer"
      width="100vw"
      height="100vh"
      xmlns="http://www.w3.org/2000/svg"
    ></svg>


    <script src="app.js"></script>
  </body>
</html>
  • script

const svgContainer = document.getElementById("svgContainer");
let isDrawing = false;
let startPoint = { x: 0, y: 0 };

// 添加鼠标按下事件监听器
svgContainer.addEventListener("mousedown", (event) => {
isDrawing = true;
startPoint = getSVGCoordinates(event.clientX, event.clientY);
});

// 添加鼠标移动事件监听器
svgContainer.addEventListener("mousemove", (event) => {
if (isDrawing) {
const currentPoint = getSVGCoordinates(event.clientX, event.clientY);
const width = currentPoint.x - startPoint.x;
const height = currentPoint.y - startPoint.y;


    // 移除之前的矩形(避免重叠绘制)
    svgContainer.innerHTML = "";


    // 绘制矩形
    const rectangle = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "rect"
    );
    rectangle.setAttribute("x", startPoint.x);
    rectangle.setAttribute("y", startPoint.y);
    rectangle.setAttribute("width", width);
    rectangle.setAttribute("height", height);
    rectangle.setAttribute("fill", "none");
    rectangle.setAttribute("stroke", "black");
    svgContainer.appendChild(rectangle);

}
});

// 添加鼠标抬起事件监听器
svgContainer.addEventListener("mouseup", () => {
isDrawing = false;
});

// 辅助函数:获取鼠标事件在SVG容器中的坐标
function getSVGCoordinates(clientX, clientY) {
const svgPoint = svgContainer.createSVGPoint();
svgPoint.x = clientX;
svgPoint.y = clientY;
const svgMatrix = svgContainer.getScreenCTM().inverse();
const transformedPoint = svgPoint.matrixTransform(svgMatrix);
return { x: transformedPoint.x, y: transformedPoint.y };
}


getSVGCoordinates,通过这个函数,你可以将鼠标事件的客户端坐标(相对于浏览器窗口)转换为SVG容器内的坐标,这对于在SVG容器中进行绘图或交互时非常有用。

vue3中使用

原理是一样的,可以加上图片,下面是一个简单的示例。

<template>
  <div class="canvas" ref="canvas">
    <svg
      ref="svgContainer"
      :width="imageWidth"
      :height="imageHeight"
      @mousedown="startDrawing"
      @mousemove="draw"
      @mouseup="endDrawing"
    >
      <!-- 显示要打分的图片 -->
      <image
        :x="0"
        :y="0"
        :width="imageWidth"
        :height="imageHeight"
        href="./assets/test.png"
      />

      <!-- 绘制涂鸦的内容 -->
      <g v-for="shape in drawingObjects" :key="shape.id">
        <template v-if="shape.type === ShapeType.Rectangle">
          <rect
            :x="shape.x"
            :y="shape.y"
            :width="shape.width"
            :height="shape.height"
            fill="none"
            stroke="black"
          />
        </template>
        <template v-else-if="shape.type === ShapeType.Circle">
          <circle
            :cx="shape.cx"
            :cy="shape.cy"
            :r="shape.radius"
            fill="none"
            stroke="black"
          />
        </template>
      </g>
    </svg>
  </div>
  <div>
    <button @click="setCurrentShape(ShapeType.Rectangle)">绘制矩形</button>
    <button @click="setCurrentShape(ShapeType.Circle)">绘制圆形</button>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from "vue";


const svgContainer = ref<SVGSVGElement | null>(null);
const canvas = ref<HTMLDivElement | null>(null);

enum ShapeType {
  Rectangle = "rectangle",
  Circle = "circle",
}

interface Shape {
  id: number;
  type: ShapeType;
  x: number;
  y: number;
  width?: number;
  height?: number;
  cx?: number;
  cy?: number;
  radius?: number;
}

const drawingObjects = reactive<Shape[]>([]);

const imageWidth = 500;
const imageHeight = 300;

const isDrawing = ref(false);
const startPoint = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const currentShape = ref<ShapeType>(ShapeType.Rectangle);

function startDrawing(event: MouseEvent) {
  isDrawing.value = true;
  startPoint.value = getSVGCoordinates(event.clientX, event.clientY);
}

function draw(event: MouseEvent) {
  if (!isDrawing.value || !currentShape.value) return;
  drawingObjects.splice(0, drawingObjects.length);

  const currentPoint = getSVGCoordinates(event.clientX, event.clientY);
  const width = currentPoint.x - startPoint.value.x;
  const height = currentPoint.y - startPoint.value.y;

  if (currentShape.value === ShapeType.Rectangle) {
    drawingObjects.push({
      id: Date.now(),
      type: ShapeType.Rectangle,
      x: startPoint.value.x,
      y: startPoint.value.y,
      width,
      height,
    });
  } else if (currentShape.value === ShapeType.Circle) {
    const cx = startPoint.value.x + width / 2;
    const cy = startPoint.value.y + height / 2;
    const radius = Math.abs(width / 2);

    drawingObjects.push({
      id: Date.now(),
      type: ShapeType.Circle,
      cx,
      cy,
      radius,
      x: 0,
      y: 0,
    });
  }
}

function endDrawing() {
  isDrawing.value = false;
}

function getSVGCoordinates(clientX: number, clientY: number) {
  const svgPoint = svgContainer.value!.createSVGPoint();
  svgPoint.x = clientX;
  svgPoint.y = clientY;
  const svgMatrix = svgContainer.value!.getScreenCTM()!.inverse();
  const transformedPoint = svgPoint.matrixTransform(svgMatrix);
  return { x: transformedPoint.x, y: transformedPoint.y };
}

function setCurrentShape(shapeType: ShapeType) {
  currentShape.value = shapeType;
}
</script>

<style>
.canvas {
  position: relative;
  overflow: hidden;
}
</style>

在写的过程中发现了一个很棒的库 drauu,大家可以去看一下。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYZhtfM4' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片