diff --git a/index.html b/index.html index 0fe2fba..bff65d6 100644 --- a/index.html +++ b/index.html @@ -9,14 +9,22 @@

Happy Button

- -

Total clicks: 0

-

Clicks until celebration: 5

-
+ + +
+

Alternation sequence: 0 / 6

+

+ Alternate between both buttons to win.

+

Пасхалка ждет тебя!

+ + +
diff --git a/script.js b/script.js index 6291c97..adebf18 100644 --- a/script.js +++ b/script.js @@ -1,50 +1,121 @@ -const TOTAL_KEY = "happyButtonTotalClicks"; -const CYCLE_KEY = "happyButtonCelebrationClicks"; -const CELEBRATION_INTERVAL = 5; +const SEQUENCE_KEY = "happyButtonAlternationSequence"; +const LAST_BUTTON_KEY = "happyButtonLastButton"; +const SEQUENCE_TARGET = 6; +const EASTER_TARGET = 3; const button = document.getElementById("happy-button"); +const secondButton = document.getElementById("second-button"); const clickStatus = document.getElementById("click-status"); const celebrationStatus = document.getElementById("celebration-status"); const celebrationMessage = document.getElementById("celebration-message"); +const easterHint = document.getElementById("easter-hint"); +const confetti = document.getElementById("confetti"); +const explosion = document.getElementById("explosion"); function readStoredCount(key) { const value = Number.parseInt(window.localStorage.getItem(key) ?? "0", 10); return Number.isFinite(value) && value >= 0 ? value : 0; } -let totalClicks = readStoredCount(TOTAL_KEY); -let celebrationClicks = readStoredCount(CYCLE_KEY); +let sequenceCount = Math.min(readStoredCount(SEQUENCE_KEY), SEQUENCE_TARGET); +let lastButton = window.localStorage.getItem(LAST_BUTTON_KEY); +let easterClicks = 0; function renderState(showCelebration = false) { - const isGreenButton = totalClicks % 2 === 0; + const hasStarted = sequenceCount > 0; + const isComplete = sequenceCount >= SEQUENCE_TARGET || showCelebration; - button.classList.toggle("button-green", isGreenButton); - button.classList.toggle("button-red", !isGreenButton); - document.body.classList.toggle("state-green", isGreenButton); - document.body.classList.toggle("state-red", !isGreenButton); + document.body.classList.toggle("state-green", !isComplete && lastButton !== "one"); + document.body.classList.toggle("state-red", !isComplete && lastButton === "one"); + document.body.classList.toggle("state-win", isComplete); - clickStatus.textContent = `Total clicks: ${totalClicks}`; - celebrationStatus.textContent = `Clicks until celebration: ${ - CELEBRATION_INTERVAL - celebrationClicks - }`; - if (showCelebration) { - celebrationMessage.textContent = `Congratulations! You clicked the button ${totalClicks} times.`; + clickStatus.textContent = `Alternation sequence: ${sequenceCount} / ${SEQUENCE_TARGET}`; + celebrationStatus.textContent = hasStarted + ? `Next click: ${lastButton === "one" ? "Button 2" : "Button 1"}` + : "Alternate between both buttons to win."; + celebrationMessage.hidden = !isComplete; + + if (isComplete) { + celebrationMessage.textContent = "Ты выйграл"; + celebrationStatus.textContent = "Celebration unlocked!"; + launchConfetti(); } - celebrationMessage.hidden = !showCelebration; } -button.addEventListener("click", () => { - totalClicks += 1; - celebrationClicks += 1; +function storeSequence() { + window.localStorage.setItem(SEQUENCE_KEY, String(sequenceCount)); + if (lastButton) { + window.localStorage.setItem(LAST_BUTTON_KEY, lastButton); + } +} + +function handleButtonClick(buttonName) { + if (!lastButton) { + sequenceCount = 1; + } else if (lastButton === buttonName) { + sequenceCount = 1; + } else { + sequenceCount += 1; + } + + lastButton = buttonName; - const shouldCelebrate = celebrationClicks === CELEBRATION_INTERVAL; + const shouldCelebrate = sequenceCount >= SEQUENCE_TARGET; if (shouldCelebrate) { - celebrationClicks = 0; + sequenceCount = SEQUENCE_TARGET; } - window.localStorage.setItem(TOTAL_KEY, String(totalClicks)); - window.localStorage.setItem(CYCLE_KEY, String(celebrationClicks)); + storeSequence(); renderState(shouldCelebrate); +} + +function launchConfetti() { + confetti.replaceChildren(); + for (let index = 0; index < 24; index += 1) { + const piece = document.createElement("span"); + piece.style.setProperty("--x", `${Math.random() * 240 - 120}px`); + piece.style.setProperty("--delay", `${Math.random() * 120}ms`); + piece.style.setProperty("--color", confettiColor(index)); + confetti.append(piece); + } +} + +function confettiColor(index) { + const colors = ["#f97316", "#22c55e", "#3b82f6", "#ec4899", "#eab308"]; + return colors[index % colors.length]; +} + +function triggerExplosion() { + explosion.replaceChildren(); + explosion.classList.remove("is-active"); + for (let index = 0; index < 16; index += 1) { + const spark = document.createElement("span"); + const angle = (index / 16) * Math.PI * 2; + spark.style.setProperty("--dx", `${Math.cos(angle) * 120}px`); + spark.style.setProperty("--dy", `${Math.sin(angle) * 120}px`); + explosion.append(spark); + } + window.requestAnimationFrame(() => { + explosion.classList.add("is-active"); + }); +} + +function handleEasterHint() { + easterClicks += 1; + if (easterClicks >= EASTER_TARGET) { + easterClicks = 0; + triggerExplosion(); + } +} + +button.addEventListener("click", () => handleButtonClick("one")); +secondButton.addEventListener("click", () => handleButtonClick("two")); +easterHint.addEventListener("click", handleEasterHint); +easterHint.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleEasterHint(); + } }); renderState(false); diff --git a/style.css b/style.css index a52a25b..4095a20 100644 --- a/style.css +++ b/style.css @@ -28,7 +28,13 @@ body.state-red { background: #16a34a; } +body.state-win { + background: #fde047; +} + .app { + position: relative; + overflow: hidden; width: min(92vw, 28rem); padding: 2rem; text-align: center; @@ -43,6 +49,12 @@ h1 { line-height: 1; } +.button-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; +} + .happy-button { width: 100%; min-height: 4rem; @@ -80,6 +92,19 @@ h1 { font-weight: 600; } +.hint { + margin: 1rem 0 0; + color: #4b5563; + cursor: pointer; + font-size: 0.95rem; + font-weight: 700; +} + +.hint:focus-visible { + outline: 3px solid #111827; + outline-offset: 3px; +} + .celebration { margin: 1.25rem 0 0; padding: 0.875rem 1rem; @@ -90,6 +115,42 @@ h1 { animation: celebrate-in 240ms ease-out; } +.confetti, +.explosion { + pointer-events: none; + position: absolute; + inset: 0; + overflow: hidden; +} + +.confetti span { + position: absolute; + top: 1rem; + left: 50%; + width: 0.6rem; + height: 0.9rem; + background: var(--color); + opacity: 0; + transform: translate(-50%, 0) rotate(0deg); + animation: confetti-drop 900ms ease-out var(--delay) forwards; +} + +.explosion span { + position: absolute; + top: 58%; + left: 50%; + width: 0.75rem; + height: 0.75rem; + border-radius: 999px; + background: #f97316; + opacity: 0; + transform: translate(-50%, -50%) scale(0.4); +} + +.explosion.is-active span { + animation: explode 620ms ease-out forwards; +} + @keyframes celebrate-in { from { opacity: 0; @@ -101,8 +162,39 @@ h1 { } } +@keyframes confetti-drop { + 0% { + opacity: 1; + transform: translate(-50%, -1rem) rotate(0deg); + } + 100% { + opacity: 0; + transform: translate(calc(-50% + var(--x)), 11rem) rotate(260deg); + } +} + +@keyframes explode { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(0.4); + } + 100% { + opacity: 0; + transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) + scale(1.2); + } +} + @media (prefers-reduced-motion: reduce) { - .celebration { + .celebration, + .confetti span, + .explosion.is-active span { animation: none; } } + +@media (max-width: 28rem) { + .button-row { + grid-template-columns: 1fr; + } +}