Split-Flap Display (CSS/JS)
by PhilQ
HTML
<div class="split-flaps"></div>
<br><br><br><br>
<h2>TODO:</h2>
<ul>
<li>Better graphics (shadows, gradients, details)</li>
<li>JS class to create and interact with split-flap displays</li>
<li>Some documentation?</li>
</ul>
SCSS
// @import url('https://fonts.googleapis.com/css2?family=Oswald:[email protected]&family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Oswald:[email protected]&family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&display=block');
*, *::before, *::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
h2, ul {
margin: auto auto 1rem;
width: 350px;
color: rgba(255, 255, 255, 0.8);
}
.split-flaps {
--w: 100px;
--h: calc(1.5 * var(--w));
--p: calc(0.1 * var(--w));
--bar: 5%;
--d: 350;
--duration: calc(1ms * var(--d));
perspective: 1500px;
perspective-origin: 50% 50%;
width: 100%;
display: flex;
flex-wrap: nowrap;
justify-content: center;
user-select: none;
> div {
display: flex;
flex-direction: column;
transform-style: preserve-3d;
}
}
.split-flap {
position: relative;
width: calc(var(--w) + 2 * var(--p));
height: calc(var(--h) + 2 * var(--p));
background: #808080;
transform-style: preserve-3d;
// Fake flip element top
&::before {
content: "";
z-index: 0;
position: absolute;
top: var(--p);
left: var(--p);
width: var(--w);
height: var(--h);
box-shadow: inset 0 0 2px 1px rgba(0, 0, 0, 0.5);
clip-path: xywh(0 0 100% calc(50% - 0.5 * var(--bar)));
border-radius: calc(0.1 * var(--w)) calc(0.1 * var(--w)) 0 0;
transform: translateY(-3%);
background: #1d1d1d;
}
// Fake flip element bottom
&::after {
content: "";
z-index: 0;
position: absolute;
top: var(--p);
left: var(--p);
width: var(--w);
height: var(--h);
box-shadow: inset 0 0 2px 1px rgba(0, 0, 0, 0.5);
clip-path: xywh(0 calc(50% + 0.5 * var(--bar)) 100% calc(50% - 0.5 * var(--bar)));
border-radius: 0 0 calc(0.1 * var(--w)) calc(0.1 *...
JavaScript
const makeElement = (tag, props = {}, content = []) => {
let element = document.createElement(tag);
for (const [k, v] of Object.entries(props)) {
if ('events' === k) {
for (const [e, h] of Object.entries(v)) {
element.addEventListener(e, h);
}
} else {
element.setAttribute(k, v);
}
}
if (content.length) {
element.append(...content);
}
return element;
};
const splitflaps = [];
const characters = [
// A-Z
...(new Array(26)).fill(0).map((_, i) => String.fromCodePoint(65+i)),
// a-z
...(new Array(26)).fill(0).map((_, i) => String.fromCodePoint(97+i)),
// 0-9
...(new Array(10)).fill(0).map((_, i) => String(i)),
];
const animation_duration_max = 1000/3; // ms / character
const animation_duration_min = 1000/10; // ms / character
const getAnimationDuration = (flips) => {
return Math.max(
Math.min(
(1000 / flips),
animation_duration_max
),
animation_duration_min
);
};
const flip = (splitflap) => {
splitflap.prepend(splitflap.lastChild);
splitflap.dataset.c = (parseInt(splitflap.dataset.c) + 1) % characters.length;
}
const flipping = (splitflap, animation_duration, last_t, t, flips) => {
const t_over = (t - last_t) - animation_duration;
if (t_over >= 0) {
last_t = t;
flip(splitflap);
flips--;
}
if (flips > 0) {
requestAnimationFrame((t) => flipping(splitflap, animation_duration, last_t, t, flips));
} else {
setTimeout(() => {
splitflap.style.setProperty("--d", animation_duration_max);
}, animation_duration - t_over);
}
};
const flipTo = (splitflap, character) => {
const c = parseInt(splitflap.dataset.c);
const t = (characters.findIndex(v => v === character) + 1 || c+1) - 1;
if (c === t) {
return;
}
const flips = (t > c ? t - c : (characters.length - c) + t) - 1;
const animation_duration = getAnimationDuration(flips + 1);
...