From 8fb64f8ebc9c8ac63dcdec0b59d01b6df6f75511 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Fri, 13 Feb 2026 19:29:30 +0200 Subject: [PATCH] Initial commit --- index.html | 49 ++++++++++ main.css | 281 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.js | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 609 insertions(+) create mode 100644 index.html create mode 100644 main.css create mode 100644 main.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..05b23e8 --- /dev/null +++ b/index.html @@ -0,0 +1,49 @@ + + + + + + + Will you be my Valentine? + + + + + + + +
+
+
+

Will you be my Valentine?

+ + Cute gif + +
+ + +
+ +

+
+ +
+

Yesss! 💗

+

I knew that you'd be mine!

+ Celebration gif + +
+
+
+ + + + + diff --git a/main.css b/main.css new file mode 100644 index 0000000..11efc40 --- /dev/null +++ b/main.css @@ -0,0 +1,281 @@ +:root { + --pink-1: #ffe0f0; + --pink-2: #ffb6d9; + --pink-3: #ff7fbe; + --pink-4: #ff4fa6; + --pink-5: #d81b7d; + + --card-bg: rgba(255, 255, 255, 0.72); + --card-border: rgba(255, 255, 255, 0.65); + --shadow: 0 18px 60px rgba(216, 27, 125, 0.18); + + --yes-width: 152px; + --no-width: 152px; +} + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, + "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; + color: #4a1230; + overflow: hidden; + background: linear-gradient(180deg, var(--pink-1), #fff 55%, var(--pink-1)); +} + +/* Animated dotted background */ +.bg { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(circle at 20% 25%, rgba(255, 127, 190, 0.22) 0 2px, transparent 3px), + radial-gradient(circle at 70% 65%, rgba(255, 79, 166, 0.18) 0 2px, transparent 3px), + radial-gradient(circle at 40% 80%, rgba(216, 27, 125, 0.14) 0 2px, transparent 3px), + radial-gradient(circle at 85% 30%, rgba(255, 182, 217, 0.22) 0 2px, transparent 3px), + radial-gradient(circle at 10% 70%, rgba(255, 127, 190, 0.16) 0 2px, transparent 3px); + background-size: 120px 120px; + animation: drift 12s linear infinite; + filter: saturate(1.05); +} + +@keyframes drift { + 0% { + transform: translate3d(0, 0, 0); + } + + 50% { + transform: translate3d(-25px, -18px, 0); + } + + 100% { + transform: translate3d(0, 0, 0); + } +} + +/* Layout */ +.wrap { + position: relative; + z-index: 2; + min-height: 100dvh; + display: grid; + place-items: center; + padding: 24px; +} + +.card { + width: min(680px, 92vw); + border-radius: 22px; + background: var(--card-bg); + border: 1px solid var(--card-border); + box-shadow: var(--shadow); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding: 26px 22px 22px; + text-align: center; +} + +h1 { + margin: 4px 0 12px; + font-size: clamp(26px, 5vw, 44px); + letter-spacing: -0.02em; + line-height: 1.05; + color: #6a1440; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); +} + +.subtitle { + margin: 8px 0 0; + font-size: clamp(14px, 2.6vw, 18px); + color: rgba(106, 20, 64, 0.9); + min-height: 1.6em; +} + +.gif { + width: min(320px, 76vw); + height: auto; + border-radius: 18px; + margin: 14px auto 16px; + display: block; + box-shadow: 0 12px 28px rgba(216, 27, 125, 0.18); + border: 1px solid rgba(255, 255, 255, 0.55); + background: rgba(255, 255, 255, 0.4); +} + +.btnRow { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 14px; + align-items: center; +} + +button { + width: 100%; + max-width: 100%; + border: 0; + border-radius: 16px; + padding: 14px 16px; + font-size: 18px; + font-weight: 800; + cursor: pointer; + transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease, + opacity 140ms ease; + touch-action: manipulation; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +button:active { + transform: translateY(1px) scale(0.99); +} + +#yesBtn { + width: var(--yes-width); + justify-self: end; + background: linear-gradient(180deg, #ff7fbe, #ff4fa6); + color: #fff; + box-shadow: 0 14px 30px rgba(255, 79, 166, 0.28); +} + +#yesBtn:hover { + filter: brightness(1.02); + box-shadow: 0 16px 38px rgba(255, 79, 166, 0.34); +} + +#noBtn { + width: var(--no-width); + justify-self: start; + background: linear-gradient(180deg, #ffd3ea, #ffb6d9); + color: #6a1440; + box-shadow: 0 10px 22px rgba(216, 27, 125, 0.12); + position: relative; +} + +#noBtn:hover { + filter: brightness(1.01); +} + +/* Success screen */ +.success { + display: none; + padding: 26px 22px 22px; +} + +.success h2 { + margin: 0 0 10px; + font-size: clamp(26px, 5vw, 40px); + color: #6a1440; +} + +.success p { + margin: 8px 0 0; + font-size: clamp(15px, 2.7vw, 18px); + color: rgba(106, 20, 64, 0.92); +} + +.hearts { + margin-top: 14px; + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} + +.heart { + width: 12px; + height: 12px; + background: #ff4fa6; + transform: rotate(45deg); + border-radius: 3px; + position: relative; + animation: pop 1200ms ease-in-out infinite; +} + +.heart::before, +.heart::after { + content: ""; + position: absolute; + width: 12px; + height: 12px; + background: #ff4fa6; + border-radius: 50%; +} + +.heart::before { + left: -6px; + top: 0; +} + +.heart::after { + left: 0; + top: -6px; +} + +.heart:nth-child(2) { + animation-delay: 120ms; +} + +.heart:nth-child(3) { + animation-delay: 240ms; +} + +.heart:nth-child(4) { + animation-delay: 360ms; +} + +.heart:nth-child(5) { + animation-delay: 480ms; +} + +@keyframes pop { + + 0%, + 100% { + transform: rotate(45deg) scale(1); + opacity: 0.85; + } + + 50% { + transform: rotate(45deg) scale(1.35); + opacity: 1; + } +} + +/* Sakura canvas overlay */ +canvas#sakura { + position: fixed; + inset: 0; + z-index: 1; + pointer-events: none; +} + +/* Mobile tweaks */ +@media (max-width: 420px) { + .btnRow { + gap: 10px; + } + + button { + font-size: 16px; + padding: 13px 14px; + } +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + + .bg, + .heart { + animation: none !important; + } +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..77b054a --- /dev/null +++ b/main.js @@ -0,0 +1,279 @@ +// ========= Customization ========= +// Add any GIFs you want to show in order. +const GIF_URLS = [ + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3hlZGx4dmdubzgyeG5sN3l4ZHVpY3htcW9pZHY0Yzhoc3JzaDczayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/IVK6xNBpEAHYyOdghk/giphy.gif", + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMWI4N3JuZDY2ZHlzdGF4eG9zbmV6c215em1ndHFzYnY0Mm01YWliaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/5NzRr5INmnXiG9xrFy/giphy.gif", + "https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExcTQyM2RqZnpwa2NwaDNnZzJzZnpxN25kMThpajl1cjYwODh0dHJ5dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/964WRtxBbYHzln3uN5/giphy.gif", // FLowers in hand + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExYnhlczQ2OW4zcGE1NGFwdjJmdDk0bHpwejg0cjdtYmN6OW4wOTJpdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/lfa5Bq6Am8Pf3a5AWU/giphy.gif", // YOu know I love you + "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExM3dram11MHdpMjM1dWF5dnlkZ3E0bjQyOHc5cHZ0dXNhdG1rNmF6dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9KI8vyCCAOqHbcMYd2/giphy.gif", // Mexican + "https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExeDN5c3BxcDA4OW83Y2RzNmcxcms1cGwxdnpjaXF3aGpxZ2U2aWtmcSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/zEtQ9FrbqIt3BVKY3i/giphy.gif" +]; +// ================================= + +const gifImg = document.getElementById("gifImg"); +gifImg.src = GIF_URLS[0]; + +const yesBtn = document.getElementById("yesBtn"); +const noBtn = document.getElementById("noBtn"); +const subtitle = document.getElementById("subtitle"); +const btnRow = document.getElementById("btnRow"); + +const askScreen = document.getElementById("askScreen"); +const successScreen = document.getElementById("successScreen"); + +// 5 stages of "No" presses +const noStages = [ + "I think you accidentally pressed no", + "Are you sure?", + "Really sure?", + "Please? Pretty please?", + "Last chance... you sure you want to say no?", +]; + +let noCount = 0; + +function clamp(v, min, max) { + return Math.max(min, Math.min(max, v)); +} + +function setButtonWidths() { + // Adjust width instead of transform scaling to avoid jumpy layout on phones. + const stage = Math.min(noCount, 4); + const rowWidth = btnRow.clientWidth || window.innerWidth * 0.9; + const rowStyle = getComputedStyle(btnRow); + const gap = Number.parseFloat(rowStyle.columnGap || rowStyle.gap || "0") || 0; + const perButtonMax = Math.max(120, (rowWidth - gap) / 2); + + const baseWidth = Math.min(172, perButtonMax * 0.92); + const yesWidth = clamp(baseWidth + stage * 18, 110, perButtonMax); + const noWidth = clamp(baseWidth - stage * 14, 84, perButtonMax); + + document.documentElement.style.setProperty("--yes-width", `${Math.round(yesWidth)}px`); + document.documentElement.style.setProperty("--no-width", `${Math.round(noWidth)}px`); +} + +function showSuccess() { + askScreen.style.display = "none"; + successScreen.style.display = "block"; +} + +yesBtn.addEventListener("click", () => showSuccess()); + +function advanceNoStage() { + if (noCount < 5) noCount++; + + // Update the GIF based on the current "no" count + gifImg.src = GIF_URLS[noCount]; + console.log(noCount); + + const stageIndex = Math.min(noCount - 1, 4); + subtitle.textContent = noStages[stageIndex] || ""; + + setButtonWidths(); + + // Enter final stage after 5th "no" attempt + if (noCount >= 5) enableNoEvadeMode(); +} + +noBtn.addEventListener("click", () => { + if (noCount < 5) { + advanceNoStage(); + } else { + // In evade mode, clicking should be basically impossible, but just in case: + teleportNoButton(); + } +}); + +// Final stage: make it impossible to click No by moving away on hover and pointerdown +let evadeEnabled = false; + +function getViewportBounds() { + const vv = window.visualViewport; + if (vv) { + return { + left: vv.offsetLeft, + top: vv.offsetTop, + width: vv.width, + height: vv.height + }; + } + + return { + left: 0, + top: 0, + width: window.innerWidth, + height: window.innerHeight + }; +} + +function enableNoEvadeMode() { + if (evadeEnabled) return; + evadeEnabled = true; + + noBtn.textContent = "No"; + subtitle.textContent = noStages[4]; + + // Make the No button position fixed so it can teleport around the viewport + noBtn.style.position = "fixed"; + noBtn.style.left = ""; + noBtn.style.top = ""; + + // Start it near the bottom center + const bounds = getViewportBounds(); + const startX = Math.round(bounds.left + bounds.width * 0.5); + const startY = Math.round(bounds.top + bounds.height * 0.72); + placeNoButton(startX, startY); + + // Evade on hover and on press + noBtn.addEventListener("pointerenter", teleportNoButton, { passive: true }); + noBtn.addEventListener("pointerdown", teleportNoButton, { passive: true }); + + // If the viewport changes, keep it clamped inside the visible phone screen. + window.addEventListener("resize", keepNoButtonOnScreen, { passive: true }); + if (window.visualViewport) { + window.visualViewport.addEventListener("resize", keepNoButtonOnScreen, { passive: true }); + window.visualViewport.addEventListener("scroll", keepNoButtonOnScreen, { passive: true }); + } +} + +function placeNoButton(centerX, centerY) { + const rect = noBtn.getBoundingClientRect(); + const pad = 12; + const bounds = getViewportBounds(); + + const minX = bounds.left + pad; + const minY = bounds.top + pad; + const maxX = Math.max(minX, bounds.left + bounds.width - rect.width - pad); + const maxY = Math.max(minY, bounds.top + bounds.height - rect.height - pad); + + const x = clamp(centerX - rect.width / 2, minX, maxX); + const y = clamp(centerY - rect.height / 2, minY, maxY); + + noBtn.style.left = Math.round(x) + "px"; + noBtn.style.top = Math.round(y) + "px"; +} + +function keepNoButtonOnScreen() { + if (!evadeEnabled) return; + const rect = noBtn.getBoundingClientRect(); + placeNoButton(rect.left + rect.width / 2, rect.top + rect.height / 2); +} + +function teleportNoButton() { + if (!evadeEnabled) return; + + // "Far away" point each time: jump to a different viewport region + const bounds = getViewportBounds(); + const left = bounds.left; + const top = bounds.top; + const w = bounds.width; + const h = bounds.height; + + const candidates = [ + { x: left + w * 0.16, y: top + h * 0.22 }, + { x: left + w * 0.84, y: top + h * 0.25 }, + { x: left + w * 0.18, y: top + h * 0.78 }, + { x: left + w * 0.82, y: top + h * 0.78 }, + { x: left + w * 0.5, y: top + h * 0.18 } + ]; + + const pick = candidates[Math.floor(Math.random() * candidates.length)]; + placeNoButton(pick.x, pick.y); +} + +// Start with neutral stage +setButtonWidths(); +window.addEventListener("resize", setButtonWidths, { passive: true }); + +// ========= Sakura falling leaves (canvas) ========= +const canvas = document.getElementById("sakura"); +const ctx = canvas.getContext("2d"); + +function resizeCanvas() { + const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); + canvas.width = Math.floor(window.innerWidth * dpr); + canvas.height = Math.floor(window.innerHeight * dpr); + canvas.style.width = window.innerWidth + "px"; + canvas.style.height = window.innerHeight + "px"; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); +} + +resizeCanvas(); +window.addEventListener("resize", resizeCanvas, { passive: true }); + +const petals = []; +const PETAL_COUNT = Math.round(Math.min(90, Math.max(40, window.innerWidth / 10))); + +function rand(min, max) { + return min + Math.random() * (max - min); +} + +function makePetal() { + const size = rand(6, 14); + return { + x: rand(0, window.innerWidth), + y: rand(-window.innerHeight, 0), + vx: rand(-0.35, 0.55), + vy: rand(0.8, 1.8), + rot: rand(0, Math.PI * 2), + vr: rand(-0.02, 0.02), + wobble: rand(0.8, 1.8), + size, + hue: rand(330, 350), // pink range + alpha: rand(0.55, 0.9) + }; +} + +for (let i = 0; i < PETAL_COUNT; i++) petals.push(makePetal()); + +function drawPetal(p) { + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(p.rot); + + // Sakura-like petal shape + const s = p.size; + ctx.beginPath(); + ctx.moveTo(0, -s * 0.9); + ctx.bezierCurveTo(s * 0.9, -s * 0.9, s * 0.95, s * 0.2, 0, s); + ctx.bezierCurveTo(-s * 0.95, s * 0.2, -s * 0.9, -s * 0.9, 0, -s * 0.9); + ctx.closePath(); + + ctx.fillStyle = `hsla(${p.hue}, 90%, 82%, ${p.alpha})`; + ctx.fill(); + + // soft highlight + ctx.globalAlpha = p.alpha * 0.45; + ctx.strokeStyle = "rgba(255,255,255,0.55)"; + ctx.lineWidth = 1; + ctx.stroke(); + + ctx.restore(); +} + +let lastT = performance.now(); + +function tick(t) { + const dt = Math.min(33, t - lastT); + lastT = t; + + ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); + + for (const p of petals) { + p.x += p.vx * dt * 0.06 + Math.sin((p.y / 40) * p.wobble) * 0.2; + p.y += p.vy * dt * 0.06; + p.rot += p.vr * dt; + + if (p.y > window.innerHeight + 30 || p.x < -40 || p.x > window.innerWidth + 40) { + // respawn from top + p.x = rand(0, window.innerWidth); + p.y = rand(-120, -20); + } + + drawPetal(p); + } + + requestAnimationFrame(tick); +} + +requestAnimationFrame(tick);