Designcember 计算器

利用 Window Controls Overlay API 和 Ambient Light Sensor API 在网络上重新创建太阳能计算器的拟态尝试。

挑战

我是 20 世纪 80 年代的孩子。上高中时,太阳能计算器是一度盛行的事物。我们都获得了学校提供的 TI-30X 太阳能 (TI-30X SOLAR),并且我还记得我们通过计算 69 的阶乘(TI-30X 可以处理的最高数字)来对照基准计算我们的计算器。(速度差异非常可衡量,我仍不清楚原因)。

近 28 年过去了,我想在 Designcember 中,用 HTML、CSS 和 JavaScript 重新创建该计算器将是一项有趣的挑战。我不是很了解设计师,也没有从头开始,而是用了 Sassja CeballosCodePen

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

使其可安装

尽管开局还不错,但我决定加大力度,展现完全的拟态魅力。第一步是将其设为 PWA,以便用户可以安装它。我在 Glitch 上维护了一个基准 PWA 模板,只要我需要快速演示,我就会重新合成该模板。其 Service Worker 不会让您赢得任何编码奖项,而且它肯定做好生产准备,但足以触发 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');
      }
    })(),
  );
});

与移动平台融为一体

既然应用现在是可以安装的,下一步就是使其尽可能与操作系统应用融为一体。在移动设备上,我可以在 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);
}

接下来,我需要决定将哪些元素设为可拖动,因为我通常用于拖动的标题栏不可用。在经典 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 主题类似。现在,我可以将计算器随意地放在桌面上,并点击右上角的 V 形图标激活窗口控件功能。

在独立模式下运行的 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 太阳能发电技术确实取得了长足的进步。

演示

请务必玩转 Designcember 计算器演示,并查看 Glitch 上的源代码。(如需安装应用,您需要在应用自己的窗口中打开它。下面的嵌入式版本不会触发迷你信息栏。)

设计人员快乐!