靈感
起初只是和同事在去吃路的路上, 互相吹*, 扯一些有的沒得, 然後說到了馬賽克, 探討了一下馬賽克的實現, 覺得還蠻有意思的, 感覺可以實現一波, 後面我覺得只實現馬賽克功能又太單調, 就像做一個類似微信截圖的功能.
關於組件
實現效果
基本效果都是參照微信截圖去做的, 但是還是不一樣, 微信截圖的功能還是比較通用, 自己還有功能沒實現, 後面會說明明細
基本原理
canvas
+ image
去實現的, 說實話, canvas
我用的也不是很熟悉, 只瞭解基本的api
, 但是基本思路還是明白的, 前面截圖大小位置的拖動就是操作dom
, 後面對截圖的標註之類的都是canvas
相關的操作, 一步步實現
實現細節
最好先看下 github 的 demo, 不然解釋起來比較麻煩
打開一個圖片
其實就是展示一個image
, 瀏覽器如何展示這個image
參考手機打開一個圖片吧, 舉個例子, 1980 720 的屏幕, 打開 720 460 的圖片, 圖片大小保持不變, 上下左右留白
如果打開一個 2800 * 1000 的圖片, 應該是寬度撐滿, 類似這種
反過來, 高度撐滿
邏輯大概這樣
const t = image.width / image.height;
if (height < image.height || width < image.width) {
const ws = image.width / width;
const hs = image.height / height;
if (ws <= hs) {
return [Math.floor(height * t), Math.floor(height)];
} else {
return [Math.floor(width), Math.floor(width / t)];
}
}
return [Math.floor(image.width), Math.floor(image.height)];
需要注意的是,canvas
的width
和style.width
是兩個東西, 我會保留兩者的比例, 後面很多操作都需要這個比例
選取截圖範圍
確定一個矩形的返回, 只需要知道兩個信息, 一個是top left
, 一個是 width height
, 拖動的時候是分兩種情況的, 打個比方, 一個是從左上向右下拖動, top left
不變, 只改變width height
, 一種是從右下向左上拖動, top left
改變, width height
改變, 判斷第一個點和拖動的點就能區分這兩種情況.然後根據top left width height
在canvas
上通過drawImage
渲染出截取範圍的圖片. 還需要注意的是, 也需要給canvas
綁定mousemove
事件,不然鼠標飄到canvas
上的時候就不能拖動了.
此處有個待優化點, 就是當圖片尺寸過大時, 拖動不停的drawImage
會有一丟丟的卡頓, 這塊我後面想優化下, 大概改成這樣: 拖動的時候只是控制矩形的border
, mouseup
的時候再去在canvas
上繪畫.
拖動點控制大小
截取範圍確定後, 可以通過邊角和中間的八個點來控制大小, 這塊也是分兩種情況的:
- 拖動邊角的點的行爲基本上是一致的
- 拖動中間的點的行爲差不多, 但有一些差別, 就是控制的是寬度還是高度的差別
這塊我的處理是保存邊角的四個點, 拖動邊角的行爲本質上和我們選取截圖範圍的行爲是一致的, 因爲我保存了四個點, 只要我知道對角的兩個點的座標就能知道怎麼去控制這個矩形的變化.
拖動中間的點和邊角是很像的, 基本上就是高度不變, 寬度改變, 和寬度改變, 高度不變
拖動改變位置
這個更簡單, 就是根據座標變化計算拖動的距離, 需要注意的就是邊界的判定, 因爲你不能拖動到外面去了
放大鏡的效果
記錄鼠標的位置, 根據寬高drawImage
, 需要乘以上面提到的比例, 也需要注意邊界的判定.
標註矩形和圓形
這兩個放一起, 因爲這兩個的行爲基本上是一致的, 只是形狀不同, 剛開始的想法是, 拖動的時候不停clearRect
, 再重新drawImage
, 這裏又有上文提到的問題了, 這裏需要把繪圖動作存起來, 不然矩形沒發保存, 繪圖動作越存越多, 拖動的時候會越來越卡, 後面我做了優化, 拖動的時候, 展示的其實是個svg
, 等到mouseup
的時候再去畫 矩形|圓形.
這塊有個問題, 感覺線條寬度在svg
和canvas
裏面的表現形式有點不一樣, 這個後面還需要優化
畫線
這個比較簡單, 記住上一個點就好了, 具體不細說了
馬賽克
這個也是蠻有意思的, 因爲早就有了思路, 做起來也是比較簡單. 打個比方, 畫圖長寬都爲100, 馬賽克大小爲 10, 我點了座標(30, 40)
, 換算就是座標爲(3, 4)
的馬賽克
// 馬賽克大小
const size = 10;
const w = 100;
const h = 100;
const loc = { x: 33, y: 33};
const row = Math.floor(loc.x / size);
const col = Math.floor(loc.y / size);
const index = col * w + row;
const locX = index % w - 1;
const locY = Math.floor(index / w);
// index 是 imageData 的第幾個點, 用來填充這個方塊
const index = locY * size * w + locX * size;
const r = imgData.data[dataIndex * 4];
const g = imgData.data[dataIndex * 4 + 1];
const b = imgData.data[dataIndex * 4 + 2];
const color = rgb2hex(r, g, b);
context.fillRect(locX * size, locY * size, size, size);
上面是比較簡單的實現, 實際需要考慮畫筆的寬度啥的
打馬賽克, 目前比例是 1 的時候正常, 不是 1 的時候, 有點問題, 因爲計算的比例一般不可能爲整數, 就會導致誤差, 這塊我是打算重寫, 換種方式去處理, 目前的處理方式還是覺得不好
todo: 箭頭
這個有思路, 但暫時沒做了, 以前做過移動端對圖片的旋轉放大, 兩個行爲基本一致, 想法也是通過 svg 進行處理, 這個後面看情況補上
todo: 標註文字
這個也是有思路, 沒做, 感覺還是用操作dom
處理比較方便
撤銷
撤銷我剛開始想偷個懶, 這樣處理的, getImageData
=> 保存 => 撤銷 => putImageData
. 後面爲什麼改了呢? 一是putImageData
性能不好, 二是, 如果是個 8000 * 6000 的圖片, 每個操作我都把這些像素信息存下來, 感覺還是很恐怖的....
後面改成只保存繪畫操作, 撤銷想當於去掉最後一步操作, 把前面的繪畫操作執行一遍就好了
下載
很簡單, 轉成base64
, data-url
的形式, 不做過多說明
拷貝
這個比較麻煩, 本來是想做成拷貝的粘貼板上的, 網上找了半天資料, 最後只做成了選中效果...沒啥用的感覺, 目前只是做成觸發回調的形式.
瀏覽器拷貝圖片到粘貼板應該還是沒法實現, 不知道微信的是怎麼處理的, 有了解的同學可以幫忙科普下
做不了: 拖動
微信的拖動這塊感覺還是比較麻煩, 矩形 | 圓 | 線條, 有點思路, 但感覺很麻煩, 我選擇放棄思考, 有好的點子的同學可以幫忙提點建議
總結
大概這麼多, 涉及到細節基本都一筆帶過了, 總體還是比較有意思的一個組件, 目前還有很多優化的點, 當時只是打草稿寫着玩的, 所以現在代碼還有點亂.
內容比較簡單, 後面看情況補充(有人看的話...)
補充(招聘)
有贊零售正在招前端, 有想法的同學可以投一份簡歷給我([email protected]), 我知道你們又要說xx, 大家討論技術, 不要噴我...