Skip to content

Instantly share code, notes, and snippets.

@wenshin
Last active January 12, 2016 15:07
Show Gist options
  • Save wenshin/7af05caac1d2b49332db to your computer and use it in GitHub Desktop.
Save wenshin/7af05caac1d2b49332db to your computer and use it in GitHub Desktop.
A zoom util for mobile web app
/**
* requesteAnimationFrame 兼容支持
* @reference
* http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
* @return {undefined}
*/
(function() {
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = Date.now();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) { clearTimeout(id); };
}
})();
/**
* transition animation
* @description 使用 transition 实现动画效果,并且保证动画部分的顺滑,在移动端表现优异
*/
'use strict';
var prefixes = ['', '-webkit-', '-moz-'];
var needPrefix = { 'transform': 1, 'transition': 1};
function prefixProp(prop, handleEachProp) {
if ( needPrefix[prop] ) {
prefixes.forEach(function (prefix) {
handleEachProp(prefix + prop);
});
} else {
handleEachProp(prop);
}
}
function setStyle(elem, prop, value) {
prefixProp(prop, function (prefixed) {
elem.style.setProperty(prefixed, value);
});
}
function animate(elem, keyFrames, config, handleFinish) {
keyFrames = [].concat(keyFrames);
config = config || {};
// NOTE:
// 执行完动画后,会修改 config.duration 为 Infinity。
// 暂时不知道什么问题,这里先 Copy config 配置。
var _config = {};
_config.duration = (config.duration || 0.5) / keyFrames.length;
_config.timing = config.timing || 'ease-out';
_config.delay = config.delay || 0;
if ( _config.delay > 0 ) {
setTimeout(function(){
_config.delay = 0;
setTransition(elem, _config);
_animate(elem, keyFrames, _config, handleFinish);
}, _config.delay*1000);
return;
}
setTransition(elem, _config);
_animate(elem, keyFrames, _config, handleFinish);
}
function setTransition (elem, config) {
setStyle(elem, 'transition', [
'all' , fmtTime(config.duration),
config.timing, fmtTime(config.delay)
].join(' '));
if ( !elem.__transitionActive ) {
// 由于浏览器会等 JS 运行结束才会使设置的 style 生效,
// 这样可以减少重绘的情况。
// 第一次 js 设置 transition 时,由于transition还没有激活,所以用
// elem.getBoundingClientRect() 强制重绘制,立即激活。
elem.getBoundingClientRect();
elem.__transitionActive = true;
}
}
function _animate(elem, keyFrames, config, handleFinish) {
if ( !keyFrames.length ) {
setStyle(elem, 'transition', '');
handleFinish && handleFinish(); // jshint ignore:line
return;
}
animateToKeyFrame(elem, keyFrames[0], config, function() {
_animate(elem, keyFrames.slice(1), config, handleFinish);
});
}
function animateToKeyFrame(elem, keyFrame, config, handleFrameFinish) {
onTransitionend(elem, handleFrameFinish, config.duration);
requestAnimationFrame(function () {
for ( var style in keyFrame ) {
if ( keyFrame.hasOwnProperty(style) ) {
setStyle(elem, style, keyFrame[style]);
}
}
});
}
function onTransitionend (elem, handle, duration) {
var transitionendTriggered = false;
var timer = setTimeout(function() {
transitionendTriggered || handleTransitionend(); //jshint ignore:line
},
duration*1000);
function handleTransitionend(e) {
clearTimeout(timer);
transitionendTriggered = true;
offTransitionend(elem, handleTransitionend);
handle.call(elem, e);
}
elem.addEventListener('transitionend', handleTransitionend, false);
elem.addEventListener('webkitTransitionend', handleTransitionend, false);
}
function offTransitionend (elem, handle) {
elem.removeEventListener('transitionend', handle);
elem.removeEventListener('webkitTransitionend', handle);
}
function fmtTime(time) {
return time ? time + 's' : '0s';
}
module.exports = animate;
'use strict';
var animate = require('./tranimation');
/**
* 缩放组件。
* 缩放和拖动都是步进的改变,每次步进使用 transition 动画完成,在动画过程中,将不响应触摸事件。
* 这样避免频繁响应带来的性能,代来更顺滑的效果。
* @feature
* 1. 只使用 transform 的 translate 和 scale,不会引起页面重排和重绘;
* 2. 支持缩略图;
* 3. 支持任何类型的元素;
* 4. 支持自定义最大和最小缩放比例;
* 5. 自动根据父容器水平和垂直居中。
* @desription 响应触摸事件
* event.targetTouches: 触发元素(注意是触发元素),如果是在父级元素上监听事件,
* 将不能不能准确判断实际有多少个触点。
* event.changedTouches: touchend 事件时这个属性肯定有值,理论上可以存在多个值,
* 但是多点触发 touchend 一般不会同时出现,所以认为只有一个值。
* 其他事件代表当前发生移动的触点,不一定和 targetTouches 相同。
*
* @param {DOMElement} elem 缩放的对象
* @param {Object} options 配置信息见 Line:27
* @usage
* ```
* let zoom = new Zoom(elem, {firstScale: 3});
* // Bind event listeners
* zoom.enable();
* // Unbind touch event listeners
* zoom.disable();
* // Recover to init status
* zoom.reset();
* ```
*/
function Zoom(elem, options) {
this.elem = elem;
this.scale = Zoom.INIT_SCALE;
this.translate = {x: Zoom.INIT_TRANSLATE_X, y: Zoom.INIT_TRANSLATE_Y};
this.offsetTranslate = {x: Zoom.INIT_TRANSLATE_X, y: Zoom.INIT_TRANSLATE_Y};
this.sourceRect = null;
this.parentRect = null;
this.centerPosition = null;
this.transformOrigin = null;
this.scaleActive = false;
this.inTransition = false;
this.startDragTouch = null;
this.cachedDragTouch = null;
this.cachedTouchDistance = 0;
this.dragTouchmoveCount = 0;
options = options || {};
var zoomer = this;
// 动画结束时的回调
var animConfig = options.animConfig || {};
zoomer.options = {
// 默认在缩放和拖动时均不显示缩略图,
// 如果要关闭所有缩略图设置 thumbnail = false,
// 如果关闭其中一个使用 thumbnail = { zoom: false, drag: false }。
thumbnail: options.thumbnail || { zoom: false, drag: false },
// 默认根据元素的父元素自动水平和垂直居中,如果设为 false 将不自动处理
autoCenter: options.autoCenter === false ? false : true,
// 初始化时,快速缩放到特定的比例
firstScale: options.firstScale,
// TODO: 初始化时,快速移动到特定的位置
firstTranslate: options.firstTranslate,
// 最小的缩小比例
minScale: options.minScale || 1,
// 最大的缩放比例
maxScale: options.maxScale || 10,
// 是否禁用动画,默认为 true。设置为 false 则取消动画。这在某些浏览器不支持
// 动画时比较有用
animation: options.animation === false ? false : true,
animConfig: {
duration: animConfig.duration || 0.2,
// transition timing-function
timing: animConfig.timing || 'cubic-bezier(.12,.63,.5,.96)',
delay: animConfig.delay || 0
},
animFinish: options.animFinish || function(scale, translate) {return [scale, translate];}
};
zoomer.init();
}
Zoom.INIT_SCALE = 1;
Zoom.INIT_TRANSLATE_X = 0;
Zoom.INIT_TRANSLATE_Y = 0;
Zoom.prototype.init = function () {
var srcRect;
// transform 不再改变,均通过 translate 来纠正中心点的偏移
setStyle(this.elem, 'display', 'inline-block');
setStyle(this.elem, 'transform-origin', 'center center');
srcRect = this.sourceRect = this.elem.getBoundingClientRect();
var centerX = srcRect.width / 2;
var centerY = srcRect.height / 2;
// 保留值用于计算时使用
this.transformOrigin = { x: centerX, y: centerY };
var parentElem = this.elem.parentElement;
parentElem.style.overflow = 'hidden';
parentElem.style.position = 'relative';
this.parentRect = parentElem.getBoundingClientRect();
if ( this.options.thumbnail.zoom || this.options.thumbnail.drag ) {
this.createThumbnail();
}
if ( this.options.autoCenter ) {
var pRect = this.parentRect;
this.offsetTranslate.x = (pRect.width - srcRect.width)/2;
this.offsetTranslate.y = (pRect.height - srcRect.height)/2;
this.setTransform({translate: this.offsetTranslate, animation: false, limit: false});
}
// 为了保持缩放的稳定性,一次缩放会记录第一次的缩放中心点(1倍倍率下的坐标),
// 设置 start 为 true 表示缩放开始,结束一次缩放后设置为 false
this.centerPosition = {
start: false,
center: { x: centerX, y: centerY },
offset: { x: 0, y: 0}
};
this.enable();
};
Zoom.prototype.demount = function () {
setStyle(this.elem, 'display', '');
setStyle(this.elem, 'transform-origin', '');
setStyle(this.elem, 'transform', '');
setStyle(this.elem.parentElement, 'overflow', '');
setStyle(this.elem.parentElement, 'position', '');
};
/**
* 创建缩略图。
* 缩略是由一个容器对象(wrapper)、目标对象Clone、一个当前视窗(thumbnail viewport)组成
* @return {undefined}
*/
Zoom.prototype.createThumbnail = function () {
var wrapper = this.thumbnail = document.createElement('div');
wrapper.style.position = 'absolute';
wrapper.style.backgroundColor = 'rgba(0,0,0,.618)';
wrapper.style.left = wrapper.style.top = 0;
// 默认不显示
wrapper.style.opacity = 0;
setStyle(wrapper, 'transform', 'scale(0,0)');
setStyle(wrapper, 'transform-origin', '0 0');
// Clone 缩放对象,并且给所有 id 属性添加前缀
wrapper.innerHTML = this.elem.outerHTML.replace(/id="/gi, '$&thumbnail-');
// 阻止链接跳转
wrapper.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
}, false);
setStyle(wrapper.firstChild, 'transform', '');
this.createThumbnailViewport();
this.elem.parentElement.appendChild(wrapper);
};
Zoom.prototype.createThumbnailViewport = function () {
var viewport = this.viewport = document.createElement('div');
viewport.style.backgroundColor = 'transparent';
viewport.style.position = 'absolute';
viewport.style.top = viewport.style.left = 0;
viewport.style.border = '6px solid #f00';
viewport.style.width = viewport.style.height = '100%';
setStyle(viewport, 'box-sizing', 'border-box');
setStyle(viewport, 'transform',
'translate('+ this.offsetTranslate.x +'px,'+ this.offsetTranslate.y +'px)');
setStyle(viewport, 'transform-origin', '0 0');
this.thumbnail.appendChild(viewport);
};
/**
* 更新缩略图视窗的位置
* @return {undefined}
*/
Zoom.prototype.refreshThumbnailViewport = function () {
// 根据父级元素移动到中心位置时,会执行一次移动,
// 这时还没有创建缩略图
if ( !this.viewport ) { return; }
var k = 1 / this.scale;
var transfScale;
var transfTranslate;
var pBoundary = this.sourceRect;
var eBoundary = this.elem.getBoundingClientRect();
var dx = pBoundary.left - eBoundary.left + this.offsetTranslate.x;
var dy = pBoundary.top - eBoundary.top + this.offsetTranslate.y;
transfScale = 'scale('+ k +','+ k +')';
// 这里不用缩小,因为本来是一样的,自动等比例缩放
transfTranslate = 'translate('+ dx +'px,'+ dy +'px)';
setStyle(this.viewport, 'transform', transfScale + ' ' + transfTranslate);
};
Zoom.prototype.showThumbnail = function (type) {
if ( !this.thumbnail ) { return; }
if ( !type || this.options.thumbnail[type] ) {
this.setThumbnailStyle(1, 'scale(0.3, 0.3)');
}
};
Zoom.prototype.hideThumbnail = function () {
if ( !this.thumbnail ) { return; }
this.setThumbnailStyle(0, 'scale(0, 0)');
};
Zoom.prototype.setThumbnailStyle = function (opacity, transform) {
var tn = this.thumbnail;
if ( this.options.animation ) {
animate(tn, {opacity: opacity}, this.options.animConfig, function() {
setStyle(tn, 'transform', transform);
});
} else {
setStyle(tn, 'opacity', opacity);
setStyle(tn, 'transform', transform);
}
};
Zoom.prototype.enable = function() {
if ( this.options.firstScale ) {
this.setTransform({scale: this.options.firstScale, animation: true});
}
this.bindEvents();
};
Zoom.prototype.disable = function () {
this.setTransform({scale: Zoom.INIT_SCALE, animation: true});
this.demount();
this.unbindEvents();
};
Zoom.prototype.reset = function () {
this.setTransform({scale: this.options.firstScale || Zoom.INIT_SCALE, animation: true});
};
Zoom.prototype.handleTouchZoom = function(e, touches) {
var scaleTo;
var distanceSquare = this.calcTouchDistanceSquare(touches);
// 这里使用 e.preventDefault 和 e.stopPropagation 要慎重,会影响缩放对象原来的事件
if ( e.type === 'touchstart') {
this.scaleActive = true;
this.cacheZoomPosition(this.calcCenterPosition(touches));
}
if ( e.type === 'touchmove' && e.changedTouches.length ) {
this.scaleActive = true;
this.preventTouchEvent(e);
scaleTo = Math.sqrt(distanceSquare / this.cachedTouchDistance) * this.scale;
if ( scaleTo !== this.scale ) {
this.showThumbnail('zoom');
this.setTransform({
scale: scaleTo,
translate: this.calcScaledTranslate(touches, scaleTo)
});
}
}
// 这里不能移动到前面保存,会影响计算
this.cachedTouchDistance = distanceSquare;
};
/**
* 判断一个 DOM 节点是否是缩放对象或者其子节点。
* 在响应 touch 时间时,如果触发该事件是缩放对象的子节点,
* 那么 TouchEvent.target 将是子节点,并且 TouchEvent.targetTouches
* 只包含在 ToucheEvent.target 的 Touch 对象
* @param {DOMElement} target
* @return {Boolean}
*/
Zoom.prototype.isInZoomArea = function (target) {
if ( this.elem.parentElement === target ) {
return true;
} else if ( !target ) {
return false;
} else {
return this.isInZoomArea(target.parentElement);
}
};
/**
* 计算两个触摸事件的距离,
* NOTE: 为了减少运算结果和增加更容易比较,使用距离的平方
* @param {Touch} touches
* @return {Number}
*/
Zoom.prototype.calcTouchDistanceSquare = function(touches) {
var offset = getTouchOffset(touches[0], touches[1]);
return Math.pow(offset.x, 2) + Math.pow(offset.y, 2);
};
Zoom.prototype.calcScaledTranslate = function (touches, scaleTo) {
var position = this.centerPosition.start ?
this.centerPosition :
this.cacheZoomPosition(this.calcCenterPosition(touches));
var tran = _calcScaledTranslate(
scaleTo, position.center, this.sourceRect,
offsetAxle(this.offsetTranslate, position.offset));
return tran;
};
/**
* 计算缩放之后,保持 transform-origin 不变,通过调整 translate 来实现中心点移动
* transform-origin 的修改会导致重绘,而 translate 只会重组,性能要好一些
*
* @param {Point} centerPoint1x 1倍倍率的视觉中心点
* @param {Number} scale 缩放到的目标倍率
* @param {ClientRect} rect1x 1倍倍率缩放元素的大小属性,这里只用到 {width: 0, height: 0}
* @param {Point} tran1x 1倍倍率缩放元素相对于父元素的 translate 值
* @return {Point} 设置 translate 的 x 值和 y 值
*/
function _calcScaledTranslate (scale, centerPoint1x, rect1x, tran1x) {
// 公式1:计算 1 倍率下的 translate 值放大后的实际偏移距离。
// var scaledOffset = {
// x: srcTran.x*scale - srcRect.width*(scale-1)/2,
// y: srcTran.y*scale - srcRect.height*(scale-1)/2
// };
// 放大后返回到中心点的实际偏移距离
var sOffsetX = tran1x.x - (scale - 1)*centerPoint1x.x;
var sOffsetY = tran1x.y - (scale - 1)*centerPoint1x.y;
// 利用公式1 反向计算出 1 倍率下设置的 translate 值
return {
x: ( rect1x.width*(scale-1)/2 + sOffsetX ) / scale,
y: ( rect1x.height*(scale-1)/2 + sOffsetY ) / scale
};
}
Zoom.prototype.cacheZoomPosition = function (position) {
this.centerPosition = position;
this.centerPosition.start = true;
return this.centerPosition;
};
Zoom.prototype.calcCenterPosition = function(touches) {
var rect = this.elem.getBoundingClientRect();
var centerTouch = {
clientX: (touches[0].clientX + touches[1].clientX)/2,
clientY: (touches[0].clientY + touches[1].clientY)/2
};
var center = timesNumberProps( this.getInnerPoint(centerTouch, rect), 1/this.scale );
var offsetCenter = this.getInnerPoint(centerTouch, this.sourceRect, this.offsetTranslate);
return {
center: center,
offset: offsetAxle(offsetCenter, timesNumberProps(center, -1))
};
};
/**
* 获取相对于特定元素的坐标系坐标
* @param {Touch} touch 当前的Touch对象,Touch.clientX和Touch.clientY是相对于viewport的坐标
* @param {Object} elemClientRect 用于计算的矩形数据,一般式固定的父辈元素 getBoundingClientRect()
* @param {Object} offset 偏移位置,默认偏移为{x: 0, y: 0}
* @return {Object} {x:0, y:0} 格式对象
*/
Zoom.prototype.getInnerPoint = function(touch, elemClientRect, offset) {
offset = offset || {x: 0, y: 0};
var x = touch.clientX - elemClientRect.left - offset.x;
var y = touch.clientY - elemClientRect.top - offset.y;
x = x > elemClientRect.width ? elemClientRect.width : (x < 0 ? 0 : x);
y = y > elemClientRect.heigth ? elemClientRect.heigth : (y < 0 ? 0 : y);
return {x: x, y: y};
};
Zoom.prototype.handleTouchDrag = function(e, touch) {
var faster = 1.2;
var lastTouch = this.cachedDragTouch;
var offset = this.calcTouchOffset(touch, lastTouch);
if ( e.type === 'touchstart' ) {
this.cacheTouch('startDragTouch', touch);
}
if ( e.type === 'touchmove' ) {
this.preventTouchEvent(e);
this.dragTouchmoveCount++;
this.showThumbnail('drag');
this.setTransform({touchOffset: offset, animation: false});
}
if ( e.type === 'touchend' ) {
// 当响应 touchmove 个数 < 4 时可能是双指触摸过程的微小移动导致,影响缩放效果
if ( this.dragTouchmoveCount < 8 && this.dragTouchmoveCount > 4) {
this.showThumbnail('drag');
offset = this.calcTouchOffset(touch, this.startDragTouch, faster);
this.setTransform({touchOffset: offset, animation: true});
}
}
};
Zoom.prototype.cacheTouch = function(prop, touch) {
if ( touch ) {
this[prop] = { clientX: touch.clientX, clientY: touch.clientY };
}
};
/**
* 计算 translate 的目标坐标
* NOTE: 整个程序要保证 this.translate 不为空,或者异常值
* @param {Touch} touch Touch对象
* @param {Touche} lastTouch 上一次 Touch 对象
* @param {Number} faster 加快移动速度的倍率
* @return {null|Object} 返回{x: 0, y: 0}
*/
Zoom.prototype.calcTouchOffset = function(touch, lastTouch, faster) {
if ( !touch || !lastTouch ) { return this.translate; }
var offset = {};
var offsetTouch = getTouchOffset(touch, lastTouch);
faster = faster || 1;
offset.x = offsetTouch.x * faster;
offset.y = offsetTouch.y * faster;
return offset;
};
Zoom.prototype.handleTouch = function(e) {
if ( this.inTransition ) {
this.preventTouchEvent(e);
this.doTouchendClean(e);
return false;
}
var touches = this.getOwnTouches(e);
if ( e.touches.length === 1 && touches.length === 1 &&
!this.scaleActive && this.scale > 1 ) {
this.handleTouchDrag(e, touches[0]);
this.cacheTouch('cachedDragTouch', touches[0]);
} else if ( e.touches.length === 2 && touches.length ) {
if ( touches.length === 1 ) touches = e.touches;
this.handleTouchZoom(e, touches);
}
if ( e.type === 'touchend' && (this.scaleActive || this.dragTouchmoveCount) ) {
this.preventTouchEvent(e);
}
this.doTouchendClean(e);
}
Zoom.prototype.preventTouchEvent = function ( event ) {
// 'touchstart' 和 'touchend' 事件如果屏蔽掉,会影响元素内的 'click' 事件
event.preventDefault();
event.stopPropagation();
};
Zoom.prototype.getOwnTouches = function (event) {
var zoomer = this;
var touches = [];
function pushTouches(_touches) {
for ( var i = 0; i < _touches.length; i++ ) {
if ( zoomer.isInZoomArea(_touches[i].target) ) {
touches.push(_touches[i]);
}
}
}
if ( event.type === 'touchend' ) {
pushTouches(event.changedTouches);
} else {
pushTouches(event.touches);
}
return touches.length ? touches : null;
};
Zoom.prototype.doTouchendClean = function(e) {
if ( e.type !== 'touchend' ) { return; }
if ( this.scaleActive && !e.touches.length ) {
this.scaleActive = false;
this.cachedTouchDistance = 0;
this.centerPosition.start = false;
this.hideThumbnail();
}
if ( this.dragTouchmoveCount ) {
this.cachedDragTouch = null;
this.dragTouchmoveCount = 0;
this.hideThumbnail();
}
};
Zoom.prototype.bindEvents = function() {
var zoomer = this;
this.bindTouchEvents(zoomer._handleTouch || function(e) {
zoomer.handleTouch(e);
});
};
Zoom.prototype.unbindEvents = function() {
this.unbindTouchEvents(this._handleTouch);
};
Zoom.prototype.bindTouchEvents = function(callback) {
this.elem.parentElement.addEventListener('touchstart', callback, false);
this.elem.parentElement.addEventListener('touchmove', callback, false);
this.elem.parentElement.addEventListener('touchend', callback, false);
};
Zoom.prototype.unbindTouchEvents = function(callback) {
this.elem.parentElement.removeEventListener('touchstart', callback);
this.elem.parentElement.removeEventListener('touchmove', callback);
this.elem.parentElement.removeEventListener('touchend', callback);
};
/**
* 设置 transform 值,可选择是否使用动画
* @param {Object} config {
* scale: [Number],
* translate: [{x: [Number], y: [Number]}],
* animation: [Boolean], // false default
* limit: [Boolean] // true default
* }
* @return {undefined}
*/
Zoom.prototype.setTransform = function (config) {
var translate;
var limit = config.limit === false ? false : true;
var animation = config.animation && this.options.animation;
var scale = limit ? this.limitScale(config.scale) : scale || this.scale;
if ( config.touchOffset ) {
translate = this.calcDragTranslate(config.touchOffset, scale);
} else {
translate = config.translate || this.translate;
}
translate = limit ? this.limitTranslate(translate, scale) : translate;
// console.log('setTransform:',
// 'config=', config, ',fixedScale=', scale,
// ',fixedTranslate=', translate);
if ( this.inTransition || (
this.scale === scale &&
Math.abs(translate.x - this.translate.x) < 0.0001 &&
Math.abs(translate.y - this.translate.y < 0.0001)) ) {
return;
}
this.inTransition = true;
var transform = this.fmtTransform(scale, translate);
if ( animation ) {
var zoomer = this;
animate(
zoomer.elem,
{transform: transform},
zoomer.options.animConfig,
function handleFinish() {
zoomer.handleAnimFinish(scale, translate);
}
);
} else {
setStyle(this.elem, 'transform' ,transform);
this.handleAnimFinish(scale, translate);
}
};
Zoom.prototype.fmtTransform = function(scale, translate) {
var transform = '';
transform += 'scale('+ scale +','+ scale +') ';
transform += translate && 'translate('+ translate.x +'px,'+ translate.y +'px)';
return transform;
};
Zoom.prototype.handleAnimFinish = function(scale, translate) {
this.inTransition = false;
this.scale = scale;
this.translate = translate;
this.refreshThumbnailViewport();
if ( this.options.animFinish ) {
this.options.animFinish.call(this.elem, scale, translate);
}
};
Zoom.prototype.limitScale = function (scale) {
if ( !scale ) { return this.scale; }
return range(scale, this.options.minScale, this.options.maxScale);
};
/**
* 根据触点偏移值,计算 translate 值
* @param {Point} touchOffset 触摸事件的偏移值
* @param {Number} scale 放大值
* @return {Point}
*/
Zoom.prototype.calcDragTranslate = function ( touchOffset, scale ) {
// 如果 scale 发生变化,原来的 translate 值需要根据最新的 scale 进行比例 k 缩放。
var k = scale / this.scale;
// 如果触摸点移动 100px,要想目标也移动 100px。
// 因为 translate 会被 scale 放大或缩小。
// 所以实际 translate 值的偏移量 dx或dy 为 (100/scale)px。
touchOffset = touchOffset || {x: 0, y: 0};
return offsetAxle(timesNumberProps(this.translate, k), timesNumberProps(touchOffset, 1/scale));
};
/**
* 求移动的范围,如果移动到了边界将不再移动
* 移动范围的计算公式,前提 transformOrigin: 'center center'。Y 同 X:
* xMin: -( (视口宽度*scale - 视口宽度) /scale / 2 + 初始偏移的x值 )
* xMax: (视口宽度*scale - 视口宽度) / scale / 2 + 初始偏移的x值
* @param {Point} translate 计算的 translate 值
* @param {Number} scale 目标放大倍数
* @return {Object} 移动目标坐标
*/
Zoom.prototype.limitTranslate = function (translate, scale) {
var ret = {};
var rect = this.elem.getBoundingClientRect();
var srect = this.sourceRect;
var prect = this.parentRect;
var isDrag = this.scale === scale;
var scaledRect = timesNumberProps(this.sourceRect, scale);
scaledRect.top = srect.top - (scaledRect.height - srect.height) / 2 + translate.y * scale;
scaledRect.bottom = scaledRect.top + scaledRect.height;
scaledRect.left = srect.left - (scaledRect.width - srect.width) / 2 + translate.x * scale;
scaledRect.right = scaledRect.left + scaledRect.width;
var leftTopLimit = _calcScaledTranslate(scale, {x: 0, y: 0}, srect, {x: 0, y: 0});
var rightBottomLimit = _calcScaledTranslate(scale,
{x: srect.width, y: srect.height}, srect,
{x: this.offsetTranslate.x * 2, y: this.offsetTranslate.y * 2});
// 父级元素的 border 等设置会影响大小比较
var precision = this.elem.parentElement.offsetHeight - this.elem.parentElement.clientHeight + 5;
function limit(axle) {
var minSide = ({x: 'left', y: 'top'})[axle];
var maxSide = ({x: 'right', y: 'bottom'})[axle];
// 从上往下。或,从左往右
if ( rect[minSide] < scaledRect[minSide] ) {
//穿过父容器上边界.或,穿过左边界。
if ( scaledRect[minSide] > prect[minSide] &&
rect[minSide] - precision <= prect[minSide] ){
ret[axle] = leftTopLimit[axle];
} else {
ret[axle] = translate[axle];
}
// 从下往上。或,从右往左
} else {
//穿过父容器下边界.或,穿过右边界。
if ( scaledRect[maxSide] < prect[maxSide] &&
rect[maxSide] + precision >= prect[maxSide] ) {
ret[axle] = rightBottomLimit[axle];
} else {
ret[axle] = translate[axle];
}
}
}
var isNotOverflow = (
// 不包含相等的情况,会影响拖动中对边界的判断
rect.top > prect.top && rect.left > prect.left &&
rect.bottom < prect.bottom && rect.right < prect.right
);
if ( scale === Zoom.INIT_SCALE ) {
ret = this.offsetTranslate;
} else if ( isDrag && !isNotOverflow ) {
limit('x');
limit('y');
} else if ( isDrag && isNotOverflow ) {
ret = this.translate;
} else {
ret = translate;
}
return ret;
};
var prefixes = ['-webkit-', '-moz-'];
var needPrefix = { 'box-sizing': 1, 'transform': 1, 'transition': 1, 'transform-origin': 1};
function setStyle(elem, prop, value) {
if ( needPrefix[prop] ) {
prefixes.forEach(function (prefix) {
elem.style.setProperty(prefix + prop, value);
});
} else {
elem.style.setProperty(prop, value);
}
}
/**
* 放大给定对象的所有类型为 Number 的属性
* @param {Obj} obj
* @param {Number} scale 放大的的倍数
* @return {Obj}
*/
function timesNumberProps(obj, scale) {
var ret = {};
for ( var p in obj) {
if ( typeof obj[p] === 'number' ) { ret[p] = obj[p] * scale; }
}
return ret;
}
function getTouchOffset(touch, touchEnd) {
return { x: touch.clientX - touchEnd.clientX, y: touch.clientY - touchEnd.clientY };
}
function offsetAxle(target, offset) {
return { x: target.x + offset.x, y: target.y + offset.y };
}
function range(value, min, max, offset) {
offset = offset || 0;
value = value >= min && value <= max ? value : (value < min ? min : max );
return value + offset;
}
module.exports = Zoom;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment