Designcember-Rechner

Ein skeuomorpher Versuch, einen Sonnenrechner im Web mit der Window Controls Overlay API und der Ambient Light Sensor API nachzubilden.

Die Herausforderung

Ich bin ein Kind der 1980er-Jahre. Als ich zur Schule ging, waren Solarrechner der letzte Schrei. Die Schule stellte uns allen einen TI-30X SOLAR zur Verfügung. Ich erinnere mich gern daran, wie wir unsere Taschenrechner miteinander verglichen haben, indem wir das Fakultätenpotenz von 69 berechneten, die höchste Zahl, die der TI-30X verarbeiten konnte. (Die Geschwindigkeitsschwankungen waren sehr messbar, ich weiß aber immer noch nicht, warum.)

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

CodePen-Ansicht mit gestapelten HTML-, CSS- und JS-Bereichen auf der linken Seite und der Vorschau des Rechners auf der rechten Seite

Installation ermöglichen

Das war zwar kein schlechter Anfang, aber ich habe mich entschieden, das Ganze noch skeuomorphischer zu gestalten. Der erste Schritt bestand darin, die Website in eine PWA umzuwandeln, damit sie installiert werden konnte. Ich pflege eine Grundlegende PWA-Vorlage auf Glitch, die ich remixe, wenn ich eine schnelle Demo brauche. Der Service Worker wird Ihnen keinen Programmierpreis einbringen und ist definitiv nicht für die Produktion bereit. Er reicht jedoch aus, um die Mini-Infobar 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');
      }
    })(),
  );
});

Mit Mobilgeräten verschmelzen

Nachdem die App installiert werden kann, besteht der nächste Schritt darin, sie so weit wie möglich in die Betriebssystem-Apps einzubinden. Auf Mobilgeräten kann ich das tun, indem ich im Web-App-Manifest den Anzeigemodus auf fullscreen setze.

{
  "display": "fullscreen"
}

Auf Geräten mit Kameraloch oder Notch können Sie den Viewport so anpassen, dass die Inhalte das gesamte Display ausfüllen.

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

Der Designcember-Rechner wird auf einem Google Pixel 6 Pro im Vollbildmodus ausgeführt.

Mit dem Desktop verschmelzen

Auf dem Computer gibt es eine coole Funktion, die ich verwenden kann: das Einblenden von Fenstersteuerelementen. Damit kann ich Inhalte in die Titelleiste des App-Fensters einfügen. Im ersten Schritt überschreiben Sie die Fallback-Sequenz für den Anzeigemodus, damit zuerst window-controls-overlay verwendet wird, wenn es verfügbar ist.

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

Dadurch wird die Titelleiste effektiv ausgeblendet und der Inhalt wird in den Bereich der Titelleiste verschoben, als wäre die Titelleiste nicht vorhanden. Ich möchte die skeuomorphe Solarzelle in die Titelleiste verschieben und den Rest der Benutzeroberfläche des Rechners entsprechend nach unten. Das kann ich mit CSS tun, das die Umgebungsvariablen titlebar-area-* verwendet. Sie werden feststellen, dass alle Auswahlschaltflächen eine wco-Klasse haben, die einige Absätze weiter unten relevant wird.

#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 ich verschiebbar machen möchte, da die Titelleiste, die ich normalerweise zum Ziehen verwenden würde, nicht verfügbar ist. Im Stil eines klassischen Widgets kann ich sogar den gesamten Rechner per Drag-and-drop verschieben, indem ich (-webkit-)app-region: drag anwende, mit Ausnahme der Schaltflächen, die (-webkit-)app-region: no-drag erhalten, damit sie nicht verschoben 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;
}

Im letzten Schritt sorgen Sie dafür, dass die App auf Änderungen am Overlay der Fenstersteuerung reagiert. Bei einer echten progressiven Verbesserung wird der Code für diese Funktion nur geladen, wenn der Browser sie unterstützt.

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

Jedes Mal, wenn sich die Geometrie des Fensterkontroll-Overlays ändert, passe ich die App so an, dass sie so natürlich wie möglich aussieht. Es empfiehlt sich, dieses Ereignis zu dämpfen, da es häufig ausgelöst werden kann, wenn der Nutzer die Größe des Fensters ändert. Ich wende die Klasse wco auf einige Elemente an, damit mein CSS-Code oben angewendet wird. Außerdem ändere ich die Designfarbe. Ich kann anhand der Eigenschaft navigator.windowControlsOverlay.visible erkennen, ob das Overlay für die Fenstersteuerung 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 habe ich ein Rechenmaschinen-Widget, das fast wie das klassische Winamp mit einem der alten Winamp-Designs aussieht. Ich kann den Rechner jetzt frei auf dem Desktop platzieren und die Fenstersteuerung aktivieren, indem ich oben rechts auf den Dreipunkt-Pfeil klicke.

Der Designcember-Rechner wird im eigenständigen Modus ausgeführt und die Funktion „Fenstersteuerungs-Overlay“ ist aktiviert. Auf dem Display wird „Google“ im Rechneralphabet angezeigt.

Eine tatsächlich funktionierende Solarzelle

Für den ultimativen Nerd-Faktor musste die Solarzelle natürlich auch funktionieren. Der Rechner sollte nur bei ausreichender Beleuchtung funktionieren. Ich habe das so modelliert, dass ich das CSS opacity der Ziffern auf dem Display über eine CSS-Variable --opacity festlege, die ich über JavaScript steuere.

:root {
  --opacity: 0.75;
}

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

Um zu erkennen, ob genügend Licht für die Funktion des Rechners vorhanden ist, verwende ich die AmbientLightSensor API. 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 Verbesserung, um nur den relevanten Code zu laden, wenn die API unterstützt wird.

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

Der Sensor gibt das Umgebungslicht in Lux zurück, sobald ein neuer Messwert verfügbar ist. Anhand einer Tabelle mit Werten für typische Lichtsituationen habe ich eine sehr einfache Formel entwickelt, um den Lux-Wert in einen Wert zwischen 0 und 1 umzuwandeln, den ich der Variablen --opacity programmatisch 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 Video unten sehen Sie, wie der Rechner funktioniert, sobald ich das Raumlicht ausreichend hell einschalte. Und das ist es: ein skeuomorpher Solarrechner, der tatsächlich funktioniert. Mein guter alter, bewährter TI-30X SOLAR hat es in der Tat weit gebracht.

Demo

Sehen Sie sich die Designcember-Rechner-Demo und den Quellcode auf Glitch an. (Zum Installieren der App müssen Sie sie in einem eigenen Fenster öffnen. In der eingebetteten Version unten wird die Mini-Infobar nicht ausgelöst.)

Viel Spaß beim Designcember!