(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+前端几何学应用案例 , 我会持续分享可视化图形学相关的前端应用。


目录
相关文章
|
15天前
|
前端开发 JavaScript API
(前端3D模型开发)网页三维CAD中加载和保存STEP模型
本文介绍了如何使用`mxcad3d`库在网页上实现STEP格式三维模型的导入与导出。首先,通过官方教程搭建基本项目环境,了解核心对象如MxCAD3DObject、Mx3dDbDocument等的使用方法。接着,编写了加载和保存STEP模型的具体代码,包括HTML界面设计和TypeScript逻辑实现。最后,通过运行项目验证功能,展示了从模型加载到保存的全过程。此外,`mxcad3d`还支持多种其他格式的三维模型文件操作。
|
2天前
|
开发框架 前端开发 JavaScript
uniapp开发鸿蒙,是前端新出路吗?
相信不少前端从业者一听uniapp支持开发鸿蒙Next后非常振奋。猫林老师作为7年前端er也是非常激动,第一时间体验了下。在这里也给大家分享一下我的看法
32 17
|
8天前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
37 3
|
6天前
|
前端开发 搜索推荐 安全
陪玩系统架构设计陪玩系统前后端开发,陪玩前端设计是如何让人眼前一亮的?
陪玩系统的架构设计、前后端开发及前端设计是构建吸引用户、功能完善的平台关键。架构需考虑用户需求、技术选型、安全性等,确保稳定性和扩展性。前端可选用React、Vue或Uniapp,后端用Spring Boot或Django,数据库结合MySQL和MongoDB。功能涵盖用户管理、陪玩者管理、订单处理、智能匹配与通讯。安全性方面采用SSL加密和定期漏洞扫描。前端设计注重美观、易用及个性化推荐,提升用户体验和平台粘性。
32 0
|
23天前
|
存储 前端开发 JavaScript
前端状态管理:Vuex 核心概念与实战
Vuex 是 Vue.js 应用程序的状态管理模式和库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。本教程将深入讲解 Vuex 的核心概念,如 State、Getter、Mutation 和 Action,并通过实战案例帮助开发者掌握在项目中有效使用 Vuex 的技巧。
|
1月前
|
Web App开发 缓存 监控
前端性能优化实战:从代码到部署的全面策略
前端性能优化实战:从代码到部署的全面策略
31 1
|
1月前
|
Web App开发 前端开发 JavaScript
前端性能优化实战:从代码到部署的全面指南
前端性能优化实战:从代码到部署的全面指南
35 1
|
1月前
|
缓存 监控 前端开发
前端性能优化实战:从加载速度到用户体验
前端性能优化实战:从加载速度到用户体验
|
2月前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
189 2
|
2月前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
56 0