Calculadora de diseño

Un intento de skeumorfismo para recrear una calculadora solar en la Web con la API de Window Controls Overlay y la API de Ambient Light Sensor.

El desafío

Soy un niño de la década de 1980. Cuando estaba en la secundaria, las calculadoras solares estaban en voga. La escuela nos proporcionó a todos una TI-30X SOLAR, y tengo buenos recuerdos de cuando comparábamos nuestras calculadoras calculando el factorial de 69, el número más alto que podía manejar la TI-30X. (La variación de velocidad era muy medible, y aún no sé por qué).

Ahora, casi 28 años después, pensé que sería un desafío divertido de Designcember recrear la calculadora en HTML, CSS y JavaScript. Como no soy muy diseñador, no comencé desde cero, sino con un CodePen de Sassja Ceballos.

Vista de CodePen con paneles HTML, CSS y JS apilados a la izquierda y la vista previa de la calculadora a la derecha.

Haz que se pueda instalar

Si bien no es un mal comienzo, decidí mejorarlo para lograr un aspecto skeumórfico completo. El primer paso fue convertirla en una AWP para que se pudiera instalar. Mantengo una plantilla de PWA de referencia en Glitch que reedito cada vez que necesito una demostración rápida. Su servicio trabajador no te hará ganar ningún premio de programación y, definitivamente, no está listo para la producción, pero es suficiente para activar la barra de información mínima de Chromium para que se pueda instalar la app.

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

Combinación con dispositivos móviles

Ahora que la app se puede instalar, el siguiente paso es hacer que se integre lo más posible con las apps del sistema operativo. En dispositivos móviles, puedo hacerlo configurando el modo de visualización en fullscreen en el manifiesto de la app web.

{
  "display": "fullscreen"
}

En dispositivos con un orificio para la cámara o una muesca, ajustar el viewport para que el contenido cubra toda la pantalla hace que la app se vea hermosa.

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

Calculadora de Designcember ejecutándose en pantalla completa en un teléfono Pixel 6 Pro.

Combinación con computadoras

En computadoras, hay una función interesante que puedo usar: la superposición de controles de ventana, que me permite colocar contenido en la barra de título de la ventana de la app. El primer paso es anular la secuencia de resguardo del modo de visualización para que intente usar window-controls-overlay primero cuando esté disponible.

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

Esto hace que la barra del título desaparezca de manera efectiva y el contenido se mueva hacia arriba en el área de la barra del título como si esta no estuviera. Mi idea es mover la celda solar skeuomorphic hacia arriba en la barra del título y el resto de la IU de la calculadora hacia abajo según corresponda, lo que puedo hacer con un CSS que use las variables de entorno titlebar-area-*. Notarás que todos los selectores llevan una clase wco, que será relevante un par de párrafos más abajo.

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

A continuación, debo decidir qué elementos hacer arrastrables, ya que la barra de título que usaría para arrastrar no está disponible. Al estilo de un widget clásico, incluso puedo hacer que la calculadora completa sea arrastrable aplicando (-webkit-)app-region: drag, aparte de los botones, que obtienen (-webkit-)app-region: no-drag para que no se puedan usar para arrastrar.

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

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

El último paso es hacer que la app reaccione a los cambios de superposición de los controles de la ventana. En un verdadero enfoque de mejora progresiva, solo cargo el código de esta función cuando el navegador lo admite.

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

Cada vez que cambia la geometría de la superposición de controles de ventana, modifico la app para que se vea lo más natural posible. Es una buena idea anular el rebote de este evento, ya que se puede activar con frecuencia cuando el usuario cambia el tamaño de la ventana. Específicamente, aplico la clase wco a algunos elementos, por lo que se activa mi CSS de arriba y también cambio el color del tema. Para detectar si la superposición de controles de ventana es visible, puedo verificar la propiedad 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();

Ahora que todo está en su lugar, tengo un widget de calculadora que se siente casi como el clásico Winamp con uno de los temas de Winamp de la vieja escuela. Ahora puedo colocar la calculadora libremente en mi escritorio y activar la función de controles de ventana haciendo clic en el chevron de la esquina superior derecha.

Calculadora de Designcember ejecutándose en modo independiente con la función de superposición de controles de ventana activa. La pantalla deletrea &quot;Google&quot; en el alfabeto de la calculadora.

Una celda solar que funcione

Para lograr el máximo nivel de geek, por supuesto, necesitaba que la célula solar funcionara. La calculadora solo debería funcionar si hay suficiente luz. La forma en que modelé esto es a través de la configuración del opacity de CSS de los dígitos en la pantalla a través de una variable --opacity de CSS que controlo a través de JavaScript.

:root {
  --opacity: 0.75;
}

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

Para detectar si hay suficiente luz disponible para que funcione la calculadora, uso la API de AmbientLightSensor. Para que esta API esté disponible, tuve que establecer la marca #enable-generic-sensor-extra-classes en about:flags y solicitar el permiso 'ambient-light-sensor'. Como antes, uso la mejora progresiva para cargar solo el código relevante cuando la API es compatible.

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

El sensor muestra la luz ambiente en unidades de lux cada vez que hay una lectura nueva disponible. En función de una tabla de valores de situaciones de luz típicas, encontré una fórmula muy simple para convertir el valor de lux en un valor entre 0 y 1 que asigno de forma programática a 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();
  }
})();

En el siguiente video, puedes ver cómo la calculadora comienza a funcionar una vez que enciendo la luz de la habitación lo suficiente. Y ahí lo tienes: una calculadora solar de tipo skeuomorphic que realmente funciona. Mi TI-30X SOLAR, que ha resistido el paso del tiempo, ha recorrido un largo camino.

Demostración

Asegúrate de jugar con la demo de Designcember Calculator y de consultar el código fuente en Glitch. Para instalar la app, debes abrirla en su propia ventana. La versión incorporada que se muestra a continuación no activará la barra de información mini).

¡Feliz Designcember!