Cómo hacer CSS Slinky en 3D | trucos CSS
braden coyer recientemente lanzado mensualmente Desafío de arte CSSDe hecho, se acercó a mí para una donación de una copia de mi libro. Mueve cosas con CSS para usar como recompensa para el ganador del desafío, ¡lo cual estaba más que feliz de hacer!
¿El reto del primer mes? primaveraY cuando pensé en qué hacer con el desafío, inmediatamente me vino a la mente Slinkis. Conoces a Slinki, ¿no? Este clásico juguete que derribas las escaleras y se mueve con su propia inercia.
¿Podemos crear un Slinky que baje las escaleras de esta manera en CSS? Este es exactamente el desafío que me gusta, así que decidí que podíamos abordarlo juntos en este artículo. ¿Estás listo para rodar? (Quiero decir.)
Configurar Slinky HTML
Hagamos esto con flexibilidad. (Sin juego de palabras). Lo que quiero decir es que queremos poder controlar el comportamiento de Slinky a través de propiedades CSS personalizadas, lo que nos brinda la flexibilidad de intercambiar valores cuando sea necesario.
Así es como puse la escena escrita en Pug para abreviar:
- const RING_COUNT = 10;
.container
.scene
.plane(style=`--ring-count: ${RING_COUNT}`)
- let rings = 0;
while rings < RING_COUNT
.ring(style=`--index: ${rings};`)
- rings++;
Estas propiedades personalizadas integradas son una manera fácil de actualizar la cantidad de anillos y serán útiles cuando profundicemos en este desafío. El código anterior nos da 10
suena con HTML que parece algo como esto cuando se compila:
<div class="container">
<div class="scene">
<div class="plane" style="--ring-count: 10">
<div class="ring" style="--index: 0;"></div>
<div class="ring" style="--index: 1;"></div>
<div class="ring" style="--index: 2;"></div>
<div class="ring" style="--index: 3;"></div>
<div class="ring" style="--index: 4;"></div>
<div class="ring" style="--index: 5;"></div>
<div class="ring" style="--index: 6;"></div>
<div class="ring" style="--index: 7;"></div>
<div class="ring" style="--index: 8;"></div>
<div class="ring" style="--index: 9;"></div>
</div>
</div>
</div>
El Slinky CSS original
¡Necesitaremos algunos estilos! Lo que queremos es una escena tridimensional. Me refiero a algunas cosas que podríamos querer hacer más tarde, así que esta es la idea detrás de tener un componente de envolvente adicional con .scene
clase.
Comencemos definiendo algunas propiedades para nuestra escena "infini-slinky":
:root {
--border-width: 1.2vmin;
--depth: 20vmin;
--stack-height: 6vmin;
--scene-size: 20vmin;
--ring-size: calc(var(--scene-size) * 0.6);
--plane: radial-gradient(rgb(0 0 0 / 0.1) 50%, transparent 65%);
--ring-shadow: rgb(0 0 0 / 0.5);
--hue-one: 320;
--hue-two: 210;
--blur: 10px;
--speed: 1.2s;
--bg: #fafafa;
--ring-filter: brightness(1) drop-shadow(0 0 0 var(--accent));
}
Estas propiedades determinan las características de nuestro Slinky y del escenario. Con la mayoría de las escenas 3D CSS estableceremos transform-style
En todas partes:
* {
box-sizing: border-box;
transform-style: preserve-3d;
}
Ahora necesitamos estilos para los nuestros. .scene
El truco es traducir .plane
por lo que parece que nuestro CSS Slinky se mueve sin cesar por las escaleras. Tuve que jugar para hacer las cosas exactamente como quería, así que quédate con el número mágico por ahora, ya que tendrá sentido más adelante.
.container {
/* Define the scene's dimensions */
height: var(--scene-size);
width: var(--scene-size);
/* Add depth to the scene */
transform:
translate3d(0, 0, 100vmin)
rotateX(-24deg) rotateY(32deg)
rotateX(90deg)
translateZ(calc((var(--depth) + var(--stack-height)) * -1))
rotate(0deg);
}
.scene,
.plane {
/* Ensure our container take up the full .container */
height: 100%;
width: 100%;
position: relative;
}
.scene {
/* Color is arbitrary */
background: rgb(162 25 230 / 0.25);
}
.plane {
/* Color is arbitrary */
background: rgb(25 161 230 / 0.25);
/* Overrides the previous selector */
transform: translateZ(var(--depth));
}
Aquí está pasando bastante .container
transformación. En particular:
translate3d(0, 0, 100vmin)
: esto trae.container
hacia adelante y evita que nuestro trabajo 3D se separe del cuerpo. no usamosperspective
a este nivel para que podamos salirnos con la nuestra.rotateX(-24deg) rotateY(32deg)
: Esto cambia el escenario en función de nuestras preferencias.rotateX(90deg)
: esto gira.container
con un cuarto de vuelta, lo que iguala.scene
y.plane
De forma predeterminada, de lo contrario, ambas capas se verán como la parte superior e inferior del cubo 3D.translate3d(0, 0, calc((var(--depth) + var(--stack-height)) * -1))
: Podemos usar esto para mover la escena y centrarla en el eje y (bueno, en realidad en el eje z). Esto está en los ojos del diseñador. Aquí usamos--depth
y--stack-height
para centrar las cosas.rotate(0deg)
: Aunque actualmente no está en uso, es posible que queramos rotar la escena o animar la rotación de la escena más adelante.
Para visualizar lo que está pasando con .container
echa un vistazo a esta demostración y toca en cualquier lugar para ver transform
adjunto (lo siento, solo Chromium. 😭):
¡Ya tenemos una escena estilizada! 💪
Forma de anillo furtivo
Aquí es donde estas características personalizadas de CSS jugarán un papel. Tenemos propiedades incorporadas --index
y --ring-count
de nuestro HTML. También tenemos propiedades predefinidas en CSS que vimos anteriormente :root
.
Las propiedades integradas jugarán un papel en el posicionamiento de cada anillo:
.ring {
--origin-z:
calc(
var(--stack-height) - (var(--stack-height) / var(--ring-count))
* var(--index)
);
--hue: var(--hue-one);
--accent: hsl(var(--hue) 100% 55%);
height: var(--ring-size);
width: var(--ring-size);
border-radius: 50%;
border: var(--border-width) solid var(--accent);
position: absolute;
top: 50%;
left: 50%;
transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(0deg);
}
.ring:nth-of-type(odd) {
--hue: var(--hue-two);
}
Preste atención a la forma en que calculamos --origin-z
valor, y cómo colocamos cada anillo con transform
Esto viene después de colocar cada anillo con position: absolute
.
También vale la pena señalar cómo alternamos el color de cada anillo en el último conjunto de reglas. Cuando implementé esto por primera vez, quería crear un arcoíris, delgado, donde los anillos pasaran a través de las sombras. Pero eso agrega un poco de complejidad al efecto.
Ahora tenemos algunos anillos en nuestros ascensores. .plane
:
Transformar anillos furtivos
¡Es hora de moverse! Es posible que haya notado que establecemos un transform-origin
a todos .ring
Me gusta esto:
.ring {
transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
}
Esto se basa en .scene
Talla. Este 0.2
el valor es la mitad de la cantidad restante disponible de .scene
Después .ring
está posicionado.
¡Podemos solucionar esto seguro!
:root {
--ring-percentage: 0.6;
--ring-size: calc(var(--scene-size) * var(--ring-percentage));
--ring-transform:
calc(
100%
+ (var(--scene-size) * ((1 - var(--ring-percentage)) * 0.5))
) 50%;
}
.ring {
transform-origin: var(--ring-transform);
}
por qué esto transform-origin
? De acuerdo, el anillo debería verse como si se estuviera saliendo del centro. transform
un anillo individual es una buena manera de crear transform
queremos implementar. Mueva el control deslizante en esta demostración para ver el giro del anillo:
¡Vuelve a agregar todos los anillos y podemos voltear toda la pila!
Hmmm, pero no se caen a la próxima escalera ¿Cómo podemos hacer que cada anillo caiga en la posición correcta?
Bueno, hemos calculado --origin-z
así que calculemos --destination-z
por lo que la profundidad cambia con los anillos transform
. Si tenemos un anillo en la parte superior de la pila, debe doblarse en la parte inferior después de caer. Podemos usar nuestras propiedades personalizadas para cubrir el destino de cada anillo:
ring {
--destination-z: calc(
(
(var(--depth) + var(--origin-z))
- (var(--stack-height) - var(--origin-z))
) * -1
);
transform-origin: var(--ring-transform);
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(calc(var(--destination-z) * var(--flipped, 0)))
rotateY(calc(var(--flipped, 0) * 180deg));
}
¡Ahora trata de mover la pila! Nosotros llegamos. 🙌
Animación de los anillos.
Queremos que nuestro anillo se dé la vuelta y luego se caiga. El primer intento podría verse así:
.ring {
animation-name: slink;
animation-duration: 2s;
animation-fill-mode: both;
animation-iteration-count: infinite;
}
@keyframes slink {
0%, 5% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(0deg);
}
25% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(180deg);
}
45%, 100% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(var(--destination-z))
rotateY(180deg);
}
}
¡Uf, eso no está bien en absoluto!
Pero eso es solo porque no lo usamos. animation-delay
Todos los anillos son, um, corrimiento mientras. Vamos a presentar uno animation-delay
Residencia en --index
en el anillo para que se deslicen secuencialmente.
.ring {
animation-delay: calc(var(--index) * 0.1s);
}
Bien, eso es realmente "mejor". Pero el clima aún no se descarta. Lo que sobresale más, sin embargo, es la desventaja de animation-delay
Se aplica solo en la primera iteración de la animación Luego perdemos el efecto.
En este punto, vamos a colorear los anillos para que pasen a través de la rueda de sombras. Esto hará que sea más fácil ver lo que está pasando.
.ring {
--hue: calc((360 / var(--ring-count)) * var(--index));
}
¡Eso es mejor! ✨
Volviendo a la pregunta. Dado que no podemos especificar el retraso que se aplica a cada iteración, tampoco podemos obtener el efecto deseado. Para nuestro Slinky, si fuéramos capaces de tener un animation-delay
podemos ser capaces de lograr el efecto deseado. Y podríamos usar un solo fotograma clave mientras confiamos en nuestras propiedades de alcance personalizadas. Incluso si animation-repeat-delay
puede ser una adición interesante.
Esta funcionalidad está disponible en las soluciones de animación de JavaScript. Por ejemplo, GreenSock le permite establecer un delay
y un repeatDelay
.
Pero nuestro ejemplo de Slinky no es lo más fácil de ilustrar. Vamos a dividirlo en un ejemplo básico. Piensa en dos cajas. Y quieres que se turnen.
¿Cómo hacer esto con CSS y sin "trucos"? Una idea es agregar un retraso a uno de los campos:
.box {
animation: spin 1s var(--delay, 0s) infinite;
}
.box:nth-of-type(2) {
--delay: 1s;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Pero esto no funcionará, porque el cuadro rojo seguirá girando, así como el azul después de su inicial animation-delay
.
con algo asi Calcetín verdesin embargo, podemos lograr el efecto deseado con relativa facilidad:
import gsap from 'https://cdn.skypack.dev/gsap'
gsap.to('.box', {
rotate: 360,
/**
* A function based value, means that the first box has a delay of 0 and
* the second has a delay of 1
*/
delay: (index) > index,
repeatDelay: 1,
repeat: -1,
ease: 'power1.inOut',
})
¡Y aquí está!
Pero, ¿cómo podemos hacer esto sin JavaScript?
Bueno, tenemos que "hackear" el nuestro. @keyframes
y deshacerse por completo de animation-delay
pagaremos en su lugar @keyframes
con espacio vacío. Esto viene con varias rarezas, pero sigamos adelante y construyamos un nuevo fotograma clave primero. Esto rotará completamente el elemento dos veces:
@keyframes spin {
50%, 100% {
transform: rotate(360deg);
}
}
Es como si partiéramos el fotograma clave por la mitad y ahora lo vamos a tener que doblar animation-duration
para obtener la misma velocidad. Sin uso animation-delay
podemos tratar de ajustar animation-direction: reverse
en la segunda caja:
.box {
animation: spin 2s infinite;
}
.box:nth-of-type(2) {
animation-direction: reverse;
}
casi.
La rotación es incorrecta. Podemos usar un elemento de caparazón y rotarlo, pero esto puede ser difícil ya que hay más cosas para equilibrar. El otro enfoque es crear dos fotogramas clave en lugar de uno:
@keyframes box-one {
50%, 100% {
transform: rotate(360deg);
}
}
@keyframes box-two {
0%, 50% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Y aqui lo tenemos:
Esto sería mucho más fácil si tuviéramos una forma de señalar el retraso en la repetición con algo como esto:
/* Hypothetical! */
animation: spin 1s 0s 1s infinite;
O si el retraso repetido coincide con el retraso inicial, podríamos tener un combinador para ello:
/* Hypothetical! */
animation: spin 1s 1s+ infinite;
¡Esta sería una adición interesante seguro!
Entonces, ¿necesitamos fotogramas clave para todos estos anillos?
Sí, eso es, si queremos un retraso constante. Y tenemos que hacerlo basándonos en lo que vamos a usar como ventana de animación. Todos los anillos deben ser "capturados" e identificados antes de que se repitan los fotogramas clave.
Sería terrible escribir a mano. Pero es por eso que tenemos preprocesadores CSS, ¿verdad? Bueno, al menos hasta que obtengamos bucles y algunas funciones personalizadas adicionales para la propiedad web. 😉
El arma favorita de hoy será el Stylus. Este es mi preprocesador CSS favorito y lo ha sido durante algún tiempo. Hábito significa que no me he mudado a Sass. También me gusta la falta de gramática y flexibilidad de Stylus.
Menos mal que solo tenemos que escribir esto una vez:
// STYLUS GENERATED KEYFRAMES BE HERE...
$ring-count = 10
$animation-window = 50
$animation-step = $animation-window / $ring-count
for $ring in (0..$ring-count)
// Generate a set of keyframes based on the ring index
// index is the ring
$start = $animation-step * ($ring + 1)
@keyframes slink-{$ring} {
// In here is where we need to generate the keyframe steps based on ring count and window.
0%, {$start * 1%} {
transform
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(0deg)
}
// Flip without falling
{($start + ($animation-window * 0.75)) * 1%} {
transform
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(180deg)
}
// Fall until the cut-off point
{($start + $animation-window) * 1%}, 100% {
transform
translate3d(-50%, -50%, var(--origin-z))
translateZ(var(--destination-z))
rotateY(180deg)
}
}
Esto es lo que significan estas variables:
$ring-count
: El número de anillos en nuestro accesorio.$animation-window
: Este es el porcentaje del fotograma clave en el que podemos colarnos. En nuestro ejemplo, decimos que queremos colarnos50%
de personal clave. Los demás50%
hay que acostumbrarse a los retrasos.$animation-step
: Este es el escalonamiento calculado para cada anillo. Podemos usarlo para calcular los porcentajes únicos de fotogramas clave para cada anillo.
Aquí se explica cómo compilar en CSS, al menos durante las primeras iteraciones:
Ver código completo
@keyframes slink-0 {
0%, 4.5% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(0deg);
}
38.25% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(180deg);
}
49.5%, 100% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(var(--destination-z))
rotateY(180deg);
}
}
@keyframes slink-1 {
0%, 9% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(0deg);
}
42.75% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(0)
rotateY(180deg);
}
54%, 100% {
transform:
translate3d(-50%, -50%, var(--origin-z))
translateZ(var(--destination-z))
rotateY(180deg);
}
}
Lo último que debe hacer es aplicar cada conjunto de fotogramas clave a cada anillo. Podemos hacer esto, usando nuestro etiquetado si queremos, actualizándolo para definir ambos --index
y un --name
:
- const RING_COUNT = 10;
.container
.scene
.plane(style=`--ring-count: ${RING_COUNT}`)
- let rings = 0;
while rings < RING_COUNT
.ring(style=`--index: ${rings}; --name: slink-${rings};`)
- rings++;
Lo que nos da esto al compilar:
<div class="container">
<div class="scene">
<div class="plane" style="--ring-count: 10">
<div class="ring" style="--index: 0; --name: slink-0;"></div>
<div class="ring" style="--index: 1; --name: slink-1;"></div>
<div class="ring" style="--index: 2; --name: slink-2;"></div>
<div class="ring" style="--index: 3; --name: slink-3;"></div>
<div class="ring" style="--index: 4; --name: slink-4;"></div>
<div class="ring" style="--index: 5; --name: slink-5;"></div>
<div class="ring" style="--index: 6; --name: slink-6;"></div>
<div class="ring" style="--index: 7; --name: slink-7;"></div>
<div class="ring" style="--index: 8; --name: slink-8;"></div>
<div class="ring" style="--index: 9; --name: slink-9;"></div>
</div>
</div>
</div>
Y luego nuestro estilo se puede actualizar en consecuencia:
.ring {
animation: var(--name) var(--speed) both infinite cubic-bezier(0.25, 0, 1, 1);
}
El tiempo es todo. Por eso abandonamos el estándar. animation-timing-function
y usamos un cubic-bezier
También nos beneficiamos de --speed
una propiedad personalizada que definimos al principio.
AW si. ¡Ahora tenemos Sliming CSS Slinky! Juegue con algunas de las variables en el código y vea qué comportamientos diferentes puede dar.
Crear animación sin fin
Ahora que tenemos la parte más difícil del viaje, podemos llegar al punto en que la animación se repita sin cesar. Para hacer esto, traduciremos la escena mientras nuestro Slinky se desliza para que parezca que se está deslizando hacia su posición original.
.scene {
animation: step-up var(--speed) infinite linear both;
}
@keyframes step-up {
to {
transform: translate3d(-100%, 0, var(--depth));
}
}
Wow, eso tomó muy poco esfuerzo!
Podemos eliminar los colores de la plataforma de .scene
y .plane
para evitar una animación demasiado inestable:
¡Casi termino! Lo último a lo que debe prestar atención es que la pila de anillos se gira antes de que vuelva a salir. Aquí es donde mencionamos anteriormente que usar color sería útil. Cambie el número de timbres a un número impar, p. 11
y vuelva a alternar el color del timbre:
¡Auge! ¡Tenemos un Slinky CSS en funcionamiento! ¡También es configurable!
Variaciones divertidas
¿Qué tal el efecto chanclas? Con eso me refiero a hacer que Slink se mueva de formas alternativas. Si añadimos un elemento shell extra a la escena, podemos rotar la escena, como 180deg
en cada saliva.
- const RING_COUNT = 11;
.container
.flipper
.scene
.plane(style=`--ring-count: ${RING_COUNT}`)
- let rings = 0;
while rings < RING_COUNT
.ring(style=`--index: ${rings}; --name: slink-${rings};`)
- rings++;
En cuanto a la animación, podemos usar steps()
función de sincronización y uso dos veces --speed
:
.flipper {
animation: flip-flop calc(var(--speed) * 2) infinite steps(1);
height: 100%;
width: 100%;
}
@keyframes flip-flop {
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(180deg);
}
100% {
transform: rotate(360deg);
}
}
Por último, pero no menos importante, cambiemos la forma .scene
elemento step-up
la animación funciona. Ya no necesita moverse en el eje x.
@keyframes step-up {
0% {
transform: translate3d(-50%, 0, 0);
}
100% {
transform: translate3d(-50%, 0, var(--depth));
}
}
presta atención a animation-timing-function
Este uso de steps(1)
es lo que lo hace posible.
Si quieres otro uso divertido de steps()
Mirar esto #SpeedyCSSTip!!
Para un toque extra, podríamos rotar toda la escena lentamente:
.container {
animation: rotate calc(var(--speed) * 40) infinite linear;
}
@keyframes rotate {
to {
transform:
translate3d(0, 0, 100vmin)
rotateX(-24deg)
rotateY(-32deg)
rotateX(90deg)
translateZ(calc((var(--depth) + var(--stack-height)) * -1))
rotate(360deg);
}
}
¡Me gusta! Por supuesto, el estilo es subjetivo... así que hice una pequeña aplicación que puedes usar para configurar tu Slinky:
Y aquí están las versiones "Original" y "Flip-Flop", que hice un poco más con sombras y temas.
demostraciones finales
¡Está!
Esta es al menos una forma de hacer un Slinky CSS puro que sea 3D y configurable. Por supuesto, es posible que no alcance algo como esto todos los días, pero presenta técnicas interesantes para la animación CSS. También plantea la cuestión de si se debe tener animation-repeat-delay
La propiedad en CSS sería útil. ¿Qué piensas? ¿Crees que habrá algunos buenos usos para él? Me gustaría saber.
No te olvides de jugar con el código: todo está disponible en esta colección CodePen!!
Deja una respuesta