現(xiàn)圖片翻轉(zhuǎn):Canvas坐標(biāo)系與像素級(jí)處理實(shí)戰(zhàn))
簡介一套用JavaScript實(shí)現(xiàn)圖片翻轉(zhuǎn)效果的入門示例資源面向Web前端初學(xué)者與對(duì)圖片交互感興趣的開發(fā)者。資源演示了通過操作CSS transform屬性、綁定點(diǎn)擊事件監(jiān)聽器以及管理翻轉(zhuǎn)狀態(tài)實(shí)現(xiàn)圖片沿X軸或Y軸180度動(dòng)態(tài)翻轉(zhuǎn)的方法可加深對(duì)JavaScript DOM操作與事件機(jī)制的理解。壓縮包共36個(gè)文件以26張GIF動(dòng)圖、7張JPG圖片為主包含2個(gè)HTML頁面script01.html、script02.html和1個(gè)核心JS腳本script02.js附帶多種素材與按鈕狀態(tài)圖整體僅413KB輕量易用。已有136人學(xué)習(xí)下載。通過這份示例可學(xué)習(xí)事件監(jiān)聽、transform樣式切換與CSS過渡動(dòng)畫的協(xié)同寫法并借鑒項(xiàng)目中多個(gè)HTML結(jié)構(gòu)不同實(shí)現(xiàn)風(fēng)格快速搭建自己的翻轉(zhuǎn)交互效果。1. 用javascript寫的圖片翻轉(zhuǎn)先別急著加CSS濾鏡翻轉(zhuǎn)背后是一套坐標(biāo)系在管做圖片上傳、頭像裁剪、證件照處理的前端遲早會(huì)遇到用javascript寫的圖片翻轉(zhuǎn)這個(gè)需求——比如用戶自拍時(shí)鏡像方向不對(duì)或者要生成一張水平翻轉(zhuǎn)的縮略圖。很多人第一反應(yīng)是加一行transform: scaleX(-1)界面秒變但導(dǎo)出圖片時(shí)才發(fā)現(xiàn)Canvas畫出來還是原樣或者轉(zhuǎn)成base64后方向根本沒變。原因很簡單CSS transform只改變了元素的繪制表現(xiàn)像素?cái)?shù)據(jù)還躺在原圖里沒動(dòng)。真正用javascript寫的圖片翻轉(zhuǎn)常見做法是用Canvas讀入圖片、通過坐標(biāo)系映射重新繪制再把結(jié)果導(dǎo)出為新圖片。它適合的不只是預(yù)覽還包括批量生成翻轉(zhuǎn)后的文件、給OCR或人臉檢測模型喂預(yù)處理圖像、把翻轉(zhuǎn)和裁剪壓縮組合成一條流水線。這篇按我實(shí)際調(diào)過的路子走先講清翻轉(zhuǎn)的底層邏輯再給最小可運(yùn)行代碼參數(shù)和坑放在后面最后落成一個(gè)能用的JavaScript函數(shù)。2. 圖片翻轉(zhuǎn)的兩種實(shí)現(xiàn)CSS transform與Canvas像素復(fù)制的差別2.1 CSS transform看一眼就翻轉(zhuǎn)但拿不到像素真相業(yè)務(wù)里最常見的快速翻轉(zhuǎn)是transform: scaleX(-1)。它不改變文檔流也不會(huì)觸發(fā)重繪GPU合成器直接把元素沿著垂直軸鏡像效果是即時(shí)且平滑的。但它的邊界很清楚只作用于屏幕上的渲染結(jié)果。如果你用canvas.drawImage()把這個(gè)被翻轉(zhuǎn)的元素畫進(jìn)去或者用html2canvas截圖要么拿到原方向要么因?yàn)榭缬蚝途彺鎲栴}翻車。所以在我經(jīng)手的項(xiàng)目里CSS transform只用來做預(yù)覽態(tài)等用戶確認(rèn)翻轉(zhuǎn)后再用Canvas真正生成一張新圖片。如果你的需求就是屏幕上左右對(duì)稱看一下CSS的scaleX(-1)夠用一旦涉及導(dǎo)出、上傳、像素級(jí)處理必須走向Canvas。2.2 Canvas的scale(-1,1)翻轉(zhuǎn)背后的坐標(biāo)系操作Canvas里翻轉(zhuǎn)的核心不是倒像素而是變換坐標(biāo)系。ctx.scale(-1, 1)的效果是把x軸反向原來x100的點(diǎn)會(huì)變成x-100所有內(nèi)容沿著y軸鏡像。但這里有個(gè)新手繞不過的坑——坐標(biāo)系反轉(zhuǎn)后圖片會(huì)被畫到負(fù)x區(qū)域也就是說你很可能在Canvas上什么都看不到。常見的做法是先用ctx.translate(width, 0)把原點(diǎn)移到畫布右邊緣再執(zhí)行scale(-1, 1)這樣圖片才會(huì)落在可見區(qū)域內(nèi)。垂直翻轉(zhuǎn)同理用ctx.translate(0, height)加ctx.scale(1, -1)。我一般建議把這兩步封裝成一對(duì)操作順序不能反先平移再縮放否則你平移的坐標(biāo)也被鏡像了結(jié)果完全錯(cuò)位。下面這段代碼展示水平翻轉(zhuǎn)的核心動(dòng)作// 假設(shè) canvas 和 ctx 已創(chuàng)建img 已加載 ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(canvas.width, 0); // 先把原點(diǎn)移到右邊界 ctx.scale(-1, 1); // x軸反向畫面被拉回可見區(qū) ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 繪制原圖 ctx.restore();邏輯說明translate(canvas.width, 0)是把坐標(biāo)原點(diǎn)從左上角挪到右上角。此時(shí)x軸正方向朝左再配合scale(-1, 1)原本朝右的繪制方向變成朝左于是畫出來的圖像正好是水平鏡像而且完整落在畫布內(nèi)。ctx.save()和ctx.restore()保證這次變換不影響后續(xù)繪制。這里的canvas和img的真實(shí)尺寸要一致否則drawImage的五參形式會(huì)做拉伸而不是翻轉(zhuǎn)。2.3 getImageData與像素矩陣翻轉(zhuǎn)的本質(zhì)是坐標(biāo)映射如果你以為翻轉(zhuǎn)只能靠scale那還可以更底層一點(diǎn)。Canvas的ctx.getImageData(0, 0, width, height)會(huì)返回一個(gè)Uint8ClampedArray每四個(gè)元素組成一個(gè)RGBA像素。水平翻轉(zhuǎn)的本質(zhì)是構(gòu)造一個(gè)新數(shù)組把原圖中(x, y)處的像素放到目標(biāo)數(shù)組的(width - 1 - x, y)位置。這種做法的好處是可以在翻轉(zhuǎn)的同時(shí)做像素級(jí)的調(diào)整比如同時(shí)修掉透明通道的雜色或者和鏡像濾鏡、灰度化混在一起。代價(jià)是慢——1000萬像素的圖片getImageData和逐像素循環(huán)在純JavaScript里跑一遍可能要一兩百毫秒移動(dòng)端逼近半秒。我通常只在小區(qū)域裁剪或做特效時(shí)用像素矩陣普通翻轉(zhuǎn)優(yōu)先走scale加drawImage的GPU路徑。這里有一個(gè)JavaScript判斷數(shù)據(jù)類型的細(xì)節(jié)容易踩坑getImageData返回的不是普通數(shù)組而是Uint8ClampedArray。用Array.isArray()判斷會(huì)返回false很多人在這里翻車。我一般用Object.prototype.toString.call(pixel.data)來確認(rèn)類型或者干脆不做判斷要知道它就是整數(shù)數(shù)組下標(biāo)從0開始每四位一組。處理像素時(shí)data[i]、data[i1]、data[i2]、data[i3]分別對(duì)應(yīng)紅、綠、藍(lán)、透明通道順序別記反。3. 用Canvas實(shí)現(xiàn)圖片翻轉(zhuǎn)從加載到導(dǎo)出的最小可運(yùn)行代碼3.1 在本地用FileReader和Image對(duì)象加載圖片跑通翻轉(zhuǎn)的第一步是把用戶選擇的圖片讀成Image對(duì)象。常見做法是用FileReader把文件讀成DataURL再賦給Image.src。這里有一個(gè)JavaScript運(yùn)行時(shí)報(bào)錯(cuò)的高發(fā)點(diǎn)FileReader的onload事件里e.target.result是字符串你不能再對(duì)它調(diào)用FileReader相關(guān)方法。下面是一個(gè)能直接用的小加載器function loadImageFromFile(file) { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload (e) { const img new Image(); img.onload () resolve(img); img.onerror () reject(new Error(圖片解碼失敗)); img.src e.target.result; // 這里 result 是 base64 字符串 }; reader.onerror () reject(new Error(文件讀取失敗)); reader.readAsDataURL(file); }); }參數(shù)說明FileReader.readAsDataURL會(huì)把圖片讀成data:image/png;base64,...樣式的字符串適合小圖一般5MB以內(nèi)。new Image()創(chuàng)建的是HTMLImageElement實(shí)例它加載base64是異步的所以要用Promise把圖像準(zhǔn)備狀態(tài)暴露給調(diào)用方。如果你的圖片源是遠(yuǎn)程URL需要先配置跨域否則后面導(dǎo)出時(shí)會(huì)遇到Canvas被污染的問題第5節(jié)會(huì)細(xì)講。3.2 用ctx.translate和ctx.scale完成水平/垂直翻轉(zhuǎn)拿到Image對(duì)象后就可以創(chuàng)建Canvas并執(zhí)行翻轉(zhuǎn)了。水平翻轉(zhuǎn)是剛那兩行核心代碼垂直翻轉(zhuǎn)我寫成參數(shù)化的函數(shù)順手解決旋轉(zhuǎn)角度不居中的問題function flipImage(img, direction horizontal) { const canvas document.createElement(canvas); const ctx canvas.getContext(2d); canvas.width img.naturalWidth; canvas.height img.naturalHeight; ctx.save(); if (direction horizontal) { ctx.translate(canvas.width, 0); ctx.scale(-1, 1); } else if (direction vertical) { ctx.translate(0, canvas.height); ctx.scale(1, -1); } ctx.drawImage(img, 0, 0, canvas.width, canvas.height); ctx.restore(); return canvas; }這段代碼的邏輯說明canvas.width和canvas.height直接取原圖自然尺寸保證不拉伸。水平翻轉(zhuǎn)的transform順序是先平移后縮放垂直方向同理是translate(0, height)。drawImage繪制時(shí)因?yàn)樽鴺?biāo)系已經(jīng)變換所以圖片內(nèi)容會(huì)被鏡像繪制到畫布上。save/restore是必須的否則下一次繪制會(huì)疊加之前的變換。注意這里沒有處理EXIF方向如果拍照?qǐng)D片自帶orientation信息你會(huì)看到翻轉(zhuǎn)結(jié)果不符合直覺這個(gè)在避坑章里處理。3.3 導(dǎo)出翻轉(zhuǎn)結(jié)果canvas.toDataURL與toBlob的選型翻轉(zhuǎn)完成后需要把Canvas變成可上傳的文件數(shù)據(jù)。兩個(gè)選擇canvas.toDataURL(mimeType, quality)返回base64字符串適合直接塞進(jìn)表單或存localStoragecanvas.toBlob(callback, mimeType, quality)返回Blob對(duì)象適合FormData上傳體積也比base64小約33%。我的習(xí)慣是預(yù)覽用toDataURL(image/jpeg, 0.92)上傳用toBlob包裝成File。下面代碼演示如何把Canvas變成Blobfunction canvasToBlob(canvas, mimeType image/png, quality 0.92) { return new Promise((resolve, reject) { canvas.toBlob((blob) { if (blob) { resolve(blob); } else { reject(new Error(Canvas導(dǎo)出失敗可能畫布被污染)); } }, mimeType, quality); }); }值得強(qiáng)調(diào)的是toBlob的回調(diào)風(fēng)格它不走Promise必須手動(dòng)包一層。質(zhì)量參數(shù)quality只對(duì)JPEG和WebP有意義PNG傳了也會(huì)被忽略。如果你要在上傳前把Blob轉(zhuǎn)成File以便添加文件名可以new File([blob], fileName, { type: mimeType })。這段代碼里沒有做Canvas尺寸上限處理超大圖導(dǎo)出時(shí)內(nèi)存占用會(huì)飆升建議在創(chuàng)建Canvas前先做壓縮具體參數(shù)下一章說明。4. 翻轉(zhuǎn)相關(guān)的三個(gè)必調(diào)參數(shù)圖片尺寸、插值質(zhì)量與輸出格式4.1 width/height怎么定設(shè)備像素比與CSS像素的換算很多人在翻轉(zhuǎn)后把Canvas直接塞進(jìn)頁面顯示結(jié)果圖片發(fā)虛。這是因?yàn)镃anvas的位圖尺寸和CSS顯示尺寸不是一回事。Canvas的width/height是像素網(wǎng)格數(shù)量樣式表里的width/height是CSS像素高DPI屏幕上1個(gè)CSS像素可能對(duì)應(yīng)2個(gè)物理像素。如果你只設(shè)樣式尺寸Canvas的實(shí)際像素默認(rèn)只有300x150或原大小在Retina屏上會(huì)被拉伸。我一般遵循Canvas像素尺寸 原圖尺寸 × 設(shè)備像素比如果只是預(yù)覽或者直接設(shè)為原圖的naturalWidth/naturalHeight如果要導(dǎo)出原圖大小。下面是計(jì)算預(yù)覽畫布尺寸的常用寫法const dpr window.devicePixelRatio || 1; const displayWidth Math.round(img.naturalWidth * dpr); const displayHeight Math.round(img.naturalHeight * dpr); canvas.width displayWidth; canvas.height displayHeight; canvas.style.width (img.naturalWidth) px; canvas.style.height (img.naturalHeight) px;這段參數(shù)說明canvas.width決定實(shí)際像素?cái)?shù)canvas.style.width決定CSS顯示大小。翻轉(zhuǎn)后導(dǎo)出時(shí)我建議用原圖尺寸而不是dpr放大因?yàn)榉糯髸?huì)導(dǎo)致位圖插值JPEG重新編碼后文件體積變大畫質(zhì)卻不一定提升。導(dǎo)出場景下dpr只影響屏幕顯示不影響文件像素。如果你是把翻轉(zhuǎn)后的圖片用于OCR保持原分辨率通常命中率更高。4.2 插值質(zhì)量參數(shù)imageSmoothingEnabled與imageSmoothingQuality翻轉(zhuǎn)本質(zhì)是重繪如果原圖尺寸和目標(biāo)尺寸不一致Canvas會(huì)做插值。ctx.imageSmoothingEnabled默認(rèn)是true意味著縮放時(shí)做平滑ctx.imageSmoothingQuality可以設(shè)置low、medium、high三個(gè)檔位。很多人忽略這個(gè)參數(shù)導(dǎo)致翻轉(zhuǎn)縮放出鋸齒或糊成一團(tuán)。我調(diào)證件照的經(jīng)驗(yàn)是縮小圖片用high放大圖片也用high如果做像素風(fēng)或需要硬邊就關(guān)掉平滑。下面是一段上下文const ctx canvas.getContext(2d); ctx.imageSmoothingEnabled true; ctx.imageSmoothingQuality high; ctx.drawImage(img, 0, 0, targetWidth, targetHeight);注意imageSmoothingQuality是較新的Canvas屬性老版本W(wǎng)ebView里可能不支持但設(shè)置不支持的屬性不會(huì)報(bào)錯(cuò)只會(huì)被忽略。如果你發(fā)現(xiàn)翻轉(zhuǎn)后的文字邊緣發(fā)虛優(yōu)先檢查是不是目標(biāo)尺寸小于原圖卻在用medium。另外drawImage在翻轉(zhuǎn)時(shí)也會(huì)觸發(fā)插值所以翻轉(zhuǎn)這種等尺寸操作其實(shí)不需要插值——如果你畫布尺寸和原圖完全一致插值不影響結(jié)果。只有當(dāng)你同時(shí)做翻轉(zhuǎn)和縮放時(shí)這套參數(shù)才真正起作用。4.3 輸出格式與壓縮質(zhì)量PNG、JPEG、WebP怎么選翻轉(zhuǎn)完成后導(dǎo)出的格式直接影響文件體積和適用范圍。我的選擇邏輯很簡單需要透明背景或邊緣銳利截圖、貼紙、驗(yàn)證碼選PNG照片或頭像選JPEG質(zhì)量0.8~0.92支持WebP的環(huán)境選WebP體積最小。下面是一個(gè)綜合導(dǎo)出示例function exportFlipedImage(canvas, format image/jpeg, quality 0.85) { if (format image/jpeg) { // JPEG不支持透明先把透明底填成白色否則導(dǎo)出后透明區(qū)域變黑 const exportCanvas document.createElement(canvas); const exportCtx exportCanvas.getContext(2d); exportCanvas.width canvas.width; exportCanvas.height canvas.height; exportCtx.fillStyle #ffffff; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); exportCtx.drawImage(canvas, 0, 0); return exportCanvas.toDataURL(image/jpeg, quality); } return canvas.toDataURL(format image/webp ? image/webp : image/png); }這塊有一個(gè)血淚經(jīng)驗(yàn)JPEG編碼時(shí)透明像素會(huì)被當(dāng)作黑色透明背景的頭像導(dǎo)出后角落全變黑。所以必須先填充白色底。質(zhì)量參數(shù)0.85通常是人像可接受的最小值低于0.7會(huì)出現(xiàn)明顯的色塊條紋。WebP在Chrome和Edge上支持很好但導(dǎo)出時(shí)要檢測瀏覽器是否真正支持canvas.toDataURL(image/webp)不支持時(shí)會(huì)返回image/png數(shù)據(jù)可能造成體積膨脹。檢測可以用canvas.toDataURL(image/webp).startsWith(data:image/webp)。5. 圖片翻轉(zhuǎn)避坑運(yùn)行時(shí)報(bào)錯(cuò)、黑邊和性能崩潰的五個(gè)真實(shí)記錄5.1 跨域圖片導(dǎo)致Canvas被污染toDataURL運(yùn)行時(shí)報(bào)錯(cuò)現(xiàn)象用遠(yuǎn)程URL加載圖片翻轉(zhuǎn)后調(diào)用canvas.toDataURL()瀏覽器拋出SecurityErrorPage里console報(bào)“Failed to execute toDataURL on HTMLCanvasElement: Tainted canvases may not be exported”。原因Canvas的“污染”機(jī)制——只要繪制了跨域圖片且圖片服務(wù)器沒返回正確的CORS頭Canvas格式就會(huì)被標(biāo)記為不干凈禁止任何形式的導(dǎo)出。解決在設(shè)置img.src之前先設(shè)置img.crossOrigin anonymous同時(shí)要求圖片服務(wù)器響應(yīng)頭帶Access-Control-Allow-Origin。如果你控制不了服務(wù)器只能把圖片打成base64再加載或者要求后端做代理。這是我踩過最深的坑——本地開發(fā)沒問題部署到CDN后就炸排查半天發(fā)現(xiàn)是CDN沒配頭。5.2 翻轉(zhuǎn)后出現(xiàn)黑邊或透明邊緣坐標(biāo)系平移沒做對(duì)現(xiàn)象翻轉(zhuǎn)后的圖片一側(cè)多了一條黑邊或透明條另一側(cè)被裁掉一點(diǎn)。原因最常見的是只調(diào)用了scale(-1, 1)而沒有translate(width, 0)。翻轉(zhuǎn)后圖像落到負(fù)坐標(biāo)區(qū)域Canvas只繪制可見區(qū)于是左邊界出現(xiàn)空白或者你translate(width, 0)但width用的是CSS尺寸而不是Canvas的像素寬度導(dǎo)致偏移量差了幾個(gè)像素。解決嚴(yán)格用canvas.width作為translate參數(shù)不是canvas.clientWidth也不是img.style.width。同時(shí)檢查drawImage的繪制矩形是否和畫布完全對(duì)齊ctx.drawImage(img, 0, 0, canvas.width, canvas.height)。如果設(shè)置了dpr倍數(shù)的尺寸注意translate要用實(shí)際像素值。這類問題肉眼可能看不出來導(dǎo)出后放到Photoshop里放大比對(duì)邊緣才明顯。5.3 大圖翻轉(zhuǎn)卡死不要直接getImageData處理超大原圖現(xiàn)象用手機(jī)拍的高清照片4000x3000左右做翻轉(zhuǎn)頁面卡頓2秒以上內(nèi)存占用暴增低端安卓甚至直接白屏崩潰。原因getImageData創(chuàng)建了一個(gè)和畫布同等大小的RGBA數(shù)組4000x3000x4約48MB再加上原圖解碼的Bitmap和Canvas的離屏緩沖區(qū)移動(dòng)端內(nèi)存直接爆。更別說Pixel級(jí)的for循環(huán)還要再分配一個(gè)相等大小的數(shù)組用來存放翻轉(zhuǎn)結(jié)果。解決常規(guī)翻轉(zhuǎn)直接用ctx.scale加drawImage走GPU合成不會(huì)產(chǎn)生大內(nèi)存數(shù)組。只有真正需要逐像素處理時(shí)才用getImageData而且先縮小畫布。我的做法是每次操作前先限制最大邊超過2048像素時(shí)等比縮小到2048以內(nèi)再處理。這樣既能保住絕大多數(shù)場景的顯示需求又不會(huì)卡死。5.4 JavaScript判斷數(shù)據(jù)類型翻車e.target.result是字符串不是圖片現(xiàn)象在FileReader.onload里拿到e.target.result直接把它當(dāng)成Image對(duì)象去設(shè)置canvas.width結(jié)果得到undefinedCanvas被清空成0x0。原因FileReader返回的是DataURL字符串而Image實(shí)例需要src屬性賦予后、觸發(fā)onload事件才能使用。很多人誤以為讀取完就拿到圖片二進(jìn)制了。解決用Promise包裝在img.onload回調(diào)里resolve真正的HTMLImageElement。另外一定要判斷img.complete img.naturalWidth 0否則可能拿到一個(gè)空?qǐng)D。這種問題JavaScript運(yùn)行時(shí)報(bào)錯(cuò)不會(huì)提示只會(huì)在后續(xù)繪制時(shí)莫名其妙地什么都不顯示。5.5 保留兩位小數(shù)的錯(cuò)覺旋轉(zhuǎn)角度計(jì)算中浮點(diǎn)誤差累積現(xiàn)象做翻轉(zhuǎn)和旋轉(zhuǎn)組合操作時(shí)Math.tan和Math.atan反復(fù)計(jì)算后圖片邊緣出現(xiàn)亞像素縫隙或者導(dǎo)出尺寸少了1個(gè)像素。原因浮點(diǎn)數(shù)表達(dá)精度是有限的0.1 0.2 ! 0.3這種經(jīng)典問題在矩陣變換中會(huì)累積成像素偏移。很多人只關(guān)心JavaScript保留兩位小數(shù)卻忘了變換矩陣?yán)锏淖鴺?biāo)精度會(huì)影響到繪圖結(jié)果。解決在坐標(biāo)計(jì)算處用Math.round()把最終像素坐標(biāo)取整或者用Number.toFixed(2)再轉(zhuǎn)為Number。比例計(jì)算時(shí)盡量避免連續(xù)乘除。如果做旋轉(zhuǎn)加翻轉(zhuǎn)我一般用Math.hypot(width, height)算對(duì)角線長度來推導(dǎo)目標(biāo)Canvas尺寸可以減少一次中間誤差。翻轉(zhuǎn)本質(zhì)是坐標(biāo)取反理論上沒有精度問題但組合操作會(huì)引入。建議每次變換完成后立即導(dǎo)出不要反復(fù)在同一個(gè)Canvas上疊加操作。6. 把翻轉(zhuǎn)封裝成JavaScript函數(shù)參數(shù)校驗(yàn)、邊界處理與驗(yàn)證技巧最后把前面跑的流程收攏成一個(gè)可復(fù)用的函數(shù)順便說兩個(gè)我常用的驗(yàn)證習(xí)慣。這個(gè)函數(shù)同時(shí)支持水平和垂直翻轉(zhuǎn)自動(dòng)處理EXIF方向、跨域和導(dǎo)出格式問題。參數(shù)上我會(huì)接受一個(gè)options對(duì)象包含direction、outputFormat、quality和maxSize。校驗(yàn)順序是先檢查圖片是否加載完成再判斷Canvas能否導(dǎo)出最后檢查輸出格式支持度。maxSize默認(rèn)2048超過則等比縮放到最大邊這是防內(nèi)存崩潰的底線。驗(yàn)證技巧有兩個(gè)第一翻轉(zhuǎn)后用ctx.getImageData在像素級(jí)抽查比如原圖左上角像素是紅色水平翻轉(zhuǎn)后右下角像素應(yīng)該變成紅色。寫一個(gè)assertPixelColor的小工具能自動(dòng)跑回歸測試。第二對(duì)照真實(shí)物理鏡像判斷黑邊和方向我會(huì)準(zhǔn)備一張寫了數(shù)字的純色圖片水平翻轉(zhuǎn)后數(shù)字應(yīng)呈鏡像且背景無黑邊。這個(gè)習(xí)慣幫我抓到了不止一次translate參數(shù)用錯(cuò)的翻車現(xiàn)場。如果你要批量驗(yàn)證假設(shè)調(diào)用flipImage(source, { direction:horizontal, maxSize:1024 })返回Canvas再把它繪制到一個(gè)隱藏測試畫布上檢查像素分布就行。第一人稱教訓(xùn)早期我只寫CSS transform覺得Canvas是小題大做直到全量導(dǎo)出發(fā)現(xiàn)全是原圖才回頭補(bǔ)課?,F(xiàn)在碰到圖片翻轉(zhuǎn)我會(huì)先問一句是不是需要拿到像素?cái)?shù)據(jù)需要就用Canvas不需要才用CSS。這不是什么玄學(xué)而是把預(yù)覽和產(chǎn)出分開管理。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取