מחשבון Designcember

ניסיון סקאומורפי ליצור מחדש מחשבון סולארי באינטרנט באמצעות Window Controls Overlay API ו-Ambient Light Sensor API.

האתגר

נולדתי בשנות ה-80. כשהייתי בתיכון, כולם רצו מחשבונים סולאריים. כולנו קיבלנו מבית הספר מחשבון TI-30X SOLAR, ויש לי זיכרונות נעימים מהתקופה שבה השווינו את המחשבונים שלנו זה לזה על ידי חישוב העצרת של 69, המספר הכי גבוה שמחשבון TI-30X יכול היה להתמודד איתו. (ההבדלים במהירות היו מאוד משמעותיים, ועדיין לא ברור לי למה).

עכשיו, כמעט 28 שנים אחרי, חשבתי שיהיה כיף ליצור מחדש את המחשבון באמצעות HTML,‏ CSS ו-JavaScript במסגרת אתגר Designcember. אני לא מעצב גדול, אז לא התחלתי מאפס, אלא השתמשתי ב-CodePen של Sassja Ceballos.

תצוגת CodePen עם חלוניות HTML,‏ CSS ו-JS מוערמות בצד ימין ותצוגה מקדימה של המחשבון בצד שמאל.

איך מאפשרים התקנה

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

self.addEventListener('install', (event) => {
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  self.clients.claim();
  event.waitUntil(
    (async () => {
      if ('navigationPreload' in self.registration) {
        await self.registration.navigationPreload.enable();
      }
    })(),
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    (async () => {
      try {
        const response = await event.preloadResponse;
        if (response) {
          return response;
        }
        return fetch(event.request);
      } catch {
        return new Response('Offline');
      }
    })(),
  );
});

שילוב עם נייד

עכשיו, כשהאפליקציה ניתנת להתקנה, השלב הבא הוא לגרום לה להשתלב עם אפליקציות מערכת ההפעלה בצורה הכי טובה שאפשר. בנייד, אפשר לעשות את זה על ידי הגדרת מצב התצוגה ל-fullscreen בקובץ המניפסט של אפליקציית האינטרנט.

{
  "display": "fullscreen"
}

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

<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />

מחשבון Designcember פועל במסך מלא בטלפון Pixel 6 Pro.

התמזגות עם שולחן העבודה

במחשב, יש תכונה מגניבה שאני יכול להשתמש בה: Window Controls Overlay, שמאפשרת לי להציב תוכן בסרגל הכותרת של חלון האפליקציה. השלב הראשון הוא לשנות את רצף ברירת המחדל של מצב התצוגה, כך שהמערכת תנסה להשתמש ב-window-controls-overlay קודם אם הוא זמין.

{
  "display_override": ["window-controls-overlay"]
}

כך סרגל הכותרת נעלם והתוכן עולה לאזור של סרגל הכותרת, כאילו סרגל הכותרת לא היה שם. ההצעה שלי היא להעביר את התא הסולארי הסקאומורפי לחלק העליון של סרגל הכותרת ואת שאר ממשק המשתמש של המחשבון לחלק התחתון בהתאם, ואני יכול לעשות את זה באמצעות CSS שמשתמש במשתני הסביבה titlebar-area-*. אפשר לראות שלכל הרכיבים יש מאפיין wco class, שיהיה רלוונטי בהמשך המאמר.

#calc_solar_cell.wco {
  position: fixed;
  left: calc(0.25rem + env(titlebar-area-x, 0));
  top: calc(0.75rem + env(titlebar-area-y, 0));
  width: calc(env(titlebar-area-width, 100%) - 0.5rem);
  height: calc(env(titlebar-area-height, 33px) - 0.5rem);
}

#calc_display_surface.wco {
  margin-top: calc(env(titlebar-area-height, 33px) - 0.5rem);
}

אחר כך צריך להחליט אילו רכיבים יהיו ניתנים לגרירה, כי סרגל הכותרות שבו בדרך כלל משתמשים לגרירה לא זמין. בסגנון של ווידג'ט קלאסי, אפשר אפילו להגדיר את כל המחשבון כפריט שאפשר לגרור באמצעות (-webkit-)app-region: drag, חוץ מהלחצנים שמקבלים את הערך (-webkit-)app-region: no-drag כדי שלא יהיה אפשר להשתמש בהם לגרירה.

#calc_inside.wco,
#calc_solar_cell.wco {
  -webkit-app-region: drag;
  app-region: drag;
}

button {
  -webkit-app-region: no-drag;
  app-region: no-drag;
}

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

if ('windowControlsOverlay' in navigator) {
  import('/wco.js');
}

בכל פעם שהגיאומטריה של שכבת-העל של אמצעי הבקרה של החלון משתנה, אני משנה את האפליקציה כדי שהיא תיראה טבעית ככל האפשר. מומלץ להשתמש בטכניקת ביטול כפילויות (debounce) באירוע הזה, כי הוא יכול להיות מופעל לעיתים קרובות כשהמשתמש משנה את גודל החלון. כלומר, אני מוסיף את המחלקה wco לכמה אלמנטים, כך שקוד ה-CSS שלמעלה יופעל, ואני גם משנה את צבע העיצוב. אני יכול לזהות אם שכבת העל של אמצעי הבקרה של החלון גלויה על ידי בדיקת המאפיין navigator.windowControlsOverlay.visible.

const meta = document.querySelector('meta[name="theme-color"]');
const nodes = document.querySelectorAll(
  '#calc_display_surface, #calc_solar_cell, #calc_outside, #calc_inside',
);

const toggleWCO = () => {
  if (!navigator.windowControlsOverlay.visible) {
    meta.content = '';
  } else {
    meta.content = '#385975';
  }
  nodes.forEach((node) => {
    node.classList.toggle('wco', navigator.windowControlsOverlay.visible);
  });
};

const debounce = (func, wait) => {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
};

navigator.windowControlsOverlay.ongeometrychange = debounce((e) => {
  toggleWCO();
}, 250);

toggleWCO();

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

מחשבון Designcember פועל במצב עצמאי עם התכונה &#39;שכבת-העל של פקדי החלונות&#39; פעילה. התצוגה מאייתת &#39;Google&#39; באלפבית של המחשבון.

תא סולארי שבאמת פועל

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

:root {
  --opacity: 0.75;
}

#calc_expression,
#calc_result {
  opacity: var(--opacity);
}

כדי לזהות אם יש מספיק אור כדי שהמחשבון יפעל, אני משתמש ב-API‏ AmbientLightSensor. כדי שה-API הזה יהיה זמין, הייתי צריך להגדיר את הדגל #enable-generic-sensor-extra-classes ב-about:flags ולבקש את ההרשאה 'ambient-light-sensor'. כמו קודם, אני משתמש בשיפור הדרגתי כדי לטעון רק את הקוד הרלוונטי כשה-API נתמך.

if ('AmbientLightSensor' in window) {
  import('/als.js');
}

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

const luxToOpacity = (lux) => {
  if (lux > 250) {
    return 1;
  }
  return lux / 250;
};

const sensor = new window.AmbientLightSensor();
sensor.onreading = () => {
  console.log('Current light level:', sensor.illuminance);
  document.documentElement.style.setProperty(
    '--opacity',
    luxToOpacity(sensor.illuminance),
  );
};
sensor.onerror = (event) => {
  console.log(event.error.name, event.error.message);
};

(async () => {
  const {state} = await navigator.permissions.query({
    name: 'ambient-light-sensor',
  });
  if (state === 'granted') {
    sensor.start();
  }
})();

בסרטון שלמטה אפשר לראות איך המחשבון מתחיל לפעול ברגע שאני מגביר את התאורה בחדר. והנה לכם: מחשבון סולארי סקאומורפי שבאמת עובד. המחשבון הישן והטוב שלי, TI-30X SOLAR, עבר כברת דרך ארוכה.

הדגמה (דמו)

מומלץ להתנסות בהדמו של מחשבון Designcember ולעיין בקוד המקור ב-GitHub. (כדי להתקין את האפליקציה, צריך לפתוח אותה בחלון משלה. הגרסה המוטמעת שלמטה לא תפעיל את סרגל המידע הקטן.)

Designcember שמח!