(10月最新) 前端图形学实战: 从零开发几何画板(vue3 + vite版)

简介: (10月最新) 前端图形学实战: 从零开发几何画板(vue3 + vite版)

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究

前言

hello, 大家好, 我是徐小夕, 今天又到了我们的博学时间。

本文是 100+前端几何学应用案例 专栏的第二篇文章, 在第一篇文章几何学在前端边界计算中的应用和原理分析 中我介绍了几何学在前端领域里的应用, 同时用 vue3 带大家一起实现了常见图形的边界计算算法, 并且分享了如何用几何原理Web Dom生成任意三角形的方式:

image.png image.png

如果大家感兴趣可以在 gitee 查看我的具体代码实现: gitee.com/lowcode-chi…

接下来就继续这个话题, 我们进一步扩展, 来从零实现一个几何画板

你将收获

  • vue3 + vite的实用技巧
  • 几何画板的基本开发思路
  • 元素创建,
  • 编辑,
  • 拖拽,
  • 图层管理
  • 撤销和重做
  • 导入导出
  • 利用几何和代数学知识解决前端问题

demo演示

在分享方案之前, 我先给大家演示一下做好的demo, 这样可以更好的理解我们接下来要做的事情:

image.png

技术实现

我们继续沿用上一篇文章几何学在前端边界计算中的应用和原理分析的工程, 由于几何画板相当于一个独立的小应用, 具备一定的复杂度, 这里我们来对 vite 工程配置一下对 less 的支持:

  1. 安装 less 和less-loader (推荐yarn, pnpm)
  2. 在vite.config.ts里做如下配置:
export default defineConfig({
  plugins: [vue()],
  css: {
    preprocessorOptions: {
        less: {
            modifyVars: {
                hack: `true; @import (reference) "${path.resolve("src/base.less")}";`,
            },
            javascriptEnabled: true,
        },
    },
  },
})

这样配置完成之后我们就可以在 vite项目 里用 less 的方式写样式代码了, modifyVars属性里面的配置是为了指定 less 全局变量的地址, 这样我们可以把主题, 通用样式放在该目录下, 以便直接在项目的任何页面直接使用。

好了, 准备工作完成了, 我们开始接下来的实现部分。

1. 画板搭建

画板搭建主要是静态和交互部分, 这里简单和大家介绍一下基本构造:

image.png

上图可知画板主要分两个部分:

  • 画布区(包含记录鼠标移动坐标的文本提示)
  • 侧边控件区

画布的点阵背景我们用 css 的背景样式实现, 这块网上也有很多教程, 我就不一一和大家分析了,这里直接上实现的代码, 大家可以拿来就用:

section .card {
  position: relative;
  height: 480px;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
  background-image: radial-gradient(rgba(9, 89, 194, 0.3) 6%, transparent 0),
    radial-gradient(#faf9f8 6%, transparent 0);
  background-size: 10px 10px;
  background-position: 0 0, 2px 2px;
}

整个画板应用的基本结构如下:

image.png

如果大家对这一块知识感兴趣的可以参考我实现的代码, 具体代码地址: gitee.com/lowcode-chi…

接下来我们开始进行比较核心的方案设计。

2. 创建并绘制几何图形

因为是画版应用, 所以图形的创建一定要简单且灵活, 控制权交给用户和鼠标, 所以这里实现的效果如下:

image.png

用户只需要选择对应的图形, 用鼠标在画布里拖动即可创建任意大小比例的图形, 为了实现这一效果, 我们需要做如下准备:

  • 定义图形的schema结构
  • 根据鼠标光标的位置计算图形创建的元信息(图形id, 顶点坐标, 宽高样式等属性)

1.定义图形的schema结构

像任何可视化低代码产品一样, 我们都需要一个统一且可扩展的组件schema结构, 目的是为了更好的识别组件和派发属性。画板应用里的图形, 我们可以设计如下 schhema 结构:

type IShapeTypes = 'rect' | 'circle' | 'line';
interface IShapeProps {
    id: string,
    type: IShapeTypes,
    style: {
        width: number,
        height: number,
        left: number,
        top: number,
        ...otherStyle
    },
    isEditable: boolean,
    ...otherSchema
}

具体配置只要具备通用性, 我们就可以结合自己的业务来配置。

定义好了 schema, 我们只需要实现对应的图形即可, 这里以矩形为例和大家分享一下实现细节。

2. 根据鼠标光标的位置计算图形创建的元信息

我们都知道, 要想通过鼠标拖动来创建任意一个矩形, 我们需要知道几个条件:

  • 鼠标按下的初始点的坐标
  • 鼠标拖动过程中的实时位置

这两个问题其实都可以在全局实现, 基于组件设计的原子化原则, 我们可以在画布组件里捕获并计算出鼠标的实时位置, 然后派发给其他组件消费, 这样我们也可以是实现记录鼠标移动坐标的文本提示 这一功能了。

在上一篇文章中已经介绍了如何用 vue3 的组合式函数来实现通用 hooks, 我们接下来要做的就是把 useMouse 获取到的结果加工后让其他组件能使用, 这里我用 vue3toRefs 来实现。先来看一下代码:

// BaseBoard.tsx
<script setup lang="ts">
import { ref, onMounted, onUnmounted, toRefs } from "vue";
import { useMouse } from "../hooks/mouse";
import { getElPagePos } from "../utils/math";
const props = defineProps<{
  msg: string;
  onMouseChange: (x: number, y: number) => void;
}>();
const { onMouseChange } = toRefs(props);
const cardOffset = ref({ x: 0, y: 0 });
const boardDom = ref<any>(null);
const { x, y } = useMouse(window, (x, y) => {
  onMouseChange && onMouseChange.value(x - cardOffset.value.x, y - cardOffset.value.y);
});
onMounted(() => {
  const { x, y } = getElPagePos(boardDom.value);
  cardOffset.value.x = x;
  cardOffset.value.y = y;
});
onUnmounted(() => {});
defineExpose({ boardDom });
</script>
<template>
  <section>
    <h3>{{ msg }}</h3>
    <div class="card" ref="boardDom">
      <slot></slot>
      <div style="user-select: none">
        x: {{ x - cardOffset.x }} , y: {{ y - cardOffset.y }}
      </div>
    </div>
  </section>
</template>

BaseBoard 就是我们的画布组件, 我们使用这个组件可以在页面上创建任意数量的画布, 同时由于vue3 的组合函数支持使用defineProps 来定义组件的props, 所以我们可以通过它定义组件的属性, 这里对外暴露了两个属性:

  • msg 用来在外部控制画布的名称
  • onMouseChange 用来将内部鼠标监听的事件传到外部, 让外部可以拿到内部是事件运行时

我们使用 useMouse 的时候就可以实时拿到鼠标的x, y的绝对坐标, 再减去画布在页面的实际偏移cardOffset.x, cardOffset.y, 就可以得出鼠标在画布中正确的坐标:

image.png

这样我们就可以通过onMouseChange回调把鼠标相对画布的坐标实时传给父组件了:

const { x, y } = useMouse(window, (x, y) => { 
    onMouseChange && 
      onMouseChange.value(x - cardOffset.value.x, y - cardOffset.value.y); 
});

同时我们在代码中发现了 defineExpose, 这个 api 作用就是把需要暴露的数据导出,供父组件使用,相当于子传父, 我们可以在父组件里拿到暴露的值, 在这里我们把画布的 dom 暴露出来, 让父组件可以拿到子组件的dom

有了以上的前提, 我们就可以来创建矩形元素了, 为了更好的管理画布中的元素, 我们定义一个元素集合canvasBox:

type shapeType = "rect" | "circle" | "line";
interface IBaseShapeProp {
  type: shapeType;
  key: string;
  style: any;
}
const canvasBox = ref<{ [key in shapeType]: IBaseShapeProp[] }>({
  rect: [],
  circle: [],
  line: [],
});

当用户选择一个图形, 在画布中按下鼠标的那一刻, 我们创建一个基本的元数据:

const handleMouseDown = () => {
  const { x, y } = mouseAbsPos.value;
  if (curShape.value) {
    templateDot = [x, y];
    templateDot[2] = Date.now() + "";
    canvasBox.value["rect"].push({
      type: "rect",
      key: templateDot[2],
      style: {},
    });
  }
};

由上面的代码可知, 我们会创建一个矩形的元数据, 包含了矩形的:

  • 元素类型
  • 矩形的唯一key(方便后续快速查找该图形)
  • 矩形的初始化样式

同时我们在 templateDot 变量中缓存了鼠标的初始位置, 方便后续生成矩形完整的元数据。

image.png

我们在图中可以看出当拖动鼠标时矩形是实时跟随鼠标创建的, 要想实现这个效果, 我们需要对鼠标的mousemove 进行监听, 并动态更新矩形的元数据, 如下:

const handleMouseChange = (x: number, y: number) => {
  mouseAbsPos.value = { x, y };
  // 1.如果有选中的元素, 则判断为移动当前选中元素
  if (curSelect.value && templateDot.length) {
    // something...
    return;
  }
  // 2.否则则生成元素
  const [a1, b1, key] = templateDot;
  if (curShape.value && templateDot.length) {
    let dx = x - a1;
    let dy = y - b1;
    let curIndex = canvasBox.value["rect"].findIndex((v) => v.key === key);
    if (curIndex > -1) {
      canvasBox.value["rect"][curIndex] = {
        ...canvasBox.value["rect"][curIndex],
        style: {
          left: (dx > 0 ? a1 : x) + "px",
          top: (dy > 0 ? b1 : y) + "px",
          width: Math.abs(dx) + "px",
          height: Math.abs(dy) + "px",
        },
      };
    }
  }
};

由代码可知我是通过实时改变矩形元素的 lefttop 来实现矩形跟随鼠标实时更新的, 我们使用 transform 也可以实现同样的效果, 感兴趣的朋友可以尝试一下。

这里顺便扩展一下, 我们平时看到的拖拽框架, 对组件进行多选操作时也用了同样的方式, 通过鼠标拖拽滑动来产生多选区域:

image.png

感兴趣的朋友可以把这个方案进行扩展, 实现更有意思的应用场景。

3. 移动, 编辑几何图形

有了上面创建元素的基础, 我们继续来实现移动和编辑元素的功能。

3.1 移动元素

首先我们需要找到当前要移动的元素, 然后动态改变它的位置, 因为每个元素我都设置唯一的key, 所以当元素被选中的时候我们就可以根据key找到此元素, 并只对该元素进行操作:

// 如果有选中的元素, 则判断为移动当前选中元素
  if (curSelect.value && templateDot.length) {
    const [x0, y0] = templateDot;
    canvasBox.value["rect"] = canvasBox.value["rect"].map((v) => {
      if (v.key === curSelect.value) {
        const { left, top } = v.style;
        templateDot = [x, y];
        return {
          ...v,
          style: {
            ...v.style,
            left: parseFloat(left) + (x - x0) + "px",
            top: parseFloat(top) + (y - y0) + "px",
          },
        };
      }
      return v;
    });
    return;
  }

以上代码中主要是通过计算鼠标移动的位置差(通过缓存鼠标上一步的坐标)来改变元素的 lefttop 值, 在 mouseup 时重置缓存变量即可完成一次移动过程。

const handleMouseUp = () => {
  const { x, y } = mouseAbsPos.value;
  if (curShape.value) {
    // 1. 如果开始点和结束点一样,则不创建
    if (templateDot[0] === x && templateDot[1] === y) {
      canvasBox.value["rect"] = canvasBox.value["rect"].filter(
        (v) => v.key !== templateDot[2]
      );
      templateDot = [];
      return;
    }
  }
  // 重置
  templateDot = [];
};

这里有一个细节需要注意, 就是如果在鼠标按下之后没有拖动(也就是好点击画布的操作), 其实需要把mousedown创建的元素清空删除, 所以才有了上述代码的第一步判断。

3.2 编辑元素

编辑元素其实和移动元素的模式差不多, 改变的是元素的静态属性, 比如我们可以编辑元素的背景颜色, 边框样式等, 这里我以删除元素为例给大家介绍一下实现过程。

image.png

首先我们展示一下元素的 dom 结构:

<div
  v-for="item in canvasBox.rect"
  :key="item.key"
  :class="['shape', 'rect', curSelect === item.key ? 'active' : '']"
  :style="{
    left: item.style.left,
    top: item.style.top,
    width: item.style.width,
    height: item.style.height,
  }"
  :data-key="item.key"
  @dblclick.stop="handleSelected(item.key)"
