Uma tentativa esqueuomórfica de recriar uma calculadora solar na Web com a API Window Controls Overlay e a API Ambient Light Sensor.
O desafio
Sou uma criança dos anos 1980. Quando eu estava no ensino médio, as calculadoras solares eram a sensação do momento. A escola deu uma TI-30X SOLAR para cada um de nós, e tenho boas lembranças de quando comparávamos nossas calculadoras calculando o fatorial de 69, o maior número que a TI-30X conseguia processar. A variância de velocidade era muito mensurável, e ainda não sei por quê.
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.
Facilite a instalação
Embora não seja um mau começo, decidi aumentar a dose para uma beleza esceumórfica completa. A primeira etapa foi transformar o site em um PWA para que ele 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');
}
})(),
);
});
Fusã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 com os apps do sistema operacional. Em dispositivos móveis, isso pode ser feito 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 lindo.
<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />
Integração com o computador
No computador, há um recurso interessante que posso usar: a sobreposição de controles de janelas, 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 suba para a área da barra de título como se
ela não estivesse lá. Minha ideia é mover a célula solar esqueuomó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-*
. 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 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 não serem 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 é tornar o app reativo às mudanças na sobreposição de controles de janelas. Em uma abordagem de melhoria progressiva, 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 de controles da 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. Ou seja, aplico a classe wco
a alguns elementos para que meu
CSS acima seja ativado, 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 configurado, recebo um widget de calculadora que parece quase o clássico Winamp com um dos temas Winamp da velha guarda. Agora posso colocar a calculadora livremente na minha área de trabalho e ativar o recurso de controles de janela clicando no chevron no canto superior direito.
Uma célula solar que funciona de verdade
Para o máximo de geekery, eu precisava fazer a célula solar funcionar de verdade. A calculadora
só funciona se houver luz suficiente. Modelei isso definindo o
CSS opacity
dos dígitos no display usando uma variável CSS --opacity
que 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 esteja 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 típicas de iluminação, 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 assim que eu acendo a luz do ambiente o suficiente. E pronto: uma calculadora solar esqueuomórfica que funciona de verdade. Minha boa e velha TI-30X SOLAR, testada pelo tempo, já passou por muita coisa.
Demonstração
Não deixe de testar a demonstração da calculadora do Designcember e confira o código-fonte no GitHub. Para instalar o app, abra-o em uma janela separada. A versão incorporada abaixo não vai acionar a mini barra de informações.)
Feliz Designcember!