ImageZoom 图片放大镜效果(多功能扩展篇)


Posted in Javascript onApril 14, 2010

主要扩展了原图和显示框的展示模式,有以下几种模式:
"follow" 跟随模式:显示框能跟随鼠标移动的效果;
"handle" 拖柄模式:原图上有一个拖柄来标记显示范围;
"cropper" 切割模式:原图用不透明的来标记显示范围,其他部分用半透明显示;
"handle-cropper" 拖柄切割模式:拖柄模式和切割模式的混合版,同时用透明度和拖柄来标记显示范围。
当然更多的扩展等待你的想象力来发掘。
兼容:ie6/7/8, firefox 3.6.2, opera 10.51, safari 4.0.4, chrome 4.1

程序说明

【扩展模式】

上次ImagesLazyLoad使用了继承做扩展,这次用插件的形式来做扩展。

先看看基础模式,这些模式是保存在ImageZoom._MODE中的,类似这样的结构:

ImageZoom._MODE = { 
模式名: { 
options: { 
... 
}, 
methods: { 
init: function() { 
... 
}, 
... 
} 
}, 
... 
}

其中模式名就是基础模式的名字,options是可选参数扩展,methods是程序结构的扩展。
基础模式包含"follow", "handle"和"cropper"模式,后面再详细介绍。
methods包含要扩展的钩子程序,是扩展的主要部分。
ps:这里说的模式不是“设计模式”里面的模式。

扩展需要在程序初始化时进行,要放在_initialize程序之前执行。
为了不影响原程序的结构,这里用织入法在_initialize之前插入一段程序:

ImageZoom.prototype._initialize = (function(){ 
var init = ImageZoom.prototype._initialize, 
...; 
return function(){ 
... 
init.apply( this, arguments ); 
} 
})();

原理就是先保存原来的函数,插入一段程序组成新函数,然后重新替换原来的函数。

考虑到组合基础模式的情况,使用了一个对象保存真正使用的模式:

mode = ImageZoom._MODE, 
modes = { 
"follow": [ mode.follow ], 
"handle": [ mode.handle ], 
"cropper": [ mode.cropper ], 
"handle-cropper": [ mode.handle, mode.cropper ] 
};

可以看到"handle-cropper"模式其实就是"handle"和"cropper"的组合模式。

插入的程序的主要任务是根据设定好的基础模式,进行扩展:

var options = arguments[2]; 
if ( options && options.mode && modes[ options.mode ] ) { 
$$A.forEach( modes[ options.mode ], function( mode ){ 
$$.extend( options, mode.options, false ); 
$$A.forEach( mode.methods, function( method, name ){ 
$$CE.addEvent( this, name, method ); 
}, this ); 
}, this ); 
}

首先扩展options可选参数对象,由于可选参数是第三个参数,所以用arguments[2]获取。
extend的第三个参数设为false,说明不重写相同属性,即保留自定义的属性值。
然后把methods里面的方法作为钩子函数逐个添加到程序中。

methods可以包含init, load, start, repair, move, end, dispose这几个方法,分别对应ImageZoom中初始化、加载、开始、修正、移动、结束和销毁事件。
在扩展时,不同的事件执行不同的任务:
init初始化函数:用来设置扩展需要的属性,注意这些属性不要跟ImageZoom本身的属性冲突了,即重名。
load加载函数:图片加载完成,相关参数也设置完成,主要做执行放大效果前的准备工作。
start开始函数:触发放大效果时执行的。
repair修正函数:用于修正大图定位的坐标值。
move移动函数:触发放大效果后,鼠标移动时执行的。
end结束函数就:结束放大效果时执行的。
dispose销毁函数:在移除程序时清理程序的。
ps:具体位置参考ImageZoom中使用$$CE.fireEvent的部分。

可以看到这里用了织入法(weave)和钩子法(hook)对程序做扩展。
织入法是一种aop,可以在不改变原程序的基础上进行扩展,但只能在函数前面或后面加入程序。
而钩子法必须在原程序中设置好对应钩子才能配合使用,但位置相对灵活。

【跟随模式】

在"follow"跟随模式中,进行放大时,显示框会跟随鼠标移动,就像拿着放大镜看的效果。

首先显示框要绝对定位,要实现显示框跟随鼠标移动,只要在move中设置对应的left/top就行了:

var style = this._viewer.style;
style.left = e.pageX - this._repairFollowLeft + "px";
style.top = e.pageY - this._repairFollowTop + "px";
其中pageX/pageY是鼠标当前的坐标,_repairFollowLeft/_repairFollowTop是坐标的修正参数。

