עוצמת האינטרנט למאיירים: איך חברת pixiv משתמשת בטכנולוגיות אינטרנט עבור אפליקציית הציור שלה

pixiv הוא שירות קהילתי באינטרנט שמאפשר למאיירים ולחובבי איור לתקשר ביניהם באמצעות התוכן שלהם. היא מאפשרת לאנשים לפרסם איורים משלהם. ל-Pinterest יש יותר מ-84 מיליון משתמשים ברחבי העולם, ומאז מאי 2023 נוספו אליה יותר מ-120 מיליון יצירות אמנות.

pixiv Sketch הוא אחד מהשירותים ש-pixiv מספקת. הוא משמש לציור גרפיקה באתר באמצעות אצבעות או סטיילוסים. הוא תומך במגוון תכונות לאיורים מדהימים, כולל סוגים רבים של מכחולים, שכבות וצביעה של קטגוריות, וגם מאפשר לאנשים לבצע סטרימינג בשידור חי של תהליך הציור.

במחקר המקרה הזה נסביר איך צוות pixiv Sketch שיפר את הביצועים והאיכות של אפליקציית האינטרנט שלהם באמצעות כמה תכונות חדשות בפלטפורמת האינטרנט, כמו WebGL,‏ WebAssembly ו-WebRTC.

למה כדאי לפתח אפליקציה לציור באינטרנט?

הכלי pixiv Sketch הושק לראשונה באינטרנט וב-iOS בשנת 2015. קהל היעד שלהם לגרסה האינטרנטית היה בעיקר מחשבים, שעדיין נחשבים לפלטפורמה העיקרית שבה משתמשים בקהילה של המאיירים.

אלה שתי הסיבות העיקריות לכך שב-pixiv בחרו לפתח גרסה לאינטרנט במקום אפליקציה למחשב:

  • יצירת אפליקציות ל-Windows, ל-Mac, ל-Linux ועוד הרבה יותר יקרה. האתר זמין בכל דפדפן במחשב.
  • האינטרנט הוא הפלטפורמה עם פוטנציאל החשיפה הגבוה ביותר בפלטפורמות שונות. האינטרנט זמין במחשבים ובניידים, בכל מערכת הפעלה.

טכנולוגיה

ב-pixiv Sketch יש כמה מברשות שונות שמשתמשים יכולים לבחור מתוכן. לפני שעברנו ל-WebGL, היה רק סוג אחד של מברשת כי לוח הציור הדו-מימדי היה מוגבל מדי כדי לתאר את המרקם המורכב של מברשות שונות, כמו קצוות גסים של עיפרון ורמות שונות של רוחב ועוצמת צבע שמשתנות בהתאם ללחץ על העיפרון.

סוגי מברשות קריאייטיב באמצעות WebGL

עם זאת, בעזרת WebGL הם הצליחו להוסיף מגוון רחב יותר של פרטים למברשות ולהגדיל את מספר המברשות הזמינות לשבע.

שבע המכחולים השונות ב-pixiv נעות בין עדין לגס, חדות לא חדות, מפוקסלות לחלקות וכו'.

באמצעות ההקשר של לוח הציור בדו-ממד אפשר היה לצייר רק קווים שיש להם מרקם פשוט ברוחב פיזור שווה, כמו בצילום המסך הבא:

משיחת מכחול עם מרקם פשוט.

השורות האלה נוצרו על ידי יצירת נתיבים וציור קווים, אבל WebGL יוצר אותן באמצעות נקודות רפאים ו-shaders, כפי שמוצג בדוגמאות הקוד הבאות.

הדוגמה הבאה מדגימה שדה צבעים של קודקוד.

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

השימוש ב-point sprites מאפשר לשנות בקלות את העובי וההצללה בתגובה ללחץ על הכלי לציור, וכך ליצור קווים חזקים וחלשים, כמו אלה:

משיחות מברשת חדות ואחידה עם קצוות דקים.

קו מברשת לא חד עם לחץ חזק יותר במרכז.

בנוסף, עכשיו אפשר לצרף טקסטורות להטמעות שמשתמשות ב-point sprites באמצעות שדרן נפרד, וכך לייצג בצורה יעילה מברשות עם טקסטורות כמו עיפרון ועט פלסטיק.

תמיכה בסטיילוס בדפדפן

השימוש בעט דיגיטלי הפך לפופולרי מאוד בקרב אומנים דיגיטליים. דפדפנים מודרניים תומכים ב-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() ופעולות הרכבה. עם זאת, הבעיה היא שבהקשר של קנבס 2D אין ברירה אלא להשתמש במצב ההרכבה 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, אבל לא בגרסה לאינטרנט. גרסת האפליקציה של פונקציית הקטגוריה יושמה ב-C++‎.

קוד הבסיס כבר זמין ב-C++‎, ולכן ב-pixiv Sketch השתמשו ב-Emscripten וב-asm.js כדי להטמיע את פונקציית הקטגוריות בגרסה לאינטרנט.

bfsQueue.push(startPoint);

while (!bfsQueue.empty()) {
  Point point = bfsQueue.front();
  bfsQueue.pop();
  /* ... */
  bfsQueue.push(anotherPoint);
}

השימוש ב-asm.js אפשר פתרון עם ביצועים טובים. בהשוואה בין זמן הביצוע של JavaScript טהור לבין זמן הביצוע של asm.js, זמן הביצוע באמצעות asm.js קצר ב-67%. הביצועים צפויים להיות טובים יותר כשמשתמשים ב-WASM.

פרטי הבדיקה:

  • איך: ציור אזור בגודל 1,180 על 800 פיקסלים באמצעות פונקציית קטגוריה
  • מכשיר הבדיקה: MacBook Pro‏ (M1 Max)

זמן ביצוע:

  • JavaScript טהור: 213.8ms
  • asm.js: 70.3 אלפיות שנייה

באמצעות Emscripten ו-asm.js, הצוות של pixiv Sketch הצליח להשיק את תכונת הקטגוריות על ידי שימוש חוזר בקוד הבסיסי מגרסת האפליקציה הספציפית לפלטפורמה.

שידור חי בזמן ציור

ב-pixiv Sketch אפשר להשתמש בתכונה כדי לשדר בשידור חי בזמן ציור דרך אפליקציית האינטרנט pixiv. Sketchiv 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, אפשר ליצור אפליקציה מורכבת בפלטפורמת האינטרנט ולהתאים אותה לכל מכשיר. מידע נוסף על הטכנולוגיות שמפורטות בניתוח המקרה הזה זמין בקישורים הבאים: