Generador de Pirámides 3D

Hace algún tiempo, cuando comencé a investigar con las pirámides en CSS, pensé que molaría crear un Generador de Pirámides 3D, donde introduciendo un ancho para la base y una altura se generara una pirámide personalizada, inspirado en gran medida en el Generador de Cuboides CSS de Jhey , al que buen y repetido uso le he dado.

Hice algunas pruebas pero mis conocimientos de trigonometría estaban algo oxidados, solo llegué a calcular la longitud de la hipotenusa y me quedé atascado intentando desarrollar una función en Javascript para sacar el ángulo de inclinación de las paredes de la pirámide.

Hace poco volví a la carga repasando algunos conceptos básicos de trigonometría ayudado por la Calculadora de Ángulos de Pirámides de www.omnicalculator.com … ¡y ya lo tenemos!

Trigonometría

Según la Real Academia Española :

Estudio de las relaciones numéricas entre los elementos que forman los triángulos planos y esféricos.

La descripción de la Wikipedia nos da algo más de información :

Rama de la matemática cuyo significado etimológico es ‘la medición de los triángulos’. Deriva de los términos griegos τριγωνοϛ (trigōnos) ‘triángulo’ y μετρον (metron) ‘medida’.

Vamos, que no hay que asustarse, que solo vamos a trabajar con triángulos y los ángulos que los forman. En realidad solo usaremos el bien conocido por todos Teorema de Pitágoras y una de las funciones trigonométricas más básicas, la del seno.

Lo que hay que tener en cuenta es que vamos a trabajar con un triángulo rectángulo que estará formado por: la altura de la pirámide, la mitad del ancho de la base, y una de las caras inclinadas de la pirámide.

Las longitudes de los catetos a y b (que son altura y base respectivamente) ya las tendremos, y con esos dos datos debemos hallar la hipotenusa (longitud de la cara inclinada) y el ángulo alfa (grado de inclinación de esa cara).

Teorema de Pitágoras

Quien no recuerda del colegio ese mantra que todos aprendímos:

«La hipotenusa al cuadrado es igual a la suma de los cuadrados de los catetos.»

En nuestra pirámide vamos a poder controlar el ancho de la base y la altura de la cúspide, por lo que vamos a disponer de dos datos que van a ser las longitudes de los dos catetos, con eso nos vale para obtener la longitud de la hipotenusa.

Función Trigonométrica Seno

Una vez que tengamos la hipotenusa podemos calcular el ángulo de inclinación de esta, que en nuestra pirámide van a ser sus cuatro caras. A este ángulo lo vamos a llamar alfa (α ) y lo vamos a calcular con la función trigonométrica del seno.

El seno de α es igual a la longitud del cateto opuesto dividido por la longitud de la hipotenusa.

En nuestro caso el cateto opuesto (opuesto al ángulo alfa) será siempre la altura de la pirámide y la hipotenusa la hemos sacado antes con el Teorema de Pitágoras, con esos dos datos ya podemos obtener el seno de alfa fácilmente. ¡Ojo! tendremos el seno de alfa, y no el ángulo alfa en grados, que es lo que necesitamos (aquí es donde me perdí la primera vez).

Ya hemos visto de que manera lo vamos a hacer, y ahora, vamos al lío… usaremos HTML, CSS y Javascript.

HTML

Para probar nuestra herramienta genera-pirámides vamos a usar el mínimo HTML necesario, un par de input tipo range con sus label y a continuación el elemento div que contiene nuestra pirámide. Con los deslizadores controlaremos el ancho de la base y la altura de la pirámide, y los cambios, al final, deberían mostrarse en vivo…

<label>BASE<input id="base" type="range" min="5" max="50" value="20" step="1"></label>
<label>ALTO<input id="apex" type="range" min="5" max="50" value="15" step="1"></label>

<div class="pyramid" id="pyramid">
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>

Aún no funcionan los deslizadores ni se llega a formar correctamente la pirámide..

Después habrá que añadir alguna cosa mas en el HTML, pero de momento vamos a dejarlo así.

CSS

Con el CSS hacemos igual, vamos a mostrar aquí solo el mínimo necesario para que funcione la pirámide. Realmente la estructura HTML de la pirámide no tiene ningún misterio, se compone de un elemento div que es la base de la pirámide y dentro tiene cuatro elementos span que son las cuatro caras de la pirámide y que están inclinados hacia el centro.

Con media docena de reglas CSS lo podemos ir apañando.

.pyramid {
  --width: 20;
  --height: 15;
  --hypo: 18.03;
  --alpha: 56.31deg;
  width: calc(var(--width) * 1vmin);
  height: calc(var(--width) * 1vmin);
  transform: rotateX(65deg) rotateZ(-160deg);
  transform-style: preserve-3d;
  margin: calc(var(--height) * 0.5vmin) 0 calc(var(--height) * 0.25vmin);
  background: hsl(210deg 100% 50% / 0.5);
}
.pyramid span {
  width: 100%;
  height: calc(var(--hypo) * 1vmin);
  --hypo2: calc(calc(var(--hypo) / 2) * 1vmin);
  position: absolute;
  box-sizing: border-box;
  clip-path: polygon(0% 100%, 50% 0%, 100% 100%);
  background: hsl(210deg 100% 50% / 0.5);
}
.pyramid span:nth-child(1) {
  transform-origin: center bottom;
  transform: rotateX(calc(var(--alpha) * -1));
  bottom: 0;
  filter: brightness(0.65);
}
.pyramid span:nth-child(2) {
  transform-origin: right center;
  transform: rotateY(calc(var(--alpha) * 1)) rotateZ(-90deg) 
             translate3d(var(--hypo2), calc(var(--hypo2) * -1), 0vmin);
  filter: brightness(0.85);
}
.pyramid span:nth-child(3) {
  transform-origin: center top;
  transform: rotateX(calc(var(--alpha) * 1)) rotateZ(180deg) 
             translate3d(0vmin, calc(var(--hypo) * -1vmin), 0vmin);
  filter: brightness(1.15);
}
.pyramid span:nth-child(4) {
  transform-origin: left center;
  transform: rotateY(calc(var(--alpha) * -1)) rotateZ(90deg) 
             translate3d(calc(var(--hypo2) * -1), calc(var(--hypo2) * -1), 0vmin);
  filter: brightness(1.25);
}

Aún no funcionan los deslizadores, aunque la pirámide ya se muestra en 3D correctamente.

Custom Properties

Lo único que hay que destacar del CSS son las cuatro custom properties del principio y lo que definen: el ancho --width , el alto --height , la hipotenusa --hypo y el ángulo alfa --alpha.

.pyramid {
  --width: 20;
  --height: 15;
  --hypo: 18.03;
  --alpha: 56.31deg;
}

Más adelante las modificaremos con Javascript y con ello cambiaremos las medidas de nuestra pirámide en tiempo real.

clip-path

Otra cosa a destacar del CSS es la manera de cortar cada cara de la pirámide para crear el triángulo que necesitamos. Lo hacemos de manera muy fácil y además autoajustable con la propiedad clip-path en el selector .pyramid span que afecta a las cuatro caras de la pirámide.

.pyramid span {
  clip-path: polygon(0% 100%, 50% 0%, 100% 100%);
}

A continuación se muestran algunos ejemplos más de como crear triángulos con la propiedad clip-path. Nosotros vamos a usar el que está situado en el centro.

.triangulo {
  width: 10vmin;
  height: 10vmin;
  background: hsl(210deg 100% 50% / 0.5);
}
.t-izquierda {
  clip-path: polygon(0% 100%, 0% 0%, 100% 100%);
}
.t-centro{
  clip-path: polygon(0% 100%, 50% 0%, 100% 100%);
}
.t-derecha{
  clip-path: polygon(0% 100%, 100% 0%, 100% 100%);
}

El borde punteado solo se ha añadido para destacar el borde real de cada elemento y ver mejor el corte.

Con este sistema, aunque la pirámide cambie de altura o anchura, el triangulo siempre estará centrado, y repitiéndolo en las cuatro caras encajarán perfectamente con cualquier altura o ancho de base.

Para saber más sobre clip-path podéis echar un vistazo al W3C, y no dejeis de probar Clippy, una útil herramienta que nos permite hacer maravillas con esta propiedad de CSS.

Javascript

Llegamos a la chicha… lo que hará que todo funcione… los cálculos de nuestro generador de pirámides.

Todos los cálculos de trigonometría que explicábamos al comienzo hay que hacerlos en Javascript y pasárselos a la pirámide mediante CSS properties, que pueden modificarse desde el propio Javascript.

Podemos separar el código en tres partes:

Lo primero que haremos será preparar las CSS properties para poder modificar las longitudes de base y altura desde los deslizadores, y en consecuencia esto modificará también la hipotenusa y el ángulo alfa.

var eps = document.getElementById('pyramid').style;
eps.setProperty("--width", '20' );
eps.setProperty("--height", '15' );
eps.setProperty("--hypo", '18.03' );
eps.setProperty("--alpha", '56.31deg' );

La primera línea define una variable para llamar a la propiedad style del elemento #pyramid .

Las otras cuatro líneas modifican la propiedad style y definien las propiedades iniciales de la pirámide.

Una vez hecho esto, en el segundo paso, vamos a crear sendas funciones para modificar las longitudes de base y altura con los deslizadores de los input de tipo range.

function setBase(val) { 
  eps.setProperty("--width", val );
  setAngle(); 
}

function setApex(val) {
  eps.setProperty("--height", val );
  setAngle();
}

Estas dos simples funciones, que perfectamente podrían fusionarse en una sola (tras un buen depurado), son muy claras:

La primera línea modifica la propiedad CSS, ya sea --width o --height, cogiendo el valor introducido en el respectivo deslizador y pasándolo como argumento de la función.

La segunda línea, después de modificar los valores de las propiedades CSS, llama a la función setAngle() y es en esta función donde haremos nuestros cálculos trigonométricos para hallar y definir la hipotenusa y el ángulo alfa.

Y por fin, el último paso, la función setAngle()

function setAngle() {
  var width = eps.getPropertyValue("--width");
  var height = eps.getPropertyValue("--height");
  var hypo = Math.sqrt((width/2 * width/2) + (height * height));
  var alpha = Math.asin(height / hypo) * (180 / Math.PI);
  eps.setProperty("--hypo", hypo);
  eps.setProperty("--alpha", alpha + 'deg');
}

Vamos a ver el código de esta función detalladamente, por líneas:

width y height

var width = eps.getPropertyValue("--width");
var height = eps.getPropertyValue("--height");

Las dos primeras líneas definen las variables para la base y la altura, cogiendo los valores de las respectivas propiedades CSS, de esta manera cada vez que se modifique una de ellas desde cualquier deslizador se actualizará aquí su valor.

hypo

var hypo = Math.sqrt((width/2 * width/2) + (height * height));

La siguiente línea calcula y define la hipotenusa usando el ya mencionado Teorema de Pitágoras, y como decíamos antes:

h2 = b2 + a2

Modificando ya el teorema con nuestras variables de base y altura quedaría algo así:

h2 = (width * width) + (height * height)

Hay que tener un par de cosas en cuenta:

La primera es que la longitud de la base que introducimos en el deslizador hay que dividirla entre dos, pues esa es la longitud del cateto de la base del triángulo que estamos midiendo, la mitad del ancho de la base, por lo que nuestro teorema cambiaría a algo así:

h2 = (width/2 * width/2) + (height * height)

La segunda cuestión a tener en cuenta es que lo que obtenemos en este cálculo es la hipotenusa al cuadrado, así que debemos calcular su raíz cuadrada para conseguir la longitud de la hipotenusa, que es lo que realmente buscamos. Esto lo haremos con la función Math.sqrt() de Javascript, encerrando dentro de los paréntesis toda la operación con los catetos, así:

hypo = Math.sqrt( (width/2 * width/2) + (height * height) )

Y ya tenemos la variable hypo con la longitud de la hipotenusa hallada a partir de los catetos.

alpha

var alpha = Math.asin(height / hypo) * (180 / Math.PI);

Pasamos a la siguiente línea para el último cálculo, el del ángulo alfa. Como ya avisábamos antes, empezaremos usando la función trigonométrica de seno para acercarnos al resultado, en Javascript para esto se usa Math.asin() que devuelve el seno de un número… pero ojo, lo devuelve en radianes, una de las unidades de medición de ángulos. Después veremos como proceder con esto, de momento, el comienzo sería algo así:

seno de alfa = cateto opuesto / hipotenusa

Añadiendo nuestras variables quedaría de esta manera:

sen α = height / hypo

Y encerrando la segunda parte dentro de la función seno de Javascript el resultado sería algo así:

alpha = Math.asin(height/hypo)

Pero esto aún no ha acabado, porque el valor de la variable alpha que acabamos de crear está en radianes y necesitamos que sean grados (y sexagesimales)… Por suerte está casi todo ya inventado y encontré la manera de hacerlo multiplicando el seno en radianes que obtenemos por 180/π, ya que una circunferencia completa son 2π radianes ó 360º. Por lo que (redoble de tambor) finalmente queda de la siguiente manera:

alpha = Math.asin(height/hypo) * (180 / Math.PI)

Y ahora si que si, tenemos por fin el ángulo alfa con el grado de inclinación de nuestra pirámide.

hypo y alpha

eps.setProperty("--hypo", hypo);
eps.setProperty("--alpha", alpha + 'deg');

Estas dos últimas líneas actualizan los valores de las propiedades CSS --hypo y --alpha con los valores que acabamos de calcular, rematando la función.

¿Y ya está…?

Pues no, falta un último paso en el HTML, que no quise añadirlo antes para no liarnos, y son las llamadas a las funciones setBase() y setApex() que se hacen desde los input tipo range. Llamando a la función con el atributo onchange el valor del input solo se modifica al soltar el deslizador, si llamamos a la función también con el atributo oninput el valor se modificará cada vez que este cambie de posición, aunque no soltemos el deslizador. Todo esto se puede hacer también con unos Listeners en Javascript.

<label>
  BASE
  <input id="base" type="range" min="5" max="50" value="20" step="1" 
         onchange="setBase(this.value)"
         oninput="setBase(this.value)">
</label>

<label>
  ALTO
  <input id="apex" type="range" min="5" max="50" value="15" step="1" 
         onchange="setApex(this.value)"
         oninput="setApex(this.value)">
</label>

Ahora sí, deberían funcionar los deslizadores y modificarse las medidas de la pirámide correctamente.

Solucionado ya el lío de los cálculos, y viendo las posibilidades, empezamos a pensar en nuevas opciones

Opciones

… y surgieron unas cuantas.

Tamaño de pirámide

Bueno, es la opción básica y de la que parte realmente toda la idea del generador de pirámides, pero ya que también es una opción la he añadido a esta lista.

Diseños de pared

Aprovechando los patrones teselados en CSS hemos añadido una docena de diseños diferentes para las paredes de la pirámide. La idea es crear algunos patrones nuevos, de tipo ladrillo/bloque, que estén más relacionados con la estructura de las pirámides.

Pirámide animada

Con algo de movimiento se aprecia mucho mejor el 3D, así que, hemos añadido una animación a la pirámide y la opción para el usuario de poder detenerla si así lo desea.

Copiar Código

Para que el generador sirviera de algo más que el simple disfrute visual, lo ideal sería poder llevarte el código de la pirámide generada, con su tamaño, su diseño de pared y su animación (o sin ella). Aunque usamos Javascript para generar el código de la pirámide (pues css no hace raíces cuadradas ni senos de ángulos… aún) el código generado para mostrar la pirámide es solo HTML y CSS. Como remate final hemos añadido unos botones que permiten copiar ambos códigos y de esta manera puedas crear fácilmente tus propias pirámides.

Panel de Datos

Y sacando provecho de todo lo aprendido en el primer #CodePenChallenge de este año con la pseudoclase :has() de CSS, he añadido también un panel de Datos de Trigonometría donde se muestra en vivo la información sobre la pirámide.

Al pasar el cursor por encima de cualquier dato del panel lo que hacemos con :has() es mostrar una regla de color sobre la propia pirámide que nos marcará dicho dato: el ancho de la base, la altura del vértice, la hipotenusa o el ángulo alfa.

Algunos navegadores como Firefox aún no soportan del todo la pseudoclase :has() y no se mostraran las reglas sobre la pirámide. De cualquier manera :has() va a ser algo muy útil desde el punto de vista del CSS, quizá debiera escribir un artículo sobre las mil opciones que ofrece…

En el tintero…

Siempre me pasa, a medida que avanzo se me van ocurriendo nuevas cosas que añadir, y es como un bucle del que tengo que obligarme a salir, descartando a veces algunas cosas muy chulas. Esta vez dejo pendientes algunas que me gustaría añadir, como:

  • Nuevos diseños de pared
  • Insertar imagen en pared
  • Selector de velocidad de animación
  • Selector de dirección de animación
  • Selector de color en el diseño liso
  • Mejoras en Panel de Datos
  • Depurar código Javascript

Demo

Y aquí está el resultado final, aunque debido el ancho máximo del blog no se muestra aquí la mejor versión.

Por eso recomiendo verlo a pantalla completa.

Espero que encuentres útil este Generador de Pirámides 3D. Y si te gustan las pirámides no dejes de echar un vistazo a mi Colección Pirámides 3D en Codepen.

2 Comentarios en “Generador de Pirámides 3D

  1. Buenas noches. He llegado de casualidad a tu página (buscando maneras de construir una pirámide en miniatura) y… me ha encantado; no sólo el resultado final (espectacular), sino también (más, en realidad), tus explicaciones sobre cada fase que has ido realizando. Chapeau.
    Muchas gracias.

    1. Hola Jose!

      Me alegro de que te haya servido, a veces creo que me enrollo demasiado explicando algunos pasos, pero prefiero que se entienda bien aunque me lleve varios días escribir algunos de los artículos.

      Gracias a ti por comentar!
      Un saludo!

Deja un Comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *