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 uns com os outros por meio do conteúdo. Ele permite que as pessoas postem ilustrações próprias. Eles têm mais de 84 milhões de usuários no mundo todo e mais de 120 milhões de obras de arte postadas até maio de 2023.

O pixiv Sketch é um dos serviços oferecidos pela pixiv. Ele é usado para desenhar obras de arte no site com os dedos ou canetas stylus. Ele oferece suporte a vários recursos para criar ilustrações incríveis, incluindo vários tipos de pincéis, camadas e pintura com balde. Também permite 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 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 usada pela comunidade de ilustradores.

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

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

Tecnologia

O pixiv Sketch tem vários pincéis diferentes para os usuários escolherem. Antes de adotar o 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 ásperas de um lápis e largura e intensidade de cor diferentes que mudam com a pressão do esboço.

Tipos criativos de pincéis usando WebGL

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

Os sete pincéis diferentes do pixiv variam de finos a grossos, nítidos a sem nitidez, pixelados a suaves etc.

Usando o contexto de canvas 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 pontos e shaders, mostrados nos exemplos de código a seguir.

O exemplo a seguir demonstra um shader 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 shader 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 ponto facilita a variação da espessura e do sombreamento em resposta à pressão do desenho, permitindo que as linhas fortes e fracas a seguir sejam expressas, como estas:

Pincelada nítida 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 pontos agora podem anexar texturas usando um shader separado, permitindo uma representação eficiente de pincéis com texturas como lápis e caneta hidrográfica.

Suporte à stylus no navegador

O uso de uma stylus digital se tornou extremamente popular entre artistas digitais. Os navegadores modernos são compatíveis com a API PointerEvent, que permite aos usuários usar 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 fazer 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. Eles permitem que os usuários desenhem diferentes partes da ilustração umas sobre as outras e façam edições camada por camada. O pixiv Sketch oferece funções de camada muito parecidas com as de outros apps de desenho digital.

Normalmente, é 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 opção 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 shader, 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 camadas usando WebGL para maior escalonabilidade e flexibilidade.

Confira o exemplo de código para 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 uma área grande com a função de preenchimento

Os apps pixiv Sketch para iOS e Android já ofereciam o recurso de balde, mas a versão Web não. A versão do app da função de agrupamento 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);
}

O uso de asm.js permitiu uma solução eficiente. Ao comparar o tempo de execução de JavaScript puro com asm.js, o tempo de execução usando asm.js é reduzido em 67%. Isso deve melhorar ainda mais ao usar WASM.

Detalhes do teste:

  • Como:pinte uma área de 1180 x 800 pixels com a função de preenchimento.
  • 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 transmissões ao vivo enquanto desenha

O pixiv Sketch oferece o recurso de transmissão ao vivo enquanto você desenha pelo web app 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 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, é possível criar um app complexo na plataforma da Web e escalonar em qualquer dispositivo. Saiba mais sobre as tecnologias apresentadas neste estudo de caso nos links a seguir: