Введение
Есть проект, который позволяет пользователям добавлять собственные графические изображения их меток на карту. Загрузив собственное изображение, необходимо указать точку привязки — та точка, которой будут соответствовать географические координаты на карте. Указать желаемую точку привязки с точностью до пикселя будет достаточно проблематично, поэтому возникла идея сделать лупу, которая должна была показывать увеличенное изображение метки с использованием интерполяции 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;
}
};




Комментариев нет:
Отправить комментарий