guess that song added

This commit is contained in:
Michal Vrba
2026-10-01 08:03:00 +02:00
parent fdcd52a301
commit 1f2c102ff7
302 changed files with 9889 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Nenájdené</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Stránka nebola nájdená.</p>
</LayoutView>
</NotFound>
</Router>
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<WasmFingerprintAssets>false</WasmFingerprintAssets>
<FingerprintAssets>false</FingerprintAssets>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.12" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GuessTheSong.Shared\GuessTheSong.Shared.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
@inherits LayoutComponentBase
<div class="game-container">
@Body
</div>
@@ -0,0 +1,11 @@
.game-container {
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
background: radial-gradient(circle at center, #1b1534 0%, #0c081e 60%, #05030e 100%);
color: #ffffff;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
user-select: none;
overflow-x: hidden;
}
+365
View File
@@ -0,0 +1,365 @@
@page "/"
@inject GameEngineService Game
@inject AudioEngineService Audio
@inject SongService SongSvc
@implements IDisposable
<PageTitle>🎵 Guess That Song – Amber &amp; Sunset Party Edition</PageTitle>
<div class="game-wrapper" @ref="_gameContainer" tabindex="0" @onkeydown="HandleKeyDown" @onclick="HandleScreenClick">
<!-- ARCADE HEADER -->
<header class="game-header">
<div class="brand-container">
<img src="logo.svg" alt="Guess That Song Logo" class="header-logo" />
<div class="brand-title">GUESS THAT SONG</div>
</div>
@if (Game.Phase != GamePhase.Welcome && Game.Phase != GamePhase.GameOver)
{
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
<div class="round-badge">
SKLADBA #@Game.SongNumberInDeck @(Game.LoopCycle > 1 ? $"• KOLO {Game.LoopCycle}" : "")
</div>
<div class="score-badge">
🏆 UHÁDNUTÉ: @Game.PartyGuessedCount / @Game.TotalPlayedCount (@Game.PartyTotalScore B)
</div>
</div>
}
</header>
<!-- MAIN ARENA -->
<main class="game-arena">
@if (Game.Phase == GamePhase.Welcome)
{
<!-- WELCOME SCREEN -->
<div class="lobby-card" style="text-align: center; padding: 3rem 1.5rem; background: var(--card-bg); border-radius: 24px; border: 1px solid var(--card-border); box-shadow: 0 0 50px rgba(251, 133, 0, 0.25); max-width: 680px; margin: 0 auto;">
<div style="margin-bottom: 1.2rem;">
<img src="logo.svg" alt="Guess That Song Logo" style="width: 120px; height: 120px; animation: pulseGlow 2.2s infinite alternate;" />
</div>
<h1 style="font-family: 'Righteous', cursive; font-size: 3.2rem; background: linear-gradient(90deg, #ffd166, #ffb703, #fb8500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.3rem; letter-spacing: 2px;">
GUESS THAT SONG
</h1>
<div style="color: var(--neon-gold); font-weight: 800; font-size: 1.1rem; letter-spacing: 4px; text-transform: uppercase; margin-bottom: 2rem;">
AMBER &amp; SUNSET PARTY EDITION
</div>
<div style="background: rgba(0,0,0,0.5); padding: 0.9rem 1.8rem; border-radius: 14px; max-width: 480px; margin: 0 auto 2.2rem; border: 1px solid rgba(255, 183, 3, 0.35); display: flex; justify-content: center; gap: 1.2rem; flex-wrap: wrap; font-size: 0.95rem;">
<span><strong style="color: #fff; background: rgba(251, 133, 0, 0.25); padding: 0.2rem 0.6rem; border-radius: 6px; border: 1px solid rgba(251, 133, 0, 0.5);">SPACE</strong> = Nápoveda</span>
<span><strong style="color: #fff; background: rgba(0, 255, 136, 0.25); padding: 0.2rem 0.6rem; border-radius: 6px; border: 1px solid rgba(0, 255, 136, 0.5);">ENTER</strong> = Uhádli sme!</span>
</div>
<div style="display: flex; gap: 1rem; justify-content: center; align-items: center; flex-wrap: wrap;">
<button class="btn-neon" style="font-size: 1.25rem; padding: 1rem 3rem;" @onclick:stopPropagation="true" @onclick="HandleStartClickAsync">
▶ ŠTART (SPACE)
</button>
<button class="btn-intro-toggle" @onclick:stopPropagation="true" @onclick="ToggleIntroMusicAsync">
@(_isIntroPlaying ? "⏸️ PAUZA INTRO" : "🎵 INTRO THEME")
</button>
</div>
</div>
}
else
{
<!-- AUDIO VISUALIZER & LOOP BAR -->
<div class="visualizer-box">
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%; font-size: 0.85rem; color: var(--neon-gold);">
<div style="display: flex; align-items: center; gap: 0.6rem;">
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: @(Game.Phase == GamePhase.PlayingClue ? "var(--neon-green)" : "var(--neon-amber)"); animation: bounce 0.6s infinite alternate;"></span>
<span style="font-weight: 800; text-transform: uppercase; letter-spacing: 1px;">
@(Game.Phase == GamePhase.PlayingClue ? $"🔁 Hrá ukážka ({Game.CurrentClue?.AudioSnippetSec.ToString("0.0")}s v slučke)" : "⏸️ Zvuk pozastavený – tipovanie")
</span>
</div>
<button class="btn-replay-small" @onclick:stopPropagation="true" @onclick="() => Game.ReplayCurrentAudioAsync()">
🔁 Prehrať ukážku znova (R)
</button>
</div>
<canvas id="audioVisualizer" class="visualizer-canvas"></canvas>
</div>
<!-- REVEAL SCREEN -->
@if (Game.Phase == GamePhase.Revealed)
{
<div class="reveal-card">
<div style="text-transform: uppercase; font-size: 0.95rem; letter-spacing: 2.5px; color: var(--neon-gold); font-weight: 900; margin-bottom: 0.5rem;">
🎉 SPRÁVNA ODPOVEĎ
</div>
<div style="display: flex; gap: 2.5rem; align-items: center; justify-content: center; flex-wrap: wrap; margin: 1.5rem 0;">
@if (!string.IsNullOrEmpty(Game.CurrentSong?.CoverImage))
{
<div class="reveal-cover-wrapper">
<img src="@Game.CurrentSong.CoverImage" alt="Album Cover" class="reveal-cover-img" />
</div>
}
<div style="text-align: left; max-width: 500px;">
<div class="reveal-song-title">@Game.CurrentSong?.Title</div>
<div class="reveal-artist">Interpret: @Game.CurrentSong?.Artist</div>
@if (!string.IsNullOrEmpty(Game.CurrentSong?.Album))
{
<div style="color: var(--neon-gold); font-size: 1.15rem; font-weight: 600; margin-bottom: 0.8rem;">
💿 Album: @Game.CurrentSong.Album
</div>
}
<div>
<button class="btn-neon" style="font-size: 0.95rem; padding: 0.6rem 1.6rem;" @onclick:stopPropagation="true" @onclick="() => Audio.PlayFullSongAsync(Game.CurrentSong?.AudioFile ?? string.Empty)">
▶ Prehrať celú skladbu
</button>
</div>
</div>
</div>
<!-- SHOCKING TRIVIA & STORY -->
@if (!string.IsNullOrEmpty(Game.CurrentSong?.ShockingTrivia))
{
<div class="shocking-trivia-box">
<div class="shocking-trivia-header">
<span>🔥 ŠOKUJÚCA PIKOŠKA ZO ZÁKULISIA:</span>
</div>
<div class="shocking-trivia-content">
@Game.CurrentSong.ShockingTrivia
</div>
</div>
}
@if (!string.IsNullOrEmpty(Game.CurrentSong?.StorySummary))
{
<div class="story-summary-box">
<div style="font-size: 0.85rem; text-transform: uppercase; color: var(--neon-gold); font-weight: 800; margin-bottom: 0.4rem; letter-spacing: 1px;">
📖 PRÍBEH A KONTEXT SKLADBY:
</div>
<div style="color: #e2e8f0; font-size: 1.05rem; line-height: 1.6;">
@Game.CurrentSong.StorySummary
</div>
</div>
}
<!-- DETAILS GRID -->
<div class="reveal-details-grid">
<div class="reveal-detail-item">
<div class="reveal-detail-label">Rok vydania</div>
<div class="reveal-detail-value">@(Game.CurrentSong?.Year > 0 ? Game.CurrentSong.Year.ToString() : "Neznámy")</div>
</div>
<div class="reveal-detail-item">
<div class="reveal-detail-label">Žáner</div>
<div class="reveal-detail-value">@Game.CurrentSong?.Genre</div>
</div>
<div class="reveal-detail-item">
<div class="reveal-detail-label">Album</div>
<div class="reveal-detail-value">@(string.IsNullOrEmpty(Game.CurrentSong?.Album) ? "Singel" : Game.CurrentSong.Album)</div>
</div>
<div class="reveal-detail-item">
<div class="reveal-detail-label">Súbor</div>
<div class="reveal-detail-value" style="font-size: 0.85rem; color: #94a3b8; word-break: break-all;">
@Game.CurrentSong?.AudioFile
</div>
</div>
</div>
</div>
}
else
{
<!-- STAGE CLUES LIST -->
<div class="clues-container">
@if (Game.CurrentSong?.Clues is { Count: > 0 } clues)
{
@for (int i = 0; i < clues.Count; i++)
{
var clue = clues[i];
bool isUnlocked = (i <= Game.CurrentStageIndex);
bool isCurrent = (i == Game.CurrentStageIndex);
<div class="clue-card @(isUnlocked ? "unlocked" : "") @(isCurrent ? "current" : "")">
<div class="clue-header">
<span class="clue-badge">@clue.HintType</span>
<span class="clue-points">@clue.Points @(clue.Points >= 5 ? "BODOV" : "BODY")</span>
</div>
<div class="clue-body">
@if (isUnlocked)
{
<p style="font-weight: 500; white-space: pre-line;">@clue.Question</p>
@if (clue.Stage >= 2 && !string.IsNullOrEmpty(Game.CurrentSong?.CoverImage))
{
<div class="clue-cover-hint-box">
<div class="clue-cover-crop clue-blur-stage-@clue.Stage">
<img src="@Game.CurrentSong.CoverImage" alt="Cover Hint" />
</div>
<div style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.4rem;">
🖼️ Výrez z obalu @(clue.Stage == 2 ? "(Jemne rozostrený)" : clue.Stage == 3 ? "(Zreteľný náhľad)" : "(Kompletný obal)")
</div>
</div>
}
@if (!string.IsNullOrEmpty(clue.MaskedTitle))
{
<div class="masked-title-box">
@clue.MaskedTitle
</div>
}
}
else
{
<p style="color: var(--text-muted); font-style: italic;">
🔒 Indícia @(i + 1) uzamknutá (odhalí sa stlačením MEDZERNÍKA)...
</p>
}
</div>
</div>
}
}
</div>
}
}
</main>
<!-- ACTION FOOTER: DUAL DOCK DURING CLUES, UNIVERSAL ON REVEAL/WELCOME -->
@if (Game.Phase == GamePhase.PlayingClue || Game.Phase == GamePhase.Discussing)
{
<footer class="party-action-dock">
<button class="dock-btn dock-btn-clue" @onclick:stopPropagation="true" @onclick="() => Game.HandleSpacebarActionAsync()">
<div class="dock-key-tag">⎵ MEDZERNÍK</div>
<div class="dock-btn-text">
@(Game.CurrentStageIndex < 3 ? $"❌ NEUHÁDLI SME (ĎALŠIA NÁPOVEDA {Game.CurrentStageIndex + 2}/4)" : "❌ NEUHÁDLI SME (ODHALIŤ SKLADBU)")
</div>
</button>
<button class="dock-btn dock-btn-win" @onclick:stopPropagation="true" @onclick="() => Game.HandleEnterActionAsync()">
<div class="dock-key-tag key-enter">↵ ENTER</div>
<div class="dock-btn-text">
✅ UHÁDLI SME! (+@Game.CurrentPoints b.)
</div>
</button>
</footer>
}
else
{
<footer class="space-action-bar" @onclick:stopPropagation="true" @onclick="() => Game.HandleSpacebarActionAsync()">
<div class="space-key-pill">
<span>⎵</span> MEDZERNÍK / ↵ ENTER
</div>
<div class="space-action-text">
@GetSpacebarActionLabel()
</div>
</footer>
}
</div>
@code {
private ElementReference _gameContainer;
private bool _isIntroPlaying = false;
private async Task ToggleIntroMusicAsync()
{
if (_isIntroPlaying)
{
await Audio.StopAudioAsync();
_isIntroPlaying = false;
}
else
{
await Audio.InitAudioAsync();
await Audio.PlayIntroThemeSongAsync(loop: true);
_isIntroPlaying = true;
}
}
private async Task HandleStartClickAsync()
{
try
{
_isIntroPlaying = false;
await Game.StartGameAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error starting game: {ex}");
}
}
protected override async Task OnInitializedAsync()
{
Game.OnStateChanged += OnGameStateChanged;
await Game.InitializeLobbyAsync();
}
private void OnGameStateChanged()
{
_ = InvokeAsync(StateHasChanged);
}
private bool _visualizerStarted = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
await _gameContainer.FocusAsync();
}
catch { }
}
if (Game.Phase != GamePhase.Welcome && !_visualizerStarted)
{
_visualizerStarted = true;
try
{
await Audio.StartVisualizerAsync("audioVisualizer");
}
catch { }
}
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
try
{
if (e.Code == "Space" || e.Key == " ")
{
await Game.HandleSpacebarActionAsync();
}
else if (e.Code is "Enter" or "NumpadEnter" or "KeyY" || e.Key is "y" or "Y")
{
await Game.HandleEnterActionAsync();
}
else if (e.Key is "r" or "R")
{
await Game.ReplayCurrentAudioAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in HandleKeyDown: {ex}");
}
}
private async Task HandleScreenClick()
{
try
{
await _gameContainer.FocusAsync();
}
catch { }
}
private string GetSpacebarActionLabel()
{
return Game.Phase switch
{
GamePhase.Welcome => "STLAČ MEDZERNÍK / ENTER PRE ŠTART ▶",
GamePhase.PlayingClue => (Game.CurrentStageIndex < 3 ? $"MEDZERNÍK: ĎALŠIA NÁPOVEDA ({Game.CurrentStageIndex + 2}/4)" : "MEDZERNÍK: ODHALIŤ SKLADBU"),
GamePhase.Revealed => "STLAČ MEDZERNÍK / ENTER PRE ĎALŠIU SKLADBU ▶",
GamePhase.GameOver => "STLAČ MEDZERNÍK / ENTER PRE NOVÚ HRU",
_ => "STLAČ MEDZERNÍK PRE POKRAČOVANIE"
};
}
public void Dispose()
{
Game.OnStateChanged -= OnGameStateChanged;
}
}
+15
View File
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using GuessTheSong.Client;
using GuessTheSong.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<AudioEngineService>();
builder.Services.AddScoped<SongService>();
builder.Services.AddScoped<GameEngineService>();
await builder.Build().RunAsync();
@@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"GuessTheSong.Client": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7150;http://localhost:5150",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,149 @@
using Microsoft.JSInterop;
namespace GuessTheSong.Client.Services;
public class AudioEngineService : IAsyncDisposable
{
private readonly IJSRuntime _js;
private IJSObjectReference? _module;
public AudioEngineService(IJSRuntime js)
{
_js = js;
}
private async ValueTask<IJSObjectReference?> GetModuleAsync()
{
try
{
return _module ??= await _js.InvokeAsync<IJSObjectReference>("import", "./js/audioPlayer.js");
}
catch (Exception ex)
{
Console.WriteLine($"Error importing audioPlayer.js: {ex.Message}");
return null;
}
}
public async Task InitAudioAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("initAudio");
}
catch (Exception ex) { Console.WriteLine($"InitAudioAsync error: {ex.Message}"); }
}
public async Task PlaySnippetAsync(string audioUrl, double durationSec = 1.5, double? customStartSec = null, bool autoLoop = true)
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playSnippet", audioUrl, durationSec, customStartSec, autoLoop);
}
catch (Exception ex) { Console.WriteLine($"PlaySnippetAsync error: {ex.Message}"); }
}
public async Task ReplaySnippetAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("replayLastSnippet");
}
catch (Exception ex) { Console.WriteLine($"ReplaySnippetAsync error: {ex.Message}"); }
}
public async Task PlayFullSongAsync(string audioUrl)
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playFullSong", audioUrl);
}
catch (Exception ex) { Console.WriteLine($"PlayFullSongAsync error: {ex.Message}"); }
}
public async Task StopAudioAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("stopAudio");
}
catch (Exception ex) { Console.WriteLine($"StopAudioAsync error: {ex.Message}"); }
}
public async Task PlayIntroThemeSongAsync(bool loop = true)
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playIntroThemeSong", loop);
}
catch (Exception ex) { Console.WriteLine($"PlayIntroThemeSongAsync error: {ex.Message}"); }
}
public async Task PlayArcadeIntroSoundAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playArcadeIntroSound");
}
catch (Exception ex) { Console.WriteLine($"PlayArcadeIntroSoundAsync error: {ex.Message}"); }
}
public async Task PlayBuzzerSoundAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playBuzzerSound");
}
catch (Exception ex) { Console.WriteLine($"PlayBuzzerSoundAsync error: {ex.Message}"); }
}
public async Task PlayNextClueSoundAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playNextClueSound");
}
catch (Exception ex) { Console.WriteLine($"PlayNextClueSoundAsync error: {ex.Message}"); }
}
public async Task PlayScoreWinSoundAsync()
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("playScoreWinSound");
}
catch (Exception ex) { Console.WriteLine($"PlayScoreWinSoundAsync error: {ex.Message}"); }
}
public async Task StartVisualizerAsync(string canvasId)
{
try
{
var module = await GetModuleAsync();
if (module != null) await module.InvokeVoidAsync("startVisualizer", canvasId);
}
catch (Exception ex) { Console.WriteLine($"StartVisualizerAsync error: {ex.Message}"); }
}
public async ValueTask DisposeAsync()
{
if (_module is not null)
{
try
{
await _module.DisposeAsync();
}
catch { }
}
}
}
@@ -0,0 +1,296 @@
using GuessTheSong.Shared.Models;
namespace GuessTheSong.Client.Services;
public class GameEngineService
{
private readonly AudioEngineService _audio;
private readonly SongService _songService;
public event Action? OnStateChanged;
public GamePhase Phase { get; private set; } = GamePhase.Welcome;
public List<SongModel> PlayList { get; private set; } = new();
public int CurrentSongIndex { get; private set; } = 0;
public SongModel? CurrentSong => (CurrentSongIndex >= 0 && CurrentSongIndex < PlayList.Count) ? PlayList[CurrentSongIndex] : null;
public int CurrentStageIndex { get; private set; } = 0; // 0..3 (Stage 1..4)
public ClueModel? CurrentClue => (CurrentSong?.Clues is { Count: > 0 } clues && CurrentStageIndex < clues.Count)
? clues[CurrentStageIndex]
: null;
public int CurrentPoints => CurrentClue?.Points ?? 100;
public int TotalSongsInDeck => PlayList.Count > 0 ? PlayList.Count : _songService.SongCount;
public int SongNumberInDeck => CurrentSongIndex + 1;
public int TotalPlayedCount { get; private set; } = 0;
public int LoopCycle { get; private set; } = 1;
// Party Score Statistics
public int PartyGuessedCount { get; private set; } = 0;
public int PartyTotalScore { get; private set; } = 0;
public GameEngineService(AudioEngineService audio, SongService songService)
{
_audio = audio;
_songService = songService;
}
public async Task InitializeLobbyAsync()
{
try
{
var allSongs = await _songService.GetSongsAsync();
if (PlayList.Count == 0 && allSongs.Count > 0)
{
var rand = new Random();
PlayList = allSongs.OrderBy(_ => rand.Next()).ToList();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in InitializeLobbyAsync: {ex}");
}
Notify();
}
public async Task StartGameAsync()
{
try
{
await _audio.InitAudioAsync();
await _audio.PlayArcadeIntroSoundAsync();
var allSongs = await _songService.GetSongsAsync();
// Fisher-Yates True Random Shuffle (bez opakovania)
var rand = new Random();
PlayList = allSongs.OrderBy(_ => rand.Next()).ToList();
CurrentSongIndex = 0;
TotalPlayedCount = 0;
PartyGuessedCount = 0;
PartyTotalScore = 0;
LoopCycle = 1;
if (PlayList.Count > 0)
{
// Plynulý prechod po doznení úvodnej znelky
await Task.Delay(850);
await StartSongRoundAsync();
}
else
{
Phase = GamePhase.Welcome;
Notify();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in StartGameAsync: {ex}");
}
}
public async Task HandleSpacebarActionAsync()
{
try
{
await _audio.InitAudioAsync();
switch (Phase)
{
case GamePhase.Welcome:
case GamePhase.GameOver:
await StartGameAsync();
break;
case GamePhase.PlayingClue:
case GamePhase.Discussing:
// SPACE = Neuhádli sme -> Chceme ďalšiu nápovedu (alebo odhalenie po 4. indícii)
await AdvanceToNextClueOrRevealAsync();
break;
case GamePhase.Revealed:
// SPACE = Posun na ďalšiu náhodnú pesničku
await NextSongAsync();
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in HandleSpacebarActionAsync: {ex}");
}
}
public async Task HandleEnterActionAsync()
{
try
{
await _audio.InitAudioAsync();
switch (Phase)
{
case GamePhase.Welcome:
case GamePhase.GameOver:
await StartGameAsync();
break;
case GamePhase.PlayingClue:
case GamePhase.Discussing:
// ENTER = Uhádli sme v izbe!
await ConfirmGuessedSuccessAsync();
break;
case GamePhase.Revealed:
// ENTER = Posun na ďalšiu pesničku
await NextSongAsync();
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in HandleEnterActionAsync: {ex}");
}
}
private async Task StartSongRoundAsync()
{
try
{
CurrentStageIndex = 0;
Phase = GamePhase.PlayingClue;
TotalPlayedCount++;
Notify();
if (CurrentSong != null && CurrentClue != null)
{
await _audio.PlaySnippetAsync(CurrentSong.AudioFile, CurrentClue.AudioSnippetSec, CurrentClue.CustomAudioStartSec, autoLoop: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in StartSongRoundAsync: {ex}");
}
}
public async Task PauseForDiscussionAsync()
{
try
{
Phase = GamePhase.Discussing;
await _audio.StopAudioAsync();
await _audio.PlayBuzzerSoundAsync();
Notify();
}
catch (Exception ex)
{
Console.WriteLine($"Error in PauseForDiscussionAsync: {ex}");
}
}
public async Task ConfirmGuessedSuccessAsync()
{
try
{
PartyGuessedCount++;
PartyTotalScore += CurrentPoints;
await _audio.PlayScoreWinSoundAsync();
await RevealSongAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error in ConfirmGuessedSuccessAsync: {ex}");
}
}
public async Task AdvanceToNextClueOrRevealAsync()
{
try
{
if (CurrentSong == null) return;
if (CurrentStageIndex < CurrentSong.Clues.Count - 1)
{
// Posun na ďalšiu indíciu
CurrentStageIndex++;
Phase = GamePhase.PlayingClue;
await _audio.PlayNextClueSoundAsync();
Notify();
if (CurrentClue != null)
{
await _audio.PlaySnippetAsync(CurrentSong.AudioFile, CurrentClue.AudioSnippetSec, CurrentClue.CustomAudioStartSec, autoLoop: true);
}
}
else
{
// Vyčerpané všetky 4 indície -> Odhalenie
await RevealSongAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in AdvanceToNextClueOrRevealAsync: {ex}");
}
}
public async Task ReplayCurrentAudioAsync()
{
try
{
if (CurrentSong != null && CurrentClue != null)
{
await _audio.PlaySnippetAsync(CurrentSong.AudioFile, CurrentClue.AudioSnippetSec, CurrentClue.CustomAudioStartSec, autoLoop: true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in ReplayCurrentAudioAsync: {ex}");
}
}
private async Task RevealSongAsync()
{
try
{
Phase = GamePhase.Revealed;
Notify();
if (CurrentSong != null)
{
await _audio.PlayFullSongAsync(CurrentSong.AudioFile);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error in RevealSongAsync: {ex}");
}
}
public async Task NextSongAsync()
{
try
{
await _audio.StopAudioAsync();
CurrentSongIndex++;
// NEVER-ENDING RANDOM LOOP
if (CurrentSongIndex >= PlayList.Count)
{
var rand = new Random();
PlayList = PlayList.OrderBy(_ => rand.Next()).ToList();
CurrentSongIndex = 0;
LoopCycle++;
}
await StartSongRoundAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error in NextSongAsync: {ex}");
}
}
private void Notify()
{
OnStateChanged?.Invoke();
}
}
@@ -0,0 +1,91 @@
using System.Net.Http.Json;
using GuessTheSong.Shared.Helpers;
using GuessTheSong.Shared.Models;
namespace GuessTheSong.Client.Services;
public class SongService
{
private readonly HttpClient _http;
private List<SongModel> _songs = new();
private bool _isLoaded;
public SongService(HttpClient http)
{
_http = http;
}
public int SongCount => _songs.Count;
public async Task<List<SongModel>> GetSongsAsync()
{
if (_isLoaded && _songs.Count > 0)
{
return _songs;
}
try
{
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
// Read string first to strip any BOM if present
var jsonString = await _http.GetStringAsync("music/songs.json");
if (!string.IsNullOrEmpty(jsonString))
{
// Strip UTF-8 BOM if present
if (jsonString.StartsWith("\uFEFF", StringComparison.Ordinal))
{
jsonString = jsonString.Substring(1);
}
var loaded = System.Text.Json.JsonSerializer.Deserialize<List<SongModel>>(jsonString, options);
if (loaded is { Count: > 0 })
{
foreach (var song in loaded)
{
var (parsedArtist, parsedTitle) = SongTitleHelper.ParseArtistAndTitle(song.AudioFile, song.Artist);
if (string.IsNullOrWhiteSpace(song.Artist) && !string.IsNullOrWhiteSpace(parsedArtist))
{
song.Artist = parsedArtist;
}
if (string.IsNullOrWhiteSpace(song.Title))
{
song.Title = parsedTitle;
}
else
{
song.Title = SongTitleHelper.CleanSongTitle(song.Title, song.Artist);
}
// Auto-generate masked title if missing in Stage 4
var lastClue = song.Clues.LastOrDefault();
if (lastClue != null && string.IsNullOrWhiteSpace(lastClue.MaskedTitle) && !string.IsNullOrWhiteSpace(song.Title))
{
lastClue.MaskedTitle = SongTitleHelper.GenerateMaskedTitle(song.Title);
}
}
_songs = loaded;
_isLoaded = true;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading songs.json: {ex}");
}
return _songs;
}
public void SetCustomSongs(List<SongModel> songs)
{
_songs = songs;
_isLoaded = true;
}
}
+12
View File
@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using GuessTheSong.Client
@using GuessTheSong.Client.Layout
@using GuessTheSong.Client.Services
@using GuessTheSong.Shared.Models
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

@@ -0,0 +1,808 @@
/* Game.css - Guess That Song Amber Gold & Sunset Orange Concert Lounge Theme */
:root {
--bg-dark: #0a0806;
--card-bg: rgba(22, 16, 11, 0.88);
--card-border: rgba(255, 183, 3, 0.18);
--neon-gold: #ffd166;
--neon-amber: #ffb703;
--neon-orange: #fb8500;
--neon-fiery: #ff5400;
--neon-yellow: #ffe600;
--neon-green: #00ff88;
--neon-cyan: #00e5ff;
--text-main: #ffffff;
--text-muted: #d4a373;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: 'Outfit', sans-serif;
min-height: 100vh;
overflow-x: hidden;
background-image:
radial-gradient(circle at 50% 30%, rgba(18, 12, 7, 0.5) 0%, rgba(8, 5, 3, 0.92) 100%),
url('../bg.jpg');
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
background-attachment: fixed;
}
/* Loading screen */
.loading-screen {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 50% 40%, rgba(36, 25, 14, 0.95) 0%, rgba(10, 8, 6, 0.98) 100%), url('../bg.jpg');
background-size: cover;
background-position: center;
font-family: 'Outfit', sans-serif;
padding: 1.5rem;
z-index: 9999;
}
.loading-box {
background: rgba(22, 16, 11, 0.88);
border: 1px solid rgba(255, 183, 3, 0.35);
border-radius: 24px;
padding: 2.5rem 2.5rem;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.8), 0 0 35px rgba(251, 133, 0, 0.25);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
display: flex;
flex-direction: column;
align-items: center;
animation: fadeInScale 0.6s ease-out forwards;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.94);
}
to {
opacity: 1;
transform: scale(1);
}
}
.loading-logo {
width: 90px;
height: 90px;
margin-bottom: 1.2rem;
animation: pulseGlow 1.8s infinite alternate ease-in-out;
}
.loading-title {
font-family: 'Righteous', cursive;
font-size: 2rem;
color: var(--neon-gold);
letter-spacing: 2px;
margin-bottom: 0.3rem;
text-shadow: 0 0 20px rgba(255, 183, 3, 0.6);
}
.loading-subtitle {
font-size: 0.82rem;
font-weight: 700;
color: var(--neon-orange);
letter-spacing: 3px;
margin-bottom: 2rem;
text-transform: uppercase;
}
.loading-progress-container {
width: 100%;
height: 14px;
background: rgba(10, 8, 6, 0.85);
border: 1px solid rgba(255, 183, 3, 0.3);
border-radius: 12px;
overflow: hidden;
position: relative;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8);
margin-bottom: 1rem;
}
.loading-progress-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #ff5400 0%, #fb8500 40%, #ffb703 80%, #ffd166 100%);
border-radius: 12px;
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
box-shadow: 0 0 16px rgba(251, 133, 0, 0.9), 0 0 25px rgba(255, 183, 3, 0.5);
}
.loading-progress-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.35) 50%,
rgba(255, 255, 255, 0) 100%
);
background-size: 200% 100%;
animation: shimmerProgress 1.5s infinite linear;
}
@keyframes shimmerProgress {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.loading-status-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.88rem;
color: var(--text-muted);
}
.loading-status-text {
color: #e0d0c0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 78%;
text-align: left;
}
.loading-percentage-text {
font-weight: 700;
color: var(--neon-gold);
text-shadow: 0 0 8px rgba(255, 209, 102, 0.5);
}
#blazor-error-ui {
display: none;
background: #1c1308;
color: #ffd166;
bottom: 0;
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.7);
left: 0;
padding: 0.8rem 1.5rem;
position: fixed;
width: 100%;
z-index: 10000;
border-top: 1px solid var(--neon-orange);
text-align: center;
font-family: 'Outfit', sans-serif;
font-weight: 600;
}
#blazor-error-ui .reload {
color: #ff5400;
margin-left: 1rem;
font-weight: 800;
text-decoration: underline;
}
@keyframes pulseGlow {
from {
transform: scale(0.98);
filter: drop-shadow(0 0 15px rgba(251, 133, 0, 0.5)) drop-shadow(0 0 25px rgba(255, 209, 102, 0.4));
}
to {
transform: scale(1.04);
filter: drop-shadow(0 0 30px rgba(251, 133, 0, 0.85)) drop-shadow(0 0 50px rgba(255, 183, 3, 0.7));
}
}
/* Header */
.game-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2.5rem;
background: rgba(16, 11, 7, 0.86);
backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(255, 183, 3, 0.18);
}
.brand-container {
display: flex;
align-items: center;
gap: 0.85rem;
text-decoration: none;
cursor: default;
}
.header-logo {
width: 44px;
height: 44px;
filter: drop-shadow(0 0 10px rgba(251, 133, 0, 0.6));
transition: transform 0.3s ease;
}
.header-logo:hover {
transform: rotate(15deg) scale(1.1);
}
.brand-title {
font-family: 'Righteous', cursive;
font-size: 2rem;
background: linear-gradient(90deg, #ffd166, #ffb703, #fb8500);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 2px;
text-shadow: 0 0 25px rgba(251, 133, 0, 0.5);
}
.round-badge {
background: rgba(255, 183, 3, 0.1);
padding: 0.5rem 1.2rem;
border-radius: 999px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.95rem;
font-weight: 700;
border: 1px solid rgba(255, 183, 3, 0.35);
color: #ffd166;
}
.score-badge {
background: rgba(0, 255, 136, 0.15);
border: 1px solid var(--neon-green);
color: var(--neon-green);
padding: 0.5rem 1.2rem;
border-radius: 999px;
font-family: 'JetBrains Mono', monospace;
font-weight: 800;
font-size: 0.95rem;
box-shadow: 0 0 15px rgba(0, 255, 136, 0.2);
}
/* Main arena layout */
.game-arena {
display: flex;
flex-direction: column;
flex: 1;
max-width: 1100px;
margin: 0 auto;
width: 100%;
padding: 1.5rem;
gap: 1.5rem;
}
/* Audio Visualizer Box */
.visualizer-box {
background: rgba(18, 13, 8, 0.88);
border: 1px solid rgba(255, 183, 3, 0.3);
border-radius: 16px;
padding: 1rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6rem;
box-shadow: 0 0 30px rgba(251, 133, 0, 0.12);
}
.visualizer-canvas {
width: 100%;
height: 75px;
}
/* Clues Container */
.clues-container {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
.clue-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 18px;
padding: 1.4rem 1.8rem;
position: relative;
opacity: 0.35;
transform: scale(0.98);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
backdrop-filter: blur(10px);
}
.clue-card.unlocked {
opacity: 1;
transform: scale(1);
border-color: rgba(255, 183, 3, 0.35);
}
.clue-card.current {
border-color: var(--neon-amber);
box-shadow: 0 0 25px rgba(251, 133, 0, 0.35);
background: linear-gradient(135deg, rgba(251, 133, 0, 0.12) 0%, rgba(22, 16, 11, 0.96) 100%);
}
.clue-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.8rem;
}
.clue-badge {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 1.2px;
font-weight: 800;
padding: 0.3rem 0.8rem;
border-radius: 8px;
background: rgba(255, 183, 3, 0.15);
border: 1px solid rgba(255, 183, 3, 0.25);
color: #ffd166;
}
.clue-points {
font-family: 'JetBrains Mono', monospace;
font-weight: 900;
font-size: 1.25rem;
color: var(--neon-yellow);
text-shadow: 0 0 10px rgba(255, 230, 0, 0.5);
}
.clue-body {
font-size: 1.2rem;
line-height: 1.6;
}
.masked-title-box {
margin-top: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 1.8rem;
font-weight: 800;
letter-spacing: 5px;
color: var(--neon-green);
background: rgba(0, 0, 0, 0.55);
padding: 0.8rem 1.4rem;
border-radius: 10px;
border: 1px solid rgba(0, 255, 136, 0.35);
display: inline-block;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.2);
}
/* Clue Cover Visual Hints */
.clue-cover-hint-box {
margin: 1.2rem 0 0.5rem;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
}
.clue-cover-crop {
width: 165px;
height: 165px;
border-radius: 16px;
overflow: hidden;
border: 2px solid rgba(255, 183, 3, 0.55);
position: relative;
background: #000;
box-shadow: 0 0 25px rgba(251, 133, 0, 0.35);
}
.clue-cover-crop img {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.5s ease;
}
.clue-blur-stage-2 img {
filter: blur(3.5px) contrast(115%);
transform: scale(1.25);
}
.clue-blur-stage-3 img {
filter: blur(1.2px);
transform: scale(1.08);
}
.clue-blur-stage-4 img {
filter: none;
transform: scale(1.0);
border-color: var(--neon-green);
}
/* Discussion / Tip Bar */
.discuss-decision-bar {
background: linear-gradient(135deg, rgba(251, 133, 0, 0.28) 0%, rgba(20, 14, 8, 0.96) 100%);
border: 2px solid var(--neon-orange);
border-radius: 20px;
padding: 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 1.2rem;
box-shadow: 0 0 35px rgba(251, 133, 0, 0.45);
animation: popIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.btn-decision-yes {
background: linear-gradient(90deg, #10b981, #059669);
border: none;
color: #fff;
font-size: 1.2rem;
font-weight: 800;
padding: 0.9rem 2.2rem;
border-radius: 14px;
cursor: pointer;
box-shadow: 0 0 25px rgba(16, 185, 129, 0.5);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-decision-yes:hover {
transform: scale(1.05);
box-shadow: 0 0 35px rgba(16, 185, 129, 0.8);
}
.btn-decision-no {
background: linear-gradient(90deg, #ef4444, #dc2626);
border: none;
color: #fff;
font-size: 1.2rem;
font-weight: 800;
padding: 0.9rem 2.2rem;
border-radius: 14px;
cursor: pointer;
box-shadow: 0 0 25px rgba(239, 68, 68, 0.5);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-decision-no:hover {
transform: scale(1.05);
box-shadow: 0 0 35px rgba(239, 68, 68, 0.8);
}
/* Reveal Card */
.reveal-card {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.12) 0%, rgba(22, 16, 11, 0.96) 100%);
border: 2px solid var(--neon-green);
border-radius: 24px;
padding: 2.2rem;
text-align: center;
box-shadow: 0 0 45px rgba(0, 255, 136, 0.25);
animation: popIn 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes popIn {
from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.reveal-cover-wrapper {
width: 200px;
height: 200px;
border-radius: 18px;
overflow: hidden;
border: 3px solid var(--neon-amber);
box-shadow: 0 0 30px rgba(251, 133, 0, 0.5);
flex-shrink: 0;
position: relative;
}
.reveal-cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.reveal-song-title {
font-family: 'Righteous', cursive;
font-size: 2.8rem;
color: var(--neon-green);
margin-bottom: 0.3rem;
letter-spacing: 1px;
}
.reveal-artist {
font-size: 1.8rem;
color: var(--neon-gold);
margin-bottom: 0.8rem;
font-weight: 700;
}
.reveal-details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.8rem;
margin: 1.5rem 0 0;
text-align: left;
}
.reveal-detail-item {
background: rgba(0, 0, 0, 0.4);
padding: 0.9rem 1.1rem;
border-radius: 12px;
border: 1px solid rgba(255, 183, 3, 0.12);
}
.reveal-detail-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
.reveal-detail-value {
font-size: 1.05rem;
font-weight: 700;
color: #fff;
margin-top: 0.2rem;
}
/* Shocking Trivia & Story Summary Box */
.shocking-trivia-box {
margin-top: 1.4rem;
background: linear-gradient(135deg, rgba(251, 133, 0, 0.22) 0%, rgba(255, 84, 0, 0.16) 100%);
border: 2px solid var(--neon-orange);
border-radius: 16px;
padding: 1.2rem 1.6rem;
text-align: left;
box-shadow: 0 0 25px rgba(251, 133, 0, 0.35);
animation: amberGlow 2.5s infinite alternate;
}
@keyframes amberGlow {
from { box-shadow: 0 0 15px rgba(251, 133, 0, 0.25); }
to { box-shadow: 0 0 35px rgba(251, 133, 0, 0.6); }
}
.shocking-trivia-header {
font-family: 'Righteous', cursive;
font-size: 1.05rem;
letter-spacing: 1.5px;
color: var(--neon-gold);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.shocking-trivia-content {
font-size: 1.15rem;
line-height: 1.6;
color: #ffffff;
font-weight: 600;
}
.story-summary-box {
margin-top: 1rem;
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 183, 3, 0.3);
border-radius: 14px;
padding: 1.1rem 1.4rem;
text-align: left;
}
/* Bottom Party Dual Action Dock (SPACE = NEXT CLUE, ENTER = GUESSED) */
.party-action-dock {
position: sticky;
bottom: 1.2rem;
display: flex;
gap: 1.2rem;
max-width: 900px;
margin: 0 auto;
width: 100%;
z-index: 100;
}
.dock-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem 1.6rem;
border-radius: 999px;
border: 2px solid;
backdrop-filter: blur(16px);
cursor: pointer;
transition: all 0.22s cubic-bezier(0.16, 1, 0.3, 1);
outline: none;
}
.dock-btn:hover {
transform: translateY(-3px) scale(1.02);
}
.dock-btn:active {
transform: translateY(1px) scale(0.99);
}
.dock-btn-clue {
background: rgba(26, 16, 8, 0.94);
border-color: var(--neon-orange);
box-shadow: 0 0 30px rgba(251, 133, 0, 0.35);
}
.dock-btn-clue:hover {
box-shadow: 0 0 45px rgba(251, 133, 0, 0.65);
border-color: #ffd166;
}
.dock-btn-win {
background: rgba(10, 30, 20, 0.94);
border-color: var(--neon-green);
box-shadow: 0 0 35px rgba(0, 255, 136, 0.4);
}
.dock-btn-win:hover {
box-shadow: 0 0 55px rgba(0, 255, 136, 0.75);
border-color: #fff;
}
.dock-key-tag {
background: #ffffff;
color: #000000;
font-family: 'JetBrains Mono', monospace;
font-weight: 900;
font-size: 1rem;
padding: 0.45rem 1rem;
border-radius: 8px;
box-shadow: 0 3px 0 #d4a373;
display: flex;
align-items: center;
gap: 0.3rem;
flex-shrink: 0;
}
.dock-key-tag.key-enter {
background: #00ff88;
color: #062b16;
box-shadow: 0 3px 0 #059669;
}
.dock-btn-text {
font-family: 'Outfit', sans-serif;
font-weight: 900;
font-size: 1.15rem;
letter-spacing: 1px;
text-transform: uppercase;
}
.dock-btn-clue .dock-btn-text {
color: #ffd166;
}
.dock-btn-win .dock-btn-text {
color: #00ff88;
}
/* Bottom Spacebar Action Footer - Ultra Radiant Arcade Prompt */
.space-action-bar {
position: sticky;
bottom: 1.2rem;
background: rgba(18, 12, 7, 0.94);
backdrop-filter: blur(16px);
border: 2px solid var(--neon-amber);
border-radius: 999px;
padding: 1.1rem 2.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
box-shadow: 0 0 35px rgba(251, 133, 0, 0.4);
cursor: pointer;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
max-width: 750px;
margin: 0 auto;
width: 100%;
}
.space-action-bar:hover, .space-action-bar:active {
transform: scale(1.03);
box-shadow: 0 0 50px rgba(251, 133, 0, 0.75);
border-color: #ffd166;
}
.space-key-pill {
background: #ffffff;
color: #000000;
font-family: 'JetBrains Mono', monospace;
font-weight: 900;
font-size: 1.1rem;
padding: 0.5rem 1.4rem;
border-radius: 10px;
box-shadow: 0 4px 0 #d4a373;
display: flex;
align-items: center;
gap: 0.5rem;
}
.space-action-text {
font-family: 'Outfit', sans-serif;
font-weight: 900;
font-size: 1.35rem;
letter-spacing: 1.5px;
text-transform: uppercase;
color: #ffd166;
}
.btn-replay-small {
background: rgba(255, 183, 3, 0.15);
border: 1px solid var(--neon-amber);
color: #ffd166;
padding: 0.4rem 1rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.btn-replay-small:hover {
background: rgba(255, 183, 3, 0.35);
transform: scale(1.05);
}
.btn-intro-toggle {
background: rgba(255, 183, 3, 0.12);
border: 1px solid var(--neon-amber);
color: var(--neon-gold);
padding: 1.1rem 2.2rem;
border-radius: 12px;
font-family: 'Outfit', sans-serif;
font-size: 1.15rem;
font-weight: 800;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 0 15px rgba(251, 133, 0, 0.25);
}
.btn-intro-toggle:hover {
background: rgba(255, 183, 3, 0.25);
color: #fff;
transform: translateY(-2px);
box-shadow: 0 0 25px rgba(251, 133, 0, 0.55);
}
.btn-neon {
background: linear-gradient(90deg, var(--neon-orange), var(--neon-fiery));
border: none;
color: #fff;
padding: 0.9rem 2.2rem;
font-size: 1.15rem;
font-weight: 800;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 0 25px rgba(251, 133, 0, 0.45);
transition: all 0.2s;
}
.btn-neon:hover {
transform: scale(1.05);
box-shadow: 0 0 40px rgba(251, 133, 0, 0.85);
}
.game-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
outline: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -0,0 +1,79 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<radialGradient id="bgGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#24190e"/>
<stop offset="70%" stop-color="#140d07"/>
<stop offset="100%" stop-color="#080503"/>
</radialGradient>
<linearGradient id="amberSunset" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffd166"/>
<stop offset="50%" stop-color="#fb8500"/>
<stop offset="100%" stop-color="#ff5400"/>
</linearGradient>
<linearGradient id="goldGlowGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffe600"/>
<stop offset="100%" stop-color="#ff9e00"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-strong" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="14" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Background Circle with Gold Amber Border -->
<circle cx="256" cy="256" r="240" fill="url(#bgGrad)" stroke="url(#amberSunset)" stroke-width="8" filter="url(#glow)"/>
<!-- Vinyl Grooves -->
<circle cx="256" cy="256" r="200" fill="none" stroke="rgba(255, 209, 102, 0.12)" stroke-width="3"/>
<circle cx="256" cy="256" r="170" fill="none" stroke="rgba(255, 209, 102, 0.15)" stroke-width="2"/>
<circle cx="256" cy="256" r="140" fill="none" stroke="rgba(255, 209, 102, 0.12)" stroke-width="3"/>
<!-- Equalizer Amber Sunset Waveform -->
<g filter="url(#glow)" opacity="0.95">
<rect x="130" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
<rect x="154" y="300" width="12" height="56" rx="6" fill="#ff6b00"/>
<rect x="178" y="280" width="12" height="76" rx="6" fill="#fb8500"/>
<rect x="202" y="260" width="12" height="96" rx="6" fill="#ff9e00"/>
<rect x="226" y="235" width="12" height="121" rx="6" fill="#ffb703"/>
<rect x="250" y="220" width="12" height="136" rx="6" fill="#ffd166"/>
<rect x="274" y="235" width="12" height="121" rx="6" fill="#ffb703"/>
<rect x="298" y="260" width="12" height="96" rx="6" fill="#ff9e00"/>
<rect x="322" y="280" width="12" height="76" rx="6" fill="#fb8500"/>
<rect x="346" y="300" width="12" height="56" rx="6" fill="#ff6b00"/>
<rect x="370" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
</g>
<!-- DJ Headphones Arc -->
<path d="M 120 250 A 136 136 0 0 1 392 250" fill="none" stroke="url(#amberSunset)" stroke-width="18" stroke-linecap="round" filter="url(#glow-strong)"/>
<!-- Left Ear Cup -->
<rect x="100" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#fb8500" stroke-width="7" filter="url(#glow)"/>
<rect x="108" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
<!-- Right Ear Cup -->
<rect x="376" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#ff5400" stroke-width="7" filter="url(#glow)"/>
<rect x="392" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
<!-- Center Icon: Musical Beamed Note -->
<ellipse cx="215" cy="235" rx="26" ry="20" transform="rotate(-20 215 235)" fill="url(#amberSunset)" filter="url(#glow)"/>
<ellipse cx="295" cy="205" rx="26" ry="20" transform="rotate(-20 295 205)" fill="url(#amberSunset)" filter="url(#glow)"/>
<path d="M 233 225 L 233 130 L 313 100 L 313 195" fill="none" stroke="url(#amberSunset)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" filter="url(#glow-strong)"/>
<path d="M 230 130 L 315 100" fill="none" stroke="#fff8e7" stroke-width="16" stroke-linecap="round" filter="url(#glow)"/>
<!-- Glowing Gold Core Spark in Spindle -->
<circle cx="256" cy="180" r="16" fill="#ffffff" stroke="#ffd166" stroke-width="4" filter="url(#glow)"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🎵 Guess That Song - Amber &amp; Sunset Edition</title>
<base href="/" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="icon" type="image/png" sizes="64x64" href="favicon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800;900&family=Righteous&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/game.css" />
</head>
<body>
<div id="app">
<div class="loading-screen">
<div class="loading-box">
<img src="logo.svg" alt="Guess That Song Logo" class="loading-logo" />
<h2 class="loading-title">GUESS THAT SONG</h2>
<div class="loading-subtitle">Amber &amp; Sunset Party Edition</div>
<div class="loading-progress-container">
<div id="loadingProgressBar" class="loading-progress-bar" style="width: 8%;"></div>
</div>
<div class="loading-status-row">
<span id="loadingStatusText" class="loading-status-text">Pripravujem herný engine...</span>
<span id="loadingPercentageText" class="loading-percentage-text">8%</span>
</div>
</div>
</div>
</div>
<div id="blazor-error-ui">
Nastala neočakávaná chyba pri spúšťaní.
<a href="" class="reload">Znovu načítať</a>
</div>
<script>
(function () {
const bar = document.getElementById('loadingProgressBar');
const pct = document.getElementById('loadingPercentageText');
const statusEl = document.getElementById('loadingStatusText');
const statusStages = [
{ threshold: 18, text: "Pripravujem prostredie..." },
{ threshold: 45, text: "Sťahujem WebAssembly runtime..." },
{ threshold: 75, text: "Načítavam herné moduly a knižnice..." },
{ threshold: 92, text: "Inicializujem hudobný engine..." },
{ threshold: 100, text: "Spúšťam párty..." }
];
let currentProgress = 8;
function setProgress(val) {
currentProgress = Math.max(currentProgress, Math.min(100, val));
const rounded = Math.round(currentProgress);
if (bar) bar.style.width = rounded + '%';
if (pct) pct.textContent = rounded + '%';
if (statusEl) {
const stage = statusStages.find(s => rounded <= s.threshold) || statusStages[statusStages.length - 1];
statusEl.textContent = stage.text;
}
}
setProgress(8);
// 1. Observe Blazor WASM native CSS custom property if populated
try {
const observer = new MutationObserver(() => {
const style = getComputedStyle(document.documentElement);
const blazorPct = style.getPropertyValue('--blazor-load-percentage');
if (blazorPct) {
const parsed = parseFloat(blazorPct);
if (!isNaN(parsed) && parsed > 0) {
setProgress(parsed);
}
}
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
} catch (e) {}
// 2. Continuous progress updater that smoothly animates toward 95%
const interval = setInterval(() => {
if (currentProgress < 94) {
const step = Math.max(0.4, (95 - currentProgress) * 0.05);
setProgress(currentProgress + step);
}
}, 80);
window.addEventListener('unload', () => clearInterval(interval));
})();
</script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>
@@ -0,0 +1,473 @@
// audioPlayer.js - Robust Web Audio API Engine for Guess That Song
let audioCtx = null;
let currentSource = null;
let gainNode = null;
let analyserNode = null;
let visualizerAnimationId = null;
let audioCache = {};
let loopTimeoutId = null;
let isLooping = false;
let lastPlayedParams = null;
function getOrCreateAudioContext() {
try {
if (!audioCtx) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (AudioContextClass) {
audioCtx = new AudioContextClass();
}
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume().catch(() => {});
}
} catch (e) {
console.warn("AudioContext init error:", e);
}
return audioCtx;
}
export async function initAudio() {
try {
getOrCreateAudioContext();
return true;
} catch (e) {
return false;
}
}
// Play a short snippet with continuous repeating loop (with pause) until stopped
export async function playSnippet(audioUrl, durationSec = 1.5, customStartSec = null, autoLoop = true, pauseBetweenSec = 1.2) {
try {
stopAudio();
isLooping = autoLoop;
lastPlayedParams = { audioUrl, durationSec, customStartSec, autoLoop, pauseBetweenSec };
return await playSingleSnippetIteration(audioUrl, durationSec, customStartSec, autoLoop, pauseBetweenSec);
} catch (e) {
console.warn("playSnippet error:", e);
return { success: false, error: e.message };
}
}
export async function replayLastSnippet() {
try {
if (lastPlayedParams) {
return await playSnippet(
lastPlayedParams.audioUrl,
lastPlayedParams.durationSec,
lastPlayedParams.customStartSec,
true,
lastPlayedParams.pauseBetweenSec
);
}
} catch (e) {
console.warn("replayLastSnippet error:", e);
}
}
async function playSingleSnippetIteration(audioUrl, durationSec, customStartSec, autoLoop, pauseBetweenSec) {
const ctx = getOrCreateAudioContext();
if (!ctx) return { success: false, isSynthetic: true };
try {
let buffer = audioCache[audioUrl];
if (!buffer) {
const fetchUrl = encodeURI(audioUrl);
const response = await fetch(fetchUrl);
if (!response.ok) {
throw new Error(`Failed to load audio from ${fetchUrl} (HTTP ${response.status})`);
}
const arrayBuffer = await response.arrayBuffer();
buffer = await ctx.decodeAudioData(arrayBuffer);
audioCache[audioUrl] = buffer;
}
let startOffset = 0;
if (customStartSec !== null && customStartSec !== undefined && customStartSec >= 0) {
startOffset = customStartSec;
} else {
const maxStart = Math.max(0, buffer.duration - durationSec - 3);
const minStart = Math.min(8, maxStart);
startOffset = minStart + Math.random() * (maxStart - minStart);
}
currentSource = ctx.createBufferSource();
currentSource.buffer = buffer;
gainNode = ctx.createGain();
const now = ctx.currentTime;
const fadeTime = Math.min(0.18, durationSec * 0.2);
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(1, now + fadeTime);
gainNode.gain.setValueAtTime(1, now + durationSec - fadeTime);
gainNode.gain.linearRampToValueAtTime(0, now + durationSec);
setupAnalyser(ctx);
currentSource.connect(gainNode);
gainNode.connect(analyserNode);
analyserNode.connect(ctx.destination);
currentSource.start(0, startOffset, durationSec);
if (autoLoop && isLooping) {
const totalCycleMs = (durationSec + pauseBetweenSec) * 1000;
loopTimeoutId = setTimeout(() => {
if (isLooping) {
playSingleSnippetIteration(audioUrl, durationSec, startOffset, autoLoop, pauseBetweenSec).catch(() => {});
}
}, totalCycleMs);
}
return { success: true, isSynthetic: false, duration: buffer.duration, startOffset };
} catch (err) {
console.warn("Audio file could not be played, generating synthetic preview tone:", err.message);
playSyntheticClueTone(durationSec);
if (autoLoop && isLooping) {
const totalCycleMs = (durationSec + pauseBetweenSec) * 1000;
loopTimeoutId = setTimeout(() => {
if (isLooping) {
playSyntheticClueTone(durationSec);
}
}, totalCycleMs);
}
return { success: true, isSynthetic: true, error: err.message };
}
}
// Play full song when revealed
export async function playFullSong(audioUrl) {
try {
stopAudio();
const ctx = getOrCreateAudioContext();
if (!ctx) return false;
let buffer = audioCache[audioUrl];
if (!buffer) {
const fetchUrl = encodeURI(audioUrl);
const response = await fetch(fetchUrl);
if (!response.ok) throw new Error(`Audio not found from ${fetchUrl}`);
const arrayBuffer = await response.arrayBuffer();
buffer = await ctx.decodeAudioData(arrayBuffer);
audioCache[audioUrl] = buffer;
}
currentSource = ctx.createBufferSource();
currentSource.buffer = buffer;
gainNode = ctx.createGain();
const now = ctx.currentTime;
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.85, now + 0.8);
setupAnalyser(ctx);
currentSource.connect(gainNode);
gainNode.connect(analyserNode);
analyserNode.connect(ctx.destination);
currentSource.start(0, 0);
return true;
} catch (err) {
console.warn("Could not play full song:", err.message);
playSyntheticVictoryChime();
return false;
}
}
export function stopAudio() {
try {
isLooping = false;
if (loopTimeoutId) {
clearTimeout(loopTimeoutId);
loopTimeoutId = null;
}
if (currentSource) {
try {
currentSource.stop();
currentSource.disconnect();
} catch (e) { }
currentSource = null;
}
} catch (e) {}
}
export async function playIntroThemeSong(loop = true) {
try {
stopAudio();
const ctx = getOrCreateAudioContext();
if (!ctx) return false;
const audioUrl = "music/intro_theme.wav";
let buffer = audioCache[audioUrl];
if (!buffer) {
const response = await fetch(audioUrl);
if (!response.ok) throw new Error(`Intro audio file not found (${response.status})`);
const arrayBuffer = await response.arrayBuffer();
buffer = await ctx.decodeAudioData(arrayBuffer);
audioCache[audioUrl] = buffer;
}
currentSource = ctx.createBufferSource();
currentSource.buffer = buffer;
currentSource.loop = loop;
gainNode = ctx.createGain();
const now = ctx.currentTime;
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.85, now + 0.4);
setupAnalyser(ctx);
currentSource.connect(gainNode);
gainNode.connect(analyserNode);
analyserNode.connect(ctx.destination);
currentSource.start(0);
return true;
} catch (err) {
console.warn("Could not load intro WAV, playing synthetic fanfare:", err.message);
playArcadeIntroSound();
return false;
}
}
export function playArcadeIntroSound() {
try {
const ctx = getOrCreateAudioContext();
if (!ctx) return;
const startTime = ctx.currentTime + 0.05;
// Synth Bass Note
const bass = ctx.createOscillator();
const bassGain = ctx.createGain();
bass.type = 'sawtooth';
bass.frequency.setValueAtTime(110, startTime);
bass.frequency.exponentialRampToValueAtTime(55, startTime + 1.0);
bassGain.gain.setValueAtTime(0.3, startTime);
bassGain.gain.exponentialRampToValueAtTime(0.001, startTime + 1.0);
bass.connect(bassGain);
bassGain.connect(ctx.destination);
bass.start(startTime);
bass.stop(startTime + 1.0);
// Fanfare Chords
const melody = [
{ f: 220, t: 0.0 },
{ f: 277.18, t: 0.10 },
{ f: 329.63, t: 0.20 },
{ f: 440, t: 0.30 },
{ f: 554.37, t: 0.40 },
{ f: 659.25, t: 0.50 },
{ f: 880, t: 0.60 }
];
melody.forEach(note => {
try {
const osc = ctx.createOscillator();
const g = ctx.createGain();
const noteStart = startTime + note.t;
osc.type = 'triangle';
osc.frequency.setValueAtTime(note.f, noteStart);
g.gain.setValueAtTime(0.2, noteStart);
g.gain.exponentialRampToValueAtTime(0.001, noteStart + 0.6);
osc.connect(g);
g.connect(ctx.destination);
osc.start(noteStart);
osc.stop(noteStart + 0.6);
} catch (e) {}
});
} catch (e) {}
}
export function playBuzzerSound() {
try {
const ctx = getOrCreateAudioContext();
if (!ctx) return;
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(140, ctx.currentTime);
osc.frequency.linearRampToValueAtTime(90, ctx.currentTime + 0.35);
g.gain.setValueAtTime(0.35, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.35);
osc.connect(g);
g.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.35);
} catch (e) {}
}
export function playNextClueSound() {
try {
const ctx = getOrCreateAudioContext();
if (!ctx) return;
const notes = [330, 440, 550];
notes.forEach((freq, i) => {
try {
const osc = ctx.createOscillator();
const g = ctx.createGain();
const start = ctx.currentTime + i * 0.08;
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, start);
g.gain.setValueAtTime(0.2, start);
g.gain.exponentialRampToValueAtTime(0.001, start + 0.2);
osc.connect(g);
g.connect(ctx.destination);
osc.start(start);
osc.stop(start + 0.2);
} catch (e) {}
});
} catch (e) {}
}
export function playScoreWinSound() {
try {
const ctx = getOrCreateAudioContext();
if (!ctx) return;
const notes = [440, 554, 659, 880, 1108];
notes.forEach((freq, i) => {
try {
const osc = ctx.createOscillator();
const g = ctx.createGain();
const start = ctx.currentTime + i * 0.09;
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, start);
g.gain.setValueAtTime(0.3, start);
g.gain.exponentialRampToValueAtTime(0.001, start + 0.5);
osc.connect(g);
g.connect(ctx.destination);
osc.start(start);
osc.stop(start + 0.5);
} catch (e) {}
});
} catch (e) {}
}
function playSyntheticClueTone(durationSec) {
try {
const ctx = getOrCreateAudioContext();
if (!ctx) return;
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(220, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + durationSec);
g.gain.setValueAtTime(0, ctx.currentTime);
g.gain.linearRampToValueAtTime(0.2, ctx.currentTime + 0.2);
g.gain.setValueAtTime(0.2, ctx.currentTime + durationSec - 0.2);
g.gain.linearRampToValueAtTime(0, ctx.currentTime + durationSec);
setupAnalyser(ctx);
osc.connect(g);
g.connect(analyserNode);
analyserNode.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + durationSec);
} catch (e) {}
}
function playSyntheticVictoryChime() {
try {
const ctx = getOrCreateAudioContext();
if (!ctx) return;
const chords = [523.25, 659.25, 783.99, 1046.50];
chords.forEach(freq => {
try {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, ctx.currentTime);
g.gain.setValueAtTime(0.15, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.5);
osc.connect(g);
g.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 1.5);
} catch (e) {}
});
} catch (e) {}
}
function setupAnalyser(ctx) {
try {
if (!analyserNode && ctx) {
analyserNode = ctx.createAnalyser();
analyserNode.fftSize = 64;
}
} catch (e) {}
}
export function startVisualizer(canvasId) {
try {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx2d = canvas.getContext('2d');
const render = () => {
visualizerAnimationId = requestAnimationFrame(render);
const width = canvas.width = canvas.parentElement.clientWidth || 300;
const height = canvas.height = 70;
ctx2d.clearRect(0, 0, width, height);
if (!analyserNode) {
ctx2d.fillStyle = 'rgba(255, 183, 3, 0.18)';
ctx2d.fillRect(0, height / 2 - 2, width, 4);
return;
}
const bufferLength = analyserNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);
const barWidth = (width / bufferLength) * 1.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * height;
const grad = ctx2d.createLinearGradient(0, height - barHeight, 0, height);
grad.addColorStop(0, '#ffd166');
grad.addColorStop(0.5, '#ffb703');
grad.addColorStop(1, '#fb8500');
ctx2d.fillStyle = grad;
ctx2d.fillRect(x, height - barHeight, barWidth - 2, barHeight);
x += barWidth;
}
};
if (visualizerAnimationId) {
cancelAnimationFrame(visualizerAnimationId);
}
render();
} catch (e) {}
}
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680 120" width="100%" height="100%">
<defs>
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ffd166"/>
<stop offset="50%" stop-color="#fb8500"/>
<stop offset="100%" stop-color="#ff5400"/>
</linearGradient>
<filter id="textGlow" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<linearGradient id="iconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffd166"/>
<stop offset="100%" stop-color="#ff5400"/>
</linearGradient>
</defs>
<!-- Left Icon Mark -->
<g transform="translate(10, 10) scale(0.2)">
<circle cx="256" cy="256" r="230" fill="#140e08" stroke="url(#iconGrad)" stroke-width="14" filter="url(#textGlow)"/>
<path d="M 120 250 A 136 136 0 0 1 392 250" fill="none" stroke="url(#iconGrad)" stroke-width="24" stroke-linecap="round"/>
<rect x="96" y="225" width="44" height="74" rx="22" fill="#24190e" stroke="#fb8500" stroke-width="8"/>
<rect x="372" y="225" width="44" height="74" rx="22" fill="#24190e" stroke="#ff5400" stroke-width="8"/>
<ellipse cx="215" cy="235" rx="30" ry="24" transform="rotate(-20 215 235)" fill="url(#iconGrad)"/>
<ellipse cx="295" cy="205" rx="30" ry="24" transform="rotate(-20 295 205)" fill="url(#iconGrad)"/>
<path d="M 233 225 L 233 130 L 313 100 L 313 195" fill="none" stroke="url(#iconGrad)" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 230 130 L 315 100" fill="none" stroke="#fff8e7" stroke-width="20" stroke-linecap="round"/>
</g>
<!-- Title Text -->
<text x="130" y="65" font-family="'Righteous', 'Outfit', sans-serif" font-size="44" font-weight="900" fill="url(#textGrad)" filter="url(#textGlow)" letter-spacing="3">
GUESS THAT SONG
</text>
<text x="132" y="96" font-family="'Outfit', sans-serif" font-size="15" font-weight="800" fill="#ffd166" letter-spacing="6" opacity="0.95">
AMBER &amp; SUNSET PARTY EDITION
</text>
<rect x="490" y="82" width="6" height="15" fill="#fb8500" rx="3"/>
<text x="505" y="96" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#cbd5e1" letter-spacing="1">
SPACE TO PLAY
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+60
View File
@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
<defs>
<linearGradient id="logoAmber" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffd166"/>
<stop offset="50%" stop-color="#fb8500"/>
<stop offset="100%" stop-color="#ff5400"/>
</linearGradient>
<radialGradient id="coreGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(251, 133, 0, 0.45)"/>
<stop offset="100%" stop-color="rgba(0, 0, 0, 0)"/>
</radialGradient>
<filter id="logoGlow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<circle cx="256" cy="256" r="230" fill="url(#coreGlow)"/>
<circle cx="256" cy="256" r="220" fill="#140e08" stroke="url(#logoAmber)" stroke-width="8" filter="url(#logoGlow)"/>
<!-- Grooves -->
<circle cx="256" cy="256" r="180" fill="none" stroke="rgba(255, 209, 102, 0.12)" stroke-width="2"/>
<circle cx="256" cy="256" r="150" fill="none" stroke="rgba(255, 209, 102, 0.08)" stroke-width="2"/>
<!-- Equalizer -->
<g filter="url(#logoGlow)" opacity="0.95">
<rect x="134" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
<rect x="158" y="295" width="12" height="61" rx="6" fill="#ff6b00"/>
<rect x="182" y="275" width="12" height="81" rx="6" fill="#fb8500"/>
<rect x="206" y="250" width="12" height="106" rx="6" fill="#ff9e00"/>
<rect x="230" y="225" width="12" height="131" rx="6" fill="#ffb703"/>
<rect x="254" y="210" width="12" height="146" rx="6" fill="#ffd166"/>
<rect x="278" y="225" width="12" height="131" rx="6" fill="#ffb703"/>
<rect x="302" y="250" width="12" height="106" rx="6" fill="#ff9e00"/>
<rect x="326" y="275" width="12" height="81" rx="6" fill="#fb8500"/>
<rect x="350" y="295" width="12" height="61" rx="6" fill="#ff6b00"/>
<rect x="374" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
</g>
<!-- Headphones -->
<path d="M 120 250 A 136 136 0 0 1 392 250" fill="none" stroke="url(#logoAmber)" stroke-width="18" stroke-linecap="round" filter="url(#logoGlow)"/>
<rect x="100" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#fb8500" stroke-width="7"/>
<rect x="108" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
<rect x="376" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#ff5400" stroke-width="7"/>
<rect x="392" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
<!-- Beamed Note -->
<ellipse cx="215" cy="235" rx="26" ry="20" transform="rotate(-20 215 235)" fill="url(#logoAmber)"/>
<ellipse cx="295" cy="205" rx="26" ry="20" transform="rotate(-20 295 205)" fill="url(#logoAmber)"/>
<path d="M 233 225 L 233 130 L 313 100 L 313 195" fill="none" stroke="url(#logoAmber)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" filter="url(#logoGlow)"/>
<path d="M 230 130 L 315 100" fill="none" stroke="#fff8e7" stroke-width="16" stroke-linecap="round"/>
<!-- Spark -->
<circle cx="256" cy="180" r="15" fill="#ffffff" stroke="#ffd166" stroke-width="4" filter="url(#logoGlow)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More