s94-imgclip
Version:
图片裁剪工具
408 lines (378 loc) • 16.4 kB
JavaScript
/**
* 图片裁剪工具,使用方法:
* 第一步、var imgclip = new ImgClip(outer[, config]);在outer内构建裁剪作业的canvas,config为设置默认配置
* 第二步、imgclip.start(img[, config]);传入需要处理的图片img,此次的config区别于构造函数的config,仅对当前图标生效
* 第三步、imgclip.rotate(deg);旋转图片,deg表示旋转角度
* 第三步、imgclip.toBase64([type, quality]);输出裁剪图片,返回图片base64数据,type表示生成图片类型[jpg,png,webp]默认为png,quality表示生成图片质量[0-1]
* 第三步、imgclip.toBlob(callback[, type, quality]);输出裁剪图片,callback接收裁剪后图片的Blob数据,type,quality参数同imgclip.toBase64
* 第三步、imgclip.clip([type, quality]);裁剪图片,同imgclip.toBase64
*/
(function($){
"use strict";
const Vector = require('./vector.js');
function ImgClip(outer, config){
if(!(this instanceof ImgClip)) return new ImgClip(outer, config);
if(!outer.nodeType || outer.nodeType != 1) throw new Error( "缺少容器元素" );
if(outer.imgclip) throw new Error( "当前容器已经构建过ImgClip了,不能重复操作" );
//默认配置
this.gc = {
//输出图片的宽高比,如果不为零,剪切框伸缩会固定
ratioWH: 0,
//输出分辨率,设置后会使scale无效,可能会导致图片形变,字符串,格式为width*height,如400*400
outputWH: false,
}
this.gc = Object.assign(this.gc, config);
//创建样式
if(!ImgClip.style && !ImgClip.className){
var className = ImgClip.className = 's94_ImgClip'+(new Date()).getTime();
ImgClip.style = document.createElement('style');
ImgClip.style.innerHTML = `
.${className}{position: relative;width: 100%;height: 100%;background: none;margin: 0;padding: 0;}
.${className} *{box-sizing: border-box;margin: 0;padding: 0;touch-action: none;}
.${className}>canvas{position: absolute;width: 100%;height: 100%;background: none;z-index:1;}
.${className}>div{position: absolute;width: 100%;height: 100%;z-index:2;display: flex;flex-direction: column;}
.${className}>div>div{display: flex;}
.${className} dd,
.${className} dt{background: rgba(0,0,0,.5);}
.${className} dt{flex-grow: 1;}
.${className} dd{position: relative;font-size: 1em;}
.${className} dd>p{display: block;width: 0.4em;height: 0.4em;position: absolute;border: 0.06em none #ccc;}
.${className} dd>.fx-tl{left: -0.06em;top: -0.06em;border-style: solid none none solid;cursor: nw-resize;}
.${className} dd>.fx-tr{right: -0.06em;top: -0.06em;border-style: solid solid none none;cursor: ne-resize;}
.${className} dd>.fx-br{right: -0.06em;bottom: -0.06em;border-style: none solid solid none;cursor: se-resize;}
.${className} dd>.fx-bl{left: -0.06em;bottom: -0.06em;border-style: none none solid solid;cursor: sw-resize;}
.${className} dd>.fx-t{display: none;left: 50%;top: -0.06em;transform: translateX(-50%);border-top-style: solid;cursor: n-resize;}
.${className} dd>.fx-r{display: none;right: -0.06em;top: 50%;transform: translateY(-50%);border-right-style: solid;cursor: e-resize;}
.${className} dd>.fx-b{display: none;left: 50%;bottom: -0.06em;transform: translateX(-50%);border-bottom-style: solid;cursor: s-resize;}
.${className} dd>.fx-l{display: none;left: -0.06em;top: 50%;transform: translateY(-50%);border-left-style: solid;;cursor: w-resize;}`;
document.querySelector('head').appendChild(ImgClip.style);
}
//创建对象
this.box = document.createElement('div');
this.box.className = ImgClip.className;
this.canvas = document.createElement('canvas');
this.clip_rect = document.createElement('div');
this.clip_rect.innerHTML = '<dd></dd><div><dd></dd><dd></dd><dt></dt></div><dt></dt>';
this.clip_rect.style.display = 'none';
this.box.appendChild(this.canvas);
this.box.appendChild(this.clip_rect);
outer.appendChild(this.box);
var dd = this.box.querySelectorAll('dd');
this.clip_rect['box_top'] = dd[0];
this.clip_rect['box_left'] = dd[1];
this.clip_rect['box_center'] = dd[2];
$(this.clip_rect['box_center']).css({cursor:'move', background:'none', border:'1px dashed #fff'});
this.ctx = this.canvas.getContext('2d');
//开启平滑模式
this.ctx.mozImageSmoothingEnabled = true;
this.ctx.webkitImageSmoothingEnabled = true;
this.ctx.msImageSmoothingEnabled = true;
this.ctx.imageSmoothingEnabled = true;
//插入dom对象
outer.imgclip = this;
this.ltwh = {};
var imgclip = this,
clip_rect = this.clip_rect,
canvas = this.canvas,
cache={l:0, t:0, w:0, h:0, ex:0, ey:0}; //用于记录touchstart时剪切框的ltwh状态和事件坐标ex,ey
//剪切框构建及事件绑定
$.Touch.on(clip_rect['box_center'], 'change', function(res){
window.event.stopPropagation();
var ltwh = imgclip.ltwh;
switch (res.type){
case 'start':{
cache = Object.assign(cache, ltwh);
cache.ex = res.x, cache.ey = res.y;
}break;
case 'move':{
imgclip.change({
l: cache.l + (res.x - cache.ex),
t: cache.t + (res.y - cache.ey)
})
}break;
}
});
(['tl','tr','bl','br','t','r','b','l']).forEach(function(fx){
var fx_dom = document.createElement('p');
fx_dom.className = 'fx-'+fx;
clip_rect['box_center'].appendChild(fx_dom);
clip_rect['fx_'+fx] = fx_dom;
$.Touch.on(fx_dom, 'change',function(res){
window.event.stopPropagation();
var ud={}, dt = {x: res.x - cache.ex, y: res.y - cache.ey},
cacheData = imgclip.cacheData, drawData = imgclip.drawData, ltwh = imgclip.ltwh;
switch (res.type){
case 'start':{
cache = Object.assign(cache, ltwh);
cache.ex = res.x, cache.ey = res.y;
}break;
case 'move':{
if (imgclip.ratioWH) {
var o = new Vector( imgclip.ratioWH, 1).unit();
o.x *= fx.indexOf('l')==-1 ? 1 : -1;
o.y *= fx.indexOf('t')==-1 ? 1 : -1;
dt = o.multiply(o.dot(dt))
}
for (let i = 0; i < fx.length; i++) {
switch (fx[i]) {
case 'r': ud['w'] = cache.w + dt.x; break;
case 'b': ud['h'] = cache.h + dt.y; break;
case 'l': {
ud['l'] = cache.l + dt.x;
ud['w'] = cache.w - dt.x;
} break;
case 't': {
ud['t'] = cache.t + dt.y;
ud['h'] = cache.h - dt.y;
} break;
}
}
imgclip.change(ud);
}break;
case 'end':{
function change_c(r,originP){
var xy = imgclip.getRectForCanvas(); //选框在“正坐标”上的位置
var kc_xy = {x:xy.x+xy.w*0.5, y:xy.y+xy.h*0.5};
var kc_img_xyP = Vector(kc_xy).divide(drawData.scale)
.add(rotateRes(cacheData.originP, drawData.rotate-cacheData.rotate));
cacheData.originP = originP || kc_img_xyP;
drawData.origin = rotateRes(cacheData.originP, -drawData.rotate);//转换到“旋转后的坐标”上的位置
kc_xy = Vector(kc_img_xyP).subtract(cacheData.originP);
r = r || 2;
drawData.scale *= r;
var ud={};
ud.w = ltwh.w * r;
ud.h = ltwh.h * r;
ud.l = canvas.offsetWidth*0.5 - ud.w*0.5 + kc_xy.x*drawData.scale/imgclip.ratio;
ud.t = canvas.offsetHeight*0.5 - ud.h*0.5 + kc_xy.y*drawData.scale/imgclip.ratio;
if(ud.l<0) { ud.w += ud.l;ud.l = 0; }
if(ud.l+ud.w>canvas.offsetWidth) {ud.w = canvas.offsetWidth-ud.l}
if(ud.t<0) { ud.h += ud.t;ud.t = 0; }
if(ud.t+ud.h>canvas.offsetHeight) {ud.h = canvas.offsetHeight-ud.t}
cacheData.rotate = drawData.rotate;
imgclip.change(ud);
draw(imgclip);
}
if(ltwh.w<=cache.w && ltwh.h<=cache.h){
if(ltwh.w < 0.3*canvas.offsetWidth && ltwh.h < 0.3*canvas.offsetHeight && drawData.scale < imgclip.max_scale ){
change_c(2);
}
}else{
if( (ltwh.w > 0.7*canvas.offsetWidth || ltwh.h > 0.7*canvas.offsetHeight) && drawData.scale > imgclip.min_scale){
if(drawData.scale <= 2*imgclip.min_scale){
change_c(imgclip.min_scale/drawData.scale, {x:0, y:0});
}else{
change_c(0.5);
}
}
}
}break;
}
})
})
}
//勾股
function hypot(a, b){
return Math.hypot ? Math.hypot(a, b) : Math.sqrt(a*a+b*b);
}
//计算点在坐标系旋转后的坐标
function rotateRes(x, y, deg){
if (typeof(x)=="object") {
deg = y;y = x.y;x = x.x;
}
return {
x: x*Math.cos(deg) - y * Math.sin(deg),
y: x*Math.sin(deg) + y * Math.cos(deg),
}
}
function draw(imgclip){
if(draw.runing) return;
draw.runing = true;
requestAnimationFrame(function(){
var ctx = imgclip.ctx,
canvas = imgclip.canvas,
img = imgclip.img,
drawData = imgclip.drawData;
//开始绘画
//规定合成操作类型为:在现有的画布内容后面绘制新的图形。
ctx.globalCompositeOperation = 'destination-over';
//清除
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
//移动坐标原点到中心
ctx.translate(canvas.width*0.5, canvas.height*0.5);
//旋转坐标系
ctx.rotate(drawData.rotate);
//画图片
var dxy = Vector({x:-img.width*0.5, y:-img.height*0.5}).subtract(drawData.origin).multiply(drawData.scale);
ctx.drawImage(img, 0, 0, img.width, img.height, dxy.x, dxy.y, img.width*drawData.scale, img.height*drawData.scale);
ctx.restore();
draw.runing = undefined;
})
}
function clip(imgclip){
if(!(imgclip.img instanceof Image)) throw new Error('图片不存在,请先调用start方法传入图片');
if (!imgclip.clipCanvas) {
imgclip.clip_canvas = document.createElement('canvas');
imgclip.clip_ctx = imgclip.clip_canvas.getContext('2d');
}
var clip_canvas = imgclip.clip_canvas;
var clip_ctx = imgclip.clip_ctx;
var ctc={};
for (var k in imgclip.ltwh) {
ctc[k] = imgclip.ltwh[k]*imgclip.ratio;
}
var wh = (typeof(imgclip.outputWH)=="string" ? imgclip.outputWH : '').match(/(\d+)?(\*(\d+))?/);
clip_canvas.width = parseInt(wh[1]);
clip_canvas.height = parseInt(wh[3]);
if (clip_canvas.width && !clip_canvas.height) {
clip_canvas.height = clip_canvas.width * (ctc.h/ctc.w);
}else if(!clip_canvas.width && clip_canvas.height){
clip_canvas.width = clip_canvas.height * (ctc.w/ctc.h);
}else if(!clip_canvas.width && !clip_canvas.height){
clip_canvas.width = ctc.w / imgclip.drawData.scale;
clip_canvas.height = ctc.h / imgclip.drawData.scale;
}
var img = imgclip.img, drawData = imgclip.drawData;
var scale = drawData.scale * (clip_canvas.width / ctc.w);
clip_ctx.save();
//移动坐标原点到中心
clip_ctx.translate(clip_canvas.width*0.5, clip_canvas.height*0.5);
//移动坐标原点到裁剪选择框的中心
var xy = imgclip.getRectForCanvas();
var rectOffsetCenter = Vector({x:xy.x+xy.w*0.5, y:xy.y+xy.h*0.5}).multiply( clip_canvas.width / ctc.w );
clip_ctx.translate(-rectOffsetCenter.x, -rectOffsetCenter.y);
//旋转坐标系
clip_ctx.rotate(drawData.rotate);
//画图片
var dxy = Vector({x:-img.width*0.5, y:-img.height*0.5}).subtract(drawData.origin).multiply(scale);
clip_ctx.drawImage(img, 0, 0, img.width, img.height, dxy.x, dxy.y, img.width*scale, img.height*scale);
return clip_ctx;
}
//操作方法
ImgClip.prototype.change = function(ud){
var cache = Object.assign({}, this.ltwh, ud);
if('l' in ud || 'w' in ud){
if(cache.l<0 || cache.l+cache.w>this.canvas.offsetWidth) {
if(this.ratioWH && 'w' in ud){
return
}else{
delete cache.l;delete cache.w;
}
}
}
if('t' in ud || 'h' in ud){
if(cache.t<0 || cache.t+cache.h>this.canvas.offsetHeight) {
if(this.ratioWH && 'h' in ud){
return
}else{
delete cache.t;delete cache.h;
}
}
}
Object.assign(this.ltwh, cache);
var clip_rect = this.clip_rect;
//设置选框样式
clip_rect['box_left'].style.width = this.ltwh.l+'px';
clip_rect['box_top'].style.height = this.ltwh.t+'px';
clip_rect['box_center'].style.width = this.ltwh.w+'px';
clip_rect['box_center'].style.height = this.ltwh.h+'px';
if(!this.ratioWH){
if (this.ltwh.w < 4*this.fxw) {
clip_rect['fx_t'].style.display = 'none';
clip_rect['fx_b'].style.display = 'none';
} else{
clip_rect['fx_t'].style.display = 'block';
clip_rect['fx_b'].style.display = 'block';
}
if(this.ltwh.h < 4*this.fxw) {
clip_rect['fx_l'].style.display = 'none';
clip_rect['fx_r'].style.display = 'none';
}else{
clip_rect['fx_l'].style.display = 'block';
clip_rect['fx_r'].style.display = 'block';
}
}
}
ImgClip.prototype.getRectForCanvas = function(){
return {
x: this.ltwh.l*this.ratio - this.canvas.width*0.5,
y: this.ltwh.t*this.ratio - this.canvas.height*0.5,
w: this.ltwh.w*this.ratio,
h: this.ltwh.h*this.ratio,
}
}
ImgClip.prototype.start = function(img, config){
if(!(img instanceof Image)) throw new Error( "img参数必须为Image" );
img.wph = img.width / img.height;
this.img = img;
this.fxw = $.rem.get()*0.4;
if(Math.min(this.box.offsetWidth, this.box.offsetHeight) < this.fxw*2) throw new Error( "当前容器尺寸太小,宽高至少为:"+this.fxw*3 );
this.box.style.fontSize = (this.fxw/0.4)+'px';
this.clip_rect.style.display = 'flex';
this.config = $.merge(this.gc, config);
this.ratioWH = this.config.ratioWH;
this.outputWH = this.config.outputWH;
var canvas = this.canvas;
canvas.wph = canvas.offsetWidth / canvas.offsetHeight;
this.min_scale = (canvas.wph > 1 ? canvas.offsetHeight : canvas.offsetWidth) / hypot(img.width, img.height);
this.max_scale = this.min_scale * 2;
this.ratio = 1;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
//初始化
this.drawData = {
rotate: 0, //旋转角度
origin: {x:0, y:0},
scale: (img.wph > canvas.wph ? canvas.width/img.width : canvas.height/img.height), //放大倍数
}
this.cacheData = {
rotate: 0,
originP: {x:0,y:0}
}
var ltwh = {l:0, t:0, w:canvas.offsetWidth, h:canvas.offsetHeight};
if (this.ratioWH && this.ratioWH > canvas.wph) {
ltwh.h = canvas.offsetWidth / this.ratioWH;
ltwh.t = (canvas.offsetHeight - ltwh.h)/2;
} else if(this.ratioWH){
ltwh.w = canvas.offsetHeight * this.ratioWH;
ltwh.l = (canvas.offsetWidth - ltwh.w)/2
}
this.change(ltwh);
draw(this);
return this;
}
ImgClip.prototype.rotate = function(d){
this.drawData.rotate = d;
draw(this);
return this;
}
ImgClip.prototype.clean = function(d){//清除
this.img = undefined;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.clip_rect.style.display = 'none';
return this;
}
ImgClip.prototype.toBlob = function(callback,type,quality){
clip(this);
var mime_map = {'jpg': 'image/jpeg','png':'image/png','webp': 'image/webp'};
callback = typeof(callback)=='function' ? callback : function(){};
quality = typeof(quality)!="number" ? 1 : quality;
var mime = type in mime_map ? mime_map[type] : mime_map['png'];
var imgclip = this;
return typeof(Promise)!='undefined' ? new Promise(function(ok){
imgclip.clip_canvas.toBlob(function(data){
callback(data);
ok(data);
}, mime, quality);
}) : this.clip_canvas.toBlob(callback, mime, quality);
},
ImgClip.prototype.toBase64 = function(type,quality){
clip(this);
var mime_map = {'jpg': 'image/jpeg','png':'image/png','webp': 'image/webp'};
quality = typeof(quality)!="number" ? 1 : quality;
var mime = type in mime_map ? mime_map[type] : mime_map['png'];
return this.clip_canvas.toDataURL(mime, quality);
},
module.exports = ImgClip;
})(require('s94-web'));