pixiv to internetowa usługa społecznościowa dla ilustratorów i miłośników ilustracji, która umożliwia im komunikowanie się ze sobą za pomocą treści. Umożliwia użytkownikom publikowanie własnych ilustracji. Na całym świecie ma ponad 84 miliony użytkowników, a do maja 2023 roku opublikowano w niej ponad 120 milionów dzieł sztuki.
pixiv Sketch to jedna z usług świadczonych przez pixiv. Służy do rysowania prac w witrynie za pomocą palców lub rysików. Obsługuje wiele funkcji do tworzenia niesamowitych ilustracji, w tym różne rodzaje pędzli, warstwy i malowanie za pomocą narzędzia do wypełniania. Umożliwia też transmitowanie na żywo procesu rysowania.
W tym studium przypadku przyjrzymy się, jak pixiv Sketch poprawił wydajność i jakość swojej aplikacji internetowej, korzystając z nowych funkcji platformy internetowej, takich jak WebGL, WebAssembly i WebRTC.
Dlaczego warto stworzyć aplikację do szkicowania w internecie?
pixiv Sketch został po raz pierwszy udostępniony w 2015 roku w wersji na przeglądarki i iOS. Główną grupą docelową wersji internetowej były komputery, które nadal są najważniejszą platformą używaną przez społeczność ilustratorów.
Oto 2 najważniejsze powody, dla których pixiv zdecydował się na opracowanie wersji internetowej zamiast aplikacji na komputery:
- Tworzenie aplikacji na Windowsa, Maca, Linuksa i inne platformy jest bardzo kosztowne. Sieć dociera do każdej przeglądarki na komputerze.
- Internet ma największy zasięg na różnych platformach. Internet jest dostępny na komputerach i urządzeniach mobilnych oraz na każdym systemie operacyjnym.
Technologia
pixiv Sketch ma wiele różnych pędzli do wyboru. Przed wprowadzeniem WebGL dostępny był tylko jeden rodzaj pędzla, ponieważ płótno 2D było zbyt ograniczone, aby przedstawić złożoną teksturę różnych pędzli, np. szorstkie krawędzie ołówka oraz różną szerokość i intensywność koloru, która zmienia się w zależności od nacisku podczas szkicowania.
Kreatywne typy pędzli korzystające z WebGL
Dzięki WebGL mogli jednak dodać więcej odmian szczegółów pędzla i zwiększyć liczbę dostępnych pędzli do siedmiu.

Korzystając z kontekstu płótna 2D, można było rysować tylko linie o prostej teksturze z równomiernie rozłożoną szerokością, jak na tym zrzucie ekranu:

Te linie zostały narysowane przez utworzenie ścieżek i narysowanie kresek, ale WebGL odtwarza je za pomocą sprite’ów punktowych i shaderów, co pokazują te przykłady kodu:
Poniższy przykład przedstawia shader wierzchołków.
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;
}
Poniższy przykład zawiera przykładowy kod shadera fragmentów.
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);
}
Użycie punktowych sprite’ów ułatwia zmianę grubości i cieniowania w odpowiedzi na nacisk rysika, co pozwala na tworzenie mocnych i słabych linii, takich jak te:


Dodatkowo implementacje korzystające z punktowych sprite’ów mogą teraz dołączać tekstury za pomocą osobnego shadera, co umożliwia wydajne przedstawianie pędzli z teksturami, takimi jak ołówek czy flamaster.
Obsługa rysika w przeglądarce
Korzystanie z rysika cyfrowego stało się niezwykle popularne wśród artystów cyfrowych. Nowoczesne przeglądarki obsługują PointerEvent API, które umożliwia użytkownikom korzystanie z rysika na urządzeniu: użyj PointerEvent.pressure, aby zmierzyć nacisk pióra, oraz PointerEvent.tiltX i PointerEvent.tiltY, aby zmierzyć kąt nachylenia pióra względem urządzenia.
Aby wykonywać pociągnięcia pędzlem za pomocą punktowego sprite’a, należy interpolować i przekształcać PointerEvent w bardziej szczegółową sekwencję zdarzeń. W przypadku zdarzenia PointerEvent orientację rysika można uzyskać w postaci współrzędnych biegunowych, ale pixiv Sketch przekształca je w wektor reprezentujący orientację rysika przed użyciem.
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);
}
Wiele warstw rysunkowych
Warstwy to jedno z najbardziej unikalnych pojęć w rysunku cyfrowym. Umożliwiają one użytkownikom rysowanie różnych elementów ilustracji na sobie i edytowanie ich warstwa po warstwie. pixiv Sketch udostępnia funkcje warstw podobne do innych aplikacji do rysowania cyfrowego.
Tradycyjnie warstwy można implementować za pomocą kilku elementów <canvas> z atrybutem drawImage() i operacji kompozycji. Jest to jednak problematyczne, ponieważ w przypadku kontekstu płótna 2D nie ma innego wyboru niż użycie trybu kompozycji CanvasRenderingContext2D.globalCompositeOperation, który jest predefiniowany i w dużym stopniu ogranicza skalowalność. Korzystając z WebGL i pisząc shader, deweloperzy mogą używać trybów kompozycji, które nie są predefiniowane przez interfejs API. W przyszłości funkcja warstw w pixiv Sketch będzie korzystać z WebGL, co zapewni większą skalowalność i elastyczność.
Oto przykładowy kod kompozycji warstw:
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);
}
}
Malowanie dużych obszarów za pomocą funkcji wiadra
Aplikacje pixiv Sketch na iOS i Androida już udostępniały funkcję wiadra, ale nie było jej w wersji internetowej. Wersja funkcji zasobnika w aplikacji została zaimplementowana w C++.
Ponieważ baza kodu była już dostępna w C++, pixiv Sketch używał Emscripten i asm.js do implementowania funkcji wiadra w wersji internetowej.
bfsQueue.push(startPoint);
while (!bfsQueue.empty()) {
Point point = bfsQueue.front();
bfsQueue.pop();
/* ... */
bfsQueue.push(anotherPoint);
}
Użycie asm.js umożliwiło stworzenie wydajnego rozwiązania. Porównując czas wykonania czystego JavaScriptu z czasem wykonania asm.js, można zauważyć, że w przypadku asm.js jest on krótszy o 67%. W przypadku WASM powinno być jeszcze lepiej.
Szczegóły testu:
- Jak to zrobić: wypełnij obszar o wymiarach 1180 x 800 pikseli za pomocą funkcji wypełniania.
- Urządzenie testowe: MacBook Pro (M1 Max)
Czas wykonania:
- Czysty JavaScript: 213,8 ms
- asm.js: 70,3 ms
Dzięki Emscripten i asm.js udało się z powodzeniem wprowadzić funkcję koszyka w pixiv Sketch, wykorzystując ponownie bazę kodu z wersji aplikacji na konkretną platformę.
Transmisja na żywo podczas rysowania
pixiv Sketch oferuje funkcję transmisji na żywo podczas rysowania za pomocą aplikacji internetowej pixiv Sketch LIVE. Korzysta ona z interfejsu WebRTC API, łącząc ścieżkę dźwiękową z mikrofonu uzyskaną z getUserMedia() i ścieżkę wideo pobraną z elementu <canvas>.MediaStream
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());
Podsumowanie
Dzięki nowym interfejsom API, takim jak WebGL, WebAssembly i WebRTC, możesz tworzyć złożone aplikacje na platformie internetowej i skalować je na dowolnym urządzeniu. Więcej informacji o technologiach przedstawionych w tym studium przypadku znajdziesz pod tymi linkami:
- WebGL
- Sprawdź też WebGPU, następcę WebGL.
- WebAssembly
- WebRTC
- Oryginalny artykuł w języku japońskim