Files
valentines/main.js
2026-02-13 19:29:30 +02:00

280 lines
9.1 KiB
JavaScript

// ========= 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);