三角形的 N 種畫法與瀏覽器的開放世界

最近,我徹底沉迷在了任天堂 Switch 上的《塞爾達傳說:荒野之息》裏,以致於專欄都快要停更了(罪過罪過)。大概每一個塞爾達玩家都會有這個疑問,那就是 這個遊戲爲何這麼好玩?! 很是有意思的是,這個問題的答案彷佛和「前端爲何這麼突飛猛進」有着微妙的關係,這讓我有了一些全新的認識…css

塞爾達的遊戲體驗有一點廣受好評,那就是符合直覺的開放世界。換句話說,在這個遊戲裏想要作到一件事,只要你能想到什麼方式,那麼你幾乎就能基於這種方式去實現。好比,你看到樹上掛着一顆蘋果,那麼想要摘下這顆蘋果,至少有如下這些辦法:html

  • 把樹砍倒,撿到蘋果
  • 爬樹、騎在立刻或者搬來箱子墊腳夠到蘋果
  • 用弓箭把蘋果射下來
  • 扇風或者炸彈製造衝擊波,把蘋果吹下來
  • 從周圍的高地滑翔到蘋果樹上
  • 放火把樹點着,留下烤蘋果
  • ……

這種自由度使得遊戲的冒險體驗充滿了驚喜。對各類棘手的機關謎題,解法經常是開放而不惟一的。巧的是,我近期的工做也和折騰前端的各類渲染機制有些關係。當用自由程度來評價瀏覽器的時候,能看到的幾乎也是一個塞爾達級別的開放世界了。前端

咱們不妨用三角形做爲例子吧。三角形做爲最簡單的幾何圖形,繪製它對於任何一位前端同窗都不會是一件難事。但在今天的前端領域裏,到底有多少種技術方案可以畫出一個三角形呢?答案能夠說很是的百花齊放了。讓咱們按部就班地開始吧。下面的各類套路能夠按照折騰程度分爲三種:面試

  • 2B Play
  • 普通 Play
  • 羞恥 Play

2B Play

首先讓咱們從最不費勁的耍無賴方法開始吧:編程

字符

還有什麼比複製粘貼一個 字符更簡單的繪製方式呢?這其實就是個形如 '\u25b3' 的 Unicode 特殊字符而已。canvas

圖片

看起來 <img src="三角形.jpg"/> 的套路很 low,但徹底沒毛病啊🙄api

HTML

只要垂直居中一系列寬度均勻增加的矩形,咱們是否是就獲得了一個三角形呢😅瀏覽器

<div class="triangle">
  <div style="width: 1px; height: 1px;"></div>
  <div style="width: 2px; height: 1px;"></div>
  <div style="width: 3px; height: 1px;"></div>
  <div style="width: 4px; height: 1px;"></div>
  <!-- ...... -->
</div>
複製代碼

Demosvg

普通 Play

若是感受上面的實現太過於玩世不恭,接來下咱們能夠用一些略微「正常」一點的操做來畫出一樣的三角形:函數

CSS

CSS 裏充斥着大量的奇技淫巧,而下面這個操做多是不少面試題的標準答案了。咱們只須要簡單的 HTML:

<div class="triangle"></div>
複製代碼

配合魔改容器邊框的樣式:

.triangle {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
}
複製代碼

就可以模擬出一個三角形了。Demo

Icon Font

把字體當作圖標使用的作法也是老調重彈了。只須要大體這樣的字體樣式配置:

@font-face {
  font-family: Triangle;
  src: url(./triangle.woff) format("woff");
}

.triangle:before { content:"\t666" }
複製代碼

這樣一個 <i class="triangle"></i> 的標籤,就能經過 :before 插入特殊字符,進而渲染對應的圖標字體了😑Demo

SVG

不少時候咱們習慣把 SVG 當作圖片同樣的靜態資源直接引入使用,但其實只要稍微瞭解一下它的語法後,就會發現直接手寫 SVG 來繪製簡單圖形也並不複雜:

<svg width="100" height="100">
  <polygon points="50,0 100,100 0,100" style="fill: red;"/>
</svg>
複製代碼

Demo

Clip Path

SVG 和 CSS 有不少類似之處,但 CSS 雖然長於樣式,長久以來卻一直缺少「繪製出一個形狀」的能力。好在 CSS 規範中剛加入不久的 clip path 可以名正言順地讓咱們用相似 SVG 的形式繪製出更多樣的形狀。這隻須要形以下面的樣式:

.triangle {
  width: 10px; height: 10px;
  background: red;
  clip-path: polygon(50% 0, 0 100%, 100% 100%);
}
複製代碼

這和熟悉的 border 套路有什麼區別呢?除了代碼更直觀簡潔之外,它還可以爲繪製出的形狀支持背景圖片屬性,惋惜的地方主要是 IE 兼容了。Demo

Canvas

到目前爲止的方法沒有一個須要編寫 JS 代碼,這多少有些對不起工錢。還好咱們有 Canvas 來名正言順地折騰。只須要一個 <canvas> 標籤配上這樣的膠水代碼就行:

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.fillStyle = 'red'
ctx.moveTo(50, 0)
ctx.lineTo(0, 100)
ctx.lineTo(100, 100)
ctx.fill()
複製代碼

Demo

羞恥 Play

若是你仍是嫌棄上面的操做過於中規中矩,讓咱們用最後的幾種方法來探索瀏覽器的自由尺度吧:

CSS Houdini

近期的 CSS 大會上 CSS Houdini 能夠說賺足了眼球。這套大大加強 CSS 控制力的規範中,目前已經實裝的主要也就是 CSS Paint 了。簡而言之,經過這個 API,只要 CSS 屬性須要圖片的地方,你就能夠編程式地經過 canvas 控制圖片的渲染過程。

經過 CSS.paintWorklet.addModule API,咱們能夠定義繪製 canvas 所用的 paint worklet:

<script> CSS.paintWorklet.addModule('/worklet.js') </script>
複製代碼

Paint worklet 中可以拿到正常的 canvas 上下文:

class TrianglePainter {
  paint(ctx, geom, properties) {
	 const offset = geom.width
    ctx.beginPath()
    ctx.fillStyle = 'red'
    ctx.moveTo(offset / 2, 0)
    ctx.lineTo(offset, offset)
    ctx.lineTo(0, offset)
    ctx.fill()
  }
}

registerPaint('triangle', TrianglePainter)
複製代碼

只要這樣,就能在 CSS 裏使用 paint 規則了:

.demo {
  width: 100px;
  height: 100px;
  background-image: paint(triangle);
}
複製代碼

咱們還可使用 CSS Variable 在 CSS 中定義形如 --triangle-size--triangle-fill 的參數,來控制 canvas 的渲染,這樣在參數更新時 canvas 會自動重繪。結合上 animation,它在特效領域的想象空間也很大。雖然最後使用的仍是前面說起的 canvas,但 Houdini 確實給基於 CSS 的渲染帶來了更大的掌控。

WebGL 多邊形

主流瀏覽器對 WebGL 的支持已經至關不錯了,但目前看來它仍然不是前端領域人人必備的主流技術。這或許和它較爲陡峭的學習曲線有關。可能有很多同窗對 WebGL 有一種誤解,即它和 canvas 同樣,是一套 JS API。實際上,編寫 WebGL 應用時,除了須要編寫運行在 CPU 範疇內的 JS 膠水代碼外,真正在 GPU 上執行的是 GLSL 語言編寫的着色器。可是因爲繪圖庫自己的複雜性,在入門示例中,JS 的膠水代碼佔了絕對的大頭。按照計算機圖形學循序漸進的教程,即使只是完成一個三角形的渲染過程,也須要百行左右的代碼。限於篇幅,咱們只簡要地將這個流程裏所須要作的關鍵事項歸納爲如下三步:

  1. 用 GLSL 語言編寫頂點着色器和片元着色器。
  2. 定義出一個頂點緩衝區,向其中傳入三角形逐個頂點的數據。
  3. 在咱們本身實現的 render 函數裏作一些準備。在加載完着色器程序後,調用 drawArray API 繪製緩衝區中數據。

這個過程(Demo)初看之下控制的不過是一個更囉嗦而折騰的 canvas 而已,除了能夠支持 3D 之外,有什麼不一樣呢?在最後一種方法裏咱們就能看到區別了。

WebGL 造型函數

上面的流程基本是每個 WebGL 教程都會循序漸進地去作的。考慮這個問題:繪製三角形必定須要提供三個頂點嗎?這可不必定。

熟悉 canvas 的同窗都知道,在處理圖像時,像下面這樣的逐像素操做很容易帶來性能問題:

for (let i = 0; i < width; i++) {
  for (let j = 0; j < height; j++) {
    // ...
  }
}
複製代碼

可是在 WebGL 中,是不存在這樣串行的循環的。你用 GLSL 語言所編寫的着色器,會被編譯到 GPU 上去並行執行。聽起來是否是比較酷?上面已經提到,咱們有兩種着色器,即頂點着色器片元着色器

  • 頂點着色器的代碼逐頂點執行,好比對於三角形,它就執行三次。
  • 片元着色器的代碼逐片元(粗略的理解就是像素)執行,對於一個 100x100 的區域,GPU 會並行地對這 1w 個像素調用片元着色器,這個並行的過程對你是透明的。

因此對於一個「逐像素執行」的片元着色器來講,只要它知道本身每次被調用時所在的座標,那麼就可以根據這個位置計算出最終的顏色。這樣一來,咱們甚至不須要頂點緩衝區,就可以基於特定的公式去計算逐像素的顏色了。這樣爲着色器設計的函數咱們稱爲 shaping function,即造型函數。一個正多邊形的着色器形如:

#define TWO_PI 6.28318530718

// 由 JS 傳入的屏幕分辨率
uniform vec2 u_resolution;

void main() {
  vec2 st = gl_FragCoord.xy/u_resolution.xy;
  st.x *= u_resolution.x/u_resolution.y;
  vec3 color = vec3(0.0);
  float d = 0.0;

  // 從新映射空間座標到 -1. 與 1. 間
  st = st * 2.-1.;

  // 多邊形邊數量
  int N = 3;

  // 當前像素的角度與半徑
  float a = atan(st.x,st.y)+PI;
  float r = TWO_PI/float(N);

  // 調節距離的造型函數
  d = cos(floor(.5+a/r)*r-a)*length(st);

  color = vec3(1.0-smoothstep(.4,.41,d));
  // color = vec3(d);

  gl_FragColor = vec4(color,1.0);
}
複製代碼

這就是一個船新的領域了,因爲 shader 編程要求對衆多的像素編寫出同一份簡潔而並行執行的代碼,彼此之間還徹底透明且沒法隨意 log 調試,這使得面向着色器編程的門檻實際上很高。這裏的示例在很是好的入門書 The Book of Shaders 中有相應的章節,有興趣的同窗或許會打開新世界的大門哦🤔

P.S. 在這裏咱們爲何要捨近求遠呢?這個途徑其實和字體渲染的原理有些接近,近期我也在學習一些相關的知識,但願屆時能有更多的內容能夠分享~

總結

不能否認,常規的業務開發很容易進入枯燥的重複勞動階段,但再看開一點,咱們能夠發現實際上咱們已經有了很是多可用的技術手段來優化前端這個領域裏的交互了。一個簡單的三角形都能用 HTML / CSS / JS / GLSL 四種語言的十幾種方案來畫,更復雜的場景下就更是百花齊放了。瀏覽器的渲染能力之強應該也算得上是個開放世界了吧:別管你想畫什麼,總有適合你的方法去實現。

不過和塞爾達裏越高級的操做看起來越風騷簡潔不一樣,越是掌控力強的技術方案,在實現上就會更加複雜。但總之不論是遊戲仍是代碼仍是生活,相信快樂的方式都不止一種~但願你們都可以享受過程,找到屬於本身的那份樂趣~

相關文章
相關標籤/搜索