Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions designs/flap.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ html[data-theme="flap"] #home, html[data-theme="flap"] #app { position: relative
font: 600 var(--fs)/1 "Oswald", "Barlow Condensed", sans-serif; color: var(--c); text-transform: uppercase; letter-spacing: 0;
background: #1d1d20; border-radius: calc(var(--fs) * 0.1);
box-shadow: 0 1px 0 #000, 0 calc(var(--fs) * 0.06) 0 #000, inset 0 0 0 1px rgba(255, 255, 255, 0.04);
contain: layout style; /* a flipping tile never relayouts the page */
}
.fl > span { position: absolute; left: 0; right: 0; height: 50%; overflow: hidden; background: #1d1d20; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.fl .t, .fl .ft { top: 0; border-radius: calc(var(--fs) * 0.1) calc(var(--fs) * 0.1) 0 0; background: linear-gradient(#34343a, #232327); }
Expand All @@ -80,10 +81,7 @@ html[data-theme="flap"] #home, html[data-theme="flap"] #app { position: relative
.fl .ft { transform-origin: 50% 100%; }
.fl .fb { transform-origin: 50% 0; }
.fl.go .ft, .fl.go .fb { visibility: visible; }
.fl.go .ft { animation: fl-top 70ms ease-in forwards; }
.fl.go .fb { animation: fl-bot 70ms ease-out 70ms both; }
@keyframes fl-top { to { transform: rotateX(-90deg); } }
@keyframes fl-bot { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
/* the flip itself runs through element.animate() (flap.js) */
.flrow.amber { --c: #ffb42a; }
.flrow.amber .fl { background: #1a1710; }
.flrow.dim { --c: #b3a98b; }
Expand All @@ -93,7 +91,7 @@ html[data-theme="flap"] #home .hero { position: relative; text-align: left; max-
html[data-theme="flap"] #home .site-link { position: absolute; z-index: 5; right: 16px; top: 62px; margin: 0; padding: 4px 6px; font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase; color: #1a1200; font-weight: 600; background: #ffb42a; border-radius: 4px; }
html[data-theme="flap"] #home .site-link:hover { background: #ffc455; color: #000; }
.fl-bar { display: flex; align-items: center; gap: 16px; padding: 10px 16px; background: #0a0a0b; border-bottom: 2px solid #000; font: 600 13px/1 var(--font); letter-spacing: 0.22em; color: #ffb42a; text-transform: uppercase; }
.fl-bar span:nth-child(2) { color: #8a7a4f; }
.fl-bar > span:nth-child(2) { color: #8a7a4f; }
.fl-bar .grow { flex: 1; }
.fl-bar .clock { display: flex; align-items: center; gap: 8px; margin-right: 96px; }
.fl-bar .clock .flrow { --fs: 17px; }
Expand Down Expand Up @@ -143,9 +141,15 @@ html[data-theme="flap"] .controls { background: #19191c; }
html[data-theme="flap"] .info-title { color: #ffb42a; letter-spacing: 0.16em; }
html[data-theme="flap"] .preset-row select option { background: #1c1c1f; }
html[data-theme="flap"] .theme-opt:hover { background: #2a2a2e; }
@media (max-width: 640px) { .fl-bar .clock { margin-right: 0; } html[data-theme="flap"] #home .site-link { top: auto; bottom: 8px; right: 12px; } .fl-bar span:nth-child(2) { display: none; } }
@media (prefers-reduced-motion: reduce) { .fl.go .ft, .fl.go .fb, .fl-bar .colon { animation: none; } }
@media (max-width: 640px) { .fl-bar .clock { margin-right: 0; } html[data-theme="flap"] #home .site-link { top: auto; bottom: 8px; right: 12px; } .fl-bar > span:nth-child(2) { display: none; } }
@media (prefers-reduced-motion: reduce) { .fl-bar .colon { animation: none; } }

/* primary button keeps its own colours over the design's generic button look */
html[data-theme="flap"] button.primary { background: var(--primary-bg); color: var(--primary-fg); border-color: var(--primary-border); }
html[data-theme="flap"] button.primary:hover { background: var(--primary-hover); color: var(--primary-fg); border-color: var(--primary-border); }

/* card descriptions: amber board text, revealed in steps (cheap clip animation instead of one flip tile per letter) */
html[data-theme="flap"] #home .card-desc.fl-desc { color: #ffb42a; text-transform: uppercase; letter-spacing: 0.12em; font: 600 11px/1.5 "Oswald", "Barlow Condensed", sans-serif; opacity: 0; }
html[data-theme="flap"] #home .card-desc.fl-desc.on { opacity: 1; animation: fl-reveal 0.7s steps(24) both; }
@keyframes fl-reveal { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
html.perf-idle[data-theme="flap"] .fl-bar .colon, html.perf-hidden[data-theme="flap"] .fl-bar .colon, html.perf-idle[data-theme="flap"] #home .card::after, html.perf-hidden[data-theme="flap"] #home .card::after { animation-play-state: paused !important; }
54 changes: 39 additions & 15 deletions designs/flap.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,43 @@ function _flNorm(s) {
function _flTile(ch) {
const t = document.createElement("span");
t.className = "fl"; t.dataset.c = ch;
t.innerHTML = '<span class="t"><i></i></span><span class="b"><i></i></span><span class="ft"><i></i></span><span class="fb"><i></i></span>';
t._i = [...t.querySelectorAll("i")]; // t, b, ft, fb
t.innerHTML = '<span class="t"><i></i></span><span class="b"><i></i></span>';
t._i = [...t.querySelectorAll("i")]; // t, b (the two moving flaps are added only while the tile is flipping)
t._i.forEach((i) => { i.textContent = ch; });
return t;
}

// the moving flaps exist only while a tile flips — an idle board is 5 nodes per tile instead of 9
function _flFlaps(tile) {
if (!tile._ft) {
const ft = document.createElement("span"), fb = document.createElement("span");
ft.className = "ft"; fb.className = "fb"; ft.innerHTML = fb.innerHTML = "<i></i>";
tile.append(ft, fb); tile._ft = ft; tile._fb = fb;
}
return [tile._ft, tile._fb];
}
function _flDropFlaps(tile) {
if (tile._ft) { tile._ft.remove(); tile._fb.remove(); tile._ft = tile._fb = null; }
}

// Flip with the Web Animations API: no class toggling + forced reflow (`void offsetWidth`) per flip, which made
// every flip a synchronous layout of the whole page (≈1.5 ms each, ~2 s of blocked main thread for a full board).
function _flipOnce(tile, next, done) {
const [ti, bi, fti, fbi] = tile._i, old = tile.dataset.c;
ti.textContent = next; fti.textContent = old; bi.textContent = old; fbi.textContent = next;
tile.classList.remove("go"); void tile.offsetWidth; tile.classList.add("go");
const [ti, bi] = tile._i, old = tile.dataset.c;
const [ft, fb] = _flFlaps(tile);
if (tile._anims) tile._anims.forEach((a) => a.cancel());
ti.textContent = next; ft.firstChild.textContent = old; bi.textContent = old; fb.firstChild.textContent = next;
tile.classList.add("go");
tile._anims = [
ft.animate([{ transform: "rotateX(0)" }, { transform: "rotateX(-90deg)" }], { duration: 70, easing: "ease-in", fill: "forwards" }),
fb.animate([{ transform: "rotateX(90deg)" }, { transform: "rotateX(0)" }], { duration: 70, delay: 70, easing: "ease-out", fill: "both" }),
];
tile.dataset.c = next;
setTimeout(() => { bi.textContent = next; tile.classList.remove("go"); if (done) done(); }, _FL_STEP);
setTimeout(() => {
bi.textContent = next; tile.classList.remove("go");
if (tile._anims) { tile._anims.forEach((a) => a.cancel()); tile._anims = null; }
if (done) done();
}, _FL_STEP);
}

// run the tile through (up to `steps`) characters ending on `target`
Expand All @@ -37,7 +62,7 @@ function _flSettle(tile, target, steps, delay) {
const seq = [];
for (let k = n - 1; k >= 0; k--) seq.push(_FL_SET[(tgt - k + L) % L]);
let i = 0;
const next = () => { if (tile._run !== run || i >= seq.length) return; _flipOnce(tile, seq[i++], next); };
const next = () => { if (tile._run !== run) return; if (i >= seq.length) { _flDropFlaps(tile); return; } _flipOnce(tile, seq[i++], next); };
setTimeout(() => { if (tile._run === run) next(); }, delay);
}

Expand Down Expand Up @@ -129,18 +154,17 @@ DESIGN_FX.flap = {
card(el) {
const tEl = el.querySelector(".card-title"), dEl = el.querySelector(".card-desc");
if (!tEl || !dEl) return;
// tiles are built only when the card is near the viewport, not all at page load
let title = null, desc = null;
const build = () => {
if (title) return;
title = _flRow(tEl, tEl.textContent, {});
desc = _flRow(dEl, dEl.textContent, { cls: "amber" });
};
const go = () => { build(); title.play(9, 55, 80); desc.play(5, 16, 400); };
// title tiles are built only when the card is near the viewport. The description stays plain text (a tile per
// character made ~1400 extra tiles / 12k nodes on the hub) and just reveals left to right.
let title = null;
dEl.classList.add("fl-desc");
const build = () => { if (!title) title = _flRow(tEl, tEl.textContent, {}); };
const go = () => { build(); title.play(9, 55, 80); setTimeout(() => dEl.classList.add("on"), 400); };
if ("IntersectionObserver" in window) {
const io = new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { io.disconnect(); go(); } }, { threshold: 0.25, rootMargin: "200px" });
io.observe(el);
} else go();
el.addEventListener("pointerenter", () => { if (title) title.shuffle(4); });
if (matchMedia("(prefers-reduced-motion: reduce)").matches) dEl.classList.add("on");
},
};
8 changes: 8 additions & 0 deletions designs/kinetic.css
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,11 @@ html[data-theme="kinetic"] ::selection { background: #ff3b1f; color: #fff; }
/* primary button keeps its own colours over the design's generic button look */
html[data-theme="kinetic"] button.primary { background: var(--primary-bg); color: var(--primary-fg); border-color: var(--primary-border); }
html[data-theme="kinetic"] button.primary:hover { background: var(--primary-hover); color: var(--primary-fg); border-color: var(--primary-border); }

/* Hovering widens the title (wdth 62 → 150), so it no longer fit next to the description and the flex row wrapped —
the row jumped taller. On wide screens the row never wraps now: the description shrinks (ellipsis) instead. */
@media (min-width: 641px) {
html[data-theme="kinetic"] #home .card-text { flex-wrap: nowrap; }
html[data-theme="kinetic"] #home .card-title { flex: 0 0 auto; }
html[data-theme="kinetic"] #home .card-desc { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
34 changes: 6 additions & 28 deletions designs/liquid.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,9 @@ html[data-theme="liquid"] {
color-scheme: dark;
}

/* the blob field (injected by liquid.js): metaballs via an SVG goo filter */
/* the blob field (injected by liquid.js): metaballs computed on a small canvas */
.liq-fx { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; contain: strict; transform: translateZ(0); }
.liq-goo { position: absolute; inset: 0; filter: url(#liq-goo); }
.liq-goo i, .liq-goo b {
position: absolute; border-radius: 50%;
width: var(--s); height: var(--s); left: var(--x); top: var(--y);
background: radial-gradient(circle at 35% 30%, var(--c1), var(--c2));
animation: liq-drift var(--t) ease-in-out infinite alternate;
animation-delay: var(--d, 0s);
opacity: 0.5;
}
.liq-goo i:nth-child(1) { --s: 46vmax; --x: -14vmax; --y: -12vmax; --c1: #ff6fb0; --c2: #c2306f; --t: 19s; }
.liq-goo i:nth-child(2) { --s: 38vmax; --x: 62vw; --y: -10vmax; --c1: #7a7bff; --c2: #4a2fd0; --t: 23s; --d: -6s; }
.liq-goo i:nth-child(3) { --s: 34vmax; --x: 10vw; --y: 62vh; --c1: #5ef0c8; --c2: #1aa3b8; --t: 21s; --d: -9s; }
.liq-goo i:nth-child(4) { --s: 30vmax; --x: 70vw; --y: 58vh; --c1: #ffd666; --c2: #ff8a3d; --t: 25s; --d: -3s; }
.liq-goo i:nth-child(5) { --s: 22vmax; --x: 40vw; --y: 30vh; --c1: #ba8cff; --c2: #7b4ae6; --t: 17s; --d: -12s; }
.liq-goo b { --s: 9vmax; left: 0; top: 0; margin: -4.5vmax 0 0 -4.5vmax; --c1: #fff2a8; --c2: #ff8ac8; animation: none; opacity: 0.95; }
@keyframes liq-drift {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(9vw, 12vh) scale(1.18); }
100% { transform: translate(-8vw, 6vh) scale(0.9); }
}
.liq-cv { position: absolute; inset: 0; width: 100%; height: 100%; } /* 1/8-resolution metaball canvas, scaled up smoothly */
html[data-theme="liquid"] #home, html[data-theme="liquid"] #app { position: relative; z-index: 1; }

/* hero: the title ripples like it's under water */
Expand All @@ -89,12 +70,11 @@ html[data-theme="liquid"] #home h1 {
line-height: 0.95;
background-size: 300% 100%;
animation: liq-hue 12s steps(90) infinite;
filter: drop-shadow(0 8px 30px rgba(255, 88, 160, 0.35));
}
@keyframes liq-hue { to { background-position: -300% 0; } }
html[data-theme="liquid"] #home .subtitle { font-size: 19px; font-weight: 500; }
html[data-theme="liquid"] #home .hero { margin-bottom: 56px; }
html[data-theme="liquid"] #app h1 { filter: drop-shadow(0 2px 8px rgba(20, 5, 60, 0.75)); background: var(--hero-fill); background-size: 300% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: liq-hue 12s steps(90) infinite; }
html[data-theme="liquid"] #app h1 { background: var(--hero-fill); background-size: 300% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: liq-hue 12s steps(90) infinite; }
html[data-theme="liquid"] .back-link,
html[data-theme="liquid"] #home .site-link { color: var(--muted); }

Expand Down Expand Up @@ -161,11 +141,9 @@ html[data-theme="liquid"] .theme-opt { border-radius: 24px; }
:root { --liq-ease: cubic-bezier(0.3, 1.25, 0.4, 1); }
@media (max-width: 640px) { html[data-theme="liquid"] #home .grid { gap: 22px; } html[data-theme="liquid"] #home .card { border-radius: 38px 48px 36px 52px / 46px 38px 50px 40px; } }
@media (prefers-reduced-motion: reduce) {
.liq-goo i { animation: none; }
html[data-theme="liquid"] #home .card, html[data-theme="liquid"] #canvas-wrap, html[data-theme="liquid"] #home h1, html[data-theme="liquid"] #app h1 { animation: none; }
html[data-theme="liquid"] #home h1 { filter: none; }
}
}

/* battery: nothing decorative runs while the tab is hidden / the page is idle / in lite mode */
html.perf-hidden .liq-goo *, html.perf-idle .liq-goo *, html.perf-hidden[data-theme="liquid"] h1, html.perf-idle[data-theme="liquid"] h1 { animation-play-state: paused !important; }
html.perf-lite .liq-goo i, html.perf-lite[data-theme="liquid"] h1 { animation: none !important; }
html.perf-hidden[data-theme="liquid"] h1, html.perf-idle[data-theme="liquid"] h1 { animation-play-state: paused !important; }
html.perf-lite[data-theme="liquid"] h1 { animation: none !important; }
Loading
Loading