0
ART/VISUAL#2aee54c7
Happy Birthday! π
@OwnerΒ·deposited 3w agoΒ·updated 2w agoΒ·73 views
ART/VISUAL#2aee54c7
Happy Birthday! π
OW
@Owner
73Views
0Comments
0Forks
0Saves
SHARE Β· REMIX
Happy Birthday! π β a HTML Art/Visual widget by @Owner.
CONTROLS
No comments yet. Be the first!
β¦ Remix with AI
SDK in this widgetNo Vibes SDK features detected yet
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Happy Birthday! π" by @Owner]
Source: https://itjustvibes.com/Owner/happy-birthday
Type: HTML
--- SOURCE CODE ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Happy Birthday! π</title>
<link href="https://fonts.googleapis.com/css2?family=Pacifico&family=Nunito:wght@400;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
background: #1a0533;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: 'Nunito', sans-serif;
}
/* Stars */
.stars {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
}
.star {
position: absolute;
border-radius: 50%;
background: white;
animation: twinkle var(--d) ease-in-out infinite;
animation-delay: var(--delay);
}
@keyframes twinkle {
0%,100% { opacity: 0.2; transform: scale(1); }
50% { opacity: 1; transform: scale(1.4); }
}
/* Confetti */
.confetti-wrap {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
}
.piece {
position: absolute;
top: -20px;
width: var(--w);
height: var(--h);
background: var(--color);
border-radius: var(--r);
animation: fall var(--fall-d) linear infinite;
animation-delay: var(--fall-delay);
opacity: 0.85;
transform: rotate(var(--rot));
}
@keyframes fall {
0% { top: -5%; transform: rotate(var(--rot)) translateX(0); }
50% { transform: rotate(calc(var(--rot) + 180deg)) translateX(40px); }
100% { top: 105%; transform: rotate(calc(var(--rot) + 360deg)) translateX(-20px); opacity: 0; }
}
/* Card */
.card {
position: relative;
z-index: 10;
text-align: center;
padding: 3rem 3.5rem 2.5rem;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 28px;
backdrop-filter: blur(12px);
max-width: 520px;
width: 90vw;
animation: cardIn 0.8s cubic-bezier(0.22,1,0.36,1) both;
}
@keyframes cardIn {
from { opacity:0; transform: translateY(40px) scale(0.95); }
to { opacity:1; transform: translateY(0) scale(1); }
}
/* Balloons row */
.balloons {
display: flex;
justify-content: center;
gap: 14px;
margin-bottom: 1.5rem;
}
.balloon {
display: flex;
flex-direction: column;
align-items: center;
animation: floatBalloon var(--fb-d) ease-in-out infinite;
animation-delay: var(--fb-delay);
}
.balloon-body {
width: 44px;
height: 56px;
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
background: var(--bc);
position: relative;
box-shadow: inset -8px -6px 0 rgba(0,0,0,0.12), inset 6px 6px 0 rgba(255,255,255,0.25);
}
.balloon-body::after {
content: '';
position: absolute;
bottom: -7px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 8px solid var(--bc);
}
.balloon-string {
width: 1.5px;
height: 38px;
background: rgba(255,255,255,0.4);
margin-top: 1px;
}
@keyframes floatBalloon {
0%,100% { transform: translateY(0) rotate(-3deg); }
50% { transform: translateY(-12px) rotate(3deg); }
}
/* Title */
h1 {
font-family: 'Pacifico', cursive;
font-size: clamp(2.2rem, 8vw, 3.4rem);
color: #fff;
line-height: 1.15;
text-shadow: 0 0 40px rgba(255,180,100,0.5), 0 2px 0 rgba(0,0,0,0.3);
animation: titlePop 0.6s cubic-bezier(0.22,1,0.36,1) 0.3s both;
}
@keyframes titlePop {
from { opacity:0; transform: scale(0.7); }
to { opacity:1; transform: scale(1); }
}
.subtitle {
margin-top: 0.8rem;
font-size: 1.05rem;
color: rgba(255,255,255,0.65);
letter-spacing: 0.04em;
animation: fadeUp 0.6s ease 0.6s both;
}
@keyframes fadeUp {
from { opacity:0; transform: translateY(10px); }
to { opacity:1; transform: translateY(0); }
}
/* Cake */
.cake-wrap {
margin: 1.8rem auto 0;
width: 160px;
animation: fadeUp 0.6s ease 0.5s both;
}
svg.cake { width: 100%; height: auto; overflow: visible; }
@keyframes flicker {
0%,100% { transform: scaleY(1) scaleX(1); opacity:1; }
25% { transform: scaleY(1.2) scaleX(0.85); }
50% { transform: scaleY(0.8) scaleX(1.1); opacity:0.85; }
75% { transform: scaleY(1.1) scaleX(0.9); }
}
.flame { animation: flicker 0.5s ease-in-out infinite; transform-origin: 50% 100%; }
.flame:nth-child(2) { animation-delay: 0.13s; }
.flame:nth-child(3) { animation-delay: 0.27s; }
/* Divider */
.divider {
margin: 1.8rem auto 0;
width: 60px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.35), transparent);
border-radius: 2px;
}
/* Footer */
.footer {
margin-top: 1.2rem;
font-size: 0.82rem;
color: rgba(255,255,255,0.3);
letter-spacing: 0.08em;
text-transform: uppercase;
}
</style>
</head>
<body>
<!-- Stars -->
<div class="stars" id="stars"></div>
<!-- Confetti -->
<div class="confetti-wrap" id="confetti"></div>
<div class="card">
<!-- Balloons -->
<div class="balloons">
<div class="balloon" style="--bc:#f472b6; --fb-d:3.2s; --fb-delay:0s">
<div class="balloon-body"></div><div class="balloon-string"></div>
</div>
<div class="balloon" style="--bc:#facc15; --fb-d:2.8s; --fb-delay:0.4s">
<div class="balloon-body"></div><div class="balloon-string"></div>
</div>
<div class="balloon" style="--bc:#fb923c; --fb-d:3.5s; --fb-delay:0.2s">
<div class="balloon-body"></div><div class="balloon-string"></div>
</div>
<div class="balloon" style="--bc:#34d399; --fb-d:3s; --fb-delay:0.6s">
<div class="balloon-body"></div><div class="balloon-string"></div>
</div>
<div class="balloon" style="--bc:#a78bfa; --fb-d:2.6s; --fb-delay:0.3s">
<div class="balloon-body"></div><div class="balloon-string"></div>
</div>
</div>
<h1>Happy Birthday!</h1>
<p class="subtitle">Wishing you a magical day β¨</p>
<!-- Cake SVG -->
<div class="cake-wrap">
<svg class="cake" viewBox="0 0 160 130" xmlns="http://www.w3.org/2000/svg">
<!-- Candles -->
<rect x="44" y="32" width="10" height="24" rx="3" fill="#f472b6"/>
<rect x="75" y="28" width="10" height="28" rx="3" fill="#facc15"/>
<rect x="106" y="32" width="10" height="24" rx="3" fill="#60a5fa"/>
<!-- Flames -->
<g class="flame">
<ellipse cx="49" cy="28" rx="5" ry="8" fill="#fbbf24"/>
<ellipse cx="49" cy="30" rx="3" ry="5" fill="#fef3c7" opacity="0.9"/>
</g>
<g class="flame">
<ellipse cx="80" cy="24" rx="5" ry="8" fill="#fbbf24"/>
<ellipse cx="80" cy="26" rx="3" ry="5" fill="#fef3c7" opacity="0.9"/>
</g>
<g class="flame">
<ellipse cx="111" cy="28" rx="5" ry="8" fill="#fbbf24"/>
<ellipse cx="111" cy="30" rx="3" ry="5" fill="#fef3c7" opacity="0.9"/>
</g>
<!-- Top cake tier -->
<rect x="30" y="56" width="100" height="30" rx="6" fill="#86efac"/>
<path d="M30 62 Q45 52 60 62 Q75 52 90 62 Q105 52 120 62 Q130 56 130 62" fill="none" stroke="white" stroke-width="2" opacity="0.6"/>
<!-- Middle tier -->
<rect x="16" y="82" width="128" height="30" rx="6" fill="#f9a8d4"/>
<path d="M16 88 Q30 78 44 88 Q58 78 72 88 Q86 78 100 88 Q114 78 128 88 Q136 82 144 88" fill="none" stroke="white" stroke-width="2" opacity="0.6"/>
<!-- Bottom tier -->
<rect x="4" y="108" width="152" height="18" rx="6" fill="#fcd34d"/>
<path d="M4 113 Q16 104 28 113 Q40 104 52 113 Q64 104 76 113 Q88 104 100 113 Q112 104 124 113 Q136 104 148 113 Q153 108 156 113" fill="none" stroke="white" stroke-width="2" opacity="0.6"/>
<!-- Plate shadow -->
<ellipse cx="80" cy="128" rx="76" ry="5" fill="rgba(0,0,0,0.2)"/>
</svg>
</div>
<div class="divider"></div>
<p class="footer">Made with β₯</p>
</div>
<script>
// Generate stars
const starsEl = document.getElementById('stars');
for (let i = 0; i < 80; i++) {
const s = document.createElement('div');
s.className = 'star';
const size = Math.random() * 2.5 + 1;
s.style.cssText = `width:${size}px;height:${size}px;left:${Math.random()*100}%;top:${Math.random()*100}%;--d:${1.5+Math.random()*3}s;--delay:${Math.random()*4}s`;
starsEl.appendChild(s);
}
// Generate confetti
const confettiEl = document.getElementById('confetti');
const colors = ['#f472b6','#facc15','#fb923c','#34d399','#60a5fa','#a78bfa','#f87171','#2dd4bf'];
for (let i = 0; i < 55; i++) {
const p = document.createElement('div');
p.className = 'piece';
const isRect = Math.random() > 0.4;
p.style.cssText = `
left:${Math.random()*100}%;
--color:${colors[Math.floor(Math.random()*colors.length)]};
--w:${isRect ? (5+Math.random()*7)+'px' : (4+Math.random()*5)+'px'};
--h:${isRect ? (10+Math.random()*8)+'px' : (4+Math.random()*5)+'px'};
--r:${isRect ? '2px' : '50%'};
--rot:${Math.random()*360}deg;
--fall-d:${4+Math.random()*6}s;
--fall-delay:${-Math.random()*8}s;
`;
confettiEl.appendChild(p);
}
</script>
</body>
</html>
```
[REQUESTED CHANGES]
(no specific request β apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget β tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** β do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT β Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state β it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected β do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` β state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** β they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** β it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** β the SDK may not be ready immediately.
4. **Await all SDK calls** β every method (except `vibes.shared.get`) returns a Promise.
5. **Do NOT import external JS libraries via script tags** β they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** β that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser β no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/Owner/happy-birthday */