自由拼圖是美圖秀秀中的一個功能,它可讓用戶在背景圖片上插入本身的圖片,並能夠對插入圖片旋轉,拖拽,縮放
。固然,若是用戶對插入的圖片不滿意,能夠用另外一張圖片替換
選中的圖片,或者刪除
選中圖片。javascript
本屌以前用actionscript實現過,參見仿美圖秀秀的自由拼圖(注意裏面基本上沒代碼說明).
這裏用html5的canvas實現。
這個是本屌博客園去年一篇文章的效果,內容也是說的這個,不過很無恥的只貼了代碼,沒有任何說明。
下面的是錄製的效果視頻
http://v.youku.com/v_show/id_XMTM3MTgzMzIyOA==.htmlhtml
<div id='html5_puzzle' ms-controller='html5_puzzle' class='ms-controller'> <div id='puzzle_model'> <ul> <li> <img src='imgs/small_img/1.jpg'> ... </li> </ul> </div ><div id='puzzle_canvas'> <div id='canvas_inner'> <canvas id="main_canvas"></canvas> <img class='middleware_img'> <img class='middleware_img'> ... <div id='canvas_menu'> <a href='javascript:void(0)' id='puzzle_delete'>刪除</a> <a href='javascript:void(0)' id='puzzle_update'>更改圖片</a> </div> <img id="puzzle_bg" src="imgs/1.jpg" width='548' height='411'/> <canvas id='canvas_middleware'></canvas> </div> </div> <div id='puzzle_control'> <a href="javascript:;" id="puzzle_add"> <span>添加</span> <input type="file" multiple="" id='puzzle_add_input'> </a ><a id='puzzle_upload'>上傳</a> </div> </div>
#main_canvas
是主要的工做區域html5
.middleware_img
是若干<img>
,<input>
讀取從外面選中的若干圖片數據,將數據以Base64編碼,依次寫入<img>
的src屬性。這些<img>
後面將會被看成參數,傳給canvasjava
#canvas_middleware
是代理。<input>
讀取的數據由#canvas_middleware
調用toDataURL()
方法編碼成Base64形式git
#puzzle_bg
和.middleware_img
做用差很少,只不過這裏是將背景圖片的src傳給它。它會將背景圖片數據傳給canvasgithub
var img_upload_instance=new img_upload({ add_btn:'puzzle_add', onSelect:onSelect//讀取圖片回調 });
define("html5_imgupload",["avalon-min"], function(avalon){ var html5_img_upload=function(options){ this.init(options); } html5_img_upload.prototype = { init : function(options) { //若是有自定義屬性,覆蓋默認屬性 avalon.mix(html5_img_upload.prototype,options); this.init_events(); }, init_events : function() { var _this=this; avalon.bind($(this.add_btn),'change',function(e) { _this.get_files(e); }); }, file_filter:[], ori_images:[], add_btn:null, upload_btn:null, max_upload_num:9, onSelect:function(file_filter){}, _start:0,//已經讀取圖片數量 filter:function(files) { var arrFiles=[]; for (var i=0,file;file=files[i];i++){ if(this._start+i<this.max_upload_num){ if(file.type.indexOf("image")==0||(!file.type&&/\.(?:jpg|png|gif)$/. test(file.name))) arrFiles.push(file); else { alert('文件'+file.name+'不是圖片'); } }else{ alert('一次最多能上傳'+this.max_upload_num+'張圖片'); break; } } return arrFiles; }, get_files:function(e) { var files=e.target.files||e.dataTransfer.files; this.file_filter=this.file_filter.concat(this.filter(files)); this.onSelect(this.file_filter); }, destroy_hook:function(){}, _destroy:function(){ this.file_filter=[]; $(this.add_btn).value=''; this.ori_images=[]; this._start=0; this.destroy_hook(); }, upload:function(i,file_filter){} }; return html5_img_upload; });
自定義圖片選取回調web
<div id='puzzle_canvas'> <div id='canvas_inner'> <canvas id="main_canvas"></canvas> <img ms-repeat='middleware_list' ms-attr-src='el' class='middleware_img'> ... </div> </div>
function onSelect(file_filter){ for(var i=this._start,len=file_filter.length;i<len;i++) {//遍歷選中圖片 var reader=new FileReader(); reader.onload=(function(i){//圖片讀取的回調 return function(e){ var dataURL=e.target.result,canvas_middleware=$('canvas_middleware'), ctx=canvas_middleware.getContext('2d'),img=new Image(); img.onload = function() {//圖片加載的回調 if(img.width>200||img.height>200){//等比例縮放 var prop=Math.min(200/img.width,200/img.height); img.width=img.width*prop; img.height=img.height*prop; } //設置中轉canvas尺寸 canvas_middleware.width=img.width; canvas_middleware.height=img.height; ctx.drawImage(img, 0, 0, img.width, img.height); //將讀取圖片轉換成base64,寫入.middleware_list的src html5_puzzle.middleware_list.push(canvas_middleware. toDataURL("image/jpeg")); if(!file_filter[i+1]){ //圖片延遲加載到canvas,由於canvas有個讀取過程,可是沒有回調 var t = window.setTimeout(function() { canvas_puzzle.init(); clearTimeout(t); }, 1000); } }; img.src = dataURL; }; delete reader; })(i); reader.readAsDataURL(file_filter[i]);//開始讀取圖片 } this._start=0; img_upload_instance._destroy(); }
var canvas = new canvasElement.Element(); canvas.init('main_canvas', { width : canvas_w, height : canvas_h });
Canvas.Element.prototype.init = function(el, oConfig) { if (el == '') { return; } this._initElement(el); this._initConfig(oConfig); this._createCanvasBackground(); this._createContainer(); this._initEvents(); this._initCustomEvents(); }; Canvas.Element.prototype._initElement = function(el) { this._oElement = document.getElementById(el); this._oContextTop = this._oElement.getContext('2d'); }; Canvas.Element.prototype._initCustomEvents = function() {//設置自定義事件 this.onRotateStart = new Canvas.CustomEvent('onRotateStart'); this.onRotateMove = new Canvas.CustomEvent('onRotateMove'); this.onRotateComplete = new Canvas.CustomEvent('onRotateComplete'); this.onDragStart = new Canvas.CustomEvent('onDragStart'); this.onDragMove = new Canvas.CustomEvent('onDragMove'); this.onDragComplete = new Canvas.CustomEvent('onDragComplete'); }; Canvas.Element.prototype._initConfig = function(oConfig) { this._oConfig = oConfig; this._oElement.width = this._oConfig.width; this._oElement.height = this._oConfig.height; this._oElement.style.width = this._oConfig.width + 'px'; this._oElement.style.height = this._oConfig.height + 'px'; }; Canvas.Element.prototype._initEvents = function() { var _this=this; avalon.bind(this._oElement,'mousedown',function(e){ _this.onMouseDown(e); }); avalon.bind(this._oElement,'mouseup',function(e){ _this.onMouseUp(e); }); avalon.bind(this._oElement,'mousemove',function(e){ _this.onMouseMove(e); }); }; Canvas.Element.prototype._createContainer = function() { var canvasEl = document.createElement('canvas'); canvasEl.id = this._oElement.id + '-canvas-container'; ... this._oContextContainer = oContainer.getContext('2d'); }; Canvas.Element.prototype._createCanvasBackground = function() { var canvasEl = document.createElement('canvas'); canvasEl.id = this._oElement.id + '-canvas-background'; ... this._oContextBackground = oBackground.getContext('2d'); };
能夠看到初始化過程當中屢次建立<canvas>
json
main_canvas-background-canvas
繪製背景圖片canvas
main_canvas-container-canvas
繪製除當前操做的圖片外的其他圖片數組
main_canvas
繪製當前操做的圖片
從上到下:main_canvas
->main_canvas-container-canvas
->main_canvas-background-canvas
canvas context:
main_canvas
->_oContextTop
main_canvas-container-canvas
->_oContextContainer
main_canvas-background-canvas
->_oContextBackground
這下就看到canvas自由拼圖的原理了,原來是3個canvas上下重疊起來,操做的時候對不一樣的canvas進行不一樣目標的繪製。
接着圖片選取完成後canvas_puzzle.init()
var canvas_img=[]; ... var canvas_puzzle= function() { return { init : function() { var img_list=document.querySelectorAll('.middleware_img'); //第一張做爲背景圖片 canvas_img[0]=new canvasImg.Img($('puzzle_bg'), {}); avalon.each(img_list,function(i,el){ canvas_img.push(new canvasImg.Img(el, {})); canvas.addImage(canvas_img[i+1]); }); canvas.setCanvasBackground(canvas_img[0]); ... } }; }();
canvasImg.Img
是canvas對圖片的封裝.第一個參數是<img>
,前面提到的#puzzle_bg和.middleware_img就是做爲第一個參數傳入canvasImg.Img
.第二個參數用來自定義圖片的一些屬性,好比邊框寬度,4個角的大小等,若是定義的話會覆蓋默認值。
canvasImg.Img
封裝圖片後的效果
canvas.addImage(canvas_img[i+1])
將canvas對除背景圖片外的圖片的封裝
繪製到canvas上
Canvas.Element.prototype.addImage = function(oImg) { if(isEmptyObject(this._aImages)) this._aImages = []; this._aImages.push(oImg); this.renderAll(false,true); };
_aImages
是保存canvas圖片封裝的數組,renderAll()
方法很重要,後面會說到。
canvas.setCanvasBackground(canvas_img[0])
將背景圖片繪製到canvas
Canvas.Element.prototype.setCanvasBackground = function(oImg) { this._backgroundImg = oImg; var originalImgSize = oImg.getOriginalSize(); this._oContextBackground.drawImage(oImg._oElement, 0, 0, originalImgSize.width, originalImgSize.height); }; Canvas.Img.prototype.getOriginalSize = function() {//得到canvas尺寸 return { width: this._oElement.width, height: this._oElement.height } };
Canvas.Element.prototype.onMouseDown = function(e) { $('canvas_menu').style.display="none"; var mp = this.findMousePosition(e);//鼠標相對位置 if (this._currentTransform != null || this._aImages == null) { return; } var oImg = this.findTargetImage(mp, false);//獲取目標圖片 //事件觸發位置是否是在4個角上 var action = (!this.findTargetCorner(mp, oImg)) ? 'drag' : 'rotate'; if (action == "rotate") { this.onRotateMove.fire(e);//觸發自定義事件 } else if (action == "drag") { this.onDragMove.fire(e); } this._prevTransform=this._currentTransform = { target: oImg, action: action, scalex: oImg.scalex, offsetX: mp.ex - oImg.left, offsetY: mp.ey - oImg.top, ex: mp.ex, ey: mp.ey, left: oImg.left, top: oImg.top, theta: oImg.theta }; //設置菜單位置 $('canvas_menu').style.transform='rotate('+oImg.theta*180/3.14+'deg)'; $('canvas_menu').style.left=oImg.left+"px"; $('canvas_menu').style.top=oImg.top+"px"; this.renderAll(false,false); };
this._prevTransform
保存當前目標圖片狀態,替換圖片時會用到這個變量
renderAll()
方法是整個繪製的核心方法
Canvas.Element.prototype.renderAll=function(allOnTop,allowCorners) { var containerCanvas=allOnTop?this._oContextTop:this._oContextContainer; this._oContextTop.clearRect(0,0,parseInt(this._oConfig.width),parseInt(this._oConfig.height)); containerCanvas.clearRect(0,0,parseInt(this._oConfig.width),parseInt(this._oConfig.height)); if(allOnTop){//全部圖片都要在最上面 var originalImgSize=this._backgroundImg.getOriginalSize(); //在最上層canvas繪製背景圖片 this._oContextTop.drawImage(this._backgroundImg._oElement, 0, 0, originalImgSize.width,originalImgSize.height); } for(var i=0,l=this._aImages.length-1;i<l;i+=1){ this.drawImageElement(containerCanvas,this._aImages[i],allowCorners); } var last_aImages=this._aImages[this._aImages.length-1]; this.drawImageElement(this._oContextTop,last_aImages ,allowCorners); };
能夠看到,若是allOnTop=false
,從_aImages
封裝圖片的數組的第一個元素到倒數第二個元素,會繪製到中間一層container-canvas
,而_aImages
數組的最後一個元素,即當前操做的圖片,會繪製到最上面一層top-canvas
,固然若是改變操做對象,_aImages
數組也會相應的變化,保證當前操做的圖片在_aImages
數組的最後一個位置。
若是allOnTop=true
,_aImages
數組中的全部圖片還有背景圖片都會被繪製到最上面一層top-canvas
.
設置allOnTop參數
的目的在於上傳時只有全部圖片都在一個canvas context上,調用toDataURL()
方法,就能得到整個拼圖的base64字符串。
第二個參數allowCorners
表示繪製時是否添加邊框,邊角。
前面將選中圖片及背景圖片繪製到canvas,最後this.renderAll(false,true)讓邊框,邊角可見,是爲了讓用戶知道圖片能夠進行操做。
Canvas.Element.prototype.addImage = function(oImg) { ... this.renderAll(false,true); };
另外,不管怎麼設置,renderAll()
方法最終都會調用drawImageElement()
方法進行實際意義上的繪製
Canvas.Element.prototype.drawImageElement = function(context,oImg,allowCorners) { if(oImg){ oImg.cornervisibility=allowCorners; var offsetY = oImg.height / 2; var offsetX = oImg.width / 2; context.save(); context.translate(oImg.left, oImg.top); context.rotate(oImg.theta); context.scale(oImg.scalex, oImg.scaley); this.drawBorder(context, oImg, offsetX, offsetY); var originalImgSize = oImg.getOriginalSize(); var polaroidHeight =((oImg.height-originalImgSize.height)-(oImg.width-originalImgSize.width))/2; context.drawImage(oImg._oElement,-originalImgSize.width/2,(-originalImgSize.height)/2-polaroidHeight, originalImgSize.width,originalImgSize.height); if (allowCorners) this.drawCorners(context, oImg, offsetX, offsetY); context.restore(); } };
Canvas.Element.prototype.onMouseMove = function(e) { var mp = this.findMousePosition(e); if(this._aImages == null) return; if(this._currentTransform==null){ var targetImg = this.findTargetImage(mp, true); this.setCursor(mp, targetImg); } else { if (this._currentTransform.action == 'rotate') { this.rotateImage(mp); this.scaleImage(mp); this.onRotateMove.fire(e); } else { this.translateImage(mp); this.onDragMove.fire(e); } this.renderTop(); } };
裏面的renderTop()
方法只在最上層canvas繪製當前操做的圖片
Canvas.Element.prototype.renderTop = function() { this._oContextTop.clearRect(0,0,parseInt(this._oConfig.width), parseInt(this._oConfig.height)); this.drawImageElement(this._oContextTop, this._aImages[this._aImages.length-1],true); };
Canvas.Element.prototype.onMouseUp = function(e) { if (this._aImages == null) { return; } var target=this._currentTransform.target; if (target) target.setImageCoords();//重置圖片canvas封裝 if(this._currentTransform!= null&&this._currentTransform.action=="rotate") { this.onRotateComplete.fire(e); } else if (this._currentTransform!=null&&this._currentTransform.action == "drag"){ this.onDragComplete.fire(e); } this._currentTransform = null; this.renderTop(); if(this._aImages.length>0)//沒有選中的圖片 $('canvas_menu').style.display="block"; };
<div id='canvas_menu'> ... <a href='javascript:void(0)' id='puzzle_update'>更改圖片</a> </div>
avalon.bind($('puzzle_update'),'click',function(){ update_puzzle=true; $('puzzle_add_input').click(); });
能夠看到,這裏依然使用了<input>
點擊選中圖片,不過設置了update_puzzle=true
,表示當前處在替換圖片的狀況下
function onSelect(file_filter){ for(var i=this._start,len=file_filter.length;i<len;i++) { var reader=new FileReader(); reader.onload=(function(i){//圖片讀取的回調 return function(e){ ... img.onload = function() { ... var t = window.setTimeout(function() { if(!update_puzzle) canvas_puzzle.init(); else{ //當前操做圖片 var target=canvas._prevTransform.target; //傳入替換的圖片和被替換圖片的位置,狀態信息 canvas._aImages[getCurImg()]=new canvasImg.Img(document. querySelectorAll('.middleware_img')[0],{ top:target.top, left:target.left, scalex:target.scalex, scaley:target.scaley, angle:canvas.curAngle }); //從新繪製最上層 canvas.renderTop(); html5_puzzle.middleware_list.clear(); update_puzzle=false; } clearTimeout(t); }, 1000); }; }; delete reader; })(i); reader.readAsDataURL(file_filter[i]); } ... } function getCurImg(){//獲取讀取操做圖片在_aImages中的位置 var oImg=canvas._prevTransform.target; for(var i=0;i<canvas._aImages.length;i++){ if(canvas._aImages[i]._oElement.src==oImg._oElement.src){ return i; } } }
<div id='canvas_menu'> <a href='javascript:void(0)' id='puzzle_delete'>刪除</a> ... </div>
avalon.bind($('puzzle_delete'),'click',function(){ canvas._aImages.splice(getCurImg(),1);//從_aImages數組中刪除 canvas.renderAll(false,false);//從新繪製 $('canvas_menu').style.display="none"; ... });
Canvas.Element.prototype.canvasTo = function(format) {//canvas=>dataurl this.renderAll(true,false);//全部圖片都繪製到最上層,而且不繪製邊框,邊角 if (format == 'jpeg' || format == 'png') { return this._oElement.toDataURL('image/'+format); } };
avalon.post('...',{ imgData:canvas.canvasTo('jpeg').substr(22) },function(data){ ... },'json');
後臺用Base64解析imgData字符串就能夠了