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

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

接下来就是一些收尾的工作,实现镜像翻转功能。这个功能就相对来说比较简单了,这里只做简单的实现,使用 css 即可。

首先为元素新增字段来确定翻转效果,这个效果就两种,翻转和不翻转,所以使用 boolean 值。

/**
 * 处理元素属性
 *
 * @param {Object} options 元素属性
 */
handleOptions(options = {}) {
  // 之前的代码省略......

  // 新增镜像翻转字段
  _options.mirrorFlip = false;

  // 之前的代码省略......
}

接着定义翻转区域,接着前面实现区域列表新增一个翻转区域即可,然后翻转是通过点击实现的,所以添加在点击状态数组里面

const MIRROR_FLIP_W = 20;
const MIRROR_FLIP_H = 20;

const STATUS_MIRROR_FLIP = 'mirrorFlip';

const DOWN_STATUS_ARRAY = [STATUS_DEL, STATUS_MIRROR_FLIP, null];

constructor(options) {
  // 其他代码省略......

  // 区域列表
  this.zoneList = [
    // 之前的省略......

    // 设置镜像翻转区域
    {
      status: STATUS_MIRROR_FLIP,
      width: MIRROR_FLIP_W,
      height: MIRROR_FLIP_H,
      xRatio: 0,
      yRatio: 1,
      icon: './images/fz_icon.png',
      trigger: 'down'
    }
  ];

  // 其他代码省略......
}

然后再新增处理镜像翻转的方法

/**
 * 处理元素镜像翻转
 */
handleEleMirrorFlip() {
  this.currItem.mirrorFlip = !this.currItem.mirrorFlip;
}

/**
 * 处理元素 status,对应的 status 做对应的事情
 * 
 * @param {Number} x 移动的 x 坐标
 * @param {Number} y 移动的 y 坐标
 */
handleEleStatus(x, y) {
  return {
    // 之前的省略......

    [STATUS_MIRROR_FLIP]: () => this.handleEleMirrorFlip()
  }
}

最后执行渲染

/**
 * 渲染
 */
render() {
  let str = '';

  this.elementArray.forEach(item => {
    // 之前的代码省略......

    // 新增镜像翻转
    // 这样就可以发现套层结构的好处
    // 这样我们只需要改变渲染元素自身的翻转效果
    // 而不需要像之前那样,按钮渲染在元素内部,
    // 要考虑按钮受到的影响
    str += `
      <div style="${styleStr}">
        <div style="width:${item.width}px;height:${item.height}px;background-color:${item.backgroundColor};transform:rotateY(${item.mirrorFlip ? 180 : 0}deg);"></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;
}

效果

在这里插入图片描述

到此,功能实现差不多了,基于此,如果后面还要实现删除功能,接着上面的套路再写即可。

下面我们考虑这样一种情况,如果我想各个区域位置变换一下,或者我不想要其他的一些区域,例如翻转,这时候怎么办?直接修改代码里面吗,那如果有多个地方都使用了,那么怎么改?所以为了这部分的灵活性,我们抽取配置,外界可以传入配置,通过配置来生成内置的区域。

{
  status: STATUS_MIRROR_FLIP,
  width: MIRROR_FLIP_W,
  height: MIRROR_FLIP_H,
  xRatio: 0,
  yRatio: 1,
  icon: './images/fz_icon.png',
  trigger: 'down'
}

观察之前定义的数据,我们可以发现,可配置的项有 width height xRatio yRatio icon,然后通过什么来确定当前更改项?观察数据中就只有 status 了,那么对于内置的 status 必须让外界传递正确,又是一层限制吧,哈哈哈,无奈~

constructor(options) {
  let {
    id,
    scrollLeft = 0,
    scrollTop = 0,
    // 新增区域配置
    zoneConf
  } = options;

  // 之前的代码省略......

  // 如果存在配置,就执行对应的方法
  if (zoneConf) this.handleZoneConf(zoneConf);

  // 之前的代码省略......
}

/**
 * 处理 zone 配置
 *
 * @param {Array} zoneConf zone 配置
 */
handleZoneConf(zoneConf) {
  if (!Array.isArray(zoneConf)) {
    throw new Error('zoneConf 类型错误!');
  }

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

      if (zone.status === conf.status) {
        // 是否使用
        if (conf.use === false) {
          this.zoneList.splice(i, 1);
          i--;
        } else {
          let {
            status,
            width,
            height,
            xRatio,
            yRatio
          } = conf;

          // status 必须是我们定义的
          if (
            status === null ||
            [
              ...MOVE_STATUS_ARRAY,
              ...DOWN_STATUS_ARRAY
            ].findIndex(o => o === status) === -1
          ) {
            throw new Error('status 值错误: ' + status);
          }

          this.isNumber(width, 'width');
          this.isNumber(height, 'height');
          this.isNumber(xRatio, 'xRatio');
          this.isNumber(yRatio, 'yRatio');

          zone = Object.assign(zone, conf);
        }
        break;
      }
    }
  });
}

/**
 * 判断是否是 number
 *
 * @param {*} value 值
 * @param {*} field 字段名
 */
isNumber(value, field) {
  if (value !== undefined && typeof value !== 'number') {
    throw new Error(`${field} 值类型错误,应该为 number 类型!传递的值为:${value}`);
  }
}

然后简单的测试一下

const eleDrop = new EleDrop({
  id: 'eleBox',
  // 测试区域按钮配置
  zoneConf: [{
    status: 'mirrorFlip',
    use: false
  }, {
    status: 'rotate',
    xRatio: 0,
    yRatio: 0
  }]
});

效果

在这里插入图片描述

现在我们又多了一丝丝的灵活性,最后再扩展一些东西,如果用户想挂一些自定义的属性在元素上,现在继续新增东西。

constructor(options) {
  let {
    id,
    scrollLeft = 0,
    scrollTop = 0,
    // 新增元素自定义字段配置
    itemFields = [],
    zoneConf
  } = options;

  // 省略之前的部分代码......

  this.itemFields = itemFields;

  // 省略之前的部分代码......
}

/**
 * 处理元素属性
 *
 * @param {Object} options 元素属性
 */
handleOptions(options = {}) {
  // 省略之前的部分代码......

  // 新增自定义字段
  _this.itemFields.forEach(o => {
    _options[o.field] = o.default;
  });

  // 省略之前的部分代码......
}

至此功能差不多都实现了,当然,这里只是简单的渲染一个元素,这个元素可以还可以更改,例如渲染一张图片,元素结构里面可以再复杂一下,这个后面有时间再来慢慢优化实现~

可以简单的说一下实现渲染图片,可以为元素增加一个 type 属性,用来确定元素类型,再新增一个 imageSrc 字段记录元素的图片地址,如果是图片类型的元素,则渲染这个图片,渲染函数里面就要新增渲染图片逻辑。代码实现就不探究了~

至此,结束,下面给出完整的代码

// 抽离部分常量,方便维护
const INIT_WIDTH = 50;
const INIT_HEIGHT = 50;
const INIT_X = 10;
const INIT_Y = 10;
const INIT_COLOR = '#CCCCCC';
const ROTATE_W = 20;
const ROTATE_H = 20;
// 新增删除区域常量
const DEL_W = 20;
const DEL_H = 20;
// 新增缩放区域常量
const SCALE_W = 20;
const SCALE_H = 20;
const MIRROR_FLIP_W = 20;
const MIRROR_FLIP_H = 20;
const MIN_WIDTH = 40;
// 元素状态
const STATUS_MOVE = 'move';
const STATUS_ROTATE = 'rotate';
const STATUS_SCALE = 'scale';
const STATUS_DEL = 'del';
const STATUS_MIRROR_FLIP = 'mirrorFlip';
const MOVE_STATUS_ARRAY = [STATUS_MOVE, STATUS_ROTATE, STATUS_SCALE, null];
const DOWN_STATUS_ARRAY = [STATUS_DEL, STATUS_MIRROR_FLIP, null];

class EleDrop {
  constructor(options) {
    let {
      id,
      scrollLeft = 0,
      scrollTop = 0,
      itemFields = [],
      zoneConf
    } = options;

    if (typeof id !== 'string' || id === '') {
      throw new Error('请传入正确的容器id');
      return
    }

    this.elementContainer = document.getElementById(id);

    if (!this.elementContainer) {
      throw new Error('未找到传入id的容器: ' + id);
      return
    }

    this.elementContainer.style.position = 'relative';

    // 容器元素的偏移量
    this.offsetX = this.elementContainer.offsetLeft;
    this.offsetY = this.elementContainer.offsetTop;
    // 容器的宽高,不包含边框这些......
    this.width = this.elementContainer.clientWidth;
    this.height = this.elementContainer.clientHeight;
    // 预留scroll,
    // 如果页面存在滚动条的时候,
    // 滚动条的位置也会影响点击的位置
    this.scrollTop = scrollTop;
    this.scrollLeft = scrollLeft;
    this.startX = 0;
    this.startY = 0;
    this.currItem = null;
    this.currItemStartOpt = null;
    this.fnDown = (e) => this.handleDown(e);
    this.fnMove = (e) => this.handleMove(e);
    this.fnUp = (e) => this.handleUp(e);
    this.itemFields = itemFields;

    // 新增区域列表
    this.zoneList = [
      // 旋转区域
      {
        status: STATUS_ROTATE,
        width: ROTATE_W,
        height: ROTATE_H,
        // xy 坐标比例
        // 就是响应区域在元素内部所在中心点
        // 例如元素整体宽高为20*20
        // 我需要响应的中心点在(5,5)
        // 那么比例就是 x: 5/20, y: 5/20
        // 基于这个规则,我们就可以得到元素内部区域的任何位置
        xRatio: 1,
        yRatio: 0,
        icon: './images/xz_icon.png',
        trigger: 'move'
      },
      // 设置删除区域
      {
        status: STATUS_DEL,
        width: DEL_W,
        height: DEL_H,
        xRatio: 0,
        yRatio: 0,
        icon: './images/del_icon.png',
        trigger: 'down'
      },
      // 设置缩放区域
      {
        status: STATUS_SCALE,
        width: SCALE_W,
        height: SCALE_H,
        xRatio: 1,
        yRatio: 1,
        icon: './images/sf_icon.png',
        trigger: 'move'
      },
      // 设置镜像翻转区域
      {
        status: STATUS_MIRROR_FLIP,
        width: MIRROR_FLIP_W,
        height: MIRROR_FLIP_H,
        xRatio: 0,
        yRatio: 1,
        icon: './images/fz_icon.png',
        trigger: 'down'
      }
    ];

    if (zoneConf) this.handleZoneConf(zoneConf);

    this.elementContainer.addEventListener('mousedown', this.fnDown);
    document.addEventListener('mousemove', this.fnMove);
    document.addEventListener('mouseup', this.fnUp);

    const _this = this;

    // 可以根据实际情况更改,
    // 为了方便操作数组的时候自动调用更新,
    // 后期融合到框架中可以删除
    this.elementArray = new Proxy([], {
      set(target, property, value, receiver) {
        _this.render();
        return Reflect.set(target, property, value, receiver);
      }
    });
  }

  /**
   * 处理 zone 配置
   *
   * @param {Array} zoneConf zone 配置
   */
  handleZoneConf(zoneConf) {
    if (!Array.isArray(zoneConf)) {
      throw new Error('zoneConf 类型错误!');
    }

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

        if (zone.status === conf.status) {
          if (conf.use === false) {
            this.zoneList.splice(i, 1);
          } else {
            let {
              status,
              width,
              height,
              xRatio,
              yRatio
            } = conf;

            if (
              status === null ||
              [
                ...MOVE_STATUS_ARRAY,
                ...DOWN_STATUS_ARRAY
              ].findIndex(o => o === status) === -1
            ) {
              throw new Error('status 值错误: ' + status);
            }

            this.isNumber(width, 'width');
            this.isNumber(height, 'height');
            this.isNumber(xRatio, 'xRatio');
            this.isNumber(yRatio, 'yRatio');

            zone = Object.assign(zone, conf);
          }
          break;
        }
      }
    });
  }

  isNumber(value, field) {
    if (value !== undefined && typeof value !== 'number') {
      throw new Error(`${field} 值类型错误,应该为 number 类型!传递的值为:${value}`);
    }
  }

  /**
   * 新增区域
   * 可以自行添加响应的区域,然后执行对应的方法
   *
   * @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
    });
  }

  /**
   * 清空数组
   */
  clear() {
    this.elementArray.splice(0, this.elementArray.length);
  }

  /**
   * 卸载
   */
  unload() {
    this.elementContainer.removeEventListener('mousedown', this.fnDown);
    document.removeEventListener('mousemove', this.fnMove);
    document.removeEventListener('mouseup', this.fnUp);
  }

  /**
   * 设置滚动条
   *
   * @param {Number} top 滚动条 top 值
   * @param {Number} left 滚动条 left 值
   */
  setScroll(top, left) {
    this.scrollLeft = left;
    this.scrollTop = top;
  }

  /**
   * 处理鼠标按下事件
   *
   * @param {*} e 事件参数
   */
  handleDown(e) {
    // 为了兼容直接通过 clientX 与 clientY 计算,
    // 只不过这样得让外界传递滚动条位置参数
    //   如果元素的父级元素有几个都存在滚动条,
    //   那么传递进来的就要是所有的滚动条位置参数相加
    // 实际使用中就看实际情况了
    const { clientX, clientY } = e;
    let x = clientX - this.offsetX + this.scrollLeft;
    let y = clientY - this.offsetY + this.scrollTop;

    let currItem = null;
    // 临时元素状态
    let tempStatus = null;

    // 记录初始点击坐标
    this.startX = x;
    this.startY = y;

    this.elementArray.forEach(item => {
      let status = this.isEleClickZone(x, y, item);

      // 选中点击坐标下顶层元素
      if (
        (status && !currItem) ||
        (status && currItem.zIndex < item.zIndex)
      ) {
        currItem = item;
        tempStatus = status;
      }
    });

    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) {
      this.currItem = currItem;
      // 新增元素状态
      this.currItem.status = tempStatus;
      // 记录选中元素的部分参数,用于后面计算
      this.currItemStartOpt = {
        x: this.currItem.x,
        y: this.currItem.y,
        centerX: this.currItem.centerX,
        centerY: this.currItem.centerY,
        width: this.currItem.width,
        height: this.currItem.height,
        rotate: this.currItem.rotate
      };
    } else {
      this.currItem = null;
      this.currItemStartOpt = null;
      this.render();
    }
  }

  /**
   * 处理鼠标移动事件
   *
   * @param {*} e 事件参数
   */
  handleMove(e) {
    requestAnimationFrame(() => {
      if (!this.currItem || !this.currItemStartOpt) return;

      const { clientX, clientY } = e;
      let x = clientX - this.offsetX + this.scrollLeft;
      let y = clientY - this.offsetY + this.scrollTop;
      let status = this.currItem.status;
      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;
          };
        }
      }
    });
  }

  /**
   * 处理鼠标按键松开
   *
   * @param {*} e 事件参数
   */
  handleUp(e) {
    if (this.currItem) this.currItem.status = null;
  }

  /**
   * 处理元素 status,对应的 status 做对应的事情
   * 
   * @param {Number} x 移动的 x 坐标
   * @param {Number} y 移动的 y 坐标
   */
  handleEleStatus(x, y) {
    return {
      [STATUS_DEL]: () => this.handleEleDel(),
      [STATUS_MOVE]: () => this.handleEleMove(x, y),
      [STATUS_ROTATE]: () => this.handleEleRotate(x, y),
      [STATUS_SCALE]: () => this.handleEleScale(x, y),
      [STATUS_MIRROR_FLIP]: () => this.handleEleMirrorFlip()
    }
  }

  /**
   * 处理元素镜像翻转
   */
  handleEleMirrorFlip() {
    this.currItem.mirrorFlip = !this.currItem.mirrorFlip;
  }

  /**
   * 处理元素删除
   */
  handleEleDel() {
    if (!this.currItem) return
    let i = this.elementArray.findIndex(o => o.id === this.currItem.id);
    if (i > -1) {
      this.elementArray.splice(i, 1);
    }
  }

  /**
   * 处理元素缩放
   *
   * @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;
  }

  /**
   * 处理元素移动
   *
   * @param {Number} clientX
   * @param {Number} clientY
   */
  handleEleMove(x, y) {
    const temp = this.currItemStartOpt;

    // 处理元素移动方法
    // 通过鼠标点击位置来计算中心点,
    //   再通过中心点去计算元素的 xy
    // 不考虑边界情况的中心点坐标
    let _centerX = temp.centerX + x - this.startX;
    let _centerY = temp.centerY + y - this.startY;
    // 最小中心点坐标,用于处理边界情况
    let minCenterX = temp.width / 2;
    let minCenterY = temp.height / 2;
    // 最大中心点坐标,用于处理边界情况
    let maxCenter = this.width - minCenterX;
    let maxHeight = this.height - minCenterY;
    // 最终计算出的中心点坐标
    let centerX = _centerX >= maxCenter ?
      maxCenter :
      _centerX <= minCenterX ?
      minCenterX : _centerX;
    let centerY = _centerY >= maxHeight ?
      maxHeight :
      _centerY <= minCenterY ?
      minCenterY : _centerY;

    this.currItem.centerX = centerX;
    this.currItem.centerY = centerY;
    this.currItem.x = this.currItem.centerX - minCenterX;
    this.currItem.y = this.currItem.centerY - minCenterY;
  }

  /**
   * 处理元素变换
   * 因为抽离功能后单一,所以更改函数名称,
   * 涉及到的地方都要更改
   *
   * @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 {Object} options 元素属性
   */
  addElement(options = {}) {
    const item = this.handleOptions(options);

    this.elementArray.push(item);
  }

  /**
   * 处理元素属性
   *
   * @param {Object} options 元素属性
   */
  handleOptions(options = {}) {
    const _options = {};
    const _this = this;
    const {
      id,
      width,
      height,
      x,
      y,
      backgroundColor,
      zIndex
    } = options;

    _options.id = id ? id : Date.now();
    _options.width = width ? width : INIT_WIDTH;
    _options.height = height ? height : INIT_HEIGHT;
    _options.x = x ? x : INIT_X;
    _options.y = y ? y : INIT_Y;
    _options.backgroundColor = backgroundColor ?
      backgroundColor :
      INIT_COLOR;
    _options.square = [
      [_options.x, _options.y],
      [_options.x + _options.width, _options.y],
      [_options.x + _options.width, _options.y + _options.height],
      [_options.x, _options.y + _options.height]
    ];
    _options.centerX = _options.x + _options.width / 2;
    _options.centerY = _options.y + _options.height / 2;
    _options.zIndex = zIndex ? zIndex : _this.genZIndex();
    // 新增元素状态,用于判断鼠标点击的是元素的什么位置,
    // 好做出相应的操作
    _options.status = null;
    // 新增旋转角度
    _options.rotate = 0;
    // 镜像翻转
    _options.mirrorFlip = false;

    // 新增自定义字段
    _this.itemFields.forEach(o => {
      _options[o.field] = o.default;
    });

    // 当元素的属性发生更改的时候,重新执行渲染
    // 当元素的 xy 发生改变的时候,重新计算顶点坐标
    return new Proxy(_options, {
      set(target, property, value, receiver) {
        if (
          property === 'x' ||
          property === 'y'
        ) {
          _this.rotateSquare();
        }

        if (
          property === 'status' &&
          MOVE_STATUS_ARRAY.indexOf(value) === -1 &&
          DOWN_STATUS_ARRAY.indexOf(value) === -1
        ) {
          throw new Error('元素状态设置错误:' + value);
        }

        _this.render();

        return Reflect.set(target, property, value, receiver);
      }
    });
  }

  /**
   * 生成元素数组中的 zIndex
   *
   * @return 返回最大 zIndex + 1
   */
  genZIndex() {
    if (this.elementArray.length === 0) return 1;

    let maxZIndex = Math.max(
      ...this.elementArray.map(o => o.zIndex)
    );

    return maxZIndex + 1;
  }

  /**
   * 渲染
   */
  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};transform:rotateY(${item.mirrorFlip ? 180 : 0}deg);"></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 不在元素任何区域内返回 false,在区域内返回相应的操作字符串
   */
  isEleClickZone(x, y, item) {
    const zoneStatus = this.handleZone(x, y, item);

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

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

  /**
   * 处理响应区域
   *
   * @param {Number} x 点击的 x 坐标
   * @param {Number} y 点击的 y 坐标
   * @param {*} item 要判断响应区域的元素
   * @return 在区域内返回区域定义的状态,不在则返回false
   */
  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 {Object} item 要判断的元素
   * @return 在元素内返回 true, 不在元素内返回 false
   */
  insideEle(x, y, item) {
    const square = item.square;
    let inside = false;

    for (let i = 0, j = square.length - 1; i < square.length; j = i++) {
      let xi = square[i][0];
      let yi = square[i][1];
      let xj = square[j][0];
      let yj = square[j][1];
      let intersect = yi > y != yj > y &&
        x < (xj - xi) * (y - yi) / (yj - yi) + xi;

      if (intersect) inside = !inside;
    }

    return inside;
  }

  /**
   * 新增计算变换后的元素顶点坐标
   * 因为要加入旋转,所以单独提出来
   *
   * @param {Number} x 元素 x 坐标
   * @param {Number} y 元素 y 坐标
   * @param {Number} centerX 元素中心点 x 坐标
   * @param {Number} centerY 元素中心点 y 坐标
   * @param {Number} degrees 元素旋转角度
   * @return 返回计算好的顶点坐标数组
   * @description
   */
    rotatePoint(x, y, centerX, centerY, degrees) {
    let deg = degrees * Math.PI / 180;
    let _x = (x - centerX) *
      Math.cos(deg) -
      (y - centerY) *
      Math.sin(deg) +
      centerX;
    let _y = (x - centerX) *
      Math.sin(deg) +
      (y - centerY) *
      Math.cos(deg) +
      centerY;
    return [_x, _y];
  }

  /**
   * 重新计算当前元素的顶点坐标
   */
  rotateSquare() {
    const currItem = this.currItem;

    if (!currItem) return;

    // 因为加入旋转,所以更改顶点计算方式
    currItem.square = [
      this.rotatePoint(
        currItem.x,
        currItem.y,
        currItem.centerX,
        currItem.centerY,
        currItem.rotate
      ),
      this.rotatePoint(
        currItem.x + currItem.width,
        currItem.y,
        currItem.centerX,
        currItem.centerY,
        currItem.rotate
      ),
      this.rotatePoint(
        currItem.x + currItem.width,
        currItem.y + currItem.height,
        currItem.centerX,
        currItem.centerY,
        currItem.rotate
      ),
      this.rotatePoint(
        currItem.x,
        currItem.y + currItem.height,
        currItem.centerX,
        currItem.centerY,
        currItem.rotate
      )
    ];
  }
}

const eleDrop = new EleDrop({
  id: 'eleBox',
  // // 测试区域按钮配置
  // zoneConf: [{
  //   status: 'mirrorFlip',
  //   use: false
  // }, {
  //   status: 'rotate',
  //   xRatio: 0,
  //   yRatio: 0
  // }]
});

// 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) {
//       // 需要对里面封装的东西比较了解~
//       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();
//     }
//   }
// });

// eleDrop.addZone({
//   status: 'center',
//   xRatio: 1 / 2,
//   yRatio: 1 / 2,
//   trigger: 'move',
//   icon: './images/del_icon.png',
//   fn(x, y) {
//     console.log(x, y);
//   }
// });

后面有时间再将此在框架中使用,这样会涉及到部分代码的更改~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值