简介
因为业务需要,需要写一个可以在图片上涂鸦打分的功能。在考虑了Canvas和SVG之后,决定使用SVG图片来实现(这里遇到一个坑,一开始需求是只做PC端的,需求变更后面新增了一个小程序,但是小程序不支持SVG,无奈只能用Canvas又写了一套)。
- 最终效果图,可以随意涂鸦打分、自定义工具、拖拽、缩放等。
实现方案
一开始得到这个需求,考虑过三种实现方案:
- 直接使用使用DIV元素结合CSS来实现涂鸦功能,相对简单,不需要像SVG和Canvas那样处理绘图逻辑。DIV元素可以很容易地响应用户的鼠标或触摸事件,通过CSS样式可以实现一些简单的涂鸦效果。
- 使用Canvas来实现,画布嘛,功能强大,玩得溜的话可以做出十分炫酷的功能。对于涂鸦功能,Canvas能够提供更好的性能,特别是当需要频繁更新画面时。但是使用起来没有直接操作DOM元素方便。
- 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)的时候根据起点坐标和当前移动时的坐标来计算矩形的宽高,随后根据宽高来绘制矩形,就可以随着鼠标的移动画出一个矩形了。
鼠标移动的时候需要移除之前的矩形,避免重叠绘制。
- 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,大家可以去看一下。
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END