Designcember の電卓

Window Controls Overlay API と 周囲光センサー API を使用して、ウェブ上でソーラー電卓を再現するスキューモーフィックな試み。

課題

私は 1980 年代に子供時代を過ごしました。私が高等学校生の頃に大流行したのが、ソーラー電卓です。私たちは全員、学校から TI-30X SOLAR を支給されました。TI-30X で処理できる最大数である 69 の階乗を計算して、電卓のベンチマークを競い合ったのは良い思い出です。(速度のばらつきは非常に測定可能でしたが、理由はまだわかりません)。

それから 28 年近く経った今、HTML、CSS、JavaScript で電卓を再現することは、Designcember の楽しいチャレンジになると思いました。私はデザイナーではないので、ゼロから始めるのではなく、Sassja Ceballos 氏の CodePen を使用しました。

CodePen ビュー。左側に HTML、CSS、JS のパネルが積み重ねられ、右側に電卓のプレビューが表示されている。

インストール可能にする

悪くないスタートですが、さらにスキューモーフィズムを追求することにしました。最初のステップは、インストールできるように PWA にすることでした。

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

モバイルとのブレンド

アプリをインストール可能にしたら、次のステップは、アプリをオペレーティング システムのアプリにできるだけ溶け込ませることです。モバイルでは、ウェブアプリ マニフェストで表示モードを fullscreen に設定することで、これを実現できます。

{
  "display": "fullscreen"
}

カメラホールやノッチがあるデバイスでは、コンテンツが画面全体を覆うようにビューポートを調整すると、アプリの見栄えが良くなります。

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

Google Pixel 6 Pro で全画面表示されている Designcember 電卓。

デスクトップとの融合

デスクトップには、ウィンドウ コントロール オーバーレイという便利な機能があります。これを使用すると、アプリ ウィンドウのタイトルバーにコンテンツを配置できます。最初のステップは、ディスプレイ モードのフォールバック シーケンスをオーバーライドして、window-controls-overlay が利用可能な場合は最初に使用を試みるようにすることです。

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

これにより、タイトルバーが実質的に消え、タイトルバーがないかのようにコンテンツがタイトルバー領域に移動します。そこで、スキューモーフィックな太陽電池をタイトルバーに移動し、電卓 UI の残りの部分をそれに合わせて下に移動することにしました。これは、titlebar-area-* 環境変数を使用する CSS で実現できます。すべてのセレクタに wco クラスが含まれていることがわかります。これについては、数段落下で説明します。

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

次に、ドラッグ可能にする要素を決定する必要があります。通常はドラッグに使用するタイトルバーが利用できないためです。従来のウィジェットのスタイルでは、(-webkit-)app-region: drag を適用することで、電卓全体をドラッグ可能にすることもできます。ただし、ボタンは (-webkit-)app-region: no-drag を取得するため、ドラッグには使用できません。

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

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

最後のステップは、ウィンドウ コントロール オーバーレイの変更にアプリが反応するようにすることです。真のプログレッシブ エンハンスメント アプローチでは、ブラウザがサポートしている場合にのみ、この機能のコードを読み込みます。

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

ウィンドウ コントロール オーバーレイのジオメトリが変更されるたびに、アプリをできるだけ自然に見えるように変更します。ユーザーがウィンドウのサイズを変更すると、このイベントが頻繁にトリガーされる可能性があるため、このイベントをデバウンスすることをおすすめします。つまり、一部の要素に wco クラスを適用して、上記の CSS が有効になるようにし、テーマの色も変更します。ウィンドウ コントロール オーバーレイが表示されているかどうかは、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();

これで、昔ながらの Winamp テーマの 1 つを使用した、クラシックな Winamp のような電卓ウィジェットが完成しました。これで、電卓をデスクトップに自由に配置し、右上隅のシェブロンをクリックしてウィンドウ コントロール機能を有効にできるようになりました。

ウィンドウ コントロール オーバーレイ機能が有効なスタンドアロン モードで動作する Designcember Calculator。ディスプレイに、電卓のアルファベットで「Google」と表示されます。

実際に動作する太陽電池

究極のオタクとして、もちろん太陽電池を実際に機能させる必要がありました。電卓は、十分な光がある場合にのみ機能します。このモデル化は、JavaScript で制御する CSS 変数 --opacity を使用して、ディスプレイ上の数字の CSS opacity を設定することで行いました。

:root {
  --opacity: 0.75;
}

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

電卓が動作するのに十分な光があるかどうかを検出するには、AmbientLightSensor API を使用します。この API を使用できるようにするには、about:flags で #enable-generic-sensor-extra-classes フラグを設定し、'ambient-light-sensor' 権限をリクエストする必要がありました。以前と同様に、プログレッシブ エンハンスメントを使用して、API がサポートされている場合にのみ関連するコードを読み込みます。

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

このセンサーは、新しい読み取り値が利用可能になるたびに、周囲光をルクス単位で返します。一般的な光の状況の値の表に基づいて、ルクス値を 0 ~ 1 の値に変換する非常にシンプルな数式を考案し、プログラムで --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();
  }
})();

下の動画では、部屋の照明を十分に明るくすると電卓が動作し始める様子を確認できます。これで、実際に動作するスキューモーフィックなソーラー電卓が完成しました。長年愛用してきた TI-30X SOLAR は、本当に長い道のりを歩んできました。

デモ

Designcember Calculator のデモを試し、GitHub のソースコードを確認してください。(アプリをインストールするには、アプリを専用のウィンドウで開く必要があります。以下の埋め込みバージョンでは、ミニ情報バーはトリガーされません)。

Designcember をお楽しみください。