canvas自由拼圖

自由拼圖?

自由拼圖是美圖秀秀中的一個功能,它可讓用戶在背景圖片上插入本身的圖片,並能夠對插入圖片旋轉,拖拽,縮放。固然,若是用戶對插入的圖片不滿意,能夠用另外一張圖片替換選中的圖片,或者刪除選中圖片。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();
    }

canvas初始化

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繪製圖片

接着圖片選取完成後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事件操做

mousedown

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();
    }
};

mousemove

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);
};

mouseup

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";
        ...
    });

拼圖轉換成base64字符串

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字符串就能夠了

下載

相關文章
相關標籤/搜索