记录元素平移、旋转、缩放和镜像翻转(3)

记录元素平移、旋转、缩放和镜像翻转(3)

接着前面的继续写,前面简单实现了元素的移动,旋转变换,接下来还有一个镜像反转功能实现。其实对于常规功能来说,这样就算可以了,最多再新增个元素的删除功能,对于镜像翻转的话,可能没有这个需求,所以我们实现外界可以自定义区域,只要鼠标在这个自定义区域,就执行自定义的方法,来达到我们封装的功能有部分的灵活性。

因为之前实现的变换包含了随着鼠标的拖动也会进行缩放,为了使功能单一,所以进行抽离~,当然抽离完也可以保留边旋转边缩放的功能。因为抽离了功能,为了方便阅读,所以将旋转涉及到的名称也更改一下。

const ROTATE_W = 20;
const ROTATE_H = 20;
// 新增缩放区域常量
const SCALE_W = 20;
const SCALE_H = 20;
// 旋转
const STATUS_ROTATE = 'rotate';
// 缩放
const STATUS_SCALE = 'scale';
const STATUS_ARRAY = [
  // ......之前有的状态
  STATUS_ROTATE, STATUS_SCALE
];

/**
 * 处理元素变换
 * 因为抽离功能后单一,所以更改函数名称,
 * 涉及到的地方都要更改
 *
 * @param {Number} x
 * @param {Number} y
 */
handleEleRotate(x, y) {
  const currItem = this.currItem;
  const {
    rotate: oRotate
  } = this.currItemStartOpt;

  // 计算旋转角度
  let diffStartX = this.startX - currItem.centerX;
  let diffStartY = this.startY - currItem.centerY;
  let diffEndX = x - currItem.centerX;
  let diffEndY = y - currItem.centerY;
  let angleStart = Math.atan2(diffStartY, diffStartX) / Math.PI * 180;
  let angleEnd = Math.atan2(diffEndY, diffEndX) / Math.PI * 180;

  currItem.rotate = oRotate + angleEnd - angleStart;

  // 抽离缩放逻辑,旋转就是单旋转
  // this.handleEleScale(x, y);
}

/**
 * 抽离处理元素缩放
 *
 * @param {Number} x 点击的 x 坐标
 * @param {Number} y 点击的 y 坐标
 */
handleEleScale(x, y) {
  const currItem = this.currItem;
  const {
    x: oX,
    y: oY,
    width: oWidth,
    height: oHeight
  } = this.currItemStartOpt;

  // 利用中心点计算鼠标移动前后点距离,
  // 用于计算缩放比例,
  // 再基于这个比例重新计算元素宽高
  let lineStart = Math.sqrt(
    Math.pow(currItem.centerX - this.startX, 2) +
    Math.pow(currItem.centerY - this.startY, 2)
  );
  let lineEnd = Math.sqrt(
    Math.pow(currItem.centerX - x, 2) +
    Math.pow(currItem.centerY - y, 2)
  );
  // 计算宽高方法1
  let resizeRaito = lineEnd / lineStart;
  let newW = oWidth * resizeRaito;
  let newH = oHeight * resizeRaito;

  // // 计算新的宽高方法2
  // let resize = lineEnd - lineStart;
  // let newW = oWidth + resize * 2;
  // let newH = oHeight * newW / oWidth;

  // 以短边为基准来计算最小宽高
  if (oWidth <= oHeight && newW < MIN_WIDTH) {
    newW = MIN_WIDTH;
    newH = MIN_WIDTH * oHeight / oWidth;
  } else if (oHeight < oWidth && newH < MIN_WIDTH) {
    newH = MIN_WIDTH;
    newW = MIN_WIDTH * oWidth / oHeight;
  }

  // 以长边为基准来计算最大宽高
  if (oWidth >= oHeight && newW >= this.width) {
    newW = this.width;
    newH = this.width * oHeight / oWidth;
  } else if (oHeight > oWidth && newH >= this.height) {
    newH = this.height;
    newW = this.height * oWidth / oHeight;
  }

  currItem.width = Math.round(newW);
  currItem.height = Math.round(newH);
  currItem.x = Math.round(oX - (newW - oWidth) / 2);
  currItem.y = Math.round(oY - (newH - oHeight) / 2);
  // 重新计算元素的中心点坐标
  currItem.centerX = currItem.x + currItem.width / 2;
  currItem.centerY = currItem.y + currItem.height / 2;
}

接下来就开始抽取区域,让对应的区域做对应的事情,目前已有的功能有旋转和缩放,所以用这两个为基准开始修改代码。对于自定义区域,我们能够确定的是这个区域一定是根据元素的宽高来确定的,所以在定义区域的时候最好和元素宽高挂钩,不能胡乱去定义。根据之前定义的变换区域,我们定义了宽高,所以抽离出来依然要有宽高。

根据上面的分析,可以得到这样一种计算区域的方式,假设我们拥有一个宽高为 60 的元素,想要确定的区域中心点为 (10, 10),那么得到这个中心点的计算方式就是 60 * (1 / 6),元素宽高不变,通过调整宽高比例,我们就可以通过宽高计算出我们想要的任何区域。

// 新增区域列表
this.zoneList = [
  // 旋转区域
  {
    width: ROTATE_W,
    height: ROTATE_H,
    // xy 坐标比例
    // 就是响应区域在元素内部所在中心点
    // 例如元素整体宽高为20*20
    // 我需要响应的中心点在(5,5)
    // 那么比例就是 x: 5/20, y: 5/20
    // 基于这个规则,我们就可以得到元素内部区域的任何位置
    xRatio: 1,
    yRatio: 0
  },
  // 设置缩放区域
  {
    width: SCALE_W,
    height: SCALE_H,
    xRatio: 1,
    yRatio: 1
  }
];

因为我们通过定义区域是想要执行一些操作,所以我们之前定义过元素的状态,通过状态去确定操作,所以在抽取出来的数据里面还得包含状态字段。然后,目前我们涉及到的操作都是在鼠标按下拖动的时候触发,设想这样一种情况,如果我们想实现元素的删除,那么触发方式必然是点击到删除区域然后删除元素,基于此,我们还得定义这个区域的触发方式,目前我们可以使用的触发方式就是鼠标按下和移动,所以定义两种触发方式 move 和 down。

// 因为事件分为 move 和 down,所以抽离数组
const MOVE_STATUS_ARRAY = [STATUS_MOVE, STATUS_ROTATE, STATUS_SCALE, null];
const DOWN_STATUS_ARRAY = [null];

// 新增区域列表
this.zoneList = [
  // 旋转区域
  {
    status: STATUS_ROTATE,
    width: ROTATE_W,
    height: ROTATE_H,
    xRatio: 1,
    yRatio: 0,
    trigger: 'move'
  },
  // 缩放区域
  {
    status: STATUS_SCALE,
    width: SCALE_W,
    height: SCALE_H,
    xRatio: 1,
    yRatio: 1,
    trigger: 'move'
  }
];

这样数据结构就定义的差不多了吧~,嗯…应该还差点点,在之前我们的区域都是用文本来展示的,这样有一点点的怪异,大多数情况还是使用图片来展示,所以还加上图标路径。

// 新增区域列表
this.zoneList = [
  // 旋转区域
  {
    status: STATUS_ROTATE,
    width: ROTATE_W,
    height: ROTATE_H,
    xRatio: 1,
    yRatio: 0,
    icon: './images/xz_icon.png',
    trigger: 'move'
  },
  // 缩放区域
  {
    status: STATUS_SCALE,
    width: SCALE_W,
    height: SCALE_H,
    xRatio: 1,
    yRatio: 1,
    icon: './images/sf_icon.png',
    trigger: 'move'
  }
];

接下来,我们就渲染定义的区域,后期使用框架就不需要这样了,目前这样写只是图方便,如果真要考虑全面,那么这里可以每次只更改变化的部分,不用每次都全部渲染。

/**
 * 渲染
 */
render() {
  // 更改渲染结构
  let str = '';

  this.elementArray.forEach(item => {
    let styleStr = `position:absolute;top:0;left:0;z-index:${item.zIndex};width:${item.width}px;height:${item.height}px;transform:translateX(${item.x}px) translateY(${item.y}px) translateZ(0px) rotate(${item.rotate}deg);`

    // 多添加一层容器,包裹内层元素,这样做如果后面涉及到镜像翻转,
    // 只需要变换内层元素即可,不会影响到外层的区域
    str += `
      <div style="${styleStr}">
        <div style="width:${item.width}px;height:${item.height}px;background-color:${item.backgroundColor};"></div>
        ${
          this.currItem && this.currItem.id === item.id ?
            this.zoneList.map(o => {
              // 因为我们定义的是区域的中心点,
              // 所以渲染区域的时候,减去自身宽高的一半
              // 里面还有一些可以抽离的常量,感兴趣的自行修改~
              return `<div style="box-sizing:border-box;position:absolute;top:${item.height * o.yRatio - o.height / 2}px;left:${item.width * o.xRatio - o.width / 2}px;width:${o.width}px;height:${o.height}px;border:1px solid #666;font-size:12px;text-align:center;line-height:${o.height}px;border-radius:50%;">
                  <img src="${o.icon}" style="width:60%;height:60%;-webkit-user-drag:none;-moz-user-drag:none;-ms-user-drag:none;user-drag:none;" />
                </div>
              `
            }).join('') :
            ''
        }
      </div>
    `;
  });

  this.elementContainer.innerHTML = str;
}

接下来实现处理区域事件,之前我们使用的是直接将判断的代码写在一个函数中,其实对于区域的判断逻辑,大致相同,所以得改。

/**
 * 处理响应区域
 *
 * @param {Number} x 点击的 x 坐标
 * @param {Number} y 点击的 y 坐标
 * @param {*} item 要判断响应区域的元素
 * @return 在区域内返回区域定义的状态,不在则返回null
 */
handleZone(x, y, item) {
  let tempStatus = null;

  for (let i = 0, len = this.zoneList.length; i < len; i++) {
    const zone = this.zoneList[i];

    let pos = this.rotatePoint(
      item.x + item.width * zone.xRatio,
      item.y + item.height * zone.yRatio,
      item.centerX,
      item.centerY,
      item.rotate
    );
    let minX = pos[0] - zone.width / 2;
    let minY = pos[1] - zone.height / 2;
    let maxX = pos[0] + zone.width / 2;
    let maxY = pos[1] + zone.height / 2;

    // 按理来说同一时刻只响应一个事件,
    // 所以在判断到这个区域的时候就直接跳出整个循环
    if (
      x >= minX &&
      x <= maxX &&
      y >= minY &&
      y <= maxY
    ) {
      tempStatus = zone.status;
      break;
    }
  }

  return tempStatus;
}

/** 
 * 元素点击区域判断,对于不同的点击区响应不同的操作方法
 *
 * @param {Number} x 点击的 x 坐标
 * @param {Number} y 点击的 y 坐标
 * @param {*} item 要判断的元素
 * @return 不在元素任何区域内返回 false,在区域内返回相应的操作字符串
 */
isEleClickZone(x, y, item) {
  // // 因为有多个区域判断,且后期可以自定义区域,所以我们抽离区域定义
  // // 判断是否在旋转区域
  // // 默认定义的区域在右下角
  // let tranPosition = this.rotatePoint(
  //   item.x + item.width / 2,
  //   item.y + item.height / 2,
  //   item.centerX,
  //   item.centerY,
  //   item.rotate
  // );
  // // let tranX = tranPosition[0] - ROTATE_W / 2;
  // // let tranY = tranPosition[1] - ROTATE_H / 2;
  // // console.log(tranX, tranY, x, y, item.centerX, item.centerY);
  // // if (
  // //   x - tranX >= 0 &&
  // //   y - tranY >= 0 &&
  // //   tranX + ROTATE_W - x >= 0 &&
  // //   tranY + ROTATE_H - y >= 0
  // // ) {
  // //   return STATUS_ROTATE;
  // // } else if (this.insideEle(x, y, item)) {
  // //   return STATUS_MOVE;
  // // }

  // let minTranX = tranPosition[0] - ROTATE_W / 2;
  // let minTranY = tranPosition[1] - ROTATE_H / 2;
  // let maxTranX = tranPosition[0] + ROTATE_W / 2;
  // let maxTranY = tranPosition[1] + ROTATE_H / 2;
  // if (
  //   x >= minTranX &&
  //   x <= maxTranX &&
  //   y >= minTranY &&
  //   y <= maxTranY
  // ) {
  //   return STATUS_ROTATE;
  // } else if (this.insideEle(x, y, item)) {
  //   return STATUS_MOVE;
  // }

  const zoneStatus = this.handleZone(x, y, item);

  if (zoneStatus) return zoneStatus;
  else if (this.insideEle(x, y, item)) return STATUS_MOVE;

  // 不在元素区域里面并且不在元素操作按钮区域内
  return false;
}

上面我们实现了抽取区域,对应的区域做相应的事情,基于此,我们可以让外界也传入同样的配置,来实现自己的操作,不过这个操作有部分限制,函数定义在配置对象内,那么 this 指向会变,所以也不能完全方便的使用方法,当然这个也可以更改(等等,这里我有个好sou主意,对于涉及到 this 的地方可以增加可以外界传入,然后可以通过 call apply bind 来更改 this 指向),算了,我反正不想改了!哪有什么绝对的自由,在我代码定义的规则里面,那就按照我的规则来实现功能吧~

