Designcember 计算器

尝试使用 Window Controls Overlay API 和 Ambient Light Sensor API 在 Web 上重新创建太阳能计算器的拟物化界面。

挑战

我是 80 年代的孩子。我上高中时,太阳能计算器很流行。学校给我们每人发了一台 TI-30X SOLAR,我至今仍记得我们计算 69 的阶乘(这是 TI-30X 能处理的最大数字)时,彼此对比计算器性能的愉快时光。(速度差异非常明显,但我仍不知道原因。)

现在,距离那次经历已经过去了将近 28 年,我认为在 HTML、CSS 和 JavaScript 中重新创建计算器会是一项有趣的 Designcember 挑战。我不是设计师,因此并未从头开始,而是使用了 Sassja CeballosCodePen

CodePen 视图,左侧堆叠了 HTML、CSS 和 JS 面板,右侧显示了计算器预览。

使其可安装

虽然这是一个不错的开端,但我决定再加把劲,让它成为一款出色的拟物化应用。第一步是将其转换为 PWA,以便用户可以安装。我在 Glitch 上维护一个基准 PWA 模板,每当需要快速演示时,我都会对其进行混剪。该服务工作器不会为您赢得任何编码奖项,而且绝对适合正式版,但足以触发 Chromium 的迷你信息栏,以便用户安装应用。

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" />

Designcember 计算器在 Pixel 6 Pro 手机上全屏运行。

与桌面设备融为一体

在桌面设备上,我可以使用一项很酷的功能:窗口控件叠加层,它允许我在应用窗口的标题栏中放置内容。第一步是替换显示模式回退序列,以便在 window-controls-overlay 可用时先尝试使用 window-controls-overlay

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

这样一来,标题栏就会有效地消失,内容会向上移动到标题栏区域,就像没有标题栏一样。我的想法是将拟物太阳能电池向上移动到标题栏,并相应地向下移动计算器界面的其余部分,我可以使用一些使用 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);
}

接下来,我需要决定要将哪些元素设为可拖动,因为我通常用于拖动的标题栏不可用。在传统 widget 的样式中,除了按钮(会获得 (-webkit-)app-region: no-drag,因此无法用于拖动)之外,我甚至可以通过应用 (-webkit-)app-region: 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,并采用了某个老式 Winamp 主题。现在,我可以将计算器自由放置在桌面上,并通过点击右上角的箭头来激活窗口控制功能。

Designcember 计算器在独立模式下运行,窗口控件叠加层功能处于启用状态。显示屏会用计算器字母拼写“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');
}

每当有新读数可用时,传感器都会返回环境光强度(以 lux 为单位)。根据典型光照情况的值表,我设计了一个非常简单的公式,用于将勒克斯值转换为 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 计算器演示,并查看 Glitch 上的源代码。(如需安装该应用,您需要在其自己的窗口中打开该应用。以下嵌入版本不会触发迷你信息栏。)

Designcember 快乐!