跳至主要內容

SVG涂鸦组件(可 npm 发布)

sharebraverySVG画图图形大约 2 分钟

SVG 涂鸦组件(可 npm 发布)

我司有一个阅卷系统,涉及到了阅卷打分,需要写一个涂鸦组件,经过 SVG 和 Canvas 的对比使用了 SVG(后续增加需求开发小程序端,由于小程序不支持 svg 画图,使用了 canvas)。

独立组件配置

当时写这个组件的时候考虑到可能会多个项目使用,于是是作为一个可 npm 安装的独立组件来开发的。

package.json

  "files": [
    "lib"
  ],
  "main": "./lib/ch2-lib.umd.ts",
  "module": "./lib/ch2-lib.es.ts",
  "types": "./lib/types/packages/index.d.ts",
  "exports": {
    ".": {
      "import": "./lib/ch2-lib.es.ts",
      "require": "./lib/ch2-lib.umd.ts"
    },
    "./lib/style.css": {
      "import": "./lib/style.css",
      "require": "./lib/style.css"
    }
  },

vite.config.ts

  build: {
    outDir: "lib",
    lib: {
      entry: resolve(__dirname, "src/packages/index.ts"),
      name: "Ch2Lib",
      fileName: (format) => `ch2-lib.${format}.ts`,
    },
    rollupOptions: {
      // 确保外部化处理那些你不想打包进库的依赖
      external: ["vue", "@types/node"],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: "Vue",
        },
      },
    },
  },

tsconfig.json

在该文件中加入这两行

"declaration": true,

"declarationDir": "./lib/types",

本地开发

本项目运行 yarn link

使用项目运行* yarn link your project name

项目结构

avatar
avatar

几个画图、解析数据的函数

createElement

注意创建 SVG 元素需要使用 createElementNS

document.createElementNS("http://www.w3.org/2000/svg", name);

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;
}

drawingText

function drawingText() {
  const el = createDrawingTextNode();

  canvas!.value!.appendChild(el);

  el.focus();

  isEdit.value = true;

  const inputBlur = () => {
    const displayNodeOnClick = (e: any) => {
      e.stopPropagation();

      e.preventDefault();
      displayNode.style.display = "none";

      el.style.display = "block";
      el.focus();
    };

    const textEl: IText = {
      node: el,
      outerHTML: el.outerHTML,
      mode: currentMode.value,
      left: upPoint.value?.x!,
      top: upPoint.value?.y!,
      text: el.value,
    };

    const index = designData.findIndex((e) => e.node === el);

    el.style.display = "none";

    const displayNode = createTextDisplayNode(
      textEl,
      defaultOptions.value.scale
    );

    displayNode.style.cursor = "pointer";

    displayNode.addEventListener("click", displayNodeOnClick);

    canvas!.value!.appendChild(displayNode);

    // 将数据添加到数据源

    const displayEl: IText = {
      node: displayNode,
      outerHTML: displayNode.outerHTML,
      mode: currentMode.value,
      left: upPoint.value?.x!,
      top: upPoint.value?.y!,
      text: el.value,
    };

    if (index > -1) {
      designData[index].text = el.value;
      emit("update:designData", designData, displayEl);
    } else {
      if (el.value) {
        const newItem = percentCustomToolPosition(displayEl);
        const temp = { ...displayEl, ...newItem };
        designData.push(temp);
        emit("update:designData", designData, temp);
      } else {
        el.remove();
      }
    }
  };

  // 监听焦点离开
  el.addEventListener("blur", inputBlur);

  currentTextNode.value = el;
}

数据百分比转换,保持图形比例、位置

avatar
avatar

template 图片拖动

 <div
    class="container"
    ref="container"
    :key="count"
    :style="`background:${defaultOptions.backgroundColor}`"
  >
    <div
      class="canvas"
      ref="canvas"
      :style="`transform: scale(${defaultOptions.scale});text-align:${
        defaultOptions.imgPosition
      };pointer-events:${defaultOptions.toolbarShow ? 'auto' : 'none'}`"
    >
      <slot>
        <img
          @mousedown.prevent="onDragMousedown"
          class="bgImg"
          ref="bgImgRef"
          :src="defaultOptions.bgImgUrl"
          :style="`transform:rotate(${defaultOptions.rotate}deg );`"
          @load="imgLoad"
        />
      </slot>
      <svg
        ref="target"
        :style="`transform:rotate(${defaultOptions.rotate}deg );`"
      ></svg>
    </div>

    <div
      style="
        position: fixed;
        bottom: 0;
        display: flex;
        margin: 12px auto;
        justify-content: center;
      "
      v-show="defaultOptions.toolbarShow"
    >
      <Actions :actions="defaultOptions.actions" @actionChange="actionChange" />
      <Tools :tools="defaultOptions.tools" @toolChange="toolChange" />
    </div>
  </div>
</template>
function onDragMousedown(downEvent: MouseEvent) {
  downEvent.preventDefault();

  if (!target.value) return;

  const { clientX: startX, clientY: startY } = downEvent;

  const { offsetLeft, offsetTop } = target.value;

  document.onmousemove = (moveEvent: MouseEvent) => {
    const { clientX, clientY } = moveEvent;

    const x = offsetLeft + clientX - startX;
    const y = offsetTop + clientY - startY;
    target.value!.style["left"] = x + "px";
    target.value!.style["top"] = y + "px";
  };

  document.onmouseup = () => {
    document.onmousemove = document.onmouseup = null;
  };
}

用例

<script lang="ts" setup>
import { Ch2PaperOfPainter } from "ch2-paper-of-painter";
import "ch2-paper-of-painter/lib/style.css";
</script>

<template>
  <Ch2PaperOfPainter style="width: 800px; height: 700px" />
</template>
avatar
avatar