Сила Интернета для иллюстраторов: как pixiv использует веб-технологии в своем приложении для рисования

Pixiv — это онлайн-сообщество для иллюстраторов и любителей иллюстрации, позволяющее им общаться друг с другом посредством своего контента. Здесь люди могут публиковать свои собственные иллюстрации. У платформы более 84 миллионов пользователей по всему миру, и по состоянию на май 2023 года было опубликовано более 120 миллионов работ.

Sketch — один из сервисов, предоставляемых Pixiv. Он используется для создания рисунков на сайте с помощью пальцев или стилуса. Сервис поддерживает множество функций для создания потрясающих иллюстраций, включая многочисленные типы кистей, слои и технику заливки, а также позволяет пользователям транслировать процесс рисования в прямом эфире.

В этом примере мы рассмотрим, как pixiv Sketch улучшил производительность и качество своего веб-приложения, используя новые возможности веб-платформы, такие как WebGL, WebAssembly и WebRTC.

Зачем разрабатывать веб-приложение для рисования?

Приложение pixiv Sketch впервые было выпущено в веб-версии и на iOS в 2015 году. Целевой аудиторией веб-версии были в основном пользователи настольных компьютеров, которые до сих пор остаются наиболее распространенной платформой среди сообщества иллюстраторов.

Вот две главные причины, по которым Pixiv предпочитает разрабатывать веб-версию вместо настольного приложения:

  • Создание приложений для Windows, Mac, Linux и других операционных систем обходится очень дорого. Веб-доступен в любом браузере на настольном компьютере.
  • Интернет обладает наилучшим охватом на всех платформах. Он доступен на настольных компьютерах и мобильных устройствах, а также на всех операционных системах.

Технологии

В Sketch на Pixiv доступно множество различных кистей на выбор. До внедрения 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);
}

Использование точечных спрайтов позволяет легко изменять толщину и затенение в зависимости от силы нажатия при рисовании, что дает возможность создавать, например, такие четкие и четкие линии:

Четкие, ровные мазки кисти с тонкими концами.

Нерезкие мазки кисти с более сильным нажимом в средней части.

Кроме того, в реализациях, использующих точечные спрайты, теперь можно добавлять текстуры с помощью отдельного шейдера, что позволяет эффективно отображать кисти с текстурами, такими как карандаш и фломастер.

Поддержка стилуса в браузере

Использование цифрового стилуса стало чрезвычайно популярным среди цифровых художников. Современные браузеры поддерживают API PointerEvent , который позволяет пользователям использовать стилус на своем устройстве: используйте PointerEvent.pressure для измерения силы нажатия пера, а PointerEvent.tiltX и PointerEvent.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 для реализации функции bucket в веб-версии.

bfsQueue.push(startPoint);

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

Использование asm.js позволило создать высокопроизводительное решение. Сравнивая время выполнения чистого JavaScript и asm.js , можно отметить, что время выполнения с использованием asm.js сокращается на 67%. Ожидается, что при использовании WASM этот показатель будет еще лучше.

Подробности теста:

  • Как это сделать: Закрасьте область размером 1180x800 пикселей с помощью функции «Заливка».
  • Тестируемое устройство: MacBook Pro (M1 Max)

Время выполнения:

  • Чистый JavaScript: 213,8 ​​мс
  • asm.js: 70.3ms

Используя Emscripten и asm.js , pixiv Sketch смог успешно реализовать функцию «ведра», повторно используя код из версии приложения, предназначенной для конкретной платформы.

Прямая трансляция во время рисования

В pixiv Sketch есть функция прямой трансляции процесса рисования через веб-приложение pixiv Sketch LIVE. Для этого используется API WebRTC, объединяющий аудиодорожку с микрофона, полученную с помощью getUserMedia() , и видеодорожку MediaStream , полученную из элемента <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());

Выводы

Благодаря возможностям новых API, таких как WebGL, WebAssembly и WebRTC, вы можете создать сложное приложение на веб-платформе и масштабировать его на любом устройстве. Подробнее о технологиях, представленных в этом примере, вы можете узнать по следующим ссылкам: