
1 elemento HTML + 5 propiedades CSS = ¡Magia!
Digamos que te dije que podemos obtener los siguientes resultados con solo un elemento HTML y cinco propiedades CSS para cada uno. Sin SVG, sin imágenes (excepto background
en la raíz que está ahí solo para dejar en claro que nuestro único elemento HTML tiene algunas partes transparentes), sin JavaScript. ¿Qué crees que implica eso?
Bueno, este artículo explicará cómo hacer esto y luego también mostrará cómo hacer las cosas divertidas agregando algo de animación.
CSS-ing los rayos degradados
El HTML es sólo uno div
.
*/ mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);}
Tenga en cuenta que, a diferencia de los gradientes lineales y radiales, las posiciones de parada para los gradientes cónicos no pueden ser sin unidades. Deben ser porcentajes o valores angulares. Esto significa que usar algo como transparent 0 $p
no funciona, necesitamos transparent 0% $p
(o 0deg
en lugar de 0%
, no importa cuál elijamos, simplemente no puede ser sin unidades).
Hay algunas cosas a tener en cuenta aquí cuando se trata de soporte:
- Edge no admite el enmascaramiento de elementos HTML en este momento, aunque esto aparece como En desarrollo y ya apareció una marca (que no hace nada por ahora) en
about:flags
. conic-gradient()
solo es compatible de forma nativa con los navegadores Blink que cuentan con la bandera de características de la Plataforma web experimental (que se puede habilitar desdechrome://flags
oopera://flags
). Safari también contará con soporte, pero, hasta que eso suceda, Safari aún depende del polyfill, al igual que Firefox (o Edge cuando también admita el enmascaramiento en la próxima versión). Actualización : a partir de Chrome 69,conic-gradient()
ya no está detrás de una bandera; ahora funciona en cualquier navegador Blink actualizado, independientemente de que la bandera esté habilitada o no.- Los navegadores WebKit aún necesitan el
-webkit-
prefijo paramask
las propiedades de los elementos HTML. Se podría pensar que eso no es un problema ya que estamos usando el polyfill que depende de -prefix-free de todos modos, por lo que, si usamos el polyfill, debemos incluir -prefix-free antes de eso de todos modos. Lamentablemente, es un poco más complicado que eso. Esto se debe a que -prefix-free funciona mediante la detección de funciones, lo que falla en este caso porque todos los navegadores admiten elementosmask
sin prefijo... ¡en elementos SVG! Pero aquí estamos usandomask
un elemento HTML, por lo que estamos en la situación en la que los navegadores WebKit necesitan el-webkit-
prefijo, pero -prefix-free no lo agregarán. Supongo que eso significa que debemos agregarlo manualmente:*/ -webkit-mask: $m; mask: $m;}Supongo que también podríamos usar Autoprefixer, incluso si necesitamos incluir -prefix-free de todos modos, pero usar ambos solo para esto se siente un poco como usar una escopeta para matar una mosca.
Añadiendo animación
Una de las ventajas de conic-gradient()
que los navegadores Blink admitan de forma nativa es que podemos usar variables CSS en su interior (no podemos hacerlo cuando usamos polyfill). Y ahora las variables CSS también se pueden animar en los navegadores Blink con un poco de magia de Houdini (necesitamos que la bandera de características de la plataforma web experimental esté habilitada para eso, aunque ya no la necesitamos para la conic-gradient()
compatibilidad nativa a partir de Chrome 69+).
Para preparar nuestro código para la animación, cambiamos nuestro gradiente de enmascaramiento para que utilice valores alfa variables:
*/ animation: a 2s linear infinite alternate;}@keyframes a { to { --a: 0 } }
Esto nos da el siguiente resultado:
Meh. No se ve muy bien. Sin embargo, podríamos hacer las cosas más interesantes usando múltiples valores alfa:
*/ animation: a 2s infinite alternate; animation-name: a0, a1, a2, a3; animation-timing-function: /* easings from easings.net */ cubic-bezier(.57, .05, .67, .19) /* easeInCubic */, cubic-bezier(.21, .61, .35, 1); /* easeOutCubic */}@for $i from 0 to 4 { @keyframes a#{$i} { to { --a#{$i}: #{floor($i/2)} } }}
Tenga en cuenta que, dado que estamos configurando valores para propiedades personalizadas, necesitamos interpolar la floor()
función.
Ahora parece un poco más interesante, pero ¿seguramente podemos hacerlo mejor?
Intentemos utilizar una variable CSS para la posición de detención entre el rayo y el espacio:
*/ animation: p .5s linear infinite alternate}@keyframes p { to { --p: #{$p} } }
El resultado es más interesante en este caso:
Pero aún podemos darle un poco más de sabor volteando todo horizontalmente entre cada iteración, de modo que siempre esté volteado para las reversas. Esto significa que no se invierte cuando --p
va de 0%
a $p
y se invierte cuando --p
vuelve de $p
a 0%
.
La forma en que volteamos un elemento horizontalmente es aplicándole un transform: scalex(-1)
. Como queremos que este giro se aplique al final de la primera iteración y luego se elimine al final de la segunda (inversa), lo aplicamos animation
también en un fotograma clave, en uno con una steps()
función de temporización y el doble de animation-duration
.
*/ animation: p $t linear infinite alternate, s 2*$t steps(1) infinite;}@keyframes p { to { --p: #{$p} } }@keyframes s { 50% { transform: scalex(-1); } }
Ahora finalmente tenemos un resultado que realmente se ve bastante genial:
Rayos y ondas degradados con CSS
Para obtener el resultado de rayos y ondas, necesitamos agregar un segundo degradado al mask
, esta vez un repeating-radial-gradient()
.
*/ mask: $m;}
Lamentablemente, el uso de varias posiciones de parada solo funciona en los navegadores Blink con la misma bandera de características de la Plataforma web experimentalconic-gradient()
habilitada. Y si bien polyfill cubre esto para la repeating-conic-gradient()
parte en los navegadores que admiten enmascaramiento CSS en elementos HTML, pero no admiten gradientes cónicos de forma nativa (Firefox, Safari, navegadores Blink sin la bandera habilitada), nada soluciona el problema para la repeating-radial-gradient()
parte en estos navegadores.
Esto significa que estamos obligados a tener alguna repetición en nuestro código:
*/ mask: $m;}
Obviamente nos estamos acercando, pero aún no hemos llegado:
Para obtener el resultado que queremos, debemos usar la mask-composite
propiedad y establecerla en exclude
.
mask-composite
Por ahora, solo se admite en Firefox 53+, aunque los navegadores WebKit tienen un muy buen soporte (desde Chrome 1.0 y Safari 4.0) para una propiedad no estándar similar, , -webkit-mask-composite
que nos ayuda a obtener el mismo resultado para un valor de xor
y Edge debería unirse cuando finalmente admita el enmascaramiento CSS en elementos HTML. Sin embargo, tenga en cuenta que Edge admitirá tanto como mask-composite
con -webkit-mask-composite
los valores estándar, aunque cualquiera que use -webkit-mask-composite
navegadores WebKit probablemente lo usará con los no estándar, ya que de lo contrario ni siquiera funciona en navegadores WebKit.
*/ -webkit-mask: $lyr1, $lyr0; -webkit-mask-composite: xor; mask: $lyr1 exclude, $lyr0}
Tenga en cuenta que lo no estándar -webkit-mask-composite
no se puede utilizar en la -webkit-mask
abreviatura de la misma manera que utilizamos el estándar mask-composite
en la mask
abreviatura de Firefox.
Si crees que parece que los rayos y los espacios entre ellos no son iguales en los navegadores que no los admiten conic-gradient()
de forma nativa, tienes razón. Esto se debe a un problema de polyfill.
Añadiendo animación
Dado que el estándar mask-composite
solo funciona en Firefox por ahora y Firefox aún no lo admite conic-gradient()
de forma nativa, no podemos colocar variables CSS dentro repeating-conic-gradient()
(porque Firefox todavía recurre al polyfill y el polyfill no admite el uso de variables CSS). Pero podemos ponerlos dentro repeating-radial-gradient()
e incluso si no podemos animarlos con animaciones de fotogramas clave CSS, ¡podemos hacerlo con JavaScript!
Debido a que ahora estamos colocando variables CSS dentro de repeating-radial-gradient()
, pero no dentro de repeating-conic-gradient()
(ya que queremos una mejor compatibilidad con el navegador y Firefox no admite gradientes cónicos de forma nativa, por lo que recurre al polyfill, que no admite el uso de variables CSS), Ya no podemos usar lo mismo $stop-list
para ambas capas de degradado mask
.
Pero si de todos modos tenemos que reescribir nuestro mask
sin un común $stop-list
, podemos aprovechar esta oportunidad para usar diferentes posiciones de parada para los dos gradientes:
*/ --c0: #{rgba(#000, var(--a))}; --c1: #{rgba(#000, calc(1 - var(--a)))}; -webkit-mask: $lyr1, $lyr0; -webkit-mask-composite: xor; mask: $lyr1 exclude, $lyr0}
La variable alfa --a
es la que animamos de ida y vuelta (de 0
a 1
y luego de 0
nuevo a) con un poco de JavaScript básico. Comenzamos estableciendo un número total de fotogramas NF
en los que se desarrolla la animación, un índice de fotograma actual f
y una dirección de animación actual dir
:
*/let rID = null;function stopAni() { cancelAnimationFrame(rID); rID = null};function update() { /* same as before */ if(!(f%NF)) { stopAni(); return } rID = requestAnimationFrame(update)};
Al hacer clic, detenemos cualquier animación que pueda estar ejecutándose, invertimos la dirección de la animación dir
y llamamos a la update()
función:
*/function easeInOut(k) { return .5*(Math.sin((k - .5)*Math.PI) + 1)};(function update() { f += dir; document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2)); /* same as before */})();
Podemos hacer más interesante el efecto si añadimos una transparent
tira antes de la opaca y también animamos el progreso de la posición de parada --p0
donde pasamos de esta transparent
tira a la opaca:
$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc); $lyr0: repeating-radial-gradient(closest-side, transparent, transparent calc(var(--p0)*#{$pr}), #000, #000 calc(var(--p1)*#{$pr}), transparent 0, transparent $pr);
En JavaScript, ahora necesitamos animar dos variables CSS: --p0
y --p1
. Usamos una ease-in
función de sincronización para la primera y una ease-out
para la segunda. Tampoco invertimos más la dirección de la animación:
const NF = 120, TFN = { 'ease-in': function(k, e = 1.675) { return Math.pow(k, e) }, 'ease-out': function(k, e = 1.675) { return 1 - Math.pow(1 - k, e) } };let f = 0;(function update() { f = (f + 1)%NF; for(var i = 0; i 2; i++) document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF); requestAnimationFrame(update)})();
Esto nos da un resultado bastante interesante:
Deja una respuesta