Designcember 计算器

尝试使用 Window Controls Overlay API 和环境光传感器 API 在 Web 上重新创建太阳能计算器。

挑战

我是 20 世纪 80 年代出生的。我上中学时,太阳能计算器风靡一时。学校给我们每个人都发了一个 TI-30X SOLAR,我至今还清晰地记得,我们通过计算 69 的阶乘(TI-30X 可以处理的最大数字)来比较各自计算器的性能。(速度差异非常明显,我仍然不知道原因。)

现在,大约 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');
      }
    })(),
  );
});

与移动设备融合

现在,应用已可安装,下一步是让它尽可能与操作系统应用融为一体。在移动设备上,我可以通过在 Web 应用清单中将显示模式设置为 fullscreen 来实现此目的。

{
  "display": "fullscreen"
}

在具有摄像头孔或刘海屏的设备上,调整视口使内容覆盖整个屏幕,可让应用看起来更美观。

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

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

与桌面设备融合

在桌面设备上,我可以使用一个很酷的功能:窗口控件叠加层,它允许我将内容放在应用窗口的标题栏中。第一步是替换显示模式回退序列,以便在 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);
}

接下来,我需要决定哪些元素可以拖动,因为我通常用于拖动的标题栏不可用。为了打造经典微件风格,我甚至可以通过应用 (-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,带有其中一个老派的 Winamp 主题。现在,我可以将计算器随意放置在桌面上,并通过点击右上角的 V 形图标来激活窗口控件功能。

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 计算器演示,并查看 GitHub 上的源代码。(如需安装该应用,您需要在其自己的窗口中打开它。下面的嵌入式版本不会触发迷你信息栏。)

Designcember 快乐!