(() => {
const onReady = (fn) => {
const wait = () => (window.anime && document.readyState !== 'loading') ? fn() : setTimeout(wait, 40);
wait();
};
onReady(() => {
const stage = document.querySelector('.stage');
const cube = stage.querySelector('#cube');
const topFace = stage.querySelector('#top');
const leftFace = stage.querySelector('#left');
const rightFace = stage.querySelector('#right');
const backFace = stage.querySelector('#back');
const range = stage.querySelector('#progress');
const out = stage.querySelector('#progressOut');
if (!cube || !topFace || !leftFace || !rightFace || !backFace || !range) return;
const getS = () => parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--s')) || 280;
const S = getS();
const LIFT_PX = Math.round(S * 0.7);
const FOLD_DEG = 90;
topFace.style.setProperty('--lift-top', '0px');
leftFace.style.setProperty('--fold-left', '0deg');
rightFace.style.setProperty('--fold-right', '0deg');
backFace.style.setProperty('--fold-back', '0deg');
const makeVarTL = (el, name, from, to, unit, duration = 1000, easing = 'linear') => {
const holder = {
v: from
};
return anime.timeline({
autoplay: false,
duration,
easing
})
.add({
targets: holder,
v: to,
update: () => el.style.setProperty(name, holder.v + unit)
});
};
const tlRotate = anime.timeline({
autoplay: false,
duration: 1600,
easing: 'linear'
})
.add({
targets: cube,
rotateX: [0, 360],
rotateY: [0, 360]
});
const tlTopLift = makeVarTL(topFace, '--lift-top', 0, LIFT_PX, 'px', 900);
const tlLeft = makeVarTL(leftFace, '--fold-left', 0, -FOLD_DEG, 'deg', 700);
const tlRight = makeVarTL(rightFace, '--fold-right', 0, FOLD_DEG, 'deg', 700);
const tlBack = makeVarTL(backFace, '--fold-back', 0, FOLD_DEG, 'deg', 700);
const seg = (g, a, b) => {
if (g <= a) return 0;
if (g >= b) return 1;
return (g - a) / (b - a);
};
const seekAll = (g) => {
const p1 = seg(g, 0.00, 0.25);
const p2 = seg(g, 0.25, 0.50);
const p3 = seg(g, 0.50, 0.75);
const p4 = seg(g, 0.75, 1.00);
tlRotate.seek(p1 * tlRotate.duration);
tlTopLift.seek(p2 * tlTopLift.duration);
tlLeft.seek(p3 * tlLeft.duration);
tlRight.seek(p3 * tlRight.duration);
tlBack.seek(p4 * tlBack.duration);
};
const onInput = () => {
const g = (parseFloat(range.value) || 0) / 100;
out.value = Math.round(g * 100) + '%';
seekAll(g);
};
range.addEventListener('input', onInput);
onInput();
});
})();
:root {
--s: min(20vmin, 340px);
--isoX: -35.264deg;
--isoY: 45deg;
--lift-top: 0px;
--fold-left: 0deg;
--fold-right: 0deg;
--fold-back: 0deg;
--glass-rgb: 47 107 255;
--glass-blur: 8px;
--glass-sat: 160%;
--glass-bright: 1.05;
}
.stage {
width: 100%;
height: 70vh;
display: block;
padding: 12px;
box-sizing: border-box;
}
.controls {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
font: 14px system-ui, sans-serif
}
.controls input[type="range"] {
width: 320px
}
.cube-viewport {
position: relative;
width: 100%;
height: calc(100% - 42px);
overflow: visible;
perspective: none;
}
.cube-camera {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transform-style: preserve-3d;
transform: rotateX(var(--isoX)) rotateY(var(--isoY));
}
.cube-world {
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
}
.cube {
position: absolute;
inset: 0;
transform-style: preserve-3d;
will-change: transform;
}
.face {
position: absolute;
top: 50%;
left: 50%;
width: var(--s);
height: var(--s);
transform-origin: center;
transform-style: preserve-3d;
backface-visibility: hidden;
border-radius: 12px;
overflow: hidden;
background:
linear-gradient(135deg, rgb(var(--glass-rgb) / 0.22), rgb(var(--glass-rgb) / 0.08)),
radial-gradient(120% 120% at 0% 0%, rgb(255 255 255 / 0.28), transparent 60%);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)) brightness(var(--glass-bright));
box-shadow: 0 8px 24px rgb(0 0 0 / 0.28), inset 0 0 0 1px rgb(255 255 255 / 0.06);
}
.face::after {
content: "";
position: absolute;
inset: 0;
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
pointer-events: none;
}
/* Face-Positionen */
#right {
transform: translate(-50%, -50%) rotateY(-90deg) translateZ(calc(var(--s) * -0.5)) rotateX(var(--fold-right));
transform-origin: 50% 100% 0;
}
#left {
transform: translate(-50%, -50%) rotateY(-90deg) translateZ(calc(var(--s) * 0.5)) rotateX(var(--fold-left));
transform-origin: 50% 100% 0;
}
#top {
transform: translate(-50%, -50%) rotateX(90deg) translateZ(calc(var(--s) * 0.5 + var(--lift-top)));
}
#bottom {
transform: translate(-50%, -50%) rotateX(90deg) translateZ(calc(var(--s) * -0.5));
}
#back {
transform: translate(-50%, -50%) rotateX(0deg) translateZ(calc(var(--s) * -0.5)) rotateX(var(--fold-back));
transform-origin: 50% 100% 0;
}
<main class="stage">
<div class="controls">
<label for="progress">Progress</label>
<input id="progress" type="range" min="0" max="100" value="0" step="0.1" />
<output id="progressOut">0%</output>
</div>
<div class="cube-viewport">
<div class="cube-camera">
<div class="cube-world">
<div class="cube" id="cube">
<div id="top" class="face">
<div class="face-inner"></div>
</div>
<div id="bottom" class="face">
<div class="face-inner"></div>
</div>
<div id="left" class="face">
<div class="face-inner"></div>
</div>
<div id="right" class="face">
<div class="face-inner"></div>
</div>
<div id="back" class="face">
<div class="face-inner"></div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- anime.js -->
<script src="https://unpkg.com/[email protected]/lib/anime.min.js"></script>