几何学在前端边界计算中的应用和原理分析(一)

简介: 几何学在前端边界计算中的应用和原理分析(一)

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

前言

之所以会开设这个专栏, 是为了弥补部分程序员对代数几何学的短板(当然也是为了巩固我的数学基础), 同时在实用价值上, 代数几何学在编程界也起到了非常重要的推动作用, 比如我们看到的各种建模软件, 仿真&设计软件, 内部都涉及了很多数学原理, 在Web界, 我们比较熟悉的可视化图表, 在线设计软件Figma, 各式各样的可视化低代码产品, 都或多或少的应用了几何学原理, 所以要先让自己做出高价值的产品, 让自己的编程水平更进一步, 代数几何学知识是非常有必要的。

《100+前端几何学应用案例》 专栏中, 我会和大家由浅入深地分享一些应用几何学知识实现的经典Web案例, 比如:

  • 游戏领域的边界问题(碰撞, 射击策略等)
  • 几何画板的实现方案
  • 常见的几种可视化图表实现方案
  • 骨骼动画实现原理
  • 从零封装一个图形库

等等, 每一个实战案例我都同步到 阿几里德编程实践 代码仓库中, 有兴趣的朋友可以参考学习。

接下来开始我们的第一篇分享——几何边界问题的编程实践

几个常见边界计算的例子和实现原理

image.png

这篇文章主要会介绍三种常见图形(矩形, 圆形, 三角形)的边界计算方案, 其中会应用一些几何学和代数知识, 相信大家会从中汲取到自己需要的知识, 并应用到自己的项目中。

在开始实现之前我们先做一些准备工作:

  1. 约定坐标体系(左上角为原点, x轴向右为正方向, y轴向下为正方向)
  2. 工程采用vite构建, 前端使用vue3作为开发语言(当然其他框架也是完全没问题的, 看个人喜好)

1. 计算鼠标指针是否在矩形内部

有了上面的坐标体系, 我们就来解决矩形边界问题。为了让大家更好的理解边界问题的价值, 我这里来举一个形象的例子:

image.png

比如说我们在玩射击游戏, 只有射中靶子才能得分, 如上图, 这里有涉及到靶的边界问题, 这里转换为矩形边界问题就是: 判断一个点 (x,y) 是否在矩形(ABCD)内部:

image.png

由上图我们很容易就可以想出一种方案, 即判断 x 是否在区间 [a0, a1] 之间, y 是否在 [b0, b1] 之间。

我们先来构造一个矩形, 这里为了保证矩形足够通用, 我写了一个生成矩形的函数:

const generateRectangleMeta = (startPos: [number, number], w: number, h: number) => {
  if (
    startPos &&
    Array.isArray(startPos) &&
    startPos.length >= 2 &&
    typeof w === "number" &&
    typeof h === "number"
  ) {
    const [a0, b0] = startPos;
    const a1 = a0 + w;
    const b1 = b0 + h;
    return {
      startPos,
      w,
      h,
      endPos: [a1, b1],
      pos: [
        [a0, b0],
        [a1, b0],
        [a1, b1],
        [a0, b1],
      ],
    };
  } else {
    throw new Error("Please pass in the correct parameters");
  }
};

由上面的代码可知, 我们只需要传入矩形的左顶点坐标宽高, 即可生成一个矩形元数据集合, 包含了:

  • 左顶点坐标
  • 矩形的宽高数据
  • 右底点坐标
  • 矩形四个顶点的坐标集合

有了以上数据之后, 我们就可以画出一个任意位置的矩形。

下一步就是获取任意点的坐标, 为了方便演示, 这里以鼠标指针作为点(x, y), 我们再来构造一个画布:

image.png

我们以画布的左上角作为坐标原点(0,0), 来计算一下鼠标在画布中的相对位置, 这里我使用vue3hooks 来实现, 具体代码如下:

const cardOffset = ref({ x: 0, y: 0 });
onMounted(() => {
    // 获取画布左上角距离页面左上角的距离
  const { offsetTop, offsetLeft } = document.querySelector("#boundCard") as HTMLElement;
  cardOffset.value.x = offsetLeft;
  cardOffset.value.y = offsetTop;
});
const { x, y } = useMouse(window, (x, y) => {
  // 边界计算
  const { x: x0, y: y0 } = cardOffset.value;
  const dotX = x - x0;
  const dotY = y - y0;
  const { startPos, endPos } = rectangle;
  if (
    startPos[0] <= dotX &&
    endPos[0] >= dotX &&
    startPos[1] <= dotY &&
    endPos[1] >= dotY
  ) {
      // 在矩形内
    console.log("inset");
  } else {
      // 在矩形外
    console.log("out");
  }
});

