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

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

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

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

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

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

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

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

טכנולוגיה

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

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

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

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

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

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

קווים אלה שורטטו על ידי יצירת נתיבים ושרטוטי קווים, אבל WebGL משחזר אותם באמצעות Sprites של נקודות והצללות, המוצגים בדוגמאות הקוד הבאות

הדוגמה הבאה מציגה לוח הצללה (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;
}

בדוגמה הבאה מוצג קוד לדוגמה של תוכנת הצללה לקטעים.

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

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

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

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

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

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

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

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

זמן הביצוע:

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

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

סטרימינג בשידור חי תוך כדי ציור

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