Skip to content

Instantly share code, notes, and snippets.

@pete-rai
Created January 13, 2021 10:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pete-rai/587c51877aa7ceb863a90c03a9968723 to your computer and use it in GitHub Desktop.
Save pete-rai/587c51877aa7ceb863a90c03a9968723 to your computer and use it in GitHub Desktop.
A 3D wheel of cards where cards can be added and removed and the wheel spun to place any card at the front. This is a pure CSS solution based on transforms. Modify the card template to make whatever you want to appear inside each card. See it in action here: https://codepen.io/pete-rai/full/dypgwLw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
<title>3D Card Wheel</title>
<style>
html,
body {
font-family: sans-serif;
}
#container {
left: 50%;
margin: -1% auto;
perspective: 1000px;
position: fixed;
top: 40%;
transform: translate(-50%, -50%);
width: 350px;
}
#template {
display: none;
}
.wheel {
height: 100%;
position: absolute;
transform-style: preserve-3d;
transition: transform 1s;
width: 100%;
}
.wheel .card {
background: lightgrey;
border-radius: 10px;
border: 1px solid grey;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1), 0 3px 6px rgba(0, 0, 0, 0.2);
color: #666;
font-size: 3rem;
height: 200px;
line-height: 6rem;
position: absolute;
text-align: center;
transition: transform 1s, opacity 1s;
width: 300px;
}
.wheel .card.current {
background: darkgrey;
color: #ddd;
}
#control {
bottom: 0;
left: 50%;
margin: 25px;
position: fixed;
transform: translateX(-50%);
width: 700px;
}
#control button {
font-size: 1.4rem;
}
#control button.disabled {
color: lightgrey;
}
#control span {
font-size: 0.8rem;
}
#control input[type="text"] {
border: 1px solid grey;
font-size: 1.2rem;
padding: 2px;
text-align: center;
}
#control input[type="range"] {
vertical-align: middle;
}
table#info {
border-collapse: collapse;
margin: 15px;
position: fixed;
right: 0;
top: 0;
}
table#info tr td,
table#info tr th {
border: 1px solid grey;
padding: 8px;
}
table#info tr th {
background: lightgrey;
font-weight: normal;
}
</style>
</head>
<body>
<div id="template" class="card"></div>
<div id="container">
<div class="wheel"></div>
</div>
<div id="control">
<span>angle</span>
<input id="angle" type="range" min="-90" max="+90" value="0" step="1"/>
<button id="del">-</button>
<button id="prev">&laquo;</button>
<input id="curr" type="text" size="3"/>
<button id="next">&raquo;</button>
<button id="add">+</button>
<input id="speed" type="range" min="0.5" max="3.0" value="0" step="0.25"/>
<span>speed</span>
</div>
<table id="info">
<tr><th>angle</th><td id="_angle"></td></tr>
<tr><th>showing</th><td id="_showing"></td></tr>
<tr><th>current</th><td id="_current"></td></tr>
<tr><th>speed</th><td id="_speed"></td></tr>
</table>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.0/jquery.min.js"></script>
<script type="text/javascript">
// --- default values
var angle = 10; // degrees
var showing = 12; // cards
var max = 16; // cards
var speed = 1; // seconds
// --- global variables
var current = 0;
var radius = 0;
var theta = 0;
var timer = null;
// --- generates the card id
function id(idx, count = showing) {
idx %= count;
return (idx < 0 ? idx + count : idx).toString().padStart(2, "0");
}
// --- rotate the card wheel
function rotate() {
$(".wheel").css(
"transform",
`translateZ(${-radius}px) rotateX(${-angle}deg) rotateY(${
-theta * current
}deg)`
);
$("#curr").val(current);
$(".card").removeClass("current");
clearTimeout(timer);
timer = setTimeout(function () {
$(`#card_${id(current)}`).addClass("current");
}, 0.9 * (speed * 1000)); // a tad before the animation ends
info();
}
// --- handlers
$("#prev").click(function () {
current--;
rotate();
});
$("#next").click(function () {
current++;
rotate();
});
$("#curr").change(function () {
current = Math.min(showing - 1, Math.max(0, parseInt($(this).val())));
rotate();
});
$("#angle").change(function () {
angle = $(this).val();
rotate();
});
$("#speed").change(function () {
speed = $(this).val();
change();
});
$("#add").click(function () {
showing = Math.min(showing + 1, max);
change();
});
$("#del").click(function () {
showing = Math.max(showing - 1, 0);
change();
});
// --- show the info
function info() {
let info = $("#info");
$("#_angle").text(`${angle} deg`);
$("#_showing").text(`${showing} of ${max}`);
$("#_current").text(`card ${id(current)}`);
$("#_speed").text(`${speed} secs`);
}
// --- process a change
function change() {
width = $(".wheel").outerWidth();
theta = showing ? 360 / showing : 1;
radius = Math.max(100, Math.round(width / 2 / Math.tan(Math.PI / showing))); // 100 is an arbitary minimum width
$(".wheel .card").each(function (idx) {
if (idx < showing) {
$(this)
.css("opacity", "1")
.css("transform", `rotateY(${theta * idx}deg) translateZ(${radius}px)`);
} else {
$(this).css("opacity", "0").css("transform", "none");
}
});
$(".wheel, .wheel .card").css("transition-duration", `${speed}s`);
$("#add").toggleClass("disabled", showing == max);
$("#del").toggleClass("disabled", showing == 0);
rotate();
}
// --- initial setup
function setup() {
for (let i = 0; i < max; i++) {
let name = id(i, max);
let card = $("#template")
.clone()
.css("opacity", "0")
.attr("id", `card_${name}`)
.text(name);
$(".wheel").append(card);
}
$("#angle").val(angle);
$("#speed").val(speed);
change();
}
setup();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment