พลังของเว็บสำหรับนักวาดภาพประกอบ: วิธีที่ pixiv ใช้เทคโนโลยีเว็บสำหรับแอปวาดภาพของตน

pixiv เป็นบริการชุมชนออนไลน์สำหรับนักวาดภาพและผู้ที่ชื่นชอบภาพวาด เพื่อสื่อสารกันผ่านเนื้อหาของตน ซึ่งช่วยให้ผู้ใช้ โพสต์ภาพวาดของตนเองได้ โดยมีผู้ใช้กว่า84 ล้านคนทั่วโลก และมีผลงานศิลปะมากกว่า120 ล้านชิ้นที่โพสต์ ณ เดือนพฤษภาคม 2023

pixiv Sketch เป็นหนึ่งในบริการที่ pixiv ให้บริการ ใช้เพื่อวาด อาร์ตเวิร์กบนเว็บไซต์โดยใช้นิ้วหรือสไตลัส โดยรองรับฟีเจอร์ต่างๆ มากมายสำหรับวาดภาพที่น่าทึ่ง ซึ่งรวมถึงแปรงหลายประเภท เลเยอร์ และการระบายสีด้วยเครื่องมือถังสี รวมถึงยังให้ผู้ใช้ไลฟ์สดกระบวนการวาดภาพได้ด้วย

ในกรณีศึกษาครั้งนี้ เราจะมาดูวิธีที่ pixiv Sketch ปรับปรุงประสิทธิภาพและคุณภาพของเว็บแอปโดยใช้ฟีเจอร์ใหม่ๆ ของแพลตฟอร์มเว็บ เช่น WebGL, WebAssembly และ WebRTC

เหตุใดจึงพัฒนาแอปวาดภาพบนเว็บ

pixiv Sketch เปิดตัวครั้งแรกบนเว็บและใน iOS ในปี 2015 กลุ่มเป้าหมาย ของเวอร์ชันเว็บคือเดสก์ท็อปเป็นหลัก ซึ่งยังคงเป็นแพลตฟอร์ม หลักที่ชุมชนนักวาดภาพใช้กันมากที่สุด

เหตุผล 2 ข้อที่สำคัญที่สุดของ pixiv ที่เลือกพัฒนาเวอร์ชันเว็บแทนแอปบนเดสก์ท็อปมีดังนี้

  • การสร้างแอปสำหรับ Windows, Mac, Linux และอื่นๆ มีค่าใช้จ่ายสูงมาก เว็บ เข้าถึงเบราว์เซอร์ใดก็ได้บนเดสก์ท็อป
  • เว็บมีการเข้าถึงที่ดีที่สุดในแพลตฟอร์มต่างๆ เว็บมีให้บริการบนเดสก์ท็อป และอุปกรณ์เคลื่อนที่ รวมถึงในทุกระบบปฏิบัติการ

เทคโนโลยี

pixiv Sketch มีแปรงหลายแบบให้ผู้ใช้เลือก ก่อนที่จะนำ WebGL มาใช้ มีแปรงเพียงประเภทเดียวเนื่องจาก Canvas แบบ 2 มิติมีข้อจำกัดมากเกินไปที่จะแสดงพื้นผิวที่ซับซ้อนของแปรงต่างๆ เช่น ขอบหยาบของดินสอ รวมถึงความกว้างและความเข้มของสีที่แตกต่างกันซึ่งจะเปลี่ยนไปตามแรงกดในการสเก็ตช์

ประเภทแปรงที่สร้างขึ้นโดยใช้ WebGL

อย่างไรก็ตาม เมื่อนำ WebGL มาใช้ นักพัฒนาแอปก็สามารถเพิ่มความหลากหลายใน รายละเอียดของแปรงและเพิ่มจำนวนแปรงที่ใช้ได้เป็น 7 แปรง

แปรง 7 แบบใน pixiv มีตั้งแต่แบบละเอียดไปจนถึงแบบหยาบ คมชัดไปจนถึงไม่คมชัด เป็นพิกเซลไปจนถึงเรียบเนียน และอื่นๆ

