Explorando la API de CSS Paint: Redondeo de formas parte 1

by Silvia Mazzetta Date: 25-10-2021 css javascript

Añadir bordes a las formas complejas es un auténtico rollo (a veces), pero redondear las esquinas de las formas complejas es un suplicio jejeje. Por suerte, la API de pintura de CSS o CSS Paint API ha llegado a rescatarnos.




 

En general, cuando trabajamos con la API de pintura CSS seguimos el siguiente patrón:

  • Escribimos un CSS básico que podemos ajustar fácilmente.
  • La lógica compleja se encuetra de la escena dentro de la función paint().
 

En realidad podemos hacer esto sin la API de Paint
Probablemente hay muchas maneras de poner esquinas redondeadas en formas complejas, pero voy a compartir con ustedes tres métodos que he utilizado en mi propio trabajo.

Vosotros diréis: si ya conoces tres métodos, entonces ¿por qué usas la API de Paint?
Buena pregunta. La respuesta es simple: siempre hay que estudiar y formarse para poder abarcar y resolver un problema desde varios puntos de vista.

En este caso podéis utilizar:

Método clip-path: path()

Si eres un gurú de SVG o simplemente os gustan las imágenes vectoriales, este método es para ti. La propiedad clip-path acepta rutas SVG. Eso significa que podemos pasar fácilmente la ruta de una forma redondeada compleja y listo. Este método es súper fácil si ya tienes la forma que quieres, pero es inadecuado si quieres una forma modificable a la que, por ejemplo, quieres ajustar el radio.

Abajo un ejemplo de una forma de hexágono redondeado.

¡Suerte tratando de ajustar la curvatura y el tamaño de la forma ;-)!

Es vuestro primer reto ;-). Yo os pasaré la pelota, ya que hoy iremos a tratar est mismo problema utilizando la Paint API.

<div class="box"></div>
.box {    
width: 150px;    
height: 150px;    
background: red;    
clip-path: path(      
"M38,2 L82,2 A12,12 0 0,1 94,10 L112,44 A12,12 0 0,1 112,56 L94,90 A12,12 0 0,1 82,98 L38,98 A12,12 0 0,1 26,90 L8,56 A12,12 0 0,1 8,44 L26,10 A12,12 0 0,1 38,2");  
}

 

Filtros SVG

 

La idea es aplicar un filtro SVG a cualquier elemento para redondear sus esquinas.

 
 

Simplemente usamos clip-path para crear la forma que queremos y luego aplicamos el filtro SVG en un elemento padre.

Para controlar el radio, ajustamos la variable stdDeviation.

Esta es una buena técnica, pero requiere un profundo nivel de conocimientos de SVG.

 

Usando un enfoque "sólo CSS"

Ana Tudor ha encontrado una técnica sólo en CSS para un efecto que podemos utilizar para redondear la esquina de formas complejas.

Abajo una demo donde reemplazo el filtro SVG con su técnica:

 
 

Utilizamos la Paint API

Aquí vamos a explicar el método como alcanzar nuestro objetivo con la API Paint.

Utilizaremos la variable --path que define nuestra forma, la función cc() para convertir nuestros puntos, y algunos otros trucos que cubriremos en el camino.

Primero, la configuración del CSS




 

Empezamos con un elemento rectangular clásico y definimos nuestra forma dentro de la variable --path (forma 2 arriba). La variable --path se comporta de la misma manera que la trayectoria que definimos dentro de clip-path: polygon(). Puedes utulizar Clippy para generarlo.

 
.box {
display: inline-block;
height: 200px;
width: 200px;
--path: 50% 0,100% 100%,0 100%;
--radius: 20px;
-webkit-mask: paint(rounded-shape);  
}
 

Hasta aquí nada complejo. Aplicamos la máscara personalizada y definimos la variable --path y una --radius. Esta última se utilizará para controlar la curvatura.

 

La configuración del código JavaScript




 

Además de los puntos definidos por la variable path (representados como puntos rojos arriba), estamos añadiendo aún más puntos (representados como puntos verdes arriba) que son simplemente los puntos medios de cada segmento de la forma. A continuación, utilizamos la función arcTo() para construir la forma final (la forma 4 de arriba).

