介绍

实现下图所示的图片拖拽缩放效果:

image.png
  1. 通常实现方式都是 <img> 元素外面包裹个 DIV,然后定位一些方框框,然后再去拉伸。如果是非编辑器产品,这么实现并没有多大的问题。
  2. 但是如果是需要实时编辑的产品,IMG 外面还有其他标签,势必会影响很多编辑操作。
  3. 当然,还有方法就是 JS 定位,拖拽层覆盖在图像上,从技术成本上讲,也是一个不错的实现,但如果页面发生了滚动,或者拖拽很快,拖拽的小方块就有可能跟不上(具体要看你的实现)。

CSS

image.png

四个角四个圆圈圈,比较简洁,凡是这种在元素边框(不包括边角)包含规则图形(没有图形也是一种规律)的效果,一定是使用 CSS border-image 属性。

下图是使用处理后的素材配合 border-image 属性实现的效果:

image.png

可能图有些小,看不到细节,把边角放大 N 倍看下:

image.png

border-image 生成的图形藏在了图像内容的后面。在 Web 中,content 内容的层级是最高的,outline 轮廓、border 边框、background 背景色等都是比图文内容的层级低的。因此,border-image 的图形在 IMG 元素内容的后面,导致边角的拖拽圈圈显示不全。

将拖拽图形全部改造为在图像元素的外部,这样就不会有被内容覆盖的问题了:

image.png

相关 CSS代码如下:

img.resizable, img[resizable] {
  border: 3px solid transparent;
  border-image: url(./作者zhangxinxu.svg) 12 / 12px / 0; 
}

JS 代码

function onlyImageResize(options) {
  var doc = document;
  var win = window;
  // 参数处理
  var defaults = {
    selector: '.resizable, [resizable]',
    maxWidth: true,
    whenDisabled: function() {
      return win.imgResizable === false || doc.imgResizable === false;
    },
    // 拖拽完成
    onFinish: function() {}
  };
  options = options || {};
  var params = {};
  for (var key in defaults) {
    params[key] = options[key] || defaults[key];
  }
  // 存放临时数据的地方
  var store = {};
  // 匹配目标元素的选择器
  var strSelector = params.selector;
  var strSelectorImg = strSelector.split(',').map(function(selector) {
    return 'img' + selector.trim();
  }).join();
  var strSelectorActive = strSelector.split(',').map(function(selector) {
    return selector.trim() + '.active';
  }).join();
  // 载入必要的 CSS 样式
  var eleStyle = document.createElement('style');
  var strSvg = "data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='%23914AFF' d='M2.5 2.5h25v25h-25z'/%3E%3Cpath d='M0 0v12h2V2h10V0H0zM0 30V18h2v10h10v2H0zM30 0H18v2h10v10h2V0zM30 30H18v-2h10V18h2v12z' fill='%23914AFF'/%3E%3C/svg%3E";
  eleStyle.innerHTML = strSelectorImg + '{display:inline-block;vertical-align: bottom;font-size:12px;border: 3px solid transparent;margin:-1px;position: relative;-webkit-user-select: none; user-select: none; }' + strSelectorActive + '{border-image: url("' + strSvg + '") 12 / 12px / 0; cursor: default; z-index: 1;}';
  document.head.appendChild(eleStyle);
  // 先点击图片,进入可拉伸状态
  doc.addEventListener('click', function(event) {
    var eleTarget = event.target;
    if (!eleTarget || !eleTarget.matches) {
      return;
    }
    var eleActive = document.querySelector(strSelectorActive);
    if (eleActive && eleActive != eleTarget) {
      eleActive.classList.remove('active');
    }
    if (params.whenDisabled()) {
      return;
    }
    if (eleTarget.matches(strSelector)) {
      eleTarget.classList.add('active');
    }
  });
  // 设置拉伸触发的标志量
  doc.addEventListener('mousedown', function(event) {
    var eleTarget = event.target;
    if (eleTarget.matches && eleTarget.matches(strSelectorActive) && eleTarget.style.cursor) {
      event.preventDefault();
      store.reszing = true;
      store.image = eleTarget;
      store.clientX = event.clientX;
      store.clientY = event.clientY;
      // 此时图片的尺寸
      store.imageWidth = eleTarget.width || eleTarget.clientWidth;
      store.imageHeight = eleTarget.height || eleTarget.clientHeight;
      // 此时图片的拉伸方位
      store.position = eleTarget.position;
      // 最大宽度
      if (typeof params.maxWidth == 'number') {
        store.maxWidth = params.maxWidth;
      } else if (params.maxWidth) {
        // 使用第一个非内联水平的祖先元素的尺寸作为最大尺寸
        var eleParent = (function(element) {
          var step = function(ele) {
            var display = getComputedStyle(ele).style;
            if (/inline/.test(display)) {
              return step(ele.parentElement);
            }
            return ele;
          }
          return step(element);
        })(eleTarget.parentElement);
        // 设置最大尺寸
        if (eleParent) {
          store.maxWidth = eleParent.clientWidth - 4;
        }
      }
    }
  });
  // 设置手形,或者拖拽,视标志量决定
  doc.addEventListener('mousemove', function(event) {
    var eleTarget = event.target;
    if (store.reszing) {
      event.preventDefault();
      // 移动距离
      var distanceX = event.clientX - store.clientX;
      var distanceY = event.clientY - store.clientY;
      // 变化的尺寸
      var width = 0;
      var height = 0;
      // 方位计算是加还是减
      var scale = 1;
      // 不同方位有着不同的判断逻辑
      var position = store.position;
      // 左下角
      if ((position == 'bottom left' || position == 'top right') && distanceX * distanceY < 0) {
        // 左下方是变大,右上是变小
        // distanceX- distanceY+ 变大,distanceX+ distanceY-是变小
        // 右上角
        // 左下方是变小,右上是变大,正好和 'bottom left' 相反
        if (position == 'top right') {
          scale = -1;
        }
        width = store.imageWidth - distanceX * scale;
        height = store.imageHeight + distanceY * scale;
      } else if ((position == 'top left' || position == 'bottom right') && distanceX * distanceY > 0) {
        // 左上角
        // distanceX+, distanceY+是缩小
        // distanceX-, distanceY-是放大
        // 如果是右下角,则相反
        if (position == 'bottom right') {
          scale = -1;
        }
        width = store.imageWidth - distanceX * scale;
        height = store.imageHeight - distanceY * scale;
      }
      if (!width && !height) {
        return;
      }
      // 目标尺寸
      var imageWidth = 0;
      var imageHeight = 0;
      // 图像的原始比例
      var ratio = store.imageWidth / store.imageHeight;
      // 选择移动距离大的方向
      if (Math.abs(distanceX) > Math.abs(distanceY)) {
        // 宽度变化为主
        imageWidth = width;
        imageHeight = width / ratio;
      } else {
        // 高度变化为主
        imageHeight = height;
        imageWidth = height * ratio;
      }
      // 最终设置图片的尺寸
      store.image.width = Math.round(imageWidth);
      store.image.height = Math.round(imageHeight);
    } else if (eleTarget.matches && eleTarget.matches(strSelectorActive)) {
      // 根据位置设置手形
      var clientX = event.clientX;
      var clientY = event.clientY;
      var bounding = eleTarget.getBoundingClientRect();
      // 边缘判断
      if ((clientX - bounding.left < 20 && clientY - bounding.bottom > -20) || (clientX - bounding.right > -20 && clientY - bounding.top < 20)) {
        eleTarget.style.cursor = 'nesw-resize';
        // 判断位置
        if (clientX - bounding.left < 20) {
          eleTarget.position = 'bottom left';
        } else {
          eleTarget.position = 'top right';
        }
      } else if ((clientX - bounding.left < 20 && clientY - bounding.top < 20) || (clientX - bounding.right > -20 && clientY - bounding.bottom > -
          20)) {
        eleTarget.style.cursor = 'nwse-resize';
        // 判断位置
        if (clientX - bounding.left < 20) {
          eleTarget.position = 'top left';
        } else {
          eleTarget.position = 'bottom right';
        }
      } else {
        eleTarget.style.cursor = '';
        eleTarget.position = '';
      }
    }
  });
  // 拖拽结束
  doc.addEventListener('mouseup', function(event) {
    // 图片尺寸超出100%限制
    if (store.image && store.maxWidth && store.image.width > store.maxWidth) {
      // 目标尺寸
      var imageWidth = store.maxWidth;
      var imageHeight = imageWidth / (store.imageWidth / store.imageHeight);
      // 最终设置图片的尺寸
      store.image.width = Math.round(imageWidth);
      store.image.height = Math.round(imageHeight);
    }
    if (store.reszing) {
      store.reszing = false;
      params.onFinish();
    }
  });
};

使用

在 HTML 页面中直接引入上面 JS 代码就可以使用了:

<script>
  onlyImageResize({
    // 参数在这里
  });
</script>

此时,页面中所有设置了类名 .resizable,或者设置了 HTML 属性 resizable 的元素都可以四面拉伸了。

options 为可选参数,包括:

  • selector

    字符串值。默认值是 '.resizable, [resizable]',表示识别为可拉伸图片的选择器。

  • maxWidth

    数值或布尔值。默认是 true,表示有最大宽度限制,最大宽度值是第一个非内联祖先元素的宽度。支持设置为数值,指定最大宽度值。

  • whenDisabled

    函数值,如果返回 true,表示禁用图像的拉伸,如果是 false,则拉伸执行。默认值是:

    function () {
        return window.imgResizable === false || document.imgResizable === false;
    }
    

    表示,如果 window.imgResizable 或者 document.imgResizable 的值是 false,则禁用拉伸。

  • onFinish

    函数值,默认是空函数,拖拽结束的时候触发。

码上掘金

参考资料

  1. JS之我用单img元素实现了图像resize拉伸效果
  2. 体验地址:https://zhangxinxu.gitee.io/only-img-resize/