使用 CSS 创建 DigitalOcean 标志的 3D 效果

Avatar of Jhey Tompkins
Jhey Tompkins

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

大家好!除非你一直生活在石头底下(也许即使如此),否则你一定已经听说过 CSS-Tricks 被 DigitalOcean 收购 的消息。恭喜大家!🥳

为了纪念这个时刻,我想用 CSS 创建一个 DigitalOcean 的标志。我做到了,但随后又用一些 3D 和视差效果进一步完善了它。这篇文章也非常有用,因为我制作这个标志的方式使用了我之前写的 文章 中的一些内容。这个很酷的小演示将很多这些概念整合在一起。

好了,让我们深入探讨吧!

我们将通过从 simpleicons.org 获取 SVG 版本的 DigitalOcean 标志来对其进行“描摹”。

<svg role="img" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <title>DigitalOcean</title>
  <path d="M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z"></path>
</svg>

考虑到我们要将它制作成 3D,我们可以将 SVG 包含在一个 .scene 元素中。然后,我们可以使用我 “高级 CSS 插图建议” 文章中的描摹技术。我们使用的是 Pug,这样我们就可以利用它的 mixin 并减少为 3D 部分编写的大量标记。

- const SIZE = 40
.scene
  svg(role='img' viewbox='0 0 24 24' xmlns='http://www.w3.org/2000/svg')
    title DigitalOcean
    path(d='M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z')
  .logo(style=`--size: ${SIZE}`)
    .logo__arc.logo__arc--inner
    .logo__arc.logo__arc--outer
    .logo__square.logo__square--one
    .logo__square.logo__square--two
    .logo__square.logo__square--three

我们的目标是将这些元素设置为与我们的标志重叠。我们不需要创建标志的“弧形”部分,因为我们已经考虑过之后要将其制作成 3D,并且可以用两个圆柱体形状来创建弧形。这意味着现在我们只需要为每个圆柱体的包含元素、内弧和外弧创建元素。

查看这个演示,它展示了 DigitalOcean 标志的不同部分。如果你切换“展开”和悬停元素,你可以看到标志是由什么组成的。

如果我们想要一个扁平的 DigitalOcean 标志,我们可以使用一个带有 圆锥形渐变 的 CSS 遮罩。然后,我们只需要一个使用实线边框的“弧形”元素。

.logo__arc--outer {
  border: calc(var(--size) * 0.1925vmin) solid #006aff;
  mask: conic-gradient(transparent 0deg 90deg, #000 90deg);
  transform: translate(-50%, -50%) rotate(180deg);
}

这样就得到了标志。“显示”动画会转换一个显示下方的描摹 SVG 图像的 clip-path

查看我 “高级 CSS 插图建议” 文章,了解有关在 CSS 中使用高级插图的技巧。

挤压以制作 3D

我们已经有了 DigitalOcean 标志的蓝图,所以现在是时候把它制作成 3D 了。为什么我们一开始没有创建 3D 块?创建包含元素可以让通过挤压来创建 3D 更容易。

我们在我的 “学会用立方体而不是盒子思考” 文章中介绍了如何在 CSS 中创建 3D 场景。我们将在制作这里的东西时使用其中的一些技术。让我们从标志中的方块开始。每个方块都是一个长方体。使用 Pug,我们将创建一个 cuboid mixin 并使用它来帮助生成所有方块。

mixin cuboid()
  .cuboid(class!=attributes.class)
    if block
      block
    - let s = 0
    while s < 6
      .cuboid__side
      - s++

然后,我们可以在标记中使用它

.scene
  .logo(style=`--size: ${SIZE}`)
    .logo__arc.logo__arc--inner
    .logo__arc.logo__arc--outer
    .logo__square.logo__square--one
      +cuboid().square-cuboid.square-cuboid--one
    .logo__square.logo__square--two
      +cuboid().square-cuboid.square-cuboid--two
    .logo__square.logo__square--three
      +cuboid().square-cuboid.square-cuboid--three

接下来,我们需要样式来显示我们的长方体。请注意,长方体有六个面,因此我们在使用 nth-of-type() 伪选择器时,会利用 vmin 长度单位来保持响应性。

.cuboid {
  width: 100%;
  height: 100%;
  position: relative;
}
.cuboid__side {
  filter: brightness(var(--b, 1));
  position: absolute;
}
.cuboid__side:nth-of-type(1) {
  --b: 1.1;
  height: calc(var(--depth, 20) * 1vmin);
  width: 100%;
  top: 0;
  transform: translate(0, -50%) rotateX(90deg);
}
.cuboid__side:nth-of-type(2) {
  --b: 0.9;
  height: 100%;
  width: calc(var(--depth, 20) * 1vmin);
  top: 50%;
  right: 0;
  transform: translate(50%, -50%) rotateY(90deg);
}
.cuboid__side:nth-of-type(3) {
  --b: 0.5;
  width: 100%;
  height: calc(var(--depth, 20) * 1vmin);
  bottom: 0;
  transform: translate(0%, 50%) rotateX(90deg);
}
.cuboid__side:nth-of-type(4) {
  --b: 1;
  height: 100%;
  width: calc(var(--depth, 20) * 1vmin);
  left: 0;
  top: 50%;
  transform: translate(-50%, -50%) rotateY(90deg);
}
.cuboid__side:nth-of-type(5) {
  --b: 0.8;
  height: 100%;
  width: 100%;
  transform: translate3d(0, 0, calc(var(--depth, 20) * 0.5vmin));
  top: 0;
  left: 0;
}
.cuboid__side:nth-of-type(6) {
  --b: 1.2;
  height: 100%;
  width: 100%;
  transform: translate3d(0, 0, calc(var(--depth, 20) * -0.5vmin)) rotateY(180deg);
  top: 0;
  left: 0;
}

我们采用了一种与过去文章中不同的方式来处理这个问题。我们不是将高度、宽度和深度应用于长方体,而是只关注它的深度。并且,我们不是试图为每个面着色,而是可以使用 filter: brightness 来帮助我们完成这项工作。

如果你需要将长方体或其他 3D 元素作为使用 filter 的面的子元素,你可能需要调整顺序。经过过滤的面会使所有 3D 子元素变平。

DigitalOcean 标志有三个长方体,因此我们为每个长方体都创建了一个类,并像这样对它们进行样式设置

.square-cuboid .cuboid__side {
  background: hsl(var(--hue), 100%, 50%);
}
.square-cuboid--one {
  /* 0.1925? It's a percentage of the --size for that square */
  --depth: calc((var(--size) * 0.1925) * var(--depth-multiplier));
}
.square-cuboid--two {
  --depth: calc((var(--size) * 0.1475) * var(--depth-multiplier));
}
.square-cuboid--three {
  --depth: calc((var(--size) * 0.125) * var(--depth-multiplier));
}

…这将给我们这样的效果

你可以玩弄深度滑块,根据需要挤压长方体!在我们的演示中,我们选择使长方体成为具有相同高度、宽度和深度的正方体。弧形的深度将与最大的长方体匹配。

现在是圆柱体。我们的目标是创建两个使用 border-radius: 50% 的端点。然后,我们可以使用多个元素作为圆柱体的侧面来创建这种效果。诀窍是将所有侧面定位。

我们可以在 CSS 中创建圆柱体,有很多方法。但是,对我来说,如果这是我预计会多次使用的东西,我会尝试让它具有前瞻性。这意味着制作一个 mixin 和一些样式,以便我可以在其他演示中重复使用它们。这些样式应该尝试满足我能预见到的场景。对于圆柱体,我们可能需要考虑一些配置

  • 半径
  • 侧面
  • 显示这些侧面的数量
  • 是否显示圆柱体的一个或两个端点

将所有这些内容组合在一起,我们可以创建一个满足这些需求的 Pug mixin

mixin cylinder(radius = 10, sides = 10, cut = [5, 10], top = true, bottom = true)
  - const innerAngle = (((sides - 2) * 180) / sides) * 0.5
  - const cosAngle = Math.cos(innerAngle * (Math.PI / 180))
  - const side =  2 * radius * Math.cos(innerAngle * (Math.PI / 180))
  //- Use the cut to determine how many sides get rendered and from what point
  .cylinder(style=`--side: ${side}; --sides: ${sides}; --radius: ${radius};` class!=attributes.class)
    if top
      .cylinder__end.cylinder__segment.cylinder__end--top
    if bottom
      .cylinder__end.cylinder__segment.cylinder__end--bottom
    - const [start, end] = cut
    - let i = start
    while i < end
      .cylinder__side.cylinder__segment(style=`--index: ${i};`)
      - i++

看到代码中的注释前面有 //- 吗?这告诉 Pug 忽略注释 并在编译的 HTML 标记中将其删除。

为什么我们需要将半径传递给圆柱体?好吧,不幸的是,我们还不能用 CSS calc() 来处理三角函数(但它 即将推出)。我们需要计算一些东西,例如圆柱体侧面的宽度以及它们应该从中心向外延伸的距离。好消息是我们有很好的方法可以通过内联自定义属性将这些信息传递给我们的样式。

.cylinder(
  style=`
    --side: ${side};
    --sides: ${sides};
    --radius: ${radius};`
  class!=attributes.class
)

我们的 mixin 的示例用法如下

+cylinder(20, 30, [10, 30])

这将创建一个半径为 2030 个侧面的圆柱体,其中只渲染侧面 1030

然后我们需要一些样式。幸运的是,为 DigitalOcean 标志的圆柱体设置样式非常简单

.cylinder {
  --bg: hsl(var(--hue), 100%, 50%);
  background: rgba(255,43,0,0.5);
  height: 100%;
  width: 100%;
  position: relative;
}
.cylinder__segment {
  filter: brightness(var(--b, 1));
  background: var(--bg, #e61919);
  position: absolute;
  top: 50%;
  left: 50%;
}
.cylinder__end {
  --b: 1.2;
  --end-coefficient: 0.5;
  height: 100%;
  width: 100%;
  border-radius: 50%;
  transform: translate3d(-50%, -50%, calc((var(--depth, 0) * var(--end-coefficient)) * 1vmin));
}
.cylinder__end--bottom {
  --b: 0.8;
  --end-coefficient: -0.5;
}
.cylinder__side {
  --b: 0.9;
  height: calc(var(--depth, 30) * 1vmin);
  width: calc(var(--side) * 1vmin);
  transform: translate(-50%, -50%) rotateX(90deg) rotateY(calc((var(--index, 0) * 360 / var(--sides)) * 1deg)) translate3d(50%, 0, calc(var(--radius) * 1vmin));
}

我们的想法是,我们创建圆柱体的所有侧面,并将它们放在圆柱体的中间。然后,我们绕 Y 轴旋转它们,并以大约等于半径的距离将它们向外延伸。

在内部部分,没有必要显示圆柱体的端点,因为它们已经被遮挡了。但是,我们确实需要为外部部分显示它们。我们使用的两个圆柱体 mixin 如下所示

.logo(style=`--size: ${SIZE}`)
  .logo__arc.logo__arc--inner
    +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner
  .logo__arc.logo__arc--outer
    +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer

我们知道之前描摹标志时使用的直径的半径。此外,我们可以使用外部圆柱体的端点来创建 DigitalOcean 标志的面。这里,border-widthclip-path 的组合非常有用。

.cylinder-arc--outer .cylinder__end--top,
.cylinder-arc--outer .cylinder__end--bottom {
  /* Based on the percentage of the size needed to cap the arc */
  border-width: calc(var(--size) * 0.1975vmin);
  border-style: solid;
  border-color: hsl(var(--hue), 100%, 50%);
  --clip: polygon(50% 0, 50% 50%, 0 50%, 0 100%, 100% 100%, 100% 0);
  clip-path: var(--clip);
}

我们已经非常接近我们的目标了!

不过,还缺少一个东西:弧形的封顶。我们需要为弧形创建一些端点,这需要两个我们可以定位并在 X 或 Y 轴上旋转的元素

.scene
  .logo(style=`--size: ${SIZE}`)
    .logo__arc.logo__arc--inner
      +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner
    .logo__arc.logo__arc--outer
      +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer
    .logo__square.logo__square--one
      +cuboid().square-cuboid.square-cuboid--one
    .logo__square.logo__square--two
      +cuboid().square-cuboid.square-cuboid--two
    .logo__square.logo__square--three
      +cuboid().square-cuboid.square-cuboid--three
    .logo__cap.logo__cap--top
    .logo__cap.logo__cap--bottom

弧形的封顶端点将根据端点的 border-width 值以及弧形的深度来确定高度和宽度。

.logo__cap {
  --hue: 10;
  position: absolute;
  height: calc(var(--size) * 0.1925vmin);
  width: calc(var(--size) * 0.1975vmin);
  background: hsl(var(--hue), 100%, 50%);
}
.logo__cap--top {
  top: 50%;
  left: 0;
  transform: translate(0, -50%) rotateX(90deg);
}
.logo__cap--bottom {
  bottom: 0;
  right: 50%;
  transform: translate(50%, 0) rotateY(90deg);
  height: calc(var(--size) * 0.1975vmin);
  width: calc(var(--size) * 0.1925vmin);
}

我们已经封顶了弧形!

将所有内容组合在一起,我们就得到了 DigitalOcean 标志。这个演示允许你将它向不同的方向旋转。

但我们还有最后一个诀窍!

我们已经得到了我们的 3D DigitalOcean 标志,但如果它以某种方式具有交互性,那就太棒了。在 2021 年 11 月,我们介绍了 如何使用 CSS 自定义属性创建视差效果。让我们在这里使用相同的技术,我们的想法是让标志通过跟随用户的鼠标光标来旋转和移动。

我们需要一点 JavaScript,以便我们可以更新在 CSS 中设置标志在 X 和 Y 轴上移动的系数所需的自定义属性。这些系数根据用户的指针位置计算得出。我通常会使用 GreenSock,这样我就可以使用 gsap.utils.mapRange。但是,这里有一个实现了 mapRange 的原生 JavaScript 版本

const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => {
  const INPUT_RANGE = inputUpper - inputLower
  const OUTPUT_RANGE = outputUpper - outputLower
  return value => outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
}

const BOUNDS = 100      
const update = ({ x, y }) => {
  const POS_X = mapRange(0, window.innerWidth, -BOUNDS, BOUNDS)(x)
  const POS_Y = mapRange(0, window.innerHeight, -BOUNDS, BOUNDS)(y)
  document.body.style.setProperty('--coefficient-x', POS_X)
  document.body.style.setProperty('--coefficient-y', POS_Y)
}

document.addEventListener('pointermove', update)

神奇之处在于 CSS 领域。这是使用自定义属性以这种方式的主要优点之一。JavaScript 告诉 CSS 正在发生什么交互。但它不在乎 CSS 会怎么做。这是一种非常棒的解耦。我将这个 JavaScript 代码片段用于很多我的演示,原因正是如此。我们只需更新 CSS 就可以创建不同的体验。

我们如何做到这一点?使用 calc() 和直接作用于 .scene 元素的 范围自定义属性。请考虑以下 .scene 的更新样式

.scene {
  --rotation-y: 75deg;
  --rotation-x: -14deg;
  transform: translate3d(0, 0, 100vmin)
    rotateX(-16deg)
    rotateY(28deg)
    rotateX(calc(var(--coefficient-y, 0) * var(--rotation-x, 0deg)))
    rotateY(calc(var(--coefficient-x, 0) * var(--rotation-y, 0deg)));
}

这将使场景根据用户的指针移动在 X 和 Y 轴上旋转。但我们可以通过调整 --rotation-x--rotation-y 的值来调整这种行为。

每个长方体将以自己的方式移动。它们可以沿着 X、Y 或 Z 轴移动。但是,我们只需要定义一个 transform。然后,我们可以使用范围自定义属性来完成其余工作。

.logo__square {
  transform: translate3d(
    calc(min(0, var(--coefficient-x, 0) * var(--offset-x, 0)) * 1%),
    calc((var(--coefficient-y) * var(--offset-y, 0)) * 1%),
    calc((var(--coefficient-x) * var(--offset-z, 0)) * 1vmin)
  );
}
.logo__square--one {
  --offset-x: 50;
  --offset-y: 10;
  --offset-z: -2;
}
.logo__square--two {
  --offset-x: -35;
  --offset-y: -20;
  --offset-z: 4;
}
.logo__square--three {
  --offset-x: 25;
  --offset-y: 30;
  --offset-z: -6;
}

这将给你这样的效果

我们可以随意调整这些设置,直到得到我们满意的效果!

将介绍动画添加到组合中

好吧,我撒了个小谎,还有一种方法(我保证是最后一种!)可以增强我们的作品。如果我们有一个开场动画,怎么样?比如一个浪潮,从 logo 上扫过并展现 logo?

我们可以用 `body` 元素的伪元素来实现这个效果。

:root {
  --hue: 215;
  --initial-delay: 1;
  --wave-speed: 2;
}

body:after,
body:before {
  content: '';
  position: absolute;
  height: 100vh;
  width: 100vw;
  background: hsl(var(--hue), 100%, calc(var(--lightness, 50) * 1%));
  transform: translate(100%, 0);
  animation-name: wave;
  animation-duration: calc(var(--wave-speed) * 1s);
  animation-delay: calc(var(--initial-delay) * 1s);
  animation-timing-function: ease-in;
}
body:before {
  --lightness: 85;
  animation-timing-function: ease-out;
}
@keyframes wave {
  from {
    transform: translate(-100%, 0);
  }
}

现在,我们的想法是让 DigitalOcean 的 logo 隐藏起来,直到浪潮从上面扫过。为了实现这个效果,我们将为 3D 元素设置 `opacity` 属性,从 `0` 开始动画。我们还将为所有 3D 元素的侧面设置 `brightness` 属性,从 `1` 开始动画,以展现 logo。由于浪潮颜色与 logo 颜色匹配,所以我们不会看到它淡入。另外,使用 `animation-fill-mode: both` 表示我们的元素将在两个方向上扩展关键帧的样式。

这需要某种动画时间线。这就是自定义属性发挥作用的地方。我们可以使用动画的持续时间来计算其他动画的延迟。我在我的文章 “如何用纯 CSS 制作 3D 包裹切换”“CSS 中的动画套娃” 中提到了这一点。

:root {
  --hue: 215;
  --initial-delay: 1;
  --wave-speed: 2;
  --fade-speed: 0.5;
  --filter-speed: 1;
}

.cylinder__segment,
.cuboid__side,
.logo__cap {
  animation-name: fade-in, filter-in;
  animation-duration: calc(var(--fade-speed) * 1s),
    calc(var(--filter-speed) * 1s);
  animation-delay: calc((var(--initial-delay) + var(--wave-speed)) * 0.75s),
    calc((var(--initial-delay) + var(--wave-speed)) * 1.15s);
  animation-fill-mode: both;
}

@keyframes filter-in {
  from {
    filter: brightness(1);
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

如何确保时间准确?在 Chrome 的 DevTool 中使用“动画检查器”进行一些微调和使用,可以帮助我们完成很多工作。尝试调整此演示中的时间。

如果你希望 logo 在浪潮过去后就出现,你可能会发现淡入动画不必要。在这种情况下,尝试将淡入时间设置为 `0`。尤其要尝试 `filter` 和 `fade` 系数。它们与代码中的 `0.75s` 和 `1.15s` 相对应。在 Chrome 的动画检查器 中进行调整和尝试,看看时间是如何协调的。

就这些了!

将所有内容放在一起,我们为我们的 3D DigitalOcean logo 创建了这个简洁的开场动画!

当然,这只是使用 CSS 创建 3D DigitalOcean logo 的一种方法。如果你看到其他可能性,或者发现可以进一步优化的方面,请在评论区中链接你的演示!

再次恭喜 CSS-Tricks 团队和 DigitalOcean 建立新的合作关系。我期待着看到收购后的发展。有一点是肯定的:CSS-Tricks 将继续为社区提供灵感和精彩的内容。😎