EN
JavaScript - create zoom-able image element
9 points
In this article, we would like to show how to create simple zoom-able image element using JavaScript.
The solution presented in the article:
- scales image on mouse wheel and on touch screen events,
- allows to move when image is scaled,
- uses position and size styles to move and resize image element.

Practical example:
xxxxxxxxxx
1
2
<html>
3
<head>
4
<style>
5
6
* { /* this style disables selection for all elements on web page */
7
user-select: none; /* Safari */
8
user-select: none; /* Konqueror HTML */
9
user-select: none; /* Firefox in the past (old versions) */
10
user-select: none; /* Internet Explorer (>=10) / Edge */
11
user-select: none; /* Currently supported: */
12
/* Chrome, Opera and Firefox */
13
}
14
15
* { /* this style disables dragging for all elements on web page */
16
user-drag: none; /* Safari */
17
user-drag: none; /* Konqueror HTML */
18
user-drag: none; /* Firefox in the past (old versions) */
19
user-drag: none; /* Internet Explorer (>=10) / Edge */
20
user-drag: none; /* Opera */
21
user-drag: none; /* Currently supported: */
22
/* Chrome, Opera and Firefox */
23
}
24
25
* {
26
touch-callout: none; /* iOS Safari */
27
}
28
29
.container {
30
position: relative;
31
border: 1px solid #e4e4e4;
32
width: 400px;
33
height: 300px;
34
overflow: hidden;
35
}
36
37
.image {
38
position: absolute;
39
}
40
41
</style>
42
</head>
43
<body>
44
<div id="container" class="container">
45
<img id="image" class="image" style="display: none" src="https://dirask.com/static/bucket/1669209714713-xgDEWrBQWo--image.png" onload="this.style.display = ''" />
46
</div>
47
<script>
48
49
function calculateMeanValue(a, b) {
50
return 0.5 * (a + b);
51
}
52
53
function keepValueRange(value, min, max) {
54
if (min > max) {
55
throw new Error('Minimum value should be greater than maximum value.');
56
}
57
if (value < min) {
58
return min;
59
}
60
if (value > max) {
61
return max;
62
}
63
return value;
64
}
65
66
function executeArrayFunctions(array) {
67
for (var i = 0; i < array.length; ++i) {
68
var action = array[i];
69
action.call(null);
70
}
71
}
72
73
function replaceElementClasses(classes, oldClass, newClass) {
74
classes.remove(oldClass);
75
classes.add(newClass);
76
}
77
78
function createEventListener(element, event, preventing, callback) {
79
var handler = function(e) {
80
if (preventing) {
81
e.preventDefault();
82
}
83
callback(e);
84
};
85
element.addEventListener(event, handler, false);
86
return function() {
87
element.removeEventListener(event, handler, false);
88
};
89
}
90
91
function getRelativePosition(event) {
92
var rect = event.currentTarget.getBoundingClientRect();
93
return {
94
x: event.clientX - rect.left,
95
y: event.clientY - rect.top
96
};
97
}
98
99
function calculateTouchSpace(a, b) {
100
var dx = b.pageX - a.pageX;
101
var dy = b.pageY - a.pageY;
102
return Math.sqrt(dx * dx + dy * dy);
103
}
104
105
function createMouseListener(element, event, callback) {
106
return createEventListener(element, 'mouse' + event, true, callback)
107
}
108
109
function createTouchListener(element, event, size, callback) {
110
var handler = function(e) {
111
var touches = e.targetTouches;
112
if (touches.length === size) {
113
e.preventDefault();
114
callback(e);
115
}
116
};
117
return createEventListener(element, 'touch' + event, false, handler)
118
}
119
120
function createWheelListener(element, speed, callback) {
121
var handler = function(e) {
122
var position = getRelativePosition(e);
123
callback({
124
x: position.x,
125
y: position.y,
126
delta: -speed * e.deltaY
127
});
128
};
129
return createEventListener(element, 'wheel', true, handler);
130
}
131
132
function createSpaceListener(element, event, callback) {
133
var handler = function(e) {
134
var touches = e.targetTouches;
135
var a = touches[0];
136
var b = touches[1];
137
callback({
138
x: calculateMeanValue(b.pageX, a.pageX),
139
y: calculateMeanValue(b.pageY, a.pageY),
140
space: calculateTouchSpace(a, b)
141
});
142
};
143
return createTouchListener(element, event, 2, handler);
144
}
145
146
function createDeltaListener(element, speed, callback) {
147
var x, y;
148
var zero;
149
var handleTouchStart = function(e) {
150
x = e.x;
151
y = e.y;
152
zero = e.space;
153
};
154
var handleTouchMove = function(e) {
155
var change = Math.round(e.space - zero);
156
callback({
157
x: x,
158
y: y,
159
delta: speed * change
160
});
161
zero = e.space;
162
};
163
var events = [
164
createSpaceListener(element, 'start', handleTouchStart),
165
createSpaceListener(element, 'move', handleTouchMove),
166
createSpaceListener(element, 'end', handleTouchStart) // when triple touch is being ended
167
];
168
return function() {
169
executeArrayFunctions(events);
170
};
171
}
172
173
function createZoomListener(element, min, max, value, speed, callback) {
174
var handler = function(e) {
175
var delta = e.delta + 1.0;
176
var change = keepValueRange(value * delta, min, max);
177
if (change !== value) {
178
callback({
179
x: e.x,
180
y: e.y,
181
zoom: change / value
182
});
183
value = change;
184
}
185
};
186
var events = [
187
createWheelListener(element, 0.2 * speed, handler),
188
createDeltaListener(element, 1.0 * speed, handler)
189
];
190
return function() {
191
executeArrayFunctions(events);
192
};
193
}
194
195
function predictImageRectangle(srcImage, maxWidth, maxHeight) {
196
var srcWidth = srcImage.width;
197
var srcHeight = srcImage.height;
198
var srcRadio = srcWidth / srcHeight;
199
var maxRadio = maxWidth / maxHeight;
200
if (maxRadio < srcRadio) {
201
var newHeight = maxWidth / srcRadio;
202
return {
203
x: 0.0,
204
y: 0.5 * (maxHeight - newHeight),
205
width: maxWidth,
206
height: newHeight
207
};
208
} else {
209
var newWidth = maxHeight * srcRadio;
210
return {
211
x: 0.5 * (maxWidth - newWidth),
212
y: 0.0,
213
width: newWidth,
214
height: maxHeight
215
};
216
}
217
}
218
219
function locateImageElement(containerWidth, containerHeight, imageStyle, imageWidth, imageHeight, computeX, computeY) {
220
var limitX = containerWidth - imageWidth;
221
var limitY = containerHeight - imageHeight;
222
if (imageWidth < containerWidth) {
223
var newX = 0.5 * limitX;
224
imageStyle.left = newX + 'px';
225
} else {
226
var newX = computeX();
227
if (newX < limitX) {
228
newX = limitX;
229
}
230
if (newX > 0.0) {
231
newX = 0.0;
232
}
233
imageStyle.left = newX + 'px';
234
}
235
if (imageHeight < containerHeight) {
236
var newY = 0.5 * limitY;
237
imageStyle.top = newY + 'px';
238
} else {
239
var newY = computeY();
240
if (newY < limitY) {
241
newY = limitY;
242
}
243
if (newY > 0.0) {
244
newY = 0.0;
245
}
246
imageStyle.top = newY + 'px';
247
}
248
}
249
250
function resizeImageElement(imageStyle, imageWidth, imageHeight) {
251
imageStyle.width = imageWidth + 'px';
252
imageStyle.height = imageHeight + 'px';
253
}
254
255
function prepareImagePosition(container, image) {
256
var style = image.style;
257
var handleImageLoad = function() {
258
var rectangle = predictImageRectangle(image, container.clientWidth, container.clientHeight);
259
style.left = rectangle.x + 'px';
260
style.top = rectangle.y + 'px';
261
style.width = rectangle.width + 'px';
262
style.height = rectangle.height + 'px';
263
};
264
return createEventListener(image, 'load', false, handleImageLoad);
265
}
266
267
function prepareImageDragging(container, image) {
268
var dragging = false;
269
var style = image.style;
270
var shiftX, shiftY;
271
var handleMouseDown = function(e) {
272
shiftX = image.offsetLeft - e.clientX;
273
shiftY = image.offsetTop - e.clientY;
274
dragging = true;
275
};
276
var handleMouseUp = function(e) {
277
dragging = false;
278
};
279
var handleMouseMove = function(e) {
280
if (dragging) {
281
var calculateX = function() {
282
return shiftX + e.clientX;
283
};
284
var calculateY = function() {
285
return shiftY + e.clientY;
286
};
287
locateImageElement(container.clientWidth, container.clientHeight, style, image.offsetWidth, image.offsetHeight, calculateX, calculateY);
288
}
289
};
290
var handleTouchStart = function(e) {
291
var touch = e.targetTouches[0];
292
shiftX = image.offsetLeft - touch.clientX;
293
shiftY = image.offsetTop - touch.clientY;
294
dragging = true;
295
};
296
var handleTouchEnd = function(e) {
297
dragging = false;
298
};
299
var handleTouchMove = function(e) {
300
if (dragging) {
301
var touch = e.targetTouches[0];
302
var calculateX = function() {
303
return shiftX + touch.clientX;
304
};
305
var calculateY = function() {
306
return shiftY + touch.clientY;
307
};
308
locateImageElement(container.clientWidth, container.clientHeight, style, image.offsetWidth, image.offsetHeight, calculateX, calculateY);
309
}
310
};
311
var events = [
312
createMouseListener(image, 'down', handleMouseDown),
313
createMouseListener(window, 'move', handleMouseMove),
314
createMouseListener(window, 'up', handleMouseUp),
315
createTouchListener(image, 'start', 1, handleTouchStart),
316
createTouchListener(window, 'move', 1, handleTouchMove),
317
createTouchListener(window, 'end', 1, handleTouchStart), // when double touch is being ended
318
createTouchListener(window, 'end', 0, handleTouchEnd) // when single touch is being ended
319
];
320
return function() {
321
executeArrayFunctions(events);
322
};
323
}
324
325
function prepareImageZooming(container, image, minZoom, maxZoom, zoomValue, zoomSpeed) {
326
var style = image.style;
327
var handleImageZoom = function(e) {
328
var newWidth = e.zoom * image.offsetWidth;
329
var newHeight = e.zoom * image.offsetHeight;
330
var calculateX = function() {
331
return e.x - e.zoom * (e.x - image.offsetLeft);
332
};
333
var calculateY = function() {
334
return e.y - e.zoom * (e.y - image.offsetTop);
335
};
336
locateImageElement(container.clientWidth, container.clientHeight, style, newWidth, newHeight, calculateX, calculateY);
337
resizeImageElement(style, newWidth, newHeight);
338
};
339
return createZoomListener(container, minZoom, maxZoom, zoomValue, zoomSpeed, handleImageZoom);
340
}
341
342
343
// Usage example:
344
345
var container = document.querySelector('#container');
346
var image = document.querySelector('#image');
347
348
prepareImagePosition(container, image);
349
prepareImageDragging(container, image);
350
prepareImageZooming(container, image, 0.5, 6.0, 1.0, 0.005);
351
352
</script>
353
</body>
354
</html>