מחשבון 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 כשהוא זמין.

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

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

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

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

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

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