修正参数是在load中设置的,如果显示框隐藏的话,用上一篇获取显示范围的方法获取参数:

if ( !viewer.offsetWidth ) { 
styles = { display: style.display, visibility: style.visibility }; 
$$D.setStyle( viewer, { display: "block", visibility: "hidden" }); 
} 
... 
if ( styles ) { $$D.setStyle( viewer, styles ); }

为了跟随时,让鼠标固定在显示框中心位置,先根据显示框的offsetWidth/offsetHeight修正参数:
this._repairFollowLeft = viewer.offsetWidth / 2; 
this._repairFollowTop = viewer.offsetHeight / 2;

如果显示框的offsetParent不是body,还需要根据offsetParent修正坐标:
if ( !/BODY|HTML/.test( viewer.offsetParent.nodeName ) ) { 
var parent = viewer.offsetParent, rect = $$D.rect( parent ); 
this._repairFollowLeft += rect.left + parent.clientLeft; 
this._repairFollowTop += rect.top + parent.clientTop; 
}

ps:在Maxthon测试时发现body子元素的offsetParent不是body而是html。

为了在移除程序后,能恢复显示框的样式,在load程序中做了样式的备份:

var viewer = this._viewer, style = viewer.style, styles; 
this._stylesFollow = { 
left: style.left, top: style.top, position: style.position 
};

并在dispose中恢复:

$$D.setStyle( this._viewer, this._stylesFollow );

现在已经达到了基本的效果,但由于大图移动范围的限制,当鼠标移动到接近边界时,大图就卡住不会动了。
为了实现在鼠标移动时,大图会持续变化的效果,在repair中修正了移动坐标:

pos.left = ( viewerWidth / 2 - pos.left ) * ( viewerWidth / zoom.width - 1 ); 
pos.top = ( viewerHeight / 2 - pos.top ) * ( viewerHeight / zoom.height - 1 );

原理稍有些复杂,以水平坐标为例,先看下图:
ImageZoom 图片放大镜效果(多功能扩展篇)
大方框代表大图对象,小方框代表显示框。
当前位置是根据鼠标坐标得到的实际显示的位置,目标位置想要实现效果的位置。
有一些物理或几何知识应该明白这个等式:x / y = m / n
可以推出:y = x * n / m = x * ( zoom.width - viewerWidth ) / zoom.height
x当前坐标可以通过pos.left来得到:x = viewerWidth / 2 - pos.left
最后得到:left = -y = ( viewerWidth / 2 - pos.left ) * ( viewerWidth / zoom.width - 1 )
垂直坐标也差不多。

【拖柄模式】

拖柄是一个层,在原图上面,用来表示显示范围在原图的位置和范围。
显示范围可以根据_rangeWidth/_rangeHeight获取。
至于位置的指定可以根据鼠标坐标或大图定位坐标来设置。
如果鼠标坐标的话还必须做其他处理,例如范围控制,而根据大图定位坐标相对就方便准确得多,程序也是用后面一个方法。

首先在init定义一个_handle拖柄对象:

var handle = $$( this.options.handle ); 
if ( !handle ) { 
var body = document.body; 
handle = body.insertBefore(this._viewer.cloneNode(false), body.childNodes[0]); 
handle.id = ""; 
handle["_createbyhandle"] = true; 
} 
$$D.setStyle( handle, { padding: 0, margin: 0, display: "none" } );

如果没有自定义拖柄对象,会复制显示框作为拖柄对象。
对于自动生成的拖柄对象,会添加"_createbyhandle"属性作标记,方便在dispose中移除。

在load时,设置拖柄样式:

$$D.setStyle( handle, { 
position: "absolute", 
width: this._rangeWidth + "px", 
height: this._rangeHeight + "px", 
display: "block", 
visibility: "hidden" 
});

绝对定位是必须的,并根据_rangeWidth/_rangeHeight设置尺寸。
设置display和visibility是为了下面获取参数。

先根据原图坐标获取修正参数:

this._repairHandleLeft = rect.left + this._repairLeft - handle.clientLeft;
this._repairHandleTop = rect.top + this._repairTop - handle.clientTop;

和跟随模式类似,也要做offsetParent定位的修正:

if ( handle.offsetParent.nodeName.toUpperCase() != "BODY" ) { 
var parent = handle.offsetParent, rect = $$D.rect( parent ); 
this._repairHandleLeft -= rect.left + parent.clientLeft; 
this._repairHandleTop -= rect.top + parent.clientTop; 
}

然后重新隐藏:

$$D.setStyle( handle, { display: "none", visibility: "visible" });

在start时,显示拖柄对象。

在move时,根据大图定位坐标设置拖柄定位:

var style = this._handle.style, scale = this._scale; 
style.left = Math.ceil( this._repairHandleLeft - x / scale ) + "px"; 
style.top = Math.ceil( this._repairHandleTop - y / scale ) + "px";

在end时,隐藏拖柄对象。

【切割模式】

“切割”就是选择的部分全透明,其他部分半透明的效果。
主要通过clip来实现,具体原理可以看图片切割效果。

为了实现切割效果,需要在init中新建一个_cropper切割层:

var body = document.body, 
cropper = body.insertBefore(document.createElement("img"), body.childNodes[0]); 
cropper.style.display = "none";

并在load中设置这个切割层:
cropper.src = image.src; 
cropper.width = image.width; 
cropper.height = image.height; 
$$D.setStyle( cropper, { 
position: "absolute", 
left: rect.left + this._repairLeft + "px", 
top: rect.top + this._repairTop + "px" 
});

差不多是复制一个原图对象,并且绝对定位到原图对象上面。

在start时,显示切割层,并根据透明度设置原图为半透明状态。

在move时,根据大图移动的坐标设置切割层要clip的范围:

var w = this._rangeWidth, h = this._rangeHeight, scale = this._scale; 
x = Math.ceil( -x / scale ); y = Math.ceil( -y / scale ); 
this._cropper.style.clip = "rect(" + y + "px " + (x + w) + "px " + (y + h) + "px " + x + "px)";

在end时,隐藏切割层,并重新设置原图为不透明,来恢复原来的状态。

还要记得在dispose中移除切割层。

使用技巧

需要扩展的效果时才需要添加这个扩展程序。

可自行对ImageZoom._MODE进行扩展,扩展后记得在modes添加对应模式。

可以组合多个基础模式同时使用,具体参考"handle-cropper"模式。

使用说明

使用方法跟ImageZoom差不多,只是多了一个可选参考mode设置显示模式。
使用"handle"模式时,可选参数的"handle"属性可以设置拖柄对象。
使用"cropper"模式时,可选参数的"opacity"属性可以设置透明度。
使用"handle-cropper"模式时,以上两个参数都可以使用。
程序源码