เมื่อใช้บริบท Canvas 2 มิติ คุณจะวาดได้เฉพาะเส้นที่มีพื้นผิวเรียบง่ายซึ่งมีความกว้างที่กระจายอย่างสม่ำเสมอ ดังภาพหน้าจอต่อไปนี้

รอยแปรงที่มีพื้นผิวเรียบง่าย

เส้นเหล่านี้วาดขึ้นโดยการสร้างเส้นทางและวาดเส้น แต่ WebGL สร้างเส้นเหล่านี้ซ้ำโดยใช้สปริงพอยต์และ Shader ดังที่แสดงในตัวอย่างโค้ดต่อไปนี้

ตัวอย่างต่อไปนี้แสดง Vertex Shader

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

ตัวอย่างต่อไปนี้แสดงโค้ดตัวอย่างสำหรับ Fragment Shader

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

การใช้สไปรต์จุดทำให้การปรับความหนาและการแรเงาเป็นเรื่องง่าย เมื่อตอบสนองต่อแรงกดในการวาด ซึ่งช่วยให้แสดงเส้นที่หนาและบางได้ เช่น เส้นต่อไปนี้

พู่กันที่คมชัดและสม่ำเสมอพร้อมปลายเรียว

การลากแปรงแบบไม่คมชัดโดยใช้แรงกดมากขึ้นตรงกลาง

นอกจากนี้ การใช้งานที่ใช้สไปรต์จุดยังแนบพื้นผิวได้แล้วในตอนนี้โดย ใช้ Shader แยกต่างหาก ซึ่งช่วยให้แสดงแปรงที่มี พื้นผิว เช่น ดินสอและปากกาเมจิก ได้อย่างมีประสิทธิภาพ

การรองรับสไตลัสในเบราว์เซอร์

การใช้สไตลัสดิจิทัลได้รับความนิยมอย่างมากในหมู่ศิลปินดิจิทัล เบราว์เซอร์รุ่นใหม่รองรับ 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 มีฟังก์ชันเลเยอร์คล้ายกับแอปวาดภาพดิจิทัลอื่นๆ

โดยปกติแล้ว คุณสามารถใช้เลเยอร์ได้โดยใช้<canvas> องค์ประกอบหลายรายการที่มีdrawImage()และการดำเนินการคอมโพสิต อย่างไรก็ตาม วิธีนี้มีปัญหาเนื่องจากบริบท Canvas 2 มิติไม่มีทางเลือกอื่นนอกจากการใช้โหมดการผสม CanvasRenderingContext2D.globalCompositeOperation ซึ่งกำหนดไว้ล่วงหน้าและจำกัดความสามารถในการปรับขนาดเป็นอย่างมาก การใช้ WebGL และการเขียน Shader ช่วยให้นักพัฒนาแอปใช้โหมดการผสม ที่ 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 มีฟีเจอร์ถังอยู่แล้ว แต่เวอร์ชันเว็บยังไม่มี ฟังก์ชัน Bucket เวอร์ชันแอปได้รับการติดตั้งใช้งาน ใน C++

pixiv Sketch ใช้ Emscripten และ asm.js เพื่อติดตั้งใช้งานฟังก์ชันถังลงในเวอร์ชันเว็บ เนื่องจากมีโค้ดเบสใน C++ อยู่แล้ว

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 ซึ่งใช้ WebRTC API โดยรวมแทร็กเสียงจากไมโครโฟนที่ได้จาก getUserMedia() และแทร็กวิดีโอ MediaStream ที่ดึงข้อมูลจากองค์ประกอบ <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());

บทสรุป

ด้วยประสิทธิภาพของ API ใหม่ๆ เช่น WebGL, WebAssembly และ WebRTC คุณจึงสร้าง แอปที่ซับซ้อนบนแพลตฟอร์มเว็บและปรับขนาดแอปในอุปกรณ์ใดก็ได้ ดูข้อมูลเพิ่มเติมเกี่ยวกับเทคโนโลยีที่กล่าวถึงในกรณีศึกษาฉบับนี้ได้ที่ลิงก์ต่อไปนี้