使用 Temporal 和 CSS color-mix 实现基于时间的背景颜色过渡
摘要
本文演示了如何使用 Temporal API 检测本地时间,并结合 CSS color-mix 在个人网站上实现根据一天中的时间变化背景颜色过渡的效果。
<p><a href="https://lobste.rs/s/hzurnm/time_based_background_colour">评论</a></p>
查看缓存全文
缓存时间: 2026/07/05 20:34
# 基于时间的背景颜色过渡:Temporal 与 CSS color-mix 的实践 - localghost
来源:https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/
我给我的网站做了点小更新!桌面版布局略有调整,还去掉了`etc`页面,并改版了[链接页](https://localghost.dev/links)(现由 [raindrop.io](https://raindrop.io/) 驱动)。
[极简主题](https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/?theme=minimalist)依然极简,但更精致了;
[蒸汽波主题](https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/?theme=vaporwave)的导航栏焕然一新,配上了可爱的小图标。
但最大的变化要数[城市主题](https://localghost.dev/blog/time-based-background-colour-transitions-with-temporal-and-css-color-mix/?theme=city)——之前它是个星空暗色模式主题。
如果你在晚上9点到凌晨5点之间读到这篇文章,可能会觉得没什么大不了——它看起来和以前差不多。
那是因为主题会随一天中的时间变化!
你可以通过右上角主题切换器后面的时间选择器来手动调整时间。
我将会话选择保存在 `sessionStorage` 中,这样切换页面时就不会被突然的亮色模式吓到;但下次再访问时,它会重置为“现在”。
我原本只是想用柔和的 lo-fi 美学来设计布局,但后来意识到:a) 我确实需要*某种*暗色模式;b) 我会想念星星的!
于是我想……为什么不能两者兼得?又何必只限于白天和黑夜呢?
(致敬 [Alistair Shepherd](https://alistairshepherd.uk/),他用美丽的 Firewatch 风格网站做了一些类似的事情。)
接着我想起了 Temporal API 在 Chrome 和 Firefox 中已是实验性可用,而我正好在找机会尝试它。
## 介绍 Temporal
对于不了解的人,Temporal 是 JavaScript 中那个客观糟糕的 Date API 的解决方案。Date 基于 Java 的 Date 库,后者同样糟糕且早已被弃用。`Date` 实例显示当地时间还是 UTC 时间完全取决于你用什么函数来展示,这点特别令人困惑;而日期操作又非常繁琐,以至于大多数人都求助于像 `date-fns` 或 `luxon` 这样的第三方库。
Temporal 极大地简化了 API,并引入了一些新概念:
- `PlainDateTime`:不带时区的日期和时间
- `PlainDate`:不带时间信息和时区的日期
- `PlainTime`:不带日期信息和时区的时间
- `ZonedDateTime`:指定时区的日期和时间
本项目中使用 `PlainTime` 非常合适,因为我们并不关心具体是哪一天——只知道几点了,以便决定显示什么颜色。
### 获取用户的本地时间
首先要根据用户浏览器确定时间。`Temporal.Now` 命名空间([MDN 文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Now))提供了多种与当前时间交互的方法,包括 `plainTimeISO()`,默认返回本地时间的 `PlainTime`。(你也可以传入时区来获得带时区的时间。)
```js
const timeNow = Temporal.Now.plainTimeISO();
```
现在我们需要知道何时显示不同的颜色。
## 定义阶段
一天被分为四个阶段:日出、白天、日落和夜晚。白天和夜晚各持续 11.5 小时,而日出和日落各持续 90 分钟。页面背景采用双色渐变:
```css
--background: fixed linear-gradient(var(--bg-gradient-top), var(--bg-gradient-mid) 80%);
```
页脚还有一个由 `transparent okhch(0 0 0 / 0)` 到所选第三种颜色的线性渐变生成的额外颜色:
```css
background: linear-gradient(oklch(0 0 0 / 0) 40%, var(--bg-gradient-bottom));
```
这意味着最后一种颜色会贴在页面底部,而不是横跨整个视口高度(即使指定了百分比,渐变也很难控制)。这也带来了更多的发光感,看起来就像日出/日落或城市的光芒——我非常喜欢。
我定义了阶段和颜色的对象:
```js
const stages = {
sunrise: {
start: Temporal.PlainTime.from("06:30:00"),
next: "day",
color1: "oklch(0.618 0.3157 265.76)",
color2: "oklch(0.8867 0.1222 328.24)",
color3: "oklch(0.9529 0.1222 106.94)",
},
day: {
start: Temporal.PlainTime.from("08:00:00"),
next: "sunset",
color1: "oklch(58% 0.15433 300)",
color2: "oklch(85% 0.22133 302)",
color3: "oklch(98 0.22133 302)",
},
sunset: {
start: Temporal.PlainTime.from("19:30:00"),
next: "night",
color1: "oklch(0.6933 0.1899 297.53)",
color2: "oklch(75.504% 0.24612 357.26)",
color3: "oklch(88.591% 0.1422 62.595)",
},
night: {
start: Temporal.PlainTime.from("21:00:00"),
next: "sunrise",
color1: "oklch(25.27% 0.0919 276.73)",
color2: "oklch(47.35% 0.284 283.78)",
color3: "oklch(62.831% 0.23521 310.291)",
},
};
```
CSS 自定义属性通过 JS 设置很容易——可以用 `root.style.setProperty`:
```js
root.style.setProperty(
"--bg-gradient-top",
"oklch(25.27% 0.0919 276.73)",
);
```
与 `Date` 不同,我们不需要做任何体操来比较 Temporal 实例:每种实例都有一个 `compare` 函数。就像其他 JS 比较函数一样,如果第一个实例大于第二个则返回 `1`,相等返回 `0`,小于则返回 `-1`。
```js
const compare = Temporal.PlainTime.compare // 为简洁而提取
switch (true) {
case compare(timeNow, stages.sunrise.start) < 0 || compare(timeNow, stages.night.start) >= 0: {
currentStageName = "night";
break;
}
case compare(timeNow, stages.sunrise.start) >= 0 && compare(timeNow, stages.day.start) < 0: {
currentStageName = "sunrise";
break;
}
case compare(timeNow, stages.day.start) >= 0 && compare(timeNow, stages.sunset.start) < 0: {
currentStageName = "day";
break;
}
case compare(timeNow, stages.sunset.start) >= 0 && compare(timeNow, stages.night.start) < 0: {
currentStageName = "sunset";
break;
}
default: break;
}
```
一旦得到了阶段名称,我们就可以查找颜色并设置自定义属性的值。
```js
root.style.setProperty(
"--bg-gradient-top",
stages[currentStageName].color1,
);
root.style.setProperty(
"--bg-gradient-mid",
stages[currentStageName].color2,
);
root.style.setProperty(
"--bg-gradient-bottom",
stages[currentStageName].color3,
);
```
我还在根元素上设置了一个 data 属性,以便进行一些基于阶段的额外定制,例如在夜间显示星星。
```js
root.setAttribute("data-time", currentStageName);
```
这样就能在一天中的不同时间看到不同的渐变颜色了!
然后*我*想起了还有 `color-mix` 这个功能。为什么要把自己限制在每天的 4 个时间和 4 组颜色上呢?我们完全可以让它们……互相过渡啊???
## 用 color-mix 混合过渡
`color-mix` 是一个非常酷的 CSS 函数,它可以混合两种颜色。你告诉它颜色空间、颜色,浏览器就会神奇地输出混合后的颜色。
```css
background: color-mix(in oklch, color1, color2)
```
和渐变类似,你也可以为颜色指定百分比,表示颜色的比例:
```css
background: color-mix(in oklch, color1 20%, color2)
```
这样我就可以逐渐混入下一阶段的颜色,直到下一阶段完全接管。
为了得到下一阶段颜色的百分比值,我需要计算当前阶段已经过去了多少。
首先,计算到下一阶段的时间——用 Temporal 实例的 `until` 函数超级简单:
```js
time1.until(time2)
```
这会返回一个 `Temporal.Duration`,表示两个时间点之间的时间段。例如,如果现在是晚上 7:45,计算 `timeUntilNextStage`:
```js
const timeUntilNextStage = timeNow.until(stages.night.start)
console.log(timeUntilNextStage.toString()) // PT1H15M
```
`Duration` 使用 [ISO 8601 持续时间格式](https://en.wikipedia.org/wiki/ISO_8601#Durations)进行字符串化(和指定),所以 "PT1H15M" 表示“时间段,时间分隔符,1小时,15分钟”。时间信息出现在 `T` 之后;如果持续时间包含任何日期信息,会出现在 `T` 之前。
我们在判断当前阶段的 `switch` 语句中设置 `timeUntilNextStage`,例如:
```js
case compare(timeNow, stages.sunrise.start) >= 0 && compare(timeNow, stages.day.start) < 0: {
currentStageName = "sunrise";
timeUntilNextStage = timeNow.until(stages.night.start);
break;
}
```
有了表示到下一阶段时间的 `Duration` 之后,我们还需要知道当前阶段开始到下一阶段开始之间的时长——我们称之为“过渡时长”。对于日落→夜晚和日出→白天,过渡时长总是 90 分钟;对于夜晚→日出和白天→日落,则是 11.5 小时。我并不想让颜色混合一整天都在进行,只希望在日出/日落附近像现实那样——所以我决定硬编码白天和夜晚的过渡时长为 90 分钟,以匹配另外两个。
因此,我可以用同样的 ISO 8601 语法实例化一个 `Duration`:
```js
const entireTransitionDuration = Temporal.Duration.from("PT1H30M")
```
现在我需要计算总时长与到下一阶段时间之间的差值——基本上就是我们在过渡期中已经走了多远,以及应该混入多少百分比的下一颜色。
方便的是,Temporal 还提供了 `subtract` 函数:
```js
const diff = entireTransitionDuration.subtract(timeUntilNextStage)
```
然后,为了计算过渡进度百分比,我们可以将 `diff` 除以 `entireTransitionDuration`。我们使用实例的 `total` 函数,将时间值转换为秒再进行除法:
```js
const entireTransitionDurationInSeconds = entireTransitionDuration.total({ unit: "seconds" })
const diffInSeconds = diff.total({ unit: "seconds" })
const transitionProgressPercent = Math.round((diffInSeconds / entireTransitionDurationInSeconds)*100).toFixed()
// 返回保留0位小数的字符串
```
### 午夜问题
“夜晚”阶段变得更加复杂,因为它跨越了午夜进入第二天。请记住,我们的 `PlainTime` 只包含时间信息,不包含日期信息——所以如果现在是晚上 10 点,你问它到早上 6:30 的日出还有多久,它会返回一个负数!
```js
const now = Temporal.PlainTime.from("22:00")
const sunrise = Temporal.PlainTime.from("06:30")
const d = now.until(sunrise)
// Temporal.Duration -PT15H30M
```
这在我计算差值时会导致问题,因为它会得出一个很大的数,完全打乱计算。
我通过使用 `.abs()` 获取时长的绝对值来解决这个问题,这样 `timeUntilNextStage` 始终为正,即使是在午夜之前:例如,原来是 `-PT15H30M`,现在会变成 `PT15H30M`。然后从 `transitionDuration`(90 分钟)中减去它来计算差值,这样做结果总是负的。
```js
case compare(timeNow, stages.sunrise.start) < 0 || compare(timeNow, stages.night.start) >= 0: {
currentStageName = "night";
timeUntilNextStage = timeNow.until(stages.sunrise.start).abs();
break;
}
```
然后,我们只在 `diff` 大于 0 时才计算过渡百分比:
```js
let transitionProgressPercent = 0;
if (diffInSeconds > 0) {
transitionProgressPercent = Math.round((diffInSeconds / entireTransitionDurationInSeconds) * 100);
}
```
这对于白天阶段也同样有效:如果距离日落还有 90 分钟以上,差值会是负数——这样就会直接显示白天的颜色,没有过渡。
### 开始混合!
现在,我们可以用这个百分比值(总是整数)来在 `color-mix` 函数中指定要插入多少下一颜色。
```css
color-mix(in oklch, ${color1} ${transitionProgressPercent}%, ${color2})
```
我更新了 `stages` 对象,加入了下一阶段名称:
```js
night: {
start: Temporal.PlainTime.from("21:00:00"),
next: "sunrise",
color1: "oklch(25.27% 0.0919 276.73)",
color2: "oklch(47.35% 0.284 283.78)",
color3: "oklch(62.831% 0.23521 310.291)",
},
// 等等
```
这样,当用 `color-mix` 设置变量时,我们就可以动态获取两个颜色:
```js
root.style.setProperty(
"--bg-gradient-top",
`color-mix(in oklch, ${stages[nextStageName].color1} ${transitionProgressPercent}%, ${stages[currentStageName].color1})`,
);
```
这就是颜色过渡的实现方式!
## 过渡的过渡
作为一个额外的小亮点,我希望当你使用右上角的时间选择器手动切换阶段时,颜色变化能平滑过渡。通过使用 `@property` 声明我的 `bg-gradient-xx` 变量,我可以告诉浏览器它们确实是颜色——因此是可以动画的。如果没有这种显式的自定义属性声明,我可以将 `--bg-gradient-top` 设置为数字、位置或任何值。而明确声明它为颜色后,浏览器就知道如何将其过渡到同一类型的其他值。
我最初在 CSS 中用 `@property` 声明来实现:
```css
@property --bg-gradient-top {
syntax: "<color>";
inherits: true;
initial-value: oklch(...);
}
@property --bg-gradient-mid {
syntax: "<color>";
inherits: true;
initial-value: oklch(...);
}
@property --bg-gradient-bottom {
syntax: "<color>";
inherits: true;
initial-value: oklch(...);
}
```
不幸的是,在 CSS 中设置这些会导致在 JS 启动并设置适合当前时间的颜色之前,会出现一闪而过的初始值。如果这个页面是服务端驱动的,或者对所有人总是从同一颜色开始,那就没问题。但起始颜色取决于你的时区,并且只在初始 JS 运行时计算。
我通过 JS 设置属性来解决这个问题:
```js
window.CSS.registerProperty({
name: "--bg-gradient-top",
syntax: "<color>",
inherits: true,
initialValue: stages[currentStageName].color1,
});
window.CSS.registerProperty({
name: "--bg-gradient-mid",
syntax: "<color>",
inherits: true,
initialValue: stages[currentStageName].color2,
});
window.CSS.registerProperty({
name: "--bg-gradient-bottom",
syntax: "<color>",
inherits: true,
initialValue: stages[currentStageName].color3,
});
```
我需要将这些代码包裹在 `try/catch` 中,因为如果属性已定义,它会抛出异常。判断该属性是否已设置并不是很简单,因为 CSS 确实用常规的 `--bg-gradient-xx: ...` 语法定义了一些值。
在 `body` 和 `footer` 上,我设置了 `transition-property` 和 `transition-duration` 来告诉它哪些属性需要动画效果:
```css
body {
--background: fixed linear-gradient(var(--bg-gradient-top), var(--bg-gradient-mid) 80%);
transition-property: --bg-gradient-top, --bg-gradient-mid;
transition-duration: 0.5s;
}
footer {
background: linear-gradient(oklch(0 0 0 / 0) 40%, var(--bg-gradient-bottom));
transition: --bg-gradient-bottom 0.5s;
}
```
就像变魔术一样,当值变化时,颜色之间会无缝过渡!我喜欢 CSS。
这种动画完全是锦上添花,但这是我的网站,所以“不必要的点缀”就是游戏的名字。
## 为 Safari 提供 Temporal 的 Polyfill
可惜的是,Safari 落后于时代。我们热爱渐进增强,当然也可以直接为不支持 Temporal 的浏览器移除所有过渡逻辑,但这没意思。他们也应该拥有日落!
手动为 Temporal 编写 shim 也不好玩——我查阅了[兼容性表格](https://caniuse.com/?search=temporal),Safari 完全缺席。所以我使用了 [temporal-polyfill](https://www.npmjs.com/package/temporal-polyfill) 包。它很小(3KB),而且完美支持 `Temporal.Now` 和 `Temporal.PlainTime`。在支持 Temporal 的浏览器中,它不会产生任何影响;在不支持的浏览器中,它会添加必要的函数。
我通过 CDN 加载它(条件式,仅在需要时):
```html
<script type="module">
// 检测 Temporal 是否可用
if (typeof Temporal === 'undefined') {
// 加载 polyfill
const polyfill = await import('https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js');
// polyfill 会自动添加 Temporal 对象
}
// 现在可以安全地使用 Temporal 了
</script>
```
这样,所有用户都能体验到基于时间的颜色过渡——无论他们使用什么浏览器!
## 结束语
这个项目让我同时接触了 Temporal API 和 CSS `color-mix`,两者都让我印象深刻。
Temporal 极大地简化了时间操作,而 `color-mix` 为颜色混合提供了强大的原生 CSS 功能。
如果你想了解更多,可以查看我的[代码仓库](https://github.com/your-repo),或者直接打开开发者工具检查实时效果。
如果你有任何问题或想法,欢迎在评论区留言!
相似文章
我如何在博客中使用CSS View Transitions
作者详细介绍了如何在静态博客上仅使用CSS跨文档视图过渡来为页面导航添加动画,无需JavaScript路由,包括设置布局列和动画。
算法主题引擎
本文介绍了新的CSS `contrast-color()`函数,该函数允许开发人员自动选择黑色或白色文本,以实现与任何背景颜色的可访问对比度,解决网络上长期存在的低对比度问题,而无需依赖JavaScript。
Tw-fade: 纯CSS滚动驱动的边缘遮罩
Tw-fade 是一个新的 Tailwind CSS 插件,用于纯 CSS 滚动驱动的边缘淡出效果,无需 JavaScript 即可实现优雅的淡出效果。
仅用CSS指定每主题颜色的几种方法
本文介绍了五种仅用CSS实现每主题颜色(浅色/深色/自动)的技术,无需JavaScript,利用prefers-color-scheme、:has()、CSS变量和color-mix()等特性。
氛围网站背景云朵
background-clouds 是一个轻量级、零依赖的 JavaScript 库,它通过单个 canvas 在网站背后渲染一个动画 low-poly 云团,并支持减少动画和 SPA 生命周期方法。