La puissance du Web pour les illustrateurs: comment pixiv utilise les technologies Web pour son application de dessin

pixiv est un service communautaire en ligne permettant aux illustrateurs et aux passionnés d'illustrations de communiquer entre eux via leur contenu. Il permet aux gens de publier leurs propres illustrations. Elle compte plus de 84 millions d'utilisateurs à travers le monde et plus de 120 millions d'œuvres d'art sont publiées en mai 2023.

pixiv Sketch est l'un des services fournis par pixiv. Il est utilisé pour dessiner des œuvres sur le site web, à l'aide de doigts ou de stylets. Il prend en charge de nombreuses fonctionnalités permettant de dessiner de superbes illustrations, y compris de nombreux types de pinceaux, de calques et de peinture au seau, et permet également aux utilisateurs de diffuser leur processus de dessin en direct.

Dans cette étude de cas, nous verrons comment pixiv Sketch a amélioré les performances et la qualité de son application Web à l'aide de nouvelles fonctionnalités de plate-forme Web, telles que WebGL, WebAssembly et WebRTC.

Pourquoi développer une application de dessin sur le Web ?

pixiv Sketch a été lancé pour la première fois sur le Web et sur iOS en 2015. Pour la version Web, l'audience cible était principalement l'ordinateur de bureau, qui reste la plate-forme la plus utilisée par la communauté de l'illustration.

Voici les deux principales raisons pour lesquelles Pixiv choisit de développer une version Web plutôt qu'une application de bureau:

  • Il est très coûteux de créer des applications pour Windows, Mac, Linux, etc. Le Web est accessible à n'importe quel navigateur, sur le bureau.
  • Le Web bénéficie de la meilleure couverture sur toutes les plates-formes. Le Web est disponible sur ordinateur et mobile, et sur tous les systèmes d'exploitation.

Technologie

pixiv Sketch propose un certain nombre de pinceaux différents parmi lesquels les utilisateurs peuvent choisir. Avant d'adopter WebGL, il n'y avait qu'un seul type de pinceau, car le canevas 2D était trop limité pour représenter la texture complexe des différents pinceaux, comme les bords grossiers d'un crayon, et une largeur et une intensité de couleur différentes qui changent en fonction de la pression du croquis.

Types de pinceaux créatifs utilisant WebGL

Cependant, l'adoption de WebGL a permis à l'entreprise d'ajouter davantage de variétés dans les détails du pinceau et d'augmenter le nombre de pinceaux disponibles à sept.

Il existe sept pinceaux différents en pixiv : fin à grossier, net à flou, pixélisé à lisse, etc.

En utilisant le contexte du canevas 2D, il était uniquement possible de dessiner des lignes ayant une texture simple avec une largeur répartie uniformément, comme dans la capture d'écran suivante:

Coup de pinceau avec texture simple.

Ces lignes ont été tracées en créant des tracés et des traits, mais WebGL la reproduit à l'aide de sprites et de nuanceurs de points, comme illustré dans les exemples de code suivants.

L'exemple suivant illustre un nuanceur de sommets.

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

L'exemple de code suivant est utilisé pour un nuanceur de fragments.

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

L'utilisation de sprites pointus permet de modifier facilement l'épaisseur et l'ombre en fonction de la pression de dessin, ce qui permet d'exprimer les lignes fortes et faibles suivantes:

Coup de pinceau net et même avec les extrémités fines

Coup de pinceau net en appliquant davantage de pression au milieu.

De plus, les implémentations utilisant des lutins pointus peuvent désormais associer des textures à l'aide d'un nuanceur distinct, ce qui permet de représenter efficacement les pinceaux avec des textures telles qu'un crayon et un feutre.

Compatibilité du navigateur avec les stylets

L'utilisation d'un stylet numérique est devenue extrêmement populaire auprès des artistes numériques. Les navigateurs modernes sont compatibles avec l'API PointerEvent qui permet aux utilisateurs d'utiliser un stylet sur leur appareil. Utilisez PointerEvent.pressure pour mesurer la pression du stylet et PointerEvent.tiltX ou PointerEvent.tiltY pour mesurer l'angle du stylet par rapport à l'appareil.

Pour effectuer des coups de pinceau avec un lutin de type point, l'élément PointerEvent doit être interpolé et converti en une séquence d'événements plus précise. Dans PointerEvent, l'orientation du stylet peut être obtenue sous la forme de coordonnées polaires, mais pixiv Sketch les convertit en un vecteur représentant l'orientation du stylet avant de les utiliser.

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

Plusieurs calques de dessin

Les calques sont l’un des concepts les plus uniques du dessin numérique. Elles permettent aux utilisateurs de dessiner différentes illustrations les unes sur les autres et d'effectuer des modifications couche par couche. Pixiv Sketch fournit des fonctions de calque semblables à celles des autres applications de dessin numériques.

De manière conventionnelle, il est possible d'implémenter des couches en utilisant plusieurs éléments <canvas> avec des drawImage() et des opérations de composition. Toutefois, cela est problématique, car avec le contexte de canevas 2D, il n'y a pas d'autre choix que d'utiliser le mode de composition CanvasRenderingContext2D.globalCompositeOperation, qui est prédéfini et limite en grande partie l'évolutivité. En utilisant WebGL et en écrivant le nuanceur, les développeurs peuvent utiliser des modes de composition non prédéfinis par l'API. À l'avenir, pixiv Sketch implémentera la fonctionnalité de calque à l'aide de WebGL pour plus d'évolutivité et de flexibilité.

Voici l'exemple de code pour la composition des calques:

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

Peinture pour grandes zones avec la fonction de bucket

Les applications iOS et Android de pixiv Sketch fournissaient déjà la fonctionnalité de bucket, mais pas la version Web. La version de l'application de la fonction de bucket a été implémentée en C++.

Le codebase étant déjà disponible en C++, pixiv Sketch a utilisé Emscripten et asm.js pour implémenter la fonction de bucket dans la version Web.

bfsQueue.push(startPoint);

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

L'utilisation de asm.js a permis d'optimiser la solution. En comparant le temps d'exécution du code pur JavaScript à celui de asm.js, le temps d'exécution avec asm.js est réduit de 67%. Cela devrait être encore mieux lorsque vous utilisez WASM.

Détails du test:

  • Comment:Peindre une zone de 1 180 x 800 pixels avec la fonction "Bucket"
  • Appareil de test:MacBook Pro (M1 Max)

Temps d'exécution:

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

Grâce à Emscripten et asm.js, pixiv Sketch a pu publier la fonctionnalité de bucket en réutilisant le codebase de la version de l'application spécifique à la plate-forme.

Diffusion en direct pendant le dessin

pixiv Sketch propose une fonctionnalité permettant de diffuser du contenu en direct pendant le dessin, via l'application Web pixiv Sketch LIVE. Cette fonctionnalité utilise l'API WebRTC, qui combine la piste audio du micro obtenue à partir de getUserMedia() et la piste vidéo MediaStream récupérée à partir de l'élément <canvas>.

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

Conclusions

Grâce à la puissance de nouvelles API telles que WebGL, WebAssembly et WebRTC, vous pouvez créer une application complexe sur la plate-forme Web et la faire évoluer sur n'importe quel appareil. Pour en savoir plus sur les technologies présentées dans cette étude de cas, consultez les liens suivants: