Calculadora Designcember

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

O desafio

Sou criança dos anos 1980. Uma coisa que foi toda a raiva quando eu estava no ensino médio são as calculadoras solares. Todos nós recebemos um TI-30X SOLAR da escola, e eu tenho boas memórias de quando comparamos nossas calculadoras calculando o fator 69, o número mais alto que o TI-30X consegue processar. (A variação de velocidade foi muito mensurável, ainda não tenho ideia do motivo.)

Agora, quase 28 anos depois, pensei que seria um desafio divertido da DesignCember recriar a calculadora em HTML, CSS e JavaScript. Não sendo um designer muito útil, não comecei do zero, mas com um CodePen de Sassja Ceballos (links em inglês).

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

Torne-o instalável

Embora não tenha sido um mau começo, decidi aumentar o ritmo para ter uma incrível incrível imaginação esquemórfica. A primeira etapa foi torná-lo um PWA para que pudesse ser instalado. Eu mantenho um modelo de PWA de referência no Glitch que eu remixo sempre que preciso de uma demonstração rápida. O service worker não vai ganhar nenhum prêmio de programação e definitivamente 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 furo ou entalhe para a câmera, ajustar a janela de visualização para que o conteúdo cubra toda a tela deixa o app bonito.

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

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

Mesclagem com o computador

No computador, há um recurso legal que posso usar: a sobreposição dos 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 substituição 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 esqueomórfica para cima na barra de título e o restante da interface da calculadora para baixo, o que posso fazer com alguns CSS que usam 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 depois.

#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 devo 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é mesmo 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 reative as mudanças na sobreposição dos controles da janela. Em uma abordagem de aprimoramento progressivo real, carrego o código desse recurso apenas quando o navegador oferece suporte a ele.

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

Sempre que a janela controla a geometria da sobreposição, modifico o app para que ele pareça o mais natural possível. É uma boa ideia retardar esse evento, já que ele pode ser acionado com frequência quando o usuário redimensiona a janela. Ou seja, eu aplico a classe wco a alguns elementos para que o CSS acima seja iniciado e também mudo a cor do tema. Posso detectar se a sobreposição de controles da 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 definido, tenho um widget de calculadora que parece com o Winamp clássico, com um dos antigos temas Winamp. Agora posso colocar a calculadora livremente na minha área de trabalho e ativar o recurso de controles da janela clicando na divisa no canto superior direito.

a Calculadora da Designcember em execução no modo autônomo com o recurso de sobreposição de controles da janela ativo. A exibição escreve &quot;Google&quot; no alfabeto da calculadora.

Uma célula solar em funcionamento

Sei que é preciso fazer a célula solar funcionar de verdade. A calculadora só vai funcionar se houver luz suficiente. Modelei isso definindo o CSS opacity dos dígitos na tela por uma variável CSS --opacity que eu controlo via JavaScript.

:root {
  --opacity: 0.75;
}

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

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

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 iluminação típicas, criei uma fórmula muito simples para converter o valor de lux em um valor entre 0 e 1 que atribuo de maneira programática à 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 depois que aceno a luz da sala o suficiente. E aí está: uma calculadora solar esqueomórfica que de fato funciona. Meu bom e velho TI-30X SOLAR já percorreu um longo caminho, de fato.

Demonstração

Teste a demonstração da Calculadora da Designcember e confira o código-fonte no Glitch. Para instalar o app, abra-o em uma janela própria. a versão incorporada abaixo não acionará a minibarra de informações.

Feliz Designcember!