Wrap icons around an extendable circle

637 views Asked by At

I have a disc whose size can change, and some icons arranged around that disc. The icons should always be on the border of the disc, and the intervals between each icon should always stay the same.

Can this be done in pure css without having to compute each icon's position whenever the disc grows or shrinks?

Expected result:

.container:nth-child(1)
{
    position: absolute;
    top: 20px;
    left: 30px;
    height: 280px;
    width: 280px;
}

.container:nth-child(2)
{
    position: absolute;
    top: 20px;
    right: 30px;
    height: 200px;
    width: 200px;
}

.container > *
{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.circle
{
    height: 90%;
    width: 90%;
    border-radius: 50%;
    border: 1px solid blue;
    z-index: 20;
}

.icons
{
    height: 100%;
    width: 100%;
    border: 1px dashed gray;
    box-sizing: border-box;
    z-index: 10;
}

.icons > *
{
    position: absolute;
    top: 50%;
    left: 50%;
    height: 20px;
    width: 20px;
    margin-top: -10px;
    margin-left: -10px;
    border: 1px solid red;
}


.container:nth-child(1) span:nth-child(1)
{
    transform: rotate(230deg) translate(150px) rotate(-230deg);
}

.container:nth-child(1) span:nth-child(2)
{
    transform: rotate(217deg) translate(150px) rotate(-217deg);
}

.container:nth-child(1) span:nth-child(3)
{
    transform: rotate(204deg) translate(150px) rotate(-204deg);
}

.container:nth-child(1) span:nth-child(4)
{
    transform: rotate(191deg) translate(150px) rotate(-191deg);
}


.container:nth-child(2) span:nth-child(1)
{
    transform: rotate(230deg) translate(110px) rotate(-230deg);
}

.container:nth-child(2) span:nth-child(2)
{
    transform: rotate(212deg) translate(110px) rotate(-212deg);
}

.container:nth-child(2) span:nth-child(3)
{
    transform: rotate(194deg) translate(110px) rotate(-194deg);
}

.container:nth-child(2) span:nth-child(4)
{
    transform: rotate(176deg) translate(110px) rotate(-176deg);
}
<div class="container">
    <div class="circle"></div>
    <div class="icons">
        <span>A</span>
        <span>B</span>
        <span>C</span>
        <span>D</span>
    </div>
</div>

<div class="container">
    <div class="circle"></div>
    <div class="icons">
        <span>A</span>
        <span>B</span>
        <span>C</span>
        <span>D</span>
    </div>
</div>

2

There are 2 answers

2
jbutler483 On

Without a preprocessor, I believe you'll need a script to achieve this. Here I've used jQuery in order to compute the intervals on hover.

Note

this would also support a dynamic number of .circle elements

+ function() {
  var to;
  $(".wrap").on('mouseenter', function() {
    var circles = $(this).find(".circle");
    var degree = (2 * Math.PI) / circles.length; //calc delta angle
    var transforms = [];

    // Calculate the position for each circle
    circles.each(function(index) {
      var x = 100 * Math.cos(-0.5 * Math.PI + degree * (-1 * index - 0.5));
      var y = 100 * Math.sin(-0.5 * Math.PI + degree * (-1 * index - 0.5));

      transforms.push('translate(' + x + 'px,' + y + 'px)');
    });

    // Function to moves all the circles
    // We'll pop a circle each time and than call this function recursively
    function moveCircles() {
      var transform = transforms.shift();
      circles.css('transform', transform);

      circles.splice(0, 1);
      if (circles.length) to = setTimeout(moveCircles, 400);
    }

    moveCircles();
  });

  $(".wrap").on('mouseleave', function() {
    var circles = $(this).children().css('transform', '');
    clearTimeout(to);
  });
}();
html {
  height: 100%;
  background: radial-gradient(ellipse at center, rgba(79, 79, 79, 1) 0%, rgba(34, 34, 34, 1) 100%);
}
.wrap {
  height: 300px;
  width: 300px;
  position: relative;
  transform-origin: center center;
  transition: all 0.8s;
}
.circle {
  transition: all 0.8s;
  position: absolute;
  height: 5px;
  width: 5px;
  text-align: center;
  line-height: 15px;
  top: calc(50% - 2px);
  left: calc(50% - 2px);
  border-radius: 50%;
  overflow: hidden;
}
.parent {
  transition: all 0.8s;
  position: absolute;
  background: gray;
  height: 50px;
  width: 50px;
  text-align: center;
  line-height: 25px;
  top: calc(50% - 25px);
  left: calc(50% - 25px);
  border-radius: 50%;
  z-index: 8;
  box-shadow: inset 2px 2px 10px black, inset 0 0 15px black, 0 0 15px black;
}
.parent:before,
.parent:after {
  content: "";
  position: absolute;
  transition: all 0.8s;
  height: 5px;
  width: 25px;
  top: 22px;
  left: 12px;
  background: black;
  opacity: 1;
}
.parent:before {
  top: 15px;
  box-shadow: 0 14px 0 black;
}
.parent:hover:before,
.parent:hover:after {
  transform: translate(0, 20px);
  color: gray;
  opacity: 0;
  box-shadow: 0 14px 0 none;
}
.wrap:hover .parent,
.wrap:hover .parent:before,
.wrap:hover .parent:after {
  background: darkgray;
}
.wrap:hover .parent:before {
  box-shadow: none;
}
.wrap:hover .circle {
  height: 50px;
  width: 50px;
  line-height: 25px;
  top: calc(50% - 25px);
  left: calc(50% - 25px);
  box-shadow: inset 2px 2px 10px black, inset 0 0 15px black, 0 0 15px black;
}
.circle img {
  position: absolute;
  height: 100%;
  width: 100%;
  left: 0;
  top: 0;
}
.circle:before {
  border-radius: 50%;
  transition: all 0.8s;
  content: "";
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
  z-index: 8;
}
.circle:after,
button:after {
  transition: all 0.8s;
  border-radius: 50%;
  content: "";
  position: absolute;
  height: 200%;
  width: 200%;
  top: 50%;
  left: 200%;
  z-index: 8;
  background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
  background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(50%, rgba(255, 255, 255, 0.4)), color-stop(100%, rgba(255, 255, 255, 0)));
  background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
  background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
  background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
  background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
  filter: progid: DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#00ffffff', GradientType=1);
}
.circle:hover:after,
button:hover:after {
  left: -200%;
  top: -50%;
}
.circle:hover:before {
  box-shadow: inset 2px 2px 10px black, inset 0 0 15px black, 0 0 15px black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrap">
  <div class="parent"></div>
  <div class="circle">
    <img src="http://lorempixel.com/300/300" />
  </div>
  <div class="circle">
    <img src="http://placekitten.com/g/200/300" />
  </div>
  <div class="circle">
    <img src="http://cdn.flaticon.com/png/256/56729.png" />
  </div>
  <div class="circle">
    <img src="http://cdn.flaticon.com/png/256/54976.png" />
  </div>
  <div class="circle">Just Text</div>
  <div class="circle">
    <img src="http://cdn.flaticon.com/png/256/56582.png" />
  </div>
</div>

12
somethinghere On

There is a way to do it programmatically, but only with CSS3. What you have to do is have an element inside it that takes the full width and can rotate, and then counterrotate the inner containers of those rotating elements.

So you would have to define the circles by using :nth-child. In order to achieve the correct effect and not apply a style twice, we will have to count down from our last element in order to only attract elements at a certain position. This is because :nth-child(2n) applies to :nth-child(4n) as well, which might create overlapping styles. Skip to the snippet if this seems a bit too complicated - it's actually quite simple.

So your basic HTML would be:

<wrapper> <!-- This is your disc -->
    <rotation> <!-- A rotation element. -->
       <counter> <!-- A counter-rotation element. This contains your content. -->

The first element has to have a position enabled. The second element needs to overlap with all the other secondary elements, so it needs a position of absolute, and the third element, well, that's up to you. It's just there to counter rotate it again.

Heres an implementation:

/*The following animation shows that the outer frame is resizeable.*/
@-webkit-keyframes sizeBounce { 0% {width: 50vw; height: 50vw; }  50% {width: 20vw; height: 20vw; } 100% {width: 50vw; height: 50vw; }}
@keyframes sizeBounce { 0% {width: 50vw; height: 50vw; }  50% {width: 20vw; height: 20vw; } 100% {width: 50vw; height: 50vw; }}

div {
  position: relative;
  /* Any height you want here */
  width: 20vw;
  height: 20vw;
  border-radius: 50%;
  border: 1px solid #000;

  -webkit-animation: sizeBounce 2s infinite;
  animation: sizeBounce 2s infinite;
  
  /* You can do anything you want with this circle! */
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translate(-50%,-50%);
  transform: translate(-50%,-50%);
}

div > span {
  position: absolute;
  display: block;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  transform: rotateZ(0deg);
  transform-origin: 50% 50%;
}

div > span > em {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 20%;
  height: 20%;
  border-radius: 50%;
  border: 1px solid #dd0300;
  text-align: center;
}

div > span:nth-child(6n-6) {
  -webkit-transform: rotateZ(0deg);
  transform: rotateZ(0deg);
}
div > span:nth-child(6n-6) > em {
  -webkit-transform: rotateZ(0deg);
  transform: rotateZ(0deg);
}

div > span:nth-child(6n-4) {
  -webkit-transform: rotateZ(60deg);
  transform: rotateZ(60deg);
}
div > span:nth-child(6n-4) > em {
  -webkit-transform: rotateZ(-60deg);
  transform: rotateZ(-60deg);
}

div > span:nth-child(6n-3) {
  -webkit-transform: rotateZ(120deg);
  transform: rotateZ(120deg);
}
div > span:nth-child(6n-3) > em {
  -webkit-transform: rotateZ(-120deg);
  transform: rotateZ(-120deg);
}

div > span:nth-child(6n-2) {
  -webkit-transform: rotateZ(180deg);
  transform: rotateZ(180deg);
}
div > span:nth-child(6n-2) > em {
  -webkit-transform: rotateZ(-180deg);
  transform: rotateZ(-180deg);
}

div > span:nth-child(6n-1) {
  -webkit-transform: rotateZ(240deg);
  transform: rotateZ(240deg);
}
div > span:nth-child(6n-1) > em {
  -webkit-transform: rotateZ(-240deg);
  transform: rotateZ(-240deg);
}
  
div > span:nth-child(6n) {
  -webkit-transform: rotateZ(300deg);
  transform: rotateZ(300deg);
}
div > span:nth-child(6n) > em {
  -webkit-transform: rotateZ(-300deg);
  transform: rotateZ(-300deg);
}
<div>
  <span><em>1</em></span>
  <span><em>2</em></span>
  <span><em>3</em></span>
  <span><em>4</em></span>
  <span><em>5</em></span>
  <span><em>6</em></span>
</div>

There is a way to automate this is SASS as well and it looks something like this:

@mixin rotatePiecemeally($pieces:6,$wrapper:span,$container:em){
    /* First calculate the angle between each piece */
    $degrees : 360 / $pieces;
    /* We want to count back from the amount of pieces to 0
     * The counting is because simple counting 2n, 3n, 4n, ... 
     * will result in 4n inheriting from 2n - to keep things clean,
     * we want to avoid that.  */
    @for $i from 0 through ($pieces - 1){
        & #{$wrapper}:nth-child(#{$pieces}n-#{$i}){
            transform: rotateZ(#{$i * $degrees}deg);
        }
        & #{$wrapper}:nth-child(#{$pieces}n-#{$i}) > #{$container}{
            transform: rotateZ(#{-$i * $degrees}deg);
        }
    }
}

You can call it by doing the following:

#mycircle {
    @include rotatePiecemeally(6);
}

And you can optionally include which elements you want to use as the children. Don't forget they all need some absolute positioning for this to work. Don't forget to add any prefixes you need (I added them to the snippet because I am using Safari)