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

pixiv ist ein Online-Community-Dienst für Illustratoren und Illustrationsbegeisterte, über den sie über ihre Inhalte miteinander kommunizieren können. 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 von pixiv angebotenen Dienste. Mit diesem Tool können Sie mit Fingern oder Eingabestiften Artwork auf der Website zeichnen. Es bietet eine Vielzahl von Funktionen zum Zeichnen beeindruckender Illustrationen, darunter zahlreiche Pinseltypen, Ebenen und das Ausmalen mit dem Farbtopf. Außerdem können Nutzer ihren Zeichenprozess live streamen.

In dieser Fallstudie sehen wir uns an, wie das Team von pixiv Sketch die Leistung und Qualität seiner Web-App durch die Verwendung 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-Geräten veröffentlicht. Die Zielgruppe für die Webversion war hauptsächlich der Desktop-Computer, der immer noch die wichtigste Plattform der Illustrations-Community ist.

Hier sind die beiden wichtigsten Grü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 jeden 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.

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

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

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

Unscharfer Pinselstrich mit mehr 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 sehr beliebt 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 abgerufen werden. In Pixiv Sketch werden sie jedoch vor der Verwendung in einen Vektor umgewandelt, 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 Zeichenanwendungen ähneln.

Traditionell ist es möglich, Ebenen mit mehreren <canvas>-Elementen mit drawImage()- und Kompositionsvorgängen zu implementieren. Das ist jedoch problematisch, da im 2D-Canvas-Kontext nur der vordefinierte CanvasRenderingContext2D.globalCompositeOperation-Kompositionmodus 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 Ebenenkomposition:

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ächen mit der Farbwannenfunktion malen

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

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

Ergebnisse

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: