最近做了一个挪动端流动页的需要,大略就是 diy 一个页面。用户能够对图片进行拖动、缩放、旋转,来达到 diy 的目标。
我采纳了 translate、scale、rotate 来实现和用户的交互。开发过程中,波及到了对元素的拖动、缩放、旋转等。

本文将具体介绍在不应用任何第三方库的状况下,如何实现这些性能。最终的成果 demo,能够参考下面的 gif 图。

扫描二维码体验

需要剖析

整个需要的大抵流程是:

用户点击按钮上传图片,图片内容审核(举荐 好将来 AILab ),图片显示在页面上。
用户在对元素进行拖动、缩放、旋转等操作。
用户能够生成 操作之后的 图片。

实现拖动、缩放、旋转等交互,最外围的两个点就是

1.用户触摸图片时,单指、双指操作记录 2.用户挪动图片时,依据手指的门路,管制元素的静止

拖拽、缩放、旋转对应的 JS 事件

手指按下会触发 onTouchStart 事件 onTouchStart={(e) => touchstartCallback(e)}
手指挪动会触发 onTouchMove 事件 onTouchMove={(e) => touchmoveCallback(e)}
手指松开会触发 onTouchEnd 事件 onTouchEnd={(e) => touchendCallback(e)}

拖拽、缩放、旋转对应的变量 和 Dom

    const [stv, setStv] = useState({    offsetX: 0, //图片坐标 x    offsetY: 0, //图片坐标 y    zoom: false, //是否缩放状态    distance: 0, //两指间隔    scale: 1, //缩放倍数    rotate: 0, //旋转角度,    offsetLeftX: 0,    offsetLeftY: 0,    });    const [originImg, setOriginImg] = useState({    url: "",    width: 100,    height: 300,    });
<div  className="img"  onTouchStart={(e) => touchstartCallback(e)}  onTouchMove={(e) => touchmoveCallback(e)}  onTouchEnd={(e) => touchendCallback(e)}>    <img      style={{        transform: `translate(${stv.offsetX}px,${stv.offsetY}px) scale(${stv.scale}) rotate(${stv.rotate}deg)`,        width: `${originImg.width}px`,        height: `${originImg.height}px`,        position: "absolute",      }}      src={originImg.url}    ></img></div>

拖拽

拖拽其实是通过获取挪动的间隔来实现的,
即计算挪动前的地位的坐标(x,y)与挪动中的地位的坐标(x,y)差值
当手指按下或挪动时,都能够获取到以后手指的地位,即挪动前的地位与挪动中的地位

 let startX: number, startY: number; /**触摸*/ const touchstartCallback = (e: React.TouchEvent<HTMLDivElement>) => {  if (e.touches.length === 1) {    const { clientX, clientY } = e.touches[0];    startX = clientX;    startY = clientY;  }};/**触摸挪动中*/const touchmoveCallback = (e: React.TouchEvent<HTMLDivElement>) => {  /** 单指挪动*/  if (e.touches.length === 1) {    /** 缩放状态,不解决单指*/    if (stv.zoom) {      return;    }    const { clientX, clientY } = e.touches[0];    const offsetX = clientX - startX;    const offsetY = clientY - startY;    startX = clientX;    startY = clientY;    const stv2 = { ...stv } as {      offsetX: number;      offsetY: number;      zoom: boolean;      distance: number;      scale: number;      rotate: number;      offsetLeftX: number;      offsetLeftY: number;    };    stv2.offsetX += offsetX;    stv2.offsetY += offsetY;    stv2.offsetLeftX = -stv2.offsetX;    stv2.offsetLeftY = -stv2.offsetLeftY;    setStv({ ...stv2 });  }};/** 手指松开 触摸完结*/const touchendCallback = (e: React.TouchEvent<HTMLDivElement>) => {  if (e.touches.length === 0) {    const obj = {} as {      zoom: boolean;    };     /** 重置缩放状态 缩放状态用来管制缩放 缩放代码解说中有实现*/    obj["zoom"] = false;    setStv({      ...stv,      ...obj,    });  }};

缩放

以触摸的两点的连线的中心点为变换中心点,做缩放变换
以后两根手指之间的间隔除去上一次两根手指的间隔就是这一次的缩放量
同样也是在 start 的时候记录两指之间的间隔,间隔用勾股定理就能够算进去,在 move 的时候计算出以后两指的间隔,以后间隔减去上次记录的间隔

let twoPoint = {  x1: 0,  y1: 0,  x2: 0,  y2: 0,}; const touchstartCallback = (e: React.TouchEvent<HTMLDivElement>) => {  if (e.touches.length !== 1) {    const xMove = e.touches[1].clientX - e.touches[0].clientX;    const yMove = e.touches[1].clientY - e.touches[0].clientY;    const distance = Math.sqrt(xMove * xMove + yMove * yMove);    twoPoint.x1 = e.touches[0].pageX * 2;    twoPoint.y1 = e.touches[0].pageY * 2;    twoPoint.x2 = e.touches[1].pageX * 2;    twoPoint.y2 = e.touches[1].pageY * 2;    const obj = {} as {      distance: number;      zoom: boolean;    };    obj["distance"] = distance;    obj["zoom"] = true; //缩放状态    setStv({      ...stv,      ...obj,    });  }};const touchMove = (e: React.TouchEvent<HTMLDivElement>) => {  if (e.touches.length === 2) {      //双指缩放      const xMove = e.touches[1].clientX - e.touches[0].clientX;      const yMove = e.touches[1].clientY - e.touches[0].clientY;      const distance = Math.sqrt(xMove * xMove + yMove * yMove);      const distanceDiff = distance - stv.distance;      const newScale = stv.scale + 0.005 * distanceDiff;      if (newScale < 0.2 || newScale > 2.5) {        return;      }      const obj = {} as {        distance: number;        scale: number;      };      obj["distance"] = distance;      obj["scale"] = newScale;      setStv({ ...stv, ...obj });  }}

旋转

旋转中用到知识点 点乘 、叉乘

点乘用来计算旋转的角度,叉乘用来计算旋转的方向

向量(也称为矢量),指具备大小和方向的量。它能够形象化地示意为带箭头的线段。
箭头所指:代表向量的方向;线段长度:代表向量的大小。

向量的点乘

点乘有什么用呢,咱们有:A B = |A||B|Cos()
是向量 A 和向量 B 见的夹角。这里|A|咱们称为向量 A 的模(norm),也就是 A 的长度, 在二维空间中就是|A| = sqrt(x2+y2)。
这样咱们就和容易计算两条线的夹角: Cos() = AB /(|A||B|)。

用一下反余弦函数 acos(),返回值的单位为弧度,弧度(rad)换算成角度(deg):x=∠A*(180/)

向量的叉乘

叉乘的运算后果是一个向量而不是一个标量, 向量 C 的方向与 A,B 所在的立体垂直,方向用“右手法令”判断。

判断办法如下:
右手手掌张开,四指并拢,大拇指垂直于四指指向的方向;
伸出右手,四指蜿蜒,四指与 A 旋转到 B 方向统一,那么大拇指指向为 C 向量的方向。

旋转、缩放、拖拽 残缺的 touchstartCallback 代码

  const touchstartCallback = (e: React.TouchEvent<HTMLDivElement>) => {    if (e.touches.length === 1) {      const { clientX, clientY } = e.touches[0];      startX = clientX;      startY = clientY;    } else {      const xMove = e.touches[1].clientX - e.touches[0].clientX;      const yMove = e.touches[1].clientY - e.touches[0].clientY;      const distance = Math.sqrt(xMove * xMove + yMove * yMove);      twoPoint.x1 = e.touches[0].pageX * 2;      twoPoint.y1 = e.touches[0].pageY * 2;      twoPoint.x2 = e.touches[1].pageX * 2;      twoPoint.y2 = e.touches[1].pageY * 2;      const obj = {} as {        distance: number;        zoom: boolean;      };      obj["distance"] = distance;      obj["zoom"] = true; //缩放状态      setStv({        ...stv,        ...obj,      });    }  };

旋转 touchMove 代码

/** touchMove计算旋转代码 */ //计算叉乘const calculateVC = (vector1: { x: number; y: number }, vector2: { x: number; y: number }) => {  return vector1.x * vector2.y - vector2.x * vector1.y > 0 ? 1 : -1;};//计算点乘const calculateVM = (vector1: { x: number; y: number }, vector2: { x: number; y: number }) => {  return (    (vector1.x * vector2.x + vector1.y * vector2.y) /    (Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y) *      Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y))  );}; const vector = function(open: OpenAttribute, x1: number, y1: number, x2: number, y2: number) {    const open2 = open;    open2.x = x2 - x1;    open2.y = y2 - y1;    return open2;  }; const touchMove = (e: React.TouchEvent<HTMLDivElement>) => {   if (e.touches.length === 2) {     //计算旋转    const preTwoPoint = JSON.parse(JSON.stringify(twoPoint));    twoPoint.x1 = e.touches[0].pageX * 2;    twoPoint.y1 = e.touches[0].pageY * 2;    twoPoint.x2 = e.touches[1].pageX * 2;   const vector1 = vector(      {x:0,y:0},      preTwoPoint.x1,      preTwoPoint.y1,      preTwoPoint.x2,      preTwoPoint.y2,    );    const vector2 =  vector({x:0,y:0},twoPoint.x1, twoPoint.y1, twoPoint.x2, twoPoint.y2);    const cos = calculateVM(vector1, vector2);    const angle = (Math.acos(cos) * 180) / Math.PI;    const direction = calculateVC(vector1, vector2);    const _allDeg = direction * angle;    if (Math.abs(_allDeg) > 1) {      const obj = {} as {        rotate: number;      };      obj["rotate"] = stv.rotate + _allDeg;      setStv({ ...stv, ...obj });   } }

以上是我对 旋转 拖拽 缩放技术点的总结,如有疑难欢送在评论区一起探讨

残缺 TS less 文件 下载

插播一条招聘信息,LeapFE 招聘前端工程师

如果你对 用户体验、交互操作流程及用户需要 "有一些" 谋求
如果你对 web 、小程序 、Electron 技术 "有一些" 意识
如果你 很善于前端新技术的学习和分享

欢送退出好将来,欢送退出 LeapFE 一起做一些有意思的事件

参考文献

https://segmentfault.com/a/11...
https://juejin.cn/post/684490...
https://juejin.cn/post/691159...