你知道你可以得到完全平坦的纸板箱吗?你把它们折叠起来,然后用胶带粘住,使它们成为一个有用的盒子。然后当你要回收它们的时候,你再把它们切开,把它们压平。最近,有人向我提出将这一概念制作成三维动画,我想如果完全用CSS来做,会是一个有趣的教程,所以我们就这样做了!"。
这个动画看起来如何?我们如何创建包装时间线?尺寸是否可以灵活调整?让我们做一个纯CSS的包装切换。
这就是我们的工作目标。点击包装和拆开纸盒。
从哪里开始呢?
像这样的事情,你甚至从哪里开始呢?最好的办法是提前计划。我们知道,我们要为我们的包装准备一个模板。而这将需要在三维空间中折叠起来。如果你对CSS中的三维工作感到陌生,我推荐这篇文章来帮助你开始。
如果你对三维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>
混合器是个好主意
那里发生了很多事情。有很多div。我经常喜欢用Pug来生成标记,这样我就可以把东西分割成可重复使用的块。例如,每个侧面都会有两个挡板。我们可以为侧面创建一个Pug混合器,并使用属性来应用一个修改器类名,使所有的标记更容易编写。
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")
我们使用了两个混集器。一个是为盒子的每一面创建挡板。另一个创建了盒子的侧面。注意,在side 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>
嵌套的边
嵌套侧面使我们的包更容易折叠起来。就像每个侧面都有两个挡板一样。一个侧面的子代可以继承侧面的变换,然后应用自己的变换。如果我们从一个立方体开始,就很难利用这一点了。
看看这个在嵌套和非嵌套元素之间翻转的演示,看看其中的区别。
每个盒子都有一个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>
不嵌套。
<div class="boxes">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
改变所有的东西
在对我们的HTML应用了一些样式后,我们有了我们的包装模板。
样式指定了不同的颜色,并将两侧定位到包装上。每个边都有一个相对于 "主 "边的位置。(你会看到为什么所有的嵌套都是有用的,就在一会儿)。
有一些事情是需要注意的。与处理立方体一样,我们使用--height,--width, 和--depth 变量来确定尺寸。这将使我们在下一步改变包的大小更容易。
.package {
height: calc(var(--height, 20) * 1vmin);
width: calc(var(--width, 20) * 1vmin);
}
为什么要这样定义大小?我们正在使用一个无单位的默认尺寸20 ,这个想法是我从Lea Verou的2016年亚洲CSS会议的演讲中获得的(从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%;
}
我们也开始考虑各个部件的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%;
}
让我们开始在一个三维平面上使用我们的模板。我们可以先在X和Y轴上旋转.scene 。
.scene {
transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}
折叠
我们已经准备好开始折叠我们的模板了!我们的模板将根据一个自定义的属性--packaged 来折叠。如果值是1 ,那么我们就可以折叠模板了。例如,让我们折叠一些边和伪元素标签。
.package__side--tabbed,
.package__side--tabbed:after {
transform: rotateY(calc(var(--packaged, 0) * -90deg));
}
.package__side--extra {
transform: rotateY(calc(var(--packaged, 0) * 90deg));
}
或者,我们可以为所有不是 "主 "的边写一个规则。
.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; }
而这将涵盖所有的边。
还记得我说过,嵌套的边允许我们继承父方的变换吗?如果我们更新我们的演示,以便我们可以改变--packaged 的值,我们可以看到这个值是如何影响变换的。试着在1 和0 之间滑动--packaged 的值,你会看到我的意思。
现在我们有一种方法来切换我们模板的折叠状态,我们可以开始处理一些动作。我们之前的演示在这两种状态之间翻转。我们可以利用transition 来做到这一点。最快的方法是什么?在.scene 中的每个孩子的transform ,添加一个transition 。
.scene *,
.scene *::after {
transition: transform calc(var(--speed, 0.2) * 1s);
}
多步过渡!
但我们不会一次就把模板全部折叠起来--在现实生活中,有一个顺序,我们会先把一边和它的挡板折叠起来,然后再到下一边,以此类推。范围内的自定义属性对这一点非常完美。
.scene *,
.scene *::after {
transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step, 1) * var(--delay, 0.2)) * 1s);
}
这里我们说的是,对于每个transition ,使用--step 乘以--delay 的transition-delay 。--delay 的值不会改变,但每个元素可以定义它在序列中的哪个 "步骤"。然后我们就可以明确事情发生的顺序了。
.package__side--extra {
--step: 1;
}
.package__side--tabbed {
--step: 2;
}
.package__side--flipped,
.package__side--tabbed::after {
--step: 3;
}
同样的技术是我们要做的事情的关键。我们甚至可以引入一个--initial-delay ,为一切添加一个轻微的停顿,以获得更多的真实感。
.race__light--animated,
.race__light--animated:after,
.car {
animation-delay: calc((var(--step, 0) * var(--delay-step, 0)) * 1s);
}
CodePen Embed Fallback
如果我们回过头来看我们的包,我们可以更进一步,对所有要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;
}
但是,这感觉不是很现实。
也许我们也应该翻转盒子
如果我在现实生活中折起盒子,我可能会在折起顶部挡板之前将盒子翻转起来。那么,我们应该怎样做呢?好吧,那些敏锐的人可能已经注意到了.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%));
}
相应地调整--step 声明,我们就可以得到这样的东西。
CodePen嵌入回退
展开包装盒
如果你在折叠状态和未折叠状态之间进行翻转,你会发现展开的方式看起来不对。展开的顺序应该是与折叠的顺序完全相反。我们可以根据--packaged 和步骤的数量来翻转--step 。我们的最新步骤是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);
}
这是相当大的口气,calc ,以扭转transition-delay 。但是,它是有效的!不过,我们必须提醒自己,保持--no-of-steps 的值是最新的!
我们确实有另一个选择。当我们继续走 "纯CSS "路线时,我们最终会利用复选框黑客来在折叠状态之间进行切换。我们可以有两套定义好的 "步骤",当我们的复选框被选中时,其中一套会被激活。这当然是一个更冗长的解决方案。但是,它确实给了我们更多有限的控制。
/* Folding */
:checked ~ .scene .package__side--extra {
--step: 1;
}
/* Unfolding */
.package__side--extra {
--step: 15;
}
尺寸和居中
在我们的演示中放弃使用[dat.gui](https://github.com/dataarts/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));
}
这让我们在场景中获得可靠的中心位置。虽然这一切都归结于偏好!
添加到复选框黑客
现在,让我们把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>
在最后的演示中,我们将隐藏输入并使用标签元素。不过现在,让我们让它们全部可见。诀窍是,当某些控件得到: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;
}
最后的润色
现在我们可以让事情看起来更 "漂亮",并添加一些额外的修饰。让我们从隐藏所有的输入开始。
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;
}
我们可以把尺寸选项设计成圆形的按钮。
.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;
}
我们希望能够在任何地方点击来切换折叠和展开我们的包装。所以我们的.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);
}
这就是了!
这就是你如何制作一个纯CSS的3D包装切换器。你打算把这个放到你的网站上吗?不太可能。但是,看看你如何用CSS实现这些东西是很有趣的。自定义属性是如此强大。
为什么不把它作为一个超级节日,并把它作为CSS的礼物呢?
保持威武!ʕ -ᴥ-ʔ