Sức mạnh của web dành cho các hoạ sĩ minh hoạ: Cách pixiv sử dụng công nghệ web cho ứng dụng vẽ của họ

pixiv là một dịch vụ cộng đồng trực tuyến dành cho các nhà minh hoạ và những người đam mê minh hoạ để giao tiếp với nhau thông qua nội dung của họ. Ứng dụng này cho phép mọi người đăng hình minh hoạ của riêng họ. Ứng dụng này có hơn 84 triệu người dùng trên toàn cầu và hơn 120 triệu tác phẩm nghệ thuật được đăng tính đến tháng 5 năm 2023.

pixiv Sketch là một trong những dịch vụ do pixiv cung cấp. Công cụ này dùng để vẽ hình minh hoạ trên trang web bằng ngón tay hoặc bút cảm ứng. Ứng dụng này hỗ trợ nhiều tính năng để vẽ các hình minh hoạ tuyệt vời, bao gồm nhiều loại bút vẽ, lớp và tính năng tô bằng bộ chứa, đồng thời cho phép mọi người phát trực tiếp quá trình vẽ.

Trong nghiên cứu điển hình này, chúng ta sẽ xem xét cách pixiv Sketch cải thiện hiệu suất và chất lượng của ứng dụng web bằng cách sử dụng một số tính năng mới của nền tảng web như WebGL, WebAssembly và WebRTC.

Tại sao nên phát triển ứng dụng phác thảo trên web?

pixiv Sketch được phát hành lần đầu trên web và iOS vào năm 2015. Đối tượng mục tiêu của họ cho phiên bản web chủ yếu là máy tính, vẫn là nền tảng chính mà cộng đồng minh hoạ sử dụng.

Sau đây là hai lý do hàng đầu khiến pixiv chọn phát triển phiên bản web thay vì ứng dụng dành cho máy tính:

  • Việc tạo ứng dụng cho Windows, Mac, Linux và các nền tảng khác sẽ tốn kém rất nhiều. Trang web truy cập vào mọi trình duyệt trên máy tính.
  • Web có phạm vi tiếp cận tốt nhất trên các nền tảng. Trang web này có trên máy tính và thiết bị di động, cũng như trên mọi hệ điều hành.

Công nghệ

pixiv Sketch có một số bút vẽ khác nhau để người dùng lựa chọn. Trước khi sử dụng WebGL, chỉ có một loại bút vẽ vì canvas 2D quá hạn chế để mô tả kết cấu phức tạp của các bút vẽ khác nhau, chẳng hạn như các cạnh thô của bút chì và độ rộng cũng như cường độ màu khác nhau thay đổi theo áp lực vẽ phác thảo.

Các loại bút vẽ sáng tạo sử dụng WebGL

Tuy nhiên, nhờ sử dụng WebGL, họ có thể thêm nhiều loại chi tiết hơn vào bút vẽ và tăng số lượng bút vẽ lên 7.

Có 7 loại bút vẽ khác nhau trong pixiv, từ mịn đến thô, sắc nét đến không sắc nét, pixel đến mượt, v.v.

Khi sử dụng ngữ cảnh canvas 2D, bạn chỉ có thể vẽ các đường có hoạ tiết đơn giản với chiều rộng được phân phối đều, như ảnh chụp màn hình sau:

Nét vẽ bằng bút vẽ có hoạ tiết đơn giản.

Các đường này được vẽ bằng cách tạo đường dẫn và vẽ nét vẽ, nhưng WebGL tái tạo điều này bằng cách sử dụng các điểm ảnh và chương trình đổ bóng, như minh hoạ trong các mã mẫu sau

Ví dụ sau đây minh hoạ một chương trình đổ bóng đỉnh.

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;
}

Ví dụ sau đây cho thấy mã mẫu cho chương trình đổ bóng mảnh.

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);
}

Việc sử dụng các sprite điểm giúp bạn dễ dàng thay đổi độ dày và độ bóng để phản hồi áp lực vẽ, cho phép thể hiện các đường mạnh và yếu sau đây, chẳng hạn như:

Nét vẽ bằng bút vẽ sắc nét, đều đặn với đầu nét mỏng.

Nét vẽ bằng bút không sắc nét với áp lực được áp dụng nhiều hơn ở giữa.

Ngoài ra, các phương thức triển khai sử dụng sprite điểm hiện có thể đính kèm hoạ tiết bằng cách sử dụng chương trình đổ bóng riêng biệt, cho phép thể hiện hiệu quả các bút vẽ có hoạ tiết như bút chì và bút dạ.

Hỗ trợ bút cảm ứng trên trình duyệt

Việc sử dụng bút cảm ứng kỹ thuật số đã trở nên cực kỳ phổ biến đối với các nghệ sĩ kỹ thuật số. Các trình duyệt hiện đại hỗ trợ PointerEvent API cho phép người dùng sử dụng bút cảm ứng trên thiết bị của họ: Sử dụng PointerEvent.pressure để đo áp lực của bút và sử dụng PointerEvent.tiltX, PointerEvent.tiltY để đo góc của bút với thiết bị.

Để thực hiện các nét vẽ bằng sprite điểm, bạn phải nội suy và chuyển đổi PointerEvent thành một trình tự sự kiện chi tiết hơn. Trong PointerEvent, bạn có thể lấy hướng của bút cảm ứng ở dạng tọa độ cực, nhưng pixiv Sketch sẽ chuyển đổi các tọa độ này thành một vectơ đại diện cho hướng của bút cảm ứng trước khi sử dụng.

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);
}

Nhiều lớp vẽ

Lớp là một trong những khái niệm độc đáo nhất trong kỹ thuật vẽ kỹ thuật số. Các lớp này cho phép người dùng vẽ nhiều hình minh hoạ chồng lên nhau và chỉnh sửa từng lớp. pixiv Sketch cung cấp các chức năng lớp giống như các ứng dụng vẽ kỹ thuật số khác.

Theo thông lệ, bạn có thể triển khai các lớp bằng cách sử dụng một số phần tử <canvas> với drawImage() và các thao tác kết hợp. Tuy nhiên, điều này gây ra vấn đề vì với ngữ cảnh canvas 2D, bạn không có lựa chọn nào khác ngoài việc sử dụng chế độ kết hợp CanvasRenderingContext2D.globalCompositeOperation. Chế độ này được xác định trước và phần lớn giới hạn khả năng mở rộng. Bằng cách sử dụng WebGL và viết chương trình đổ bóng, nhà phát triển có thể sử dụng các chế độ kết hợp không được API xác định trước. Trong tương lai, pixiv Sketch sẽ triển khai tính năng lớp bằng cách sử dụng WebGL để tăng khả năng mở rộng và linh hoạt.

Dưới đây là mã mẫu cho thành phần kết hợp lớp:

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);
 
}
}

Vẽ vùng rộng lớn bằng hàm bộ chứa

Ứng dụng pixiv Sketch dành cho iOS và Android đã cung cấp tính năng nhóm, nhưng phiên bản web thì không. Phiên bản ứng dụng của hàm bộ chứa được triển khai trong C++.

Với cơ sở mã có sẵn trong C++, pixiv Sketch đã sử dụng Emscripten và asm.js để triển khai hàm bộ chứa vào phiên bản web.

bfsQueue.push(startPoint);

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

Việc sử dụng asm.js đã cho phép một giải pháp hiệu quả. Khi so sánh thời gian thực thi của JavaScript thuần tuý với asm.js, thời gian thực thi bằng asm.js sẽ ngắn hơn 67%. Điều này dự kiến sẽ còn tốt hơn khi sử dụng WASM.

Thông tin chi tiết về kiểm thử:

  • Cách thực hiện: Vẽ vùng 1180x800px bằng hàm nhóm
  • Thiết bị thử nghiệm: MacBook Pro (M1 Max)

Thời gian thực thi:

  • JavaScript thuần tuý: 213,8 mili giây
  • asm.js: 70,3 mili giây

Bằng cách sử dụng Emscripten và asm.js, pixiv Sketch đã có thể phát hành thành công tính năng nhóm bằng cách sử dụng lại cơ sở mã từ phiên bản ứng dụng dành riêng cho nền tảng.

Phát trực tiếp trong khi vẽ

pixiv Sketch cung cấp tính năng phát trực tiếp trong khi vẽ thông qua ứng dụng web pixiv Sketch LIVE. Tính năng này sử dụng API WebRTC, kết hợp kênh âm thanh micrô thu được từ getUserMedia() và kênh video MediaStream được truy xuất từ phần tử <canvas>.

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());

Kết luận

Với sức mạnh của các API mới như WebGL, WebAssembly và WebRTC, bạn có thể tạo một ứng dụng phức tạp trên nền tảng web và mở rộng ứng dụng đó trên mọi thiết bị. Bạn có thể tìm hiểu thêm về các công nghệ được giới thiệu trong nghiên cứu điển hình này tại các đường liên kết sau: