Initial commit
This commit is contained in:
49
index.html
Normal file
49
index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Will you be my Valentine?</title>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="bg" aria-hidden="true"></div>
|
||||
<canvas id="sakura" aria-hidden="true"></canvas>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="card" id="mainCard">
|
||||
<div id="askScreen">
|
||||
<h1 id="title">Will you be my Valentine?</h1>
|
||||
|
||||
<img class="gif" id="gifImg" alt="Cute gif" />
|
||||
|
||||
<div class="btnRow" id="btnRow">
|
||||
<button id="yesBtn" type="button">Yes</button>
|
||||
<button id="noBtn" type="button">No</button>
|
||||
</div>
|
||||
|
||||
<p class="subtitle" id="subtitle"></p>
|
||||
</div>
|
||||
|
||||
<div class="success" id="successScreen">
|
||||
<h2>Yesss! 💗</h2>
|
||||
<p>I knew that you'd be mine!</p>
|
||||
<img class="gif" alt="Celebration gif"
|
||||
src="https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExOWpxbjdkejJjd3hydzlseTFxc2VvYTg3aDR2MTk3aXhuc3RidG52eiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/6CdI7BikMdZT2jRLF7/giphy.gif" />
|
||||
<div class="hearts" aria-hidden="true">
|
||||
<div class="heart"></div>
|
||||
<div class="heart"></div>
|
||||
<div class="heart"></div>
|
||||
<div class="heart"></div>
|
||||
<div class="heart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
281
main.css
Normal file
281
main.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
279
main.js
Normal file
279
main.js
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user