>
  <span v-if="curSelect === item.key" @click="handleDel(item.key)">x</span>
</div>

当我们双击元素的时候, 我们通过key会给当前选中元素一个激活态, 此时v-if的删除按钮就会显示, 我们绑定一个删除方法 handleDel :

const handleDel = (key: string) => {
  canvasBox.value["rect"] = canvasBox.value["rect"].filter((v) => v.key !== key);
  curSelect.value = "";
  templateDot = [];
};

删除元素的方法是典型的单向操作, 比较简单, 如果我们要改变元素的整体属性, 我们需要设计一个属性面板,并实现表单渲染器来动态的更新元素的属性, 类似于 H5-Dooring 中的编辑面板:

image.png

在后面的文章中我会实现一个min版的属性编辑器来完善我们的几何画板。

4. 图层管理, 图片导出等方案介绍

图层管理也是编辑器常用的功能, 有了我们之前设计的 canvasBox, 我们就很容易实现一个图层管理面板了, 我们只需要把存储在canvasBox 元素数组遍历到图层面板, 并对其绑定操作方法即可实现涂图层管理的常用功能, 比如:

  • 显示隐藏
  • 快捷删除
  • 批量删除
  • 多选
  • 图层移动
  • 切换元素

等等功能, 如 H5-Dooring 中的图层管理面板:

image.png

5. 后期规划

在后面的文章中我会继续带大家一起实现

  • 撤销重做,
  • 图层管理面板,
  • 样式编辑,
  • 图片导出

等功能模块, 最终实现一个功能完备的画板应用, 大家感兴趣的话可以关注我的专栏 100+前端几何学应用案例 , 我会持续分享可视化图形学相关的前端应用。


目录
相关文章
|
6天前
|
资源调度 前端开发 JavaScript
vite3+vue3 实现前端部署加密混淆 javascript-obfuscator
【11月更文挑战第10天】本文介绍了在 Vite 3 + Vue 3 项目中使用 `javascript-obfuscator` 实现前端代码加密混淆的详细步骤,包括安装依赖、创建混淆脚本、修改 `package.json` 脚本命令、构建项目并执行混淆,以及在 HTML 文件中引用混淆后的文件。通过这些步骤,可以有效提高代码的安全性。
|
9天前
|
缓存 前端开发 搜索推荐
前端性能优化实战:提升网页加载速度
前端性能优化实战:提升网页加载速度
|
14天前
|
前端开发 JavaScript 安全
揭秘!前端大牛们如何高效解决跨域问题,提升开发效率!
【10月更文挑战第30天】在Web开发中,跨域问题是一大挑战。本文介绍前端大牛们常用的跨域解决方案,包括JSONP、CORS、postMessage和Nginx/Node.js代理,对比它们的优缺点,帮助初学者提升开发效率。
38 4
|
17天前
|
前端开发 数据管理 测试技术
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第27天】本文介绍了前端自动化测试中Jest和Cypress的实战应用与最佳实践。Jest适合React应用的单元测试和快照测试,Cypress则擅长端到端测试,模拟用户交互。通过结合使用这两种工具,可以有效提升代码质量和开发效率。最佳实践包括单元测试与集成测试结合、快照测试、并行执行、代码覆盖率分析、测试环境管理和测试数据管理。
35 2
|
18天前
|
前端开发 JavaScript 数据可视化
前端自动化测试:Jest与Cypress的实战应用与最佳实践
【10月更文挑战第26天】前端自动化测试在现代软件开发中至关重要,Jest和Cypress分别是单元测试和端到端测试的流行工具。本文通过解答一系列问题,介绍Jest与Cypress的实战应用与最佳实践,帮助开发者提高测试效率和代码质量。
28 2
|
22天前
|
前端开发 JavaScript 开发工具
Vite 4.0 发布,下一代的前端工具链
【10月更文挑战第21天】Vite 4.0 的发布标志着前端开发领域的又一次重要进步。它为开发者带来了更高效、更智能、更具创新性的开发体验,正逐渐成为下一代前端工具链的引领者。
|
9天前
|
前端开发 JavaScript 安全
vite3+vue3 实现前端部署加密混淆 javascript-obfuscator
【11月更文挑战第7天】本文介绍了在 Vite 3 + Vue 3 项目中使用 `javascript-obfuscator` 实现前端代码加密混淆的详细步骤。包括项目准备、安装 `javascript-obfuscator`、配置 Vite 构建以应用混淆,以及最终构建项目进行混淆。通过这些步骤,可以有效提升前端代码的安全性,防止被他人轻易分析和盗用。
|
1月前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
130 2
|
1月前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
38 0
|
1月前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。