Das Potenzial des Webs für Illustratoren: So nutzt Pixiv Webtechnologien für seine Zeichen-App

pixiv ist ein Online-Communitydienst für Illustratoren und Illustrationsbegeisterte, um über ihre Inhalte miteinander zu kommunizieren. Nutzer können dort eigene Illustrationen posten. Die Plattform hat weltweit über 84 Millionen Nutzer und es wurden bis Mai 2023 mehr als 120 Millionen Kunstwerke gepostet.

pixiv Sketch ist einer der Dienste von pixiv. Sie wird verwendet, um mit Fingern oder Eingabestiften Artworks auf der Website zu zeichnen. Es unterstützt eine Vielzahl von Funktionen zum Zeichnen großartiger Illustrationen, darunter verschiedene Arten von Pinseln, Ebenen und Eimerbemalung. 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 Webanwendung mithilfe neuer Webplattformfunktionen wie WebGL, WebAssembly und WebRTC verbessert hat.

Warum sollte man eine Zeichen-App im Web entwickeln?

pixiv Sketch wurde 2015 erstmals im Web und auf iOS-Geräten veröffentlicht. Die Zielgruppe für die Webversion bestand hauptsächlich aus Desktop-Nutzern, was nach wie vor die wichtigste Plattform für die Illustrations-Community ist.

Hier sind die beiden Hauptgründe, warum sich pixiv für die Entwicklung einer Webversion anstelle einer Desktop-App entschieden hat:

  • Es ist sehr teuer, Apps für Windows, Mac, Linux und andere Betriebssysteme zu entwickeln. Das Web ist für alle Browser auf dem Computer zugänglich.
  • Das Web hat die beste plattformübergreifende Reichweite. Das Web ist auf Computern und Mobilgeräten sowie auf allen Betriebssystemen verfügbar.

Technologie

In pixiv Sketch stehen Nutzern verschiedene Pinsel zur Auswahl. Vor der Einführung von WebGL gab es nur einen Pinseltyp, da die 2D-Leinwand zu eingeschränkt 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.

Kreative Pinseltypen mit WebGL

Durch die Verwendung von WebGL konnten sie jedoch mehr Vielfalt in die Pinseldetails einbringen und die Anzahl der verfügbaren Pinsel auf sieben erhöhen.

Die sieben verschiedenen Pinsel in pixiv reichen von fein bis grob, scharf bis unscharf, pixelig bis glatt usw.

Mit dem 2D-Canvas-Kontext konnten nur Linien mit einer einfachen Textur mit gleichmäßig verteilter Breite gezeichnet werden, wie im folgenden Screenshot:

Pinselstrich mit einfacher Textur.

Diese Linien wurden durch Erstellen von Pfaden und Zeichnen von Strichen gezeichnet. In WebGL werden sie jedoch mit Punkt-Sprites und Shadern reproduziert, wie in den folgenden Codebeispielen gezeigt.

Im folgenden Beispiel wird ein Vertex-Shader veranschaulicht.

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);
}

Mit Punkt-Sprites lässt sich die Dicke und Schattierung ganz einfach in Abhängigkeit vom Zeichendruck variieren. So können starke und schwache Linien wie diese dargestellt werden:

Scharfe, gleichmäßige Pinselstriche mit dünnen Enden.

Unscharfer Pinselstrich mit stärkerem Druck in der Mitte.

Außerdem können bei Implementierungen mit Punkt-Sprites jetzt Texturen mithilfe eines separaten Shaders angehängt werden. So lassen sich Pinsel mit Texturen wie Bleistift und Filzstift effizient darstellen.

Eingabestift-Unterstützung im Browser

Die Verwendung eines digitalen Eingabestifts ist bei digitalen Künstlern immer beliebter geworden. Moderne Browser unterstützen die PointerEvent API, mit der Nutzer einen Eingabestift auf ihrem Gerät verwenden können: Mit PointerEvent.pressure wird der Stiftdruck gemessen 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 PointerEvent interpoliert und in eine detailliertere Ereignissequenz umgewandelt werden. In PointerEvent kann die Ausrichtung des Eingabestifts in Form von Polarkoordinaten ermittelt werden. Vor der Verwendung wandelt pixiv Sketch sie in einen Vektor um, der die Ausrichtung des Eingabestifts darstellt.

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. Mit Ebenen können Nutzer verschiedene Illustrationen übereinander zeichnen und sie Schicht für Schicht bearbeiten. pixiv Sketch bietet Ebenenfunktionen, die denen anderer digitaler Zeichen-Apps ähneln.

Herkömmlicherweise können Ebenen mit mehreren <canvas>-Elementen mit drawImage()- und Kompositionsvorgängen implementiert werden. Das ist jedoch problematisch, da im 2D-Canvas-Kontext nur der vordefinierte CanvasRenderingContext2D.globalCompositeOperation-Kompositionsmodus verwendet werden kann, der die Skalierbarkeit stark einschränkt. Durch die Verwendung von WebGL und das Schreiben des Shaders können die Entwickler Kompositionmodi verwenden, die nicht von der API vordefiniert sind. In Zukunft wird die Ebenenfunktion in pixiv Sketch mit WebGL implementiert, um die Skalierbarkeit und Flexibilität zu verbessern.

Hier ist der Beispielcode für die Ebenenzusammensetzung:

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 Flächenmalerei mit der Bucket-Funktion

Die iOS- und Android-Apps von pixiv Sketch boten bereits die Farbtonfunktion, die Webversion jedoch nicht. Die App-Version der Bucket-Funktion wurde in C++ implementiert.

Da die Codebasis bereits in C++ verfügbar war, verwendete das Team von pixiv Sketch Emscripten und asm.js, um die Bucket-Funktion in der Webanwendung 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. Im Vergleich zur Ausführungszeit von reinem JavaScript wird die Ausführungszeit mit asm.js um 67 % verkürzt.asm.js Mit WASM wird dies voraussichtlich noch besser.

Test details:

  • Anleitung:Bereich von 1.180 × 800 Pixeln mit der Bucket-Funktion ausmalen
  • Testgerät:MacBook Pro (M1 Max)

Ausführungszeit:

  • Reines JavaScript:213,8 ms
  • asm.js::70,3 ms

Mit Emscripten und asm.js konnte das Team von pixiv Sketch die Bucket-Funktion erfolgreich veröffentlichen, indem es die Codebasis der plattformspezifischen App-Version wiederverwendete.

Live-Streaming beim Zeichnen

In der Web-App „pixiv Sketch LIVE“ kannst du über pixiv Sketch während des Zeichnens einen Livestream starten. Dabei wird die WebRTC API verwendet, um den Mikrofon-Audiotrack von getUserMedia() und den MediaStream-Videotrack vom <canvas>-Element zu kombinieren.

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());

Schlussfolgerungen

Mit neuen APIs wie WebGL, WebAssembly und WebRTC können Sie eine komplexe App auf der Webplattform erstellen und auf allen Geräten skalieren. Weitere Informationen zu den in dieser Fallstudie vorgestellten Technologien finden Sie unter den folgenden Links: