運用網路技術輔助插圖:pixiv 如何在繪圖應用程式上運用網頁技術

pixiv 是專為插畫師和插畫愛好者打造的線上社群服務,讓他們透過內容彼此交流。讓使用者發布自己的插圖。截至 2023 年 5 月,他們在全球擁有超過 8400 萬名使用者,並發布超過 1 億件藝術作品。

pixiv Sketch 是 pixiv 提供的服務之一。這項功能可讓使用者使用手指或觸控筆在網站上繪製圖片。它支援多種功能,可繪製精彩的插圖,包括多種筆刷、圖層和桶子繪圖,還可讓使用者直播繪圖過程。

在本案例研究中,我們將探討 pixiv Sketch 如何運用 WebGL、WebAssembly 和 WebRTC 等新網頁平台功能,改善其網頁應用程式的效能和品質。

pixiv Sketch 最初於 2015 年在網頁版和 iOS 版推出。他們的網頁版目標對象主要是電腦使用者,因為插圖社群仍以電腦為主要平台。

以下是 pixiv 選擇開發網頁版而非電腦版的兩大原因:

  • 為 Windows、Mac、Linux 等系統建立應用程式需要耗費大量成本。網頁可在電腦上透過任何瀏覽器存取。
  • 網站是跨平台觸及率最高的管道。網頁版適用於電腦和行動裝置,以及所有作業系統。

科技

pixiv Sketch 提供多種不同的筆刷供使用者選擇。在採用 WebGL 之前,只有一種筆刷,因為 2D 畫布太過受限,無法描繪不同筆刷的複雜紋理,例如鉛筆的粗邊緣,以及根據草圖壓力而變化的寬度和色彩強度。

使用 WebGL 的筆刷類型

不過,採用 WebGL 後,他們就能在筆刷細節中加入更多變化,並將可用的筆刷數量增加至七個。

pixiv 提供七種不同的筆刷,從細到粗、從銳利到模糊、從像素化到平滑等。

使用 2D 畫布內容時,只能繪製具有簡單紋理且寬度平均分配的線條,如下圖所示:

含有簡單紋理的筆觸。

這些線條是透過建立路徑和繪製筆劃所繪製,但 WebGL 會使用點圖元和著色器重現這些線條,如以下程式碼範例所示。

以下範例說明頂點著色器。

precision highp float;

attribute vec2 pos
;
attribute
float thicknessFactor;
attribute
float opacityFactor;

uniform
float pointSize;

varying
float varyingOpacityFactor;
varying
float hardness;

// Calculate hardness from actual point size
float calcHardness(float s) {
 
float h0 = .1 * (s - 1.);
 
float h1 = .01 * (s - 10.) + .6;
 
float h2 = .005 * (s - 30.) + .8;
 
float h3 = .001 * (s - 50.) + .9;
 
float h4 = .0002 * (s - 100.) + .95;
 
return min(h0, min(h1, min(h2, min(h3, h4))));
}

void main() {
 
float actualPointSize = pointSize * thicknessFactor;
  varyingOpacityFactor
= opacityFactor;
  hardness
= calcHardness(actualPointSize);
  gl_Position
= vec4(pos, 0., 1.);
  gl_PointSize
= actualPointSize;
}

以下範例顯示片段著色器的程式碼範例。

precision highp float;

const float strength = .8;
const float exponent = 5.;

uniform vec4 color
;

varying
float hardness;
varying
float varyingOpacityFactor;

float fallOff(const float r) {
   
// w is for width
   
float w = 1. - hardness;
   
if (w < 0.01) {
     
return 1.;
   
} else {
     
return min(1., pow(1. - (r - hardness) / w, exponent));
   
}
}

void main() {
    vec2 texCoord
= (gl_PointCoord - .5) * 2.;
   
float r = length(texCoord);

   
if (r > 1.) {
     discard
;
   
}

   
float brushAlpha = fallOff(r) * varyingOpacityFactor * strength * color.a;

    gl_FragColor
= vec4(color.rgb, brushAlpha);
}

使用點陣圖元後,您就能輕鬆根據繪圖壓力變更線條粗細和陰影,以便繪製下列粗細線條:

筆觸銳利、均勻,且末端細長。

筆觸不清晰,且中間施加較大壓力。

此外,使用點陣圖的實作項目現在可以透過使用獨立的著色器附加紋理,以便以紋理 (例如鉛筆和毛筆) 有效呈現筆刷。

瀏覽器支援觸控筆

數位藝術家非常喜歡使用數位觸控筆,新版瀏覽器支援 PointerEvent API,可讓使用者在裝置上使用觸控筆:使用 PointerEvent.pressure 可測量筆壓,使用 PointerEvent.tiltXPointerEvent.tiltY 可測量筆與裝置的角度。

如要使用點圖元執行筆觸,PointerEvent 必須經過插補並轉換為更精細的事件序列。在 PointerEvent 中,可以以極座標座標的形式取得觸控筆的方向,但 pixiv Sketch 會先將這些資料轉換為代表觸控筆方向的向量,然後再使用。

function getTiltAsVector(event: PointerEvent): [number, number, number] {
 
const u = Math.tan((event.tiltX / 180) * Math.PI);
 
const v = Math.tan((event.tiltY / 180) * Math.PI);
 
const z = Math.sqrt(1 / (u * u + v * v + 1));
 
const x = z * u;
 
const y = z * v;
 
return [x, y, z];
}

function handlePointerDown(event: PointerEvent) {
 
const position = [event.clientX, event.clientY];
 
const pressure = event.pressure;
 
const tilt = getTiltAsVector(event);

  interpolateAndRender
(position, pressure, tilt);
}

多個繪圖圖層

圖層是數位繪圖最獨特的概念之一。讓使用者在不同圖層上繪製不同的插圖,並允許逐層編輯。pixiv Sketch 提供的圖層功能與其他數位繪圖應用程式類似。

傳統上,您可以使用多個 <canvas> 元素搭配 drawImage() 和合成作業,實作圖層。不過,這會造成問題,因為在 2D 畫布內容中,您只能使用 CanvasRenderingContext2D.globalCompositeOperation 合成模式,而這類模式是預先定義的,且大幅限制了可擴充性。開發人員可以使用 WebGL 和著色器,使用 API 未預先定義的合成模式。日後,pixiv Sketch 將使用 WebGL 導入圖層功能,以提升可擴充性和彈性。

以下是圖層組合的程式碼範例:

precision highp float;

uniform sampler2D baseTexture
;
uniform sampler2D blendTexture
;
uniform mediump
float opacity;

varying highp vec2 uv
;

// for normal mode
vec3 blend
(const vec4 baseColor, const vec4 blendColor) {
 
return blendColor.rgb;
}

// for multiply mode
vec3 blend
(const vec4 baseColor, const vec4 blendColor) {
 
return blendColor.rgb * blendColor.rgb;
}

void main()
{
  vec4 blendColor
= texture2D(blendTexture, uv);
  vec4 baseColor
= texture2D(baseTexture, uv);

  blendColor
.a *= opacity;

 
float a1 = baseColor.a * blendColor.a;
 
float a2 = baseColor.a * (1. - blendColor.a);
 
float a3 = (1. - baseColor.a) * blendColor.a;

 
float resultAlpha = a1 + a2 + a3;

 
const float epsilon = 0.001;

 
if (resultAlpha > epsilon) {
    vec3 noAlphaResult
= blend(baseColor, blendColor);
    vec3 resultColor
=
        noAlphaResult
* a1 + baseColor.rgb * a2 + blendColor.rgb * a3;
    gl_FragColor
= vec4(resultColor / resultAlpha, resultAlpha);
 
} else {
    gl_FragColor
= vec4(0);
 
}
}

使用桶子功能繪製大面積

pixiv Sketch iOS 和 Android 應用程式已提供分割區功能,但網頁版並未提供。應用程式版本的值區函式是以 C++ 實作。

由於 C++ 中已有程式碼集,pixiv Sketch 使用了 Emscripten 和 asm.js,將桶函式實作到網頁版本中。

bfsQueue.push(startPoint);

while (!bfsQueue.empty()) {
 
Point point = bfsQueue.front();
  bfsQueue
.pop();
 
/* ... */
  bfsQueue
.push(anotherPoint);
}

使用 asm.js 可啟用高效能解決方案。比較純 JavaScript 與 asm.js 的執行時間,使用 asm.js 的執行時間縮短了 67%。使用 WASM 時,這項功能的效能預期會更佳。

測試詳細資料:

  • 方法:使用值區函式繪製 1180 x 800 像素的區域
  • 測試裝置:MacBook Pro (M1 Max)

執行時間:

  • 純 JavaScript:213.8 毫秒
  • asm.js:70.3 毫秒

使用 Emscripten 和 asm.js,pixiv Sketch 可透過重複使用特定平台應用程式版本的程式碼庫,成功發布分頁功能。

在繪圖時直播

pixiv Sketch 提供透過 pixiv Sketch LIVE 網路應用程式,在繪圖時進行直播的功能。這項功能會使用 WebRTC API,結合從 getUserMedia() 取得的麥克風音訊音軌,以及從 <canvas> 元素擷取的 MediaStream 影像音軌。

const canvasElement = document.querySelector('#DrawCanvas');
const framerate = 24;
const canvasStream = canvasElement.captureStream(framerate);
const videoStreamTrack = canvasStream.getVideoTracks()[0];

const audioStream = await navigator.mediaDevices.getUserMedia({
  video
: false,
  audio
: {},
});
const audioStreamTrack = audioStream.getAudioTracks()[0];

const stream = new MediaStream();
stream
.addTrack(audioStreamTrack.clone());
stream
.addTrack(videoStreamTrack.clone());

結論

您可以利用 WebGL、WebAssembly 和 WebRTC 等新 API 的強大功能,在網頁平台上建立複雜的應用程式,並在任何裝置上擴充應用程式。如要進一步瞭解本個案研究中介紹的技術,請參閱下列連結: