Uma tentativa de recriar uma calculadora solar na Web com a API Window Controls Overlay e a API Sensor de Luz Ambiente.
O desafio
Sou uma criança dos anos 1980. Quando eu estava no ensino médio, as calculadoras solares eram a última moda. Todos nós recebemos uma TI-30X SOLAR da escola, e tenho boas lembranças de nós comparando nossas calculadoras calculando o fatorial de 69, o maior número que a TI-30X conseguia processar. (A variação de velocidade era muito mensurável, mas ainda não sei o motivo.)
Agora, quase 28 anos depois, achei que seria um desafio divertido do Designcember recriar a calculadora em HTML, CSS e JavaScript. Como não sou muito designer, não comecei do zero, mas com um CodePen de Sassja Ceballos.

Tornar o app instalável
Não é um começo ruim, mas decidi dar um toque de realismo para ficar ainda mais incrível. A primeira etapa foi transformá-lo em um PWA para que pudesse 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 integre o máximo possível aos apps do sistema operacional. No
dispositivo móvel, 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 de câmera, ajustar a janela de visualização para que o conteúdo cubra toda a tela deixa o app com uma aparência incrível.
<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />

Mesclagem com o computador
No computador, há um recurso interessante que posso usar:
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 substituição do modo de exibição para que ele 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 move para a área dela como se
ela não estivesse lá. Minha ideia é mover a célula solar esqueumórfica 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-*. Todos os seletores têm uma classe wco,
que será relevante em 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 serão 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 reaja às mudanças na sobreposição de controles de janela. Em uma abordagem de aprimoramento progressivo verdadeira, só carrego o código desse recurso quando o navegador é compatível com ele.
if ('windowControlsOverlay' in navigator) {
import('/wco.js');
}
Sempre que a geometria da sobreposição dos controles de janela muda, modifico o app para que
ele pareça o mais natural possível. É uma boa ideia usar o debounce nesse evento, já que ele pode ser acionado
com frequência quando o usuário redimensiona a janela. Ou seja, aplico a classe wco a alguns elementos para que o
CSS acima seja ativado e também mudo a cor do tema. Posso 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, tenho um widget de calculadora que parece quase o clássico Winamp com um dos temas antigos do Winamp. Agora posso colocar a calculadora livremente na minha área de trabalho e ativar o recurso de controles de janela clicando na seta no canto superior direito.

Uma célula solar que realmente funciona
Para o máximo de nerdice, é claro que eu precisava fazer a célula solar funcionar de verdade. A calculadora
só deve funcionar se houver luz suficiente. Para modelar isso, defini o
opacity de CSS dos dígitos na tela usando uma variável de CSS --opacity que controlo com
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 ficasse 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 é compatível.
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 forma 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 quando eu acendo a luz do ambiente o suficiente. E pronto: uma calculadora solar esqueumórfica que realmente funciona. Minha boa e velha TI-30X SOLAR, testada pelo tempo, realmente percorreu um longo caminho.
Demonstração
Não deixe de testar a demonstração da calculadora do Designcember e conferir o código-fonte no GitHub. Para instalar o app, abra-o em uma janela própria. A versão incorporada abaixo não aciona a minibarra de informações.)
Feliz Designcember!