El poder de la Web para los ilustradores: Cómo pixiv usa las tecnologías web en su app de dibujo

pixiv es un servicio de comunidad en línea para ilustradores y entusiastas de la ilustración que se comunican entre sí a través de su contenido. Permite que las personas publiquen sus propias ilustraciones. Tiene más de 84 millones de usuarios en todo el mundo y, hasta mayo de 2023, se habían publicado más de 120 millones de obras de arte.

pixiv Sketch es uno de los servicios que proporciona pixiv. Se usa para dibujar obras de arte en el sitio web con los dedos o lápices ópticos. Admite una variedad de funciones para dibujar ilustraciones increíbles, incluidos numerosos tipos de pinceles, capas y pintura con balde, y también permite que las personas transmitan en vivo su proceso de dibujo.

En este caso de estudio, analizaremos cómo pixiv Sketch mejoró el rendimiento y la calidad de su app web con algunas funciones nuevas de la plataforma web, como WebGL, WebAssembly y WebRTC.

¿Por qué desarrollar una app de bocetos en la Web?

pixiv Sketch se lanzó por primera vez en la Web y en iOS en 2015. Su público objetivo para la versión web era principalmente el de computadoras de escritorio, que sigue siendo la plataforma más importante que usa la comunidad de ilustradores.

Estos son los dos principales motivos por los que pixiv decidió desarrollar una versión web en lugar de una app para computadoras:

  • Crear apps para Windows, Mac, Linux y otros sistemas operativos es muy costoso. La Web llega a cualquier navegador en la computadora de escritorio.
  • La Web tiene el mejor alcance en todas las plataformas. La Web está disponible en computadoras y dispositivos móviles, y en todos los sistemas operativos.

Tecnología

pixiv Sketch tiene varios pinceles diferentes para que los usuarios elijan. Antes de adoptar WebGL, solo había un tipo de pincel, ya que el lienzo 2D era demasiado limitado para representar la textura compleja de diferentes pinceles, como los bordes gruesos de un lápiz y la diferente intensidad de ancho y color que cambia con la presión del boceto.

Tipos de pinceles creativos con WebGL

Sin embargo, con la adopción de WebGL, pudieron agregar más variedades en los detalles de los pinceles y aumentar la cantidad de pinceles disponibles a siete.

Los siete pinceles diferentes de pixiv, que van desde finos hasta gruesos, nítidos hasta desenfocados, pixelados hasta suaves, etcétera.

Con el contexto del lienzo 2D, solo era posible dibujar líneas con una textura simple y un ancho distribuido de manera uniforme, como en la siguiente captura de pantalla:

Pincelada con textura simple.

Estas líneas se dibujaron creando rutas y dibujando trazos, pero WebGL reproduce esto con sprites de puntos y sombreadores, como se muestra en las siguientes muestras de código.

En el siguiente ejemplo, se muestra un sombreador de vértices.

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

En el siguiente ejemplo, se muestra código de muestra para un sombreador de fragmentos.

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

El uso de sprites de puntos facilita la variación del grosor y el sombreado en respuesta a la presión de dibujo, lo que permite expresar las siguientes líneas fuertes y débiles, como estas:

Pincelada uniforme y definida con extremos delgados.

Pincelada desenfocada con más presión aplicada en el medio.

Además, las implementaciones que usan sprites de puntos ahora pueden adjuntar texturas con un sombreador independiente, lo que permite una representación eficiente de pinceles con texturas, como lápices y marcadores.

Compatibilidad con la pluma stylus en el navegador

El uso de un lápiz óptico digital se ha vuelto muy popular entre los artistas digitales. Los navegadores modernos admiten la API de PointerEvent, que permite a los usuarios usar una pluma stylus en su dispositivo: usa PointerEvent.pressure para medir la presión de la pluma y PointerEvent.tiltX y PointerEvent.tiltY para medir el ángulo de la pluma con respecto al dispositivo.

Para realizar pinceladas con un sprite de punto, el PointerEvent debe interpolarse y convertirse en una secuencia de eventos más detallada. En PointerEvent, la orientación del lápiz se puede obtener en forma de coordenadas polares, pero pixiv Sketch las convierte en un vector que representa la orientación del lápiz antes de usarlas.

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

Varias capas de dibujo

Las capas son uno de los conceptos más singulares del dibujo digital. Permiten a los usuarios dibujar diferentes partes de una ilustración una sobre otra y realizar ediciones capa por capa. pixiv Sketch proporciona funciones de capas similares a las de otras apps de dibujo digital.

Convencionalmente, es posible implementar capas usando varios elementos <canvas> con drawImage() y operaciones de composición. Sin embargo, esto es problemático porque, con el contexto de Canvas 2D, no hay otra opción que usar el modo de composición CanvasRenderingContext2D.globalCompositeOperation, que está predefinido y limita en gran medida la escalabilidad. Con WebGL y la escritura del sombreador, los desarrolladores pueden usar modos de composición que no están predefinidos por la API. En el futuro, pixiv Sketch implementará la función de capas con WebGL para una mayor escalabilidad y flexibilidad.

Este es el código de muestra para la composición de capas:

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

Pintar un área grande con la función de cubo

Las apps de pixiv Sketch para iOS y Android ya proporcionaban la función de cubo, pero la versión web no. La versión de la función de discretización de la app se implementó en C++.

Como la base de código ya estaba disponible en C++, pixiv Sketch usó Emscripten y asm.js para implementar la función de bucket en la versión web.

bfsQueue.push(startPoint);

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

El uso de asm.js permitió crear una solución eficaz. Si se compara el tiempo de ejecución de JavaScript puro con el de asm.js, el tiempo de ejecución con asm.js se reduce en un 67%. Se espera que esto mejore aún más cuando se use WASM.

Detalles de la prueba:

  • Cómo: Pinta un área de 1,180 x 800 px con la función de bucket.
  • Dispositivo de prueba: MacBook Pro (M1 Max)

Tiempo de ejecución:

  • JavaScript puro: 213.8 ms
  • asm.js: 70.3 ms

Con Emscripten y asm.js, pixiv Sketch pudo lanzar con éxito la función de cubo reutilizando la base de código de la versión de la app específica para la plataforma.

Transmisión en vivo mientras dibujo

pixiv Sketch ofrece la función de transmitir en vivo mientras dibujas a través de la app web de pixiv Sketch LIVE. Esta función usa la API de WebRTC y combina la pista de audio del micrófono obtenida de getUserMedia() y la pista de video MediaStream recuperada del elemento <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());

Conclusiones

Con el poder de las nuevas APIs, como WebGL, WebAssembly y WebRTC, puedes crear una app compleja en la plataforma web y escalarla en cualquier dispositivo. Puedes obtener más información sobre las tecnologías que se presentan en este caso de estudio en los siguientes vínculos: