Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSS/Javascript: How to make rotating circular menu with multiple states?

Normally I don't post myself I usually find what I need via other's threads so I'm sorry if any of this is in the wrong place or improperly formatted. I've never done this before really.

So here's the situation:

I'm trying to rebuild my website and I opted to go with the X Theme for WordPress. Mostly it's going great, but the few times I've wanted to customize and go around X it's proved a bit more difficult. If you know of a way to do this within X that would accomplish this without doing custom coding, I'm all ears.

So here's what I'm trying to do:

I had an idea for a circular menu that would position it's elements to where the top one is the "selected" element of the menu. So it would look something like this in terms of layout:

(Sorry, apparently I'm too new to use images in my posts :/)

Basic State: http://i.stack.imgur.com/Gs2Nz.jpg

Now when a user were to click on an item, I'd like it to rotate the new selected item up to the top where the "1" item was in the previous image. So it would be like this:

Menu Items Are Rotated If The User Selected Item 3: http://i.stack.imgur.com/KWseu.jpg

Some other things to note: I want the text or images of the menu items to always be normally aligned, in other words I don't want the elements text to be upside down or something after it rotates.

The original positioning of the elements I'd like to be handle when the page loads instead of hardcoded in the CSS style. Mainly just so that it can be done dynamically.

I'm planning to do more with the menu but it's THIS behavior that I'm having problems with.

I've tried things like Jquery's Animate() method, or using JavaScript to affect each elements css "top" & "left" properties, but it just doesn't seem to be working as the elements don't seem to want to move.

I don't know that this isn't a problem with trying to go through X's customizer area or not as that's where I was told to add JavaScript code. Or this could have to do with me not connecting the JavaScript/JQuery code with the CSS properly, I have a decent amount of coding experience, but I'm relatively new to JQuery/CSS etc.

So short version: I'm trying to find a way that when the page loads the elements are positioned dynamically around a center point. Then when a user clicks on an element all the elements rotate around the center, till the newly selected item is at the top. This behavior should continue as the user selects different items.

Sorry for this being a long post, but I'm just trying to explain as best I can. Any insight or advice would be greatly appreciated! Thanks in advance! :)

UPDATE: So I ended up trying marzelin's answer as it looked perfect for what I wanted. However, when I added it into the X-Theme's Javascript area and updated my CSS the elements aren't moving. They all stack in the center, but they don't encircle the center point and clicking on them doesn't seem to be doing anything. Seems like the CSS has taken affect but the Javascript part is not affecting the elements for some reason?

Here's the marzelin's answer I used (just the JavaScript part):

const buttons = Array.from(document.querySelectorAll('.button'))
const count = buttons.length
const increase = Math.PI * 2 / buttons.length
const radius = 150
let angle = 0

buttons.forEach((button, i) => {
  button.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px'
  button.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px'
  button.addEventListener('click', move)
})

function move(e) {
  const n = buttons.indexOf(e.target)
  const endAngle = (n % count) * increase
  turn()
  function turn() {
    if (Math.abs(endAngle - angle) > 1/8) {
      const sign = endAngle > angle ? 1 : -1
      angle = angle + sign/8
      setTimeout(turn, 20)
    } else {
      angle = endAngle
    }
    buttons.forEach((button, i) => {
      button.style.top = Math.sin(-Math.PI / 2 + i * increase - angle) * radius + 'px'
      button.style.left = Math.cos(-Math.PI / 2 + i * increase - angle) * radius + 'px'
    })
  }
}

Here's how my the javascript section of my X-Theme looks at the moment (excluding other code for other functions, like hiding my navbar and such):

jQuery(function($){

/* javascript or jquery code goes here */
  const stars = Array.from(document.querySelectorAll('.btnStars'));
  const count = stars.length;
  const increase = Math.PI * 2 / stars.length;
  const radius = 300;
  let angle = 0;

  stars.forEach((star, i) => {
    star.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px';
    star.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px';
    });

  $('.btnStar').click(function(e) {
    const n = stars.indexOf(e.target);
    const endAngle = (n % count) * increase;

    function turn() {
      if (Math.abs(endAngle - angle) > 1/8) {
        const sign = endAngle > angle ? 1 : -1;
        angle = angle + sign/8;
        setTimeout(turn, 20);
      } else {
        angle = endAngle;
      }

      stars.forEach((star, i) => {
        star.style.top = Math.sin(-Math.PI / 2 + i * increase - angle) * radius + 'px';
        star.style.left = Math.cos(-Math.PI / 2 + i * increase - angle) * radius + 'px';
      })
    }

    turn();
  });
});

I did change a few things, namely the CSS class names and such, but most of it's the same. I did a couple things like reorganizing as the editor for X Theme didn't seem to know what a few of the functions were so I moved them to before their calls and then it seemed to find them. So little things like that.

I also tried to change the move function to a JQuery .click function to see if that would trigger anything but it didn't seem to change anything.

While I've worked with Javascript and some JQuery before, I've never really dealt with trying to incorporate it into a WordPress theme so I really don't know what this isn't working.

Does anyone see anything I'm doing wrong? Cause I'm pretty perplexed as to why this won't work. :/

like image 356
ASparoWitAGun Avatar asked Sep 10 '16 17:09

ASparoWitAGun


2 Answers

Simple MVP

const buttons = Array.from(document.querySelectorAll('.button'))
const count = buttons.length
const increase = Math.PI * 2 / buttons.length
const radius = 150

buttons.forEach((button, i) => {
  button.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px'
  button.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px'
  button.addEventListener('click', move)
})

function move(e) {
  const n = buttons.indexOf(e.target)
  buttons.forEach((button, i) => {
    button.style.top = Math.sin(-Math.PI / 2 + (i - n % count) * increase) * radius + 'px'
    button.style.left = Math.cos(-Math.PI / 2 + (i - n % count) * increase) * radius + 'px'
  })
}
html,
body {
  height: 100%;
}
.menu {
  height: 100%;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  background-color: seagreen;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: center;
  -webkit-align-items: center;
  -ms-flex-align: center;
  align-items: center;
}
.center {
  width: 100px;
  height: 100px;
  background-color: goldenrod;
  border-radius: 100%;
  position: relative;
  line-height: 100px;
  text-align: center;
}
.button {
  position: absolute;
  width: 100px;
  height: 100px;
  border-radius: 100%;
  -webkit-transition: all 0.5s;
  transition: all 0.5s;
  background-color: pink;
  line-height: 100px;
  text-align: center;
}
<div class="menu">
  <div class="center">Menu
    <div class="button">1</div>
    <div class="button">2</div>
    <div class="button">3</div>
    <div class="button">4</div>
    <div class="button">5</div>
  </div>
</div>

Circular Motion

const buttons = Array.from(document.querySelectorAll('.button'))
const count = buttons.length
const increase = Math.PI * 2 / buttons.length
const radius = 150
let angle = 0

buttons.forEach((button, i) => {
  button.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px'
  button.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px'
  button.addEventListener('click', move)
})

function move(e) {
  const n = buttons.indexOf(e.target)
  const endAngle = (n % count) * increase
  turn()
  function turn() {
    if (Math.abs(endAngle - angle) > 1/8) {
      const sign = endAngle > angle ? 1 : -1
      angle = angle + sign/8
      setTimeout(turn, 20)
    } else {
      angle = endAngle
    }
    buttons.forEach((button, i) => {
      button.style.top = Math.sin(-Math.PI / 2 + i * increase - angle) * radius + 'px'
      button.style.left = Math.cos(-Math.PI / 2 + i * increase - angle) * radius + 'px'
    })
  }
}
html, body {
  height: 100%;
}

.menu {
  height: 100%;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  background-color: seagreen;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
      -ms-flex-pack: center;
          justify-content: center;
  -webkit-box-align: center;
  -webkit-align-items: center;
      -ms-flex-align: center;
          align-items: center;
  line-height: 100px;
  text-align: center;
}

.center {
  width: 100px;
  height: 100px;
  background-color: goldenrod;
  border-radius: 100%;
  position: relative;
}

.button {
  position: absolute;
  width: 100px;
  height: 100px;
  border-radius: 100%;
  background-color: pink;
  line-height: 100px;
  text-align: center;
  cursor: pointer;
}
<div class="menu">
  <div class="center">menu
    <div class="button">1</div>
    <div class="button">2</div>
    <div class="button">3</div>
    <div class="button">4</div>
    <div class="button">5</div>
  </div>
</div>
like image 148
marzelin Avatar answered Oct 19 '22 10:10

marzelin


The trigonometrical approach here feels wrong.

It's pretty much like trying to program in binary code. It's doable, but not necessarily how our programs should look like, if we want to keep our ability to read the code later on and maybe further modify its logic.

In order not to have to calculate the position of each menu element, we'd have to separate the rotation of the menu from the rotation of each axis.

Once those are separated, their values could be placed in CSS variables, rotating both the element they're aimed at (menu or axis) while rotating the respective button backwards by the same amount. This way, the buttons will always stand upright, because the rotations cancel each other out.

Here's a demo of the principle. Notice the use of CSS variables,
using style="{ '--var-name': value }". You can also inspect the markup during runtime to read the current rotation values:

new Vue({
  el: '#app',
  data: () => ({
    buttons: 3,
    useTransitions: true,
    isMenuOpen: true,
    rotation: -90
  }),
  computed: {
    axisRotations() {
      return Array.from({
        length: this.buttons
      }).map((_, i) => 360 * (this.buttons - i) / this.buttons)
    },
    menuRotation: {
      get() {
        return this.rotation
      },
      set(val) {
        this.rotation = isNaN(Number(val)) ? -90 : Number(val)
      }
    }
  },
  methods: {
    updateButtons(n) {
      if (this.buttons + n > 0) {
        this.buttons += n;
        this.isMenuOpen = true;
        this.menuRotation = -90;
      }
    },
    goToTop(axis) {
      let diff = this.degreesToTop(axis);
      diff = diff > 180
        ? diff - 360
        : diff <= -180
          ? diff + 360
          : diff;
      this.menuRotation = Math.round((this.menuRotation + diff) * 10) / 10;
    },
    degreesToTop(axis) {
      return (Math.round(this.axisRotations[axis - 1]) - this.menuRotation - 90) % 360;
    },
    isActive(axis) {
      return !(this.degreesToTop(axis));
    },
    toggleMenu() {
      this.isMenuOpen = !this.isMenuOpen;
    }
  }
})
.menu {
  width: 0;
  height: 0;
  top: 110px;
  left: 110px;
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: rotate(var(--menu-rotation));
  --menu-rotation: 0deg;
}

.menu .center {
  height: 54px;
  min-width: 54px;
  border-radius: 27px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: white;
  border: 1px solid #eee;
  cursor: pointer;
  z-index: 2;
  transform: rotate(calc(-1 * var(--menu-rotation))) translateZ(0);
}

.menu .axis {
  position: absolute;
  width: 100px;
  left: 0;
  height: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  transform-origin: 0 0;
  transform: rotate(var(--axis-rotation));
}

.animated .axis.axis {
  transition: all .54s cubic-bezier(.4, 0, .2, 1);
}

.menu .axis.closed {
  width: 27px;
  transform: rotate(calc(var(--axis-rotation) + 180deg));
  opacity: .1;
}

.axis.closed button,
.axis.active button {
  color: white;
  background-color: #f50;
}

.axis.active:not(.closed) {
  z-index: 1;
}

.axis button {
  background-color: white;
  cursor: pointer;
  width: 54px;
  height: 54px;
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 27px;
  border: 1px solid #eee;
  transform: rotate(calc(calc(-1 * var(--axis-rotation)) - var(--menu-rotation))) translateZ(0);
  outline: none;
}

.flexer {
  display: flex;
  height: 240px;
  padding-left: 220px;
}

.controls {
  flex-grow: 1
}

input {
  width: 100%;
}

label input {
  width: auto;
}

label {
  display: block;
  margin-top: 1rem;
  cursor: pointer;
}

.animated,
.animated .center,
.animated .axis,
.animated .axis>* {
  transition: transform .35s cubic-bezier(.4, 0, .2, 1);
}

body {
  background-color: #f8f8f8;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<div id="app">
  <div>
    <div class="flexer">
      <div class="menu"
           :class="{ animated: useTransitions }"
           :style="{'--menu-rotation': `${menuRotation}deg`}">
        <div class="center" @click="toggleMenu">menu</div>
        <div v-for="axis in buttons"
             class="axis"
             :class="{ closed: !isMenuOpen, active: isActive(axis) }"
             :style="{'--axis-rotation': `${360 * (axis - 1) / buttons}deg`}">
          <button v-text="axis" @click="goToTop(axis)" />
        </div>
      </div>
      <div class="controls">
        Menu rotation (<code v-text="`${menuRotation}deg`"></code>)
        <input type="range" min="-720" max="720" v-model="menuRotation">
        <button @click="updateButtons(1)">Add button</button>
        <button @click="updateButtons(-1)">Remove button</button>
        <button @click="toggleMenu">Toggle menu</button>
        <label>
          <input type="checkbox" v-model="useTransitions">Use transitions
        </label>
      </div>
    </div>
    <pre v-text="{ menuRotation, buttons, axisRotations }"></pre>
  </div>
</div>

As you can see, I'm never calculating positions of buttons. The only trigonometry used is "there are 360 degrees in a circle".

The above example is done in Vue, as it's a fast prototyping tool I happen to like. If you want a vanilla solution of getting the items to top, see my answer on a follow-up question to this one.

like image 2
tao Avatar answered Oct 19 '22 12:10

tao