/**
 * 新增区域
 * 可以自行添加响应的区域,然后执行对应的方法
 *
 * @param {*} zone 新增区域的属性描述
 */
addZone(zone) {
  let {
    status,
    width = 20,
    height = 20,
    xRatio,
    yRatio,
    icon,
    trigger = 'move',
    fn
  } = zone;

  if (
    status === undefined ||
    xRatio === undefined ||
    yRatio === undefined ||
    fn === undefined ||
    icon === undefined
  ) {
    throw new Error('status, xRatio, yRatio, icon 和 fn 是必须的, 请检查这些字段是否填写准确!');
  }

  if (trigger !== 'move' && trigger !== 'down') {
    throw new Error('trigger 字段的值只能是 move 和 down!');
  }

  if (
    typeof width !== 'number' ||
    typeof height !== 'number' ||
    typeof xRatio !== 'number' ||
    typeof yRatio !== 'number'
  ) {
    throw new Error('width, height, xRatio 和 yRatio 字段的值类型只能是 number!');
  }

  if (trigger === 'move') MOVE_STATUS_ARRAY.push(status);
  else if (trigger === 'down') DOWN_STATUS_ARRAY.push(status);

  this.zoneList.push({
    status,
    width,
    height,
    xRatio,
    yRatio,
    icon,
    trigger,
    fn
  });
}

接下来在鼠标点击和移动的时候新增触发逻辑

/**
 * 处理鼠标按下事件
 *
 * @param {*} e 事件参数
 */
handleDown(e) {
  // 之前的逻辑省略......

  // 如果当前元素和选中的元素是同一个元素
  if (currItem && this.currItem && currItem.id === this.currItem.id) {
    let fn = this.handleEleStatus(x, y)[tempStatus];
    // 如果状态在状态数组中存在,且为系统自定义的函数
    // 就执行系统自定义的函数
    if (DOWN_STATUS_ARRAY.findIndex(s => s === tempStatus) > -1 && fn) {
      fn();
    } else {
      // 否则就查找区域列表中为点击且状态和当前状态一样的元素
      // 如果存在这样的元素,则执行对应的函数
      // 将点击坐标传递过去
      for (let i = 0, len = this.zoneList.length; i < len; i++) {
        const zone = this.zoneList[i];
        if (zone.trigger !== 'down') continue;

        if (tempStatus === zone.status && zone.trigger === 'down') {
          zone.fn(x, y);
          break;
        };
      }
    }
  }

  if (currItem) {
    // 省略之前的逻辑......
  } else {
    // 如果点击区域没有任何元素,就取消之前元素的选中
    this.currItem = null;
    this.currItemStartOpt = null;
    this.render();
  }
}

/**
 * 处理鼠标移动事件
 *
 * @param {*} e 事件参数
 */
handleMove(e) {
  requestAnimationFrame(() => {
    // 省略之前的逻辑......

    let fn = this.handleEleStatus(x, y)[status];

    if (MOVE_STATUS_ARRAY.findIndex(s => s === status) > -1 && fn) {
      fn();
    } else {
      for (let i = 0, len = this.zoneList.length; i < len; i++) {
        const zone = this.zoneList[i];
        if (zone.trigger !== 'move') continue;

        if (status === zone.status && zone.trigger === 'move') {
          zone.fn(x, y);
          break;
        };
      }
    }
  });
}

接着从外界新增个来测试一下

const eleDrop = new EleDrop({
  id: 'eleBox'
});

eleDrop.addZone({
  status: 'center',
  xRatio: 1 / 2,
  yRatio: 1 / 2,
  trigger: 'down',
  icon: './images/del_icon.png',
  fn(x, y) {
    console.log(x, y);
    // 例如我想让元素移动到中心区
    if (eleDrop.currItem) {
      // 需要了解里面封装的东西比较了解~
      // emmmmm后面有时间可以优化
      eleDrop.currItem.x = eleDrop.width / 2 - eleDrop.currItem.width / 2;
      eleDrop.currItem.y = eleDrop.height / 2 - eleDrop.currItem.height / 2;
      eleDrop.currItem.centerX = eleDrop.currItem.x + eleDrop.currItem.width / 2;
      eleDrop.currItem.centerY = eleDrop.currItem.y + eleDrop.currItem.height / 2;
      eleDrop.rotateSquare();
    }
  }
});

查看效果

在这里插入图片描述

然后自定义区域移动触发可以自行实验,上面的效果也演示了之前抽离的旋转和缩放,所以不再单独看演示效果了。

至此,本篇因为篇幅也结束了,后面再实现其他的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值