Simulación de sombras paralelas con CSS Paint API
Pregúntele a cien desarrolladores front-end y la mayoría, si no todos, habrán utilizado la box-shadow
propiedad en sus carreras. Las sombras son muy populares y pueden agregar un efecto elegante y sutil si se usan correctamente. Pero las sombras ocupan un lugar extraño en el modelo de caja CSS. No tienen ningún efecto sobre el ancho y alto de un elemento y se recortan fácilmente si el desbordamiento de un elemento principal (o abuelo) está oculto.
Podemos solucionar esto con CSS estándar de diferentes maneras. Pero, ahora que algunas de las especificaciones CSS Houdini se están implementando en los navegadores, existen nuevas opciones tentadoras. La API CSS Paint , por ejemplo, permite a los desarrolladores generar imágenes mediante programación en tiempo de ejecución. Veamos cómo podemos usar esto para pintar una sombra compleja dentro de una imagen de borde .
Una introducción rápida a Houdini
Es posible que haya oído hablar de alguna tecnología CSS novedosa que llega a la plataforma con el pegadizo nombre de Houdini . Houdini promete ofrecer un mayor acceso a cómo el navegador pinta la página. Como afirma MDN, es “un conjunto de API de bajo nivel que exponen partes del motor CSS, brindando a los desarrolladores el poder de extender CSS al conectarse al proceso de estilo y diseño del motor de renderizado de un navegador”.
La API de pintura CSS
La API CSS Paint es una de las primeras API en llegar a los navegadores. Es una recomendación candidata del W3C. Esta es la etapa en la que las especificaciones comienzan a verse implementadas. Actualmente está disponible para uso general en Chrome y Edge, mientras que Safari lo tiene detrás de una bandera y Firefox lo cataloga como “digno de crear un prototipo”. Hay un polyfill disponible para navegadores no compatibles, aunque no se ejecutará en IE11.
Si bien la API CSS Paint está habilitada en Chromium, pasar argumentos a la paint()
función todavía está detrás de una bandera. Deberá habilitar las funciones experimentales de la plataforma web por el momento. Lamentablemente, es posible que estos ejemplos no funcionen en el navegador de su elección en este momento. Consideremos un ejemplo de lo que está por venir y que aún no está listo para la producción.
El enfoque
Vamos a generar una imagen con una sombra y luego usarla para border-image
… ¿eh? Bueno, echamos un vistazo más profundo.
Como se mencionó anteriormente, las sombras no agregan ancho ni alto a un elemento, sino que se extienden desde su cuadro delimitador. En la mayoría de los casos, esto no es un problema, pero esas sombras son vulnerables al recorte. Una solución común es crear algún tipo de compensación con relleno o margen.
Lo que vamos a hacer es construir la sombra directamente en el elemento pintándolo en el border-image
área. Esto tiene algunas ventajas clave:
border-width
se suma al ancho total del elemento- El contenido no se extenderá al área del borde ni se superpondrá a la sombra.
- El relleno no necesitará ningún ancho adicional para acomodar la sombra y el contenido.
- Los márgenes alrededor del elemento no interferirán con los hermanos de ese elemento.
Para el grupo antes mencionado de cien desarrolladores que han usado box-shadow
, es probable que solo unos pocos lo hayan hecho border-image
. Es una propiedad original. Básicamente, toma una imagen y la corta en nueve partes, luego las coloca en las cuatro esquinas, los lados y (opcionalmente) el centro. Puedes leer más sobre cómo funciona todo esto en el artículo de Nora Brown.
La API CSS Paint se encargará del trabajo pesado de generar la imagen. Vamos a crear un módulo que le indica cómo superponer una serie de sombras una encima de la otra. Luego, esa imagen será utilizada por border-image
.
Estos son los pasos que daremos:
- Configure el HTML y CSS para el elemento que queremos pintar.
- Crea un módulo que dibuje la imagen.
- Cargue el módulo en un trabajo de pintura.
- Llama al worklet en CSS con la nueva
paint()
función
Configurando el lienzo
Escuchará el término lienzo varias veces aquí y en otros recursos de CSS Paint API. Si ese término te suena familiar, tienes razón. La API funciona de forma similar al canvas
elemento HTML.
Primero, tenemos que configurar el lienzo sobre el que pintará la API. Esta área tendrá las mismas dimensiones que el elemento que llama a la función pintar. Hagamos una división de 300×300.
section div/div/section
Y los estilos:
.foo { border: 15px solid #efefef; box-sizing: border-box; height: 300px; width: 300px;}
Creando la clase de pintura
Se requiere HTTPS para cualquier worklet de JavaScript, incluidos los worklets de pintura. No podrás usarlo en absoluto si entregas tu contenido a través de HTTP.
El segundo paso es crear el módulo que se carga en el worklet: un archivo simple con la registerPaint()
función. Esta función toma dos argumentos: el nombre del worklet y una clase que tiene la lógica de pintura. Para mantenernos ordenados, usaremos una clase anónima.
registerPaint( "shadow", class {});
En nuestro caso, la clase necesita dos atributos inputProperties
y inputArguments
un método paint()
.
registerPaint( "shadow", class { static get inputProperties() { return []; } static get inputArguments() { return []; } paint(context, size, props, args) {} });
inputProperties
y inputArguments
son opcionales, pero necesarios para pasar datos a la clase.
Agregar propiedades de entrada
Necesitamos decirle al worklet con qué propiedades CSS extraer del elemento de destino inputProperties
. Es un captador que devuelve una serie de cadenas.
En esta matriz, enumeramos las propiedades personalizadas y estándar que la clase necesita: --shadow-colors
, background-color
y border-top-width
. Preste especial atención a cómo utilizamos las propiedades no taquigráficas.
static get inputProperties() { return ["--shadow-colors", "background-color", "border-top-width"];}
Para simplificar, asumimos aquí que la frontera es pareja en todos los lados.
Agregar argumentos
Actualmente, inputArguments
todavía están detrás de una bandera, lo que permite funciones experimentales. Sin ellos, utilice inputProperties
propiedades personalizadas en su lugar.
También pasamos argumentos al módulo paint con inputArguments
. A primera vista, pueden parecer superfluos inputProperties
, pero existen diferencias sutiles en la forma en que se utilizan.
Cuando se llama a la función paint en la hoja de estilo, inputArguments
se pasan explícitamente en la paint()
llamada. Esto les da una ventaja sobre inputProperties
, que podría estar escuchando propiedades que podrían ser modificadas por otros scripts o estilos. Por ejemplo, si está utilizando una propiedad personalizada establecida en :root
esos cambios, puede filtrarse y afectar la salida.
La segunda diferencia importante para inputArguments
, que no es intuitiva, es que no tienen nombre. En cambio, se hace referencia a ellos como elementos de una matriz dentro del método de pintura. Cuando decimos inputArguments
lo que está recibiendo, en realidad le estamos dando el tipo de argumento.
La shadow
clase necesitará tres argumentos: uno para las posiciones X, uno para las posiciones Y y otro para los desenfoques. Lo configuraremos como tres listas de números enteros separados por espacios.
Cualquiera que haya registrado una propiedad personalizada puede reconocer la sintaxis. En nuestro caso, la integer
palabra clave significa cualquier número entero +
, mientras que denota una lista separada por espacios.
static get inputArguments() { return ["integer+", "integer+", "integer+"];}
Para usarlo inputProperties
en lugar de inputArguments
, puede establecer propiedades personalizadas directamente en el elemento y escucharlas. El espacio de nombres sería fundamental para garantizar que las propiedades personalizadas heredadas de otros lugares no se filtren.
Agregar el método de pintura
Ahora que tenemos las entradas, es hora de configurar el método de pintura.
Un concepto clave paint()
es el de context
objeto. Es similar y funciona de manera muy similar al canvas
contexto del elemento HTML, aunque con algunas pequeñas diferencias. Actualmente, no puedes leer los píxeles del lienzo (por razones de seguridad) ni renderizar texto (hay una breve explicación del motivo en este hilo de GitHub).
El paint()
método tiene cuatro parámetros implícitos:
- El objeto de contexto
- Geometría (un objeto con ancho y alto)
- Propiedades (un mapa de
inputProperties
) - Argumentos (los argumentos pasados de la hoja de estilo)
paint(ctx, geom, props, args) {}
Obteniendo las dimensiones
El geometry
objeto sabe qué tan grande es el elemento, pero necesitamos ajustarlo para los 30 píxeles de borde total en los ejes X e Y:
const width = (geom.width - borderWidth * 2);const height = (geom.height - borderWidth * 2);
Usando propiedades y argumentos
Las propiedades y los argumentos contienen los datos resueltos de inputProperties
y inputArguments
. Las propiedades vienen como un objeto similar a un mapa y podemos extraer valores con get()
y getAll()
:
const borderWidth = props.get("border-top-width").value;const shadowColors = props.getAll("--shadow-colors");
get()
devuelve un valor único, mientras que getAll()
devuelve una matriz.
--shadow-colors
Será una lista de colores separados por espacios que se pueden incluir en una matriz. Lo registraremos en el navegador más tarde para que sepa qué esperar.
También necesitamos especificar con qué color rellenar el rectángulo. Utilizará el mismo color de fondo que el elemento:
ctx.fillStyle = props.get("background-color").toString();
Como se mencionó anteriormente, los argumentos ingresan al módulo como una matriz y los referenciamos por índice. Son del tipo CSSStyleValue
en este momento; Hagamos que sea más fácil iterarlos:
- Convierte el
CSSStyleValue
en una cadena con sutoString()
método. - Divida el resultado en espacios con una expresión regular
const blurArray = args[2].toString().split(/s+/);const xArray = args[0].toString().split(/s+/);const yArray = args[1].toString().split(/s+/);// e.g. ‘1 2 3’ - [‘1’, ‘2’, ‘3’]
dibujando las sombras
Ahora que tenemos las dimensiones y propiedades, ¡es hora de dibujar algo! Como necesitamos una sombra para cada elemento en shadowColors
, los recorreremos en bucle. Comience con un forEach()
bucle:
shadowColors.forEach((shadowColor, index) = { });
Con el índice de la matriz, tomaremos los valores coincidentes de los argumentos X, Y y desenfoque:
shadowColors.forEach((shadowColor, index) = { ctx.shadowOffsetX = xArray[index]; ctx.shadowOffsetY = yArray[index]; ctx.shadowBlur = blurArray[index]; ctx.shadowColor = shadowColor.toString();});
Finalmente, usaremos el fillRect()
método para dibujar en el lienzo. Se necesitan cuatro argumentos: posición X, posición Y, ancho y alto. Para los valores de posición, usaremos border-width
desde inputProperties
; De esta manera, border-image
se recorta para contener solo la sombra alrededor del rectángulo.
shadowColors.forEach((shadowColor, index) = { ctx.shadowOffsetX = xArray[index]; ctx.shadowOffsetY = yArray[index]; ctx.shadowBlur = blurArray[index]; ctx.shadowColor = shadowColor.toString(); ctx.fillRect(borderWidth, borderWidth, width, height);});
Esta técnica también se puede realizar utilizando un filtro de sombra paralelo de lienzo y un solo rectángulo. Es compatible con Chrome, Edge y Firefox, pero no con Safari. Vea un ejemplo terminado en CodePen.
¡Casi llegamos! Sólo quedan unos pocos pasos más para conectar todo.
Registro del módulo de pintura
Primero debemos registrar nuestro módulo como un worklet de pintura con el navegador. Esto se hace en nuestro archivo JavaScript principal:
CSS.paintWorklet.addModule("https://codepen.io/steve_fulghum/pen/bGevbzm.js");https://codepen.io/steve_fulghum/pen/BazexJX
Registrar propiedades personalizadas
Algo más que deberíamos hacer, pero no es estrictamente necesario, es informarle al navegador un poco más sobre nuestras propiedades personalizadas registrándolas .
Registrar propiedades les da un tipo . Queremos que el navegador sepa que --shadow-colors
es una lista de colores reales , no solo una cadena.
Si necesita dirigirse a navegadores que no admitan la API de Propiedades y Valores, ¡no se desespere! El módulo de pintura aún puede leer las propiedades personalizadas, incluso si no están registradas. Sin embargo, serán tratados como valores no analizados, que en realidad son cadenas. Deberá agregar su propia lógica de análisis.
Me gusta addModule()
, esto se agrega al archivo JavaScript principal:
CSS.registerProperty({ name: "--shadow-colors", syntax: "color+", initialValue: "black", inherits: false});
También puede utilizarlo @property
en su hoja de estilo para registrar propiedades. Puedes leer una breve explicación en MDN.
Aplicando esto a la imagen del borde
Nuestro worklet ahora está registrado en el navegador y podemos llamar al método paint en nuestro archivo CSS principal para que reemplace la URL de una imagen:
border-image-source: paint(shadow, 0 0 0, 8 2 1, 8 5 3) 15;border-image-slice: 15;
Estos son valores sin unidades. Como estamos dibujando una imagen 1:1, equivale a píxeles.
Adaptarse a las proporciones de visualización
Ya casi hemos terminado, pero hay un problema más que abordar.
Para algunos de ustedes, es posible que las cosas no sean como esperaban. Apuesto a que te decantaste por el elegante monitor de alto DPI, ¿no? Hemos encontrado un problema con la proporción de píxeles del dispositivo. Las dimensiones que se pasaron al trabajo de pintura no se han escalado para que coincidan.
En lugar de revisar y escalar cada valor manualmente, una solución sencilla es multiplicar el border-image-slice
valor. A continuación se explica cómo hacerlo para una visualización adecuada en varios entornos.
Primero, registremos una nueva propiedad personalizada para CSS que exponga window.devicePixelRatio
:
CSS.registerProperty({ name: "--device-pixel-ratio", syntax: "number", initialValue: window.devicePixelRatio, inherits: true});
Dado que estamos registrando la propiedad y dándole un valor inicial, no necesitamos activarla :root
porque inherit: true
la pasa a todos los elementos.
Y, por último, multiplicaremos nuestro valor border-image-slice
por calc()
:
.foo { border-image-slice: calc(15 * var(--device-pixel-ratio));}
Es importante tener en cuenta que los worklets de pintura también tienen acceso al devicePixelRatio
valor de forma predeterminada. Simplemente puede hacer referencia a él en la clase, por ejemplo console.log(devicePixelRatio)
.
Finalizado
¡Uf! ¡Ahora deberíamos tener una imagen a escala adecuada pintada en los límites del área fronteriza!
Bonificación: aplique esto a una imagen de fondo
Sería negligente no demostrar también una solución que utilice background-image
en lugar de border-image
. Es fácil de hacer con sólo unas pocas modificaciones al módulo que acabamos de escribir.
Como no hay un border-width
valor para usar, lo convertiremos en una propiedad personalizada:
CSS.registerProperty({ name: "--shadow-area-width", syntax: "integer", initialValue: "0", inherits: false});
También tendremos que controlar el color de fondo con una propiedad personalizada. Dado que estamos dibujando dentro del cuadro de contenido, configurar un valor real background-color
aún se mostrará detrás de la imagen de fondo.
CSS.registerProperty({ name: "--shadow-rectangle-fill", syntax: "color", initialValue: "#fff", inherits: false});
Luego configúrelos .foo
:
.foo { --shadow-area-width: 15; --shadow-rectangle-fill: #efefef;}
Esta vez, paint()
se activa background-image
, usando los mismos argumentos que usamos para border-image
:
.foo { --shadow-area-width: 15; --shadow-rectangle-fill: #efefef; background-image: paint(shadow, 0 0 0, 8 2 1, 8 5 3);}
Como era de esperar, esto pintará la sombra en el fondo. Sin embargo, dado que las imágenes de fondo se extienden hasta el cuadro de relleno, tendremos que ajustarlas padding
para que el texto no se superponga:
.foo { --shadow-area-width: 15; --shadow-rectangle-fill: #efefef; background-image: paint(shadow, 0 0 0, 8 2 1, 8 5 3); padding: 15px;}
Reservas
Como todos sabemos, no vivimos en un mundo donde todos usan el mismo navegador o tienen acceso a lo último y lo mejor. Para asegurarnos de que no reciban un diseño defectuoso, consideremos algunas alternativas.
Arreglo de relleno
El relleno en el elemento principal condensará el cuadro de contenido para dar cabida a las sombras que se extienden desde sus elementos secundarios.
section.parent { padding: 6px; /* size of shadow on child */}
Corrección de margen
Los márgenes de los elementos secundarios se pueden utilizar para espaciar, para mantener las sombras alejadas de sus padres recortados:
div.child { margin: 6px; /* size of shadow on self */}
Combinando imagen de borde con un degradado radial
Esto es un poco más fuera de lo común que el relleno o los márgenes, pero tiene una excelente compatibilidad con el navegador. CSS permite usar degradados en lugar de imágenes, por lo que podemos usar uno dentro de un border-image
, tal como lo hicimos con paint()
. Esta puede ser una excelente opción como alternativa para la solución Paint API, siempre y cuando el diseño no requiera exactamente la misma sombra:
Los degradados pueden ser delicados y complicados de lograr, pero Geoff Graham tiene un excelente artículo sobre su uso.
div { border: 6px solid; border-image: radial-gradient( white, #aaa 0%, #fff 80%, transparent 100% ) 25%;}
Un pseudoelemento desplazado
Si no le importa un poco de marcado adicional y posicionamiento CSS, y necesita una sombra exacta, también puede usar un pseudoelemento insertado. ¡Cuidado con el z-index
! Dependiendo del contexto, es posible que sea necesario ajustarlo.
.foo { box-sizing: border-box; position: relative; width: 300px; height: 300px; padding: 15px;}.foo::before { background: #fff; bottom: 15px; box-shadow: 0px 2px 8px 2px #333; content: ""; display: block; left: 15px; position: absolute; right: 15px; top: 15px; z-index: -1;}
Pensamientos finales
Y así, amigos, es como pueden usar CSS Paint API para pintar solo la imagen que necesitan. ¿Es lo primero que debe alcanzar en su próximo proyecto? Bueno, eso lo decide tú. La compatibilidad con el navegador aún está disponible, pero avanza.
Para ser justos, puede agregar mucha más complejidad de la que requiere un problema simple. Sin embargo, si se encuentra en una situación que requiere que los píxeles se coloquen justo donde los desea, CSS Paint API es una herramienta poderosa.
Sin embargo, lo más emocionante es la oportunidad que brinda a los diseñadores y desarrolladores. Dibujar sombras es sólo un pequeño ejemplo de lo que puede hacer la API. Con un poco de imaginación e ingenio, son posibles todo tipo de nuevos diseños e interacciones.
Otras lecturas
- La especificación de la API de pintura CSS
- ¿Está Houdini listo todavía?
- API de pintura CSS (desarrolladores web de Google)
- Experimentos CSS Houdini
- Otro ejemplo que utiliza Paint API para dibujar triángulos y entradas de radio.
Deja un comentario