Designcember 计算器

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

挑战

我是 20 世纪 80 年代的孩子。在我上高中时,太阳能计算器风靡一时。学校给我们每人发了一个 TI-30X SOLAR,我至今还记得我们当时互相比较计算器的性能,计算 69 的阶乘,这是 TI-30X 能处理的最大数字。(速度差异非常明显,但我仍然不知道原因。)

现在,将近 28 年后,我想用 HTML、CSS 和 JavaScript 重建这个计算器,作为 Designcember 的一项有趣挑战。我不太擅长设计,因此没有从头开始,而是从 Sassja CeballosCodePen 开始。

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

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

与桌面设备融合

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

现在,一切就绪,我获得了一个计算器 widget,感觉几乎就像经典的 Winamp 搭配了旧式 Winamp 主题。现在,我可以随意将计算器放置在桌面上,并通过点击右上角的箭头来激活窗口控制功能。

在独立模式下运行的 Designcember 计算器,并启用了“窗口控件叠加层”功能。显示屏上会显示计算器字母表中的“Google”。

实际可用的太阳能电池

为了追求极致的极客精神,我当然需要让太阳能电池板实际发挥作用。只有在光线充足的情况下,计算器才能正常运行。我通过 CSS 变量 --opacity(通过 JavaScript 控制)设置显示屏上数字的 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 计算器演示,并查看 GitHub 上的源代码。(如需安装该应用,您需要在自己的窗口中打开该应用。以下嵌入版本不会触发迷你信息栏。)

Designcember 快乐!