使用 Temporal 和 CSS color-mix 实现基于时间的背景颜色过渡

Lobsters Hottest 工具

摘要

本文演示了如何使用 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

Lobsters Hottest

作者详细介绍了如何在静态博客上仅使用CSS跨文档视图过渡来为页面导航添加动画,无需JavaScript路由,包括设置布局列和动画。

算法主题引擎

Hacker News Top

本文介绍了新的CSS `contrast-color()`函数,该函数允许开发人员自动选择黑色或白色文本,以实现与任何背景颜色的可访问对比度,解决网络上长期存在的低对比度问题,而无需依赖JavaScript。

Tw-fade: 纯CSS滚动驱动的边缘遮罩

Hacker News Top

Tw-fade 是一个新的 Tailwind CSS 插件,用于纯 CSS 滚动驱动的边缘淡出效果,无需 JavaScript 即可实现优雅的淡出效果。

仅用CSS指定每主题颜色的几种方法

Lobsters Hottest

本文介绍了五种仅用CSS实现每主题颜色(浅色/深色/自动)的技术,无需JavaScript,利用prefers-color-scheme、:has()、CSS变量和color-mix()等特性。

氛围网站背景云朵

Hacker News Top

background-clouds 是一个轻量级、零依赖的 JavaScript 库,它通过单个 canvas 在网站背后渲染一个动画 low-poly 云团,并支持减少动画和 SPA 生命周期方法。