pixiv 是一项在线社区服务,供插画师和插画爱好者通过内容彼此交流。用户可以发布自己的插图。截至 2023 年 5 月,该平台在全球拥有超过 8, 400 万用户,发布了超过 1.2 亿件艺术作品。
pixiv Sketch 是 pixiv 提供的一项服务。它用于在网站上使用手指或触控笔绘制作品。它支持各种用于绘制精美插画的功能,包括多种类型的画笔、图层和桶式上色,还允许用户直播自己的绘画过程。
在此案例研究中,我们将了解 pixiv Sketch 如何通过使用 WebGL、WebAssembly 和 WebRTC 等一些新的 Web 平台功能来提升其 Web 应用的性能和质量。
为什么要在 Web 上开发涂鸦应用?
pixiv Sketch 于 2015 年首次在网页和 iOS 设备上发布。其网页版的目标受众群体主要是桌面设备用户,而桌面设备仍然是插画社区使用的主要平台。
以下是 pixiv 选择开发网页版而非桌面应用的两大原因:
- 为 Windows、Mac、Linux 等平台创建应用的成本非常高。Web 可在桌面设备上的任何浏览器中访问。
- Web 具有跨平台覆盖面广的优势。Web 适用于桌面设备和移动设备,并且支持所有操作系统。
技术
pixiv Sketch 提供了多种不同的画笔供用户选择。在采用 WebGL 之前,只有一种笔刷,因为 2D 画布过于有限,无法描绘不同笔刷的复杂纹理,例如铅笔的粗糙边缘以及在草图压力下会发生变化的宽度和颜色强度。
使用 WebGL 的笔刷广告素材类型
不过,随着 WebGL 的采用,他们能够添加更多种类的笔刷细节,并将可用笔刷的数量增加到七个。

使用 2D 画布上下文时,只能绘制具有宽度均匀分布的简单纹理的线条,如以下屏幕截图所示:

这些线条是通过创建路径和绘制笔画来绘制的,但 WebGL 使用点精灵和着色器来重现此效果,如以下代码示例所示
以下示例演示了一个顶点着色器。
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;
}
以下示例展示了片段着色器的示例代码。
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);
}
使用点精灵可以轻松根据绘制压力改变粗细和阴影,从而绘制出以下粗细不同的线条:


此外,使用点精灵的实现现在可以通过使用单独的着色器来附加纹理,从而高效地表示带有纹理的笔刷,例如铅笔和毡尖笔。
浏览器中的触控笔支持
对于数字艺术家来说,使用数字触控笔已变得非常流行。现代浏览器支持 PointerEvent API,使用户能够在设备上使用触控笔:使用 PointerEvent.pressure 测量笔压力,使用 PointerEvent.tiltX、PointerEvent.tiltY 测量笔与设备的角度。
为了使用点精灵执行笔触,必须对 PointerEvent 进行插值并将其转换为更精细的事件序列。在 PointerEvent 中,触控笔的方向可以极坐标的形式获得,但 pixiv Sketch 会先将它们转换为表示触控笔方向的矢量,然后再使用它们。
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);
}
多个绘制层
图层是数字绘画中最独特的概念之一。用户可以在彼此之上绘制不同的插图,并逐层进行编辑。pixiv Sketch 提供的图层功能与其他数字绘画应用非常相似。
按照惯例,可以使用多个包含 drawImage() 和合成操作的 <canvas> 元素来实现图层。不过,这会带来问题,因为对于 2D 画布上下文,除了使用 CanvasRenderingContext2D.globalCompositeOperation 合成模式之外,没有其他选择,而这种模式是预定义的,在很大程度上限制了可伸缩性。通过使用 WebGL 和编写着色器,开发者可以使用 API 未预定义的合成模式。未来,pixiv Sketch 将使用 WebGL 实现图层功能,以提高可伸缩性和灵活性。
以下是图层合成的示例代码:
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);
}
}
使用桶状工具大面积涂色
pixiv Sketch iOS 和 Android 应用已提供桶功能,但网页版尚未提供。分桶函数的应用版本是在 C++ 中实现的。
由于代码库已采用 C++ 编写,pixiv Sketch 使用 Emscripten 和 asm.js 将桶功能实现到 Web 版本中。
bfsQueue.push(startPoint);
while (!bfsQueue.empty()) {
Point point = bfsQueue.front();
bfsQueue.pop();
/* ... */
bfsQueue.push(anotherPoint);
}
使用 asm.js 可实现高性能解决方案。比较纯 JavaScript 与 asm.js 的执行时间,使用 asm.js 的执行时间缩短了 67%。使用 WASM 时,预计效果会更好。
测试详情:
- 方法:使用桶函数绘制 1180x800 像素区域
- 测试设备:MacBook Pro (M1 Max)
执行时间:
- 纯 JavaScript:213.8 毫秒
- asm.js::70.3 毫秒
借助 Emscripten 和 asm.js,pixiv Sketch 能够通过重用平台专用应用版本的代码库,成功发布桶功能。
边画画边直播
pixiv Sketch 通过 pixiv Sketch LIVE Web 应用提供边画边直播的功能。此功能使用 WebRTC API,将从 getUserMedia() 获取的麦克风音轨与从 <canvas> 元素检索到的 MediaStream 视频轨道相结合。
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());
总结
借助 WebGL、WebAssembly 和 WebRTC 等新 API 的强大功能,您可以在 Web 平台上创建复杂的应用,并将其扩展到任何设备。如需详细了解本案例研究中介绍的技术,请访问以下链接:
- WebGL
- 另请参阅 WebGL 的后继者 WebGPU
- WebAssembly
- WebRTC
- 日语原文