化實(shí)踐)
1. 從白板瀏覽痛點(diǎn)聊起MiroFish到底解決什么問題先交代一下背景。前陣子我在一個(gè)遠(yuǎn)程協(xié)作團(tuán)隊(duì)里天天要對著在線白板梳理用戶故事、架構(gòu)圖和技術(shù)方案。白板的好處是無限畫布想法可以隨便鋪開但壞處也藏在這個(gè)“無限”里內(nèi)容一多縮到全局視圖時(shí)所有字都成了螞蟻壓根看不清放大到能看清細(xì)節(jié)時(shí)又只能看到屏幕中間那一小塊旁邊的節(jié)點(diǎn)、連線、便利貼全在視野之外。來回拖拽、縮放一天下來手腕酸腦子也暈。后來我干脆做了一個(gè)小工具名字就叫MiroFish。它的核心思路很簡單在鼠標(biāo)停留的位置生成一個(gè)“魚眼放大區(qū)域”不改變白板本身的縮放比例而是在當(dāng)前視圖之上疊一層焦點(diǎn)透鏡效果。光標(biāo)附近的內(nèi)容被平滑放大周圍區(qū)域保留原始大小形成一個(gè)“中間高清、四周漸進(jìn)過渡”的瀏覽模式。這樣你既不用切換縮放級別也不用反復(fù)拖動畫布就可以在保持全局定位感的同時(shí)快速看清細(xì)節(jié)。這個(gè)思路其實(shí)借鑒了圖形學(xué)里的“焦點(diǎn)上下文”交互范式也參考了經(jīng)典的魚眼菜單Fisheye Menu。我在實(shí)際使用中發(fā)現(xiàn)這種交互方式用在在線白板上格外契合——因?yàn)榘装宓暮诵拿芫褪恰叭謱?dǎo)航”和“局部精讀”之間的沖突而魚眼透鏡恰好把這兩個(gè)需求同時(shí)滿足了。MiroFish適合誰我覺得主要是這三類人第一類是經(jīng)常在白板上做架構(gòu)評審、代碼走查的研發(fā)人員需要在整體上下文里反復(fù)查看某塊細(xì)節(jié)第二類是遠(yuǎn)程會議里做視覺引導(dǎo)的主持人希望把鼠標(biāo)當(dāng)作“激光筆”讓參會者順著焦點(diǎn)走第三類是對交互設(shè)計(jì)感興趣的前端開發(fā)者想看看魚眼變形、離屏渲染、事件注入這些技術(shù)點(diǎn)在實(shí)際項(xiàng)目里是怎么落地的。2. 拆解整體設(shè)計(jì)為什么選焦點(diǎn)上下文而不是做另一個(gè)縮放2.1 焦點(diǎn)上下文既要看得清又不能丟掉全局剛開始我想得挺簡單給白板加一個(gè)“局部放大鏡”就是了。但真做起來發(fā)現(xiàn)如果只做一個(gè)圓形放大鏡周圍全部變暗或者被裁掉用戶依然會迷失方向。原因在于人的視覺導(dǎo)航依賴周邊視覺提供的“參照物”你把焦點(diǎn)區(qū)域之外的內(nèi)容全部遮住等于把參照物也拿掉了。所以必須引入焦點(diǎn)上下文FocusContext的設(shè)計(jì)理念。核心原則是細(xì)節(jié)區(qū)域焦點(diǎn)被放大同時(shí)保留周圍場景上下文的完整信息并且兩者之間要有一個(gè)平滑的幾何過渡不能出現(xiàn)突然的斷崖。這樣用戶的眼睛在細(xì)節(jié)和全局之間來回切換時(shí)大腦不需要重新建立空間映射。在MiroFish里我實(shí)現(xiàn)的是“徑向魚眼”效果以光標(biāo)為中心隨著與光標(biāo)的距離增加放大倍率從最大逐漸衰減到1。距離越近變形越明顯距離越遠(yuǎn)畫面越接近原始視圖。整個(gè)屏幕依然是完整視圖只是靠近光標(biāo)的那一塊被“撐開”了。這個(gè)效果比傳統(tǒng)放大鏡更自然因?yàn)樗A袅艘徊糠诌^渡區(qū)域作為視覺緩沖。2.2 三條技術(shù)路線我為什么最后選了離屏CanvasMiroFish本質(zhì)上是一個(gè)運(yùn)行在瀏覽器里的增強(qiáng)腳本要對目標(biāo)白板頁面做視覺注入。我試過三條技術(shù)路線這里把對比列出來方案實(shí)現(xiàn)方式優(yōu)點(diǎn)缺點(diǎn)SVG濾鏡在頁面套一層SVG利用feDisplacementMap做像素偏移聲明式代碼量少性能差濾鏡區(qū)域需要實(shí)時(shí)更新對文本渲染不友好變形效果不好控制CSS transform對光標(biāo)附近的DOM節(jié)點(diǎn)做scale變換性能好支持矢量文本只能對已存在的DOM節(jié)點(diǎn)操作白板的畫布是整塊的無法局部縮放難以做連續(xù)平滑的衰減離屏Canvas截取屏幕內(nèi)容到Canvas做像素級再繪制再蓋回頁面靈活可精確控制每個(gè)像素的偏移兼容性最好性能開銷大需要做區(qū)域更新和緩存優(yōu)化我選離屏Canvas最重要的原因是“可控性”。白板頁面本身多是用Canvas或者WebGL渲染的你無法去修改它的內(nèi)部繪制邏輯只能在外部疊加一層“觀感層”。離屏Canvas可以自己控制采樣邏輯想放大哪里就放大哪里想加什么過渡就加什么過渡。短板是性能要自己扛但通過局部重繪和分層緩存最后能做到在實(shí)際白板頁面上比較流暢的運(yùn)行。2.3 核心框架事件層、計(jì)算層、渲染層分離MiroFish的代碼結(jié)構(gòu)沒有用什么重型框架就是一個(gè)普通的TypeScript模塊。但我把邏輯分成了三層這對后面調(diào)試幫了大忙。第一層是事件層負(fù)責(zé)監(jiān)聽鼠標(biāo)移動、滾動、快捷鍵開關(guān)。事件層需要處理一個(gè)很關(guān)鍵的問題目標(biāo)頁面可能自己也在監(jiān)聽mousemove用來做拖拽、劃選之類的操作。我不能把事件全部劫持掉否則會破壞白板本身的行為。所以我采用的方式是“監(jiān)聽不動點(diǎn)”只監(jiān)聽、不攔截把坐標(biāo)信息傳給計(jì)算層。第二層是計(jì)算層負(fù)責(zé)把鼠標(biāo)坐標(biāo)轉(zhuǎn)換成魚眼變形的參數(shù)包括放大倍率、過渡半徑、變形曲線的計(jì)算結(jié)果。這一層不依賴任何DOM操作純函數(shù)就能跑方便做單元測試。第三層是渲染層負(fù)責(zé)把計(jì)算結(jié)果應(yīng)用到Canvas上包括截圖采樣、繪制變形圖、疊加遮罩、最后掛載到頁面的頂層。渲染層只接收參數(shù)和執(zhí)行繪制不關(guān)心鼠標(biāo)怎么來、參數(shù)怎么算。三層之間用簡單的接口通信模塊替換非常容易。3. 核心算法實(shí)現(xiàn)魚眼變形參數(shù)與繪制代碼3.1 從“像素坐標(biāo)映射”理解魚眼變形魚眼變形的本質(zhì)是對原始圖像做一次“非線性坐標(biāo)重映射”。說得直白一點(diǎn)屏幕上每一個(gè)像素顯示的顏色其實(shí)是從原始圖像中某個(gè)位置采樣而來的。在正常視圖下屏幕坐標(biāo)和原始坐標(biāo)是一一對應(yīng)的魚眼變形下這種對應(yīng)關(guān)系變成了以光標(biāo)為中心的徑向扭曲。什么叫徑向扭曲我舉個(gè)例子。你把一張照片打印出來然后用一個(gè)放大鏡懸停在照片上方放大鏡里的圖像就是扭曲后的結(jié)果。放大鏡的鏡片中心放大倍數(shù)最大鏡片邊緣逐漸恢復(fù)到原始大小。MiroFish做的就是這個(gè)事情只不過不是用物理鏡片而是用算法模擬鏡片效果。在實(shí)現(xiàn)時(shí)我要計(jì)算每個(gè)屏幕像素對應(yīng)到原始圖像中的哪個(gè)位置。假設(shè)屏幕像素坐標(biāo)是p鼠標(biāo)中心坐標(biāo)是c首先計(jì)算p和c之間的方向角度angle atan2(p.y - c.y, p.x - c.x)再計(jì)算歸一化距離r distance / R其中R是過渡半徑。然后通過一個(gè)映射函數(shù)把原始距離r映射成新的距離r再根據(jù)r得到原始圖像采樣坐標(biāo)。映射函數(shù)決定了變形的“性格”。如果直接用線性映射邊緣會出現(xiàn)明顯的折痕我最終用的是一種基于冪函數(shù)的平滑曲線在中心區(qū)域有較高的放大倍率到邊緣逐漸衰減到1并且保證在r0和r1兩端的導(dǎo)數(shù)連續(xù)這樣過渡更柔和。3.2 放大倍率、過渡半徑和平滑曲線怎么定這里涉及三個(gè)關(guān)鍵參數(shù)。第一個(gè)是最大放大倍率。我試過1.5倍、2倍、3倍最后默認(rèn)值定在2倍左右。1.5倍看文字細(xì)節(jié)有點(diǎn)吃力尤其是小號代碼3倍又會讓中心區(qū)域占用太多屏幕空間周圍上下文保留得太少反而影響導(dǎo)航。2倍是一個(gè)比較均衡的值看便利貼上的字、小圖標(biāo)的輪廓基本夠用。第二個(gè)是過渡半徑。它決定了焦點(diǎn)區(qū)域有多大。半徑太小一次能看到的細(xì)節(jié)有限鼠標(biāo)移動起來很累半徑太大周圍幾乎全部被變形覆蓋失去了上下文。我默認(rèn)設(shè)置為屏幕短邊的25%左右。舉個(gè)例子我的屏幕分辨率是1920x1080短邊是1080px那么過渡半徑就是270px。這個(gè)數(shù)值可以根據(jù)屏幕尺寸自動計(jì)算用戶也可以通過配置項(xiàng)手動調(diào)整。第三個(gè)是平滑曲線。單純從中心到邊緣做線性衰減畫面看起來會非常“硬”像戴了度數(shù)不對的眼鏡。我采用的是平滑插值函數(shù)衰減過程分為兩個(gè)階段中央?yún)^(qū)域保持較高倍率迅速過渡到接近1。具體公式可以參考smoothstep變體function fishEyeScale(r: number, maxScale: number): number { // r為歸一化距離范圍[0,1] // 返回值表示該位置的局部縮放比例 const falloff 1 - Math.pow(r, 3); // 曲線在r較小的時(shí)候衰減慢r較大的時(shí)候衰減快 return 1 (maxScale - 1) * falloff; }r的三次方是調(diào)參試出來的。三次方曲線在近中心的區(qū)域下降較慢適合保留大范圍的高倍細(xì)節(jié)到邊緣快速趨向1不會占掉太多視圖。3.3 Canvas離屏繪制的完整流程與關(guān)鍵代碼離屏Canvas繪制的流程說起來并不復(fù)雜分為四步。第一步把目標(biāo)白板頁面當(dāng)前viewport的內(nèi)容截取到一張離屏Canvas里。這里有個(gè)技術(shù)前提如果要截取頁面的實(shí)際內(nèi)容必須依賴瀏覽器的html2canvas之類能力或者利用白板自身暴露的截圖API。但MiroFish定位是可注入腳本不依賴白板接口所以我采用的是“取當(dāng)前屏幕像素”的另類方式創(chuàng)建一個(gè)全屏Canvas然后用CanvasRenderingContext2D的drawWindow或者借助擴(kuò)展API的captureTab。這里為了便于演示我以Chrome擴(kuò)展的方式實(shí)現(xiàn)使用chrome.tabs.captureVisibleTab獲取當(dāng)前tab的截圖然后把截圖Image繪制到離屏Canvas。第二步針對鼠標(biāo)焦點(diǎn)把離屏Canvas作為源圖像對目標(biāo)區(qū)域的每個(gè)像素做魚眼重映射繪制到一個(gè)新的前景Canvas上。但這可不是逐像素drawPixel那樣性能早就崩了。我用的方法是把目標(biāo)區(qū)域分成網(wǎng)格每個(gè)網(wǎng)格單元根據(jù)映射函數(shù)計(jì)算新的位置和大小然后通過drawImage把源圖像的小方塊繪制到新位置。這種方式利用了GPU加速性能遠(yuǎn)好于逐像素操作。核心偽代碼如下function renderFisheye( sourceCanvas: HTMLCanvasElement, targetCanvas: HTMLCanvasElement, center: Point, radius: number, maxScale: number ) { const ctx targetCanvas.getContext(2d); ctx.clearRect(0, 0, targetCanvas.width, targetCanvas.height); // 網(wǎng)格步長步長越小效果越細(xì)膩但性能越差 const step 4; const scale0 1 / maxScale; // 目標(biāo)中心對應(yīng)的源圖采樣縮放起點(diǎn) for (let y center.y - radius; y center.y radius; y step) { for (let x center.x - radius; x center.x radius; x step) { const dx x - center.x; const dy y - center.y; const dist Math.sqrt(dx * dx dy * dy); if (dist radius) continue; const r dist / radius; // 當(dāng)前目標(biāo)像素對應(yīng)的源圖縮放比例 const localScale fishEyeScale(r, maxScale); // 目標(biāo)的矩形網(wǎng)格尺寸 const targetW step * localScale; const targetH step * localScale; // 對應(yīng)的源圖像區(qū)域注意這里需要按比例反向計(jì)算 const srcW step / localScale; const srcH step / localScale; const srcX x - srcW / 2; const srcY y - srcH / 2; ctx.drawImage( sourceCanvas, srcX, srcY, srcW, srcH, x - targetW / 2, y - targetH / 2, targetW, targetH ); } } }這段代碼是MiroFish的核心。理解它就理解了魚眼透鏡的繪制原理。第三步把這個(gè)帶有魚眼效果的前景Canvas掛載到頁面的頂層。注意前景Canvas尺寸應(yīng)與屏幕可視區(qū)一致但為了性能不需要透明完整覆蓋可以只創(chuàng)建一個(gè)跟焦點(diǎn)區(qū)域相關(guān)的小Canvas然后把它定位到鼠標(biāo)附近。這樣可以大幅減少重繪面積。做成一個(gè)跟隨鼠標(biāo)的矩形窗口再把矩形窗口內(nèi)部繪制成魚眼效果外面依然是正常視圖。第四步繪制一個(gè)平滑的圓形遮罩或漸變遮罩讓焦點(diǎn)區(qū)域與周圍正常視圖之間的邊界不顯得突兀。我在過渡半徑的最外圍加了一個(gè)半透明的陰影圈既能提示用戶當(dāng)前放大范圍又能掩蓋邊緣的輕微變形。3.4 鼠標(biāo)跟蹤、節(jié)流和坐標(biāo)換算的坑鼠標(biāo)跟蹤的坑比想象中多。首當(dāng)其沖的是坐標(biāo)換算。Chrome擴(kuò)展里captureVisibleTab截到的圖片尺寸跟目標(biāo)頁面的CSS像素不一定相等。如果設(shè)備像素比是2截圖是3840x2160而頁面CSS坐標(biāo)是1920x1080中間差了一倍。如果在繪制時(shí)直接用鼠標(biāo)的clientX作為坐標(biāo)眼睛能看到的地方和手指指的地方就會錯(cuò)位一大截。解決方式是在截圖之后記錄一下當(dāng)前截圖圖片的寬度和頁面顯示的寬度計(jì)算一個(gè)scaleRatio imageWidth / window.innerWidth然后把所有的鼠標(biāo)坐標(biāo)都乘上這個(gè)比例再參與計(jì)算。繪制到屏幕上的前景Canvas時(shí)再把坐標(biāo)除以這個(gè)比例還原回來。這步一漏光標(biāo)跑偏是必然的。其次是節(jié)流。mousemove事件觸發(fā)頻率非常高一秒鐘可以觸發(fā)幾十次甚至上百次。每次觸發(fā)都去做截圖和重繪性能再好的機(jī)器也扛不住。我采用的方式是requestAnimationFrame配合“臟標(biāo)記”機(jī)制鼠標(biāo)事件只更新一個(gè)坐標(biāo)標(biāo)記渲染循環(huán)里判斷如果標(biāo)記變化了才重新繪制。這樣繪制頻率被限制在屏幕刷新率以內(nèi)通常就是60fps。4. 性能調(diào)優(yōu)與兼容性處理4.1 從卡成幻燈片到60fps區(qū)域更新和緩存第一版MiroFish跑起來以后我的第一反應(yīng)是“這也能叫工具”。鼠標(biāo)只要一動整個(gè)屏幕都跟著卡風(fēng)扇呼呼轉(zhuǎn)CPU占用直接拉滿。后來定位到問題所在我當(dāng)時(shí)是在整個(gè)屏幕Canvas上做逐網(wǎng)格重繪屏幕是1920x1080網(wǎng)格步長4px意味著每一幀要跑約13萬個(gè)循環(huán)每次循環(huán)還有若干次drawImage調(diào)用這個(gè)計(jì)算量實(shí)在太大。后來我做了兩個(gè)優(yōu)化。第一個(gè)優(yōu)化是只重繪焦點(diǎn)周圍的局部區(qū)域。魚眼效果只發(fā)生在以鼠標(biāo)為中心、半徑R的圓形范圍內(nèi)圓外完全不變。所以我新建了一個(gè)尺寸等于“半徑R乘以2”的局部窗口Canvas渲染時(shí)只處理這個(gè)局部區(qū)域然后把它定位到鼠標(biāo)位置。這樣每幀處理的像素面積大約只占全屏的20%左右計(jì)算量瞬間降了下來。第二個(gè)優(yōu)化是源圖緩存。每次鼠標(biāo)移動不需要重新截屏因?yàn)榘装鍍?nèi)容沒有變化只需要繼續(xù)使用上一幀的源Canvas作為采樣來源。只有用戶滾動、縮放白板或者白板內(nèi)容本身更新時(shí)才需要重新截屏。為此我維護(hù)了一個(gè)“截圖版本號”只要白板畫布區(qū)域發(fā)生變化通過監(jiān)聽滾動、鍵盤、點(diǎn)擊等事件就把版本號加一渲染循環(huán)檢測到版本號變化后再刷新源圖像。經(jīng)過這兩步優(yōu)化MiroFish在實(shí)際白板頁面上的表現(xiàn)基本穩(wěn)定在50到60fps。要說完全不掉幀也不可能畢竟截圖和采樣本身有開銷但至少從“沒法用”變成了“能日常用”。4.2 邊界陰影與漸變遮罩讓過渡更自然魚眼透鏡不只是把圖像放大就完事了。如果你直接把圓形區(qū)域內(nèi)的變形圖像蓋在正常視圖上邊緣會有一圈很明顯的“剪切感”。特別是當(dāng)你放大白板里的卡片時(shí)卡片邊緣的線條會在圓邊界處突然斷掉看著非常難受。我后來加了一層徑向漸變遮罩。具體做法是在渲染完成之后用Canvas畫一個(gè)從透明到半透明的徑向漸變覆蓋在焦點(diǎn)區(qū)域的外圈。這個(gè)遮罩不會遮蓋中心內(nèi)容只是在邊緣制造出大約20到30像素寬的柔和衰減。這樣做之后用戶看到的就不是一個(gè)硬邦邦的圓形放大鏡而是一團(tuán)柔和的視覺焦點(diǎn)。除此之外我還加了一條很細(xì)的高亮描邊在焦點(diǎn)區(qū)域外圍勾勒出一個(gè)淡淡的圓環(huán)。這個(gè)圓環(huán)有兩個(gè)作用一是提示用戶當(dāng)前放大的邊界在哪里避免鼠標(biāo)移到哪都要仔細(xì)辨認(rèn)二是給畫面一個(gè)視覺落腳點(diǎn)讓眼睛能快速定位焦點(diǎn)。實(shí)測下來這條描邊讓焦點(diǎn)區(qū)域的感知清晰度提升了很多。4.3 頁面縮放、白板快捷鍵沖突的處理兼容性方面我踩了不少坑。第一個(gè)坑是瀏覽器頁面縮放。用戶用Ctrl加滾輪把頁面縮放到90%或者125%坐標(biāo)換算又要亂套。我的處理方式是在每次截圖時(shí)讀取window.devicePixelRatio和頁面縮放比例統(tǒng)一換算成“邏輯像素坐標(biāo)”參與計(jì)算渲染時(shí)再按比例縮放。第二個(gè)坑是白板自身的快捷鍵。在白板上空格加拖拽是平移Shift加滾輪是橫向滾動Ctrl加滾輪是縮放。MiroFish在捕獲鼠標(biāo)位置時(shí)不能攔截這些事件否則會破壞白板的原有交互。所以我在事件層只做passive監(jiān)聽不調(diào)用preventDefault。用戶依然可以使用空格平移Ctrl加滾輪縮放MiroFish只是被動地跟隨鼠標(biāo)位置更新焦點(diǎn)。只有當(dāng)用戶按下我們定義的自定義快捷鍵時(shí)比如雙擊鼠標(biāo)中鍵才臨時(shí)鎖定透鏡效果方便用戶在不觸發(fā)放大的情況下進(jìn)行精細(xì)拖拽。5. 常見問題與排查技巧5.1 放大區(qū)邊緣出現(xiàn)“割裂感”很多第一次跑起來的朋友反饋焦點(diǎn)區(qū)域的邊緣會看到明顯的內(nèi)容錯(cuò)位或線條斷裂。這個(gè)問題絕大多數(shù)情況下不是算法錯(cuò)了而是網(wǎng)格步長太大。當(dāng)網(wǎng)格步長設(shè)置為8px甚至10px時(shí)圖像被分成一塊塊小方塊每塊內(nèi)部的繪制位置都是按中心點(diǎn)計(jì)算的邊緣自然會出現(xiàn)鋸齒和錯(cuò)位。解決方法是調(diào)小網(wǎng)格步長通常設(shè)為2到4像素。步長越小變形越平滑但性能開銷越大。如果步長已經(jīng)調(diào)到2邊緣還是割裂那就要檢查是不是縮放比例沒算對。焦點(diǎn)區(qū)域內(nèi)的坐標(biāo)在“截圖坐標(biāo)”和“頁面坐標(biāo)”之間換算不正確會導(dǎo)致源圖像塊的位置整體偏移邊緣出現(xiàn)類似重影的效果??梢耘R時(shí)在調(diào)試模式下繪制網(wǎng)格邊界確認(rèn)采樣位置是否正確。5.2 光標(biāo)位置和放大內(nèi)容錯(cuò)位這是最容易讓人崩潰的問題。鼠標(biāo)指著一個(gè)節(jié)點(diǎn)放大區(qū)域里顯示的是旁邊另一個(gè)節(jié)點(diǎn)。我在開發(fā)時(shí)也遇到過一次排查了半天才發(fā)現(xiàn)是因?yàn)榻貓D時(shí)機(jī)不對。captureVisibleTab返回的是異步結(jié)果如果你在鼠標(biāo)剛移動時(shí)就發(fā)起截圖截圖圖像可能還是上一幀的畫面這時(shí)候用它來采樣新坐標(biāo)和舊畫面就會產(chǎn)生偏差。我建議的做法是每次截圖完成后給截圖對象打一個(gè)時(shí)間戳。渲染時(shí)只有當(dāng)截圖完成時(shí)間晚于當(dāng)前鼠標(biāo)坐標(biāo)更新時(shí)間時(shí)才用最新的坐標(biāo)做采樣否則繼續(xù)使用上一幀的渲染結(jié)果。簡單說就是“慢一步”比“錯(cuò)一路”好。鼠標(biāo)坐標(biāo)更新很快截圖慢一點(diǎn)沒關(guān)系只要保證坐標(biāo)和截圖在時(shí)間上基本對應(yīng)視覺上就不會有太大的錯(cuò)位感。5.3 在某些白板上不生效或掉幀MiroFish最開始的版本只在Miro上測試后來我試著用于其他類似的白板工具發(fā)現(xiàn)有些頁面根本截不到圖或者截圖是一張純白圖。原因是部分白板使用WebGL渲染頁面內(nèi)容并不在普通的DOM層里而是通過GPU繪制到canvas上。普通的截屏API拿到的不一定是實(shí)際內(nèi)容。這種情況只能針對具體白板做一些適配。如果白板自己有截圖導(dǎo)出接口可以在腳本里調(diào)用它的接口獲取畫布內(nèi)容然后作為源圖像。如果沒有可以嘗試把渲染層的Canvas設(shè)置成透明并利用瀏覽器擴(kuò)展的debugger協(xié)議直接抓取GPU合成后的幀緩沖但是這會犧牲通用性。目前MiroFish對基于Canvas 2D渲染的白板兼容最好對WebGL渲染的白板需要單獨(dú)處理。6. 做這個(gè)項(xiàng)目的幾點(diǎn)心得MiroFish這個(gè)項(xiàng)目從想法到能日常使用斷斷續(xù)續(xù)花了我兩個(gè)多星期的業(yè)余時(shí)間?;仡^看最大的收獲不是實(shí)現(xiàn)了魚眼效果而是我學(xué)會了一件事做交互增強(qiáng)工具一定要克制。很多人在看到魚眼效果的第一反應(yīng)是“能不能讓整個(gè)屏幕都變成魚眼”、“能不能自定義每個(gè)區(qū)域的放大倍率”但我最終都拒絕了這些想法。MiroFish的意義在于“在合適的位置提供一個(gè)短暫的放大細(xì)節(jié)”而不是改變整個(gè)白板的視覺形態(tài)。過度的變形反而會讓人覺得頭暈。還有一點(diǎn)心得是關(guān)于性能的。瀏覽器端做這種像素級操作永遠(yuǎn)要把“減少繪制面積”放在第一位。想盡辦法減少每幀處理的像素?cái)?shù)量比單純優(yōu)化單個(gè)像素的計(jì)算快上百倍。MiroFish最后能做到可用靠的不是什么高深的圖像算法而是老老實(shí)實(shí)地把局部重繪、緩存、臟標(biāo)記這些基本功做到位。最后再分享一個(gè)小技巧如果你也想做類似的屏幕增強(qiáng)工具建議先手動模擬一遍用戶操作找到用戶最頻繁的交互路徑再決定要把哪一塊做成視覺增強(qiáng)。很多人上來就追求高精尖的視覺效果卻忽略了用戶真正需要的是“一眼看清遠(yuǎn)方的細(xì)節(jié)”這個(gè)樸素需求。MiroFish的定位就是一個(gè)安靜的小工具不搶風(fēng)頭但能讓你在密密麻麻的白板上少花一半時(shí)間去尋找。