Skeuomorficzna próba odtworzenia kalkulatora słonecznego w internecie za pomocą interfejsów Window Controls Overlay API i Ambient Light Sensor API.
Wyzwanie
Jestem dzieckiem lat 80. Kiedy chodziłem do szkoły średniej, hitem były kalkulatory słoneczne. Wszyscy dostaliśmy od szkoły kalkulator TI-30X SOLAR. Miło wspominam, jak porównywaliśmy nasze kalkulatory, obliczając silnię liczby 69, czyli najwyższej liczby, jaką TI-30X mógł obsłużyć. (Różnica w szybkości była bardzo zauważalna, ale nadal nie wiem, dlaczego).
Teraz, prawie 28 lat później, pomyślałem, że odtworzenie kalkulatora w językach HTML, CSS i JavaScript będzie ciekawym wyzwaniem w ramach Designcember. Nie jestem projektantem, więc nie zaczynałem od zera, ale od CodePen autorstwa Sassji Ceballos.

Zapewnij możliwość instalacji
To nie był zły początek, ale postanowiłem go ulepszyć, aby uzyskać pełny efekt skeumorfizmu. Pierwszym krokiem było przekształcenie jej w PWA, aby można było ją zainstalować.
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');
}
})(),
);
});
Połączenie z urządzeniami mobilnymi
Teraz, gdy aplikację można zainstalować, następnym krokiem jest jak najlepsze zintegrowanie jej z aplikacjami systemu operacyjnego. Na urządzeniach mobilnych mogę to zrobić, ustawiając tryb wyświetlania na fullscreen w pliku manifestu aplikacji internetowej.
{
"display": "fullscreen"
}
Na urządzeniach z otworem lub wycięciem na aparat dostosowanie obszaru wyświetlania tak, aby treść pokrywała cały ekran, sprawia, że aplikacja wygląda świetnie.
<meta name="viewport" content="initial-scale=1, viewport-fit=cover" />

Mieszanie z pulpitem
Na komputerze mogę użyć fajnej funkcji: nakładki elementów sterujących okna, która pozwala umieścić treść na pasku tytułu okna aplikacji. Pierwszym krokiem jest zastąpienie sekwencji zastępczej trybu wyświetlania, aby w pierwszej kolejności próbowała użyć window-controls-overlay, gdy jest dostępny.
{
"display_override": ["window-controls-overlay"]
}
Dzięki temu pasek tytułu znika, a treść przesuwa się w górę do obszaru paska tytułu, tak jakby go nie było. Mój pomysł polega na przeniesieniu skeumorficznego ogniwa słonecznego na pasek tytułu i odpowiednim przesunięciu reszty interfejsu kalkulatora w dół. Mogę to zrobić za pomocą kodu CSS, który używa zmiennych środowiskowych titlebar-area-*. Zauważysz, że wszystkie selektory mają klasę wco, która będzie istotna w dalszej części artykułu.
#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);
}
Następnie muszę zdecydować, które elementy mają być przeciągane, ponieważ pasek tytułu, którego zwykle używam do przeciągania, jest niedostępny. W stylu klasycznego widżetu mogę nawet sprawić, że cały kalkulator będzie można przeciągać, stosując (-webkit-)app-region: drag, z wyjątkiem przycisków, które mają (-webkit-)app-region: no-drag, więc nie można ich używać do przeciągania.
#calc_inside.wco,
#calc_solar_cell.wco {
-webkit-app-region: drag;
app-region: drag;
}
button {
-webkit-app-region: no-drag;
app-region: no-drag;
}
Ostatnim krokiem jest sprawienie, aby aplikacja reagowała na zmiany nakładki elementów sterujących oknem. W prawdziwym podejściu opartym na progresywnym ulepszaniu ładuję kod tej funkcji tylko wtedy, gdy przeglądarka ją obsługuje.
if ('windowControlsOverlay' in navigator) {
import('/wco.js');
}
Za każdym razem, gdy zmienia się geometria nakładki elementów sterujących oknem, modyfikuję aplikację, aby wyglądała jak najbardziej naturalnie. Warto zastosować debounce w przypadku tego zdarzenia, ponieważ może ono być wywoływane często, gdy użytkownik zmienia rozmiar okna. Mianowicie stosuję klasę wco do niektórych elementów, dzięki czemu mój kod CSS zaczyna działać, a ja zmieniam kolor motywu. Mogę wykryć, czy nakładka elementów sterujących okna jest widoczna, sprawdzając właściwość 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();
Teraz mam widżet kalkulatora, który wygląda prawie jak klasyczny Winamp z jednym ze starych motywów. Teraz mogę swobodnie umieścić kalkulator na pulpicie i aktywować funkcję sterowania oknem, klikając strzałkę w prawym górnym rogu.

Działające ogniwo słoneczne
Aby uzyskać najlepszy efekt, musiałem oczywiście sprawić, żeby ogniwo słoneczne naprawdę działało. Kalkulator powinien działać tylko wtedy, gdy jest wystarczająco dużo światła. Sposób, w jaki to zamodelowałem, polega na ustawieniu właściwości CSS opacity cyfr na wyświetlaczu za pomocą zmiennej CSS --opacity, którą kontroluję za pomocą JavaScriptu.
:root {
--opacity: 0.75;
}
#calc_expression,
#calc_result {
opacity: var(--opacity);
}
Aby sprawdzić, czy jest wystarczająco dużo światła, aby kalkulator działał, używam interfejsu API AmbientLightSensor. Aby ten interfejs API był dostępny, musiałem ustawić flagę #enable-generic-sensor-extra-classes w about:flags i poprosić o uprawnienie 'ambient-light-sensor'. Podobnie jak wcześniej, używam progresywnego ulepszania, aby wczytywać odpowiedni kod tylko wtedy, gdy interfejs API jest obsługiwany.
if ('AmbientLightSensor' in window) {
import('/als.js');
}
Czujnik zwraca jasność otoczenia w luksach, gdy tylko dostępny jest nowy odczyt. Na podstawie tabeli wartości typowych sytuacji oświetleniowych opracowałem bardzo prostą formułę, która pozwala przekonwertować wartość luksów na wartość z zakresu od 0 do 1. Przypisuję ją programowo do zmiennej --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();
}
})();
Na filmie poniżej widać, jak kalkulator zaczyna działać, gdy włączam wystarczająco jasne światło w pokoju. I oto on: skeumorficzny kalkulator solarny, który naprawdę działa. Mój stary, sprawdzony TI-30X SOLAR przeszedł długą drogę.
Prezentacja
Koniecznie wypróbuj wersję demonstracyjną kalkulatora Designcember i sprawdź kod źródłowy na GitHubie. (Aby zainstalować aplikację, musisz otworzyć ją w osobnym oknie. Wersja umieszczona poniżej nie spowoduje wyświetlenia minipaska informacyjnego).
Wesołego Designcember!