您知道那些完全扁平的瓦楞纸箱吗?您可以将它们折叠起来并用胶带粘好,制成一个实用的盒子。当需要回收时,再将它们剪开压平。最近,有人向我咨询了这个概念的3D动画版本,我认为用纯CSS实现它会是一个有趣的教程,所以我们开始了!
这种动画会是什么样子?我们如何创建包装时间线?尺寸可以灵活调整吗?让我们创建一个纯CSS包裹切换效果。
最终效果如下:点击即可打包和解包纸箱。
从哪里开始呢?最好提前做好规划。我们知道我们需要一个包裹模板,并且需要将其在三维空间中折叠。如果您不熟悉CSS 3D,我建议您阅读这篇文章入门。
如果您熟悉3D CSS,您可能会倾向于构建一个长方体然后开始。但是,这会带来一些问题。我们需要考虑包裹如何从2D转换为3D。
让我们从创建模板开始。我们需要提前规划好标记,并思考我们希望包装动画如何运作。让我们从一些HTML开始。
<div> <div> <div> <div> <div></div> <div></div> <div> <div></div> <div></div> </div> <div> <div></div> <div></div> <div> <div></div> <div></div> </div> </div> </div> </div> </div> </div>
这里有很多内容。有很多div。我经常喜欢使用Pug来生成标记,这样可以将内容分成可重用的块。例如,每一面都有两个盖板。我们可以为侧面创建一个Pug mixin,并使用属性应用修饰符类名,使所有这些标记更容易编写。
mixin flaps() .package__flap.package__flap--top .package__flap.package__flap--bottom mixin side(class) .package__side(class=`package__side--${class || 'side'}`) flaps() block .scene .package__wrapper .package side('main') side('extra') side('flipped') side('tabbed')
我们使用了两个mixin。一个为盒子的每一面创建盖板。另一个创建盒子的侧面。请注意,在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--extra"> <div class="package__flap package__flap--top"></div> <div class="package__flap package__flap--bottom"></div> </div> </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 class="package__side package__side--tabbed"> <div class="package__flap package__flap--top"></div> <div class="package__flap package__flap--bottom"></div> </div> </div> </div> </div> </div>
嵌套侧面使折叠包裹更容易。就像每一面都有两个盖板一样。侧面的子元素可以继承侧面的变换,然后应用它们自己的变换。如果我们从长方体开始,就很难利用这一点。
查看这个演示,它在嵌套和非嵌套元素之间切换,以查看实际效果。
每个盒子都有一个transform-origin设置为右下角,值为100% 100%。选中“Transform”切换会将每个盒子旋转90度。但是,请注意,如果我们嵌套元素,该变换的行为会发生变化。
我们切换了两个版本的标记,但没有更改其他任何内容。
嵌套:
<div> <div> <div> <div> <div></div> </div> </div> </div> </div>
非嵌套:
<div> <div></div> <div></div> <div></div> <div></div> </div>
应用一些样式到我们的HTML之后,我们就有了我们的包裹模板。
样式指定了不同的颜色,并将侧面定位到包裹上。每个侧面的位置相对于“主”侧面。(您很快就会明白为什么嵌套如此有用。)
需要注意一些事情。就像使用长方体一样,我们使用--height
、--width
和--depth
变量来确定尺寸。这将使我们以后更容易更改包裹尺寸。
.package { height: calc(var(--height, 20) * 1vmin); width: calc(var(--width, 20) * 1vmin); }
为什么这样定义尺寸?我们使用了一个无单位的默认尺寸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%; }
我们还开始考虑各个部件的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%; }
让我们开始在3D平面上使用我们的模板。我们可以首先在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
的值,我们可以看到该值如何影响变换。尝试将--packaged
的值在1和0之间滑动,您就会明白我的意思。
现在我们有了切换模板折叠状态的方法,我们可以开始处理一些运动了。我们之前的演示在两种状态之间切换。我们可以为此使用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-delay
为--step
乘以--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); }
如果我们回顾我们的包裹,我们可以更进一步,并将“步骤”应用于所有将要转换的元素。这相当冗长,但它确实有效。或者,您可以在标记中内联这些值。
.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
声明,我们可以得到类似这样的效果。
如果您在折叠和未折叠状态之间切换,您会注意到展开看起来不对。展开顺序应该是折叠顺序的完全反向。我们可以根据--packaged
和步骤数来翻转--step
。我们最新的步骤是15。我们可以将我们的转换更新为此:
.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”路线时,我们最终将使用复选框技巧来切换折叠状态。我们可以有两组定义的“步骤”,当我们的复选框被选中时,其中一组处于活动状态。这当然是一个更冗长的解决方案。但是,它确实让我们能够更精细地控制。
/* 折叠 */ :checked ~ .scene .package__side--extra { --step: 1; } /* 展开 */ .package__side--extra { --step: 15; }
在我们放弃在演示中使用dat.gui之前,让我们来调整一下包裹的尺寸。我们要检查一下我们的包裹在折叠和翻转时是否保持居中。在这个演示中,包裹有一个更大的--height
,而.scene
有一个虚线边框。
我们不妨顺便调整一下我们的变换,以便更好地居中包裹:
/* 通过在z轴上平移来考虑包裹高度 */ .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)); } /* 通过在翻转之前滑动深度来考虑包裹深度 */ .package__wrapper { transform: translate(0, calc((var(--packaged, 0) * var(--depth, 20)) * -1vmin)); }
这使我们在场景中获得了可靠的居中效果。但这完全取决于个人喜好!
现在让我们摆脱dat.gui,使之成为“纯”CSS。为此,我们需要在HTML中引入一堆控件。我们将使用一个复选框来折叠和展开我们的包裹。然后我们将使用一个单选按钮来选择包裹尺寸。
<label for="one">S</label> <label for="two">M</label> <label for="three">L</label> <label for="four">XL</label> <input type="checkbox" id="package"> <label for="package" class="open">打开包裹</label> <label for="package" class="close">关闭包裹</label>
在最终演示中,我们将隐藏输入并使用标签元素。但是,现在,让我们先让它们都可见。诀窍是在某些控件被选中时使用同级组合器(~)。然后,我们可以设置.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); }
查看此演示,了解我们在.open
和.close
中添加了background-color
的位置。在转换期间,两个标签都不可见。
我们已经拥有了完整的功能!但是,我们的包裹目前有点平淡无奇。让我们添加额外的细节,使其看起来更像“盒子”,例如包裹胶带和包装标签。
像这样的细节仅受我们想象力的限制!我们可以使用我们的--packaged
自定义属性来影响任何内容。例如,.package__tape
正在转换scaleY
变换:
.package__tape { transform: translate3d(-50%, var(--offset-y), -2px) scaleX(var(--packaged, 0)); }
需要注意的是,每当我们添加影响序列的新功能时,都需要更新我们的步骤。不仅是--step
值,还有--no-of-steps
值。
这就是创建纯CSS 3D包裹切换的方法。你会把它放到你的网站上吗?不太可能!但是,看到你可以用CSS实现这些事情很有趣。自定义属性非常强大。
为什么不超级庆祝一下,送出CSS的礼物呢!
保持出色!ʕ •ᴥ•ʔ
以上是如何制作纯CSS 3D软件包切换的详细内容。更多信息请关注PHP中文网其他相关文章!