現(xiàn)可雕刻筆畫的Canvas 2D繪畫引擎)
之前看到一款繪畫引擎的技術(shù)演示時(shí)最大的感受是它把“畫”和“雕”這兩件事放在了一個(gè)畫布里。傳統(tǒng)繪圖軟件里一筆畫下去線條基本就是像素想微調(diào)只能撤銷、擦除或者用選區(qū)反復(fù)修改而這個(gè) demo 里筆畫像黏土一樣可以用類似雕刻筆刷的工具繼續(xù)推、拉、塑形。這種交互從直覺上非常自然但背后涉及的數(shù)據(jù)結(jié)構(gòu)和幾何算法卻和普通畫板應(yīng)用完全不同。這篇文章會圍繞“painting engine”和“sculpt drawings”這兩個(gè)關(guān)鍵詞展開。我會先說明繪圖引擎與普通畫板應(yīng)用的本質(zhì)區(qū)別再拆解實(shí)現(xiàn)“雕刻筆畫”需要哪些核心算法最后帶你在瀏覽器里用 Canvas 2D 從零實(shí)現(xiàn)一個(gè)可運(yùn)行的簡化版繪圖引擎。代碼會按文件拆分可以直接復(fù)制運(yùn)行。適合對圖形學(xué)、創(chuàng)意編程、Web 前端交互感興趣的開發(fā)者閱讀。1. 把“畫”和“雕”結(jié)合的繪畫引擎1.1 為什么需要“繪畫引擎”這個(gè)概念很多開發(fā)者畫過畫板應(yīng)用最常見的實(shí)現(xiàn)方式很簡單監(jiān)聽鼠標(biāo)移動(dòng)不斷在 Canvas 上畫線段。這套方案的優(yōu)點(diǎn)是代碼量少幾十分鐘就能做出一個(gè)有模有樣的畫板缺點(diǎn)同樣明顯一旦線條畫上去它就被“烤”進(jìn)了畫布里之后無法再作為獨(dú)立物體去編輯。繪畫引擎的底層思路是把每一筆都當(dāng)作一個(gè)對象保存在內(nèi)存中而不是直接燒進(jìn)像素。它通常包含三個(gè)層面一、數(shù)據(jù)層保存筆畫路徑、顏色、粗細(xì)、透明度等屬性。二、渲染層根據(jù)數(shù)據(jù)重新繪制畫面。三、交互層支持對單個(gè)筆畫或筆畫的局部區(qū)域進(jìn)行修改。有了這個(gè)結(jié)構(gòu)筆畫才不再是“死像素”而是可以被再次選中的曲線。雕刻本質(zhì)上就是對畫完的曲線做幾何變形。1.2 “雕刻筆畫”到底是什么意思雕刻在物理世界里是把一塊材料削掉或塑造成新形狀而在繪圖引擎中“雕刻筆畫”通常指通過指針把筆畫局部區(qū)域“推”走。想象一下你用毛筆在紙上畫了一個(gè)弧線拿起雕刻刀把弧線的某個(gè)位置向前頂了一下弧線立刻彎向另一個(gè)方向。從算法角度講這個(gè)操作可以拆成四個(gè)步驟判斷指針附近是否存在筆畫找到筆畫上距離指針小于某個(gè)半徑的控制點(diǎn)按照距離計(jì)算每個(gè)點(diǎn)的移動(dòng)權(quán)重把所有點(diǎn)移動(dòng)到位后重新渲染。由于筆畫是矢量點(diǎn)串修改只需要更新點(diǎn)的坐標(biāo)不需要做復(fù)雜的像素級圖像處理。這也是為什么這個(gè) demo 可以做到流暢的實(shí)時(shí)變形——它操作的是幾何對象而不是成百上千萬的像素。1.3 可以應(yīng)用在哪些場景“先畫線、再塑形”的交互并不是玩具它在很多創(chuàng)作場景中都有價(jià)值。插畫和漫畫輔助工具快速起稿后通過筆刷推拉線條減少反復(fù)擦除重畫。白板與在線協(xié)作工具用戶畫復(fù)雜示意圖時(shí)手勢往往不夠精確雕刻功能可以事后修線。教學(xué)演示工具把一條曲線的變化過程可視化幫助學(xué)生理解控制點(diǎn)、貝塞爾曲線的影響。創(chuàng)意編程作品通過算法把筆畫的幾何變形和音視頻、物理模擬結(jié)合起來。這也是一個(gè)典型的技術(shù) demo 方向看似簡單實(shí)際打通了輸入采集、幾何運(yùn)算、渲染更新和交互反饋一整條鏈路。2. 技術(shù)選型與整體設(shè)計(jì)2.1 數(shù)據(jù)模型筆畫不是線段而是點(diǎn)數(shù)組實(shí)現(xiàn)雕刻能力最自然的數(shù)據(jù)結(jié)構(gòu)是把一條筆畫保存為一組連續(xù)的點(diǎn)。每一個(gè)點(diǎn)包含 x 坐標(biāo)和 y 坐標(biāo)多個(gè)點(diǎn)按順序連起來就是一根路徑。舉個(gè)例子const stroke { color: #2f6fed, lineWidth: 4, points: [ { x: 100, y: 120 }, { x: 108, y: 128 }, { x: 121, y: 140 } ] };當(dāng)用戶雕刻時(shí)程序只需要修改points數(shù)組里部分點(diǎn)的x、y值。點(diǎn)和點(diǎn)之間的順序保持不變筆畫拓?fù)溥€是原來的模樣只是形狀發(fā)生了局部偏移。把筆畫設(shè)計(jì)成對象而不是直接畫成像素還有一個(gè)額外好處Stroke 對象可以附帶各種元信息。例如筆刷類型、時(shí)間戳、用戶 ID這些信息未來做撤銷、協(xié)作、導(dǎo)出矢量格式時(shí)都有用處。2.2 渲染策略全量重繪既然筆畫是數(shù)據(jù)渲染就很自然地變成“根據(jù) strokes 數(shù)組統(tǒng)一畫一遍”。每一幀重繪流程如下清空畫布 → 重繪畫布網(wǎng)格背景 → 遍歷所有筆畫對象 → 對每條筆畫執(zhí)行路徑繪制全量重繪在某些開發(fā)者看來可能浪費(fèi)性能但在筆畫數(shù)量不多、單條筆畫控制點(diǎn)數(shù)量受控時(shí)這反而是最簡單可靠的做法。Canvas 2D 本身是立即模式渲染接口它不像 DOM 那樣有 diff 機(jī)制與其記錄臟區(qū)域做局部重繪不如先保證邏輯清晰等性能瓶頸出現(xiàn)后再優(yōu)化。本文代碼就采用全量重繪策略。文章后面的工程優(yōu)化部分會介紹更復(fù)雜的局部重繪方案。2.3 為什么使用 Canvas 2D 而不是 SVG 或 WebGL實(shí)現(xiàn)矢量筆畫可以選擇很多技術(shù)方案SVG有天然的節(jié)點(diǎn)模型但修改節(jié)點(diǎn)后瀏覽器自動(dòng)重排大量筆畫時(shí)性能不容易控制。WebGL性能強(qiáng)大但需要自己處理渲染管線實(shí)現(xiàn)曲線走樣和字體、交互成本偏高。Canvas 2DAPI 簡單具備quadraticCurveTo、bezierCurveTo、arc等基礎(chǔ)繪圖能力非常適合實(shí)現(xiàn)輕量繪圖引擎。對一個(gè)技術(shù) demo 來說Canvas 2D 是最均衡的選擇。它既不要求開發(fā)者掌握著色器語言又能直接使用成熟的線條平滑和抗鋸齒能力。3. 環(huán)境準(zhǔn)備與項(xiàng)目結(jié)構(gòu)3.1 運(yùn)行環(huán)境整個(gè) demo 不依賴任何第三方庫也不需要構(gòu)建工具。只需要滿足以下條件- 現(xiàn)代瀏覽器Chrome、Edge、Firefox、Safari 均可 - HTML5 Canvas 2D 接口 - Pointer Events用于統(tǒng)一處理鼠標(biāo)和觸控筆 - 本地靜態(tài)服務(wù)器建議使用避免本地文件安全限制運(yùn)行時(shí)主要使用的是 Canvas 2D 的quadraticCurveTo方法以及 Pointer Events 的pointerdown、pointermove、pointerup。這些接口在現(xiàn)代瀏覽器中已經(jīng)非常成熟不需要額外引入 polyfill。3.2 項(xiàng)目目錄在桌面新建一個(gè)文件夾例如painting-engine-demo目錄結(jié)構(gòu)如下painting-engine-demo/ ├── index.html # 頁面結(jié)構(gòu)與樣式 ├── engine.js # 繪圖引擎核心邏輯 └── main.js # UI 事件與業(yè)務(wù)交互后面的實(shí)戰(zhàn)部分會完整給出這三個(gè)文件的代碼。4. 核心算法拆解4.1 輸入采樣控制點(diǎn)數(shù)才能保證性能鼠標(biāo)移動(dòng)事件非常高頻如果每幀把所有事件點(diǎn)都塞進(jìn)筆畫一個(gè)長筆畫可能產(chǎn)生幾千個(gè)點(diǎn)。這些點(diǎn)不僅浪費(fèi)內(nèi)存還會讓雕刻循環(huán)變慢因?yàn)槊看问髽?biāo)移動(dòng)都要遍歷所有筆畫的所有點(diǎn)。常見的做法是設(shè)置一個(gè)最小采樣間距。只有當(dāng)前指針位置距離上一個(gè)已保存點(diǎn)超過閾值時(shí)才把新點(diǎn)加入路徑。本文使用的是 2 像素閾值。function distance(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }當(dāng)用戶快速滑動(dòng)鼠標(biāo)時(shí)兩個(gè)相鄰鼠標(biāo)