O poder da Web para ilustradores: como o Pixiv usa tecnologias da Web no app de desenho

O pixiv é um serviço de comunidade on-line para ilustradores e entusiastas de ilustração se comunicarem entre si por meio do conteúdo. Ele permite que as pessoas postem as próprias ilustrações. A plataforma tem mais de 84 milhões de usuários em todo o mundo e mais de 120 milhões de obras de arte postadas em maio de 2023.

O pixiv Sketch é um dos serviços oferecidos pelo pixiv. Ele é usado para desenhar artes no site usando dedos ou stylus. Ele oferece suporte a vários recursos para desenhar ilustrações incríveis, incluindo vários tipos de pincéis, camadas e pintura com balde, além de permitir que as pessoas transmitam ao vivo o processo de desenho.

Neste estudo de caso, vamos mostrar como o pixiv Sketch melhorou a performance e a qualidade do app da Web usando alguns novos recursos da plataforma da Web, como WebGL, WebAssembly e WebRTC.

Por que desenvolver um app de desenho na Web?

O pixiv Sketch foi lançado pela primeira vez na Web e no iOS em 2015. O público-alvo da versão para Web era principalmente o computador, que ainda é a plataforma mais importante usada pela comunidade de ilustração.

Confira os dois principais motivos do pixiv para desenvolver uma versão da Web em vez de um app para computador:

  • É muito caro criar apps para Windows, Mac, Linux e muito mais. A Web chega a qualquer navegador no computador.
  • A Web tem o melhor alcance em todas as plataformas. A Web está disponível em computadores e dispositivos móveis e em todos os sistemas operacionais.

Tecnologia

O pixiv Sketch tem vários pincéis diferentes para os usuários escolherem. Antes da adoção do WebGL, havia apenas um tipo de pincel, já que a tela 2D era muito limitada para representar a textura complexa de pincéis diferentes, como bordas grossas de um lápis e largura e intensidade de cores diferentes que mudam de acordo com a pressão do desenho.

Tipos de pincéis criativos usando o WebGL

No entanto, com a adoção do WebGL, eles conseguiram adicionar mais variedades nos detalhes do pincel e aumentar o número de pincéis disponíveis para sete.

Os sete pincéis diferentes no Pixiv variam de finos a grossos, nítidos a desfocados, pixelados a suaves etc.

Usando o contexto de tela 2D, só era possível desenhar linhas com uma textura simples e largura distribuída uniformemente, como na captura de tela a seguir:

Pincelada com textura simples.

Essas linhas foram desenhadas criando caminhos e traços, mas o WebGL reproduz isso usando sprites de ponto e shaders, mostrados nos exemplos de código a seguir.

O exemplo a seguir demonstra um sombreador de vértice.

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

O exemplo a seguir mostra um código de exemplo para um 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);
}

O uso de sprites de ponto facilita a variação da espessura e do sombreamento em resposta à pressão de desenho, permitindo que as seguintes linhas fortes e fracas sejam expressas, como estas:

Traço de pincel nítido e uniforme com pontas finas.

Traço de pincel sem nitidez com mais pressão aplicada no meio.

Além disso, as implementações que usam sprites de ponto agora podem anexar texturas usando um sombreador separado, permitindo uma representação eficiente de pincéis com texturas como lápis e caneta.

Suporte à stylus no navegador

O uso de uma stylus digital se tornou extremamente popular entre artistas digitais. Os navegadores modernos oferecem suporte à API PointerEvent, que permite que os usuários usem uma stylus no dispositivo: use PointerEvent.pressure para medir a pressão da caneta e PointerEvent.tiltX, PointerEvent.tiltY para medir o ângulo da caneta em relação ao dispositivo.

Para realizar pinceladas com um sprite de ponto, o PointerEvent precisa ser interpolado e convertido em uma sequência de eventos mais refinada. Em PointerEvent, a orientação da stylus pode ser obtida na forma de coordenadas polares, mas o pixiv Sketch as converte em um vetor que representa a orientação da stylus antes de usá-las.

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

Várias camadas de desenho

As camadas são um dos conceitos mais exclusivos do desenho digital. Elas permitem que os usuários desempenhem diferentes partes da ilustração umas sobre as outras e permitem edições por camada. O pixiv Sketch oferece funções de camadas, assim como outros apps de desenho digital.

Convencionalmente, é possível implementar camadas usando vários elementos <canvas> com drawImage() e operações de composição. No entanto, isso é problemático porque, com o contexto de tela 2D, não há outra escolha a não ser usar o modo de composição CanvasRenderingContext2D.globalCompositeOperation, que é predefinido e limita bastante a escalonabilidade. Ao usar o WebGL e escrever o sombreador, os desenvolvedores podem usar modos de composição que não são predefinidos pela API. No futuro, o pixiv Sketch vai implementar o recurso de camada usando o WebGL para maior escalonabilidade e flexibilidade.

Confira o exemplo de código para a composição de camadas:

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 com a função de balde

Os apps do pixiv Sketch para iOS e Android já ofereciam o recurso de balde, mas a versão da Web não. A versão do app da função de bucket foi implementada em C++.

Com a base de código já disponível em C++, o pixiv Sketch usou o Emscripten e o asm.js para implementar a função de bucket na versão da Web.

bfsQueue.push(startPoint);

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

O uso do asm.js permitiu uma solução com bom desempenho. Comparando o tempo de execução do JavaScript puro com o asm.js, o tempo de execução usando o asm.js é reduzido em 67%. Isso deve ser ainda melhor ao usar o WASM.

Detalhes do teste:

  • Como:pintar uma área de 1.180 x 800 px com a função de balde
  • Dispositivo de teste:MacBook Pro (M1 Max)

Tempo de execução:

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

Usando o Emscripten e o asm.js, o pixiv Sketch conseguiu lançar o recurso de bucket reutilizando a base de código da versão do app específica da plataforma.

Fazer uma transmissão ao vivo enquanto desenha

O pixiv Sketch oferece o recurso de transmissão ao vivo enquanto desenha, pelo app da Web pixiv Sketch LIVE. Ele usa a API WebRTC, combinando a faixa de áudio do microfone obtida de getUserMedia() e a faixa de vídeo MediaStream extraída do 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());

Conclusões

Com o poder de novas APIs, como WebGL, WebAssembly e WebRTC, é possível criar um app complexo na plataforma da Web e dimensioná-lo para qualquer dispositivo. Saiba mais sobre as tecnologias apresentadas neste estudo de caso nos links abaixo: