阅卷批改——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,大家可以去看一下。

相关推荐
E_ICEBLUE4 小时前
PPT 批量转图片:在 Web 预览中实现翻页效果(C#/VB.NET)
c#·powerpoint·svg
Highcharts.js10 小时前
如何使用Highcharts SVG渲染器?
开发语言·javascript·python·svg·highcharts·渲染器
谜亚星1 个月前
SVG学习(五)
前端·svg
harrain1 个月前
前端svg精微操作局部动态改变呈现工程网架状态程度可视播放效果
前端·svg·工程网架图
我真的叫奥运1 个月前
scss mixin svg 颜色控制 以及与 png 方案对比讨论
前端·svg
harrain1 个月前
html里引入使用svg的方法
前端·svg
咬人喵喵1 个月前
SVG 答题类互动模板汇总(共 16 种/来自 E2 编辑器)
编辑器·svg·e2 编辑器
咬人喵喵1 个月前
16 类春节核心 SVG 交互方案拆解(E2 编辑器实战)
前端·css·编辑器·交互·svg
李少兄1 个月前
简单讲讲 SVG:前端开发中的矢量图形
前端·svg
咬人喵喵1 个月前
文生图:AI 是怎么把文字变成画的?
人工智能·编辑器·svg