280 lines
9.1 KiB
JavaScript
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);
|