מחשבון Designcember

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

האתגר

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

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

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

איך הופכים את התוכנה לניתנת להתקנה

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

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

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

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

Designcember שמח!