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à những người thích 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. Tính năng này được dùng để vẽ hình minh hoạ trên trang web bằng cách sử dụng ngón tay hoặc bút cảm ứng. Công cụ này hỗ trợ nhiều tính năng để vẽ các hình minh hoạ thú vị, bao gồm nhiều loại cọ vẽ, lớp phủ và sơn xô, đồng thời cho phép mọi người truyền trực tiếp quá trình vẽ của họ.

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

Tại sao bạn 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à trên 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, đây vẫn là nền tảng chính được cộng đồng minh hoạ sử dụng nhất.

Dưới đây là hai lý do hàng đầu mà 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.v. rất tốn kém. Web truy cập vào bất kỳ trình duyệt nào trên máy tính để bàn.
  • Web có phạm vi tiếp cận lớn nhất trên các nền tảng. Web có sẵn trên máy tính để bàn 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 cọ vẽ cho người dùng lựa chọn. Trước khi sử dụng WebGL, chỉ có một loại cọ vẽ vì canvas 2D quá giới hạn để mô tả kết cấu phức tạp của nhiều loại cọ, như các cạnh thô của bút chì và chiều rộng cũng như cường độ màu khác nhau thay đổi theo áp lực bản 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 kiểu chi tiết về cọ vẽ và tăng số lượng cọ vẽ có sẵn lên 7.

Bảy cọ vẽ khác nhau trong pixiv từ mịn đến thô, sắc nét đến không sắc nét, tạo điểm ảnh đến mượt mà, 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 được phân phối đồng đều như ảnh chụp màn hình sau:

Nét 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à nét vẽ, nhưng WebGL tái tạo quá trình này bằng cách sử dụng sprite điểm và chương trình đổ bóng, được hiển thị trong mã mẫu sau đây

Ví dụ sau đây minh hoạ 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 của 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 sprite điểm giúp bạn dễ dàng thay đổi độ dày và tô bóng để đáp ứng áp lực vẽ, cho phép biểu thị các đường mạnh và yếu sau đây, như sau:

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

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

Ngoài ra, giờ đây, các phương thức triển khai bằng hình nổi nhỏ điểm 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ẽ bằng các hoạ tiết như bút chì và bút cảm ứng.

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ợ API PointerEvent cho phép người dùng sử dụng bút cảm ứng trên thiết bị của họ: 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ẽ với một ảnh khối điểm, 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 chuyển đổi hướng của bút cảm ứng 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 nghệ thuật vẽ kỹ thuật số. Các API này cho phép người dùng vẽ nhiều mảnh hình minh hoạ chồng lên nhau và cho phép 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.

Thông thường, 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 khó khăn vì với ngữ cảnh canvas 2D, 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à giới hạn phần lớn khả năng có thể mở rộng. Bằng cách sử dụng WebGL và viết chương trình đổ bóng, điều này cho phép các nhà phát triển 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 có thể mở rộng và tính linh hoạt.

Dưới đây là mã mẫu để 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);
  }
}

Tranh vẽ trên diện tích lớn bằng hàm xô

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

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

bfsQueue.push(startPoint);

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

Sử dụng asm.js đã kích hoạt một giải pháp hiệu suất. 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 được rút ngắn 67%. Dự kiến việc này sẽ tốt hơn nữa khi bạn sử dụng WASM.

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

  • Cách thực hiện: Sơn 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 hoàn toàn: 213,8 mili giây
  • asm.js: 70,3 mili giây

Nhờ sử dụng Emscripten và asm.js, pixiv Sketch đã phát hành thành công tính năng bộ chứa 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. Ứng dụng này sử dụng API WebRTC, 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ô 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: