From 166eacdbfdc5e869a1417711c0288de4c6d86225 Mon Sep 17 00:00:00 2001 From: Zakaria Date: Thu, 21 May 2026 09:38:27 -0400 Subject: [PATCH] Adding intro to the app with a skip button --- .DS_Store | Bin 10244 -> 10244 bytes CSS/styles.css | 345 +++++++++++++++++++++++++ fancy-wordle-modern.html | 22 +- index.html | 22 +- script.js | 138 ++++++++++ wordle-intro-5.html | 532 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 1053 insertions(+), 6 deletions(-) create mode 100644 wordle-intro-5.html diff --git a/.DS_Store b/.DS_Store index a3236e6d75c946cf258221c59a84b4ac033c864f..20d52eb46f588c951df5351ed9666e6833404f35 100644 GIT binary patch delta 1487 zcmc&!&rcIk5S~{G+alQQLV;?G4ZUcfP!J=DG5nHfb3k+5^Svm&^ht_jP-12aHD!>7RubQppH6yZcfV@g_MY4k)Q z#}~9izwHH3=@6r2sU6M&0|oL(!6A!DkZf;QOS~}*aUc50m_&hf2*Dc0bI905gfJ~b zY;o!32i`tM%iHor@5x~gMBoy{AP!MjfLX+|a2e)d&aO1GH=%R1%)QT20vMQrV@XB4 zz0u_Jw5cc&z4LbY8ln)EQ@z#@+{8TteQ$iLRBo37V$c~M+C?slidp3Tki!9aLxFt9mtYBW3;7zvM$ZEd+-zToibcq*?Ibat=&K{{Hx zj|I9g^$M0RV|!%8R8>}s)rOGw^(>!7v6jc>Za>h`dC((zr7mx`*XQ%~^!E7=9rmZh zuC%P=bDXW{nHi=rKCP|Lr5sl?b46KE^<2WFx6C8*D9N(n3VfGEBLu(zR&OdSxt7 zZlEpw6O_2B$!V%Z^=W3BtdNvBm-QT-UQehB)2p?6Y+l^PIJG!~6W{PN;tEb)sDoow zAVRPT@H2PUQO%Ozn^LZd-SaxrX>c-D+hR`!U=Dz@fC)k(#X;VjyVfcjc#ay=_Wr762+!vD9dfS`O8Z z>f9?rc!@+$>Qh=y`e#BNe}^oD!)5&o9ES$zZv4MXxI|mB~BkyJ}0e{BHvO;Q; z8-->}-YzV|xMOmgK%u%sb+x&Pj)Ja1t&T#qp_wU=ZD?R&Qd`T(A*!rz9TcCPlbe^{ nH91Q}p0RuKdr9%lePZEklQm@>HkM_xZDv>a#j?3ef|(frlPV@j diff --git a/CSS/styles.css b/CSS/styles.css index 4df5f05..01c891a 100644 --- a/CSS/styles.css +++ b/CSS/styles.css @@ -48,6 +48,218 @@ body.dark-theme { --shadow: 0 24px 80px oklch(8% 0.01 50 / 0.38); } +body.intro-active { + overflow: hidden; +} + +body.intro-active .app-shell { + pointer-events: none; + user-select: none; +} + +.intro-overlay { + --intro-bg: #111111; + --intro-fg: #fafafa; + --intro-border: #e5e5e5; + --intro-accent: #2f6feb; + --intro-tile-empty: #3a3a3c; + --intro-tile-wrong: #787c7e; + --intro-tile-misplaced: #c9b458; + --intro-tile-correct: #6aaa64; + --intro-font-mono: ui-monospace, "JetBrains Mono", monospace; + + position: fixed; + inset: 0; + z-index: 1000; + display: grid; + min-block-size: 100svh; + place-items: center; + overflow: hidden; + background: linear-gradient(180deg, #181818 0%, var(--intro-bg) 58%, #080808 100%); + color: var(--intro-fg); + font-family: var(--font-body); + opacity: 1; + perspective: 1200px; + transition: opacity 520ms ease, visibility 520ms ease; + visibility: visible; +} + +.intro-overlay::before, +.intro-overlay::after { + content: ""; + position: absolute; + inset-inline: 0; + block-size: 20vh; + z-index: 4; + pointer-events: none; +} + +.intro-overlay::before { + inset-block-start: 0; + background: linear-gradient(180deg, rgba(17, 17, 17, 0.85), transparent); +} + +.intro-overlay::after { + inset-block-end: 0; + background: linear-gradient(0deg, rgba(17, 17, 17, 0.88), transparent); +} + +.intro-overlay.is-dismissing { + opacity: 0; + pointer-events: none; + visibility: hidden; +} + +.intro-overlay[hidden] { + display: none; +} + +.intro-stage { + position: relative; + display: grid; + inline-size: min(1120px, calc(100vw - 32px)); + block-size: min(640px, calc(100svh - 32px)); + min-block-size: 420px; + place-items: center; + transform-style: preserve-3d; +} + +.intro-scanline { + position: absolute; + inset-inline: 10%; + inset-block-start: 50%; + z-index: 1; + block-size: 1px; + background: linear-gradient(90deg, transparent, rgba(250, 250, 250, 0.36), transparent); + opacity: 0; + transform: translateY(-50%); + animation: intro-scan 4.8s ease forwards; +} + +.intro-tiles { + --intro-tile-size: clamp(46px, 10vw, 128px); + --intro-tile-gap: clamp(6px, 1.4vw, 16px); + + position: relative; + z-index: 2; + display: grid; + grid-template-columns: repeat(6, var(--intro-tile-size)); + gap: var(--intro-tile-gap); + transform-origin: 50% 50%; + transform-style: preserve-3d; + transition: gap 420ms cubic-bezier(0.2, 0.7, 0.15, 1), transform 520ms cubic-bezier(0.2, 0.7, 0.15, 1); +} + +.intro-tile { + position: relative; + inline-size: var(--intro-tile-size); + block-size: var(--intro-tile-size); + opacity: 0; + transform: translate3d(0, 34px, -180px); + transform-style: preserve-3d; + animation: intro-tile-enter 520ms cubic-bezier(0.2, 0.7, 0.15, 1) forwards; + animation-delay: calc(var(--intro-index) * 56ms); +} + +.intro-letter { + position: absolute; + inset: 0; + display: grid; + place-items: center; + border: 1px solid rgba(229, 229, 229, 0.18); + background: var(--intro-tile-empty); + box-shadow: inset 0 0 0 1px rgba(250, 250, 250, 0.08); + color: var(--intro-fg); + font-family: var(--font-body); + font-size: clamp(28px, 6vw, 78px); + font-weight: 900; + line-height: 1; + text-transform: uppercase; + transform-origin: 50% 50%; +} + +.intro-tile.is-flipping .intro-letter { + animation: intro-split-flap 260ms cubic-bezier(0.3, 0.7, 0.2, 1); +} + +.intro-tile[data-status="wrong"] .intro-letter { + background: var(--intro-tile-wrong); +} + +.intro-tile[data-status="misplaced"] .intro-letter { + background: var(--intro-tile-misplaced); +} + +.intro-tile[data-status="correct"] .intro-letter { + background: var(--intro-tile-correct); +} + +.intro-tile:nth-child(1) { --intro-index: 0; } +.intro-tile:nth-child(2) { --intro-index: 1; } +.intro-tile:nth-child(3) { --intro-index: 2; } +.intro-tile:nth-child(4) { --intro-index: 3; } +.intro-tile:nth-child(5) { --intro-index: 4; } +.intro-tile:nth-child(6) { --intro-index: 5; } + +.intro-tiles.is-solved .intro-tile { + opacity: 1; + animation: intro-logo-cascade 680ms cubic-bezier(0.18, 0.85, 0.22, 1) calc(var(--intro-index) * 76ms) both; +} + +.intro-tiles.is-logo { + gap: clamp(3px, 0.7vw, 8px); + transform: scale(0.88); +} + +.intro-tiles.is-logo .intro-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); +} + +.intro-caption { + position: absolute; + inset-block-end: clamp(24px, 6vh, 64px); + inset-inline: 24px; + z-index: 5; + display: flex; + justify-content: center; + color: rgba(250, 250, 250, 0.62); + font-family: var(--intro-font-mono); + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + opacity: 0; + animation: intro-caption-cue 4.8s ease forwards; +} + +.intro-skip { + position: fixed; + inset-block-start: 16px; + inset-inline-end: 16px; + z-index: 10; + min-height: 40px; + border: 1px solid rgba(229, 229, 229, 0.22); + border-radius: 999px; + padding: 0 16px; + background: rgba(17, 17, 17, 0.74); + color: var(--intro-fg); + cursor: pointer; + font: 800 0.85rem/1 var(--font-body); +} + +.intro-skip:hover { + border-color: rgba(250, 250, 250, 0.5); +} + +.intro-skip:focus-visible { + outline: 2px solid var(--intro-accent); + outline-offset: 3px; +} + +.intro-overlay.restarting * { + animation: none !important; +} + button, a { font: inherit; @@ -800,6 +1012,106 @@ h2 { text-align: start; } +@keyframes intro-tile-enter { + 0% { + transform: translate3d(0, 34px, -180px); + filter: blur(2px); + opacity: 0; + } + + 100% { + transform: translate3d(0, 0, 0); + filter: blur(0); + opacity: 1; + } +} + +@keyframes intro-split-flap { + 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 intro-logo-cascade { + 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 intro-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 intro-caption-cue { + 0%, + 18% { + opacity: 0; + transform: translateY(8px); + } + + 28%, + 64% { + opacity: 1; + transform: translateY(0); + } + + 82%, + 100% { + opacity: 0; + transform: translateY(-8px); + } +} + @keyframes shake { 10%, 90% { transform: translateX(-4%); } 30%, 70% { transform: translateX(5%); } @@ -814,6 +1126,39 @@ h2 { 100% { transform: translateY(0); } } +@media (max-width: 720px) { + .intro-stage { + min-block-size: 420px; + } + + .intro-tiles { + --intro-tile-size: min(13.5vw, 56px); + --intro-tile-gap: 5px; + } + + .intro-caption { + font-size: 10px; + } + + .intro-skip { + min-height: 38px; + padding-inline: 14px; + } +} + +@media (prefers-reduced-motion: reduce) { + .intro-overlay, + .intro-tiles, + .intro-tile, + .intro-letter, + .intro-scanline, + .intro-caption { + animation-duration: 1ms !important; + animation-delay: 0ms !important; + transition-duration: 1ms !important; + } +} + @media (max-width: 780px) { .app-shell { width: min(100% - 20px, 600px); diff --git a/fancy-wordle-modern.html b/fancy-wordle-modern.html index 7e64855..74d2c6a 100644 --- a/fancy-wordle-modern.html +++ b/fancy-wordle-modern.html @@ -5,14 +5,30 @@ Fancy Wordle - + - + - + + +
diff --git a/index.html b/index.html index 7e64855..74d2c6a 100644 --- a/index.html +++ b/index.html @@ -5,14 +5,30 @@ Fancy Wordle - + - + - + + +
diff --git a/script.js b/script.js index 28e870f..5997783 100644 --- a/script.js +++ b/script.js @@ -17,6 +17,9 @@ let authProfile = null let leaderboardScope = "hour" let latestLeaderboardRows = [] let lastDefinition = null +let introDismissed = false +let pendingInteractionStart = false +let introTimers = [] const WORD_LENGTH = 5 const MAX_GUESSES = 6 @@ -26,6 +29,25 @@ const STATS_KEY = "fancy-wordle-stats-v2" const LOCAL_ROUND_KEY = "fancy-wordle-hourly-round-v1" const PLAY_INTERVAL_MS = 60 * 60 * 1000 const GUESS_TIMEOUT_MS = 10000 +const INTRO_DURATION_MS = 5200 +const INTRO_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 INTRO_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 INTRO_SOUND_EVENTS = [ + [280, "flip-start"], + [1280, "misplaced-pass"], + [2460, "answer-lock"], + [3720, "logo-lock"] +] const KEY_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"] const FALLBACK_TARGET_WORDS = [ "about", @@ -102,6 +124,10 @@ const guessBars = document.getElementById("guess-bars") const statsDefinition = document.getElementById("stats-definition") const resetStatsButton = document.getElementById("reset-stats") const shareResultsButton = document.getElementById("share-results") +const introOverlay = document.getElementById("intro-overlay") +const introSkipButton = document.getElementById("intro-skip") +const introTileRow = document.getElementById("intro-tiles") +const introTiles = Array.from(document.querySelectorAll(".intro-tile")) if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeGame, { once: true }) @@ -113,6 +139,7 @@ async function initializeGame() { if (hasInitialized) return hasInitialized = true + initializeIntro() createBoard() createKeyboard() restoreTheme() @@ -132,6 +159,111 @@ async function initializeGame() { await startHourlyRound() } +function initializeIntro() { + if (!introOverlay || introTiles.length === 0) { + introDismissed = true + document.body.classList.remove("intro-active") + return + } + + if (introOverlay.dataset.initialized) return + introOverlay.dataset.initialized = "true" + introDismissed = false + document.body.classList.add("intro-active") + introSkipButton?.addEventListener("click", dismissIntro) + + if (prefersReducedMotion()) { + setIntroFrame(INTRO_LETTER_SETS.length - 1, false) + introTileRow?.classList.add("is-solved", "is-logo") + introTimers.push(setTimeout(dismissIntro, 700)) + return + } + + scheduleIntroSequence() + introTimers.push(setTimeout(dismissIntro, INTRO_DURATION_MS)) +} + +function prefersReducedMotion() { + return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches +} + +function setIntroTile(tile, letter, status) { + const letterElement = tile.querySelector(".intro-letter") + tile.dataset.status = status + if (letterElement) letterElement.textContent = letter +} + +function flipIntroTile(tile, letter, status, delay) { + introTimers.push(setTimeout(() => { + tile.classList.add("is-flipping") + introTimers.push(setTimeout(() => setIntroTile(tile, letter, status), 130)) + introTimers.push(setTimeout(() => tile.classList.remove("is-flipping"), 285)) + }, delay)) +} + +function setIntroFrame(frameIndex, animate = true) { + introTiles.forEach((tile, index) => { + const letter = INTRO_LETTER_SETS[frameIndex][index] + const status = INTRO_STATUS_SETS[frameIndex][index] + if (animate) { + flipIntroTile(tile, letter, status, index * 58) + return + } + + tile.classList.remove("is-flipping") + setIntroTile(tile, letter, status) + }) + + if (frameIndex === INTRO_LETTER_SETS.length - 1) { + introTimers.push(setTimeout(() => introTileRow?.classList.add("is-solved"), 420)) + introTimers.push(setTimeout(() => introTileRow?.classList.add("is-logo"), 1180)) + } +} + +function dispatchIntroSoundCue(name) { + window.dispatchEvent(new CustomEvent("wordleIntro:soundCue", { detail: { name } })) +} + +function scheduleIntroSequence() { + clearIntroTimers() + introTileRow?.classList.remove("is-solved", "is-logo") + setIntroFrame(0, false) + + const frameTimes = [880, 1720, 2580] + frameTimes.forEach((time, index) => { + introTimers.push(setTimeout(() => setIntroFrame(index + 1, true), time)) + }) + + INTRO_SOUND_EVENTS.forEach(([time, name]) => { + introTimers.push(setTimeout(() => dispatchIntroSoundCue(name), time)) + }) +} + +function clearIntroTimers() { + introTimers.forEach(clearTimeout) + introTimers = [] +} + +function dismissIntro() { + if (introDismissed) return + + introDismissed = true + clearIntroTimers() + setIntroFrame(INTRO_LETTER_SETS.length - 1, false) + introTileRow?.classList.add("is-solved", "is-logo") + introOverlay?.classList.add("is-dismissing") + document.body.classList.remove("intro-active") + + setTimeout(() => { + if (introOverlay) introOverlay.hidden = true + }, 540) + + if (pendingInteractionStart && !gameFinished) { + pendingInteractionStart = false + startInteraction() + } +} + async function loadWordLists() { targetWords = await loadWordList("targetWords") @@ -755,6 +887,12 @@ async function signOut() { } function startInteraction() { + if (!introDismissed) { + pendingInteractionStart = true + return + } + + pendingInteractionStart = false stopInteraction() document.addEventListener("click", handleMouseClick) document.addEventListener("keydown", handleKeyPress) diff --git a/wordle-intro-5.html b/wordle-intro-5.html new file mode 100644 index 0000000..cd8dd83 --- /dev/null +++ b/wordle-intro-5.html @@ -0,0 +1,532 @@ + + + + + + WORDLE Intro + + + +
+ + +
six letters flip into one answer
+
+ + + +