Calculatrice de conception

Tentative de recréation d'une calculatrice solaire sur le Web avec l'API Window Controls Overlay et l'API Ambient Light Sensor.

Problématique

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

Près de 28 ans plus tard, j'ai pensé que ce serait un défi amusant pour Designcember de recréer la calculatrice en HTML, CSS et JavaScript. N'étant pas vraiment concepteur, je ne suis pas parti de zéro, mais avec un CodePen de Sassja Ceballos.

Vue CodePen avec des panneaux HTML, CSS et JS superposés à gauche et un aperçu de la calculatrice à droite.

Rendre l'application installable

Ce n'était pas un mauvais début, mais j'ai décidé de l'améliorer pour obtenir un résultat vraiment génial. La première étape a consisté à en faire une PWA afin qu'elle puisse être installée. Je gère un modèle de PWA de référence sur Glitch que je remixe chaque fois que j'ai besoin d'une démonstration rapide. Son service worker ne vous fera pas gagner de prix de codage et n'est pas prêt pour la production, mais il suffit de déclencher la mini infobar de Chromium pour que l'application 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 peut être installée, l'étape suivante consiste à la faire se fondre autant que possible dans les 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 le viewport afin que le contenu couvre l'intégralité de l'écran rend l'application plus attrayante.

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

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

Intégration à l'ordinateur

Sur ordinateur, je peux utiliser une fonctionnalité intéressante : la superposition des commandes de fenêtre, 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'elle 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'était pas là. Mon idée est de déplacer la cellule solaire stéréotypée vers le haut dans la barre de titre et de baisser le reste de l'interface utilisateur de la calculatrice en conséquence, ce que je peux faire avec du CSS qui utilise les variables d'environnement titlebar-area-*. Vous remarquerez que tous les sélecteurs portent une classe wco, qui sera utile quelques paragraphes plus loin.

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

Ensuite, je dois décider des éléments à rendre déplaçables, car la barre de titre que j'utilise habituellement pour le glisser-déposer 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 à rendre l'application réactive aux modifications apportées à la superposition des commandes de fenêtre. Dans une approche d'amélioration progressive, je ne charge le code de cette fonctionnalité que lorsque le navigateur est compatible avec elle.

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

Chaque fois que la géométrie de superposition des commandes de fenêtre change, je modifie l'application pour la rendre aussi naturelle que possible. Il est conseillé de déjouer cet événement, car il peut être déclenché fréquemment lorsque l'utilisateur redimensionne la fenêtre. Plus précisément, j'applique la classe wco à certains éléments, de sorte que mon CSS ci-dessus s'active et que je modifie également la couleur du thème. Je peux détecter si la superposition des commandes de 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 Winamp classique avec l'un des thèmes Winamp d'antan. Je peux désormais placer librement la calculatrice sur mon bureau et activer la fonctionnalité de commandes de fenêtre en cliquant sur le chevron en haut à droite.

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

Une cellule photovoltaïque fonctionnelle

Pour être vraiment geek, j'ai bien sûr dû faire fonctionner la cellule solaire. La calculatrice ne doit fonctionner que si la luminosité est suffisante. Pour ce faire, j'ai défini le 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 suffisamment de lumière est disponible 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 que le code pertinent lorsque l'API est compatible.

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

Le capteur renvoie la luminosité ambiante en unités lux chaque fois qu'une nouvelle lecture est disponible. Sur la base d'un tableau de valeurs de situations d'éclairage typiques, j'ai élaboré 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'ai suffisamment allumé la lumière de la pièce. Et voilà: une calculatrice solaire stéréotypée qui fonctionne. Mon bon vieux TI-30X SOLAR a fait du chemin.

Démo

N'hésitez pas à tester la démo du calculateur Designcember et à consulter le code source sur Glitch. (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 info-barre.)

Joyeux Designcember !