Введение
Есть проект, который позволяет пользователям добавлять собственные графические изображения их меток на карту. Загрузив собственное изображение, необходимо указать точку привязки — та точка, которой будут соответствовать географические координаты на карте. Указать желаемую точку привязки с точностью до пикселя будет достаточно проблематично, поэтому возникла идея сделать лупу, которая должна была показывать увеличенное изображение метки с использованием интерполяции nearest, чтобы дать возможно выбрать именно тот пиксель изображения, который захочет требовательный пользователь :)
Обычно эффект лупы делают таким образом:
- берем оригинальное изображение;
- делаем его уменьшенную копию;
- при движении курсора на уменьшенной копией, высчитываем координаты оригинального изображения, которые соответствуют положению курсора над первой, и выводим нужный нам кусочек оригинала в нужном нам месте.
Какие способы существуют для создания увеличенного изображения? Посмотрим:
- библиотека GD для PHP;
- ImageMagick;
- помещать изображение в img с увеличенным размером;
- HTML5 Canvas.
Canvas
В HTML5 Canvas есть возможность общаться с изображением на уровне пикселей, т. е. можно создавать различные графические фильтры… да и все что угодно, связанное с обработкой изображений, на чистом JavaScript. Вопрос только в скорости работы, и вот какая она будет — мы узнаем в процессе написания кода.
Работа с изображениями в canvas в общем виде выглядит так:
<!DOCTYPE HTML> <html> <head> <style> body { margin: 0px; padding: 0px; } #myCanvas { border: 1px solid #9C9898; } </style> </head> <body> <canvas id="myCanvas" width="578" height="400"></canvas> <script> var canvas = document.getElementById('myCanvas'); var context = canvas.getContext('2d'); var imageObj = new Image(); imageObj.onload = function() { context.drawImage(imageObj, 69, 50); }; imageObj.src = 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg'; </script> </body> </html>
Пример честно заимствован с сайта www.html5canvastutorials.com
В этом примере изображение «darth-vader.jpg» загружается и отрисовывается на canvas с помощью функции drawImage.
Но нам нужно написать код для увеличения изображения. Почитав документацию к canvas и изучив примеры, код увеличения изображения будет следующим:
function scale_up_nearest(input, scale_factor) { var self = this; self.canvas.width = input.width; self.canvas.height = input.height; var context = self.canvas.getContext('2d'); context.drawImage(input, 0, 0); var inputImageData = context.getImageData(0, 0, self.canvas.width, self.canvas.height); /** * let's scale up the image */ var old_width = self.canvas.width; var old_height = self.canvas.height; var new_width = Math.max(Math.floor(scale_factor*old_width),1); var new_height = Math.max(Math.floor(scale_factor*old_height),1); self.canvas.width = new_width; self.canvas.height = new_height; var outputImageData = context.createImageData(new_width, new_height); var outputImageData_data = outputImageData.data; var inputImageData_data = inputImageData.data; for(var j = 0; j < new_height; j++ ) { for( var i = 0; i < new_width; i++ ) { var i_input = Math.min(Math.round((i-0.5)/scale_factor+0.5), old_width); var j_input = Math.min(Math.round((j-0.5)/scale_factor+0.5), old_height); var new_index = j * new_width * 4 + (i * 4); var old_index = j_input * old_width * 4 + (i_input * 4); outputImageData_data[new_index] = inputImageData_data[old_index]; outputImageData_data[new_index+1] = inputImageData_data[old_index+1]; outputImageData_data[new_index+2] = inputImageData_data[old_index+2]; outputImageData_data[new_index+3] = inputImageData_data[old_index+3]; } } context.putImageData(outputImageData, 0, 0); var dataURL = self.canvas.toDataURL("image/png"); var outputImage = new Image(); outputImage.src = dataURL; return outputImage; }
Для оценки скорости работы этого кода проведем тестирование: запустим код несколько раз в нескольких браузерах, запишем время выполнения и вычислим среднее значение, которое и будет для нас результатом. Исходное изображение будет иметь разрешение 300x379, увеличивать мы его будем в 10 раз (до 3000x3790). Вот что у меня получилось:
Результат очень плачевный в IE9, но не забываем, что мы увеличиваем достаточно крупное изображение в 10 раз.
Попробуем немного упростить задачу. Возможно, нам не нужна возможность увеличивать изображение в n раз, где n — не целое число. Тогда мы можем убрать кучу математических функций вроде Math.round.
После внесенных изменений код стал таким:
function scale_up_nearest(input, scale_factor) { var self = this; self.canvas.width = input.width; self.canvas.height = input.height; var context = self.canvas.getContext('2d'); context.drawImage(input, 0, 0); var inputImageData = context.getImageData(0, 0, self.canvas.width, self.canvas.height); /** * let's scale up the image */ var old_width = self.canvas.width; var old_height = self.canvas.height; var new_width = old_width * scale_factor; var new_height = old_height * scale_factor; self.canvas.width = new_width; self.canvas.height = new_height; var outputImageData = context.createImageData(new_width, new_height); var outputImageData_data = outputImageData.data; var inputImageData_data = inputImageData.data; for(var j = 0; j < new_height; j++ ) { for( var i = 0; i < new_width; i++ ) { var i_input = parseInt(i / scale_factor); var j_input = parseInt(j / scale_factor); var new_index = j * new_width * 4 + (i * 4); var old_index = j_input * old_width * 4 + (i_input * 4); outputImageData_data[new_index] = inputImageData_data[old_index]; outputImageData_data[new_index+1] = inputImageData_data[old_index+1]; outputImageData_data[new_index+2] = inputImageData_data[old_index+2]; outputImageData_data[new_index+3] = inputImageData_data[old_index+3]; } } context.putImageData(outputImageData, 0, 0); var dataURL = self.canvas.toDataURL("image/png"); var outputImage = new Image(); outputImage.src = dataURL; return outputImage; }
Результат тестирования:
В Опере и в IE скорость работы значительно возросла, в Firefox незначительно, а Chrome, к моему удивлению, замедлился почти в два раза.
Попробуем еще чуть чуть оптимизировать скрипт, вынеся некоторые константные выражения за цикл, а также, заменив умножение сдвигом.
Код получился таким:
function scale_up_nearest(input, scale_factor) { var self = this; self.canvas.width = input.width; self.canvas.height = input.height; var context = self.canvas.getContext('2d'); context.drawImage(input, 0, 0); var inputImageData = context.getImageData(0, 0, self.canvas.width, self.canvas.height); /** * let's scale up the image */ var old_width = self.canvas.width; var old_height = self.canvas.height; var new_width = old_width * scale_factor; var new_height = old_height * scale_factor; self.canvas.width = new_width; self.canvas.height = new_height; var outputImageData = context.createImageData(new_width, new_height); /** * precompute some data for optimisation purposes */ var new_width_4_precomp = new_width << 2; var old_width_4_precomp = old_width << 2; var outputImageData_data = outputImageData.data; var inputImageData_data = inputImageData.data; for(var j = 0; j < new_height; j++ ) { for( var i = 0; i < new_width; i++ ) { var i_input = parseInt(i / scale_factor); var j_input = parseInt(j / scale_factor); var new_index = j*new_width_4_precomp + (i << 2); var old_index = j_input*old_width_4_precomp + (i_input << 2); outputImageData_data[new_index++] = inputImageData_data[old_index++]; outputImageData_data[new_index++] = inputImageData_data[old_index++]; outputImageData_data[new_index++] = inputImageData_data[old_index++]; outputImageData_data[new_index] = inputImageData_data[old_index]; } } context.putImageData(outputImageData, 0, 0); var dataURL = self.canvas.toDataURL("image/png"); var outputImage = new Image(); outputImage.src = dataURL; return outputImage; }
Результат практически не изменился, т. е. подобная оптимизация в данном случае не имеет смысла, если вообще имеет в интерпритируемом языке:
Сравнительная гистограмма:
Вывод
Листинг
Файл index.html
<!DOCTYPE html> <html> <head> <title>Scale up test</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script src="imagemagnify.js"></script> <script> $(document).ready(function() { var mag = new magnify(); mag.init('image-wrap', 'image'); }) </script> <style> #image-wrap { float: left; } </style> </head> <body> <div id="image-wrap"> <img src="i/cat.jpeg" id="image" width="300px" height="379px" /> </div> </body> </html>
Файл imagemagnify.js (используется jQuery)
/** * конструктор * @param opt не используется */ var magnify = function(opt) { // canvas html this.canvasLoup = '<canvas style="display: none; position: absolute; z-index: 1000; left: 350px; top: 0; border: 1px solid RGB(0,0,0);" id="canvasLoup"></canvas>'; // the img tag id, set in init function this.imgId = null; // the image from img tag this.img = null; // the resized image this.resizeImg = undefined; this.originalImageLoaded = false; // scale up factor, default: 5 this.scale = 10; // html5 canvas 2d context this.context = undefined; // html5 canvas this.canvas = undefined; // size of popup window, where you can see resized image this.ramka = 300; // trigger for animation this.isAnim = false; // trigger, if crosshair image loaded this.crosshairImageLoaded = false; // crosshair image, that shows in popup window this.crosshairImage = null; this.containerId = null; this.container = null; }; magnify.prototype = { /** * destroy all data in the class to free resources * @param event not using */ destroy: function(event) { $('#canvasLoup').remove(); if( this.crosshairImage ) delete this.crosshairImage; this.crosshairImageLoaded = false; if( this.resizeImg ) delete this.resizeImg; $('#'+this.containerId).unbind('mousemove', self.mousemove) .unbind('mouseout', self.mouseout) .unbind('mouseover', self.mouseover); if( this.img ) delete this.img; }, /** * for realizing mousewheel function * really not using at this time * @param delta */ over: function(delta) { if( delta > 0 ) { if( window.magnify.scale < 6 ) { window.magnify.scale ++; window.magnify.init(null); } } else { if( window.magnify.scale > 1) { window.magnify.scale --; window.magnify.init(null); } } }, /** * for profiling this class * @param get_as_float */ microtime: function(get_as_float) { var now = new Date().getTime() / 1000; var s = parseInt(now); return (get_as_float) ? now : (Math.round((now - s) * 1000) / 1000) + ' ' + s; }, /** * init function, must be called after object has been created * @param id - img tag id, without # or other jquery elements, only similar text */ init: function(image_container_id, image_id) { if( image_id ) { this.imgId = image_id; } else return false; if( image_container_id ) { this.containerId = image_container_id; this.container = $('#'+this.containerId); } else return false; var self = this; /** * loading crosshair image */ self.crosshairImage = new Image(); self.crosshairImage.src = 'i/crosshair.png'; self.crosshairImage.onload = function() { self.crosshairImageLoaded = true; }; /** * adding canvas tag to container */ this.container.append(self.canvasLoup); /** * initializing 2d context */ self.canvas = document.getElementById('canvasLoup'); self.context = self.canvas.getContext('2d'); /** * getting original image from img tag, accessing by img id */ var original_image = document.getElementById(this.imgId); /** * getting jquery object for img tag, for binding events to it */ this.img = $('#'+self.imgId); /** * resize image */ this.resizeImg = null; var id = setTimeout(function() { if( original_image.complete) { clearTimeout(id); self.resizeImg = self.scale_up_nearest(original_image, self.scale); self.originalImageLoaded = true; /** * set canvas size */ self.canvas.width = self.ramka; self.canvas.height = self.ramka; } }, 50); /** * bind events */ $('#'+image_container_id).bind('mouseover', {self: this}, self.mouseover) .bind('mouseout', {self: this}, self.mouseout) .bind('mousemove', {self: this}, self.mousemove); }, /** * draw part of the resized image to context at x and y * @param x * @param y */ draw: function(x,y) { if( this.originalImageLoaded ) { this.context.rect(0, 0, this.canvas.width, this.canvas.height); this.context.fillStyle = 'white'; this.context.fill(); this.context.drawImage(this.resizeImg, -(x)*this.scale + (this.ramka >> 1), -(y)*this.scale + (this.ramka >> 1)); if( this.crosshairImageLoaded ) { this.context.drawImage( this.crosshairImage, 0, 0, this.crosshairImage.width, this.crosshairImage.height, (this.canvas.width >> 1) - (this.crosshairImage.width >> 1), (this.canvas.height >> 1) - (this.crosshairImage.height >> 1) - 2, this.crosshairImage.width, this.crosshairImage.height ); } } }, mouseover: function(event) { event.data.self.show(); }, mouseout: function(event) { event.data.self.hide(); }, mousemove: function(event) { $(this).css('cursor', 'crosshair'); var imgOffset = $(event.data.self.img).offset(); var position = $(event.data.self.container).css('position'); var canvasOffsetX = event.pageX + 10; var canvasOffsetY = event.pageY - event.data.self.ramka - 10; if( position == 'relative' || position == 'absolute' ) { canvasOffsetX -= imgOffset['left']; canvasOffsetY -= imgOffset['top']; } $('#canvasLoup') .css({left:canvasOffsetX, top: canvasOffsetY}); event.data.self.draw(event.pageX - imgOffset['left'], event.pageY - imgOffset['top']); }, show: function() { var self = this; if( this.isAnim ) { $('#canvasLoup').stop(true, true); } this.isAnim = true; $('#canvasLoup').fadeIn(500, function() { self.isAnim = false }); }, hide: function() { var self = this; if( this.isAnim ) { $('#canvasLoup').stop(true, true); } this.isAnim = true; $('#canvasLoup').fadeOut(500, function() { self.isAnim = false }); }, scale_up_nearest: function(input, scale_factor) { var self = this; self.canvas.width = input.width; self.canvas.height = input.height; var context = self.canvas.getContext('2d'); context.drawImage(input, 0, 0); var inputImageData = context.getImageData(0, 0, self.canvas.width, self.canvas.height); /** * let's scale up the image */ var old_width = self.canvas.width; var old_height = self.canvas.height; var new_width = old_width * scale_factor; var new_height = old_height * scale_factor; self.canvas.width = new_width; self.canvas.height = new_height; var outputImageData = context.createImageData(new_width, new_height); /** * precompute some data for optimisation purposes */ var new_width_4_precomp = new_width << 2; var old_width_4_precomp = old_width << 2; var outputImageData_data = outputImageData.data; var inputImageData_data = inputImageData.data; for(var j = 0; j < new_height; j++ ) { for( var i = 0; i < new_width; i++ ) { var i_input = parseInt(i / scale_factor); var j_input = parseInt(j / scale_factor); var new_index = j*new_width_4_precomp + (i << 2); var old_index = j_input*old_width_4_precomp + (i_input << 2); outputImageData_data[new_index++] = inputImageData_data[old_index++]; outputImageData_data[new_index++] = inputImageData_data[old_index++]; outputImageData_data[new_index++] = inputImageData_data[old_index++]; outputImageData_data[new_index] = inputImageData_data[old_index++]; } } context.putImageData(outputImageData, 0, 0); var dataURL = self.canvas.toDataURL("image/png"); var outputImage = new Image(); outputImage.src = dataURL; return outputImage; } };
Комментариев нет:
Отправить комментарий