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 comunitario en línea para que los ilustradores y los entusiastas de la ilustración se comuniquen entre sí a través de su contenido. Permite a las personas publicar sus propias ilustraciones. Cuentan con más de 84 millones de usuarios en todo el mundo y más de 120 millones de obras de arte publicadas hasta mayo de 2023.

pixiv Sketch es uno de los servicios que proporciona pixiv. Se usa para dibujar obras de arte en el sitio web, con dedos o plumas stylus. Es compatible con una variedad de funciones para dibujar ilustraciones asombrosas, como muchos tipos de pinceles, capas y pintura en cubos, y también permite a las personas transmitir en vivo su proceso de dibujo.

En este caso de éxito, analizaremos cómo pixiv Sketch mejoró el rendimiento y la calidad de su aplicación web mediante 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 sigue siendo la plataforma más importante usada por la comunidad de ilustraciones.

Estos son los dos motivos principales de pixiv para decidir desarrollar una versión web en lugar de una app de escritorio:

  • Es muy costoso crear apps para Windows, Mac, Linux y muchos otros servicios. La Web accede a cualquier navegador en el escritorio.
  • La Web tiene el mejor alcance en todas las plataformas. La Web está disponible en computadoras, dispositivos móviles y sistemas operativos.

Tecnología

pixiv Sketch tiene una serie de 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 bordes gruesos de un lápiz y diferencias de ancho e intensidad de color que cambia con la presión del boceto.

Tipos creativos de pinceles 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 en pixiv van de fino a grueso, de agudo a poco nítido, pixelado a suave, etcétera.

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

Pincel con textura simple.

Estas líneas se dibujaron creando rutas y trazos de dibujo, pero WebGL las reproduce con objetos de punto 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 incluye 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 objetos de punto hace que sea sencillo variar el grosor y el sombreado en respuesta a la presión de dibujo, lo que permite expresar las siguientes líneas fuertes y débiles de la siguiente manera:

Pincel agudo e incluso uniforme con extremos finos.

Trazo de pincel ligeramente definido con más presión aplicada en el medio

Además, las implementaciones que usan objetos de punto ahora pueden adjuntar texturas a través de un sombreador independiente, lo que permite una representación eficiente de los pinceles con texturas como un lápiz y un bolígrafo.

Compatibilidad con la pluma stylus en el navegador

El uso de una pluma stylus digital se ha vuelto extremadamente 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 stylus hacia el dispositivo.

Para realizar trazos del pincel con un objeto de punto, se debe interpolar el elemento PointerEvent y convertirlo en una secuencia de eventos más detallada. En PointerEvent, 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 singulares en el dibujo digital. Permiten a los usuarios dibujar diferentes ilustraciones una encima de la otra y realizar ediciones capa por capa. Pixiv Sketch proporciona funciones en capas al igual que otras apps de dibujo digital.

De manera convencional, es posible implementar capas mediante varios elementos <canvas> con drawImage() y operaciones de composición. Sin embargo, esto es un problema porque, con el contexto de lienzo 2D, no hay otra opción más que usar el modo de composición CanvasRenderingContext2D.globalCompositeOperation, que está predefinido y limita en gran medida la escalabilidad. Si se usa WebGL y se escribe el sombreador, los desarrolladores pueden usar modos de composición que la API no predefine. En el futuro, pixiv Sketch implementará la función de capas con WebGL para obtener 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 extensa con la función bucket

Las aplicaciones 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 del 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 habilitó una solución de alto rendimiento. Cuando se compara 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 usa WASM.

Detalles de la prueba:

  • Cómo: Pinta un área de 1180 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.3ms

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

Transmisiones en vivo mientras dibujas

pixiv Sketch ofrece la función para transmitir en vivo mientras se dibuja, a través de la app web de pixiv Sketch LIVE. 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 la potencia de nuevas APIs, como WebGL, WebAssembly y WebRTC, puedes crear una app compleja en la plataforma web y escalarla a cualquier dispositivo. Puedes obtener más información sobre las tecnologías presentadas en este caso de éxito en los siguientes vínculos: