Vue 3D Carousel

by Ben Clayton

HTML

<div id="select" class="scene">
  
  <div class="top-hover" @mouseover="change(true,-1)" @mouseout="change(false)">
  
  </div>
  <div class="carousel" v-bind:style="rotateStyle()">
    <div class="carousel__cell" v-for="(item,idx) in getItems()" v-bind:style="positionStyle(idx)">{{item}}</div>
    </div>
<div class="bottom-hover" @mouseover="change(true,1)" @mouseout="change(false)"></div>
</div>

CSS

* { box-sizing: border-box; }

body {
  font-family: sans-serif;
  text-align: center;
}

.scene {
  display:flex;
  border: 1px solid #CCC;
  margin: auto;
  
  position: absolute;
  width: 100%;
  height: 300px;
  perspective: 1000px;
}

.top-hover {
  position: absolute;
  display: block;
  width: 100%;
  top: 0;
  height: 40%;
  border-bottom: 2px solid gray;
  z-index: 2;
}

.bottom-hover {
  position: absolute;
  display: block;
  width: 100%;
  bottom: 0;
  height: 40%;
  border-top: 2px solid gray;
  z-index: 2;
}

/* this is the center spindle of carousel drum */
.carousel {
  width: 1px;
  height: 1px;
  position: absolute;
  transform: translateZ(-2000px);
  transform-style: preserve-3d;
  transition: transform 1s;
}

.carousel__cell {
  position: absolute;
  width: 100vw;
  left: 10px;
  top: 10px;
  border: 2px solid black;
  font-size: 40px;
  font-weight: bold;
  color: black;
  text-align: center;
  transition: transform 1s, opacity 1s;
  backface-visibility: hidden;
}

Vue

// original inspiration - https://codepen.io/desandro/pen/wjeBpp
const CITEMS = 12; 
const THETA = 360 / CITEMS;
const CHEIGHT = 60; // px
const RADIUS = Math.round( ( CHEIGHT / 2) / Math.tan( Math.PI / CITEMS ) );
const TZ = RADIUS * -1;
const FIDDLE = 60;
const VFIDDLE = 1.7;


vm1 = new Vue({
  el: "#select",
  data: {
    items: [
    "one","two","three","four"
    ],
    rotateIndex:CITEMS-1
  },
  methods: {
  	change: function(over,dir){
    	if (over){
      	this.TID = setInterval(function(){
        	var si = this.selectedIndex + dir;
					if ( si >= 0 && si < this.items.length ) this.rotateIndex = this.rotateIndex - dir;
					console.log("selectedindex:",this.selectedIndex );
        }.bind(this),1000);
      } else {
      	clearTimeout(this.TID);
      }
    },
  	getItems: function(){
    	var list = [];
       list = list.concat(this.items);
       while (list.length < CITEMS){
       		list.push("");
       }
       return list.reverse();
    },
 
 		rotateList: function(list){
      	var temp = list[0];

				for(var x=0;x<list.length-1;x++){
					list[x] = list[x+1]
        }
        list[list.length-1]=temp;
    },
    
  	rotateStyle: function(){
    	var angle = THETA * this.rotateIndex * -1;
  		return { 
      	'transform' : 'translateX(-10px) translateZ(' + TZ + 'px) rotateX(' + angle + 'deg)',
        'top': (RADIUS - (CHEIGHT/2)+FIDDLE) +"px"
      };
    	
    },
    positionStyle: function(idx){
    	var angle = THETA * idx;
      var si = this.rotateIndex % CITEMS;
      var hci = CITEMS/2;// half citems
      var opacity = 1 - (hci - Math.abs(hci - Math.abs(idx - si) ))/hci;
  		return { 
      	'transform' : 'translateY(-' + (CHEIGHT/VFIDDLE) + 'px) rotateX(' + angle + 'deg) translateZ(' + RADIUS + 'px)',
        'background' : "hsla(" + (angle/1.5) +", 100%, 50%, 0.8)",
        'height' : CHEIGHT + "px",
        'line-height' : CHEIGHT + "px",
				'opacity' : opacity * opacity * opacity   
      };
    	
    }
  },
  computed : {
 ...