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

O pixiv é um serviço comunitário on-line para que ilustradores e entusiastas se comuniquem entre si pelo conteúdo. Ele permite que as pessoas publiquem suas próprias ilustrações. Ela tem mais de 84 milhões de usuários em todo o mundo e mais de 120 milhões de obras de arte publicadas em maio de 2023.

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

Neste estudo de caso, vamos analisar como o Pixiv Sketch melhorou o desempenho e a qualidade do app da Web usando alguns novos recursos de 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 da Web era principalmente o computador, que ainda é a maior plataforma usada pela comunidade de ilustrações.

Veja os dois principais motivos do Pixel para escolher 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 desktop.
  • A Web tem o melhor alcance em todas as plataformas. Ela está disponível em computadores, dispositivos móveis e em todos os sistemas operacionais.

Tecnologia

O pixiv Sketch tem vários pincéis diferentes para os usuários. 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 diferentes pincéis, como bordas grossas de um lápis e diferentes larguras e intensidades de cor que variam de acordo com a pressão do desenho.

Tipos criativos de pincéis usando WebGL

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

Os sete pincéis em pixiv diferentes variam de fino a grosseiro, afiado a não nivelado, pixelado a liso etc.

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

Pincel com textura simples.

Essas linhas foram desenhadas criando caminhos e traços de desenho, mas o WebGL reproduz isso usando sprites de ponto e sombreadores, 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 exemplo de código para um sombreador de fragmento.

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 pontos facilita a variação de espessura e sombreamento em resposta à pressão de desenho, permitindo que as seguintes linhas fortes e fracas sejam expressas, como estas:

Pinceladas nítidas e uniformes com extremidades finas.

Pinceladas com pinceladas mais nítidas com mais pressão 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 de ponta.

Suporte à stylus no navegador

O uso de uma stylus digital se tornou extremamente famoso entre os 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 use 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. No PointerEvent, a orientação da stylus pode ser recebida na forma de coordenadas polares, mas o Sketch pixiv as converte em um vetor que representa a orientação da stylus antes de usá-la.

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. Eles permitem que os usuários desenhem diferentes ilustrações umas sobre as outras e permitem edições camada por camada. O pixiv Sketch oferece funções de camada de maneira semelhante à de 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 da tela 2D, não há outra escolha além de usar o modo de composição CanvasRenderingContext2D.globalCompositeOperation, que é predefinido e limita bastante a escalonabilidade. Ao usar o WebGL e programar o sombreador, ele permite que os desenvolvedores usem modos de composição que não são predefinidos pela API. No futuro, o Pixiv Sketch implementará o recurso de camada usando WebGL para oferecer maior escalonabilidade e flexibilidade.

Confira um exemplo de código para composição da camada:

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 áreas grandes com a função bucket

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

Com a base de código já disponível em C++, o pixiv Sketch usou Emscripten e 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);
}

Usar asm.js permitiu uma solução de alto desempenho. Comparando o tempo de execução de JavaScript puro com o de asm.js, o tempo de execução que usa asm.js é reduzido em 67%. Espera-se que isso seja ainda melhor ao usar o WASM.

Detalhes do teste:

  • Como:pinte uma área de 1180 x 800 pixels com a função de bucket
  • Dispositivo de teste:MacBook Pro (M1 Max)

Tempo de execução:

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

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

Transmissão ao vivo enquanto desenha

O pixiv Sketch oferece o recurso de transmissão ao vivo durante o desenho, por meio do app da Web pixiv Sketch LIVE. Isso usa a API WebRTC, combinando a faixa de áudio do microfone recebida de getUserMedia() e a faixa de vídeo MediaStream recuperada 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, você pode criar um aplicativo complexo na plataforma da Web e escaloná-lo em qualquer dispositivo. Saiba mais sobre as tecnologias apresentadas neste estudo de caso nos links a seguir: