在去年的時候也實現過合成海報的功能,不過當時時間倉促,實現的比較簡單。
就一個旋轉功能,圖片也不能拖動放大,也不能裁剪。
去年的實現可以參考《移動圖片操作--上傳》和《移動圖片操作--預覽旋轉合成》
這次有時間就實現一個功能稍微多點的海報。
一、概要
![]() |
![]() |
![]() |
第一屏 |
第二屏 | 第三屏 |
總共有三屏,第一屏是選擇圖片,第二屏是合成圖片,第三屏是顯示結果圖,可保存分享朋友圈。
頁面內容不是很多,分析起來也比較簡單。
1)每一屏的左右邊距相同,上邊距各不相同。
2)屏幕內的元素,大部分是居中,有些特殊邊距的可用絕對定位,例如第一屏中父親圖與標語圖,兩張圖有重疊部分。
3)第2和3屏中的按鈕布局可以用Flex中的兩端對齊。
4)4種按鈕,可將背景制作成Sprite 圖,方便重用。1種彈出框,1種Loading。
5)有3種動畫,放大、脈沖以及旋轉360°。
6)這次實現的難點是拖動、裁剪和旋轉,需要經過邏輯計算高寬、坐標等。
二、涉及的知識點
1)Sprite圖
移動端的Sprite圖在前面一篇《一張H5游戲頁引起的思考》曾重點介紹過。
在移動端的話,位置就是用百分比來計算。從上面的總覽圖中可以看到多種按鈕背景,有幾個就是字不一樣,可以重復使用。
2)PrimusUI
PrimusUI是前面一段時間整理的一個微型UI庫,為了提升開發效率,提取公用模塊而制作的。
有多個模塊可以使用,此次就用了三個模塊normalize、layout與loading。
具體內容可以參考前面一段時間寫的一篇介紹文《小身材大用途,用PrimusUI駕馭你的頁面》。
3)High DPI Canvas
引入High DPI Canvas,是為了解決在高清屏的設備中,繪制在 canvas 中的圖形(包括文字)都會出現模糊的問題。
在demo代碼中有一張hidpi.html頁面,就是在比較引入此插件后表現的區別,下圖是在iphone6中展現的樣子。
可以看到原生的比較模糊,而引入了插件后就變的清晰了。原理就是讓Canvas中的1個像素等於屏幕中的1個物理像素,關於屏幕的概念可以參考《移動開發屏幕適配分析》
下面是一段插件中的代碼,就是計算devicePixelRatio(設備像素比)與webkitBackingStorePixelRatio(Canvas緩沖區的像素比),做個除法。
然后將Canvas的width和height根據這個比來放大,而CSS中的width和height再縮小回原來的,以此達到1像素的對應。
backingStore = context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1; ratio = (window.devicePixelRatio || 1) / backingStore; if (ratio > 1) { this.style.height = this.height + 'px'; this.style.width = this.width + 'px'; this.width *= ratio; this.height *= ratio; }
4)touch.js
touch.js是個開源的手勢庫,第二屏中的拖動和捏(雙指放大縮小)就是通過這個庫實現的。
touch.on(touchPad, 'drag', function(ev) { //拖動邏輯 }); touch.on(touchPad, 'pinch', function(ev) { //捏的邏輯 });
5)FileReader
使用FileReader對象,web應用程序可以異步的讀取存儲在用戶計算機上的文件(或者原始數據緩沖)內容,可以使用File對象或者Blob對象來指定所要處理的文件或數據。
在執行上傳插件的“change”中,就是通過此對象獲取圖片的data:URL。
var file = $(this)[0].files[0]; var reader = new FileReader(); reader.readAsDataURL(file); // 將文件以Data URL形式進行讀入頁面 reader.onload = function() { var base64 = this.result; };
三、實現
1)音頻控制
為了營造父親節氣氛,特地選了首接地氣的歌曲配合頁面。
播放器就使用了HTML5標簽“audio”。
<audio id="audio" src="music.mp3" type="audio/mpeg"></audio>
這里注意下,IOS是禁止自動播放音頻的,解決辦法就是不要自動播放,或者就是第一次點擊頁面觸發播放。
剩下的就是音頻標簽綁定播放和停止,在觸發的時候添加旋轉或脈沖動畫。
$audio.on("play", function() { isAudioLoaded = true; $music.addClass('music-rotate').removeClass('music-pulse'); }).on("pause", function() { $music.removeClass('music-rotate').addClass('music-pulse'); });
2)上傳圖片
上傳就是綁定file標簽的“change”事件,除了前面說到的用FileReader獲取圖片的data:URL外,還將原圖做了一次壓縮。
壓縮其實就是將圖片放到Canvas中,然后用Canvas輸出“jpeg圖片”,並且質量是“0.7”,可以將一張800多KB的png圖片壓縮到50多KB。
還發現一個現象,如果用Canvas輸出“png”的data:URL,會比原圖還要大。
在“reader.onload”事件中除了壓縮圖片,還會保存此圖的真實際寬度和高度,下面的旋轉會用到尺寸,還保存了一條旋轉信息的緩存。
var img = new Image(); img.onload = function() { var src = poster.filterImage(img, this.width, this.height); //將圖片進行壓縮,減少頁面大小 $frameImg.data('width', this.width); //實際寬度 $frameImg.data('height', this.height); //實際高度 var realImg = new Image(); realImg.onload = function() { $frameImg.attr('src', realImg.src); //第三次載入Base64數據 }; realImg.src = src; rotates[0] = { src: src, width: this.width, height: this.height, image: realImg }; //用於旋轉的緩存 }; img.src = base64;
3)拖拽、放大、縮小
此功能是需要與上面的touch.js手勢庫結合。
拖拽使用了CSS3的“translate3d”屬性,而放大縮小使用了CSS3的“scale”屬性。
function formatTransform(offx, offy, scale) { var translate = 'translate3d(' + (offx + 'px,') + (offy + 'px,') + '0)'; scale = 'scale(' + scale + ')'; //var rotate = 'rotate('+deg+'deg)'; return translate + ' ' + scale; }
原先旋轉也想用CSS3的“rotate”屬性實現,不過后面實現后,裁剪圖片變得非常棘手,不能下手,最后是否決了這個實現方式。
4)旋轉
為了解決裁剪的問題,每次旋轉都會生成一張新的圖片,並將這個圖片信息緩存起來。
由於是新的圖片,所以就可以直接按照原先的方式來裁剪了,也不用考慮旋轉角度的問題。
旋轉的邏輯放在“filterImage”中,當時在編寫旋轉的時候,碰到旋轉后的圖形變形的問題,后面用圖片的實際寬高就解決了變形。
之所以變形是因為寬高用了CSS計算后的值,下圖中的兩個尺寸就是計算后的值。
旋轉的代碼就兩行,rotate中“deg”就是旋轉角度,這里是90。
ctx.rotate(deg * Math.PI / 180); ctx.drawImage(image, 0, -canvas.width);
下圖介紹了操作過程:
為了提升性能,每個方向的圖片信息都會被緩存起來。
rotates[direction] = {src:src, width:this.width, height:this.height, image:realImg};//緩存
5)裁剪
比較復雜的一部分,計算圖片相對於畫框的left和top邊距。
而right和bottom與以往的定義不同,這里是高度與寬度分別和top與left相加后的值。
再根據不同邏輯,分別計算畫框與圖片的X、Y、width和height的值。
最后計算實際圖片的寬度與CSS計算后的圖片寬度比,將這個值與圖片的X、Y、width和height相乘,得出最終值。
這里注意下,在iphone5S中,如果圖片的實際高度 < 計算后的高度,就會出現不顯示。具體的邏輯在“intersect”方法中。
下圖是某一種情況下的各個坐標值:
intersect: function($frame, $img) { var imgX = 0,imgY = 0,imgW = 0,imgH = 0; var frmX = 0,frmY = 0; var imgOffset, frmOffset, left, right, top, bottom; imgOffset = $img.offset(); //圖片的偏移對象 frmOffset = $frame.offset(); //畫框的偏移對象 left = imgOffset.left - frmOffset.left - 3; //圖片到邊框左邊的距離 去除3px的邊框 right = left + imgOffset.width; //畫框模型是border-box,所以圖片寬度需要減去邊框的寬度 就是574 top = imgOffset.top - frmOffset.top - 3; //圖片到邊框上邊的距離 bottom = top + imgOffset.height; //圖片在畫框內 if (!(right <= 0 || left >= frmOffset.width || bottom <= 0 || top >= frmOffset.height)) { if (left < 0) { imgX = -left; frmX = 0; imgW = (right < frmOffset.width) ? right : frmOffset.width; } else { imgX = 0; frmX = left; imgW = (right < frmOffset.width ? right : frmOffset.width) - left; } if (top < 0) { imgY = -top; frmY = 0; imgH = (bottom < frmOffset.height) ? bottom : frmOffset.height; } else { imgY = 0; frmY = top; imgH = ((bottom < frmOffset.height) ? bottom : frmOffset.height) - top; } } var ratio = $img.data('width') / $img.width(); //圖片真實寬度 與 圖片CSS寬度 //圖片的實際高度不能低於計算后的高度 否則iphone 5S中就不顯示 var imageHeight = imgH * ratio; if (+$img.data('height') < imageHeight) { imageHeight = $img.data('height'); } return { frame: {x: frmX,y: frmY,w: (imgW + 6),h: (imgH + 6)}, //此處畫框是574,而畫布是580 image: {x: imgX * ratio,y: imgY * ratio,w: imgW * ratio,h: imageHeight} }; }
6)合成
合成其實就是將兩張Canvas合並到一起。下面代碼中的“drawImage”是自定義的一個方法,最終還是會調用Canvas的“drawImage”。
poster.drawImage(ctx, rotates[direction].image, poster.intersect($frame, $frameImg)); poster.drawImage(ctx, $word, poster.intersect($frame, $word));
Canvas的“drawImage”方法有多種參數組合。第三組有9個參數, 一開始還不是理解這幾個參數的含義,后面去查了一下。
sx、sy對應的是圖片的x、y坐標,而dx、dy對應的是畫布的x、y坐標。
demo下載:
https://github.com/pwstrick/father
參考資料:
how-to-send-formdata-objects-with-ajax-requests-in-jquery