探索 @property 及其动画能力

Avatar of Jhey Tompkins
Jhey Tompkins

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始,获得 $200 免费赠金!

呃,@property 是什么? 这是一个新的 CSS 功能! 它赋予您超能力。 不是开玩笑,@property 可以做到一些事情,从而解锁了我们以前在 CSS 中从未做到过的事情。

虽然关于 @property 的一切都很令人兴奋,但也许最有趣的是它提供了一种为自定义 CSS 属性指定类型的方法。 类型为浏览器提供了更多上下文信息,这导致了一些很酷的东西:我们可以为浏览器提供过渡和动画化这些属性所需的信息!

但是,在我们对此过于兴奋之前,值得注意的是,支持还没有完全到位。 在撰写本文时,@property 在 Chrome 和 Edge 中受支持。 我们需要密切关注 浏览器支持,以便了解何时可以在其他地方使用它,例如 Firefox 和 Safari。

首先,我们进行类型检查

@property --spinAngle {
  /* An initial value for our custom property */
  initial-value: 0deg;
  /* Whether it inherits from parent set values or not */
  inherits: false;
  /* The type. Yes, the type. You thought TypeScript was cool */
  syntax: '<angle>';
}

@keyframes spin {
  to {
    --spinAngle: 360deg;
  }
}

没错! 在 CSS 中进行类型检查。 这有点像创建我们自己的迷你 CSS 规范。 这是一个简单的例子。 查看我们拥有的所有各种类型

  • 长度
  • 数字
  • 百分比
  • 长度百分比
  • 颜色
  • 图像
  • 网址
  • 整数
  • 角度
  • 时间
  • 分辨率
  • 变换列表
  • 变换函数
  • custom-ident(自定义标识符字符串)

在此之前,我们可能依赖于使用“技巧”来使用自定义属性为动画提供动力。

那么我们可以做些什么很酷的事情呢? 让我们来看看,激发我们的想象力。

让我们为颜色添加动画

您如何通过一系列颜色或在颜色之间对元素进行动画处理? 我是 HSL 颜色空间的坚定拥护者,它将事物分解为相当容易理解的数字:分别是色调、饱和度和亮度。

为色调添加动画感觉像是一件我们可以做得很开心的事情。 什么是丰富多彩的? 彩虹! 我们可以通过多种方式制作彩虹。 这是一个

在这个例子中,使用 :nth-child() 在彩虹的不同色带上设置 CSS 自定义属性,以将它们限定在各个色带上。 每个波段还设置了一个 --index 来帮助确定大小。

为了使这些波段动画化,我们可以使用该 --index 来设置一些负动画延迟,然后使用相同的关键帧动画循环浏览色调。

.rainbow__band {
  border-color: hsl(var(--hue, 10), 80%, 50%);
  animation: rainbow 2s calc(var(--index, 0) * -0.2s) infinite linear;
}

@keyframes rainbow {
  0%, 100% {
    --hue: 10;
  }
  14% {
    --hue: 35;
  }
  28% {
    --hue: 55;
  }
  42% {
    --hue: 110;
  }
  56% {
    --hue: 200;
  }
  70% {
    --hue: 230;
  }
  84% {
    --hue: 280;
  }
}

如果您想要“阶梯式”效果,那可能会奏效。 但是,这些关键帧步骤并不是特别准确。 我使用了 14% 的步长作为粗略的跳跃。

我们可以对 border-color 进行动画处理,这将完成这项工作。 但是,我们仍然会遇到关键帧步骤计算问题。 而且我们需要编写大量的 CSS 代码才能完成这项工作

@keyframes rainbow {
  0%, 100% {
    border-color: hsl(10, 80%, 50%);
  }
  14% {
    border-color: hsl(35, 80%, 50%);
  }
  28% {
    border-color: hsl(55, 80%, 50%);
  }
  42% {
    border-color: hsl(110, 80%, 50%);
  }
  56% {
    border-color: hsl(200, 80%, 50%);
  }
  70% {
    border-color: hsl(230, 80%, 50%);
  }
  84% {
    border-color: hsl(280, 80%, 50%);
  }
}

输入 @property。 让我们首先为色调定义一个自定义属性。 这告诉浏览器我们的自定义属性 --hue 将是一个数字(而不是看起来像数字的字符串)

@property --hue {
  initial-value: 0;
  inherits: false;
  syntax: '<number>';
}

HSL 中的色调值可以在 0360 之间。 我们从初始值 0 开始。 该值不会被继承。 在这种情况下,我们的值是一个数字。 动画就像

@keyframes rainbow {
  to {
    --hue: 360;
  }
}

是的,就是这样

为了使起点准确,我们可以为每个波段使用延迟。 这给了我们一些很酷的灵活性。 例如,我们可以提高 animation-duration,我们会得到一个缓慢的循环。 在此演示中试用速度。

这可能不是最“疯狂”的例子,但我认为当我们使用逻辑上使用数字的颜色空间时,为颜色添加动画有一些有趣的机会。 以前需要一些技巧才能在色轮中进行动画处理。 例如,使用预处理器(如 Stylus)生成关键帧

@keyframes party 
  for $frame in (0..100)
    {$frame * 1%}
      background 'hsl(%s, 65%, 40%)' % ($frame * 3.6)

我们这样做纯粹是因为浏览器不理解这一点。 它将色轮上的 0 到 360 视为即时过渡,因为两个 hsl 值都显示相同的颜色。

@keyframes party {
  from {
    background: hsl(0, 80%, 50%); 
  }
  to {
    background: hsl(360, 80%, 50%);
  }
}

关键帧是相同的,因此浏览器假设动画保持在相同的 background 值,而我们实际想要的是浏览器遍历整个色调范围,从一个值开始并在经过运动后以相同的值结束。

想想我们在这里拥有的所有其他机会。 我们可以

  • 为饱和度添加动画
  • 使用不同的缓动
  • 为亮度添加动画
  • 试试 rgb()
  • hsl() 中尝试度数并将我们的自定义属性类型声明为 <angle>

巧妙的是,我们可以通过作用域跨元素共享该动画值! 考虑这个按钮。 边框和阴影在悬停时会在色轮中进行动画处理。

动画颜色让我觉得……哇!

直接编号

因为我们可以为数字定义类型(例如 integernumber),这意味着我们也可以对数字进行动画处理,而不是将这些数字用作其他内容的一部分。 Carter Li 实际上写了一篇关于此的文章,就在 CSS-Tricks 上。 诀窍是将 integer 与 CSS 计数器结合使用。 这类似于我们如何在“纯 CSS”游戏中使用计数器,如下所示。

使用 counter 和伪元素提供了一种将数字转换为字符串的方法。 然后我们可以将该字符串用于伪元素的 content。 以下是重要的部分

@property --milliseconds {
  inherits: false;
  initial-value: 0;
  syntax: '<integer>';
}

.counter {
  counter-reset: ms var(--milliseconds);
  animation: count 1s steps(100) infinite;
}

.counter:after {
  content: counter(ms);
}

@keyframes count {
  to {
    --milliseconds: 100;
  }
}

这给了我们这样的东西。 很酷。

更进一步,您就拥有了一个仅使用 CSS 和 HTML 制作的工作秒表。 点击按钮! 这里的好处在于,这实际上可以用作计时器。 它不会出现漂移。 在某些方面,它可能比我们经常使用的 JavaScript 解决方案(例如 setInterval)更准确。 观看 Google Chrome Developer 关于 JavaScript 计数器的精彩视频

您还可以将动画数字用于哪些其他用途? 倒计时也许?

动画渐变

您知道那些,线性、径向和圆锥。 是否曾经想过要过渡或动画化颜色停止? 好吧,@property 可以做到这一点!

考虑一个我们正在海滩上创造一些波浪的渐变。 一旦我们对一些图像进行了分层,我们就可以制作出这样的东西。

body {
  background-image:
    linear-gradient(transparent 0 calc(35% + (var(--wave) * 0.5)), var(--wave-four) calc(75% + var(--wave)) 100%),
    linear-gradient(transparent 0 calc(35% + (var(--wave) * 0.5)), var(--wave-three) calc(50% + var(--wave)) calc(75% + var(--wave))),
    linear-gradient(transparent 0 calc(20% + (var(--wave) * 0.5)), var(--wave-two) calc(35% + var(--wave)) calc(50% + var(--wave))),
    linear-gradient(transparent 0 calc(15% + (var(--wave) * 0.5)), var(--wave-one) calc(25% + var(--wave)) calc(35% + var(--wave))), var(--sand);
}

那里发生了很多事情。 但是,为了分解它,我们使用 calc() 创建每个颜色停止。 在该计算中,我们添加 --wave 的值。 这里巧妙的技巧是,当我们对 --wave 值进行动画处理时,所有波浪层都会移动。

这是我们实现这一点所需的全部代码

body {
  animation: waves 5s infinite ease-in-out;
}
@keyframes waves {
  50% {
    --wave: 25%;
  }
}

如果不使用 @property,我们的波浪会在涨潮和退潮之间步进。 但是,有了它,我们就会得到像这样一种令人愉快的冷却效果。

想到我们在处理图像时获得的其他好机会真是令人兴奋。 就像旋转一样。 或者如何为 conic-gradient 的角度添加动画……但是,在 border-image 中。 Bramus Van Damme 做了一个出色的工作来涵盖这个概念

让我们通过创建一个充电指示器来分解它。 我们将同时为角度和色调添加动画。 我们可以从两个自定义属性开始

@property --angle {
  initial-value: 0deg;
  inherits: false;
  syntax: '<number>';
}

@property --hue {
  initial-value: 0;
  inherits: false;
  syntax: '<angle>';
}

动画将在每次迭代中更新角度和色调,并稍作停顿。

@keyframes load {
  0%, 10% {
    --angle: 0deg;
    --hue: 0;
  }
  100% {
    --angle: 360deg;
    --hue: 100;
  }
}

现在让我们将其应用为元素的 border-image

.loader {
  --charge: hsl(var(--hue), 80%, 50%);
  border-image: conic-gradient(var(--charge) var(--angle), transparent calc(var(--angle) * 0.5deg)) 30;
  animation: load 2s infinite ease-in-out;
}

很酷。

不幸的是,border-imageborder-radius 不兼容。 但是,我们可以在它后面使用一个伪元素。 将其与之前的数字动画技巧相结合,我们就得到了一个完整的充电/加载动画。 (是的,它会在达到 100% 时发生变化。)

变换也很酷

变换动画的一个问题是在某些部分之间进行过渡。它通常会以断裂或看起来不正常而告终。以扔球的经典例子为例。我们希望它从 A 点移动到 B 点,同时模拟重力的影响。

最初的尝试可能如下所示

@keyframes throw {
  0% {
    transform: translate(-500%, 0);
  }
  50% {
    transform: translate(0, -250%);
  }
  100% {
    transform: translate(500%, 0);
  }
}

但是,我们很快就会发现它看起来根本不像我们想要的那样。

以前,我们可能已经接触过包装元素并单独对它们进行动画处理。但是,使用 @property,我们可以对变换的各个值进行动画处理。并且都在一个时间线上。让我们通过定义自定义属性然后在球上设置变换来颠倒这种工作方式。

@property --x {
  inherits: false;
  initial-value: 0%;
  syntax: '<percentage>';
}

@property --y {
  inherits: false;
  initial-value: 0%;
  syntax: '<percentage>';
}

@property --rotate {
  inherits: false;
  initial-value: 0deg;
  syntax: '<angle>';
}

.ball {
  animation: throw 1s infinite alternate ease-in-out;
  transform: translateX(var(--x)) translateY(var(--y)) rotate(var(--rotate));
}

现在,对于我们的动画,我们可以根据关键帧组合我们想要的变换

@keyframes throw {
  0% {
    --x: -500%;
    --rotate: 0deg;
  }
  50% {
    --y: -250%;
  }
  100% {
    --x: 500%;
    --rotate: 360deg;
  }
}

结果呢?我们所希望的弯曲路径。我们可以根据我们使用的不同计时函数使外观不同。我们可以将动画分成三种方式,并使用不同的计时函数。这将为球的移动方式提供不同的结果。

考虑另一个例子,我们有一辆汽车,我们想让它绕着一个有圆角的正方形行驶。

我们可以使用与球类似的方法

@property --x {
  inherits: false;
  initial-value: -22.5;
  syntax: '<number>';
}

@property --y {
  inherits: false;
  initial-value: 0;
  syntax: '<number>';
}

