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

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

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

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

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

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

Вот две основные причины, по которым 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);
}

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

Резкий, ровный мазок кистью с тонкими концами.

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

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

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

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

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

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

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

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

Используя 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, вы можете создать сложное приложение на веб-платформе и масштабировать его на любом устройстве. Вы можете узнать больше о технологиях, представленных в этом исследовании, по следующим ссылкам: