Files
FancyWordle/wordle-intro-5.html
T
2026-05-21 09:38:27 -04:00

533 lines
17 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WORDLE Intro</title>
<style>
:root {
--bg: #111111;
--surface: #ffffff;
--fg: #fafafa;
--muted: #6b6b6b;
--border: #e5e5e5;
--accent: #2f6feb;
--tile-empty: #3a3a3c;
--tile-wrong: #787c7e;
--tile-misplaced: #c9b458;
--tile-correct: #6aaa64;
--font-display: "Inter", -apple-system, system-ui, sans-serif;
--font-body: "Inter", -apple-system, system-ui, sans-serif;
--font-mono: ui-monospace, "JetBrains Mono", monospace;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
overflow: hidden;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
}
.stage {
position: relative;
inline-size: 100vw;
block-size: 100vh;
min-block-size: 520px;
display: grid;
place-items: center;
background: linear-gradient(
180deg,
#181818 0%,
#111111 58%,
#080808 100%
);
perspective: 1200px;
isolation: isolate;
}
.stage::before,
.stage::after {
content: "";
position: absolute;
inset-inline: 0;
block-size: 20vh;
z-index: 4;
pointer-events: none;
}
.stage::before {
inset-block-start: 0;
background: linear-gradient(
180deg,
rgba(17, 17, 17, 0.85),
transparent
);
}
.stage::after {
inset-block-end: 0;
background: linear-gradient(
0deg,
rgba(17, 17, 17, 0.88),
transparent
);
}
.intro {
position: relative;
inline-size: min(1120px, calc(100vw - 32px));
block-size: min(640px, calc(100vh - 32px));
display: grid;
place-items: center;
transform-style: preserve-3d;
}
.scanline {
position: absolute;
inset-inline: 10%;
inset-block-start: 50%;
block-size: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(250, 250, 250, 0.36),
transparent
);
transform: translateY(-50%);
opacity: 0;
animation: scan 4.8s ease forwards;
z-index: 1;
}
.tiles {
--tile-size: clamp(46px, 10vw, 128px);
--tile-gap: clamp(6px, 1.4vw, 16px);
position: relative;
display: grid;
grid-template-columns: repeat(6, var(--tile-size));
gap: var(--tile-gap);
transform-style: preserve-3d;
transform-origin: 50% 50%;
transition:
gap 420ms cubic-bezier(0.2, 0.7, 0.15, 1),
transform 520ms cubic-bezier(0.2, 0.7, 0.15, 1);
z-index: 2;
}
.tile {
position: relative;
inline-size: var(--tile-size);
block-size: var(--tile-size);
transform-style: preserve-3d;
opacity: 0;
transform: translate3d(0, 34px, -180px);
animation: tileEnter 520ms cubic-bezier(0.2, 0.7, 0.15, 1)
forwards;
animation-delay: calc(var(--i) * 56ms);
}
.letter {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 1px solid rgba(229, 229, 229, 0.18);
background: var(--tile-empty);
color: var(--fg);
font-family: var(--font-display);
font-size: clamp(28px, 6vw, 78px);
font-weight: 700;
letter-spacing: 0;
line-height: 1;
text-transform: uppercase;
transform-origin: 50% 50%;
box-shadow: inset 0 0 0 1px rgba(250, 250, 250, 0.08);
}
.tile.is-flipping .letter {
animation: splitFlap 260ms cubic-bezier(0.3, 0.7, 0.2, 1);
}
.tile[data-status="wrong"] .letter {
background: var(--tile-wrong);
}
.tile[data-status="misplaced"] .letter {
background: var(--tile-misplaced);
}
.tile[data-status="correct"] .letter {
background: var(--tile-correct);
}
.tile:nth-child(1) {
--i: 0;
}
.tile:nth-child(2) {
--i: 1;
}
.tile:nth-child(3) {
--i: 2;
}
.tile:nth-child(4) {
--i: 3;
}
.tile:nth-child(5) {
--i: 4;
}
.tile:nth-child(6) {
--i: 5;
}
.tiles.is-solved .tile {
opacity: 1;
animation: logoCascade 680ms cubic-bezier(0.18, 0.85, 0.22, 1)
calc(var(--i) * 76ms) both;
}
.tiles.is-logo {
gap: clamp(3px, 0.7vw, 8px);
transform: scale(0.88);
}
.tiles.is-logo .letter {
border-color: rgba(250, 250, 250, 0.28);
box-shadow:
inset 0 0 0 1px rgba(250, 250, 250, 0.1),
0 0 28px rgba(106, 170, 100, 0.16);
}
.caption {
position: absolute;
inset-block-end: clamp(24px, 6vh, 64px);
inset-inline: 24px;
display: flex;
justify-content: center;
color: rgba(250, 250, 250, 0.62);
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
z-index: 5;
opacity: 0;
animation: captionCue 4.8s ease forwards;
}
.replay {
position: fixed;
inset-block-start: 16px;
inset-inline-end: 16px;
z-index: 10;
border: 1px solid rgba(229, 229, 229, 0.22);
border-radius: 8px;
padding: 10px 16px;
background: rgba(17, 17, 17, 0.74);
color: var(--fg);
font: 600 14px/1 var(--font-body);
cursor: pointer;
}
.replay:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
.replay:hover {
border-color: rgba(250, 250, 250, 0.5);
}
.stage.restarting * {
animation: none !important;
}
@keyframes tileEnter {
0% {
transform: translate3d(0, 34px, -180px);
filter: blur(2px);
opacity: 0;
}
100% {
transform: translate3d(0, 0, 0);
filter: blur(0);
opacity: 1;
}
}
@keyframes splitFlap {
0% {
transform: rotateX(0deg);
filter: brightness(1);
}
49% {
transform: rotateX(-88deg);
filter: brightness(0.72);
}
50% {
transform: rotateX(88deg);
filter: brightness(1.12);
}
100% {
transform: rotateX(0deg);
filter: brightness(1);
}
}
@keyframes logoCascade {
0% {
transform: translate3d(0, 0, 0) scale(1);
filter: brightness(1);
}
34% {
transform: translate3d(0, -22%, 130px) scale(1.18);
filter: brightness(1.16);
}
62% {
transform: translate3d(0, 4%, 0) scale(0.98);
filter: brightness(1.04);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
filter: brightness(1);
}
}
@keyframes scan {
0%,
12% {
opacity: 0;
transform: translateY(-50%) scaleX(0.2);
}
24% {
opacity: 0.75;
}
54% {
opacity: 0.22;
transform: translateY(-50%) scaleX(1);
}
72%,
100% {
opacity: 0;
}
}
@keyframes captionCue {
0%,
18% {
opacity: 0;
transform: translateY(8px);
}
28%,
64% {
opacity: 1;
transform: translateY(0);
}
82%,
100% {
opacity: 0;
transform: translateY(-8px);
}
}
@media (max-width: 720px) {
.stage {
min-block-size: 420px;
}
.tiles {
--tile-size: min(13.5vw, 56px);
--tile-gap: 5px;
}
.caption {
font-size: 10px;
}
.replay {
padding: 9px 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.tiles,
.tile,
.tile .letter,
.scanline,
.caption {
animation-duration: 1ms;
animation-delay: 0ms;
}
}
</style>
</head>
<body>
<main class="stage" aria-label="Animated Wordle intro">
<button class="replay" type="button" aria-label="Replay intro">
Replay
</button>
<div class="intro" aria-hidden="true">
<div class="scanline"></div>
<div class="tiles" id="tiles">
<div class="tile" data-final="W" data-status="wrong">
<span class="letter">Q</span>
</div>
<div class="tile" data-final="O" data-status="wrong">
<span class="letter">I</span>
</div>
<div class="tile" data-final="R" data-status="wrong">
<span class="letter">N</span>
</div>
<div class="tile" data-final="D" data-status="wrong">
<span class="letter">Z</span>
</div>
<div class="tile" data-final="L" data-status="wrong">
<span class="letter">A</span>
</div>
<div class="tile" data-final="E" data-status="wrong">
<span class="letter">T</span>
</div>
</div>
</div>
<div class="caption">six letters flip into one answer</div>
</main>
<script>
const LETTER_SETS = [
["D", "E", "S", "I", "G", "N"],
["C", "A", "M", "E", "R", "A"],
["A", "S", "P", "E", "C", "T"],
["W", "O", "R", "D", "L", "E"],
];
const STATUS_SETS = [
["wrong", "wrong", "wrong", "wrong", "wrong", "wrong"],
["wrong", "misplaced", "wrong", "misplaced", "wrong", "wrong"],
[
"misplaced",
"wrong",
"misplaced",
"wrong",
"wrong",
"misplaced",
],
[
"correct",
"correct",
"correct",
"correct",
"correct",
"correct",
],
];
const SOUND_EVENTS = [
[280, "flip-start"],
[1280, "misplaced-pass"],
[2460, "answer-lock"],
[3720, "logo-lock"],
];
const stage = document.querySelector(".stage");
const tileRow = document.querySelector(".tiles");
const tiles = Array.from(document.querySelectorAll(".tile"));
const replay = document.querySelector(".replay");
let timers = [];
function setTile(tile, letter, status) {
tile.dataset.status = status;
tile.querySelector(".letter").textContent = letter;
}
function flipTile(tile, letter, status, delay) {
timers.push(
setTimeout(() => {
tile.classList.add("is-flipping");
timers.push(
setTimeout(
() => setTile(tile, letter, status),
130,
),
);
timers.push(
setTimeout(
() => tile.classList.remove("is-flipping"),
285,
),
);
}, delay),
);
}
function setFrame(frameIndex, animate = true) {
tiles.forEach((tile, index) => {
const letter = LETTER_SETS[frameIndex][index];
const status = STATUS_SETS[frameIndex][index];
if (animate) {
flipTile(tile, letter, status, index * 58);
} else {
tile.classList.remove("is-flipping");
setTile(tile, letter, status);
}
});
if (frameIndex === LETTER_SETS.length - 1) {
timers.push(
setTimeout(
() => tileRow.classList.add("is-solved"),
420,
),
);
timers.push(
setTimeout(
() => tileRow.classList.add("is-logo"),
1180,
),
);
}
}
function dispatchSoundCue(name) {
window.dispatchEvent(
new CustomEvent("wordleIntro:soundCue", {
detail: { name },
}),
);
}
function scheduleSequence() {
timers.forEach(clearTimeout);
timers = [];
tileRow.classList.remove("is-solved", "is-logo");
setFrame(0, false);
[880, 1720, 2580].forEach((time, index) => {
timers.push(
setTimeout(() => setFrame(index + 1, true), time),
);
});
SOUND_EVENTS.forEach(([time, name]) => {
timers.push(setTimeout(() => dispatchSoundCue(name), time));
});
}
function replayIntro() {
stage.classList.add("restarting");
scheduleSequence();
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
stage.classList.remove("restarting");
});
});
}
replay.addEventListener("click", replayIntro);
scheduleSequence();
</script>
</body>
</html>