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.
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:
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:
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 seguintes links:
- WebGL
- Confira também a WebGPU, a sucessora da WebGL.
- WebAssembly
- WebRTC
- Artigo original em japonês