ImageZoom._MODE = { 
//跟随 
"follow": { 
methods: { 
init: function() { 
this._stylesFollow = null;//备份样式 
this._repairFollowLeft = 0;//修正坐标left 
this._repairFollowTop = 0;//修正坐标top 
}, 
load: function() { 
var viewer = this._viewer, style = viewer.style, styles; 
this._stylesFollow = { 
left: style.left, top: style.top, position: style.position 
}; 
viewer.style.position = "absolute"; 
//获取修正参数 
if ( !viewer.offsetWidth ) {//隐藏 
styles = { display: style.display, visibility: style.visibility }; 
$$D.setStyle( viewer, { display: "block", visibility: "hidden" }); 
} 
//修正中心位置 
this._repairFollowLeft = viewer.offsetWidth / 2; 
this._repairFollowTop = viewer.offsetHeight / 2; 
//修正offsetParent位置 
if ( !/BODY|HTML/.test( viewer.offsetParent.nodeName ) ) { 
var parent = viewer.offsetParent, rect = $$D.rect( parent ); 
this._repairFollowLeft += rect.left + parent.clientLeft; 
this._repairFollowTop += rect.top + parent.clientTop; 
} 
if ( styles ) { $$D.setStyle( viewer, styles ); } 
}, 
repair: function(e, pos) { 
var zoom = this._zoom, 
viewerWidth = this._viewerWidth, 
viewerHeight = this._viewerHeight; 
pos.left = ( viewerWidth / 2 - pos.left ) * ( viewerWidth / zoom.width - 1 ); 
pos.top = ( viewerHeight / 2 - pos.top ) * ( viewerHeight / zoom.height - 1 ); 
}, 
move: function(e) { 
var style = this._viewer.style; 
style.left = e.pageX - this._repairFollowLeft + "px"; 
style.top = e.pageY - this._repairFollowTop + "px"; 
}, 
dispose: function() { 
$$D.setStyle( this._viewer, this._stylesFollow ); 
} 
} 
}, 
//拖柄 
"handle": { 
options: {//默认值 
handle: ""//拖柄对象 
}, 
methods: { 
init: function() { 
var handle = $$( this.options.handle ); 
if ( !handle ) {//没有定义的话用复制显示框代替 
var body = document.body; 
handle = body.insertBefore(this._viewer.cloneNode(false), body.childNodes[0]); 
handle.id = ""; 
handle["_createbyhandle"] = true;//生成标识用于移除 
} 
$$D.setStyle( handle, { padding: 0, margin: 0, display: "none" } ); this._handle = handle; 
this._repairHandleLeft = 0;//修正坐标left 
this._repairHandleTop = 0;//修正坐标top 
}, 
load: function() { 
var handle = this._handle, rect = this._rect; 
$$D.setStyle( handle, { 
position: "absolute", 
width: this._rangeWidth + "px", 
height: this._rangeHeight + "px", 
display: "block", 
visibility: "hidden" 
}); 
//获取修正参数 
this._repairHandleLeft = rect.left + this._repairLeft - handle.clientLeft; 
this._repairHandleTop = rect.top + this._repairTop - handle.clientTop; 
//修正offsetParent位置 
if ( !/BODY|HTML/.test( handle.offsetParent.nodeName ) ) { 
var parent = handle.offsetParent, rect = $$D.rect( parent ); 
this._repairHandleLeft -= rect.left + parent.clientLeft; 
this._repairHandleTop -= rect.top + parent.clientTop; 
} 
//隐藏 
$$D.setStyle( handle, { display: "none", visibility: "visible" }); 
}, 
start: function() { 
this._handle.style.display = "block"; 
}, 
move: function(e, x, y) { 
var style = this._handle.style, scale = this._scale; 
style.left = Math.ceil( this._repairHandleLeft - x / scale ) + "px"; 
style.top = Math.ceil( this._repairHandleTop - y / scale ) + "px"; 
}, 
end: function() { 
this._handle.style.display = "none"; 
}, 
dispose: function() { 
if( "_createbyhandle" in this._handle ){ document.body.removeChild( this._handle ); } 
this._handle = null; 
} 
} 
}, 
//切割 
"cropper": { 
options: {//默认值 
opacity: .5//透明度 
}, 
methods: { 
init: function() { 
var body = document.body, 
cropper = body.insertBefore(document.createElement("img"), body.childNodes[0]); 
cropper.style.display = "none"; 
this._cropper = cropper; 
this.opacity = this.options.opacity; 
}, 
load: function() { 
var cropper = this._cropper, image = this._image, rect = this._rect; 
cropper.src = image.src; 
cropper.width = image.width; 
cropper.height = image.height; 
$$D.setStyle( cropper, { 
position: "absolute", 
left: rect.left + this._repairLeft + "px", 
top: rect.top + this._repairTop + "px" 
}); 
}, 
start: function() { 
this._cropper.style.display = "block"; 
$$D.setStyle( this._image, "opacity", this.opacity ); 
}, 
move: function(e, x, y) { 
var w = this._rangeWidth, h = this._rangeHeight, scale = this._scale; 
x = Math.ceil( -x / scale ); y = Math.ceil( -y / scale ); 
this._cropper.style.clip = "rect(" + y + "px " + (x + w) + "px " + (y + h) + "px " + x + "px)"; 
}, 
end: function() { 
$$D.setStyle( this._image, "opacity", 1 ); 
this._cropper.style.display = "none"; 
}, 
dispose: function() { 
document.body.removeChild( this._cropper ); 
this._cropper = null; 
} 
} 
} 
} 
ImageZoom.prototype._initialize = (function(){ 
var init = ImageZoom.prototype._initialize, 
mode = ImageZoom._MODE, 
modes = { 
"follow": [ mode.follow ], 
"handle": [ mode.handle ], 
"cropper": [ mode.cropper ], 
"handle-cropper": [ mode.handle, mode.cropper ] 
}; 
return function(){ 
var options = arguments[2]; 
if ( options && options.mode && modes[ options.mode ] ) { 
$$A.forEach( modes[ options.mode ], function( mode ){ 
//扩展options 
$$.extend( options, mode.options, false ); 
//扩展钩子 
$$A.forEach( mode.methods, function( method, name ){ 
$$CE.addEvent( this, name, method ); 
}, this ); 
}, this ); 
} 
init.apply( this, arguments ); 
} 
})();

在线演示地址http://demo.3water.com/js/ImageZoom_ext/ImageZoom_ext.htm
打包下载地址https://3water.com/jiaoben/25809.html
出处:http://www.cnblogs.com/cloudgamer/
Javascript 相关文章推荐
使用jquery mobile做幻灯播放效果实现步骤
Jan 04 Javascript
有关于JS辅助函数inherit()的问题
Apr 07 Javascript
JS验证控制输入中英文字节长度(input、textarea等)具体实例
Jun 21 Javascript
js清空form表单中的内容示例
May 20 Javascript
JS实现一个按钮的方法
Feb 05 Javascript
JavaScript原生对象常用方法总结(推荐)
May 13 Javascript
jquery hover 不停闪动问题的解决方法(亦为stop()的使用)
Feb 10 Javascript
JavaScript创建对象_动力节点Java学院整理
Jun 27 Javascript
ionic2屏幕适配实现适配手机、平板等设备的示例代码
Aug 11 Javascript
View.post() 不靠谱的地方你知道多少
Aug 29 Javascript
vue-router实现组件间的跳转(参数传递)
Nov 07 Javascript
Vue接口封装的完整步骤记录
May 14 Vue.js
JavaScript中的prototype使用说明
Apr 13 #Javascript
Js 刷新框架页的代码
Apr 13 #Javascript
百度Popup.js弹出框进化版 拖拽小框架发布 兼容IE6/7/8,Firefox,Chrome
Apr 13 #Javascript
JavaScript在IE和Firefox(火狐)的不兼容问题解决方法小结
Apr 13 #Javascript
JavaScript 数组运用实现代码
Apr 13 #Javascript
关于Jqzoom的使用心得 jquery放大镜效果插件
Apr 12 #Javascript
Javascript 判断Flash是否加载完成的代码
Apr 12 #Javascript
You might like
PHP网上调查系统
2006/10/09 PHP
PHP中3种生成XML文件方法的速度效率比较
2012/10/06 PHP
PHP is_subclass_of函数的一个BUG和解决方法
2014/06/01 PHP
ThinkPHP中的系统常量和预定义常量集合
2014/07/01 PHP
使用ltrace工具跟踪PHP库函数调用的方法
2016/04/25 PHP
php设计模式之装饰模式应用案例详解
2019/06/17 PHP
jquery插件 cluetip 关键词注释
2010/01/12 Javascript
jQuery中的bind绑定事件与文本框改变事件的临时解决方法
2010/08/13 Javascript
jquery ajax学习笔记2 使用XMLHttpRequest对象的responseXML
2011/10/16 Javascript
js将long日期格式转换为标准日期格式实现思路
2013/04/07 Javascript
鼠标悬浮停留三秒后自动显示大图js代码
2014/09/09 Javascript
AngularJS基础 ng-dblclick 指令用法
2016/08/01 Javascript
vue2.x select2 指令封装详解
2017/10/12 Javascript
基于cropper.js封装vue实现在线图片裁剪组件功能
2018/03/01 Javascript
vue axios post发送复杂对象问题
2019/06/04 Javascript
解决vue-cli项目开发运行时内存暴涨卡死电脑问题
2019/10/29 Javascript
vue elementUI 表单校验的实现代码(多层嵌套)
2019/11/06 Javascript
如何在postman测试用例中实现断言过程解析
2020/07/09 Javascript
从零学python系列之新版本导入httplib模块报ImportError解决方案
2014/05/23 Python
python实现在windows服务中新建进程的方法
2015/06/30 Python
Python实现的科学计算器功能示例
2017/08/04 Python
Python爬虫实例扒取2345天气预报
2018/03/04 Python
Python实现的个人所得税计算器示例
2018/06/01 Python
Django框架模板语言实例小结【变量,标签,过滤器,继承,html转义】
2019/05/23 Python
详解Python中的各种转义符\n\r\t
2019/07/10 Python
Python如何利用Har文件进行遍历指定字典替换提交的数据详解
2020/11/05 Python
详解css3 mask遮罩实现一些特效
2018/10/24 HTML / CSS
HTML5 history新特性pushState、replaceState及两者的区别
2015/12/26 HTML / CSS
Agoda西班牙:全球特价酒店预订
2017/06/03 全球购物
毕业生的自我鉴定
2013/10/29 职场文书
汽车维修专业个人求职信范文
2014/01/01 职场文书
四年级语文教学反思
2014/02/05 职场文书
关于成立领导小组的通知
2015/04/23 职场文书
2015元旦感言
2015/12/09 职场文书
为什么mysql字段要使用NOT NULL
2021/05/13 MySQL
【海涛解说】史上最给力比赛,挑战DOTA极限
2022/04/01 DOTA