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