仿谷歌超大图局部加载实现

为了跟公司的人演示一个软件从思考到开发的过程,我花了一天的时间,实现了这部分功能。

第一步,搞清原理
什么是局部加载大图,如果有一张体积超大的图,一次性传送给客户端那几乎是一次糟糕的体验。
1,就算一次传给用户,用户浏览器迫于分辨率和窗口所见局限,也有可能看不清楚。特别是需要看清图上的文字。
2,用户要等很久才能看到图片长啥样。

3,服务器带宽浪费。

4,客户端内存可能爆炸(假如这张图有10G这么大)


如果能只传送用户可见区域的部分图片给用户,等用户拖动时再加载其他部分,这将给用户一个非常有意思的体验。就像我们经常使用的百度地图,地图是非常大的图片,如果百度服务器一次性将地图传给用户,那就不可想象。

那么,我们只要知道用户正在请求图片的哪部分,我们将图片的那部分传过去就好了,其实我们可以用一些简单的数学计算,加上服务器上的GD处理,很容易办到。但如果这样,那么用户的每次请求,服务器都将进行大量的实时演算,消耗巨大的资源来处理图片,这将是可怕的。那么我们很容易想到,预先让服务器把图片的每个缩放等级处理好,再缓存下来。下次直接读取磁盘的缓存则可以解决。

我们可以通过简单的计算,把图片拆分成一个网格。再利用前端JS动态读取每个格子的图片即可。

第二步,理清系统工作流程

那么我们很快就可以画出或在脑子里想到如下图所示的系统流程。用户上传一张图片之后,将这个图片加入任务队列,后台有个守护进程始终在查询新的队列任务,当发现有新的任务需要处理时,则可把它处理成网格图片。



第三步,逐个模块实现
其他模块就不用说了,都很简单,重点在图形处理与客户端显示部分。首先我们确立缩放等级的公式,定为 块尺寸 *  2^level,这个公式的缩放可以得到比较好的结果。

在服务器上,我们首先通过图片的尺寸,计算出它的最大 缩放等级 ceil(  sqrt( width / 块尺寸 ) ) , 再把每个缩放等级按照 块尺寸 分割成图则可。如下图(4个缩放等级):


(程序将每个缩放等级的图,自动分发在相对应的文件夹)


(再在每个文件夹里,自动把图拆分成块储存)


现在,只需要前端能准确知道,当前屏幕正在显示哪几块碎片,就可以将他们读取出来。 

第四步,查看结果

结果如下图:


(这是缩略的图像)

(放大后加载缩放等级更高的局部)



(拖动后,我们可以看到之前没有在屏幕中的部分是未被加载的 ) 



(半秒后,所见区域的其他部分被加载了)



后端的代码稍微多一点,就不写在这里了,有原理就应该很容易写出来。以下为JS的源码(安卓\IOS客户端,也可以使用这个原理来实现):

var imageMap = {};

imageMap.container = function(domId,imageData,options){
var self = this;
this.main = $('#'+domId);
this.mapScreen = $('<div></div>');
this.mapContainer = $('<div></div>');
this.mapMasker = $('<div></div>');
this.structUrl = 'i/';
this.thumbUrl = this.structUrl+'thumb.png';
this.mapThumb = $('<img src="'+this.thumbUrl+'" style="position:absolute; left:0; top:0;" />');
this.screenWidth = 0;
this.screenHeight = 0;
this.structSize = 256;
this.renderImage = {};
this.containerLeft = 0;
this.containerTop = 0;
this.imageData = {};
this.level = 2;
this.minLevel = 1;
this.maxLevel = 4;
this.imageWidth = 0;
this.imageHeight = 0;
this.imageRow = 0;
this.imageCol = 0;
this.effectStatus = false;
this.imageData = imageData;
this.main.append(this.mapScreen);
this.mapScreen.append(this.mapContainer).append(this.mapMasker);
this.mapContainer.append(this.mapThumb);
this.mapScreen.css({overflow:'hidden',position:'relative',width:'100%',height:'100%'});
this.mapMasker.css({'position':'absolute',left:0,top:0});
this.mapContainer.css({width:0,height:0,position:'relative',left:0,top:0});
this.initParam();
this.initScreen();
this.enableDrag();
this.enableScroll();
this.enableResize();
var centerX,centerY;
centerX = (this.screenWidth - this.imageWidth) / 2;
centerY = (this.screenHeight - this.imageHeight) / 2;
this.move(centerX, centerY);
};

/**
 * 初始化参数
 */
imageMap.container.prototype.initParam = function(){
if( !this.imageData ){
console.error('no image data');
return ;
}
var data = this.imageData[this.level];
if(data){
this.imageWidth = data.width;
this.imageHeight = data.height;
this.imageRow = data.row;
this.imageCol = data.col;
}else{
console.error('no image level data!');
}
};


/**
 * 初始化屏幕
 */
imageMap.container.prototype.initScreen = function(){
this.screenWidth = this.main.width();
this.screenHeight = this.main.height();
this.mapMasker.css({width:this.screenWidth,height:this.screenHeight});
this.renderScreen();
};


/**
 * 允许改变尺寸
 */
imageMap.container.prototype.enableResize = function(){
var self = this;
var checkScreenChange = function(){
setTimeout(function(){
if( self.main.width() != self.screenWidth || self.main.height() != self.screenHeight ){
self.initScreen();
}
checkScreenChange();
},500);
};
checkScreenChange();
};

/**
 * 允许使用滚动缩放
 */
imageMap.container.prototype.enableScroll = function(){
var self = this;
var mousewheelEvent=(/Firefox/i.test(navigator.userAgent))?"DOMMouseScroll": "mousewheel";
this.mapMasker.bind(mousewheelEvent,function(e){
var delta = e.originalEvent.detail ?-e.originalEvent.detail: e.originalEvent.wheelDelta;
var x=e.originalEvent.offsetX,y=e.originalEvent.offsetY;
delta = delta>0?1:-1;
self.zoom(delta,x,y);
return false;
});
};


/**
 * 允许拖动
 */
imageMap.container.prototype.enableDrag = function(){
var self = this;
var down_x,down_y,down_left,down_top,render_time,render_status;
var mouseMove = function(e){
var offsetX = e.pageX - down_x;
var offsetY = e.pageY - down_y;
self.containerLeft = down_left+offsetX;
self.containerTop = down_top+offsetY;
self.position();
render_status = 0;
if( render_status == 0 ){
render_status = 1;
render_time = setTimeout(function(){
self.renderScreen();
render_status = 0;
},500);
}
};
var mouseUp = function(e){
$(window).unbind('mousemove',mouseMove)
.unbind('mouseup',mouseUp);
self.mapMasker.css('cursor','auto');
}
this.mapMasker.bind('mousedown',function(e){
down_x = e.pageX;
down_y = e.pageY;
down_left = self.containerLeft;
down_top = self.containerTop;
$(window).bind('mousemove',mouseMove);
$(window).bind('mouseup',mouseUp);
self.mapMasker.css('cursor','move');
});
};

/**
 * 把场景坐标转换成图片坐标
 */
imageMap.container.prototype.translateScreenPosToImagePos = function(x,y){
var imageX = x - this.containerLeft;
var imageY = y - this.containerTop;
return {x:imageX,y:imageY};
};

/**
 * 把图片坐标转换成场景坐标
 */
imageMap.container.prototype.translateImagePosToScreenPos = function(x,y){
var screenX = x + this.containerLeft;
var screenY = y + this.containerTop;
return {x:screenX,y:screenY};
};

/**
 * 把一个等级的图片坐标转换成另一个等级的图片坐标
 */
imageMap.container.prototype.translateLevelPos = function(x,y,srcLevel,targetLevel){
var srcData = this.imageData[srcLevel];
var targetData = this.imageData[targetLevel];
var scaleX = targetData.width / srcData.width;
var targetX = x * scaleX;
var scaleY = targetData.height / srcData.height;
var targetY = y * scaleY;
return {x:targetX,y:targetY};
};

/**
 * 缩放
 */
imageMap.container.prototype.zoom = function(zoom,centerX,centerY){
//if(this.effectStatus) return;
if(zoom == -1 && this.level-1 < this.minLevel ) return ;
if(zoom == 1 && this.level+1 > this.maxLevel) return ;
var l,t,targetPos,pos,screenPos,self = this,sourceLevel = this.level;
//this.effectStatus = true;
if(zoom == -1) this.level--; else this.level++;
if( !centerX ) centerX = this.screenWidth / 2;
if( !centerY ) centerY = this.screenHeight / 2;
pos = this.translateScreenPosToImagePos(centerX, centerY);
pos.x = pos.x>0?pos.x:0;
pos.x = pos.x>this.imageWidth?this.imageWidth:pos.x;
pos.y = pos.y>0?pos.y:0;
pos.y = pos.y>this.imageHeight?this.imageHeight:pos.y;
screenPos = this.translateImagePosToScreenPos(pos.x,pos.y);
targetPos = this.translateLevelPos(pos.x, pos.y, sourceLevel, this.level);
targetScreenPos = this.translateImagePosToScreenPos(targetPos.x, targetPos.y);
l = screenPos.x - targetScreenPos.x;
t = screenPos.y - targetScreenPos.y;
this.containerLeft += l;
this.containerTop += t;
this.position();
this.initParam();
this.mapThumb.css({width:self.imageWidth,height:self.imageHeight});
this.clearRender();
self.renderScreen();
//self.effectStatus = false;
// this.mapContainer.animate({left:this.containerLeft,top:this.containerTop},200);
// this.mapThumb.animate({width:self.imageWidth,height:self.imageHeight},200,function(){
// self.initScreen();
// self.effectStatus = false;
// });
};


/**
 * 移动到屏幕位置
 */
imageMap.container.prototype.move = function(screenX,screenY){
this.containerLeft = screenX;
this.containerTop = screenY;
this.position();
this.renderScreen();
};


/**
 * 定位
 */
imageMap.container.prototype.position = function(){
this.mapContainer.css( 'left', this.containerLeft );
this.mapContainer.css( 'top', this.containerTop );
};


/**
 * 渲染屏幕
 */
imageMap.container.prototype.renderScreen = function(){
if( this.imageWidth == 0 || this.imageHeight == 0 ) return ;
var left = this.containerLeft;
var top = this.containerTop;
var col_start_position = -left <= 0 ? 1 : -left;
var col_start = Math.ceil( col_start_position / this.structSize ) - 1;
var col_end = Math.ceil( (this.screenWidth - left) / this.structSize );
var row_start_position = -top <= 0 ? 1 : -top;
var row_start = Math.ceil( row_start_position / this.structSize ) - 1;
var row_end = Math.ceil( (this.screenHeight - top) / this.structSize );
if(col_end > this.imageCol) col_end = this.imageCol;
if(row_end > this.imageRow) row_end = this.imageRow;
for(var r=row_start; r<row_end; r++){
for(var c=col_start; c<col_end; c++){
this.createImage( r ,c );
}
}
this.mapThumb.width( this.imageWidth ).height( this.imageHeight );
}


/**
 * 创建图片
 */
imageMap.container.prototype.createImage = function(r,c){
var key = r + '_' + c;
if( this.renderImage[ key ] ) return ;
var im = $('<img class="imageMap_struct" src="'+this.structUrl+this.level+'/'+r+'_'+c+'.png" />');
im.css({
position : 'absolute',
left : c * this.structSize,
top : r * this.structSize,
});
im.hide();
im.one('load',function(){
$(this).fadeIn('slow');
});
im.appendTo(this.mapContainer);
this.renderImage[ key ] = true;
};


/**
 * 清空渲染
 */
imageMap.container.prototype.clearRender = function(){
this.mapContainer.find('.imageMap_struct').stop().remove();
this.renderImage = {};
};

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值