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

Pixiv est un service de communauté en ligne qui permet aux illustrateurs et aux passionnés d'illustration de communiquer entre eux via leurs contenus. Il permet aux utilisateurs de publier leurs propres illustrations. Elle compte plus de 84 millions d'utilisateurs dans le monde et plus de 120 millions d'œuvres d'art publiées en mai 2023.

pixiv Sketch est l'un des services proposés par pixiv. Il permet de dessiner des illustrations sur le site Web, à l'aide de doigts ou de stylets. Il propose de nombreuses fonctionnalités pour dessiner des illustrations incroyables, y compris de nombreux types de pinceaux, de calques et de peinture au seau. Il permet également aux utilisateurs de diffuser en direct leur processus de dessin.

Dans cette étude de cas, nous allons voir comment pixiv Sketch a amélioré les performances et la qualité de son application Web en utilisant de nouvelles fonctionnalités de plate-forme Web telles que WebGL, WebAssembly et WebRTC.

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

pixiv Sketch a été lancé pour la première fois sur le Web et sur iOS en 2015. L'audience cible de la version Web était principalement les ordinateurs de bureau, qui restent la plate-forme la plus utilisée par la communauté des illustrateurs.

Voici les deux principales raisons pour lesquelles pixiv a choisi de développer une version Web plutôt qu'une application pour ordinateur:

  • Il est très coûteux de créer des applications pour Windows, Mac, Linux, etc. Le Web est accessible à tous les navigateurs sur ordinateur.
  • Le Web offre la meilleure couverture sur les différentes plates-formes. Le Web est disponible sur ordinateur et mobile, et sur tous les systèmes d'exploitation.

Technologie

Pixiv Sketch propose plusieurs pinceaux différents. Avant l'adoption de WebGL, il n'existait 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 l'épaisseur et l'intensité de couleur différentes qui changent en fonction de la pression exercée sur le croquis.

Types de pinceaux créatifs utilisant WebGL

Toutefois, avec l'adoption de WebGL, ils ont pu ajouter plus de variétés dans les détails des pinceaux et augmenter le nombre de pinceaux disponibles à sept.

Les sept pinceaux différents de Pixiv, allant du fin au grossier, du net au flou, du pixellisé au lisse, etc.

Avec le contexte de canevas 2D, il n'était possible de dessiner que des lignes dont la texture est simple et dont la largeur est répartie uniformément, comme illustré dans la capture d'écran suivante:

Trait de pinceau avec une texture simple.

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

L'exemple suivant illustre un nuanceur de sommet.

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 suivant présente un exemple de code pour un nuanceur de fragment.

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 point sprites permet de faire varier facilement l'épaisseur et l'ombrage en fonction de la pression de dessin, ce qui permet d'exprimer les lignes fortes et faibles suivantes, par exemple:

Trait de pinceau net et régulier aux extrémités fines.

Trait de pinceau flou avec plus de pression appliquée au milieu.

De plus, les implémentations utilisant des sprites de point 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 que le crayon et le feutre.

Compatibilité avec les stylets dans le navigateur

L'utilisation d'un stylet numérique est devenue extrêmement populaire pour les 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, PointerEvent.tiltY pour mesurer l'angle du stylet par rapport à l'appareil.

Pour effectuer des traits de pinceau avec un sprite de point, le PointerEvent doit être interpolé et converti en 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 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. Ils permettent aux utilisateurs de dessiner différents éléments d'illustration les uns sur les autres et d'effectuer des modifications couche par couche. pixiv Sketch fournit des fonctions de calque comme les autres applications de dessin numérique.

De manière classique, il est possible d'implémenter des calques à l'aide de plusieurs éléments <canvas> avec des opérations drawImage() et de composition. Toutefois, cela pose problème, 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 largement l'évolutivité. En utilisant WebGL et en écrivant le nuanceur, les développeurs peuvent utiliser des modes de composition qui ne sont pas prédéfinis par l'API. À l'avenir, pixiv Sketch implémentera la fonctionnalité de calque à l'aide de WebGL pour une plus grande évolutivité et flexibilité.

Voici un exemple de code pour la composition de 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 d'une grande zone avec la fonction de remplissage au pinceau

Les applications iOS et Android de pixiv Sketch proposaient déjà la fonctionnalité de bac, 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 d'asm.js a permis de proposer une solution performante. En comparant le temps d'exécution du JavaScript pur à celui d'asm.js, le temps d'exécution avec asm.js est réduit de 67%. Ce chiffre devrait être encore meilleur avec WASM.

Détails du test:

  • Procédure:peindre une zone de 1 180 x 800 px 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

En utilisant Emscripten et asm.js, pixiv Sketch a pu publier avec succès la fonctionnalité de bucket en réutilisant le code de base de la version de l'application spécifique à la plate-forme.

Diffuser du contenu en direct pendant que vous dessinez

pixiv Sketch propose la fonctionnalité de diffusion en direct pendant le dessin via l'application Web pixiv Sketch LIVE. Elle utilise l'API WebRTC, combinant 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 l'adapter à n'importe quel appareil. Pour en savoir plus sur les technologies présentées dans cette étude de cas, consultez les liens suivants: