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 fanáticos de la ilustración que se comunican entre sí a través de su contenido. Permite que las personas posten sus propias ilustraciones. Tiene más de 84 millones de usuarios en todo el mundo y más de 120 millones de obras de arte publicadas a mayo de 2023.

pixiv Sketch es uno de los servicios que proporciona pixiv. Se usa para dibujar trabajos de arte en el sitio web con los dedos o las plumas stylus. Admite una variedad de funciones para dibujar ilustraciones increíbles, incluidos varios tipos de pinceles, capas y pintura con cubo, y también permite que las personas transmitan en vivo su proceso de dibujo.

En este caso de éxito, veremos cómo pixiv Sketch mejoró el rendimiento y la calidad de su app web con el uso de 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 las computadoras de escritorio, que aún es la plataforma más importante que usa la comunidad de ilustración.

Estos son los dos motivos principales 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 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 en 2D era demasiado limitado para representar la textura compleja de diferentes pinceles, como los bordes gruesos de un lápiz y la intensidad de ancho y color diferentes que cambia según 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 de finos a gruesos, nítidos a no nítidos, pixelados a lisos, etcétera

Con el contexto de lienzo 2D, solo era posible dibujar líneas que tienen una textura simple con 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 trazos, pero WebGL las reproduce con sprites de punto y sombreadores, como se muestra en los siguientes ejemplos 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 punto permite variar la grosor y las sombras de manera directa en respuesta a la presión de dibujo, lo que permite expresar las siguientes líneas fuertes y débiles, como las siguientes:

Trazos de pincel nítidos y uniformes con extremos delgados.

Trazo de pincel poco nítido con más presión aplicada en el medio.

Además, las implementaciones que usan sprites de punto ahora pueden adjuntar texturas con un sombreador independiente, lo que permite una representación eficiente de pinceles con texturas como lápiz y pluma de punta fina.

Compatibilidad con la pluma stylus en el navegador

El uso de una pluma stylus 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, PointerEvent.tiltY para medir el ángulo de la pluma en relación con el dispositivo.

Para realizar trazos de pincel con un sprite de punto, se debe interpolar PointerEvent y convertirlo en una secuencia de eventos más detallada. En el evento de puntero, la orientación de la pluma stylus se puede obtener en forma de coordenadas polares, pero pixiv Sketch las convierte en un vector que representa la orientación de la pluma stylus 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 únicos del dibujo digital. Permiten a los usuarios dibujar diferentes ilustraciones una encima de la otra y editar capa por capa. pixiv Sketch proporciona funciones de capas al igual que otras apps de dibujo digital.

De manera convencional, es posible implementar capas con varios elementos <canvas> con drawImage() y operaciones de composición. Sin embargo, esto es problemático porque, con el contexto de lienzo 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. Cuando se usa WebGL y se escribe el sombreador, se permite que los desarrolladores usen modos de composición que la API no predefine. En el futuro, pixiv Sketch implementará la función de capas con WebGL para lograr 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);
  }
}

Pintura de área grande con la función de cubo

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

Con la base de código ya 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ó obtener una solución de alto rendimiento. Si comparamos el tiempo de ejecución de JavaScript puro con asm.js, el tiempo de ejecución con asm.js se reduce en un 67%. Se espera que esto sea aún mejor cuando se use WASM.

Detalles de la prueba:

  • Cómo: Pinta un área de 1180 x 800 px con la función 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 correctamente la función de bucket reutilizando la base de código de la versión de la app específica de la plataforma.

Cómo transmitir en vivo mientras dibujas

pixiv Sketch ofrece la función de transmitir en vivo mientras dibujas a través de la app web pixiv Sketch LIVE. Para ello, se usa la API de WebRTC, que 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 éxito en los siguientes vínculos: