pixiv ist ein Online-Community-Dienst für Illustratoren und Illustrationsbegeisterte, die über ihre Inhalte miteinander kommunizieren. Nutzer können eigene Illustrationen posten. Das Unternehmen hat weltweit über 84 Millionen Nutzer und im Mai 2023 wurden mehr als 120 Millionen Kunstwerke gepostet.
pixiv Sketch ist einer der von pixiv angebotenen Dienste. Damit können Nutzer mit Fingern oder Stiften Kunstwerke auf der Website erstellen. Die App bietet eine Vielzahl von Funktionen zum Erstellen beeindruckender Illustrationen, darunter zahlreiche Pinseltypen, Ebenen und die Möglichkeit, Flächen zu füllen. Außerdem können Nutzer ihren Zeichenprozess live streamen.
In dieser Fallstudie sehen wir uns an, wie pixiv Sketch die Leistung und Qualität seiner Web-App durch die Verwendung einiger neuer Webplattformfunktionen wie WebGL, WebAssembly und WebRTC verbessert hat.
Warum eine Skizzen-App im Web entwickeln?
pixiv Sketch wurde 2015 erstmals im Web und auf iOS veröffentlicht. Die Zielgruppe für die Webversion war hauptsächlich der Desktop, der immer noch die wichtigste Plattform für die Illustrations-Community ist.
Hier sind die zwei wichtigsten Gründe, aus denen sich pixiv für die Entwicklung einer Webversion anstelle einer Desktop-App entschieden hat:
- Die Entwicklung von Apps für Windows, Mac, Linux und andere Betriebssysteme ist sehr kostspielig. Das Web ist über jeden Browser auf dem Desktop erreichbar.
- Das Web bietet die größte Reichweite auf allen Plattformen. Das Web ist auf Computern und Mobilgeräten sowie auf allen Betriebssystemen verfügbar.
Technologie
pixiv Sketch bietet Nutzern eine Reihe verschiedener Pinsel zur Auswahl. Vor der Einführung von WebGL gab es nur einen Pinseltyp, da die 2D-Canvas zu begrenzt war, um die komplexe Textur verschiedener Pinsel darzustellen, z. B. die groben Kanten eines Bleistifts und die unterschiedliche Breite und Farbintensität, die sich je nach Druck beim Skizzieren ändert.
Creative-Typen von Pinseln mit WebGL
Durch die Einführung von WebGL konnten sie jedoch mehr Pinseldetails hinzufügen und die Anzahl der verfügbaren Pinsel auf sieben erhöhen.
Mit dem 2D-Canvas-Kontext war es nur möglich, Linien mit einer einfachen Textur mit gleichmäßig verteilter Breite zu zeichnen, wie im folgenden Screenshot zu sehen ist:
Diese Linien wurden durch Erstellen von Pfaden und Zeichnen von Strichen gezeichnet. WebGL reproduziert dies jedoch mithilfe von Punkt-Sprites und Shadern, wie in den folgenden Codebeispielen gezeigt.
Das folgende Beispiel zeigt einen Vertex-Shader.
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;
}
Das folgende Beispiel zeigt Beispielcode für einen Fragment-Shader.
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);
}
Durch die Verwendung von Punkt-Sprites lässt sich die Stärke und Schattierung ganz einfach an den Zeichenstift-Druck anpassen. So können Sie starke und schwache Linien wie diese zeichnen:
Außerdem können bei Implementierungen mit Punkt-Sprites jetzt Texturen über einen separaten Shader angehängt werden. So lassen sich Pinsel mit Texturen wie Bleistift und Filzstift effizient darstellen.
Unterstützung von Eingabestiften im Browser
Die Verwendung eines digitalen Stifts ist bei digitalen Künstlern sehr beliebt. Moderne Browser unterstützen die PointerEvent API, mit der Nutzer einen Eingabestift auf ihrem Gerät verwenden können: Mit PointerEvent.pressure
lässt sich der Stiftdruck messen und mit PointerEvent.tiltX
und PointerEvent.tiltY
der Winkel des Stifts zum Gerät.
Damit Pinselstriche mit einem Punkt-Sprite ausgeführt werden können, muss die PointerEvent
interpoliert und in eine detailliertere Ereignissequenz umgewandelt werden. In PointerEvent kann die Ausrichtung des Stifts in Form von Polarkoordinaten abgerufen werden. pixiv Sketch konvertiert diese jedoch in einen Vektor, der die Ausrichtung des Stifts darstellt, bevor sie verwendet werden.
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);
}
Mehrere Zeichenebenen
Ebenen sind eines der einzigartigsten Konzepte beim digitalen Zeichnen. Damit können Nutzer verschiedene Teile einer Illustration übereinander zeichnen und Änderungen schichtweise vornehmen. pixiv Sketch bietet Layer-Funktionen, die denen anderer digitaler Zeichen-Apps sehr ähnlich sind.
Konventionell ist es möglich, Ebenen mithilfe mehrerer <canvas>
-Elemente mit drawImage()
und Compositing-Vorgängen zu implementieren. Das ist jedoch problematisch, da im 2D-Canvas-Kontext nur der Kompositionsmodus CanvasRenderingContext2D.globalCompositeOperation
verwendet werden kann. Dieser ist vordefiniert und schränkt die Skalierbarkeit stark ein. Durch die Verwendung von WebGL und das Schreiben des Shaders können Entwickler Kompositionsmodi verwenden, die nicht von der API vordefiniert sind. In Zukunft wird die Ebenenfunktion in pixiv Sketch mit WebGL implementiert, um eine höhere Skalierbarkeit und Flexibilität zu erreichen.
Hier ist der Beispielcode für die Ebenenzusammenstellung:
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);
}
}
Große Bereiche mit der Eimerfunktion malen
Die Bucket-Funktion war bereits in den iOS- und Android-Apps von pixiv Sketch verfügbar, nicht aber in der Webversion. Die App-Version der Bucket-Funktion wurde in C++ implementiert.
Da der Code bereits in C++ verfügbar war, nutzte pixiv Sketch Emscripten und asm.js, um die Bucket-Funktion in die Webversion zu implementieren.
bfsQueue.push(startPoint);
while (!bfsQueue.empty()) {
Point point = bfsQueue.front();
bfsQueue.pop();
/* ... */
bfsQueue.push(anotherPoint);
}
Durch die Verwendung von asm.js konnte eine leistungsstarke Lösung entwickelt werden. Wenn man die Ausführungszeit von reinem JavaScript mit der von asm.js vergleicht, wird die Ausführungszeit mit asm.js um 67 % verkürzt. Bei Verwendung von WASM wird dies voraussichtlich noch besser.
Test details:
- Vorgehensweise:Malen Sie einen Bereich von 1.180 × 800 Pixeln mit der Bucket-Funktion.
- Testgerät:MacBook Pro (M1 Max)
Ausführungszeit:
- Reines JavaScript:213,8 ms
- asm.js::70,3 ms
Mithilfe von Emscripten und asm.js konnte pixiv Sketch die Bucket-Funktion erfolgreich einführen, indem der Code aus der plattformspezifischen App-Version wiederverwendet wurde.
Livestreaming beim Zeichnen
pixiv Sketch bietet die Möglichkeit, während des Zeichnens zu streamen. Dazu wird die Web-App pixiv Sketch LIVE verwendet. Diese nutzt die WebRTC API und kombiniert den Mikrofon-Audio-Track, der von getUserMedia()
abgerufen wird, mit dem MediaStream
-Videotrack, der vom <canvas>
-Element abgerufen wird.
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());
Zusammenfassung
Mit neuen APIs wie WebGL, WebAssembly und WebRTC können Sie komplexe Apps auf der Webplattform erstellen und auf beliebigen Geräten skalieren. Weitere Informationen zu den in dieser Fallstudie vorgestellten Technologien finden Sie unter den folgenden Links:
- WebGL
- WebGPU ist der Nachfolger von WebGL.
- WebAssembly
- WebRTC
- Originalartikel auf Japanisch