阅卷批改——SVG涂鸦组件

简介

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

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

实现方案

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

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

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

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

xml 复制代码
<!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命名空间。

arduino 复制代码
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)的时候根据起点坐标和当前移动时的坐标来计算矩形的宽高,随后根据宽高来绘制矩形,就可以随着鼠标的移动画出一个矩形了。

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

  • html
xml 复制代码
<!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
ini 复制代码
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中使用

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

ini 复制代码
<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,大家可以去看一下。

相关推荐
明远湖之鱼4 天前
opentype.js 使用与文字渲染
前端·svg·字体
wsWmsw6 天前
[译] 浏览器里的 Liquid Glass:利用 CSS 和 SVG 实现折射
前端·css·svg
CodeCraft Studio7 天前
CAD文件处理控件Aspose.CAD教程:在 Python 中将 SVG 转换为 PDF
开发语言·python·pdf·svg·cad·aspose·aspose.cad
红烧code18 天前
【Rust GUI开发入门】编写一个本地音乐播放器(4. 绘制按钮组件)
rust·gui·svg·slint
吃饺子不吃馅18 天前
AntV X6图编辑器如何实现切换主题
前端·svg·图形学
吃饺子不吃馅20 天前
深感一事无成,还是踏踏实实做点东西吧
前端·svg·图形学
吃饺子不吃馅22 天前
AntV X6 核心插件帮你飞速创建画布
前端·css·svg
吃饺子不吃馅23 天前
揭秘 X6 核心概念:Graph、Node、Edge 与 View
前端·javascript·svg
吃饺子不吃馅23 天前
如何让AntV X6 的连线“动”起来:实现流动效果?
前端·css·svg
拜无忧1 个月前
html,svg,花海扩散效果
前端·css·svg