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, um über Inhalte miteinander zu kommunizieren. So können die Leute ihre eigenen Illustrationen posten. Sie hat über 84 Millionen Nutzer auf der ganzen Welt und im Mai 2023 wurden mehr als 120 Millionen Kunstwerke veröffentlicht.

pixiv Sketch ist einer der Dienste von pixiv. Er wird verwendet, um mit Fingern oder Eingabestiften Kunstwerke auf der Website zu zeichnen. Sie unterstützt eine Vielzahl von Funktionen zum Zeichnen eindrucksvoller Illustrationen, darunter verschiedene Arten von Pinseln, Ebenen und das Bemalen von Eimern. 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 durch den Einsatz neuer Webplattform-Features wie WebGL, WebAssembly und WebRTC verbessern konnte.

Warum sollte ich eine Zeichen-App im Web entwickeln?

Pixiv Sketch wurde 2015 erstmals im Web und für iOS veröffentlicht. Die Zielgruppe für die Webversion war vor allem der Computer. Dieser ist immer noch die wichtigste Plattform, die von der Illustrations-Community genutzt wird.

Hier sind die beiden Hauptgründe, warum pixiv eine Webversion anstelle einer Desktop-App entwickeln sollte:

  • Die Entwicklung von Apps für Windows, Mac, Linux und andere ist sehr kostspielig. Das Web erreicht jeden Browser auf dem Desktop.
  • Das Web hat plattformübergreifend die größte Reichweite. Das Web ist auf Computern und Mobilgeräten und unter jedem Betriebssystem verfügbar.

Technologie

pixiv Sketch bietet eine Reihe verschiedener Pinsel zur Auswahl. Vor der Einführung von WebGL gab es nur eine Art von Pinsel, da der 2D-Canvas zu begrenzt war, um die komplexe Textur verschiedener Pinsel darzustellen, z. B. grobe Kanten eines Bleistifts und unterschiedliche Breite und Farbintensität, die sich mit dem Skizzendruck ändern.

Creative-Arten von Pinseln mit WebGL

Mit der Einführung von WebGL konnten sie jedoch mehr Varianten der Pinseldetails hinzufügen und die Anzahl der verfügbaren Pinsel auf sieben erhöhen.

Die sieben verschiedenen Pinsel in Pixiv, 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 gezeigt:

Pinselstrich mit einfacher Textur.

Diese Linien wurden durch Erstellen von Pfaden und Strichen gezeichnet, aber WebGL reproduziert dies mithilfe von Punkt-Sprites und Shadern, 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);
}

Durch die Verwendung von Punkt-Sprites ist es einfach, Dicke und Schattierung als Reaktion auf den Druckdruck zu variieren. Dadurch können die folgenden starken und schwachen Linien ausgedrückt werden:

Scharfer, gleichmäßiger Pinselstrich mit dünnen Enden.

Unscharfe Pinselstrich mit mehr Druck in der Mitte.

Darüber hinaus können bei Implementierungen mit Punkt-Sprites jetzt Texturen durch die Verwendung eines separaten Shaders hinzugefügt werden. Dies ermöglicht eine effiziente Darstellung von Pinseln mit Texturen wie Bleistift und Filzstift.

Eingabestift-Unterstützung im Browser

Bei digitalen Künstlern ist die Verwendung eines digitalen Eingabestifts mittlerweile äußerst beliebt. Moderne Browser unterstützen die PointerEvent API, mit der Nutzer einen Eingabestift auf ihrem Gerät verwenden können: Verwenden Sie PointerEvent.pressure, um den Stiftdruck zu messen, und PointerEvent.tiltX, PointerEvent.tiltY, um den Winkel des Eingabestifts zum Gerät zu messen.

Um Pinselstriche mit einem Punkt-Sprite auszuführen, muss PointerEvent interpoliert und in eine detailliertere Ereignissequenz konvertiert werden. In PointerEvent kann die Ausrichtung des Eingabestifts in Form von polaren Koordinaten abgerufen werden. Pixiv Sketch wandelt sie jedoch in einen Vektor um, der die Ausrichtung des Eingabestifts 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 außergewöhnlichsten Konzepte beim digitalen Zeichnen. Nutzer können damit verschiedene Illustrationen übereinander zeichnen und sie Schicht für Ebene bearbeiten. Pixiv Sketch bietet Ebenenfunktionen ähnlich wie andere digitale Zeichen-Apps.

Üblicherweise ist es möglich, Ebenen mithilfe mehrerer <canvas>-Elemente mit drawImage() und Compositing-Vorgängen zu implementieren. Dies ist jedoch problematisch, da im 2D-Canvas-Kontext keine andere Wahl besteht, als der vordefinierte CanvasRenderingContext2D.globalCompositeOperation-Zusammensetzungsmodus zu verwenden, der die Skalierbarkeit stark einschränkt. Wenn Entwickler WebGL verwenden und den Shader schreiben, können sie Zusammensetzungsmodi nutzen, die nicht von der API vordefiniert sind. Zukünftig wird pixiv Sketch die Ebenenfunktion mithilfe von WebGL implementieren, 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ßflächige Malerei mit Bucket-Funktion

In den pixiv Sketch-Apps für iOS und Android war die Bucket-Funktion bereits vorhanden, in der Webversion jedoch nicht. Die App-Version der Bucket-Funktion wurde in C++ implementiert.

Da die Codebasis bereits in C++ verfügbar war, verwendete pixiv Sketch Emscripten und asm.js, um die Bucket-Funktion in der Webversion zu implementieren.

bfsQueue.push(startPoint);

while (!bfsQueue.empty()) {
  Point point = bfsQueue.front();
  bfsQueue.pop();
  /* ... */
  bfsQueue.push(anotherPoint);
}

Mit asm.js wurde eine leistungsfähige Lösung ermöglicht. Im Vergleich zur Ausführungszeit von reinem JavaScript mit asm.js wird die Ausführungszeit mit asm.js um 67 % verkürzt. Bei Verwendung von WASM sollte dies noch besser sein.

Test details:

  • Wie: Bereich mit 1180 x 800 Pixeln mit Bucket-Funktion malen
  • 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 veröffentlichen. Dazu wurde die Codebasis der plattformspezifischen App-Version wiederverwendet.

Livestreaming beim Zeichnen

pixiv Sketch bietet eine Funktion zum Livestreaming beim Zeichnen über die pixiv Sketch LIVE-Web-App. Dabei wird die WebRTC API verwendet. Dabei werden die aus getUserMedia() abgerufene Mikrofon-Audiospur und die aus dem <canvas>-Element abgerufene MediaStream-Videospur kombiniert.

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 den leistungsstarken neuen APIs wie WebGL, WebAssembly und WebRTC können Sie auf der Webplattform eine komplexe App erstellen und auf jedem Gerät skalieren. Weitere Informationen zu den in dieser Fallstudie vorgestellten Technologien finden Sie unter den folgenden Links: