<figure class="scene" data-scene>
<div class="stage" data-stage></div>
</figure>
<aside class="controls">
<div>
<label>x rotation</label>
<input class="range" type="range" min="-180" max="180" value="50" data-property="x" data-unit="deg">
<span data-value="x"></span>
</div>
<div>
<label>y rotation</label>
<input class="range" type="range" min="-180" max="180" value="20" data-property="y" data-unit="deg">
<span data-value="y"></span>
</div>
<div>
<label>z rotation</label>
<input class="range" type="range" min="-180" max="180" value="-30" data-property="z" data-unit="deg">
<span data-value="z"></span>
</div>
<div>
<label>perspective</label>
<input class="range" type="range" min="10" max="1000" value="500" data-property="perspective" data-unit="px">
<span data-value="perspective"></span>
</div>
<div>
<label>x perspective origin</label>
<input class="range" type="range" min="0" max="100" value="50" data-property="x-perspective-origin" data-unit="%">
<span data-value="x-perspective-origin"></span>
</div>
<div>
<label>y perspective origin</label>
<input class="range" type="range" min="0" max="100" value="50" data-property="y-perspective-origin" data-unit="%">
<span data-value="y-perspective-origin"></span>
</div>
<div>
<label>font size</label>
<input class="range" type="range" min="5" max="100" value="10" data-property="font-size" data-unit="vmin">
<span data-value="font-size"></span>
</div>
</aside>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@800&display=swap');
$color-primary: blue;
$color-secondary: #ffe400;
*,
*:before,
*:after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
}
body {
font-family: 'JetBrains Mono', sans-serif;
color: $color-secondary;
background-color: $color-primary;
}
.scene {
--x: 50deg;
--y: 20deg;
--z: -30deg;
--perspective: 500px;
--x-perspective-origin: 50%;
--y-perspective-origin: 50%;
--font-size: 10vmin;
position: relative;
width: 100%;
height: 100%;
perspective: var(--perspective);
transform-style: preserve-3d;
perspective-origin: var(--x-perspective-origin) var(--y-perspective-origin);
}
.stage {
position: absolute;
top: 50%;
left: 50%;
transform-style: preserve-3d;
transform: translateX(-50%) translateY(-50%) rotateX(var(--x)) rotateY(var(--y)) rotateZ(var(--z));
}
.row {
display: flex;
transform-style: preserve-3d;
}
.glyph {
display: block;
font-size: var(--font-size);
line-height: 1em;
will-change: transform;
}
.controls {
position: fixed;
bottom: 0;
left: 0;
z-index: 10;
padding: 10px;
}
View Compiled
const scene = document.querySelector('[data-scene]')
const stage = document.querySelector('[data-stage]')
const glyphs = ('VOORHOEDE').split('')
const controls = document.querySelectorAll('[data-property]')
function initStage() {
const row = `
<div class="row" data-row>
${glyphs.map(glyph => `<span class="glyph" data-glyph>${glyph}</span>`).join('')}
</div>`
const rows = new Array(3).fill(row).join('')
stage.innerHTML = rows
}
function initAnimation() {
const rows = document.querySelectorAll('[data-row]')
rows.forEach((row, index) => animateRow(row))
}
function animateRow(row) {
const duration = 500
const glyphs = row.querySelectorAll('[data-glyph]')
const zOffset = 10
glyphs.forEach((glyph, index) => {
glyph.animate({
transform: [`translateZ(${-zOffset}vmin)`, `translateZ(${zOffset}vmin)`],
easing: 'ease-in-out'
},
{
duration,
delay: index * -duration * .15,
iterations: Infinity,
direction: 'alternate-reverse'
})
})
}
function initControls(controls) {
controls.forEach(control => {
control.addEventListener('input', controlHandler)
})
function controlHandler({ currentTarget }) {
const { property, unit } = currentTarget.dataset
const value = document.querySelector(`[data-value="${property}"]`)
scene.style.setProperty(`--${property}`, `${currentTarget.value}${unit}`)
value.innerHTML = currentTarget.value
}
}
initStage()
initAnimation()
initControls(controls)
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.