Calculatrice Designcember

Une tentative skeuomorphique de recréer une calculatrice solaire sur le Web avec l'API Window Controls Overlay et l'API Ambient Light Sensor.

Le défi

Je suis un enfant des années 1980. Quand j'étais au lycée, les calculatrices solaires étaient très à la mode. L'école nous avait tous fourni une TI-30X SOLAR, et je me souviens avec nostalgie de nos comparaisons de calculatrices en calculant la factorielle de 69, le nombre le plus élevé que la TI-30X pouvait gérer. (La différence de vitesse était très mesurable, je ne sais toujours pas pourquoi.)

Aujourd'hui, près de 28 ans plus tard, j'ai pensé qu'il serait amusant de recréer la calculatrice en HTML, CSS et JavaScript pour le défi Designcember. N'étant pas un grand concepteur, je ne suis pas parti de zéro, mais d'un CodePen de Sassja Ceballos.

Vue CodePen avec les panneaux HTML, CSS et JS empilés à gauche et l'aperçu de la calculatrice à droite.

Rendre l'application installable

Bien que ce ne soit pas un mauvais début, j'ai décidé de l'améliorer pour obtenir un résultat skeuomorphique exceptionnel. La première étape a consisté à en faire une PWA pour qu'elle puisse être installée.

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

Intégration au mobile

Maintenant que l'application est installable, l'étape suivante consiste à l'intégrer autant que possible aux applications du système d'exploitation. Sur mobile, je peux le faire en définissant le mode d'affichage sur fullscreen dans le fichier manifeste de l'application Web.

{
  "display": "fullscreen"
}

Sur les appareils dotés d'un trou ou d'une encoche pour l'appareil photo, ajuster la fenêtre d'affichage pour que le contenu couvre tout l'écran permet d'obtenir un rendu visuel exceptionnel.

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

Calculatrice Designcember en plein écran sur un téléphone Pixel 6 Pro.

Fusion avec le bureau

Sur ordinateur, je peux utiliser une fonctionnalité intéressante : Window Controls Overlay, qui me permet de placer du contenu dans la barre de titre de la fenêtre de l'application. La première étape consiste à remplacer la séquence de remplacement du mode d'affichage afin qu'elle tente d'utiliser window-controls-overlay en premier lorsqu'il est disponible.

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

La barre de titre disparaît alors, et le contenu se déplace vers le haut dans la zone de la barre de titre, comme si elle n'existait pas. Mon idée est de déplacer la cellule solaire skeuomorphique vers le haut dans la barre de titre et le reste de l'interface utilisateur de la calculatrice vers le bas, ce que je peux faire avec du CSS qui utilise les variables d'environnement titlebar-area-*. Vous remarquerez que tous les sélecteurs comportent une classe wco, qui sera pertinente quelques paragraphes plus bas.

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

Je dois ensuite décider quels éléments rendre déplaçables, car la barre de titre que j'utiliserais habituellement pour le déplacement n'est pas disponible. Dans le style d'un widget classique, je peux même rendre l'ensemble de la calculatrice déplaçable en appliquant (-webkit-)app-region: drag, à l'exception des boutons, qui reçoivent (-webkit-)app-region: no-drag afin qu'ils ne puissent pas être utilisés pour le déplacement.

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

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

La dernière étape consiste à faire en sorte que l'application réagisse aux modifications de la superposition des commandes de la fenêtre. Dans une véritable approche d'amélioration progressive, je ne charge le code de cette fonctionnalité que lorsque le navigateur la prend en charge.

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

Chaque fois que la géométrie de l'overlay des commandes de la fenêtre change, je modifie l'application pour qu'elle paraisse aussi naturelle que possible. Il est judicieux de limiter cet événement, car il peut être déclenché fréquemment lorsque l'utilisateur redimensionne la fenêtre. En l'occurrence, j'applique la classe wco à certains éléments, de sorte que mon CSS ci-dessus entre en jeu, et je modifie également la couleur du thème. Je peux détecter si l'overlay des commandes de la fenêtre est visible en vérifiant la propriété 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();

Maintenant que tout est en place, j'obtiens un widget de calculatrice qui ressemble presque au classique Winamp avec l'un des anciens thèmes Winamp. Je peux maintenant placer librement la calculatrice sur mon bureau et activer la fonctionnalité de contrôle de la fenêtre en cliquant sur le chevron en haut à droite.

Calculatrice Designcember exécutée en mode autonome avec la fonctionnalité de superposition des commandes de fenêtre activée. L'écran affiche &quot;Google&quot; dans l'alphabet de la calculatrice.

Une cellule photovoltaïque qui fonctionne vraiment

Pour pousser le geekisme à son paroxysme, je devais bien sûr faire en sorte que la cellule solaire fonctionne réellement. La calculatrice ne doit fonctionner que si la luminosité est suffisante. Pour modéliser cela, j'ai défini la propriété CSS opacity des chiffres à l'écran via une variable CSS --opacity que je contrôle via JavaScript.

:root {
  --opacity: 0.75;
}

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

Pour détecter si la luminosité est suffisante pour que la calculatrice fonctionne, j'utilise l'API AmbientLightSensor. Pour que cette API soit disponible, j'ai dû définir l'indicateur #enable-generic-sensor-extra-classes dans about:flags et demander l'autorisation 'ambient-light-sensor'. Comme précédemment, j'utilise l'amélioration progressive pour ne charger le code pertinent que lorsque l'API est compatible.

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

Le capteur renvoie la luminosité ambiante en lux chaque fois qu'une nouvelle mesure est disponible. En me basant sur un tableau de valeurs de situations d'éclairage typiques, j'ai trouvé une formule très simple pour convertir la valeur en lux en une valeur comprise entre 0 et 1 que j'attribue de manière programmatique à la variable --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();
  }
})();

Dans la vidéo ci-dessous, vous pouvez voir comment la calculatrice commence à fonctionner une fois que j'allume suffisamment la lumière de la pièce. Et voilà : une calculatrice solaire skeuomorphique qui fonctionne vraiment. Ma bonne vieille TI-30X SOLAR a fait du chemin.

Démo

N'hésitez pas à tester la démo de la calculatrice Designcember et à consulter le code source sur GitHub. (Pour installer l'application, vous devez l'ouvrir dans sa propre fenêtre. La version intégrée ci-dessous ne déclenchera pas la mini-barre d'informations.)

Bon Designcember !