Files

734 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* jQuery photoClip v1.8.0
* 依赖插件
* - iscroll-zoom.js
* - hammer.js
* - lrz.all.bundle.js
*
* @author 白俊杰 625603381@qq.com 2014/07/31
* https://github.com/baijunjie/jQuery-photoClip
*
* @brief 支持手势的裁图插件
* 在移动设备上双指捏合为缩放,双指旋转可根据旋转方向每次旋转90度
* 在PC设备上鼠标滚轮为缩放,每次双击则顺时针旋转90度
* @option_param {array} size 截取框的宽和高组成的数组。默认值为[260,260]
* @option_param {array} outputSize 输出图像的宽和高组成的数组。默认值为[0,0],表示输出图像原始大小
* //@option_param {string} outputType 指定输出图片的类型,可选 "jpg" 和 "png" 两种种类型,默认为 "jpg"
* @option_param {string} file 上传图片的<input type="file">控件的选择器或者DOM对象
* @option_param {string} view 显示截取后图像的容器的选择器或者DOM对象
* @option_param {string} ok 确认截图按钮的选择器或者DOM对象
* @option_param {function} loadStart 开始加载的回调函数。this指向 fileReader 对象,并将正在加载的 file 对象作为参数传入
* @option_param {function} loadComplete 加载完成的回调函数。this指向图片对象,并将图片地址作为参数传入
* @option_param {function} loadError 加载失败的回调函数。this指向 fileReader 对象,并将错误事件的 event 对象作为参数传入
* @option_param {function} clipFinish 裁剪完成的回调函数。this指向原图片对象,会将裁剪出的图像数据DataURL作为参数传入
*/
(function(root, factory) {
"use strict";
if (typeof define === "function" && define.amd) {
define(["jquery", "iscroll-zoom", "hammer", "lrz"], factory);
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"), require("iscroll-zoom"), require("hammer"), require("lrz"));
} else {
root.bjj = root.bjj || {};
root.bjj.PhotoClip = factory(root.jQuery, root.IScroll, root.Hammer, root.lrz);
}
}(this, function($, IScroll, Hammer, lrz) {
"use strict";
var defaultOption = {
size: [260, 260],
outputSize: [0, 0],
//outputType: "jpg",
file: "",
view: "",
ok: "",
loadStart: function() {},
loadComplete: function() {},
loadError: function() {},
clipFinish: function() {}
}
function PhotoClip(container, option) {
if (!window.FileReader) {
dr_tips(0, "您的浏览器不支持 HTML5 的 FileReader API, 因此无法初始化图片裁剪插件,请更换最新的浏览器!");
return;
}
var opt = $.extend({}, defaultOption, option);
var returnValue = photoClip(container, opt);
this.destroy = returnValue.destroy;
}
function photoClip(container, option) {
var size = option.size,
outputSize = option.outputSize,
file = option.file,
view = option.view,
ok = option.ok,
//outputType = option.outputType || "image/jpeg",
outputType = "image/jpeg",
loadStart = option.loadStart,
loadComplete = option.loadComplete,
loadError = option.loadError,
clipFinish = option.clipFinish;
if (!isArray(size)) {
size = [260, 260];
}
if (!isArray(outputSize)) {
outputSize = [0, 0];
}
var clipWidth = size[0] || 260,
clipHeight = size[1] || 260,
outputWidth = Math.max(outputSize[0], 0),
outputHeight = Math.max(outputSize[1], 0);
/*if (outputType === "jpg") {
outputType = "image/jpeg";
} else if (outputType === "png") {
outputType = "image/png";
}*/
var $file = $(file);
if (!$file.length) return;
var $img,
imgWidth, imgHeight, //图片当前的宽高
imgLoaded; //图片是否已经加载完成
$file.attr("accept", "image/*");
$file.on("change", function() {
if (!this.files.length) return;
var files = this.files[0];
if (!/image\/\w+/.test(files.type)) {
dr_tips(0, "图片格式不正确,请选择正确格式的图片文件!");
return false;
} else {
var fileReader = new FileReader();
fileReader.onprogress = function(e) {
//console.log((e.loaded / e.total * 100).toFixed() + "%");
};
fileReader.onload = function(e) {
//console.log(typeof lrz);
lrz(files)
.then(function (rst) {
// 处理成功会执行
createImg(rst.base64);
})
.catch(function (err) {
// 处理失败会执行
dr_tips(0, "图片处理失败");
loadError.call(this, err);
});
};
fileReader.onerror = function(e) {
dr_tips(0, "图片加载失败");
loadError.call(this, e);
};
fileReader.readAsDataURL(files); // 读取文件内容
loadStart.call(fileReader, files);
}
});
$file.click(function() {
this.value = "";
});
var $container, // 容器,包含裁剪视图层和遮罩层
$clipView, // 裁剪视图层,包含移动层
$moveLayer, // 移动层,包含旋转层
$rotateLayer, // 旋转层
$view, // 最终截图后呈现的视图容器
canvas, // 图片裁剪用到的画布
hammerManager,
myScroll, // 图片的scroll对象,包含图片的位置与缩放信息
containerWidth,
containerHeight;
init();
initScroll();
initEvent();
initClip();
var $ok = $(ok);
if ($ok.length) {
$ok.click(function() {
clipImg();
});
}
var $win = $(window);
resize();
$win.resize(resize);
var atRotation, // 是否正在旋转中
curX, // 旋转层的当前X坐标
curY, // 旋转层的当前Y坐标
curAngle; // 旋转层的当前角度
function imgLoad() {
imgLoaded = true;
$rotateLayer.append(this);
hideAction.call(this, $img, function() {
imgWidth = this.naturalWidth;
imgHeight = this.naturalHeight;
});
hideAction($moveLayer, function() {
resetScroll();
});
loadComplete.call(this, this.src);
}
function initScroll() {
var options = {
zoom: true,
scrollX: true,
scrollY: true,
freeScroll: true,
mouseWheel: true,
wheelAction: "zoom"
}
myScroll = new IScroll($clipView[0], options);
}
function resetScroll() {
curX = 0;
curY = 0;
curAngle = 0;
$rotateLayer.css({
"width": imgWidth,
"height": imgHeight
});
setTransform($rotateLayer, curX, curY, curAngle);
calculateScale(imgWidth, imgHeight);
myScroll.zoom(myScroll.options.zoomStart);
refreshScroll(imgWidth, imgHeight);
var posX = (clipWidth - imgWidth * myScroll.options.zoomStart) * .5,
posY = (clipHeight - imgHeight * myScroll.options.zoomStart) * .5;
myScroll.scrollTo(posX, posY);
}
function refreshScroll(width, height) {
$moveLayer.css({
"width": width,
"height": height
});
// 在移动设备上,尤其是Android设备,当为一个元素重置了宽高时
// 该元素的offsetWidth/offsetHeight、clientWidth/clientHeight等属性并不会立即更新,导致相关的js程序出现错误
// iscroll 在刷新方法中正是使用了 offsetWidth/offsetHeight 来获取scroller元素($moveLayer)的宽高
// 因此需要手动将元素重新添加进文档,迫使浏览器强制更新元素的宽高
$clipView.append($moveLayer);
myScroll.refresh();
}
function initEvent() {
var is_mobile = !!navigator.userAgent.match(/mobile/i);
if (is_mobile) {
hammerManager = new Hammer($moveLayer[0]);
hammerManager.add(new Hammer.Rotate());
var rotation, rotateDirection;
hammerManager.on("rotatemove", function(e) {
if (atRotation) return;
rotation = e.rotation;
if (rotation > 180) {
rotation -= 360;
} else if (rotation < -180) {
rotation += 360 ;
}
rotateDirection = rotation > 0 ? 1 : rotation < 0 ? -1 : 0;
});
hammerManager.on("rotateend", function(e) {
if (atRotation) return;
if (Math.abs(rotation) > 30) {
if (rotateDirection == 1) {
// 顺时针
rotateCW(e.center);
} else if (rotateDirection == -1) {
// 逆时针
rotateCCW(e.center);
}
}
});
} else {
$moveLayer.on("dblclick", function(e) {
rotateCW({
x: e.clientX,
y: e.clientY
});
});
}
}
function rotateCW(point) {
rotateBy(90, point);
}
function rotateCCW(point) {
rotateBy(-90, point);
}
function rotateBy(angle, point) {
if (atRotation) return;
atRotation = true;
var loacl;
if (!point) {
loacl = loaclToLoacl($moveLayer, $clipView, clipWidth * .5, clipHeight * .5);
} else {
loacl = globalToLoacl($moveLayer, point.x, point.y);
}
var origin = calculateOrigin(curAngle, loacl), // 旋转中使用的参考点坐标
originX = origin.x,
originY = origin.y,
// 旋转层以零位为参考点旋转到新角度后的位置,与以当前计算的参考点“从零度”旋转到新角度后的位置,之间的左上角偏移量
offsetX = 0, offsetY = 0,
// 移动层当前的位置(即旋转层旋转前的位置),与旋转层以当前计算的参考点从当前角度旋转到新角度后的位置,之间的左上角偏移量
parentOffsetX = 0, parentOffsetY = 0,
newAngle = curAngle + angle,
curImgWidth, // 移动层的当前宽度
curImgHeight; // 移动层的当前高度
if (newAngle == 90 || newAngle == -270)
{
offsetX = originX + originY;
offsetY = originY - originX;
if (newAngle > curAngle) {
parentOffsetX = imgHeight - originX - originY;
parentOffsetY = originX - originY;
} else if (newAngle < curAngle) {
parentOffsetX = (imgHeight - originY) - (imgWidth - originX);
parentOffsetY = originX + originY - imgHeight;
}
curImgWidth = imgHeight;
curImgHeight = imgWidth;
}
else if (newAngle == 180 || newAngle == -180)
{
offsetX = originX * 2;
offsetY = originY * 2;
if (newAngle > curAngle) {
parentOffsetX = (imgWidth - originX) - (imgHeight - originY);
parentOffsetY = imgHeight - (originX + originY);
} else if (newAngle < curAngle) {
parentOffsetX = imgWidth - (originX + originY);
parentOffsetY = (imgHeight - originY) - (imgWidth - originX);
}
curImgWidth = imgWidth;
curImgHeight = imgHeight;
}
else if (newAngle == 270 || newAngle == -90)
{
offsetX = originX - originY;
offsetY = originX + originY;
if (newAngle > curAngle) {
parentOffsetX = originX + originY - imgWidth;
parentOffsetY = (imgWidth - originX) - (imgHeight - originY);
} else if (newAngle < curAngle) {
parentOffsetX = originY - originX;
parentOffsetY = imgWidth - originX - originY;
}
curImgWidth = imgHeight;
curImgHeight = imgWidth;
}
else if (newAngle == 0 || newAngle == 360 || newAngle == -360)
{
offsetX = 0;
offsetY = 0;
if (newAngle > curAngle) {
parentOffsetX = originX - originY;
parentOffsetY = originX + originY - imgWidth;
} else if (newAngle < curAngle) {
parentOffsetX = originX + originY - imgHeight;
parentOffsetY = originY - originX;
}
curImgWidth = imgWidth;
curImgHeight = imgHeight;
}
// 将触摸点设为旋转时的参考点
// 改变参考点的同时,要计算坐标的偏移,从而保证图片位置不发生变化
if (curAngle == 0) {
curX = 0;
curY = 0;
} else if (curAngle == 90 || curAngle == -270) {
curX -= originX + originY;
curY -= originY - originX;
} else if (curAngle == 180 || curAngle == -180) {
curX -= originX * 2;
curY -= originY * 2;
} else if (curAngle == 270 || curAngle == -90) {
curX -= originX - originY;
curY -= originX + originY;
}
curX = curX.toFixed(2) - 0;
curY = curY.toFixed(2) - 0;
setTransform($rotateLayer, curX, curY, curAngle, originX, originY);
// 开始旋转
setTransition($rotateLayer, curX, curY, newAngle, 200, function() {
atRotation = false;
curAngle = newAngle % 360;
// 旋转完成后将参考点设回零位
// 同时加上偏移,保证图片位置看上去没有变化
// 这里要另外要加上父容器(移动层)零位与自身之间的偏移量
curX += offsetX + parentOffsetX;
curY += offsetY + parentOffsetY;
curX = curX.toFixed(2) - 0;
curY = curY.toFixed(2) - 0;
setTransform($rotateLayer, curX, curY, curAngle);
// 相应的父容器(移动层)要减去与旋转层之间的偏移量
// 这样看上去就好像图片没有移动
myScroll.scrollTo(
myScroll.x - parentOffsetX * myScroll.scale,
myScroll.y - parentOffsetY * myScroll.scale
);
calculateScale(curImgWidth, curImgHeight);
if (myScroll.scale < myScroll.options.zoomMin) {
myScroll.zoom(myScroll.options.zoomMin);
}
refreshScroll(curImgWidth, curImgHeight);
});
}
function initClip() {
canvas = document.createElement("canvas");
}
function clipImg() {
if (!imgLoaded) {
dr_tips(0, "当前没有图片可以裁剪!");
return;
}
var local = loaclToLoacl($moveLayer, $clipView);
var scale = myScroll.scale;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
if (!outputWidth || !outputHeight) {
canvas.width = clipWidth / scale;
canvas.height = clipHeight / scale;
} else {
canvas.width = outputWidth;
canvas.height = outputHeight;
ctx.scale(outputWidth / clipWidth * scale, outputHeight / clipHeight * scale);
}
ctx.translate(curX - local.x / scale, curY - local.y / scale);
ctx.rotate(curAngle * Math.PI / 180);
ctx.drawImage($img[0], 0, 0);
ctx.restore();
var dataURL = canvas.toDataURL(outputType, 1);
$view.css("background-image", "url("+ dataURL +")");
clipFinish.call($img[0], dataURL);
}
function resize() {
hideAction($container, function() {
containerWidth = $container.width();
containerHeight = $container.height();
});
}
function loaclToLoacl($layerOne, $layerTwo, x, y) { // 计算$layerTwo上的x、y坐标在$layerOne上的坐标
x = x || 0;
y = y || 0;
var layerOneOffset, layerTwoOffset;
hideAction($layerOne, function() {
layerOneOffset = $layerOne.offset();
});
hideAction($layerTwo, function() {
layerTwoOffset = $layerTwo.offset();
});
return {
x: layerTwoOffset.left - layerOneOffset.left + x,
y: layerTwoOffset.top - layerOneOffset.top + y
};
}
function globalToLoacl($layer, x, y) { // 计算相对于窗口的x、y坐标在$layer上的坐标
x = x || 0;
y = y || 0;
var layerOffset;
hideAction($layer, function() {
layerOffset = $layer.offset();
});
return {
x: x + $win.scrollLeft() - layerOffset.left,
y: y + $win.scrollTop() - layerOffset.top
};
}
function hideAction(jq, func) {
var $hide = $();
$.each(jq, function(i, n){
var $n = $(n);
var $hidden = $n.parents().addBack().filter(":hidden");
var $none;
for (var i = 0; i < $hidden.length; i++) {
if (!$n.is(":hidden")) break;
$none = $hidden.eq(i);
if ($none.css("display") == "none") $hide = $hide.add($none.show());
}
});
if (typeof(func) == "function") func.call(this);
$hide.hide();
}
function calculateOrigin(curAngle, point) {
var scale = myScroll.scale;
var origin = {};
if (curAngle == 0) {
origin.x = point.x / scale;
origin.y = point.y / scale;
} else if (curAngle == 90 || curAngle == -270) {
origin.x = point.y / scale;
origin.y = imgHeight - point.x / scale;
} else if (curAngle == 180 || curAngle == -180) {
origin.x = imgWidth - point.x / scale;
origin.y = imgHeight - point.y / scale;
} else if (curAngle == 270 || curAngle == -90) {
origin.x = imgWidth - point.y / scale;
origin.y = point.x / scale;
}
return origin;
}
function getScale(w1, h1, w2, h2) {
var sx = w1 / w2;
var sy = h1 / h2;
return sx > sy ? sx : sy;
}
function calculateScale(width, height) {
myScroll.options.zoomMin = getScale(clipWidth, clipHeight, width, height);
myScroll.options.zoomMax = Math.max(1, myScroll.options.zoomMin);
myScroll.options.zoomStart = Math.min(myScroll.options.zoomMax, getScale(containerWidth, containerHeight, width, height));
}
function clearImg() {
if ($img && $img.length) {
// 删除旧的图片以释放内存,防止IOS设备的webview崩溃
$img.remove();
delete $img[0];
}
}
function createImg(src) {
clearImg();
$img = $("<img>").css({
"user-select": "none",
"pointer-events": "none"
});
$img.on('load', imgLoad);
$img.attr("src", src); // 设置图片base64值
}
function setTransform($obj, x, y, angle, originX, originY) {
originX = originX || 0;
originY = originY || 0;
var style = {};
style[prefix + "transform"] = "translateZ(0) translate(" + x + "px," + y + "px) rotate(" + angle + "deg)";
style[prefix + "transform-origin"] = originX + "px " + originY + "px";
$obj.css(style);
}
function setTransition($obj, x, y, angle, dur, fn) {
// 这里需要先读取之前设置好的transform样式,强制浏览器将该样式值渲染到元素
// 否则浏览器可能出于性能考虑,将暂缓样式渲染,等到之后所有样式设置完成后再统一渲染
// 这样就会导致之前设置的位移也被应用到动画中
$obj.css(prefix + "transform");
$obj.css(prefix + "transition", prefix + "transform " + dur + "ms");
$obj.one(transitionEnd, function() {
$obj.css(prefix + "transition", "");
fn.call(this);
});
$obj.css(prefix + "transform", "translateZ(0) translate(" + x + "px," + y + "px) rotate(" + angle + "deg)");
}
// 判断一个对象是否为数组
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
function init() {
// 初始化容器
$container = $(container).css({
"user-select": "none",
"overflow": "hidden"
});
if ($container.css("position") == "static") $container.css("position", "relative");
// 创建裁剪视图层
$clipView = $("<div class='photo-clip-view'>").css({
"position": "absolute",
"left": "50%",
"top": "50%",
"width": clipWidth,
"height": clipHeight,
"margin-left": -clipWidth/2,
"margin-top": -clipHeight/2
}).appendTo($container);
$moveLayer = $("<div class='photo-clip-moveLayer'>").appendTo($clipView);
$rotateLayer = $("<div class='photo-clip-rotateLayer'>").appendTo($moveLayer);
// 创建遮罩
var $mask = $("<div class='photo-clip-mask'>").css({
"position": "absolute",
"left": 0,
"top": 0,
"width": "100%",
"height": "100%",
"pointer-events": "none"
}).appendTo($container);
var $mask_left = $("<div class='photo-clip-mask-left'>").css({
"position": "absolute",
"left": 0,
"right": "50%",
"top": "50%",
"bottom": "50%",
"width": "auto",
"height": clipHeight,
"margin-right": clipWidth/2,
"margin-top": -clipHeight/2,
"margin-bottom": -clipHeight/2,
"background-color": "rgba(0,0,0,.5)"
}).appendTo($mask);
var $mask_right = $("<div class='photo-clip-mask-right'>").css({
"position": "absolute",
"left": "50%",
"right": 0,
"top": "50%",
"bottom": "50%",
"margin-left": clipWidth/2,
"margin-top": -clipHeight/2,
"margin-bottom": -clipHeight/2,
"background-color": "rgba(0,0,0,.5)"
}).appendTo($mask);
var $mask_top = $("<div class='photo-clip-mask-top'>").css({
"position": "absolute",
"left": 0,
"right": 0,
"top": 0,
"bottom": "50%",
"margin-bottom": clipHeight/2,
"background-color": "rgba(0,0,0,.5)"
}).appendTo($mask);
var $mask_bottom = $("<div class='photo-clip-mask-bottom'>").css({
"position": "absolute",
"left": 0,
"right": 0,
"top": "50%",
"bottom": 0,
"margin-top": clipHeight/2,
"background-color": "rgba(0,0,0,.5)"
}).appendTo($mask);
// 创建截取区域
var $clip_area = $("<div class='photo-clip-area'>").css({
"border": "1px dashed #ddd",
"position": "absolute",
"left": "50%",
"top": "50%",
"width": clipWidth,
"height": clipHeight,
"margin-left": -clipWidth/2 - 1,
"margin-top": -clipHeight/2 - 1
}).appendTo($mask);
// 初始化视图容器
$view = $(view);
if ($view.length) {
$view.css({
"background-color": "#666",
"background-repeat": "no-repeat",
"background-position": "center",
"background-size": "contain"
});
}
}
function destroy() {
$file.off("change");
$file = null;
if (hammerManager) {
hammerManager.off("rotatemove");
hammerManager.off("rotateend");
hammerManager = null;
} else {
$moveLayer.off("dblclick");
}
myScroll.destroy();
myScroll = null;
$container.empty();
$container = null;
$clipView = null;
$moveLayer = null;
$rotateLayer = null;
$view.css({
"background-color": "",
"background-repeat": "",
"background-position": "",
"background-size": ""
});
$view = null;
}
return {
destroy: destroy
};
}
var prefix = '',
transitionEnd;
(function() {
var eventPrefix,
vendors = { Webkit: 'webkit', Moz: '', O: 'o' },
testEl = document.documentElement,
normalizeEvent = function(name) { return eventPrefix ? eventPrefix + name : name.toLowerCase() };
for (var i in vendors) {
if (testEl.style[i + 'TransitionProperty'] !== undefined) {
prefix = '-' + i.toLowerCase() + '-';
eventPrefix = vendors[i];
break;
}
}
transitionEnd = normalizeEvent('TransitionEnd');
})();
return PhotoClip;
}));