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

探讨利用余弦定理和正切函数(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

相关推荐
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte8 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc