Posicione os ícones no círculo

96

Como posso posicionar vários <img>elementos em um círculo ao redor de outro e fazer com que todos esses elementos também sejam links clicáveis? Quero que se pareça com a imagem abaixo, mas não tenho ideia de como conseguir esse efeito.

Resultado desejado

Isso é mesmo possível?

FatalKeystroke
fonte

Respostas:

194

Solução 2020

Aqui está uma solução mais moderna que uso atualmente.

Eu começo gerando o HTML a partir de uma série de imagens. Se o HTML é gerado usando PHP, JS, algum pré-processador HTML, seja o que for ... isso importa menos, pois a ideia básica por trás é a mesma.

Este é o código Pug que faria isso:

//- 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)

O HTML gerado tem a seguinte aparência (e sim, você também pode escrever o HTML manualmente, mas será uma dor fazer alterações depois):

<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>

No CSS, decidimos um tamanho para as imagens, digamos 8em. Os --mitens são posicionados em um círculo e é se eles estão no meio das arestas de um polígono de --marestas, todos os quais são tangentes ao círculo.

Se você tiver dificuldade em imaginar isso, pode brincar com esta demonstração interativa que constrói o incircle e o circumcircle para vários polígonos cujo número de arestas você escolhe arrastando o controle deslizante.

incircle e circumcircle de um hexágono

Isso nos diz que o tamanho do contêiner deve ser duas vezes o raio do círculo mais duas vezes a metade do tamanho das imagens.

Ainda não sabemos o raio, mas podemos calculá-lo se soubermos o número de arestas (e, portanto, a tangente da metade do ângulo base, pré-calculado e definido como uma propriedade personalizada --tan) e a aresta do polígono. Provavelmente queremos que a borda do polígono tenha pelo menos o tamanho das imagens, mas o quanto deixamos nas laterais é arbitrário. Digamos que temos metade do tamanho da imagem em cada lado, então a borda do polígono tem o dobro do tamanho da imagem. Isso nos dá o seguinte 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% }

Veja a solução antiga para uma explicação de como funciona a cadeia de transformação.

Dessa forma, adicionar ou remover uma imagem da matriz de imagens organiza automaticamente o novo número de imagens em um círculo de forma que fiquem igualmente espaçadas e também ajusta o tamanho do contêiner. Você pode testar isso nesta demonstração .


Solução ANTIGA (preservada por razões históricas)

Sim, é muito possível e muito simples usando apenas CSS. Você só precisa ter em mente os ângulos em que deseja os links com as imagens (adicionei um trecho de código no final apenas para mostrar os ângulos sempre que você passa o mouse sobre um deles).

Você primeiro precisa de um invólucro. Eu defini seu diâmetro para ser 24em( width: 24em; height: 24em;faz isso), você pode definir o que quiser. Você dá position: relative;.

Em seguida, você posiciona seus links com as imagens no centro desse wrapper, tanto horizontal quanto verticalmente. Você faz isso configurando position: absolute;e então top: 50%; left: 50%;e margin: -2em;(onde 2emé a metade da largura do link com a imagem, que eu defini para ser 4em- novamente, você pode alterá-lo para o que quiser, mas não se esqueça de alterar a margem em Aquele caso).

Você então decide os ângulos em que deseja ter seus links com as imagens e adiciona uma classe deg{desired_angle}(por exemplo, deg0ou deg45ou qualquer outra coisa). Em seguida, para cada classe, você aplica transformações CSS encadeadas, como esta:

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

onde você substitui {desired_angle}com 0, 45e assim por diante ...

A primeira transformação de rotação gira o objeto e seus eixos, a transformação de translação traduz o objeto ao longo do eixo X girado e a segunda transformação de rotação traz o objeto de volta à posição.

A vantagem desse método é que ele é flexível. Você pode adicionar novas imagens em ângulos diferentes sem alterar a estrutura atual.

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>

Além disso, você pode simplificar ainda mais o HTML usando imagens de fundo para os links em vez de usar imgtags.


EDIT : exemplo com fallback para IE8 e mais antigo (testado no IE8 e IE7)

Ana
fonte
1
Legal, mas o que as pessoas verão ao acessar de dispositivos / navegadores sem suporte para CSS Transform?
gkond
1
Os únicos navegadores de desktop que não oferecem suporte a transformações CSS são o IE8 e mais antigos. Para aqueles, isso pode ser emulado usando as transformações de filtro de matriz do IE. Quanto aos navegadores móveis, o Opera Mini é o único que não oferece suporte a transformações CSS e eu realmente não usaria algo que ocupa tanto espaço em uma tela pequena.
Ana,
1
Quando vi a demonstração, rolei para baixo porque sabia que seria você quem responderia a uma pergunta como essa. Muito bem @Ana. Onde diabos você bloga?
Ahmad Alfy
6
@Ana isso é incrível, use seu CSS para fazer um exemplo genérico para n itens, se estiver interessado. jsfiddle.net/sajjansarkar/zgcgq8cg
Sajjan Sarkar
3
@Ana muito legal! Você me inspirou a criar uma versão dinâmica - jsfiddle.net/skwidbreth/q59s90oy
skwidbreth
18

Aqui está a solução fácil sem posicionamento 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
fonte
10

Com base na excelente resposta de @Ana, criei esta versão dinâmica que permite adicionar e remover elementos do DOM e manter o espaçamento proporcional entre os elementos - confira meu violino: 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
fonte
1
Funcionou muito bem, e eu votaria mais se pudesse. Um problema que eu tive é que se eu mudasse 360 ​​para qualquer outra coisa (eu queria um semicírculo), as coisas saíam do controle. Eu rastreei a declaração do ângulo de rotação e mudei para isso, var rotateAngle = zero_start + (offsetAngle * i || 0);eu também adicionei uma variável para zero_start, então se você quiser começar no ponto 270 em vez de 0, ou algo semelhante. jsfiddle.net/q59s90oy/13 . Por último, mudei o css dos itens da lista para usar margens negativas. Sério, obrigado por compartilhar o trabalho, ajudou muito.
Joe regular de
Que ótimo, que bom que você foi capaz de ajustá-lo conforme necessário. Bela variação!
skwidbreth
1
Ei, este é um efeito espiral muito épico 😅 i.imgur.com/1VrubKC.png
Ethan
@ Ethan Ha ha yeah! Eu amo fazer isso! Achei que poderia ser uma peça de arte legal.
skwidbreth
5

Não há como colocar itens clicáveis ​​magicamente em um círculo ao redor de outro elemento com CSS. A maneira como eu faria isso é usando um contêiner com position:relative;. Em seguida, coloque todos os elementos com position:absolute;e usando tope leftpara direcionar seu lugar.

Mesmo que você não tenha colocado em suas tags, pode ser melhor usar jQuery / javascript para isso.

O primeiro passo é colocar a imagem central perfeitamente no centro do contêiner usando position:relative;.

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

Depois disso, você pode colocar os outros elementos em torno dele usando um offset()de centerImage menos o offset()do contêiner. Dando a você o exato tope leftda imagem.

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 
});

O que fiz aqui foi colocar os elementos em relação a centerImage. Espero que isto ajude.

Sem
fonte
5

Você certamente pode fazer isso com CSS puro ou usar JavaScript. Minha sugestão:

  • Se você já sabe que o número das imagens nunca mudará, calcule seus estilos e use o CSS simples (prós: melhores performances, muito confiável)

  • Se o número pode variar dinamicamente em seu aplicativo ou apenas pode variar no futuro, vá com uma solução Js (prós: mais à prova de futuro)

Eu tinha um trabalho semelhante a fazer, então criei um script e abri o código aqui no Github para qualquer pessoa que pudesse precisar. Ele apenas aceita alguns valores de configuração e simplesmente exibe o código CSS de que você precisa.

Se você quiser ir para a solução Js, aqui está um ponteiro simples que pode ser útil para você. Usando este html como ponto de partida, sendo #boxo contêiner e .dota imagem / div no meio em que você deseja todas as outras imagens:

HTML inicial:

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

Css inicial:

 #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;
}

Você pode criar uma função rápida ao longo destas linhas:

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;
}

Você pode ver um exemplo ao vivo aqui

Nobita
fonte
4

Usando a solução proposta por @Ana:

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

Criei o seguinte jsFiddle que coloca círculos dinamicamente usando JavaScript simples (a versão jQuery também está disponível).

A forma como funciona é bastante simples:

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
fonte
2

Aqui está uma versão que fiz no React a partir dos exemplos aqui.

Exemplo 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
fonte
Ótima resposta e ótimo código, o único problema é que você postou em uma resposta que não tem nada a ver com React!
Manchester sem marca
Eu sei, uma resposta que ninguém perguntou, mas aqui está mesmo assim hehe :)
br3ntor
Vim aqui em busca de uma solução que pudesse usar no React de forma ainda muito útil
Abhishek Kasireddy
1

Você poderia fazer assim: violino

Não se preocupe com o posicionamento, é um exemplo rápido

Marca
fonte