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 hoạ sĩ minh hoạ và người yêu thích tranh minh hoạ để giao tiếp với nhau thông qua nội dung của họ. Tính năng này cho phép mọi người đăng hình minh hoạ của riêng mình. Họ 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 được dùng để vẽ tác phẩm nghệ thuật 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ẽ minh hoạ tuyệt đẹp, bao gồm nhiều loại cọ vẽ, lớp và tính năng tô màu theo vùng chọn, đồng thời cho phép mọi người phát trực tiếp quá trình vẽ của mình.

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 một ứng dụng phác thảo trên web?

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

Sau đây là 2 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 hệ điều hành khác rất tốn kém. Web có thể 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 rộng nhất trên các nền tảng. Web 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ó nhiều loại bút vẽ để người dùng lựa chọn. Trước khi áp 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 loại bút vẽ, 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 phác thảo.

Các loại cọ vẽ bằng WebGL

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

7 loại cọ trong pixiv, từ cọ mịn đến cọ thô, cọ sắc nét đến cọ không sắc nét, cọ có hiệu ứng pixel đến cọ mượt, v.v.

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

Nét vẽ cọ 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, nhưng WebGL tái tạo đường này bằng cách sử dụng các sprite điểm và chương trình đổ bóng, như minh hoạ trong các mẫu mã 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 một 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 theo áp lực vẽ, cho phép thể hiện các đường nét đậm và nhạt như sau:

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

Nét vẽ không sắc nét với áp lực lớn hơn ở giữa.

Ngoài ra, các hoạt động 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 một chương trình đổ bóng riêng biệt, cho phép biểu diễn hiệu quả các cọ 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 so với thiết bị.

Để thực hiện các nét vẽ bằng một điểm sprite, PointerEvent phải được nội suy và chuyển đổi thành một chuỗi sự kiện chi tiết hơn. Trong PointerEvent, hướng của bút cảm ứng có thể được lấy dưới dạng toạ độ cực, nhưng pixiv Sketch sẽ chuyển đổi các toạ độ này thành một vectơ biểu thị 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 vẽ kỹ thuật số. Các lớp này cho phép người dùng vẽ nhiều phần 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 về lớp tương tự như các ứng dụng vẽ kỹ thuật số khác.

Theo quy ước, 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 bối 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à hạn chế đáng kể khả năng mở rộng. Bằng cách sử dụng WebGL và viết chương trình đổ bóng, các nhà phát triển có thể sử dụng các chế độ kết hợp mà API không xác định trước. Trong tương lai, pixiv Sketch sẽ triển khai tính năng lớp bằng WebGL để có khả năng mở rộng và tính linh hoạt cao hơn.

Sau đây là mã mẫu cho thành phần 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ẽ một vùng lớn bằng chức năng đổ màu

Ứ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ì chưa. Phiên bản ứng dụng của hàm nhóm được triển khai bằng C++.

Với cơ sở mã đã có sẵn trong C++, pixiv Sketch đã sử dụng Emscripten và asm.js để triển khai chức năng nhóm 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 đã mang đến 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 khi dùng asm.js giảm 67%. Điều này dự kiến sẽ còn tốt hơn nữa khi sử dụng WASM.

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

  • Cách thực hiện: Dùng chức năng đổ màu để tô vùng 1180x800px
  • 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 đã phát hành thành công tính năng bucket 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 WebRTC API, kết hợp bản âm thanh micrô thu được từ getUserMedia() và bản 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 quy mô ứ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: