用CSS创建3D的DigitalOcean标志的方法

157 阅读10分钟

大家好!除非你一直生活在岩石下(甚至可能是这样),否则你无疑已经听到了CSS-Tricks被DigitalOcean收购的消息。祝贺大家!🥳

为了纪念这一时刻,我想用CSS创建DigitalOcean的标志。我这样做了,但后来又用一些3D和视差将其进一步放大。这也是一篇很好的文章,因为我制作标志的方式使用了我以前写过的文章中的各种片段。这个很酷的小演示将这些概念中的许多内容结合起来。

所以,让我们直接进入!

创建DigitalOcean的标志

我们将通过从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,所以我们可以利用它的混合元素,减少我们需要为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标志的不同部分。如果你切换 "爆炸 "和悬停元素,你可以看到标志的组成。

CodePen嵌入回退

如果我们想要一个平坦的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);
}

这样我们就可以得到这个标志。揭示 "转换一个clip-path ,显示下面的追踪的SVG图像。

CodePen嵌入回退

请看我的"复杂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来为我们处理这个问题。

如果你需要将立方体或其他三维元素作为一个侧面的子元素使用 filter,你可能需要对事物进行洗牌。一个经过过滤的边会把任何三维的子元素压平。

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));
}

...这给了我们这样的东西。

CodePen嵌入回退

你可以随心所欲地玩弄深度滑块来挤压立方体!对于我们的演示,我们选择让立方体成为真正的立方体,具有相同的高度、宽度和深度。弧线的深度将与最大的立方体相匹配。

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

CodePen嵌入回退

我们可以采取各种方法来创建CSS中的圆柱体。但是,对我来说,如果这是我可以预见的多次使用的东西,我就会试着让它适应未来。这意味着制作一个混合器和一些我可以在其他演示中重复使用的样式。而这些样式应该尽量满足我可以看到的各种场景。对于一个圆柱体,我们可能需要考虑一些配置。

  • 半径
  • 显示多少个边
  • 是否要显示圆柱体的一端或两端

把这些放在一起,我们可以创建一个迎合这些需求的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标记中剔除。

为什么我们需要将半径传入圆柱体?嗯,不幸的是,我们现在还不能用CSScalc() 来处理三角学(但它即将到来)。我们还需要计算出圆柱体两侧的宽度以及它们应该从中心向外伸出多远。最棒的是,我们有一个很好的方法,可以通过内联自定义属性将这些信息传递给我们的样式。

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

我们的混合器的一个使用例子如下。

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

这将创建一个半径为20 ,边长为30 的圆柱体,其中只有边长为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轴上旋转它们,并将它们投射出去,投射的距离大约是半径的一半。

CodePen嵌入回退

没有必要在内部显示圆柱体的两端,因为它们已经被遮挡了。但我们确实需要在外部部分显示它们。我们的双缸混合使用看起来像这样。

.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);
}

我们已经很接近我们想要的地方了!

CodePen嵌入回退

不过还缺少一件事:为弧线加盖。我们需要为弧线创建一些端点,这需要两个元素,我们可以在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);
}

我们已经给圆弧封顶了!

CodePen嵌入回退

把所有东西扔在一起,我们就有了DigitalOcean的标志。这个演示允许你在不同的方向上旋转它。

CodePen 嵌入回退

但我们的袖子里还有一招!

给标志添加视差效果

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

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

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;
}

这将给你带来这样的东西。

CodePen 嵌入回退

我们可以随心所欲地调整这些,直到我们得到我们满意的东西

CodePen 嵌入回退

添加一个介绍性动画的组合

好吧,我说了点假话,有一个最后的(我保证!)方法,我们可以加强我们的工作。如果我们有某种介绍性的动画呢?如果有一个波浪或什么东西,冲过并显示出标志,怎么样?

我们可以用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的标志是隐藏的,直到波浪冲过它的顶部。为了达到这个效果,我们要将我们的三维元素from ,不透明度为0 。我们要将我们的三维元素的所有侧面从brightness1 ,露出标志。因为波浪的颜色与标志的颜色一致,所以我们不会看到它淡入。此外,使用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中的 "动画检查器 "就会有很大的帮助。试着调整这个演示中的时间。

CodePen 嵌入回退

你可能会发现,如果你想让标志在波浪过后还在那里,那么淡化时间就没有必要了。在这种情况下,尝试将淡出时间设置为0 。而且特别是实验一下filterfade 的系数。它们与上述代码中的0.75s1.15s 有关。值得调整的是,在Chrome的 "动画检查器"中玩一玩,看看事情是如何进行的。

这就是了!

把这一切放在一起,我们就有了这个整洁的3D DigitalOcean标志的介绍

CodePen嵌入回退

当然,这只是用CSS创建DigitalOcean三维标志的一种方法。如果你看到了其他的可能性,或者可以进一步优化的东西,请在评论中留下你的演示链接。

再次祝贺CSS-Tricks团队和DigitalOcean的新伙伴关系。我很高兴看到收购后的情况。有一件事是肯定的。CSS-Tricks将继续为社区提供灵感和制作精彩的内容。