UNPKG

s94-imgclip

Version:

图片裁剪工具

408 lines (378 loc) 16.4 kB
/** * 图片裁剪工具,使用方法: * 第一步、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'));