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, им удалось добавить больше вариантов детализации кисти и увеличить количество доступных кистей до семи.

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