Designcember-Rechner

Skeuomorpher Versuch, einen Solarrechner im Web mit der Window Controls Overlay API und der Ambient Light Sensor API nachzubilden

Die Herausforderung

Ich bin ein Kind der 1980er Jahre. Zu meiner Schulzeit kam ich oft auf Solarrechner zurück. Wir erhielten alle von der Schule eine TI-30X-SOLAR und ich kann mich gut daran erinnern, dass wir unsere Rechner miteinander verglichen haben, indem wir den Faktor 69 berechnet haben. Das ist die höchste Zahl, die der TI-30X verarbeiten kann. (Die Geschwindigkeitsabweichung war sehr messbar, ich habe immer noch keine Ahnung, warum.)

Jetzt, fast 28 Jahre später, dachte ich, es wäre eine unterhaltsame Designherausforderung, den Rechner in HTML, CSS und JavaScript neu zu erstellen. Da ich kein großer Designer bin, habe ich nicht bei null angefangen, sondern mit einem CodePen von Sassja Ceballos.

CodePen-Ansicht mit den gestapelten HTML-, CSS- und JS-Feldern auf der linken Seite und der Taschenrechnervorschau auf der rechten Seite.

Installierbar machen

Der Anfang war zwar kein schlechter Anfang, aber ich beschloss, das Ganze für einen skeuomorphen Charakter zu packen. Der erste Schritt bestand darin, daraus eine PWA für die Installation zu machen. Ich habe auf Glitch eine ursprüngliche PWA-Vorlage, die ich für jede kurze Demo neu erstelle. Der Service Worker erhält keinen Programmierpreis und ist definitiv nicht produktionsreif. Es reicht jedoch aus, die Mini-Infoleiste von Chromium auszulösen, damit die App installiert werden kann.

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

Mobilgeräte perfekt aufeinander abstimmen

Da die App nun installierbar ist, besteht der nächste Schritt darin, sie so weit wie möglich in die Apps des Betriebssystems einzupassen. Auf Mobilgeräten kann ich den Anzeigemodus im Web App Manifest auf fullscreen setzen.

{
  "display": "fullscreen"
}

Auf Geräten mit einem Kameraloch oder einer Kerbe kann die App toll aussehen, wenn du den Darstellungsbereich so anpasst, dass der Inhalt den gesamten Bildschirm abdeckt.

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

Designcember Calculator auf einem Pixel 6 Pro im Vollbildmodus.

Kombination mit Desktop-PC

Für den Desktop gibt es eine coole Funktion, die ich verwenden kann: Overlay für Fenstersteuerelemente. Damit kann ich Inhalte in die Titelleiste des App-Fensters einfügen. Der erste Schritt besteht darin, die Fallback-Sequenz für den Anzeigemodus zu überschreiben, damit zuerst window-controls-overlay verwendet wird, wenn diese verfügbar ist.

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

Dadurch verschwindet die Titelleiste praktisch und der Inhalt wandert in den Titelleistenbereich, als ob die Titelleiste nicht vorhanden wäre. Meine Idee ist, die skeuomorphe Solarzelle nach oben in die Titelleiste und den Rest der Benutzeroberfläche des Rechners entsprechend nach unten zu verschieben. Das kann ich mit CSS tun, das die titlebar-area-*-Umgebungsvariablen verwendet. Sie werden feststellen, dass alle Selektoren eine wco-Klasse haben, die ein paar Absätze weiter unten relevant ist.

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

Als Nächstes muss ich entscheiden, welche Elemente ziehbar gemacht werden sollen, da die Titelleiste, die ich normalerweise zum Ziehen verwende, nicht verfügbar ist. Im Stil eines klassischen Widgets kann ich sogar den gesamten Rechner ziehbar machen, indem ich (-webkit-)app-region: drag anwende, außer den Schaltflächen, die (-webkit-)app-region: no-drag erhalten, sodass sie nicht zum Ziehen verwendet werden können.

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

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

Der letzte Schritt besteht darin, die App auf Änderungen des Overlays für Fenstersteuerelemente zu reagieren. Bei einem echten Progressive-Enhancement-Ansatz lade ich den Code für diese Funktion nur dann, wenn der Browser dies unterstützt.

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

Immer wenn sich die Overlay-Geometrie der Fenstersteuerelemente ändert, ändere ich die App so, dass sie so natürlich wie möglich aussieht. Es ist empfehlenswert, dieses Ereignis zu entprellen, da es häufig ausgelöst werden kann, wenn der Nutzer die Größe des Fensters ändert. Ich wende die wco-Klasse auf einige Elemente an, damit mein oben erstelltes CSS aktiviert wird und ich auch die Designfarbe ändere. Ich kann anhand der navigator.windowControlsOverlay.visible-Eigenschaft feststellen, ob das Overlay für Fenstersteuerelemente sichtbar ist.

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();

Jetzt erhalte ich ein Taschenrechner-Widget, das fast wie das klassische Winamp mit einem der traditionellen Winamp-Designs aussieht. Ich kann jetzt den Rechner frei auf meinem Desktop platzieren und die Funktion für Fenstersteuerelemente aktivieren, indem ich auf die spitze Klammer in der oberen rechten Ecke klicke.

Designcember Calculator wird im eigenständigen Modus ausgeführt, wobei die Overlay-Funktion für Fenstersteuerelemente aktiv ist. Auf der Anzeige wird im Rechner „Google“ angezeigt.

Eine funktionierende Solarzelle

Ein echter Geekery ist, dass ich die Solarzelle wirklich zum Laufen bringen musste. Der Rechner sollte nur funktionieren, wenn ausreichend Licht vorhanden ist. Ich habe dies modelliert, indem ich die CSS-opacity der Ziffern auf der Anzeige über die CSS-Variable --opacity festlege, die ich über JavaScript steuere.

:root {
  --opacity: 0.75;
}

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

Ich verwende die AmbientLightSensor API, um festzustellen, ob genügend Licht für den Rechner verfügbar ist. Damit diese API verfügbar ist, musste ich das Flag #enable-generic-sensor-extra-classes in about:flags festlegen und die Berechtigung 'ambient-light-sensor' anfordern. Wie zuvor verwende ich die progressive Optimierung, um den relevanten Code nur dann zu laden, wenn die API unterstützt wird.

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

Der Sensor gibt das Umgebungslicht in Lux-Einheiten zurück, sobald neue Messwerte verfügbar sind. Basierend auf einer Tabelle mit Werten typischer Lichtverhältnisse habe ich eine sehr einfache Formel entwickelt, um den Lux-Wert in einen Wert zwischen 0 und 1 umzuwandeln, den ich dann programmatisch der Variablen --opacity zuweise.

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

Im folgenden Video sehen Sie, wie der Rechner anfängt, wenn ich das Licht im Raum ausreichend hell genug habe. Und das war es auch schon: ein skeuomorpher Solarrechner, der tatsächlich funktioniert. Meine alte bewährte TI-30X-SOLAR-Technologie hat wirklich viel gelernt.

Demo

Probieren Sie auch die Designcember Calculator-Demo aus und sehen Sie sich den Quellcode für Glitch an. Um die App zu installieren, müssen Sie sie in einem eigenen Fenster öffnen. Die folgende eingebettete Version löst die Mini-Infoleiste nicht aus.

Einen schönen Designcember!