@property --r {
  inherits: false;
  initial-value: 0deg;
  syntax: '<angle>';
}

汽车的 transform 使用 vmin 进行计算以保持响应

.car {
  transform: translate(calc(var(--x) * 1vmin), calc(var(--y) * 1vmin)) rotate(var(--r));
}

现在可以为汽车编写极其精确的逐帧旅程。我们可以从 --x 的值开始。

@keyframes journey {
  0%, 100% {
    --x: -22.5;
  }
  25% {
    --x: 0;
  }
  50% {
    --x: 22.5;
  }
  75% {
    --x: 0;
  }
}

汽车在 x 轴上行驶正确的旅程。

然后我们通过添加 y 轴的行程来构建它

@keyframes journey {
  0%, 100% {
    --x: -22.5;
    --y: 0;
  }
  25% {
    --x: 0;
    --y: -22.5;
  }
  50% {
    --x: 22.5;
    --y: 0;
  }
  75% {
    --x: 0;
    --y: 22.5;
  }
}

好吧,这不太对。

让我们在 @keyframes 中添加一些额外的步骤来平滑它

@keyframes journey {
  0%, 100% {
    --x: -22.5;
    --y: 0;
  }
  12.5% {
    --x: -22.5;
    --y: -22.5;
  }
  25% {
    --x: 0;
    --y: -22.5;
  }
  37.5% {
    --y: -22.5;
    --x: 22.5;
  }
  50% {
    --x: 22.5;
    --y: 0;
  }
  62.5% {
    --x: 22.5;
    --y: 22.5;
  }
  75% {
    --x: 0;
    --y: 22.5;
  }
  87.5% {
    --x: -22.5;
    --y: 22.5;
  }
}

啊,现在好多了

剩下的就是汽车的旋转。我们将在拐角处使用 5% 的窗口。它并不精确,但它肯定显示了可能性的潜力

@keyframes journey {
  0% {
    --x: -22.5;
    --y: 0;
    --r: 0deg;
  }
  10% {
    --r: 0deg;
  }
  12.5% {
    --x: -22.5;
    --y: -22.5;
  }
  15% {
    --r: 90deg;
  }
  25% {
    --x: 0;
    --y: -22.5;
  }
  35% {
    --r: 90deg;
  }
  37.5% {
    --y: -22.5;
    --x: 22.5;
  }
  40% {
    --r: 180deg;
  }
  50% {
    --x: 22.5;
    --y: 0;
  }
  60% {
    --r: 180deg;
  }
  62.5% {
    --x: 22.5;
    --y: 22.5;
  }
  65% {
    --r: 270deg;
  }
  75% {
    --x: 0;
    --y: 22.5;
  }
  85% {
    --r: 270deg;
  }
  87.5% {
    --x: -22.5;
    --y: 22.5;
  }
  90% {
    --r: 360deg;
  }
  100% {
    --x: -22.5;
    --y: 0;
    --r: 360deg;
  }
}

我们做到了,一辆汽车绕着一个弯曲的正方形行驶!没有包装器,不需要复杂的数学。我们用自定义属性组合了这一切。

使用变量为整个场景提供动力

到目前为止,我们已经看到了一些非常简洁的 @property 可能性,但是将我们在这里看到的所有内容放在一起可以将事情提升到另一个层次。例如,我们只需几个自定义属性就可以为整个场景提供动力。

考虑以下 404 页面的概念。两个注册属性为不同的移动部件提供动力。我们有一个使用 -webkit-background-clip 剪辑的移动渐变。阴影通过读取属性值来移动。我们摆动另一个元素来获得灯光效果。

就是这样!

想到我们可以使用 @property 定义类型来做些什么,真是令人兴奋。通过为浏览器提供有关自定义属性的附加上下文,我们可以以以前使用基本字符串无法做到的方式疯狂。

您对其他类型有什么想法?时间和分辨率将带来有趣的过渡,尽管我承认我无法让它们以我希望的方式工作。 url 也可能很简洁,例如可能以图像轮播的典型方式在一系列来源之间进行转换。只是在这里头脑风暴!

我希望快速浏览 @property 能激发您去查看它并制作您自己的精彩演示!我期待看到你所做的。事实上,请在评论中与我分享它们!