我们向我们敬佩的网页构建者提出了同一个问题...

人们可以做些什么来让他们的网站更好?

感谢 2021 年的主要赞助商。他们为网站的运营做出了很大贡献。

使用自定义属性在 CSS Paint API 中优先考虑 prefers-color-scheme

过去几年里,我最喜欢的功能之一就是 CSS Paint API。我喜欢它。我曾经 做过一个关于它的演讲,并且制作了 一个关于我自己绘画作品的小画廊。另一个很酷的东西是 prefers-color-scheme 媒体查询 以及如何使用它来适应用户对浅色或深色模式的偏好。

最近,我发现我可以将这两项非常酷的功能与 CSS 自定义属性结合起来,以使绘画作品的显示可以根据用户的首选颜色方案进行调整!

设置舞台

我一直拖延着网站大修,最终我决定采用一个最终幻想 II 主题。我的首要任务是制作一个绘画作品,它是一个随机生成的最终幻想风格的景观,我将其命名为overworld.js

An 8-bit illustration landscape of a forest with scattered pine trees and a jagged river running through the green land.
一个随机生成的 8 位风格的景观,由 CSS Paint API 实现!

它可以多加一些装饰——这肯定在计划中——但这已经是一个相当不错的开端了!

完成绘画作品后,我继续制作网站的其他部分,例如用于浅色和深色模式的主题切换器。那时我意识到,绘画作品并没有适应这些偏好。这通常会让人头疼,但有了 CSS 自定义属性,我意识到我可以轻松地将绘画作品的渲染逻辑调整为用户首选的颜色方案!

为绘画作品设置自定义属性

如今 CSS 的状态相当棒,CSS 自定义属性就是上述“棒”的例子之一。为了确保 Paint API 和自定义属性功能都得到支持,您可以进行以下简单的功能检查

const paintAPISupported = "registerProperty" in window.CSS && "paintWorklet" in window.CSS`

第一步是定义您的自定义属性,这涉及到CSS.registerProperty 方法。它看起来像这样

CSS.registerProperty({
  name,             // The name of the property
  syntax,           // The syntax (e.g., <number>, <color>, etc.)
  inherits,         // Whether the value can be inherited by other properties
  initialValue      // The default value
});

自定义属性是使用 Paint API 最棒的一部分,因为这些值是在 CSS 中指定的,但在绘画作品上下文中是可读的。这为开发人员提供了一种超级便捷的方式来控制绘画作品的渲染方式——完全在 CSS 中

对于overworld.js 绘画作品,自定义属性用于定义随机生成的景观各个部分的颜色——草和树、河流、河岸等等。这些颜色默认值适用于浅色模式颜色方案。

我注册这些属性的方式是在一个对象中设置所有内容,然后使用Object.entries 调用该对象,然后遍历这些条目。对于我的overworld.js 绘画作品,它看起来像这样

// Specify the paint worklet's custom properties
const properties = {
  "--overworld-grass-green-color": {
    syntax: "<color>",
    initialValue: "#58ab1d"
  },
  "--overworld-dark-rock-color": {
    syntax: "<color>",
    initialValue: "#a15d14"
  },
  "--overworld-light-rock-color": {
    syntax: "<color>",
    initialValue: "#eba640"
  },
  "--overworld-river-blue-color": {
    syntax: "<color>",
    initialValue: "#75b9fd"
  },
  "--overworld-light-river-blue-color": {
    syntax: "<color>",
    initialValue: "#c8e3fe"
  }
};

// Register the properties
Object.entries(properties).forEach(([name, { syntax, initialValue }]) => {
  CSS.registerProperty({
    name,
    syntax,
    inherits: false,
    initialValue
  });
});

// Register the paint worklet
CSS.paintWorklet.addModule("/worklets/overworld.js");

由于每个属性都设置了一个初始值,因此在稍后调用绘画作品时,您不必指定任何自定义属性。但是,由于这些属性的默认值可以被覆盖,因此当用户表达对颜色方案的偏好时,可以对其进行调整。

适应用户首选的颜色方案

我正在进行的网站刷新有一个设置菜单,可以从网站的主导航中访问。用户可以从那里调整许多偏好设置,包括他们首选的颜色方案

颜色方案设置会循环遍历三个选项

  • 系统
  • 浅色
  • 深色

“系统”默认使用用户在其操作系统设置中指定的任何设置。最后两个选项通过在<html> 元素上设置lightdark 类来覆盖用户的操作系统级设置,但在没有明确指定的情况下,“系统”设置会依赖于在prefers-color-scheme 媒体查询中指定的任何内容。

此覆盖的枢纽依赖于 CSS 变量

/* Kicks in if the user's site-level setting is dark mode */
html.dark { 
  /* (I'm so good at naming colors) */
  --pink: #cb86fc;
  --firion-red: #bb4135;
  --firion-blue: #5357fb;
  --grass-green: #3a6b1a;
  --light-rock: #ce9141;
  --dark-rock: #784517;
  --river-blue: #69a3dc;
  --light-river-blue: #b1c7dd;
  --menu-blue: #1c1f82;
  --black: #000;
  --white: #dedede;
  --true-black: #000;
  --grey: #959595;
}

/* Kicks in if the user's system setting is dark mode */
@media screen and (prefers-color-scheme: dark) {
  html {
    --pink: #cb86fc;
    --firion-red: #bb4135;
    --firion-blue: #5357fb;
    --grass-green: #3a6b1a;
    --light-rock: #ce9141;
    --dark-rock: #784517;
    --river-blue: #69a3dc;
    --light-river-blue: #b1c7dd;
    --menu-blue: #1c1f82;
    --black: #000;
    --white: #dedede;
    --true-black: #000;
    --grey: #959595;
  }
}

/* Kicks in if the user's site-level setting is light mode */
html.light {
  --pink: #fd7ed0;
  --firion-red: #bb4135;
  --firion-blue: #5357fb;
  --grass-green: #58ab1d;
  --dark-rock: #a15d14;
  --light-rock: #eba640;
  --river-blue: #75b9fd;
  --light-river-blue: #c8e3fe;
  --menu-blue: #252aad;
  --black: #0d1b2a;
  --white: #fff;
  --true-black: #000;
  --grey: #959595;
}

/* Kicks in if the user's system setting is light mode */
@media screen and (prefers-color-scheme: light) {
  html {
    --pink: #fd7ed0;
    --firion-red: #bb4135;
    --firion-blue: #5357fb;
    --grass-green: #58ab1d;
    --dark-rock: #a15d14;
    --light-rock: #eba640;
    --river-blue: #75b9fd;
    --light-river-blue: #c8e3fe;
    --menu-blue: #252aad;
    --black: #0d1b2a;
    --white: #fff;
    --true-black: #000;
    --grey: #959595;
  }
}

它很重复——我相信有人知道更好的方法——但它可以完成工作。无论用户的明确站点级偏好如何,或者他们的底层系统偏好如何,页面最终都会以适当的颜色方案可靠地渲染。

在绘画作品上设置自定义属性

如果 Paint API 得到支持,文档<head> 中的一个小型内联脚本会将paint-api 类应用于<html> 元素。

/* The main content backdrop rendered at a max-width of 64rem.
   We don't want to waste CPU time if users can't see the
   background behind the content area, so we only allow it to
   render when the screen is 64rem (1024px) or wider. */
@media screen and (min-width: 64rem) {
  .paint-api .backdrop {
    background-image: paint(overworld);
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: -1;

    /* These oh-so-well-chosen property names refer to the
       theme-driven CSS variables that vary according to
       the user's preferred color scheme! */
    --overworld-grass-green-color: var(--grass-green);
    --overworld-dark-rock-color: var(--dark-rock);
    --overworld-light-rock-color: var(--light-rock);
    --overworld-river-blue-color: var(--river-blue);
    --overworld-light-river-blue-color: var(--light-river-blue);
  }
}

这里肯定有一些奇怪的地方。出于某种原因,这可能是也可能不是以后的情况——但至少在我写这篇文章时就是这样——你不能直接在<body> 元素上渲染绘画作品的输出。

此外,由于某些页面可能相当高,我不希望整个页面的背景都填充随机生成的(因此可能很昂贵的)艺术品。为了解决这个问题,我在一个元素中渲染绘画作品,该元素使用固定定位,随着用户向下滚动而跟随用户,并且占据整个视口。

除了所有这些怪癖,这里的魔法是绘画作品的自定义属性是基于用户的系统——或站点级——颜色方案偏好的,因为 CSS 变量与该偏好保持一致。对于overworld 绘画作品,这意味着我可以调整其输出以与用户首选的颜色方案保持一致!

还不错!但这不是控制绘画作品渲染方式的创新方法。如果我愿意,我可以添加一些额外的细节,这些细节只会在特定颜色方案中出现,或者做其他事情来彻底改变渲染或添加一些彩蛋。虽然我今年学到了很多东西,但我认为 API 之间的这种交集是我最喜欢的之一。