Añadir los puntos medios es bastante fácil, pero usar arcTo() es un poco complicado porque tenemos que entender cómo funciona.
Según MDN:

Añade un arco circular a la sub-ruta actual, usando los puntos de control y el radio dados. El arco se conecta automáticamente al último punto de la ruta con una línea recta, si es necesario para los parámetros especificados.

Este método se utiliza habitualmente para realizar esquinas redondeadas.

El hecho de que este método requiera puntos de control es la razón principal de los puntos intermedios extra. También requiere un radio (que estamos definiendo como una variable llamada --radius).

Si seguimos leyendo la documentación de MDN:

Una forma de pensar en arcTo() es imaginar dos segmentos rectos: uno desde el punto inicial hasta un primer punto de control, y otro desde allí hasta un segundo punto de control. Sin arcTo(), estos dos segmentos formarían una esquina aguda: arcTo() crea un arco circular que se ajusta a esta esquina y la suaviza. En otras palabras, el arco es tangente a ambos segmentos.

Cada arco/esquina se construye con tres puntos. Si compruebas la figura anterior, observa que para cada esquina tenemos un punto rojo y dos puntos verdes en cada lado. Cada combinación rojo-verde crea un segmento para obtener los dos segmentos detallados anteriormente.

Vamos a ampliar una esquina para entender mejor lo que ocurre:




 

Ahora imaginemos que tenemos un camino que va del primer punto verde al siguiente punto verde, moviéndose alrededor de ese círculo. Hacemos esto para cada esquina y tenemos nuestra forma redondeada.

Así es como se ve en el código:

// We first read the variables for the path and the radius.
  const points = properties.get('--path').toString().split(',');
const r = parseFloat(properties.get('--radius').value);var Ppoints = [];
  var Cpoints = [];
  const w = size.width;
  const h = size.height;
  var N = points.length;
  var i;
  // Then we loop through the points to create two arrays.
  for (i = 0; i < N; i++) {
  var j = i-1;
  if(j<0) j=N-1;
 
  var p = points[i].trim().split(/(?!(.*)s(?![^(]*?))/g);
  // One defines the red points (Ppoints)
  p = cc(p[0],p[1]);
  Ppoints.push([p[0],p[1]]);
  var pj = points[j].trim().split(/(?!(.*)s(?![^(]*?))/g);
  pj = cc(pj[0],pj[1]);
  // The other defines the green points (Cpoints)
  Cpoints.push([p[0]-((p[0]-pj[0])/2),p[1]-((p[1]-pj[1])/2)]);
  }
/* ... */
// Using the arcTo() function to create the shape
  ctx.beginPath();
  ctx.moveTo(Cpoints[0][0],Cpoints[0][1]);
  for (i = 0; i < (Cpoints.length - 1); i++) {
  ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[i+1][0],Cpoints[i+1][1], r
  }
  ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[0][0],Cpoints[0][1], r);

  ctx.closePath();
/* ... */
ctx.fillStyle = '#000';
ctx.fill();
 

El último paso es rellenar nuestra forma con un color sólido. Ahora tenemos nuestra forma redondeada y podemos usarla como máscara en cualquier elemento.

Ya está. ¡Ahora todo lo que tenemos que hacer es construir nuestra forma y controlar el radio como queremos - un radio que podemos animar, gracias a @property que hará las cosas más interesantes!




 

¿Existen inconvenientes con este método?

Sí, hay inconvenientes, y probablemente los hayas notado en el último ejemplo. El primer inconveniente está relacionado con el área que se puede pasar por encima. Como estamos utilizando la máscara, todavía podemos interactuar con la forma rectangular inicial. Recuerda que nos enfrentamos al mismo problema con el borde del polígono y utilizamos clip-path para solucionarlo. Desafortunadamente, el clip-path no ayuda aquí porque también afecta a la esquina redondeada.

Tomemos el último ejemplo y añadamos clip-path. Observe cómo perdemos la curvatura "hacia adentro".

 
 

No hay ningún problema con las formas hexagonales y triangulares, pero a las otras les faltan algunas curvas. Podría ser interesante mantener sólo la curvatura exterior -gracias al clip-path- y al mismo tiempo fijar el área hoverizable. Pero no podemos mantener todas las curvaturas y reducir el área hoverizable al mismo tiempo.

¿El segundo problema? Está relacionado con el uso de un valor de radio grande. Pasa el ratón por encima de las formas de abajo y mira los resultados locos que obtenemos:

 
 

En realidad no es un inconveniente "importante" ya que tenemos control sobre el radio, pero seguro que sería bueno evitar esta situación en caso de que utilicemos erróneamente un valor de radio demasiado grande. Podríamos arreglar esto limitando el valor del radio a un rango que lo limite a un valor máximo. Para cada esquina, calculamos el radio que nos permite tener el arco más grande sin ningún desbordamiento. No voy a profundizar en la lógica matemática detrás de esto, pero aquí está el código final para limitar el valor del radio:

 
var angle = 
Math.atan2(Cpoints[i+1][1] - Ppoints[i][1], Cpoints[i+1][0] - Ppoints[i][0]) -
Math.atan2(Cpoints[i][1]   - Ppoints[i][1], Cpoints[i][0]   - Ppoints[i][0]);
if (angle < 0) {
  angle += (2*Math.PI)
}
if (angle > Math.PI) {
  angle = 2*Math.PI - angle
}
var distance = Math.min(
  Math.sqrt(
    (Cpoints[i+1][1] - Ppoints[i][1]) ** 2 + 
    (Cpoints[i+1][0] - Ppoints[i][0]) ** 2),
  Math.sqrt(
    (Cpoints[i][1] - Ppoints[i][1]) ** 2 + 
    (Cpoints[i][0] - Ppoints[i][0]) ** 2)
  );
var rr = Math.min(distance * Math.tan(angle/2),r);
 

r es el radio que estamos definiendo y rr es el radio que realmente estamos usando. Es igual a r o al valor máximo permitido sin desbordamiento.

 
 

Si pasas el ratón por las formas en esa demostración, ya no obtenemos formas extrañas sino la "forma redondeada máxima" (acabo de acuñar esto) en su lugar. Fíjate en que los polígonos regulares (como el triángulo y el hexágono) tienen lógicamente un círculo como su "forma redondeada máxima", por lo que podemos tener transiciones o animaciones geniales entre diferentes formas.

 

¿Y para los bordes?

Sí. Todo lo que tenemos que hacer es utilizar stroke() en lugar de fill() dentro de nuestra función paint(). Así, en lugar de usar

 
ctx.fillStyle = '#000';  
ctx.fill();
 

usaremos

 
ctx.lineWidth = b;
ctx.strokeStyle = '#000';
ctx.stroke();
 

Esto introduce otra variable, b, que controla el grosor del borde.

 
 

¿Te has dado cuenta de que tenemos un extraño desbordamiento? Nos enfrentamos al mismo problema en el artículo anterior, y eso debido a cómo funciona stroke(). Cité a MDN en ese artículo y lo haré de nuevo aquí también:

Los trazos se alinean con el centro de un trazado; en otras palabras, la mitad del trazo se dibuja en el lado interior, y la otra mitad en el lado exterior.

De nuevo, es ese "mitad lado interior, mitad lado exterior" lo que nos está afectando. Para solucionarlo, tenemos que ocultar el lado exterior usando otra máscara, la primera en la que usamos el fill(). Primero, necesitamos introducir una variable condicional a la función paint() para elegir si queremos dibujar la forma o sólo su borde.

Esto es lo que tenemos:

 
if(t==0) {
  ctx.fillStyle = '#000';
  ctx.fill();
  } else {
  ctx.lineWidth = 2*b;
  ctx.strokeStyle = '#000';
  ctx.stroke();
  }
 

A continuación, aplicamos el primer tipo de máscara (t=0) sobre el elemento principal, y el segundo tipo (t=1) sobre un pseudoelemento. La máscara aplicada en el pseudoelemento produce el borde (el que tiene el problema de desbordamiento). La máscara aplicada en el elemento principal resuelve el problema del desbordamiento ocultando la parte exterior del borde. Y si te lo estás preguntando, esa es la razón por la que estamos añadiendo el doble del grosor del borde a lineWidth.




 

¿Lo ves? Tenemos formas redondeadas perfectas como contornos y podemos ajustar el radio al pasar por encima. Y podemos usar cualquier tipo de fondo en la forma.

Y ahora un poco de CSS:

 
div {
  --radius: 5px; /* Defines the radius */
  --border: 6px; /* Defines the border thickness */
  --path: /* Define your shape here */;
  --t: 0; /* The first mask on the main element */
  
  -webkit-mask: paint(rounded-shape);
  transition: --radius 1s;
}
div::before {
  content: "";
   background: ..; /* Use any background you want */
  --t: 1; /* The second mask on the pseudo-element */
  -webkit-mask: paint(rounded-shape); /* Remove this if you want the full shape */
}
div[class]:hover {
  --radius: 80px; /* Transition on hover */
}
 

No olvidemos que podemos introducir fácilmente guiones utilizando setLineDash() de la misma manera que hicimos en el artículo anterior.




 
 
by Silvia Mazzetta Date: 25-10-2021 css javascript visitas : 2603  
 
Silvia Mazzetta

Silvia Mazzetta

Web Developer, Blogger, Creative Thinker, Social media enthusiast, Italian expat in Spain, mom of little 9 years old geek, founder of  @manoweb. A strong conceptual and creative thinker who has a keen interest in all things relate to the Internet. A technically savvy web developer, who has multiple  years of website design expertise behind her.  She turns conceptual ideas into highly creative visual digital products. 

 
 
 

Artículos relacionados

Nuevas unidades de ventana gráfica - CSS

'Intercop 2022' es un proyecto anunciado por Google, Microsoft, Apple y la fundación Mozzilla para que todos los navegadores ofrezcan la misma experiencia web. El proyecto tiene 15 áreas de enfoque…

Container queries: libera el poder de escribir media queries

El diseño web responsivo ha sido una práctica común durante años, permitiendo que los sitios se adapten y se vean bien en una variedad de dispositivos y tamaños de pantalla.…

¿Por qué no deberíamos usar el color negro?

A día de hoy es cada vez más frecuente que las páginas cuenten con la opción de ponerlas en modo oscuro, o que directamente basen su estética en los colores…

Utilidades de los selectores :where y :is

Si aún no has oído hablar de estos selectores de pseudoclases, no te preocupes, aquí te explicamos el funcionamiento y dejamos varios usos que nos pueden ser muy útiles a…

Cómo centrar un Div con CSS - 10 formas diferentes

Para los desarrolladores web, a veces centrar un div parece uno de los trabajos más difíciles del mundo. Pues ya no. En este artículo, aprenderás 10 formas diferentes de centrar un…

Como hacer tu propio cursor personalizado para tu web

Cuando empecé a ojear webs distintas y originales para aprender de ellas, de las primeras cosas que me llamaron la atención fue que algunas de ellas tenían sus propios cursores,…

Explorando la API CSS Paint: Redondeo de formas parte 2

Vamos con la segunda parte de nuestro artícul sobre como manejar la API Paint para redondar bordes complejos   Control de los radios   En todos los ejemplos que hemos visto, siempre consideramos un…

Las ventanas gráficas o viewports grandes, pequeñas y dinámicas

Recientemente se han introducido algunos cambios en materia de unidades de ventana gráfica o viewport. Las novedades -que forman parte de la especificación de valores y unidades CSS de nivel 4-…

Imágenes de fondo responsivas con relación de aspecto fijo o fluido

¿Cuál es la forma más fácil de escalar las imágenes de fondo en los diseños responsivos? Utilizamos una antigua técnica y la mejoramos para cambiar con fluidez la ratio de…

Comenzando con Bootstrap-Vue paso a paso

Hoy te mostraremos cómo usar BootstrapVue, describiremos el proceso de instalación y mostraremos la funcionalidad básica. El proyecto está basado en el framework CSS más popular del mundo - Bootstrap, para…

Bootstrap 5 beta2. ¿Qué ofrece?

Dado que el lanzamiento de Bootstrap 4 es de tres años, en este artículo vamos a presentar lo que es nuevo en el marco más popular del mundo para la…

Creación de un sencillo spinner-loader CSS

En el artículo de hoy mostraremos cómo animar un loader básico que gira cuando se define alguna acción predefinida, como cargar una imagen. Eso se puede utilizar en un sitio…

Clicky