从余弦定理到正切函数:探索图形旋转的数学奥秘与实现方法

探讨利用余弦定理和正切函数(tan)的性质,将角度转化为坐标变换,从而使得图形能够根据鼠标位置的变化而实现旋转。

⬜ 画一个矩形及对应旋转图标

一点小样式,我这里使用了Unocss,图标偷懒了,都可以自行实现和替换,文章重点不是这个,小问题,不要介意哈~

scss 复制代码
<template>
  <div class="relative h-screen flex flex-col items-center justify-center">
    <div class="absolute h10 w10 border-1 border-black border-solid bg-blue text-center line-height-10">
      <div i-carbon-sun dark:i-carbon-moon absolute class="h2 w2 -m-3" />
    </div>
  </div>
</template>

新增属性style="transform:rotate(45deg)" 给一个45度旋转角度看下效果:

可以看到矩形成功旋转了45度角,那么接下来我们需要做的就是在拖动旋转图标的时候动态计算rotate的数值。

🛆 根据余弦定理 <math xmlns="http://www.w3.org/1998/Math/MathML"> c 2 = a 2 + b 2 − 2 a b c o s σ c^2=a^2+b^2-2abcos\sigma </math>c2=a2+b2−2abcosσ

余弦定理变式: <math xmlns="http://www.w3.org/1998/Math/MathML"> c o s σ = ( a 2 + b 2 − c 2 ) / 2 a b cos\sigma = (a^2 + b^2 -c^2) / 2ab </math>cosσ=(a2+b2−c2)/2ab

勾股定理公式: <math xmlns="http://www.w3.org/1998/Math/MathML"> a 2 + b 2 = c 2 a^2 + b^2 = c^2 </math>a2+b2=c2

计算一个角度,我第一个反应就是用余弦定理,观察以下图形,根据余弦定理我们需要找到三角形的三边长才能计算出角度,鼠标点击时的起点到终点可以得到三角形的两条直角边,根据勾股定理就可以得到第三边长。

我们需要构建一个平面直角坐标系,首先就得找到原点,观察可知,这个原点即为矩形的中心点,图形围绕这个中心点进行旋转。

⚪ 计算中心点(旋转轴心、坐标系原点)

计算矩形的中心点,这里使用到了 Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

getBoundingClientRect 返回值是一个 DOMRect 对象,是包含整个元素的最小矩形(包括 paddingborder-width)。该对象使用 lefttoprightbottomxywidthheight 这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 widthheight 以外的属性是相对于视图窗口的左上角来计算的。

  • MouseEvent.clientX:表示鼠标指针相对于浏览器窗口的横向坐标。坐标原点位于浏览器窗口的左上角,x 轴正方向向右延伸,单位是像素。
  • MouseEvent.clientY:表示鼠标指针相对于浏览器窗口的纵向坐标。坐标原点位于浏览器窗口的左上角,y 轴正方向向下延伸,单位是像素。

这两个属性通常在处理鼠标事件时被使用。例如,在拖拽、点击、绘图等场景下,可以使用这些属性来获取鼠标指针的位置。

arduino 复制代码
class Point {
  x = 0
  y = 0
}

const origin = new Point()

function rotateMousedown(e: MouseEvent) {
  const rect = rectRef.value?.getBoundingClientRect()

  if (!rect)
    return

  origin.x = rect.left + rect.width / 2
  origin.y = rect.top + rect.height / 2
}

↔ 计算两点间距离

当使用余弦定理时,我们需要知道三个边长或两边和一个夹角,以便计算第三边的长度。在这个情况下,我们可以使用鼠标位置与圆心之间的距离作为一个边,圆的半径作为第二个边,以及初始角度作为夹角。先封装一个计算两点间距离的函数:

javascript 复制代码
/**
 * 计算两点间距离
 * @param p1
 * @param p2
 */
function calculateDistance(p1: Point, p2: Point) {
  return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
}

💠 公式计算

鼠标按下记录初始位置,移动的时候记录移动后的位置,然后,我们可以使用余弦定理来计算新的鼠标位置与圆心之间的距离,从而确定旋转角度。

arduino 复制代码
function rotateMousedown(e: MouseEvent) {
  const rect = rectRef.value?.getBoundingClientRect()

  if (!rect)
    return

  origin.x = rect.left + rect.width / 2
  origin.y = rect.top + rect.height / 2

  initialDistance = calculateDistance(origin, { x: e.clientX, y: e.clientY })

  const mousemoveHandler = (e: MouseEvent) => {
    const newDistance = calculateDistance(origin, { x: e.clientX, y: e.clientY })

    // cosTheta = (a^2 + b^2 -c^2) / 2ab
    const cosTheta
      = (initialDistance ** 2 + rect.width ** 2 - newDistance ** 2)
      / (2 * initialDistance * rect.width)

    // 余弦值转为弧度再转为角度
    rotationAngle.value = Math.acos(cosTheta) * (180 / Math.PI)
  }

  const mouseupHandler = () => {
    document.removeEventListener('mousemove', mousemoveHandler)
    document.removeEventListener('mouseup', mouseupHandler)
  }

  document.addEventListener('mousemove', mousemoveHandler)
  document.addEventListener('mouseup', mouseupHandler)
}

测试

折腾半天,满怀激动的打开了页面:

晴天霹雳,一卡一卡的,完全不能够使用...

思绪良久,打开搜索引擎一顿操作,决定改换思路,使用正切函数(tan)来计算旋转角度。

🔻 根据正切函数计算

Math.atan2() 返回从原点 (0,0) 到 (x,y) 点的线段与 x 轴正方向之间的平面角度 (弧度值),也就是 Math.atan2(y,x)。

具体思路也是先计算中心点,然后根据Math.atan2来计算初始角度,鼠标移动过程中计算旋转角度的差值:

javascript 复制代码
/**
 * 计算鼠标位置与圆心的角度
 * @param p1
 * @param p2
 */
function calculateAngle(p1: Point, p2: Point) {
  return Math.atan2(p2.y - p1.y, p2.x - p1.x) * (180 / Math.PI)
}
  • 完整代码
xml 复制代码
<script setup lang="ts">
import { ref } from 'vue'

class Point {
  x = 0
  y = 0
}

const rectRef = ref<HTMLDivElement>()
const rotationAngle = ref(0)
const origin = new Point()
let initialAngle = 0

/**
 * 计算鼠标位置与圆心的角度
 * @param p1
 * @param p2
 */
function calculateAngle(p1: Point, p2: Point) {
  return Math.atan2(p2.y - p1.y, p2.x - p1.x) * (180 / Math.PI)
}

function rotateMousedown(e: MouseEvent) {
  const rect = rectRef.value?.getBoundingClientRect()

  if (!rect)
    return

  origin.x = rect.left + rect.width / 2
  origin.y = rect.top + rect.height / 2

  // 计算初始角度
  initialAngle = calculateAngle(origin, { x: e.clientX, y: e.clientY })

  const mousemoveHandler = (e: MouseEvent) => {
    // 计算鼠标位置与圆心的角度
    const currentAngle = calculateAngle(origin, { x: e.clientX, y: e.clientY })
    // 计算旋转角度的差值
    rotationAngle.value = currentAngle - initialAngle
  }

  const mouseupHandler = () => {
    document.removeEventListener('mousemove', mousemoveHandler)
    document.removeEventListener('mouseup', mouseupHandler)
  }

  document.addEventListener('mousemove', mousemoveHandler)
  document.addEventListener('mouseup', mouseupHandler)
}
</script>

<template>
  <div class="relative h-screen flex flex-col items-center justify-center">
    <div
      ref="rectRef"
      class="absolute h10 w10 border-1 border-indigo border-solid bg-blue text-center line-height-10"
      :style="{ transform: `rotate(${rotationAngle}deg)` }"
    >
      <div
        i-carbon-sun
        dark:i-carbon-moon
        absolute
        class="h2 w2 -m-3"
        @mousedown="rotateMousedown"
      />
    </div>
  </div>
</template>

测试

测试效果比使用余弦定理的方法好了很多,非常的丝滑。

最后

完整例子源码我放在github上了,作为记录学习使用,链接article-drag-rotation

相关推荐
SameX9 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_36 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito39 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°2 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184552 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室3 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang4 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发4 小时前
解锁微前端的优秀库
前端
王解5 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js