Calculadora Designcember

Uma tentativa de recriar uma calculadora solar na Web com a API Window Controls Overlay e a API Ambient Light Sensor.

O desafio

Cresci nos anos 80. Quando eu estava no ensino médio, as calculadoras solares eram a sensação. A escola nos deu uma TI-30X SOLAR, e tenho boas lembranças de comparar nossas calculadoras calculando o fatorial de 69, o número mais alto que a TI-30X conseguia processar. A variação de velocidade era muito mensurável, e ainda não sei por quê.

Agora, quase 28 anos depois, pensei que seria um desafio divertido do Designcember recriar a calculadora em HTML, CSS e JavaScript. Como não sou designer, não comecei do zero, mas com um CodePen da Sassja Ceballos.

Visualização do CodePen com painéis HTML, CSS e JS empilhados à esquerda e a prévia da calculadora à direita.

Tornar o app instalável

Embora não fosse um começo ruim, decidi aumentar o nível para uma experiência eskeuomorfa completa. A primeira etapa foi transformá-lo em um PWA para que ele pudesse ser instalado. Eu mantenho um modelo de PWA de referência no Glitch que remixo sempre que preciso de uma demonstração rápida. O service worker não vai ganhar nenhum prêmio de programação e não está pronto para produção, mas é suficiente para acionar a minibarra de informações do Chromium para que o app possa ser instalado.

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

Combinação com dispositivos móveis

Agora que o app pode ser instalado, a próxima etapa é fazer com que ele se misture o máximo possível aos apps do sistema operacional. Em dispositivos móveis, posso fazer isso definindo o modo de exibição como fullscreen no manifesto do app da Web.

{
  "display": "fullscreen"
}

Em dispositivos com um orifício ou entalhe para a câmera, ajuste a viewport para que o conteúdo cubra toda a tela e deixe o app com uma aparência incrível.

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

Calculadora do Designcember em tela cheia em um smartphone Pixel 6 Pro.

Mesclagem com o computador

No computador, há um recurso interessante que posso usar: a sobreposição de controles de janela, que permite colocar conteúdo na barra de título da janela do app. A primeira etapa é substituir a sequência de fallback do modo de exibição para que ela tente usar window-controls-overlay primeiro quando estiver disponível.

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

Isso faz com que a barra de título desapareça e o conteúdo se mova para a área da barra de título como se ela não estivesse lá. Minha ideia é mover a célula solar skeuomorphic para a barra de título e o restante da interface da calculadora para baixo, o que posso fazer com um CSS que usa as variáveis de ambiente titlebar-area-*. Você vai notar que todos os seletores têm uma classe wco, que será relevante alguns parágrafos abaixo.

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

Em seguida, preciso decidir quais elementos tornar arrastáveis, já que a barra de título que eu normalmente usaria para arrastar não está disponível. No estilo de um widget clássico, posso até tornar toda a calculadora arrastável aplicando (-webkit-)app-region: drag, exceto os botões, que recebem (-webkit-)app-region: no-drag para que não possam ser usados para arrastar.

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

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

A etapa final é fazer com que o app reaja às mudanças na sobreposição de controles de janela. Em uma abordagem de aprimoramento progressivo real, só carrego o código para esse recurso quando o navegador oferece suporte a ele.

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

Sempre que a geometria de sobreposição dos controles de janela muda, eu modifico o app para que ele pareça o mais natural possível. É recomendável usar o debounce nesse evento, já que ele pode ser acionado com frequência quando o usuário redimensiona a janela. Especificamente, aplico a classe wco a alguns elementos para que o CSS acima seja acionado e também mudo a cor do tema. É possível detectar se a sobreposição de controles de janela está visível verificando a propriedade 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();

Agora, com tudo isso em vigor, tenho um widget de calculadora que parece quase o clássico Winamp com um dos temas clássicos. Agora posso colocar a calculadora na área de trabalho e ativar o recurso de controles de janela clicando no triângulo no canto superior direito.

Calculadora Designcember em execução no modo independente com o recurso de sobreposição de controles de janela ativo. A tela mostra &quot;Google&quot; no alfabeto da calculadora.

Uma célula solar que funciona

Para a maior parte da experiência, é claro, eu precisava fazer a célula solar funcionar. A calculadora só vai funcionar se houver luz suficiente. A maneira como eu modelei isso foi definindo o CSS opacity dos dígitos na tela usando uma variável CSS --opacity que controlo pelo JavaScript.

:root {
  --opacity: 0.75;
}

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

Para detectar se há luz suficiente para que a calculadora funcione, uso a API AmbientLightSensor. Para que essa API estivesse disponível, precisei definir a flag #enable-generic-sensor-extra-classes em about:flags e solicitar a permissão 'ambient-light-sensor'. Como antes, uso o aprimoramento progressivo para carregar apenas o código relevante quando a API tem suporte.

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

O sensor retorna a luz ambiente em unidades de lux sempre que uma nova leitura está disponível. Com base em uma tabela de valores de situações de luz típicas, criei uma fórmula muito simples para converter o valor de lux em um valor entre 0 e 1 que atribuo programaticamente à variável --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();
  }
})();

No vídeo abaixo, você pode ver como a calculadora começa a funcionar quando a sala fica suficientemente iluminada. E aí está: uma calculadora solar skeuomorfológica que realmente funciona. Minha boa e velha TI-30X SOLAR, que já foi testada pelo tempo, percorreu um longo caminho.

Demonstração

Teste a demonstração da calculadora Designcember e confira o código-fonte no Glitch. Para instalar o app, é necessário abri-lo na própria janela. A versão incorporada abaixo não aciona a minibarra de informações.)

Feliz Designcember!