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 : {
...