发布于 2026-01-06 0 阅读
0

如何制作纯 CSS 3D 包切换

如何制作纯 CSS 3D 包切换

你知道那种完全扁平的纸箱是怎么回事吗?你可以把它们折叠起来,用胶带粘好,做成一个实用的盒子。等到需要回收的时候,再把它们剪开,恢复扁平状态。最近有人联系我,想把这个概念做成3D动画,我觉得用CSS完全实现会是一个很有意思的教程,所以就有了这个教程!

动画效果会是什么样的?我们如何创建打包时间轴?尺寸可以灵活调整吗?让我们用纯 CSS 实现一个打包切换功能。

这就是我们努力的目标。点击即可打包和拆包纸箱。

从哪里开始呢?

遇到这种情况,该从何入手呢?最好提前做好规划。我们知道要为软件包创建一个模板,而这个模板需要折叠成三维形式。如果您是 CSS 3D 新手,我推荐您阅读这篇文章作为入门指南。

如果你熟悉 3D CSS,你可能会想先构建一个长方体,然后以此为基础进行扩展。但是,这样做会带来一些问题。我们需要考虑一个包如何从 2D 过渡到 3D。

我们先来创建一个模板。我们需要提前规划好标记语言,并考虑一下包装动画应该如何运作。让我们从一些 HTML 代码开始。

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package __side package__ side--main">
        <div class="package __flap package__ flap--top"></div>
        <div class="package __flap package__ flap--bottom"></div>
        <div class="package __side package__ side--tabbed">
          <div class="package __flap package__ flap--top"></div>
          <div class="package __flap package__ flap--bottom"></div>
        </div>
        <div class="package __side package__ side--extra">
          <div class="package __flap package__ flap--top"></div>
          <div class="package __flap package__ flap--bottom"></div>
          <div class="package __side package__ side--flipped">
            <div class="package __flap package__ flap--top"></div>
            <div class="package __flap package__ flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

混合料是个好主意

这里内容相当丰富,有很多 div 元素。我经常喜欢用 Pug 生成标记,这样就能把代码拆分成可复用的模块。例如,每一面都会有两个翻盖。我们可以为翻盖创建一个 Pug mixin,并使用属性来添加修饰类名,这样就能大大简化标记的编写。

mixin flaps()
  .package __flap.package__ flap--top
  .package __flap.package__ flap--bottom

mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
Enter fullscreen mode Exit fullscreen mode

我们使用了两个 mixin。一个 mixin 创建盒子两侧的挡板,另一个 mixin 创建盒子的侧面。注意,在创建侧面的 mixin 中,我们使用了 block。这是 mixin 的子元素渲染的地方,这非常有用,因为我们需要嵌套一些侧面,以便后续操作更加便捷。

我们生成的标记:

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package __side package__ side--main">
        <div class="package __flap package__ flap--top"></div>
        <div class="package __flap package__ flap--bottom"></div>
        <div class="package __side package__ side--tabbed">
          <div class="package __flap package__ flap--top"></div>
          <div class="package __flap package__ flap--bottom"></div>
        </div>
        <div class="package __side package__ side--extra">
          <div class="package __flap package__ flap--top"></div>
          <div class="package __flap package__ flap--bottom"></div>
          <div class="package __side package__ side--flipped">
            <div class="package __flap package__ flap--top"></div>
            <div class="package __flap package__ flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

将侧面嵌套

将边嵌套起来可以更方便地折叠包装。就像每个边都有两个折叠片一样。边的子元素可以继承边的变换,然后应用自己的变换。如果我们从一个长方体开始,就很难利用这一点。

左侧截图显示了 HTML 代码,右侧显示了展开的纸箱渲染图。代码表明,纸箱的一侧是一个父容器,它定义了纸箱的宽边,并包含用于创建相应顶部和底部折叠部分的子元素。橙色箭头将每个元素与相应的渲染图连接起来,标明了 HTML 代码中纸箱的哪些部分与渲染图相对应。

看看这个演示,它会在嵌套元素和非嵌套元素之间切换,让你直观地了解它们的区别。

每个方框的transform-origin右下角都有一个设置100% 100%。选中“变换”开关会旋转每个方框。但是,如果我们嵌套这些元素,90deg就会看到这种行为发生了怎样的变化。transform

我们只是在两种标记版本之间切换,但其他任何内容都没有改变。

嵌套:

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

未嵌套:

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

改变一切事物

在对 HTML 应用一些样式后,我们就得到了软件包模板。

样式指定了不同的颜色以及各个边在包装中的位置。每个边的位置都是相对于“主”边而言的。(稍后您就会明白这种嵌套的用途。)

需要注意以下几点。与处理长方体类似,我们使用 `x` --height--width`y` 和--depth`z` 变量来定义尺寸。这将使我们日后更容易更改封装尺寸。

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
Enter fullscreen mode Exit fullscreen mode

为什么要这样定义尺寸?我们使用了一个无单位的默认尺寸 20,这个想法来自 Lea Verou 在2016 年 CSS ConfAsia 大会上的演讲(从52:44开始)。将自定义属性作为“数据”而不是“值”,我们可以自由地使用它们calc()。此外,JavaScript 无需关心值的单位,我们可以将其更改为像素、百分比等,而无需在其他地方进行更改。你可以将其重构为一个系数--root,但这也很容易变得过于复杂。

两侧的折片尺寸也需要比它们所属的边略小一些。这样我们才能像在现实生活中一样看到轻微的缝隙。此外,两侧的折片需要略微下移一些。这样,当我们折叠它们时,它们z-index之间就不会互相挤压。

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package __side--extra > .package__ flap--bottom,
.package __side--tabbed > .package__ flap--bottom {
  top: 99%;
}
.package __side--extra > .package__ flap--top,
.package __side--tabbed > .package__ flap--top {
  bottom: 99%;
}
Enter fullscreen mode Exit fullscreen mode

我们也开始考虑transform-origin各个部件的旋转方式。上盖将绕其底边旋转,下盖将绕其顶边旋转。

我们可以使用伪元素来实现右侧的选项卡。我们使用clip-path它来实现所需的形状。

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
Enter fullscreen mode Exit fullscreen mode

让我们开始在三维平面上使用模板。我们可以先绕.sceneX 轴和 Y 轴旋转模板。

.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}
Enter fullscreen mode Exit fullscreen mode

折叠起来

我们准备开始折叠模板!模板将根据一个自定义属性进行折叠--packaged。如果该属性的值为 true 1,则我们可以折叠模板。例如,让我们折叠一些边和伪元素 tab。

.package__side--tabbed,
.package__side--tabbed:after {
  transform: rotateY(calc(var(--packaged, 0) * -90deg));
}
.package__side--extra {
  transform: rotateY(calc(var(--packaged, 0) * 90deg));
}
Enter fullscreen mode Exit fullscreen mode

或者,我们可以为除“主要”一方之外的所有一方制定一条规则。

.package__side:not(.package__side--main),
.package__side:not(.package__side--main):after {
  transform: rotateY(calc((var(--packaged, 0) * var(--rotation, 90)) * 1deg));
}
.package__side--tabbed { --rotation: -90; }
Enter fullscreen mode Exit fullscreen mode

这样就涵盖了所有方面。

还记得我说过嵌套的边可以让我们继承父元素的变换吗?如果我们更新一下示例,让可以改变 `transforms` 的值,就能看到这个值是如何影响变换的。试着把 `transforms` 的值在`0` 和`1`之间--packaged滑动,你就会明白我的意思了。--packaged10

现在我们有了切换模板折叠状态的方法,就可以开始制作一些动画了。之前的演示会在两种状态之间切换。我们可以利用transition这一点。最快的方法是什么?给模板中的每个子transition元素添加一个属性transform.scene

.scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s);
}
Enter fullscreen mode Exit fullscreen mode

多步骤过渡!

但我们不会一次性把模板全部折叠起来——在实际操作中,折叠是有顺序的,我们会先折叠一侧及其翻盖,然后再折叠另一侧,以此类推。作用域自定义属性非常适合这种操作。

.scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step, 1) * var(--delay, 0.2)) * 1s);
}
Enter fullscreen mode Exit fullscreen mode

这里我们说的是,对于每个transition,使用一个乘以 的值transition-delay值不会改变,但每个元素都可以定义它在序列中的“步骤”。这样我们就可以明确地指定事件发生的顺序。--step--delay--delay

.package__side--extra {
  --step: 1;
}
.package__side--tabbed {
  --step: 2;
}
.package__side--flipped,
.package__side--tabbed::after {
  --step: 3;
}
Enter fullscreen mode Exit fullscreen mode

请参考以下演示,以便更好地了解其工作原理。更改滑块值即可更新事件发生的顺序。你能改变哪辆车获胜吗?

同样的技巧对我们接下来要做的事情至关重要。我们甚至可以引入一个--initial-delay效果,让一切略微停顿一下,以增强真实感。

.race__light--animated,
.race__light--animated:after,
.car {
  animation-delay: calc((var(--step, 0) * var(--delay-step, 0)) * 1s);
}
Enter fullscreen mode Exit fullscreen mode

回顾一下我们的包,我们可以更进一步,对所有要处理的元素应用一个“步骤” transform。虽然有点冗长,但确实有效。或者,你也可以将这些值直接内联到标记中。

.package __side--extra > .package__ flap--bottom {
  --step: 4;
}
.package __side--tabbed > .package__ flap--bottom {
  --step: 5;
}
.package __side--main > .package__ flap--bottom {
  --step: 6;
}
.package __side--flipped > .package__ flap--bottom {
  --step: 7;
}
.package __side--extra > .package__ flap--top {
  --step: 8;
}
.package __side--tabbed > .package__ flap--top {
  --step: 9;
}
.package __side--main > .package__ flap--top {
  --step: 10;
}
.package __side--flipped > .package__ flap--top {
  --step: 11;
}
Enter fullscreen mode Exit fullscreen mode

但是,感觉不太真实。

或许我们也应该把盒子翻过来。

如果我在现实生活中折叠这个盒子,我可能会先把盒子翻过来,然后再折叠顶部的折叠片。那么我们该如何实现呢?眼尖的朋友可能已经注意到这个.package__wrapper部件了。我们将利用这个部件来滑动包装盒。然后,我们将绕 x 轴旋转包装盒。这样就能营造出包装盒侧翻的效果。

.package {
  transform-origin: 50% 100%;
  transform: rotateX(calc(var(--packaged, 0) * -90deg));
}
.package__wrapper {
  transform: translate(0, calc(var(--packaged, 0) * -100%));
}
Enter fullscreen mode Exit fullscreen mode

相应地调整--step声明,我们得到类似这样的结果。

展开盒子

如果你在折叠状态和非折叠状态之间切换,你会发现展开过程看起来不太对劲。展开顺序应该是折叠顺序的完全逆序。我们可以根据--step步骤--packaged数来调整展开顺序。我们最新的步骤是15。我们可以将展开顺序更新transition为:

.scene *,
.scene *:after {
  --no-of-steps: 15;
  --step-delay: calc(var(--step, 1) - ((1 - var(--packaged, 0)) * (var(--step) - ((var(--no-of-steps) + 1) - var(--step)))));
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step-delay) * var(--delay, 0.2)) * 1s);
}
Enter fullscreen mode Exit fullscreen mode

calc要反转这个值,确实有​​点拗口transition-delay。但是,它确实有效!不过,我们必须提醒自己--no-of-steps及时更新这个值!

我们还有另一种选择。如果我们继续走“纯 CSS”路线,最终会用到复选框技巧来切换折叠状态。我们可以定义两组“步骤”,当复选框被选中时,其中一组步骤就会激活。这当然是一种更繁琐的解决方案,但它确实能让我们更精确地控制折叠状态。

/* Folding */
:checked ~ .scene .package__side--extra {
  --step: 1;
}
/* Unfolding */
.package__side--extra {
  --step: 15;
}
Enter fullscreen mode Exit fullscreen mode

尺寸和居中

在演示中放弃使用dat.gui之前,我们先来调整一下包裹的大小。我们需要检查包裹在折叠和翻转过程中是否保持居中。在这个演示中,包裹的尺寸较大--height,并且.scene带有虚线边框。

我们不妨趁此机会调整一下transform,让包装更好地居中:

/* Considers package height by translating on z-axis */
.scene {
  transform: rotateX(calc(var(--rotate-x, -24) * 1deg)) rotateY(calc(var(--rotate-y, -32) * 1deg)) rotateX(90deg) translate3d(0, 0, calc(var(--height, 20) * -0.5vmin));
}
/* Considers package depth by sliding the depth before flipping */
.package__wrapper {
  transform: translate(0, calc((var(--packaged, 0) * var(--depth, 20)) * -1vmin));
}
Enter fullscreen mode Exit fullscreen mode

这样就能确保画面居中。不过,最终还是要看个人喜好!

添加复选框破解

现在我们抛开dat.gui其他因素,用纯 CSS 来实现这个功能。为此,我们需要在 HTML 中引入一些控件。我们将使用一个复选框来控制包裹的折叠和展开。然后,我们将使用一个radio按钮来选择包裹尺寸。

<input id="package" type="checkbox"/>

<input id="one" type="radio" name="size"/>
<label class="size-label one" for="one">S</label>

<input id="two" type="radio" name="size" checked="checked"/>
<label class="size-label two" for="two">M</label>

<input id="three" type="radio" name="size"/>
<label class="size-label three" for="three">L</label>

<input id="four" type="radio" name="size"/>
<label class="size-label four" for="four">XL</label>

<label class="close" for="package">Close Package</label>
<label class="open" for="package">Open Package</label>
Enter fullscreen mode Exit fullscreen mode

在最终演示中,我们将隐藏输入框并使用标签元素。不过现在,我们先让它们全部可见。诀窍在于,当某些控件获得点赞时,使用兄弟组合符(~):checked。然后,我们可以为这些控件设置自定义属性值.scene

#package:checked ~ .scene {
  --packaged: 1;
}
#one:checked ~ .scene {
  --height: 10;
  --width: 20;
  --depth: 20;
}
#two:checked ~ .scene {
  --height: 20;
  --width: 20;
  --depth: 20;
}
#three:checked ~ .scene {
  --height: 20;
  --width: 30;
  --depth: 20;
}
#four:checked ~ .scene {
  --height: 30;
  --width: 20;
  --depth: 30;
}
Enter fullscreen mode Exit fullscreen mode

这是演示视频,它运行正常!

最后抛光

现在我们可以美化界面,添加一些额外的细节。首先,我们来隐藏所有输入框。

input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
Enter fullscreen mode Exit fullscreen mode

我们可以将尺寸选项样式设置为圆形按钮:

.size-label {
  position: fixed;
  top: var(--top);
  right: 1rem;
  z-index: 3;
  font-family: sans-serif;
  font-weight: bold;
  color: #262626;
  height: 44px;
  width: 44px;
  display: grid;
  place-items: center;
  background: #fcfcfc;
  border-radius: 50%;
  cursor: pointer;
  border: 4px solid #8bb1b1;
  transform: translate(0, calc(var(--y, 0) * 1%)) scale(var(--scale, 1));
  transition: transform 0.1s;
}
.size-label:hover {
  --y: -5;
}
.size-label:active {
  --y: 2;
  --scale: 0.9;
}
Enter fullscreen mode Exit fullscreen mode

我们希望用户点击屏幕上的任意位置即可切换包装的折叠和展开状态。因此,我们的“折叠”和“展开”.open标签.close将占据整个屏幕。您可能会好奇为什么有两个标签?这其实是个小技巧。如果我们使用一个元素transition-delay并放大相应的标签,就可以在包装切换时隐藏这两个标签。这样就能防止用户乱点(虽然它无法阻止用户按键盘上的空格键)。

.close,
.open {
  position: fixed;
  height: 100vh;
  width: 100vw;
  z-index: 2;
  transform: scale(var(--scale, 1)) translate3d(0, 0, 50vmin);
  transition: transform 0s var(--reveal-delay, calc(((var(--no-of-steps, 15) + 1) * var(--delay, 0.2)) * 1s));
}

#package:checked ~ .close,
.open {
  --scale: 0;
  --reveal-delay: 0s;
}
#package:checked ~ .open {
  --scale: 1;
  --reveal-delay: calc(((var(--no-of-steps, 15) + 1) * var(--delay, 0.2)) * 1s);
}
Enter fullscreen mode Exit fullscreen mode

请查看此演示,了解我们如何background-color在两个.open标签中添加内容.close。过渡期间,这两个标签均不可见。

功能已经全部实现!但是,我们目前的包装还略显不足。让我们添加一些细节,比如包装胶带和包装标签,让它更像一个“盒子”。

像这样的小细节,唯一限制我们的只有我们的想象力!我们可以使用--packaged自定义属性来影响任何事物。例如,它.package__tape正在改变scaleY变换状态:

.package__tape {
  transform: translate3d(-50%, var(--offset-y), -2px) scaleX(var(--packaged, 0));
}
Enter fullscreen mode Exit fullscreen mode

需要记住的是,每当我们添加一个会影响序列的新功能时,我们都需要更新步骤。不仅要更新--step值,还要更新--no-of-steps值本身。

就是这样!

这就是如何用纯 CSS 实现 3D 特效切换的方法。你会把它放到你的网站上吗?不太可能!不过,看看如何用 CSS 实现这些效果也挺有意思的。自定义属性真是太强大了。

何不来点更有节日气氛的礼物,送一份 CSS 课程呢!

保持精彩!ʕ •ᴥ•ʔ

文章来源:https://dev.to/jh3y/how-to-make-a-pure-css-3d-package-toggle-f55