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

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

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

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

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

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

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

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

טכנולוגיה

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

סוגים יצירתיים של מברשות באמצעות WebGL

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

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

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

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

הקווים האלה נוצרו על ידי יצירת נתיבים וציור קווים, אבל WebGL משחזר את זה באמצעות sprites של נקודות ו-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;
}

בדוגמה הבאה מוצג קוד לדוגמה של 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);
}

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

משיכת מכחול חדה ואחידה עם קצוות דקים.

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

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

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

השימוש בעט דיגיטלי הפך לפופולרי מאוד בקרב אמנים דיגיטליים. דפדפנים מודרניים תומכים ב-PointerEvent API שמאפשר למשתמשים להשתמש בסטיילוס במכשיר שלהם: אפשר להשתמש ב-PointerEvent.pressure כדי למדוד את הלחץ של העט, וב-PointerEvent.tiltX, ‏ PointerEvent.tiltY כדי למדוד את הזווית של העט ביחס למכשיר.

כדי לבצע משיכות מכחול באמצעות sprite של נקודה, צריך לבצע אינטרפולציה של 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 דו-ממדי, אין ברירה אחרת אלא להשתמש במצב ההרכבה 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, בסיס הקוד כבר היה זמין ב-C++‎, ולכן השתמשו ב-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,180x800 פיקסלים באמצעות פונקציית הדלי
  • מכשיר בדיקה: MacBook Pro ‏ (M1 Max)

זמן ביצוע:

  • Pure JavaScript:‏ 213.8ms
  • asm.js: 70.3ms

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