diff --git a/README.md b/README.md index 87953c5..1de67fb 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ A developer portfolio site for Vlo, featuring: - Projects synced from GitHub (`VloStudios`) -- Matrix-style background -- Lo‑fi music player with selectable tracks -- Cherry Clicker game (Japanese cherry vibes) -- Chill, dark, hacker aesthetic -- **Scrollable content** (fixed matrix background with scrolling foreground) +- Sakura-themed CV presentation +- Iced-tea-inspired design accents +- Background ambient music toggle +- **Scrollable card layout** with GitHub-powered profile/repo data ## Setup @@ -28,18 +27,17 @@ A developer portfolio site for Vlo, featuring: ## Features -- **Projects Tab**: Fetches public repositories from the GitHub user `VloStudios` and displays them as cards. -- **Matrix Background**: Animated green code rain. -- **Lo‑fi Player**: Choose from a few free lo‑fi tracks and play/pause. -- **Cherry Clicker**: Japanese cherry‑themed clicker game with auto‑clickers and petal animations. -- **Ice‑tea Mention**: Tagline references Vlo’s love for ice tea. -- **Scrolling**: The content scrolls while the matrix background stays fixed. +- **CV Snapshot**: Pulls profile details from `https://api.github.com/users/VloStudios`. +- **Projects Section**: Fetches public repositories and displays the most recently updated. +- **Sakura UI**: Soft pink card-based visuals with blossom-inspired background elements. +- **Ambient Background Music**: Browser-based synth music with play/pause controls. +- **Iced Tea Theme**: Dedicated section and palette accents for a calm tea vibe. ## Customization -- Edit `static/js/lofi.js` to add or change lo‑fi track URLs. +- Edit `static/js/main.js` to change profile username or CV rendering behavior. - Adjust styling in `static/css/style.css`. -- Modify the Flask app in `app.py` if you need different GitHub user or additional data. +- Modify the Flask app in `app.py` if you want server-rendered data endpoints. ## Deployment diff --git a/static/css/style.css b/static/css/style.css index 4828fef..9b4d94f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,271 +1,238 @@ -/* Reset and base */ -*, -*::before, -*::after { +:root { + --bg: #fff7fb; + --bg-soft: #ffeef4; + --ink: #4a2d3c; + --ink-soft: #7f5a6d; + --sakura: #f7b5c8; + --sakura-deep: #e98cab; + --tea: #d59f63; + --card: rgba(255, 255, 255, 0.82); + --border: rgba(233, 140, 171, 0.35); +} + +* { box-sizing: border-box; - margin: 0; - padding: 0; } -html, body { - height: 100%; - font-family: 'JetBrains Mono', monospace; - background: #000; - color: #0f0; - overflow: visible; +body { margin: 0; - padding: 0; + font-family: "Quicksand", sans-serif; + color: var(--ink); + background: + radial-gradient(circle at 10% 10%, #ffe7f0, transparent 45%), + radial-gradient(circle at 90% 80%, #ffe6d2, transparent 35%), + linear-gradient(150deg, var(--bg), var(--bg-soft)); + min-height: 100vh; } -/* Matrix canvas */ -#matrix { +.petal-layer { position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -1; -} - -/* Content overlay */ -.content { - position: relative; - max-width: 1200px; - margin: 0 auto; - min-height: 100vh; - padding: 20px 20px 60px; - display: flex; - flex-direction: column; - overflow-y: auto; - box-sizing: border-box; + inset: 0; + pointer-events: none; + opacity: 0.5; + background-image: + radial-gradient(circle at 20% 30%, rgba(247, 181, 200, 0.7) 0 6px, transparent 7px), + radial-gradient(circle at 80% 10%, rgba(247, 181, 200, 0.5) 0 8px, transparent 9px), + radial-gradient(circle at 60% 70%, rgba(233, 140, 171, 0.45) 0 7px, transparent 8px); + animation: drift 14s linear infinite alternate; } -/* Header */ -header { - text-align: center; - margin-bottom: 30px; +@keyframes drift { + from { transform: translateY(0px); } + to { transform: translateY(18px); } } -header h1 { - font-size: 3.5rem; - letter-spacing: 2px; - text-shadow: 0 0 5px #0f0, 0 0 10px #0f0; +.cv-shell { + width: min(960px, 92vw); + margin: 2.5rem auto; + display: grid; + gap: 1rem; } -.tagline { - font-size: 1.2rem; - opacity: 0.8; - margin-top: 10px; +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 18px; + padding: 1.25rem; + backdrop-filter: blur(4px); + box-shadow: 0 10px 30px rgba(109, 46, 76, 0.08); } -/* Projects section */ -.projects h2 { - margin-bottom: 20px; - font-size: 2rem; - text-shadow: 0 0 3px #0f0; +.hero { + display: flex; + gap: 1rem; + justify-content: space-between; + align-items: center; } -.projects-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 20px; +.eyebrow { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--ink-soft); + margin: 0; } -.project-card { - background: rgba(0, 20, 0, 0.6); - border: 1px solid #0f0; - border-radius: 8px; - padding: 15px; - transition: transform 0.2s, box-shadow 0.2s; +h1, h2 { + font-family: "Playfair Display", serif; + margin: 0.4rem 0; } -.project-card:hover { - transform: translateY(-5px); - box-shadow: 0 0 15px #0f0; +h1 { + font-size: clamp(2rem, 4vw, 2.8rem); } -.project-card h3 { - margin: 0 0 10px; +h2 { font-size: 1.4rem; } -.project-card p { - font-size: 0.95rem; - line-height: 1.4; - margin-bottom: 10px; +p { + margin: 0.35rem 0; + line-height: 1.5; } -.project-card .meta { - display: flex; - justify-content: space-between; - font-size: 0.85rem; - opacity: 0.7; +.avatar { + width: clamp(110px, 18vw, 150px); + height: clamp(110px, 18vw, 150px); + object-fit: cover; + border-radius: 50%; + border: 4px solid rgba(255, 255, 255, 0.9); + box-shadow: 0 0 0 5px rgba(233, 140, 171, 0.2); } -.project-card a { - color: #0f0; - text-decoration: none; +.chips { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.85rem 0; } -.project-card a:hover { - text-decoration: underline; +.chip { + background: rgba(247, 181, 200, 0.2); + border: 1px solid rgba(233, 140, 171, 0.35); + border-radius: 999px; + padding: 0.3rem 0.65rem; + font-size: 0.85rem; } -/* Extras section */ -.extras { - margin-top: auto; +.hero-links { display: flex; - flex-wrap: wrap; - gap: 20px; - align-items: flex-start; -} - -.lofi-controls, -.cookie-clicker-container { - flex: 1 1 250px; - background: rgba(0, 20, 0, 0.5); - border: 1px solid #0f0; - border-radius: 8px; - padding: 15px; -} - -.lofi-controls label, -.cookie-clicker-container h3 { - display: block; - margin-bottom: 8px; - font-size: 1.1rem; -} - -#lofi-select { - width: 100%; - padding: 6px; - margin-bottom: 10px; - background: #000; - color: #0f0; - border: 1px solid #0f0; - border-radius: 4px; -} - -#lofi-toggle { - width: 100%; - padding: 8px; - background: #000; - color: #0f0; - border: 1px solid #0f0; - border-radius: 4px; + gap: 0.6rem; +} + +.btn { + appearance: none; + border: none; + border-radius: 10px; + background: linear-gradient(135deg, var(--sakura), var(--sakura-deep)); + color: #fff; + padding: 0.55rem 0.9rem; + font-weight: 700; cursor: pointer; + text-decoration: none; + display: inline-block; } -#lofi-toggle:hover { - background: #003300; +.btn.ghost { + background: rgba(255, 255, 255, 0.85); + border: 1px solid rgba(233, 140, 171, 0.45); + color: var(--ink); } -#lofi-status { - display: block; - margin-top: 8px; - font-size: 0.9rem; +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 0.7rem; } +.stats article { + background: rgba(255, 255, 255, 0.66); + border-radius: 12px; + padding: 0.7rem; + border: 1px solid rgba(233, 140, 171, 0.2); +} -footer { - text-align: center; - margin-top: 30px; - font-size: 0.9rem; - opacity: 0.6; +.stats h3 { + margin: 0; + font-size: 0.8rem; + color: var(--ink-soft); + text-transform: uppercase; + letter-spacing: 0.05em; } -footer a { - color: #0f0; - text-decoration: none; +.stats p { + margin-top: 0.35rem; + font-size: 1.3rem; + font-weight: 700; } -footer a:hover { - text-decoration: underline; +.tea-card { + border-left: 5px solid var(--tea); } -/* Cookie Clicker Styles */ -.cookie-clicker-container { - text-align: center; - background: rgba(30,0,30,0.4); - border-color: #f09; + +.tea-note { + margin-top: 0.55rem; + background: rgba(213, 159, 99, 0.14); + border-radius: 10px; + padding: 0.65rem; } -#cookie-count { - font-size: 2.5rem; - font-weight: bold; - color: #ffb6c1; - display: block; - margin: 10px 0; +.repo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.7rem; } -#cookie-button { - background: #ffb6c1; - color: #8b0000; - border: none; - border-radius: 50%; - width: 120px; - height: 120px; - font-size: 2rem; - cursor: pointer; - box-shadow: 0 4px 8px rgba(0,0,0,0.3); - transition: transform 0.1s, box-shadow 0.2s; - position: relative; - overflow: hidden; +.repo-card { + background: rgba(255, 255, 255, 0.68); + border: 1px solid rgba(233, 140, 171, 0.25); + border-radius: 12px; + padding: 0.75rem; +} + +.repo-card h3 { + margin: 0; + font-size: 1.05rem; } -#cookie-button:hover { - transform: scale(1.05); - box-shadow: 0 6px 12px rgba(0,0,0,0.4); +.repo-card a { + color: inherit; + text-decoration: none; } -#cookie-button:active { - transform: scale(0.95); +.repo-meta { + color: var(--ink-soft); + font-size: 0.85rem; } -/* Cherry blossom petals */ -.petal { - position: absolute; - top: -20px; - width: 20px; - height: 20px; - background: radial-gradient(circle, #ffb6c1, #ff69b4); - border-radius: 50%; - opacity: 0.8; - pointer-events: none; - animation: fall linear forwards; +.small { + font-size: 0.88rem; + color: var(--ink-soft); } -@keyframes fall { - to { - transform: translateY(100vh) rotate(360deg); - } +.music-controls { + display: flex; + align-items: center; + gap: 0.7rem; } -#cps { - font-weight: bold; - color: #98fb98; +.footer-card { + text-align: center; } -/* Responsive adjustments */ -@media (max-width: 480px) { - #cookie-button { - width: 90px; - height: 90px; - font-size: 1.5rem; - } - #cookie-count { - font-size: 2rem; - } +.footer-card a { + color: var(--sakura-deep); + font-weight: 700; } -/* Responsive tweaks */ -@media (max-width: 768px) { - header h1 { - font-size: 2.5rem; +@media (max-width: 720px) { + .hero { + flex-direction: column-reverse; + text-align: center; } - .projects-grid { - grid-template-columns: 1fr; - } - .extras { - flex-direction: column; + + .hero-links, + .chips { + justify-content: center; } -} \ No newline at end of file +} diff --git a/static/index.html b/static/index.html index 64e7ea8..241cf09 100644 --- a/static/index.html +++ b/static/index.html @@ -3,60 +3,77 @@ - Vlo Studios – Developer Portfolio + Vlo Studios | Sakura CV + + + - - - - - + -
-
-

Vlo Studios

-

Python enthusiast • Ice‑tea aficionado • Lo‑fi beats

+
+
+
+

Sakura-themed portfolio

+

VloStudios

+

Brewing code like iced tea, one calm commit at a time.

+
+ 🌸 Sakura Vibes + 🧋 Iced Tea Energy + 🎧 Background Music +
+ +
+ VloStudios avatar
-
-

Projects

-
- +
+

CV Snapshot

+
+

Followers

-

+

Following

-

+

Public Repos

-

+

Location

Internet

+

Loading latest profile updates…

-
-
- - - - Stopped +
+

Iced Tea Break

+

+ Favorite workflow: chill lo-fi, clean commits, and a glass of iced tea while building cool things. +

+
Today's blend: Peach Green Tea + Cherry Blossom Mood ✨
+
+ +
+

Selected GitHub Projects

+
+

Loading repositories…

+
- +
- - - - \ No newline at end of file + diff --git a/static/js/cookieclicker.js b/static/js/cookieclicker.js deleted file mode 100644 index 4d10b1a..0000000 --- a/static/js/cookieclicker.js +++ /dev/null @@ -1,90 +0,0 @@ -// Cookie Clicker with Japanese cherry vibes -document.addEventListener('DOMContentLoaded', () => { - const cookieCountEl = document.getElementById('cookie-count'); - const cookieButton = document.getElementById('cookie-button'); - const cpsEl = document.getElementById('cps'); - const cookieDisplay = document.getElementById('cookie-display'); - const container = document.querySelector('.cookie-clicker-container'); - - let cookies = 0; - let cps = 0; - let clickValue = 1; - - // Auto-clicker upgrade - let autoClickerCost = 10; - let autoClickerCount = 0; - const autoClickerBtn = document.createElement('button'); - autoClickerBtn.id = 'auto-clicker-btn'; - autoClickerBtn.textContent = `Buy Auto-clicker (${autoClickerCost})`; - autoClickerBtn.style.display = 'block'; - autoClickerBtn.style.margin = '10px auto'; - autoClickerBtn.style.padding = '8px 12px'; - autoClickerBtn.style.background = '#98fb98'; - autoClickerBtn.style.color = '#006400'; - autoClickerBtn.style.border = 'none'; - autoClickerBtn.style.borderRadius = '4px'; - autoClickerBtn.style.cursor = 'pointer'; - container.appendChild(autoClickerBtn); - - function updateDisplay() { - cookieCountEl.textContent = Math.floor(cookies); - cpsEl.textContent = cps.toFixed(2); - autoClickerBtn.textContent = `Buy Auto-clicker (${autoClickerCost})`; - // Disable if not enough cookies - autoClickerBtn.disabled = cookies < autoClickerCost; - } - - function createPetal() { - const petal = document.createElement('div'); - petal.className = 'petal'; - // Random size between 10-20px - const size = Math.random() * 10 + 10; - petal.style.width = `${size}px`; - petal.style.height = `${size}px`; - // Random horizontal position within button - const rect = cookieButton.getBoundingClientRect(); - const x = rect.left + Math.random() * rect.width - size/2; - const y = rect.top + Math.random() * rect.height - size/2; - petal.style.left = `${x}px`; - petal.style.top = `${y}px`; - // Random fall duration 2-4 seconds - const duration = Math.random() * 2 + 2; - petal.style.animationDuration = `${duration}s`; - // Random rotation - petal.style.transform = `rotate(${Math.random() * 360}deg`; - document.body.appendChild(petal); - // Remove after animation ends - petal.addEventListener('animationend', () => { - petal.remove(); - }); - } - - cookieButton.addEventListener('click', () => { - cookies += clickValue; - // Create a few petals on click - for (let i = 0; i < 3; i++) { - createPetal(); - } - updateDisplay(); - }); - - autoClickerBtn.addEventListener('click', () => { - if (cookies >= autoClickerCost) { - cookies -= autoClickerCost; - autoClickerCount++; - cps += 0.1; // each auto-clicker adds 0.1 cookies per second - // Increase cost for next - autoClickerCost = Math.floor(autoClickerCost * 1.15); - updateDisplay(); - } - }); - - // Auto-collect loop - setInterval(() => { - cookies += cps; - updateDisplay(); - }, 1000); - - // Initial display - updateDisplay(); -}); \ No newline at end of file diff --git a/static/js/lofi.js b/static/js/lofi.js deleted file mode 100644 index 27f05a1..0000000 --- a/static/js/lofi.js +++ /dev/null @@ -1,62 +0,0 @@ -// Lo-fi player -document.addEventListener('DOMContentLoaded', () => { - const select = document.getElementById('lofi-select'); - const toggleBtn = document.getElementById('lofi-toggle'); - const statusSpan = document.getElementById('lofi-status'); - let audio = null; - let isPlaying = false; - - // Sample lo-fi tracks (free MP3 from Pixabay or similar) - const tracks = [ - { name: 'Chillhop Essentials - Summer', url: 'https://cdn.pixabay.com/download/audio/2022/03/07/audio_6f5f2a2e3b.mp3?filename=summer-breeze-12047.mp3' }, - { name: 'Lo-fi Study Beats', url: 'https://cdn.pixabay.com/download/audio/2022/03/07/audio_5e5b5a2e3b.mp3?filename=lofi-study-beats-12046.mp3' }, - { name: 'Jazz Hop', url: 'https://cdn.pixabay.com/download/audio/2022/03/07/audio_3c5b5a2e3b.mp3?filename=jazz-hop-12045.mp3' }, - { name: 'Rainy Day Vibes', url: 'https://cdn.pixabay.com/download/audio/2022/03/07/audio_1a5b5a2e3b.mp3?filename=rainy-day-vibes-12044.mp3' } - ]; - - // Populate select - tracks.forEach(track => { - const option = document.createElement('option'); - option.value = track.url; - option.textContent = track.name; - select.appendChild(option); - }); - - function updateStatus() { - statusSpan.textContent = isPlaying ? 'Playing' : 'Stopped'; - toggleBtn.innerHTML = isPlaying ? '⏸️ Pause' : '▶️ Play'; - } - - toggleBtn.addEventListener('click', () => { - if (isPlaying) { - audio.pause(); - isPlaying = false; - } else { - const url = select.value; - if (!url) { - alert('Please select a track first'); - return; - } - if (audio) { - audio.pause(); - } - audio = new Audio(url); - audio.loop = true; - audio.play().catch(err => { - console.error('Audio play failed:', err); - alert('Could not play audio. Maybe browser restrictions.'); - }); - isPlaying = true; - } - updateStatus(); - }); - - // If user changes track while playing, restart - select.addEventListener('change', () => { - if (isPlaying) { - audio.pause(); - isPlaying = false; - updateStatus(); - } - }); -}); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 56d4bf8..562d1f8 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,60 +1,187 @@ -// Main script: fetch projects and render -document.addEventListener('DOMContentLoaded', async () => { - const projectsGrid = document.getElementById('projects-list'); +const username = "VloStudios"; +const el = { + name: document.getElementById("profile-name"), + bio: document.getElementById("profile-bio"), + avatar: document.getElementById("avatar"), + followers: document.getElementById("followers"), + following: document.getElementById("following"), + publicRepos: document.getElementById("public-repos"), + location: document.getElementById("location"), + profileUpdated: document.getElementById("profile-updated"), + repoGrid: document.getElementById("repo-grid"), + musicToggle: document.getElementById("music-toggle"), + musicStatus: document.getElementById("music-status") +}; + +let audioContext; +let masterGain; +let oscillators = []; +let lfo; + +function prettyDate(isoDate) { + if (!isoDate) return "Unknown"; + return new Date(isoDate).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric" + }); +} + +function escapeHtml(text) { + return String(text) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +async function loadProfile() { + try { + const response = await fetch(`https://api.github.com/users/${username}`); + if (!response.ok) throw new Error("Failed to load profile"); + const profile = await response.json(); + + el.name.textContent = profile.name || profile.login || username; + el.bio.textContent = profile.bio || "Builder, creator, and calm code enthusiast."; + el.avatar.src = profile.avatar_url || el.avatar.src; + el.avatar.alt = `${profile.login || username} avatar`; + el.followers.textContent = profile.followers ?? "-"; + el.following.textContent = profile.following ?? "-"; + el.publicRepos.textContent = profile.public_repos ?? "-"; + el.location.textContent = profile.location || "Online"; + el.profileUpdated.textContent = `Profile updated: ${prettyDate(profile.updated_at)}`; + } catch { + el.profileUpdated.textContent = "Could not load profile from GitHub right now."; + } +} + +function renderRepos(repos) { + const visible = repos + .filter((repo) => !repo.fork) + .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) + .slice(0, 6); + + if (visible.length === 0) { + el.repoGrid.innerHTML = '

No public repositories to show yet.

'; + return; + } + + el.repoGrid.innerHTML = visible.map((repo) => { + const name = escapeHtml(repo.name); + const description = escapeHtml(repo.description || "No description provided."); + const language = escapeHtml(repo.language || "Mixed"); + const stars = Number(repo.stargazers_count || 0); + const url = escapeHtml(repo.html_url); + const updated = prettyDate(repo.updated_at); + + return ` +
+

${name}

+

${description}

+

${language} • ★ ${stars} • Updated ${updated}

+
+ `; + }).join(""); +} + +async function loadRepos() { try { - const response = await fetch('https://api.github.com/users/VloStudios/repos?per_page=100'); + const response = await fetch(`https://api.github.com/users/${username}/repos?per_page=100`); + if (!response.ok) throw new Error("Failed to load repos"); const repos = await response.json(); + renderRepos(repos); + } catch { + el.repoGrid.innerHTML = '

Could not load repositories right now.

'; + } +} - // Filter out forks and sort by updated date - const filtered = repos.filter(repo => !repo.fork); - filtered.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); +function setMusicUI(isPlaying) { + el.musicToggle.textContent = isPlaying ? "⏸ Pause Music" : "▶ Play Music"; + el.musicStatus.textContent = isPlaying ? "Playing" : "Stopped"; +} - if (filtered.length === 0) { - projectsGrid.innerHTML = '

No projects found.

'; - return; +function stopAmbient() { + oscillators.forEach((osc) => { + try { + osc.stop(); + } catch { + // no-op } + }); + oscillators = []; - filtered.forEach(repo => { - const card = document.createElement('div'); - card.className = 'project-card'; - - const name = document.createElement('h3'); - const link = document.createElement('a'); - link.href = repo.html_url; - link.target = '_blank'; - link.rel = 'noopener'; - link.textContent = repo.name; - name.appendChild(link); - card.appendChild(name); - - if (repo.description) { - const desc = document.createElement('p'); - desc.textContent = repo.description; - card.appendChild(desc); - } + if (lfo) { + try { + lfo.stop(); + } catch { + // no-op + } + lfo.disconnect(); + lfo = null; + } - const meta = document.createElement('div'); - meta.className = 'meta'; + if (masterGain) { + masterGain.disconnect(); + masterGain = null; + } +} - const lang = document.createElement('span'); - lang.textContent = repo.language || 'Unknown'; - meta.appendChild(lang); +function startAmbient() { + audioContext = audioContext || new window.AudioContext(); + masterGain = audioContext.createGain(); + masterGain.gain.value = 0.04; + masterGain.connect(audioContext.destination); - const stars = document.createElement('span'); - stars.textContent = `⭐ ${repo.stargazers_count}`; - meta.appendChild(stars); + const notes = [220, 261.63, 329.63]; + oscillators = notes.map((frequency, index) => { + const osc = audioContext.createOscillator(); + const oscGain = audioContext.createGain(); + osc.type = index === 0 ? "sine" : "triangle"; + osc.frequency.value = frequency; + oscGain.gain.value = index === 0 ? 0.5 : 0.22; + osc.connect(oscGain); + oscGain.connect(masterGain); + osc.start(); + return osc; + }); - const updated = document.createElement('span'); - const date = new Date(repo.updated_at).toLocaleDateString(); - updated.textContent = `Updated: ${date}`; - meta.appendChild(updated); + lfo = audioContext.createOscillator(); + const lfoGain = audioContext.createGain(); + lfo.type = "sine"; + lfo.frequency.value = 0.18; + lfoGain.gain.value = 9; + lfo.connect(lfoGain); + lfoGain.connect(oscillators[1].frequency); + lfo.start(); +} - card.appendChild(meta); - projectsGrid.appendChild(card); - }); - } catch (err) { - console.error('Failed to fetch projects:', err); - projectsGrid.innerHTML = '

Error loading projects.

'; - } -}); \ No newline at end of file +function setupMusic() { + let isPlaying = false; + setMusicUI(false); + + el.musicToggle.addEventListener("click", async () => { + if (!isPlaying) { + try { + if (audioContext && audioContext.state === "suspended") { + await audioContext.resume(); + } + startAmbient(); + isPlaying = true; + setMusicUI(true); + } catch { + el.musicStatus.textContent = "Audio blocked by browser."; + } + return; + } + + stopAmbient(); + isPlaying = false; + setMusicUI(false); + }); +} + +loadProfile(); +loadRepos(); +setupMusic(); diff --git a/static/js/matrix.js b/static/js/matrix.js deleted file mode 100644 index 459c585..0000000 --- a/static/js/matrix.js +++ /dev/null @@ -1,41 +0,0 @@ -// Matrix rain effect -document.addEventListener('DOMContentLoaded', () => { - const canvas = document.getElementById('matrix'); - const ctx = canvas.getContext('2d'); - - function resize() { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - } - window.addEventListener('resize', resize); - resize(); - - const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダヂヂズデドバビブベボパピプペポ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const fontSize = 14; - const columns = Math.floor(canvas.width / fontSize); - const drops = Array.from({ length: columns }, () => 1); - - function draw() { - // Semi-transparent black background - ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - ctx.fillStyle = '#0f0'; - ctx.font = `${fontSize}px monospace`; - - for (let i = 0; i < drops.length; i++) { - const text = chars.charAt(Math.floor(Math.random() * chars.length)); - const x = i * fontSize; - const y = drops[i] * fontSize; - - ctx.fillText(text, x, y); - - if (y > canvas.height && Math.random() > 0.975) { - drops[i] = 0; - } - drops[i]++; - } - } - - setInterval(draw, 50); -}); \ No newline at end of file