上述代码中 useMouse 是一个组合函数, 主要用来获取鼠标的位置, 实现如下:

// event.ts
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(
    target: HTMLElement | Window, 
    event: keyof HTMLElementEventMap, 
    callback: (this: HTMLElement, ev: any) => any) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}
// mouse.ts
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse(target: HTMLElement | Window, callback?: (x: number, y:number) => void) {
  const x = ref(0)
  const y = ref(0)
  useEventListener(target, 'mousemove', (event) => {
    const { pageX, pageY } = event;
    x.value = pageX
    y.value = pageY
    callback && callback(pageX, pageY)
  })
  return { x, y }
}

之所以要把获取鼠标坐标的方法单独提出来, 是为了更好的复用, 也符合 hooks 倡导的理念, 对vue3 hooks感兴趣的朋友也可以研究一下。

经过上述的步骤, 我们就实现了判断矩形边界的功能, 完整效果演示如下:

是不是有种实现了 csshover 的感觉呢? 通过以上方式, 我们可以轻松判断在画布中的任意点, 是否在矩形内部, 从而实现有意思的射击游戏。

当然我们探索的本质问题其实是: 判断一个点是否在指定形状的内部。我们有很多方式实现, 比如向量法, 面积法等, 对于简单的矩形, 圆形, 我们可以用精简的方式来判断, 对于比较复杂的如三角形, 我会在后面的内容中和大家详细分享实现方案。

2. 计算鼠标指针是否在圆内部

上面分享了判断一个点是否在矩形中的实现方案, 接下来我们继续探索圆形的边界问题。

image.png

我们都知道只要确定了圆的半径(R)圆心坐标(x0, y0), 就能在坐标系里画出一个圆。这里我们先写一个生成圆的函数:

const generateCircleMeta = (pos: [number, number], r: number = 1) => {
  if(pos && Array.isArray(pos) && pos.length) {
    const [x, y] = pos;
    return {
      startPos: [x - r, y - r],
      pos,
      r
    }
  }else {
    throw new Error("Please pass in the correct parameters");
  }
}

之所以要写这个函数是因为我们的目标对象是 dom, 这样方便确定 dom 的位置。(当然我们也可以用其他方式定义一个圆, 这里的方案只做参考)

同时由于圆的特殊性, 我们要判断一个点是否在圆内, 只需要判断这个点和圆心的直线距离是否大于半径(r)即可。

�1=���ℎ.����((�1−�0)2+(�1−�0)2)r1=Math.sqrt((x1x0)2+(y1y0)2)

image.png

我们用 javascript 来实现一下:

const isOutCircle = ref(false);
// 生成圆形数据元
const circle = generateCircleMeta([200, 400], 100);
useMouse(window, (x, y) => {
  // 边界计算
  const { x: x0, y: y0 } = cardOffset.value;
  const dotX = x - x0;
  const dotY = y - y0;
  const { pos, r } = circle;
  const r1 = Math.sqrt(Math.pow(dotX - pos[0], 2) + Math.pow(dotY - pos[1], 2));
  if (r >= r1) {
    console.log("inset");
    isOutCircle.value = false;
  } else {
    console.log("out");
    isOutCircle.value = true;
  }
});

由代码可知我们需要把几何中的坐标计算法转换为 javascript 支持的语法模式, 逻辑也很简单, 这里就不展开讨论了。

通过以上的实现, 我们就可以轻松计算任意矩形和圆形的边界问题了, 这也是我们工作中比较常见的计算场景, 接下来我们再来看一下如何计算三角形的边界。

3. 计算鼠标指针是否在三角形内部

image.png

要想解决这个问题, 我们需要先解决如何使用 HTMLDiv 画一个三角形。这里之所以不用 svg 或者 canvas 来画(虽然这两种方式画三角形会更简单), 主要是为了让大家更充分的感受几何学的魅力。也许有朋友会说了, 画个三角形不很方便吗? 用 css 或者 css背景渐变 都可以画出来, 但是通过上面的方式很难对三角形边界进行计算了, 我们需要知道三角形的三个顶点坐标, 所以这里我讲给大家介绍一种三角函数的方式, 来画任意的三角形。

3.1 从画一个线段开始

image.png

我们先来考虑一个简单的问题: 已知两个点的坐标 A(x0, y0)B(x1, y1), 如何用 dom 画一个线段AB

从图中我们可以分析出, 我们只要知道起点A, 线段AB的长度以及线段的角度a , 就能画出一个线段。 同时利用三角函数, 我们有以下的计算公式:

�=����=(�1−�0)/(�1−�0)A=tana=(y1y0)/(x1x0)

�=�������a=arctanA

我们可以通过坐标来计算出角度和线段的长度, 对于web中的角度我们需要做一个基本的换算:

1���=180/���ℎ.��1deg=180/Math.PI

有了以上的知识铺垫, 我们来实现一下生成线段数据元的函数:

const generateLineMeta = (pos: [[number, number], [number, number]]) => {
  if (pos && Array.isArray(pos) && pos.length >= 2) {
    const [A, B] = pos;
    const dx = B[0] - A[0];
    const dy = B[1] - A[1];
    const l = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
    const tanAB = dy / dx;
    const AB = (Math.atan(tanAB) * 180) / Math.PI;
    return {
      startPos: A,
      endPos: B,
      angle: AB,
      l,
    };
  } else {
    throw new Error("Please pass in the correct parameters");
  }
};

上面代码中之所以用 tan 来计算, 是为了简化我们的计算公式, 当然大家也可以尝试用反正玄函数来计算。 将生成的元数据应用到我们的 dom 上即可得到我们想要的线段:

image.png

线段实现了, 我们要想画三角形是不是就很方便了呢? 我们只需要用根据三角形的3个顶点坐标画出首尾相连的3条线段即可。

3.2 用 HTMLDiv 画一个三角形

image.png 我们只需要对上面的生成线段的函数稍加改造, 就可以实现生成三角形数据元的函数。(更确切的说是生成任意边的数据元的函数)

const generateLinesMeta = (pos: [number, number][], isLine: boolean = false) => {
  if (pos && Array.isArray(pos) && pos.length >= 2) {
    let i = 0,
      len = pos.length,
      result = [];
    while (i < len) {
      // 如果是线段, 则不添加闭合点
      if (isLine && !pos[i + 1]) {
        break;
      }
      let A = pos[i];
      let B = pos[i + 1 > len - 1 ? 0 : i + 1];
      if (A[0] > B[0]) {
        [B, A] = [A, B];
      }
      const dx = B[0] - A[0];
      const dy = B[1] - A[1];
      const l = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
      const tanAB = dy / dx;
      const AB = (Math.atan(tanAB) * 180) / Math.PI;
      result.push({
        startPos: A,
        endPos: B,
        angle: AB,
        l,
      });
      i++;
    }
    return result;
  } else {
    throw new Error("Please pass in the correct parameters");
  }
};

上述笔者实现的代码中, 添加了 isLine 参数, 这个参数可以控制我们绘制的图形是闭合多四边形还是折线集合

经过上面的实现, 我们终于用 HTML 画出了三角形, 接下来就是我们最后的冲刺了—— 判断空间内的点是否在三角形内部

image.png

在上面两个图形的边界计算中我们用特殊方法来计算出了任意一个点是否在其内部, 但是对于三角形, 以上方法可能都不适用了, 那我们怎么来实现它呢?

想想我们初中高中学的向量几何原理, 会不会有一些启发呢?

image.png

由上图可知, 我们是不是可以通过任意一点与三角形(S为该三角形的面积)三个顶点组成的三角形的面积(S1, S2, S3)来判断这个点是否在其内部呢? 如果点在三角形内部, 则会满足如下条件:

�=�1+�2+�3S=S1+S2+S3

如果点在三角形S外部, 则满足如下条件:

�<�1+�2+�3S<S1+S2+S3

所以说现在的问题就变成了求三角形面积的问题了。因为三角形的三个顶点坐标 (x1, y1), (x2, y2), (x3, y3) 是已知的,任意点的坐标 (x0, y0) 也是已知的, 我们可以根据向量叉积的计算方式来求出三角形的面积。

image.png

��∗��=∣∣��∣∣∣∣��∣∣���(�)�ABAC=∣∣AB∣∣∣∣AC∣∣sin(θ)n

