介绍

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

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代码如下:

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

JS 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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 代码就可以使用了:

1
2
3
4
5
<script>
onlyImageResize({
// 参数在这里
});
</script>

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

options 为可选参数,包括:

  • selector

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

  • maxWidth

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

  • whenDisabled

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

    1
    2
    3
    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/