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

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

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

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

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