Coloque los iconos en un círculo

96

¿Cómo puedo colocar varios <img>elementos en un círculo alrededor de otro y hacer que todos esos elementos también sean enlaces en los que se puede hacer clic? Quiero que se vea como la imagen de abajo, pero no tengo idea de cómo lograr ese efecto.

Resultado deseado

¿Es esto siquiera posible?

FatalKeystroke
fuente

Respuestas:

194

Solución 2020

Aquí hay una solución más moderna que uso en estos días.

Empiezo generando el HTML a partir de una serie de imágenes. Si el HTML se genera usando PHP, JS, algún preprocesador de HTML, lo que sea ... esto importa menos ya que la idea básica detrás es la misma.

Aquí está el código de Pug que haría esto:

//- start with an array of images, described by url and alt text
- let imgs = [
-   {
-       src: 'image_url.jpg', 
-       alt: 'image alt text'
-   } /* and so on, add more images here */
- ];
- let n_imgs = imgs.length;
- let has_mid = 1; /* 0 if there's no item in the middle, 1 otherwise */
- let m = n_imgs - has_mid; /* how many are ON the circle */
- let tan = Math.tan(Math.PI/m); /* tangent of half the base angle */

.container(style=`--m: ${m}; --tan: ${+tan.toFixed(2)}`)
    - for(let i = 0; i < n_imgs; i++)
        a(href='#' style=i - has_mid >= 0 ? `--i: ${i}` : null)
          img(src=imgs[i].src alt=imgs[i].alt)

El HTML generado se ve de la siguiente manera (y sí, también puede escribir el HTML manualmente, pero será complicado hacer cambios después):

<div class="container" style="--m: 8; --tan: 0.41">
  <a href='#'>
    <img src="image_mid.jpg" alt="alt text"/>
  </a>
  <a style="--i: 1">
    <img src="first_img_on_circle.jpg" alt="alt text"/>
  </a>
  <!-- the rest of those placed on the circle -->
</div>

En el CSS, decidimos el tamaño de las imágenes, digamos 8em. Los --melementos se colocan en un círculo y es si están en el medio de los bordes de un polígono de --mbordes, todos los cuales son tangentes al círculo.

Si tiene dificultades para imaginarse eso, puede jugar con esta demostración interactiva que construye el círculo y el círculo para varios polígonos cuyo número de bordes elige arrastrando el control deslizante.

círculo y circunferencia de un hexágono

Esto nos dice que el tamaño del contenedor debe ser el doble del radio del círculo más el doble de la mitad del tamaño de las imágenes.

Aún no conocemos el radio, pero podemos calcularlo si conocemos el número de aristas (y por lo tanto la tangente de la mitad del ángulo base, calculado previamente y establecido como una propiedad personalizada --tan) y la arista del polígono. Probablemente queramos que el borde del polígono tenga al menos el tamaño de las imágenes, pero la cantidad que dejamos en los lados es arbitraria. Digamos que tenemos la mitad del tamaño de la imagen en cada lado, por lo que el borde del polígono es el doble del tamaño de la imagen. Esto nos da el siguiente CSS:

.container {
  --d: 6.5em; /* image size */
  --rel: 1; /* how much extra space we want between images, 1 = one image size */
  --r: calc(.5*(1 + var(--rel))*var(--d)/var(--tan)); /* circle radius */
  --s: calc(2*var(--r) + var(--d)); /* container size */
  position: relative;
  width: var(--s); height: var(--s);
  background: silver /* to show images perfectly fit in container */
}

.container a {
  position: absolute;
  top: 50%; left: 50%;
  margin: calc(-.5*var(--d));
  width: var(--d); height: var(--d);
  --az: calc(var(--i)*1turn/var(--m));
  transform: 
    rotate(var(--az)) 
    translate(var(--r))
    rotate(calc(-1*var(--az)))
}

img { max-width: 100% }

Consulte la solución anterior para obtener una explicación de cómo funciona la cadena de transformación.

De esta manera, agregar o eliminar una imagen de la matriz de imágenes organiza automáticamente la nueva cantidad de imágenes en un círculo de manera que estén igualmente espaciadas y también ajusta el tamaño del contenedor. Puede probar esto en esta demostración .


Solución ANTIGUA (conservada por razones históricas)

Sí, es muy posible y muy simple usando solo CSS. Solo necesita tener claro los ángulos en los que desea los enlaces con las imágenes (he agregado un fragmento de código al final solo para mostrar los ángulos cada vez que pasa el mouse por uno de ellos).

Primero necesitas una envoltura. Configuré su diámetro para que sea 24em( width: 24em; height: 24em;hace eso), puede configurarlo como desee. Tu lo das position: relative;.

Luego coloque sus enlaces con las imágenes en el centro de ese envoltorio, tanto horizontal como verticalmente. Puede hacerlo configurando position: absolute;y luego top: 50%; left: 50%;y margin: -2em;(donde 2emestá la mitad del ancho del enlace con la imagen, que he configurado para ser 4em) nuevamente, puede cambiarlo a lo que desee, pero no olvide cambiar el margen en Ese caso).

A continuación, decide sobre los ángulos en los que desea tener sus enlaces con las imágenes y se agrega una clase deg{desired_angle}(por ejemplo, deg0o deg45, o lo que sea). Luego, para cada clase de este tipo, aplica transformaciones CSS encadenadas, como esta:

.deg{desired_angle} {
   transform: rotate({desired_angle}) translate(12em) rotate(-{desired_angle});
}

donde se reemplaza {desired_angle}con 0, 45y así sucesivamente ...

La primera transformación de rotación gira el objeto y sus ejes, la transformación de traslación traslada el objeto a lo largo del eje X girado y la segunda transformación de rotación devuelve el objeto a su posición.

La ventaja de este método es que es flexible. Puede agregar nuevas imágenes en diferentes ángulos sin alterar la estructura actual.

FRAGMENTO DE CÓDIGO

    .circle-container {
        position: relative;
        width: 24em;
        height: 24em;
        padding: 2.8em;
        /*2.8em = 2em*1.4 (2em = half the width of a link with img, 1.4 = sqrt(2))*/
        border: dashed 1px;
        border-radius: 50%;
        margin: 1.75em auto 0;
    }
    .circle-container a {
        display: block;
        position: absolute;
        top: 50%; left: 50%;
        width: 4em; height: 4em;
        margin: -2em;
    }
    .circle-container img { display: block; width: 100%; }
    .deg0 { transform: translate(12em); } /* 12em = half the width of the wrapper */
    .deg45 { transform: rotate(45deg) translate(12em) rotate(-45deg); }
    .deg135 { transform: rotate(135deg) translate(12em) rotate(-135deg); }
    .deg180 { transform: translate(-12em); }
    .deg225 { transform: rotate(225deg) translate(12em) rotate(-225deg); }
    .deg315 { transform: rotate(315deg) translate(12em) rotate(-315deg); }
    <div class='circle-container'>
        <a href='#' class='center'><img src='image.jpg'></a>
        <a href='#' class='deg0'><img src='image.jpg'></a>
        <a href='#' class='deg45'><img src='image.jpg'></a>
        <a href='#' class='deg135'><img src='image.jpg'></a>
        <a href='#' class='deg180'><img src='image.jpg'></a>
        <a href='#' class='deg225'><img src='image.jpg'></a>
        <a href='#' class='deg315'><img src='image.jpg'></a>
    </div>

Además, podría simplificar aún más el HTML utilizando imágenes de fondo para los enlaces en lugar de utilizar imgetiquetas.


EDITAR : ejemplo con respaldo para IE8 y versiones anteriores (probado en IE8 e IE7)

Ana
fuente
1
Bien, pero ¿qué verá la gente cuando acceda desde dispositivos / navegadores sin compatibilidad con CSS Transform?
gkond
1
Los únicos navegadores de escritorio que no admiten transformaciones CSS son IE8 y versiones anteriores. Para aquellos, esto se puede emular usando transformaciones de filtro de matriz IE. En cuanto a los navegadores móviles, Opera Mini es el único que no admite transformaciones CSS y de todos modos no usaría algo que desperdicia tanto espacio en una pantalla pequeña.
Ana
1
Cuando vi la demostración, me desplacé hacia abajo porque sabía que serías tú quien respondería una pregunta como esa. Bien hecho @Ana. ¿Dónde diablos escribes en tu blog?
Ahmad Alfy
6
@ Ana eso es increíble, usó su CSS para hacer un ejemplo genérico para n elementos, si está interesado. jsfiddle.net/sajjansarkar/zgcgq8cg
Sajjan Sarkar
3
@Ana muy guay! Me inspiraste para crear una versión dinámica - jsfiddle.net/skwidbreth/q59s90oy
skwidbreth
18

Aquí está la solución fácil sin posicionamiento absoluto:

.container .row {
  margin: 20px;
  text-align: center;
}

.container .row img {
  margin: 0 20px;
}
<div class="container">
  <div class="row">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
  </div>
  <div class="row">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
  </div>
  <div class="row">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
    <img src="https://ssl.gstatic.com/s2/oz/images/faviconr2.ico" alt="" width="64" height="64">
  </div>
</div>

http://jsfiddle.net/mD6H6/

gkond
fuente
11

Partiendo de la excelente respuesta de @ Ana, creé esta versión dinámica que le permite agregar y eliminar elementos del DOM y mantener un espacio proporcionado entre los elementos; consulte mi violín: https://jsfiddle.net/skwidbreth/q59s90oy/

var list = $("#list");

var updateLayout = function(listItems) {
  for (var i = 0; i < listItems.length; i++) {
    var offsetAngle = 360 / listItems.length;
    var rotateAngle = offsetAngle * i;
    $(listItems[i]).css("transform", "rotate(" + rotateAngle + "deg) translate(0, -200px) rotate(-" + rotateAngle + "deg)")
  };
};

$(document).on("click", "#add-item", function() {
  var listItem = $("<li class='list-item'>Things go here<button class='remove-item'>Remove</button></li>");
  list.append(listItem);
  var listItems = $(".list-item");
  updateLayout(listItems);

});

$(document).on("click", ".remove-item", function() {
  $(this).parent().remove();
  var listItems = $(".list-item");
  updateLayout(listItems);
});
#list {
  background-color: blue;
  height: 400px;
  width: 400px;
  border-radius: 50%;
  position: relative;
}

.list-item {
  list-style: none;
  background-color: red;
  height: 50px;
  width: 50px;
  position: absolute;
  top: 50%;
  left: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

<ul id="list"></ul>
<button id="add-item">Add item</button>

skwidbreth
fuente
1
Funcionó muy bien, y votaría más si pudiera. Un problema que tuve es que si cambiaba 360 a cualquier otra cosa (quería un semicírculo), las cosas se salían de control. Lo rastreé hasta la declaración de ángulo de rotación y lo cambié a esto var rotateAngle = zero_start + (offsetAngle * i || 0);. También agregué una variable para zero_start, por lo que si desea comenzar en el punto 270 en lugar de 0, o algo similar. jsfiddle.net/q59s90oy/13 . Por último, cambié el CSS para que los elementos de la lista usen márgenes negativos. En serio, gracias por compartir el trabajo, ayudó mucho.
Regular Joe
Eso es genial, me alegro de que hayas podido modificarlo según sea necesario. ¡Buena variación!
skwidbreth
1
Este es un efecto de espiral bastante épico 😅 i.imgur.com/1VrubKC.png
Ethan
@ Ethan ¡Ja, ja, sí! ¡Amo hacer eso! Pensé que podría ser una obra de arte genial.
skwidbreth
5

No hay forma de colocar mágicamente elementos en los que se puede hacer clic en un círculo alrededor de otro elemento con CSS. La forma en que haría esto es usando un contenedor con position:relative;. Y luego coloque todos los elementos con position:absolute;y usando topy leftpara apuntar a su lugar.

Aunque no hayas colocado en sus etiquetas, puede ser mejor usar jQuery / javascript para esto.

El primer paso es colocar su imagen central perfectamente en el centro del contenedor usando position:relative;.

#centerImage {
  position:absolute;
  top:50%;
  left:50%;
  width:200px;
  height:200px;
  margin: -100px 0 0 -100px;
}

Después de eso, puede colocar los otros elementos a su alrededor usando un offset()de centerImage menos el offset()del contenedor. Dándote lo exacto topy leftde la imagen.

var left = $('#centerImage').offset().left - $('#centerImage').parent().offset().left;
var top = $('#centerImage').offset().top - $('#centerImage').parent().offset().top;

$('#surroundingElement1').css({
  'left': left - 50,
  'top': top - 50 
});

$('#surroundingElement2').css({
  'left': left - 50,
  'top': top 
});

$('#surroundingElement3').css({
  'left': left - 50,
  'top': top + 50 
});

Lo que he hecho aquí es colocar los elementos en relación con centerImage. Espero que esto ayude.

Sem
fuente
5

Ciertamente puede hacerlo con CSS puro o usar JavaScript. Mi sugerencia:

  • Si ya sabe que el número de imágenes nunca cambiará, simplemente calcule sus estilos y vaya con CSS simple (ventajas: mejores rendimientos, muy confiable)

  • Si el número puede variar dinámicamente en su aplicación o simplemente puede variar en el futuro, elija una solución Js (ventajas: más preparada para el futuro)

Tenía un trabajo similar que hacer, así que creé un script y lo abrí aquí en Github para cualquiera que pudiera necesitarlo. Simplemente acepta algunos valores de configuración y simplemente genera el código CSS que necesita.

Si desea optar por la solución Js, aquí hay un puntero simple que puede serle útil. Usando este html como punto de partida, siendo #boxel contenedor y .dotla imagen / div en el medio, desea todas sus otras imágenes:

Inicio de html:

<div id="box">
  <div class="dot"></div>
  <img src="my-img.jpg">
  <!-- all the other images you need-->
</div>

Iniciando CSS:

 #box{
  width: 400px;
  height: 400px;
  position: relative;
  border-radius: 100%;
  border: 1px solid teal;
}

.dot{
    position: absolute;
    border-radius: 100%;
    width: 40px;
    height: 40px;
    left: 50%;
    top: 50%;
    margin-left: -20px;
    margin-top: -20px;
    background: rebeccapurple;
}
img{
  width: 40px;
  height: 40px;
  position: absolute;
}

Puede crear una función rápida a lo largo de estas líneas:

var circle = document.getElementById('box'),
    imgs = document.getElementsByTagName('img'),
    total = imgs.length,
    coords = {},
    diam, radius1, radius2, imgW;

// get circle diameter
// getBoundingClientRect outputs the actual px AFTER transform
//      using getComputedStyle does the job as we want
diam = parseInt( window.getComputedStyle(circle).getPropertyValue('width') ),
radius = diam/2,
imgW = imgs[0].getBoundingClientRect().width,
// get the dimensions of the inner circle we want the images to align to
radius2 = radius - imgW

var i,
    alpha = Math.PI / 2,
    len = imgs.length,
    corner = 2 * Math.PI / total;

// loop over the images and assign the correct css props
for ( i = 0 ; i < total; i++ ){

  imgs[i].style.left = parseInt( ( radius - imgW / 2 ) + ( radius2 * Math.cos( alpha ) ) ) + 'px'
  imgs[i].style.top =  parseInt( ( radius - imgW / 2 ) - ( radius2 * Math.sin( alpha ) ) ) + 'px'

  alpha = alpha - corner;
}

Puedes ver un ejemplo en vivo aquí.

Nobita
fuente
4

Usando la solución propuesta por @Ana:

transform: rotate(${angle}deg) translate(${radius}px) rotate(-${angle}deg)

Creé el siguiente jsFiddle que coloca círculos dinámicamente usando JavaScript simple (la versión jQuery también está disponible).

La forma en que funciona es bastante simple:

document.querySelectorAll( '.ciclegraph' ).forEach( ( ciclegraph )=>{
  let circles = ciclegraph.querySelectorAll( '.circle' )
  let angle = 360-90, dangle = 360 / circles.length
  for( let i = 0; i < circles.length; ++i ){
    let circle = circles[i]
    angle += dangle
    circle.style.transform = `rotate(${angle}deg) translate(${ciclegraph.clientWidth / 2}px) rotate(-${angle}deg)`
  }
})
.ciclegraph {
  position: relative;
  width: 500px;
  height: 500px;
  margin: calc(100px / 2 + 0px);
}

.ciclegraph:before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  border: 2px solid teal;
  width: calc( 100% - 2px * 2);
  height: calc( 100% - 2px * 2 );
  border-radius: 50%;
}

.ciclegraph .circle {
  position: absolute;
  top: 50%; left: 50%;
  width: 100px;
  height: 100px;
  margin: calc( -100px / 2 );
  background: teal;
  border-radius: 50%;
}
<div class="ciclegraph">
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
  <div class="circle"></div>
</div>

Itay Grudev
fuente
2

Aquí hay una versión que hice en React a partir de los ejemplos aquí.

Ejemplo de CodeSandbox

import React, { useRef, useEffect } from "react";

import "./styles.css";

export default function App() {
  const graph = useRef(null);

  useEffect(() => {
    const ciclegraph = graph.current;
    const circleElements = ciclegraph.childNodes;

    let angle = 360 - 90;
    let dangle = 360 / circleElements.length;

    for (let i = 0; i < circleElements.length; i++) {
      let circle = circleElements[i];
      angle += dangle;
      circle.style.transform = `rotate(${angle}deg) translate(${ciclegraph.clientWidth /
        2}px) rotate(-${angle}deg)`;
    }
  }, []);

  return (
    <div className="App">
      <div className="ciclegraph" ref={graph}>
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
        <div className="circle" />
      </div>
    </div>
  );
}
br3ntor
fuente
Gran respuesta y gran código, ¡el único problema es que lo ha publicado en una respuesta que no tiene nada que ver con React!
Manchester sin marca
Lo sé, una respuesta que nadie pidió, pero aquí está de todos modos jeje :)
br3ntor
Vine aquí buscando una solución que pudiera usar en React, por lo que sigue siendo muy útil
Abhishek Kasireddy
1

Podrías hacerlo así: violín

No importa el posicionamiento, es un ejemplo rápido

marca
fuente