向量叉积得到的是一个垂直于向量AB, AC所在平面的向量, n代表和平面垂直的法向量。

我们可以直接用高中课本的结论来算三角形的面积, 如下:

�=���ℎ.���(�1∗�2+�2∗�3+�3∗�1−�2∗�1−�3∗�2−�1∗�3)/2S=Math.abs(x1y2+x2y3+x3y1x2y1x3y2x1y3)/2

也可以用上面的三角函数来推导出上述的公式, 这里我就不一一和大家介绍了, 有了上面的结论, 我们就很容易算出一个点是否在三角形内部了。

文章的 demo 代码大家想参考也可以在如下地址学习参考: 趣谈前端仓库

同时如果大家有更好的方案, 也欢迎在仓库里反馈交流。

总结

几何学博大精深, 我们市面上看到的很多设计软件, 都应用了大量的几何学原理和算法, 本篇意在为大家展示其在 web 中的一个应用, 我们可以把上述的算法应用到实际工作中, 实现非常有意思的web 应用。同时我也会分享前端可视化低代码的最佳实践, 感兴趣的小伙伴也可以参考我的专栏 低代码可视化专栏.


目录
相关文章
|
2天前
|
XML 前端开发 JavaScript
前端技术的演变与实战应用
前端技术的演变与实战应用
|
5天前
|
前端开发 测试技术 开发工具
探索前端框架React Hooks的优势与应用
本文将深入探讨前端框架React Hooks的优势与应用。通过分析React Hooks的特性以及实际应用案例,帮助读者更好地理解和运用这一现代化的前端开发工具。
|
19天前
|
前端开发 JavaScript 关系型数据库
从前端到后端:构建现代化Web应用的技术探索
在当今互联网时代,Web应用的开发已成为了各行各业不可或缺的一部分。从前端到后端,这篇文章将带你深入探索如何构建现代化的Web应用。我们将介绍多种技术,包括前端开发、后端开发以及各种编程语言(如Java、Python、C、PHP、Go)和数据库,帮助你了解如何利用这些技术构建出高效、安全和可扩展的Web应用。
|
20天前
|
Web App开发 前端开发 JavaScript
前端应用实现 image lazy loading 的原理介绍
前端应用实现 image lazy loading 的原理介绍
29 0
|
24天前
|
前端开发 编解码 数据格式
浅谈响应式编程在企业级前端应用 UI 开发中的实践
浅谈响应式编程在企业级前端应用 UI 开发中的实践
20 0
浅谈响应式编程在企业级前端应用 UI 开发中的实践
|
5天前
|
缓存 安全 JavaScript
前端安全:Vue应用中防范XSS和CSRF攻击
【4月更文挑战第23天】本文探讨了在Vue应用中防范XSS和CSRF攻击的重要性。XSS攻击通过注入恶意脚本威胁用户数据,而CSRF则利用用户身份发起非授权请求。防范措施包括:对输入内容转义、使用CSP、选择安全的库;采用Anti-CSRF令牌、同源策略和POST请求对抗CSRF;并实施代码审查、更新依赖及教育团队成员。通过这些实践,可提升Vue应用的安全性,抵御潜在攻击。
|
2天前
|
前端开发 JavaScript Go
构建高性能Web应用:优化前端资源加载
在构建现代Web应用时,优化前端资源加载是至关重要的一步。本文将介绍一些提升Web应用性能的关键策略,包括减少HTTP请求、压缩和合并资源、使用CDN加速、以及异步加载技术等。通过实施这些优化策略,开发人员可以显著提升网站的加载速度和用户体验。
|
2天前
|
前端开发 JavaScript Java
前端与后端:构建现代Web应用的双翼
前端与后端:构建现代Web应用的双翼
|
29天前
|
编解码 前端开发 JavaScript
探索前端开发中的新趋势:WebAssembly 技术应用与展望
本文将深入探讨前端开发中的新趋势——WebAssembly 技术,介绍其在前端领域的应用场景和优势,并展望未来在前端开发中的潜在影响。通过对 WebAssembly 技术的原理解析和实际案例分析,帮助读者更好地了解并应用这一新兴技术。
|
8月前
|
Web App开发 前端开发 JavaScript
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-fiber解决了什么问题
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-fiber解决了什么问题
95 0