简介
看到文章题目,很多同学可能会纳闷,实现元素的旋转,只须要求得旋转角度,而后用CSS
中的transform:rotate(${旋转的角度}deg)
就能够实现旋转的需要,为什么要用到线性代数
的常识?
我感觉用线性代数
的常识实现元素拖拽旋转的理由如下:
- 矩阵中能够同时蕴含旋转、缩放、平移等信息,不须要进行冗余的计算和属性更新;
- 更加通用。
线性代数
的常识作为一种数学知识,是形象的、通用的,很多GUI
编程技术都提供了线性代数矩阵
实现元素旋转、缩放、平移等成果,例如CSS
中transform
属性的matrix()
,Canvas
中提供的setTransform()
等API
,安卓Canvas
类提供的setMatrix()
办法。学会线性代数矩阵旋转
,就能够在各个GUI
编程技术中通吃此类需要。
拖拽旋转的原理剖析
拖拽旋转实质上是绕着原点旋转,这个原点就是物体的核心。让咱们用一个矩形来形象表白这个旋转过程,以矩形核心为原点\(O\),建设\(2D\)坐标系,取一点为旋转起始点\(A\),取一点为旋转完结点\(A'\),将\(A\)、\(A'\)与\(O\)连接起来可得向量\(\overrightarrow{OA}\)、向量\(\overrightarrow{OA'}\),向量\(\overrightarrow{OA}\)和向量\(\overrightarrow{OA'}\)之间的夹角\(\theta\),可得如下图:
在JavaScript中Math.atan2()
API能够返回从\(原点(0,0)\)到\((x,y)点\)的线段与\(x轴\)正方向之间的立体角度(弧度值),所以可得求取两个向量之间的夹角弧度的代码如下:
/** * 计算向量夹角,单位是弧度 * @param {Array.<2>} av * @param {Array.<2>} bv * @returns {number} */ function computedIncludedAngle(av, bv) { return Math.atan2(av[1], av[0]) - Math.atan2(bv[1], bv[0]); }
旋转矩阵
在前文线性代数在前端中的利用(一):实现鼠标滚轮缩放元素、Canvas图片和拖拽中,咱们晓得了缩放元素能够利用缩放矩阵
,那么旋转元素也能够利用旋转矩阵
,那么怎么推导出旋转矩阵
就成了要害。因为咱们目前只关怀立体维度上的旋转,所以只须要求得\(2D\)维度中的旋转矩阵
即可。
假如在\(2D\)坐标轴中有和\(X轴\)、\(Y轴\)别离平行的基向量\(p\)和基向量\(q\),它们之间的夹角为\(90^{\circ}\),将基向量\(p\)和基向量\(q\)同时旋转\(\theta度\),能够失去基向量\(p'\)和基向量\(q'\),依据\(三角函数\)即能够推导出\(p\)、\(p'\)的值。
利用基向量结构矩阵,\(2D\)旋转矩阵就如下:
$$R(\theta)=\left[ \begin{matrix} p^{'} \\ q^{'} \\ \end{matrix} \right]=\left[ \begin{matrix} cos\theta & sin\theta \\ -sin\theta & cos\theta \end{matrix} \right]$$
转化为\(4\times4齐次矩阵\)则为:
$$R(\theta)=\left[ \begin{matrix} p^{'} \\ q^{'} \\ r^{'}\\ w^{'} \\ \end{matrix} \right]=\left[ \begin{matrix} cos\theta & sin\theta & 0 & 0 \\ -sin\theta & cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right]$$
CSS中实现矩阵变动的matrix()
函数
CSS函数matrix()
指定了一个由指定的 6 个值组成的 2D 变换矩阵。matrix(a, b, c, d, tx, ty)
是matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1)
的简写。
这些值示意以下函数:
matrix( scaleX(), skewY(), skewX(), scaleY(), translateX(), translateY() )
例如咱们要一个div元素放大两倍,程度向右平移100px,垂直向下平移200px,能够把CSS
写成:
div { transform:matrix(2, 0, 0, 2, 100, 200);}
因为咱们采纳的是\(4\times4齐次矩阵\)进行矩阵变换计算,所以采纳\(RP^{3}下的齐次坐标\)。值得注意的是,对于\(齐次坐标\)咱们还能够写成上面这种模式,本文咱们将采纳这种模式:
$$\left[ \begin{matrix} a & c & 0 & 0 \\ b & d & 0 & 0 \\ 0 & 0 & 1 & 0 \\ tx & ty & 0 & 1 \end{matrix} \right]$$
矩阵计算库gl-matrix
gl-matrix是一个用JavaScript
语言编写的开源矩阵计算库。咱们能够利用这个库提供的矩阵之间的运算性能,来简化、减速咱们的开发。为了防止升高复杂度,后文采纳原生ES6
的语法,采纳<script>
标签间接援用JS
库,不引入任何前端编译工具链。
鼠标拖拽旋转Div元素
旋转成果
代码实现
index.html
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>矩阵旋转Div元素</title> <link rel="stylesheet" href="./index.css"></head><body> <div class="shape_controls"> <div class="shape_anchor"></div> <div class="shape_rotater"></div> </div> <script src="./gl-matrix-min.js"></script> <script src="./index.js"></script></body></html>
index.css
*,*::before,*::after { box-sizing: border-box;}body { position: relative; margin: 0; padding: 0; min-height: 100vh;}.shape_controls { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; border: 1px solid rgb(0, 0, 0); z-index: 1;}.shape_controls .shape_anchor { position: absolute; left: 50%; top: 0%; transform: translate(-50%, -50%); width: 8px; height: 8px; border: 1px solid rgb(6, 123, 239); border-radius: 50%; background-color: rgb(255, 255, 255); z-index: 2;}.shape_controls .shape_rotater { position: absolute; left: 50%; top: -30px; transform: translate(-50%, 0); width: 8px; height: 8px; border: 1px solid rgb(6, 123, 239); border-radius: 50%; background-color: rgb(255, 255, 255); z-index: 2;}.shape_controls .shape_rotater:hover { cursor: url(./rotate.gif) 16 16, auto;}.shape_controls .shape_rotater::after { position: absolute; content: ""; left: 50%; top: calc(100% + 1px); transform: translate(-50%, 0); height: 18px; width: 1px; background-color: rgb(6, 123, 239);}
rotate.gif
index.js
document.addEventListener("DOMContentLoaded", () => { const $sct = document.querySelector(".shape_controls"); const $srt = document.querySelector(".shape_controls .shape_rotater"); const {left, top, width, height} = $sct.getBoundingClientRect(); // 原点坐标 const origin = [left + width / 2 , top + height / 2]; // 是否旋转中 let rotating = false; // 旋转矩阵 let prevRotateMatrix = getElementTranformMatrix($sct); let aVector = null; let bVector = null; /** * 获取元素的变换矩阵 * @param {HTMLElement} el 元素对象 * @returns {Array.<16>} */ function getElementTranformMatrix(el) { const matrix = getComputedStyle(el) .transform .replace("matrix(", "") .replace(")", "") .split(",") .map(item => parseFloat(item.trim())); return new Float32Array([ matrix[0], matrix[2], 0, 0, matrix[1], matrix[3], 0, 0, 0, 0, 1, 0, matrix[4], matrix[5], 0, 1 ]); } /** * 给元素设置变换矩阵 * @param {HTMLElement} el 元素对象 * @param {Array.<16>} hcm 齐次坐标4x4矩阵 */ function setElementTranformMatrix(el, hcm) { el.setAttribute("style", `transform: matrix(${hcm[0]} ,${hcm[4]}, ${hcm[1]}, ${hcm[5]}, ${hcm[12]}, ${hcm[13]});`); } /** * 计算向量夹角,单位是弧度 * @param {Array.<2>} av * @param {Array.<2>} bv * @returns {number} */ function computedIncludedAngle(av, bv) { return Math.atan2(av[1], av[0]) - Math.atan2(bv[1], bv[0]); } // 监听元素的点击事件,如果点击了旋转圆圈,开始设置起始旋转向量 $srt.addEventListener("mousedown", (e) => { const {clientX, clientY} = e; rotating = true; aVector = [clientX - origin[0], clientY - origin[1]]; }); // 监听页面鼠标挪动事件,如果处于旋转状态中,就计算出旋转矩阵,从新渲染 document.addEventListener("mousemove", (e) => { // 如果不处于旋转状态,间接返回,防止不必要的无意义渲染 if (!rotating) { return; } // 计算出以后坐标点与原点之间的向量 const {clientX, clientY} = e; bVector = [clientX - origin[0], clientY - origin[1]]; // 依据2个向量计算出旋转的弧度 const angle = computedIncludedAngle(aVector, bVector); const o = new Float32Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]); // 旋转矩阵 const rotateMatrix = new Float32Array([ Math.cos(angle), Math.sin(angle), 0, 0, -Math.sin(angle), Math.cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); // 把以后渲染矩阵依据旋转矩阵,进行矩阵变换,失去新矩阵 prevRotateMatrix = glMatrix.mat4.multiply(o, prevRotateMatrix, rotateMatrix); // 给元素设置变换矩阵,实现旋转 setElementTranformMatrix($sct, prevRotateMatrix); aVector = bVector; }); // 鼠标弹起后,移除旋转状态 document.addEventListener("mouseup", () => { rotating = false; }) });
鼠标拖拽旋转Canvas图形
旋转成果
代码实现
index.html
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>矩阵旋转Canvas图形</title> <link rel="stylesheet" href="./index.css"></head><body> <canvas id="app"></canvas> <script src="./gl-matrix-min.js"></script> <script src="./index.js"></script></body></html>
index.css
*,*::before,*::after { box-sizing: border-box;}body { margin: 0; padding: 0; overflow: hidden;}canvas { display: block;}.rotating,.rotating div { cursor: url(./rotate.gif) 16 16, auto !important;}
index.js
document.addEventListener("DOMContentLoaded", () => { const pageWidth = document.documentElement.clientWidth; const pageHeight = document.documentElement.clientHeight; const $app = document.querySelector("#app"); const ctx = $app.getContext("2d"); $app.width = pageWidth; $app.height = pageHeight; const width = 200; const height = 200; const cx = pageWidth / 2; const cy = pageHeight / 2; const x = cx - width / 2; const y = cy - height / 2; // 原点坐标 const origin = [x + width / 2 , y + height / 2]; // 是否旋转中 let rotating = false; let aVector = null; let bVector = null; // 以后矩阵 let currentMatrix = new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, origin[0], origin[1], 0, 1 ]); /** * 计算向量夹角,单位是弧度 * @param {Array.<2>} av * @param {Array.<2>} bv * @returns {number} */ function computedIncludedAngle(av, bv) { return Math.atan2(av[1], av[0]) - Math.atan2(bv[1], bv[0]); } /** * 渲染视图 * @param {MouseEvent} e 鼠标对象 */ function render(e) { // 清空画布内容 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.save(); // 设置线段厚度,避免在高分屏下线段发虚的问题 ctx.lineWidth = window.devicePixelRatio; // 设置变换矩阵 ctx.setTransform(currentMatrix[0], currentMatrix[4], currentMatrix[1], currentMatrix[5], currentMatrix[12], currentMatrix[13]); // 绘制矩形 ctx.strokeRect(-100, -100, 200, 200); // 设置圆圈的边框色彩和填充色 ctx.fillStyle = "rgb(255, 255, 255)"; ctx.strokeStyle = "rgb(6, 123, 239)"; // 绘制矩形上边框两头的蓝色圆圈 ctx.beginPath(); ctx.arc(0, -100, 4, 0 , 2 * Math.PI); ctx.stroke(); ctx.fill(); // 绘制能够拖拽旋转的蓝色圆圈 ctx.beginPath(); ctx.arc(0, -130, 4, 0 , 2 * Math.PI); ctx.stroke(); ctx.fill(); // 判断是否拖拽旋转的蓝色圆圈 const {pageX, pageY} = e ? e : {pageX: -99999, pageY: -9999}; if (ctx.isPointInPath(pageX, pageY)) { rotating = true; } // 绘制链接两个圆圈的直线 ctx.beginPath(); ctx.fillStyle = "transparent"; ctx.strokeStyle = "#000000"; ctx.moveTo(0, -125); ctx.lineTo(0, -105); ctx.stroke(); ctx.restore(); } // 首次渲染 render(); // 监听画布的点击事件,如果点击了旋转圆圈,开始设置起始旋转向量 $app.addEventListener("mousedown", (e) => { // 在渲染的过程中会判断是否点击了旋转圆圈,如果是,那么rotating会被设置为true render(e); if (!rotating) { return; } const { offsetX, offsetY } = e; aVector = [offsetX - origin[0], offsetY - origin[1]]; }); // 监听页面鼠标挪动事件,如果处于旋转状态中,就计算出旋转矩阵,从新渲染 document.addEventListener("mousemove", (e) => { // 如果不处于旋转状态,间接返回,防止不必要的无意义渲染 if (!rotating) { return; } // 给画布增加旋转款式 $app.classList.add("rotating"); // 计算出以后坐标点与原点之间的向量 const { offsetX, offsetY } = e; bVector = [offsetX - origin[0], offsetY - origin[1]]; // 依据2个向量计算出旋转的弧度 const angle = computedIncludedAngle(aVector, bVector); // 旋转矩阵 const rotateMatrix = new Float32Array([ Math.cos(angle), Math.sin(angle), 0, 0, -Math.sin(angle), Math.cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); // 把以后渲染矩阵依据旋转矩阵,进行矩阵变换,失去画布的新渲染矩阵 currentMatrix = glMatrix.mat4.multiply( glMatrix.mat4.create(), currentMatrix, rotateMatrix, ); render(e); aVector = bVector; }); // 鼠标弹起后,移除旋转状态 document.addEventListener("mouseup", () => { rotating = false; $app.classList.remove("rotating"); });});