Small revamps and design changes to visualizer, now in Estonian
This commit is contained in:
45
.gitignore
vendored
45
.gitignore
vendored
@@ -1,31 +1,26 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS files
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment files
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
# Paraglide
|
||||
src/lib/paraglide
|
||||
project.inlang/cache/
|
||||
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
To recreate this project with the same configuration:
|
||||
|
||||
```sh
|
||||
# recreate this project
|
||||
npx sv create --template minimal --types ts --add tailwindcss="plugins:typography,forms" paraglide="languageTags:en, et+demo:no" --install npm audio-visualizer-v2
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
31
index.html
31
index.html
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Audio Visualizer</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #0a0a0a;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
57
messages/en.json
Normal file
57
messages/en.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"audio_source": "Audio Source",
|
||||
"stop": "Stop",
|
||||
"start": "Start",
|
||||
"mic": "Mic",
|
||||
"tab": "Tab",
|
||||
"file": "File",
|
||||
"tab_hint": "Click Start, pick a tab playing music, and check",
|
||||
"tab_hint_bold": "Share tab audio",
|
||||
"drop_audio": "Drop audio file here",
|
||||
"file_loaded": "File loaded. Click Start to begin playback.",
|
||||
"themes": "Themes",
|
||||
"theme_name_placeholder": "Theme name...",
|
||||
"save": "Save",
|
||||
"visualization": "Visualization",
|
||||
"circular": "Circular",
|
||||
"bars": "Bars",
|
||||
"wave": "Wave",
|
||||
"blob": "Blob",
|
||||
"bars_count": "Bars",
|
||||
"reactivity": "Reactivity",
|
||||
"sensitivity": "Sensitivity",
|
||||
"smoothing": "Smoothing",
|
||||
"bass_emphasis": "Bass Emphasis",
|
||||
"effects": "Effects",
|
||||
"shake": "Shake",
|
||||
"zoom": "Zoom",
|
||||
"glow": "Glow",
|
||||
"particles": "Particles",
|
||||
"enable_particles": "Enable Particles",
|
||||
"density": "Density",
|
||||
"size": "Size",
|
||||
"rotation": "Rotation",
|
||||
"image": "Image",
|
||||
"clear": "Clear",
|
||||
"branding": "Branding",
|
||||
"title_placeholder": "Title...",
|
||||
"subtitle_placeholder": "Subtitle...",
|
||||
"position": "Position",
|
||||
"logo": "Logo",
|
||||
"spin_logo": "Spin Logo",
|
||||
"speed": "Speed",
|
||||
"colors": "Colors",
|
||||
"primary": "Primary",
|
||||
"secondary": "Second",
|
||||
"background": "Background",
|
||||
"vignette": "Vignette",
|
||||
"actions": "Actions",
|
||||
"screenshot": "Screenshot",
|
||||
"fullscreen": "Fullscreen",
|
||||
"reset": "Reset",
|
||||
"play_stop": "Play/Stop",
|
||||
"hide": "Hide",
|
||||
"export_theme": "Export",
|
||||
"import_theme": "Import"
|
||||
}
|
||||
57
messages/et.json
Normal file
57
messages/et.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"audio_source": "Heliallikas",
|
||||
"stop": "Peata",
|
||||
"start": "Alusta",
|
||||
"mic": "Mikrofon",
|
||||
"tab": "Vahekaart",
|
||||
"file": "Fail",
|
||||
"tab_hint": "Vajuta Alusta, vali muusikat mängiv vahekaart ja märgi",
|
||||
"tab_hint_bold": "Jaga vahekaardi heli",
|
||||
"drop_audio": "Lohista helifail siia",
|
||||
"file_loaded": "Fail laaditud. Vajuta Alusta esituse alustamiseks.",
|
||||
"themes": "Teemad",
|
||||
"theme_name_placeholder": "Teema nimi...",
|
||||
"save": "Salvesta",
|
||||
"visualization": "Visualiseerimine",
|
||||
"circular": "Ring",
|
||||
"bars": "Ribad",
|
||||
"wave": "Laine",
|
||||
"blob": "Plekk",
|
||||
"bars_count": "Ribad",
|
||||
"reactivity": "Reaktiivsus",
|
||||
"sensitivity": "Tundlikkus",
|
||||
"smoothing": "Silumine",
|
||||
"bass_emphasis": "Bassi rõhk",
|
||||
"effects": "Efektid",
|
||||
"shake": "Värin",
|
||||
"zoom": "Suum",
|
||||
"glow": "Helendus",
|
||||
"particles": "Osakesed",
|
||||
"enable_particles": "Luba osakesed",
|
||||
"density": "Tihedus",
|
||||
"size": "Suurus",
|
||||
"rotation": "Pöörlemine",
|
||||
"image": "Pilt",
|
||||
"clear": "Tühjenda",
|
||||
"branding": "Kujundus",
|
||||
"title_placeholder": "Pealkiri...",
|
||||
"subtitle_placeholder": "Alapealkiri...",
|
||||
"position": "Asukoht",
|
||||
"logo": "Logo",
|
||||
"spin_logo": "Pöörlev logo",
|
||||
"speed": "Kiirus",
|
||||
"colors": "Värvid",
|
||||
"primary": "Põhivärv",
|
||||
"secondary": "Teisene",
|
||||
"background": "Taust",
|
||||
"vignette": "Vinjett",
|
||||
"actions": "Toimingud",
|
||||
"screenshot": "Ekraanipilt",
|
||||
"fullscreen": "Täisekraan",
|
||||
"reset": "Lähtesta",
|
||||
"play_stop": "Esita/Peata",
|
||||
"hide": "Peida",
|
||||
"export_theme": "Ekspordi",
|
||||
"import_theme": "Impordi"
|
||||
}
|
||||
3866
package-lock.json
generated
3866
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -1,16 +1,28 @@
|
||||
{
|
||||
"name": "audio-visualizer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"svelte": "^4.2.12",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
"name": "audio-visualizer",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.10.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
15
project.inlang/settings.json
Normal file
15
project.inlang/settings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
},
|
||||
"baseLocale": "en",
|
||||
"locales": [
|
||||
"en",
|
||||
"et"
|
||||
]
|
||||
}
|
||||
609
src/App.svelte
609
src/App.svelte
@@ -1,609 +0,0 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import AudioVisualizer from "./lib/AudioVisualizer.svelte";
|
||||
import Controls from "./lib/Controls.svelte";
|
||||
|
||||
const STORAGE_KEY = "ncs-visualizer-settings";
|
||||
|
||||
const defaults = {
|
||||
selectedPreset: "cyanPink",
|
||||
logoUrl: "",
|
||||
title: "Audio Visualizer",
|
||||
subtitle: "Now Playing",
|
||||
bgImage: "",
|
||||
bgColor: "#0a0a0a",
|
||||
customPrimary: "#00d4ff",
|
||||
customSecondary: "#ff006e",
|
||||
useCustomColors: false,
|
||||
sidebarOpen: true,
|
||||
vignette: true,
|
||||
audioSource: "mic",
|
||||
audioUrl: "",
|
||||
vizMode: "circular",
|
||||
barCount: 48,
|
||||
sensitivity: 1.0,
|
||||
smoothing: 0.82,
|
||||
bassEmphasis: 0.25,
|
||||
particlesEnabled: true,
|
||||
particleDensity: 1.0,
|
||||
shakeAmount: 1.0,
|
||||
zoomIntensity: 1.0,
|
||||
glowIntensity: 0.6,
|
||||
logoSpin: false,
|
||||
logoSpinSpeed: 5,
|
||||
particleImage: "",
|
||||
particleSize: 1.0,
|
||||
particleRotation: 0,
|
||||
titlePosition: "top",
|
||||
};
|
||||
|
||||
function loadSettings() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return raw ? { ...defaults, ...JSON.parse(raw) } : { ...defaults };
|
||||
} catch {
|
||||
return { ...defaults };
|
||||
}
|
||||
}
|
||||
|
||||
const saved = loadSettings();
|
||||
|
||||
let selectedPreset = saved.selectedPreset;
|
||||
let logoUrl = saved.logoUrl;
|
||||
let title = saved.title;
|
||||
let subtitle = saved.subtitle;
|
||||
let bgImage = saved.bgImage;
|
||||
let bgColor = saved.bgColor;
|
||||
let customPrimary = saved.customPrimary;
|
||||
let customSecondary = saved.customSecondary;
|
||||
let useCustomColors = saved.useCustomColors;
|
||||
let isListening = false;
|
||||
let sidebarOpen = saved.sidebarOpen;
|
||||
let vignette = saved.vignette;
|
||||
let audioSource = saved.audioSource;
|
||||
let audioUrl = saved.audioUrl;
|
||||
let vizMode = saved.vizMode;
|
||||
let barCount = saved.barCount;
|
||||
let sensitivity = saved.sensitivity;
|
||||
let smoothing = saved.smoothing;
|
||||
let bassEmphasis = saved.bassEmphasis;
|
||||
let particlesEnabled = saved.particlesEnabled;
|
||||
let particleDensity = saved.particleDensity;
|
||||
let shakeAmount = saved.shakeAmount;
|
||||
let zoomIntensity = saved.zoomIntensity;
|
||||
let glowIntensity = saved.glowIntensity;
|
||||
let logoSpin = saved.logoSpin;
|
||||
let logoSpinSpeed = saved.logoSpinSpeed;
|
||||
let particleImage = saved.particleImage;
|
||||
let particleSize = saved.particleSize;
|
||||
let particleRotation = saved.particleRotation;
|
||||
let titlePosition = saved.titlePosition;
|
||||
let visualizerComponent;
|
||||
let toggleHidden = false;
|
||||
let hideTimer;
|
||||
|
||||
function startHideTimer() {
|
||||
clearTimeout(hideTimer);
|
||||
if (!sidebarOpen) {
|
||||
hideTimer = setTimeout(() => {
|
||||
toggleHidden = true;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleClick() {
|
||||
sidebarOpen = !sidebarOpen;
|
||||
toggleHidden = false;
|
||||
clearTimeout(hideTimer);
|
||||
if (!sidebarOpen) startHideTimer();
|
||||
}
|
||||
|
||||
$: if (!sidebarOpen) {
|
||||
startHideTimer();
|
||||
} else {
|
||||
toggleHidden = false;
|
||||
clearTimeout(hideTimer);
|
||||
}
|
||||
|
||||
$: {
|
||||
const settings = {
|
||||
selectedPreset,
|
||||
logoUrl,
|
||||
title,
|
||||
subtitle,
|
||||
bgImage,
|
||||
bgColor,
|
||||
customPrimary,
|
||||
customSecondary,
|
||||
useCustomColors,
|
||||
sidebarOpen,
|
||||
vignette,
|
||||
audioSource,
|
||||
audioUrl,
|
||||
vizMode,
|
||||
barCount,
|
||||
sensitivity,
|
||||
smoothing,
|
||||
bassEmphasis,
|
||||
particlesEnabled,
|
||||
particleDensity,
|
||||
shakeAmount,
|
||||
zoomIntensity,
|
||||
glowIntensity,
|
||||
logoSpin,
|
||||
logoSpinSpeed,
|
||||
particleImage,
|
||||
particleSize,
|
||||
particleRotation,
|
||||
titlePosition,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function toggleListening() {
|
||||
if (isListening) {
|
||||
visualizerComponent.stopListening();
|
||||
} else {
|
||||
visualizerComponent.startListening();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScreenshot() {
|
||||
const dataUrl = visualizerComponent.takeScreenshot();
|
||||
if (!dataUrl) return;
|
||||
const link = document.createElement("a");
|
||||
link.download = "visualizer-screenshot.png";
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
}
|
||||
|
||||
function handleFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(() => {});
|
||||
} else {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
selectedPreset = defaults.selectedPreset;
|
||||
logoUrl = defaults.logoUrl;
|
||||
title = defaults.title;
|
||||
subtitle = defaults.subtitle;
|
||||
bgImage = defaults.bgImage;
|
||||
bgColor = defaults.bgColor;
|
||||
customPrimary = defaults.customPrimary;
|
||||
customSecondary = defaults.customSecondary;
|
||||
useCustomColors = defaults.useCustomColors;
|
||||
vignette = defaults.vignette;
|
||||
audioSource = defaults.audioSource;
|
||||
audioUrl = defaults.audioUrl;
|
||||
vizMode = defaults.vizMode;
|
||||
barCount = defaults.barCount;
|
||||
sensitivity = defaults.sensitivity;
|
||||
smoothing = defaults.smoothing;
|
||||
bassEmphasis = defaults.bassEmphasis;
|
||||
particlesEnabled = defaults.particlesEnabled;
|
||||
particleDensity = defaults.particleDensity;
|
||||
shakeAmount = defaults.shakeAmount;
|
||||
zoomIntensity = defaults.zoomIntensity;
|
||||
glowIntensity = defaults.glowIntensity;
|
||||
logoSpin = defaults.logoSpin;
|
||||
logoSpinSpeed = defaults.logoSpinSpeed;
|
||||
particleImage = defaults.particleImage;
|
||||
particleSize = defaults.particleSize;
|
||||
particleRotation = defaults.particleRotation;
|
||||
titlePosition = defaults.titlePosition;
|
||||
}
|
||||
|
||||
let fileCurrentTime = 0;
|
||||
let fileDuration = 0;
|
||||
let filePlaying = false;
|
||||
let fileName = "";
|
||||
|
||||
function handleAudioFile(e) {
|
||||
const file = e.detail;
|
||||
if (file && visualizerComponent) {
|
||||
if (isListening) {
|
||||
visualizerComponent.stopListening();
|
||||
}
|
||||
audioSource = "file";
|
||||
visualizerComponent.setAudioFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilePause() {
|
||||
if (visualizerComponent) visualizerComponent.pauseAudio();
|
||||
}
|
||||
|
||||
function handleFileResume() {
|
||||
if (visualizerComponent) visualizerComponent.resumeAudio();
|
||||
}
|
||||
|
||||
function handleFileSeek(e) {
|
||||
if (visualizerComponent) visualizerComponent.seekAudio(e.detail);
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
// Ignore if user is typing in an input
|
||||
if (
|
||||
e.target.tagName === "INPUT" ||
|
||||
e.target.tagName === "TEXTAREA" ||
|
||||
e.target.isContentEditable
|
||||
)
|
||||
return;
|
||||
|
||||
switch (e.code) {
|
||||
case "Space":
|
||||
e.preventDefault();
|
||||
toggleListening();
|
||||
break;
|
||||
case "KeyF":
|
||||
e.preventDefault();
|
||||
handleFullscreen();
|
||||
break;
|
||||
case "KeyH":
|
||||
e.preventDefault();
|
||||
handleToggleClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
});
|
||||
|
||||
const colorPresets = {
|
||||
cyanPink: {
|
||||
name: "Cyan/Pink",
|
||||
primary: "#00d4ff",
|
||||
secondary: "#ff006e",
|
||||
},
|
||||
purpleOrange: {
|
||||
name: "Purple/Orange",
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#f97316",
|
||||
},
|
||||
greenYellow: {
|
||||
name: "Green/Yellow",
|
||||
primary: "#22c55e",
|
||||
secondary: "#eab308",
|
||||
},
|
||||
pinkBlue: {
|
||||
name: "Pink/Blue",
|
||||
primary: "#ec4899",
|
||||
secondary: "#3b82f6",
|
||||
},
|
||||
redOrange: {
|
||||
name: "Red/Orange",
|
||||
primary: "#ef4444",
|
||||
secondary: "#f97316",
|
||||
},
|
||||
white: {
|
||||
name: "White",
|
||||
primary: "#ffffff",
|
||||
secondary: "#888888",
|
||||
},
|
||||
};
|
||||
|
||||
$: currentColors = useCustomColors
|
||||
? { primary: customPrimary, secondary: customSecondary }
|
||||
: colorPresets[selectedPreset];
|
||||
</script>
|
||||
|
||||
<main style="--bg-color: {bgColor}">
|
||||
{#if bgImage}
|
||||
<div class="bg-image" style="background-image: url({bgImage})"></div>
|
||||
{/if}
|
||||
{#if vignette}
|
||||
<div class="vignette"></div>
|
||||
{/if}
|
||||
|
||||
<div class="stage">
|
||||
<AudioVisualizer
|
||||
bind:this={visualizerComponent}
|
||||
colors={currentColors}
|
||||
{logoUrl}
|
||||
{audioSource}
|
||||
{vizMode}
|
||||
{barCount}
|
||||
{sensitivity}
|
||||
{smoothing}
|
||||
{bassEmphasis}
|
||||
{particlesEnabled}
|
||||
{particleDensity}
|
||||
{shakeAmount}
|
||||
{zoomIntensity}
|
||||
{glowIntensity}
|
||||
{logoSpin}
|
||||
{logoSpinSpeed}
|
||||
{particleImage}
|
||||
{particleSize}
|
||||
{particleRotation}
|
||||
bind:isListening
|
||||
bind:fileCurrentTime
|
||||
bind:fileDuration
|
||||
bind:filePlaying
|
||||
bind:fileName
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="titles title-{titlePosition}">
|
||||
<h1 style="color: {currentColors.primary}">{title}</h1>
|
||||
<h2 style="color: {currentColors.secondary}">{subtitle}</h2>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="toggle-zone"
|
||||
class:hidden={toggleHidden}
|
||||
on:mouseenter={() => {
|
||||
toggleHidden = false;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="sidebar-toggle"
|
||||
class:open={sidebarOpen}
|
||||
on:click={handleToggleClick}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
{#if sidebarOpen}
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
{:else}
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<aside class="sidebar" class:open={sidebarOpen}>
|
||||
<Controls
|
||||
bind:selectedPreset
|
||||
bind:logoUrl
|
||||
bind:title
|
||||
bind:subtitle
|
||||
bind:bgImage
|
||||
bind:bgColor
|
||||
bind:vignette
|
||||
bind:audioSource
|
||||
bind:audioUrl
|
||||
bind:customPrimary
|
||||
bind:customSecondary
|
||||
bind:useCustomColors
|
||||
bind:vizMode
|
||||
bind:barCount
|
||||
bind:sensitivity
|
||||
bind:smoothing
|
||||
bind:bassEmphasis
|
||||
bind:particlesEnabled
|
||||
bind:particleDensity
|
||||
bind:shakeAmount
|
||||
bind:zoomIntensity
|
||||
bind:glowIntensity
|
||||
bind:logoSpin
|
||||
bind:logoSpinSpeed
|
||||
bind:particleImage
|
||||
bind:particleSize
|
||||
bind:particleRotation
|
||||
bind:titlePosition
|
||||
{isListening}
|
||||
{colorPresets}
|
||||
onToggle={toggleListening}
|
||||
on:screenshot={handleScreenshot}
|
||||
on:fullscreen={handleFullscreen}
|
||||
on:reset={handleReset}
|
||||
on:audiofile={handleAudioFile}
|
||||
on:filepause={handleFilePause}
|
||||
on:fileresume={handleFileResume}
|
||||
on:fileseek={handleFileSeek}
|
||||
{fileCurrentTime}
|
||||
{fileDuration}
|
||||
{filePlaying}
|
||||
{fileName}
|
||||
/>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 1) 100%
|
||||
);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.titles {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
padding: 1.5rem 2.5rem;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0.7) 0%,
|
||||
rgba(0, 0, 0, 0.4) 50%,
|
||||
transparent 80%
|
||||
);
|
||||
text-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 1),
|
||||
0 0 30px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.title-top {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.title-bottom {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.title-center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.title-top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.title-top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.title-bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.4rem, 3.5vw, 3rem);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1rem, 2vw, 1.8rem);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.15em;
|
||||
margin: 0.3rem 0 0 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.toggle-zone {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50px;
|
||||
height: 100vh;
|
||||
z-index: 30;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.toggle-zone.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toggle-zone.hidden:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-right: none;
|
||||
border-radius: 8px 0 0 8px;
|
||||
color: #fff;
|
||||
padding: 0.75rem 0.4rem;
|
||||
cursor: pointer;
|
||||
transition: right 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-toggle.open {
|
||||
right: 320px;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -320px;
|
||||
width: 320px;
|
||||
height: 100vh;
|
||||
background: rgba(10, 10, 15, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 20;
|
||||
overflow-y: auto;
|
||||
transition: right 0.3s ease;
|
||||
padding: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
18
src/app.html
Normal file
18
src/app.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="%paraglide.lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
12
src/hooks.server.ts
Normal file
12
src/hooks.server.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { paraglideMiddleware } from '$lib/paraglide/server';
|
||||
|
||||
const handleParaglide: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, ({ request, locale }) => {
|
||||
event.request = request;
|
||||
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
|
||||
});
|
||||
});
|
||||
|
||||
export const handle: Handle = handleParaglide;
|
||||
3
src/hooks.ts
Normal file
3
src/hooks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { deLocalizeUrl } from '$lib/paraglide/runtime';
|
||||
|
||||
export const reroute = (request) => deLocalizeUrl(request.url).pathname;
|
||||
@@ -1,27 +1,31 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
export let colors = { primary: "#00d4ff", secondary: "#ff006e" };
|
||||
export let logoUrl = "";
|
||||
export let isListening = false;
|
||||
export let audioSource = "mic"; // "mic", "tab", "link", "file"
|
||||
|
||||
// Visualization mode: "circular", "bars", "wave", "blob"
|
||||
export let vizMode = "circular";
|
||||
export let barCount = 48;
|
||||
export let sensitivity = 1.0;
|
||||
export let smoothing = 0.82;
|
||||
export let bassEmphasis = 0.25;
|
||||
export let particlesEnabled = true;
|
||||
export let particleDensity = 1.0;
|
||||
export let shakeAmount = 1.0;
|
||||
export let zoomIntensity = 1.0;
|
||||
export let glowIntensity = 0.6;
|
||||
export let logoSpin = false;
|
||||
export let logoSpinSpeed = 5;
|
||||
export let particleImage = "";
|
||||
export let particleSize = 1.0;
|
||||
export let particleRotation = 0;
|
||||
let {
|
||||
colors = { primary: "#00d4ff", secondary: "#ff006e" },
|
||||
logoUrl = "",
|
||||
isListening = $bindable(false),
|
||||
audioSource = "mic",
|
||||
vizMode = "circular",
|
||||
barCount = 48,
|
||||
sensitivity = 1.0,
|
||||
smoothing = 0.82,
|
||||
bassEmphasis = 0.25,
|
||||
particlesEnabled = true,
|
||||
particleDensity = 1.0,
|
||||
shakeAmount = 1.0,
|
||||
zoomIntensity = 1.0,
|
||||
glowIntensity = 0.6,
|
||||
logoSpin = false,
|
||||
logoSpinSpeed = 5,
|
||||
particleImage = "",
|
||||
particleSize = 1.0,
|
||||
particleRotation = 0,
|
||||
fileCurrentTime = $bindable(0),
|
||||
fileDuration = $bindable(0),
|
||||
filePlaying = $bindable(false),
|
||||
fileName = $bindable(""),
|
||||
} = $props();
|
||||
|
||||
const DEFAULT_SIZE = 250;
|
||||
const LINE_LIFT = 8;
|
||||
@@ -45,10 +49,10 @@
|
||||
let dataArray = new Uint8Array(128);
|
||||
let bufferLength = 128;
|
||||
|
||||
let size = DEFAULT_SIZE;
|
||||
let size = $state(DEFAULT_SIZE);
|
||||
let centerX = DEFAULT_SIZE / 2;
|
||||
let centerY = DEFAULT_SIZE / 2;
|
||||
let baseRadius = DEFAULT_SIZE * 0.25;
|
||||
let baseRadius = $state(DEFAULT_SIZE * 0.25);
|
||||
|
||||
let cosValues = new Float32Array(barCount);
|
||||
let sinValues = new Float32Array(barCount);
|
||||
@@ -59,26 +63,47 @@
|
||||
let blobPhase = 0;
|
||||
|
||||
// Particle image cache
|
||||
let pImg = null;
|
||||
$: if (particleImage) {
|
||||
const img = new Image();
|
||||
img.src = particleImage;
|
||||
pImg = img;
|
||||
} else {
|
||||
pImg = null;
|
||||
}
|
||||
let pImg = $state(null);
|
||||
$effect(() => {
|
||||
if (particleImage) {
|
||||
const img = new Image();
|
||||
img.src = particleImage;
|
||||
pImg = img;
|
||||
} else {
|
||||
pImg = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Particle system
|
||||
let particles = [];
|
||||
|
||||
$: {
|
||||
// Recalculate angles when barCount changes
|
||||
let prevBarCount = barCount;
|
||||
$effect(() => {
|
||||
const totalBars = barCount;
|
||||
const halfBars = Math.ceil(totalBars / 2);
|
||||
cosValues = new Float32Array(totalBars);
|
||||
sinValues = new Float32Array(totalBars);
|
||||
smoothedValues = new Float32Array(halfBars);
|
||||
precalculateAngles();
|
||||
}
|
||||
prevBarCount = barCount;
|
||||
});
|
||||
|
||||
// Update analyser smoothing
|
||||
$effect(() => {
|
||||
if (analyser) {
|
||||
analyser.smoothingTimeConstant = smoothing;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear particles when viz mode changes
|
||||
let prevVizMode = vizMode;
|
||||
$effect(() => {
|
||||
if (vizMode !== prevVizMode) {
|
||||
particles = [];
|
||||
prevVizMode = vizMode;
|
||||
}
|
||||
});
|
||||
|
||||
function spawnParticles(loudness) {
|
||||
if (!particlesEnabled) return;
|
||||
@@ -91,7 +116,6 @@
|
||||
let p;
|
||||
switch (vizMode) {
|
||||
case "bars": {
|
||||
// Sparks rise from random bar tops
|
||||
const halfBars = Math.ceil(barCount / 2);
|
||||
const cw = canvas ? canvas.width : window.innerWidth;
|
||||
const ch = canvas ? canvas.height : window.innerHeight;
|
||||
@@ -118,7 +142,6 @@
|
||||
break;
|
||||
}
|
||||
case "wave": {
|
||||
// Particles float upward from the wave line
|
||||
const halfBars = Math.ceil(barCount / 2);
|
||||
const cw = canvas ? canvas.width : window.innerWidth;
|
||||
const ch = canvas ? canvas.height : window.innerHeight;
|
||||
@@ -142,7 +165,6 @@
|
||||
break;
|
||||
}
|
||||
case "blob": {
|
||||
// Particles swirl around the blob surface
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const halfBars = Math.ceil(barCount / 2);
|
||||
const freqIdx =
|
||||
@@ -166,7 +188,6 @@
|
||||
}
|
||||
case "circular":
|
||||
default: {
|
||||
// Radiate outward from circle
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const spawnR = baseRadius * 0.7 + Math.random() * baseRadius * 0.3;
|
||||
const speed = 1.5 + Math.random() * 5 * loudness;
|
||||
@@ -195,9 +216,7 @@
|
||||
const p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
// Gravity for bars mode (sparks fall slightly)
|
||||
if (vizMode === "bars") p.vy += 0.04;
|
||||
// Slight drag for blob mode (swirl slows)
|
||||
if (vizMode === "blob") {
|
||||
p.vx *= 0.995;
|
||||
p.vy *= 0.995;
|
||||
@@ -237,16 +256,12 @@
|
||||
function precalculateAngles() {
|
||||
const totalBars = barCount;
|
||||
const halfBars = Math.ceil(totalBars / 2);
|
||||
// Mirrored halves: right side goes bottom→top, left side mirrors
|
||||
const angleStep = Math.PI / halfBars;
|
||||
const halfStep = angleStep / 2;
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
// Right half: top → bottom
|
||||
const rightAngle = -Math.PI / 2 + halfStep + i * angleStep;
|
||||
cosValues[i] = Math.cos(rightAngle);
|
||||
sinValues[i] = Math.sin(rightAngle);
|
||||
|
||||
// Left half: top → bottom (mirrored)
|
||||
if (halfBars + i < totalBars) {
|
||||
const leftAngle = -Math.PI / 2 - halfStep - i * angleStep;
|
||||
cosValues[halfBars + i] = Math.cos(leftAngle);
|
||||
@@ -257,12 +272,9 @@
|
||||
|
||||
async function initAudio() {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// Browsers may suspend AudioContext until a user gesture resumes it
|
||||
if (audioContext.state === "suspended") {
|
||||
await audioContext.resume();
|
||||
}
|
||||
|
||||
analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 8192;
|
||||
analyser.smoothingTimeConstant = smoothing;
|
||||
@@ -272,7 +284,6 @@
|
||||
let stream;
|
||||
|
||||
if (audioSource === "file" && audioElement) {
|
||||
// Direct Web Audio pipeline — best quality, no mic needed
|
||||
audioSourceNode = audioContext.createMediaElementSource(audioElement);
|
||||
audioSourceNode.connect(analyser);
|
||||
analyser.connect(audioContext.destination);
|
||||
@@ -280,7 +291,6 @@
|
||||
isListening = true;
|
||||
return;
|
||||
} else if (audioSource === "tab") {
|
||||
// Tab audio capture via getDisplayMedia (Chrome/Edge only)
|
||||
stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { width: 1, height: 1, frameRate: 1 },
|
||||
audio: {
|
||||
@@ -290,8 +300,6 @@
|
||||
systemAudio: "include",
|
||||
},
|
||||
});
|
||||
|
||||
// Verify audio tracks exist (user must check "Share tab audio")
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
@@ -299,19 +307,12 @@
|
||||
'No audio captured. Make sure to check "Share tab audio" in the sharing dialog.',
|
||||
);
|
||||
}
|
||||
|
||||
// Stop the video track immediately — we only need audio
|
||||
stream.getVideoTracks().forEach((t) => t.stop());
|
||||
|
||||
// Handle stream ending (user clicks "Stop sharing")
|
||||
stream.addEventListener("inactive", () => stopListening());
|
||||
audioTracks.forEach((track) =>
|
||||
track.addEventListener("ended", () => stopListening()),
|
||||
);
|
||||
} else {
|
||||
// "mic" and "link" both use microphone capture
|
||||
// For "link", the user plays music in an embedded/external player
|
||||
// and the mic picks up the audio from speakers
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: false,
|
||||
@@ -327,15 +328,10 @@
|
||||
isListening = true;
|
||||
}
|
||||
|
||||
$: if (analyser) {
|
||||
analyser.smoothingTimeConstant = smoothing;
|
||||
}
|
||||
|
||||
function getProcessedData() {
|
||||
const totalBars = barCount;
|
||||
const halfBars = Math.ceil(totalBars / 2);
|
||||
const usableBins = (bufferLength * FREQ_RANGE) | 0;
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
const t = i / (halfBars - 1 || 1);
|
||||
@@ -347,16 +343,12 @@
|
||||
(dataArray[binIdx] || 0) +
|
||||
(dataArray[hi] || 0)) /
|
||||
3;
|
||||
// Normalize and apply sensitivity
|
||||
const normalized = Math.min(1, (raw * sensitivity) / 255);
|
||||
// Gentle curve to keep activity spread evenly
|
||||
const curved = Math.pow(normalized, 8);
|
||||
// Optional bass emphasis as additive boost for lower bars
|
||||
const bassBoost = (1 - t) * bassEmphasis * normalized * 0.3;
|
||||
const spiked = Math.min(1, curved + bassBoost) * 255;
|
||||
smoothedValues[i] += (spiked - smoothedValues[i]) * SMOOTH_FACTOR;
|
||||
}
|
||||
// Spatial smoothing: blend adjacent bars for a more unified shape
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
for (let i = 1; i < halfBars - 1; i++) {
|
||||
smoothedValues[i] =
|
||||
@@ -366,27 +358,17 @@
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < halfBars; i++) sum += smoothedValues[i];
|
||||
|
||||
const avgLoudness = (sum / halfBars) * INV_255;
|
||||
smoothedLoudness += (avgLoudness - smoothedLoudness) * 0.2;
|
||||
|
||||
return { halfBars, totalBars, sum };
|
||||
}
|
||||
|
||||
// Clear particles when viz mode changes to avoid stale coordinates
|
||||
let prevVizMode = vizMode;
|
||||
$: if (vizMode !== prevVizMode) {
|
||||
particles = [];
|
||||
prevVizMode = vizMode;
|
||||
}
|
||||
|
||||
function applyWrapperEffects() {
|
||||
const shakeIntensity = 12 * shakeAmount;
|
||||
const zoom = isFinite(zoomIntensity) ? zoomIntensity : 1.0;
|
||||
const targetScale =
|
||||
MIN_SCALE + smoothedLoudness * (MAX_SCALE - MIN_SCALE) * zoom;
|
||||
smoothedScale += (targetScale - smoothedScale) * SCALE_SMOOTH;
|
||||
|
||||
const shakeX = (Math.random() - 0.5) * shakeIntensity * smoothedLoudness;
|
||||
const shakeY = (Math.random() - 0.5) * shakeIntensity * smoothedLoudness;
|
||||
const bloom = 1 + smoothedLoudness * glowIntensity;
|
||||
@@ -404,14 +386,11 @@
|
||||
function drawCircular(halfBars, totalBars) {
|
||||
const maxBarHeight = size * 0.25;
|
||||
const barHeights = new Float32Array(totalBars);
|
||||
|
||||
for (let i = 0; i < totalBars; i++) {
|
||||
const dataIdx = i < halfBars ? i : i - halfBars;
|
||||
barHeights[i] =
|
||||
baseRadius + 5 + smoothedValues[dataIdx] * maxBarHeight * INV_255;
|
||||
}
|
||||
|
||||
// Build outer points for mirrored halves
|
||||
const outerPoints = [];
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
const r = barHeights[i] + LINE_LIFT;
|
||||
@@ -427,8 +406,6 @@
|
||||
y: centerY + sinValues[i] * r,
|
||||
});
|
||||
}
|
||||
|
||||
// Draw filled area from circle edge to outer line
|
||||
const len = outerPoints.length;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(outerPoints[0].x, outerPoints[0].y);
|
||||
@@ -440,12 +417,8 @@
|
||||
ctx.quadraticCurveTo(curr.x, curr.y, mx, my);
|
||||
}
|
||||
ctx.closePath();
|
||||
|
||||
// Cut out the inner circle
|
||||
ctx.moveTo(centerX + baseRadius, centerY);
|
||||
ctx.arc(centerX, centerY, baseRadius, 0, Math.PI * 2, true);
|
||||
|
||||
// Gradient fill
|
||||
const grad = ctx.createRadialGradient(
|
||||
centerX,
|
||||
centerY,
|
||||
@@ -471,7 +444,6 @@
|
||||
const maxBarHeight = canvas.height * 0.7;
|
||||
const startX = 0;
|
||||
const bottomY = canvas.height;
|
||||
|
||||
const grad = ctx.createLinearGradient(
|
||||
0,
|
||||
bottomY - maxBarHeight,
|
||||
@@ -481,13 +453,11 @@
|
||||
grad.addColorStop(0, colors.primary);
|
||||
grad.addColorStop(1, colors.secondary);
|
||||
ctx.fillStyle = grad;
|
||||
|
||||
for (let i = 0; i < totalBarsToShow; i++) {
|
||||
const val = smoothedValues[i] * INV_255;
|
||||
const h = Math.max(2, val * maxBarHeight);
|
||||
const x = startX + i * (barWidth + barGap);
|
||||
const radius = Math.min(barWidth / 2, 4);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, bottomY - h);
|
||||
ctx.lineTo(x + barWidth - radius, bottomY - h);
|
||||
@@ -511,8 +481,6 @@
|
||||
const maxAmp = canvas.height * 0.3;
|
||||
const startX = 0;
|
||||
const midY = canvas.height * 0.5;
|
||||
|
||||
// Primary wave
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, midY);
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
@@ -533,18 +501,14 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
// Close to form filled area
|
||||
ctx.lineTo(startX + totalWidth, midY);
|
||||
ctx.lineTo(startX, midY);
|
||||
ctx.closePath();
|
||||
|
||||
const grad = ctx.createLinearGradient(0, midY - maxAmp, 0, midY);
|
||||
grad.addColorStop(0, colors.primary + "cc");
|
||||
grad.addColorStop(1, colors.primary + "11");
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
// Stroke the wave line
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
const t = i / (halfBars - 1);
|
||||
@@ -560,8 +524,6 @@
|
||||
ctx.shadowBlur = 10 * glowIntensity;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Mirror wave (secondary color, below)
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
const t = i / (halfBars - 1);
|
||||
@@ -574,13 +536,11 @@
|
||||
ctx.lineTo(startX + totalWidth, midY);
|
||||
ctx.lineTo(startX, midY);
|
||||
ctx.closePath();
|
||||
|
||||
const grad2 = ctx.createLinearGradient(0, midY, 0, midY + maxAmp * 0.6);
|
||||
grad2.addColorStop(0, colors.secondary + "11");
|
||||
grad2.addColorStop(1, colors.secondary + "88");
|
||||
ctx.fillStyle = grad2;
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < halfBars; i++) {
|
||||
const t = i / (halfBars - 1);
|
||||
@@ -602,14 +562,11 @@
|
||||
blobPhase += 0.008;
|
||||
const points = 64;
|
||||
const maxDeform = size * 0.2;
|
||||
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= points; i++) {
|
||||
const angle = (i / points) * Math.PI * 2;
|
||||
// Map angle to frequency bin
|
||||
const freqIdx = Math.floor((i / points) * halfBars) % halfBars;
|
||||
const val = smoothedValues[freqIdx] * INV_255;
|
||||
// Perlin-like noise via layered sin
|
||||
const noise =
|
||||
Math.sin(angle * 3 + blobPhase) * 0.3 +
|
||||
Math.sin(angle * 5 - blobPhase * 1.3) * 0.2 +
|
||||
@@ -621,7 +578,6 @@
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
|
||||
const grad = ctx.createRadialGradient(
|
||||
centerX,
|
||||
centerY,
|
||||
@@ -638,8 +594,6 @@
|
||||
ctx.shadowBlur = 20 * glowIntensity;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Inner glow circle
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, baseRadius * 0.95, 0, Math.PI * 2);
|
||||
const innerGrad = ctx.createRadialGradient(
|
||||
@@ -658,7 +612,6 @@
|
||||
|
||||
function draw() {
|
||||
if (!ctx) return;
|
||||
|
||||
const useFullViewport = vizMode === "bars" || vizMode === "wave";
|
||||
const cw = useFullViewport
|
||||
? window.innerWidth
|
||||
@@ -671,11 +624,9 @@
|
||||
canvas.height = ch;
|
||||
}
|
||||
ctx.clearRect(0, 0, cw, ch);
|
||||
|
||||
if (analyser) {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
}
|
||||
|
||||
if (useFullViewport) {
|
||||
centerX = cw / 2;
|
||||
centerY = ch / 2;
|
||||
@@ -684,13 +635,9 @@
|
||||
centerX = offset + size / 2;
|
||||
centerY = offset + size / 2;
|
||||
}
|
||||
|
||||
const { halfBars, totalBars } = getProcessedData();
|
||||
|
||||
applyWrapperEffects();
|
||||
spawnParticles(smoothedLoudness);
|
||||
|
||||
// Draw based on selected mode
|
||||
switch (vizMode) {
|
||||
case "bars":
|
||||
drawBars(halfBars);
|
||||
@@ -706,10 +653,7 @@
|
||||
drawCircular(halfBars, totalBars);
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw particles on top
|
||||
updateAndDrawParticles();
|
||||
|
||||
animationId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
@@ -725,7 +669,6 @@
|
||||
} catch (err) {
|
||||
console.error("Failed to start audio:", err);
|
||||
alert(err.message || "Failed to start audio. Check browser permissions.");
|
||||
// Clean up partial state
|
||||
if (audioContext) {
|
||||
audioContext.close().catch(() => {});
|
||||
audioContext = null;
|
||||
@@ -754,11 +697,6 @@
|
||||
draw();
|
||||
}
|
||||
|
||||
export let fileCurrentTime = 0;
|
||||
export let fileDuration = 0;
|
||||
export let filePlaying = false;
|
||||
export let fileName = "";
|
||||
|
||||
export function setAudioFile(file) {
|
||||
if (audioElement) {
|
||||
audioElement.pause();
|
||||
@@ -772,7 +710,6 @@
|
||||
filePlaying = false;
|
||||
fileCurrentTime = 0;
|
||||
fileDuration = 0;
|
||||
|
||||
audioElement.addEventListener("loadedmetadata", () => {
|
||||
fileDuration = audioElement.duration;
|
||||
});
|
||||
@@ -788,21 +725,15 @@
|
||||
}
|
||||
|
||||
export function pauseAudio() {
|
||||
if (audioElement) {
|
||||
audioElement.pause();
|
||||
}
|
||||
if (audioElement) audioElement.pause();
|
||||
}
|
||||
|
||||
export function resumeAudio() {
|
||||
if (audioElement) {
|
||||
audioElement.play();
|
||||
}
|
||||
if (audioElement) audioElement.play();
|
||||
}
|
||||
|
||||
export function seekAudio(time) {
|
||||
if (audioElement) {
|
||||
audioElement.currentTime = time;
|
||||
}
|
||||
if (audioElement) audioElement.currentTime = time;
|
||||
}
|
||||
|
||||
export function takeScreenshot() {
|
||||
@@ -852,7 +783,6 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.visualizer-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -862,11 +792,9 @@
|
||||
will-change: transform;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
@@ -882,17 +810,14 @@
|
||||
overflow: hidden;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.logo-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.logo-container.spinning {
|
||||
animation: logo-spin var(--spin-duration, 4s) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes -global-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 835 KiB After Width: | Height: | Size: 835 KiB |
1
src/lib/assets/favicon.svg
Normal file
1
src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -1,7 +0,0 @@
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')
|
||||
});
|
||||
|
||||
export default app;
|
||||
7
src/routes/+layout.svelte
Normal file
7
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import "./layout.css";
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
530
src/routes/+page.svelte
Normal file
530
src/routes/+page.svelte
Normal file
@@ -0,0 +1,530 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import AudioVisualizer from "$lib/AudioVisualizer.svelte";
|
||||
import Controls from "$lib/Controls.svelte";
|
||||
|
||||
const STORAGE_KEY = "ncs-visualizer-settings";
|
||||
const THEMES_KEY = "ncs-visualizer-themes";
|
||||
|
||||
const defaults = {
|
||||
selectedPreset: "cyanPink",
|
||||
logoUrl: "",
|
||||
title: "Audio Visualizer",
|
||||
subtitle: "Now Playing",
|
||||
bgImage: "",
|
||||
bgColor: "#0a0a0a",
|
||||
customPrimary: "#00d4ff",
|
||||
customSecondary: "#ff006e",
|
||||
useCustomColors: false,
|
||||
sidebarOpen: true,
|
||||
vignette: true,
|
||||
audioSource: "mic",
|
||||
vizMode: "circular",
|
||||
barCount: 48,
|
||||
sensitivity: 1.0,
|
||||
smoothing: 0.82,
|
||||
bassEmphasis: 0.25,
|
||||
particlesEnabled: true,
|
||||
particleDensity: 1.0,
|
||||
shakeAmount: 1.0,
|
||||
zoomIntensity: 1.0,
|
||||
glowIntensity: 0.6,
|
||||
logoSpin: false,
|
||||
logoSpinSpeed: 5,
|
||||
particleImage: "",
|
||||
particleSize: 1.0,
|
||||
particleRotation: 0,
|
||||
titlePosition: "top",
|
||||
};
|
||||
|
||||
function loadSettings() {
|
||||
if (!browser) return { ...defaults };
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return raw ? { ...defaults, ...JSON.parse(raw) } : { ...defaults };
|
||||
} catch {
|
||||
return { ...defaults };
|
||||
}
|
||||
}
|
||||
|
||||
function loadThemes() {
|
||||
if (!browser) return [];
|
||||
try {
|
||||
const raw = localStorage.getItem(THEMES_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const saved = loadSettings();
|
||||
|
||||
let selectedPreset = $state(saved.selectedPreset);
|
||||
let logoUrl = $state(saved.logoUrl);
|
||||
let title = $state(saved.title);
|
||||
let subtitle = $state(saved.subtitle);
|
||||
let bgImage = $state(saved.bgImage);
|
||||
let bgColor = $state(saved.bgColor);
|
||||
let customPrimary = $state(saved.customPrimary);
|
||||
let customSecondary = $state(saved.customSecondary);
|
||||
let useCustomColors = $state(saved.useCustomColors);
|
||||
let isListening = $state(false);
|
||||
let sidebarOpen = $state(saved.sidebarOpen);
|
||||
let vignette = $state(saved.vignette);
|
||||
let audioSource = $state(saved.audioSource);
|
||||
let vizMode = $state(saved.vizMode);
|
||||
let barCount = $state(saved.barCount);
|
||||
let sensitivity = $state(saved.sensitivity);
|
||||
let smoothing = $state(saved.smoothing);
|
||||
let bassEmphasis = $state(saved.bassEmphasis);
|
||||
let particlesEnabled = $state(saved.particlesEnabled);
|
||||
let particleDensity = $state(saved.particleDensity);
|
||||
let shakeAmount = $state(saved.shakeAmount);
|
||||
let zoomIntensity = $state(saved.zoomIntensity);
|
||||
let glowIntensity = $state(saved.glowIntensity);
|
||||
let logoSpin = $state(saved.logoSpin);
|
||||
let logoSpinSpeed = $state(saved.logoSpinSpeed);
|
||||
let particleImage = $state(saved.particleImage);
|
||||
let particleSize = $state(saved.particleSize);
|
||||
let particleRotation = $state(saved.particleRotation);
|
||||
let titlePosition = $state(saved.titlePosition);
|
||||
let savedThemes = $state(loadThemes());
|
||||
let visualizerComponent = $state(null);
|
||||
let toggleHidden = $state(false);
|
||||
let hideTimer;
|
||||
|
||||
let fileCurrentTime = $state(0);
|
||||
let fileDuration = $state(0);
|
||||
let filePlaying = $state(false);
|
||||
let fileName = $state("");
|
||||
|
||||
function startHideTimer() {
|
||||
clearTimeout(hideTimer);
|
||||
if (!sidebarOpen) {
|
||||
hideTimer = setTimeout(() => {
|
||||
toggleHidden = true;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleClick() {
|
||||
sidebarOpen = !sidebarOpen;
|
||||
toggleHidden = false;
|
||||
clearTimeout(hideTimer);
|
||||
if (!sidebarOpen) startHideTimer();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!sidebarOpen) {
|
||||
startHideTimer();
|
||||
} else {
|
||||
toggleHidden = false;
|
||||
clearTimeout(hideTimer);
|
||||
}
|
||||
});
|
||||
|
||||
// Persist settings
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const settings = {
|
||||
selectedPreset,
|
||||
logoUrl,
|
||||
title,
|
||||
subtitle,
|
||||
bgImage,
|
||||
bgColor,
|
||||
customPrimary,
|
||||
customSecondary,
|
||||
useCustomColors,
|
||||
sidebarOpen,
|
||||
vignette,
|
||||
audioSource,
|
||||
vizMode,
|
||||
barCount,
|
||||
sensitivity,
|
||||
smoothing,
|
||||
bassEmphasis,
|
||||
particlesEnabled,
|
||||
particleDensity,
|
||||
shakeAmount,
|
||||
zoomIntensity,
|
||||
glowIntensity,
|
||||
logoSpin,
|
||||
logoSpinSpeed,
|
||||
particleImage,
|
||||
particleSize,
|
||||
particleRotation,
|
||||
titlePosition,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch {}
|
||||
});
|
||||
|
||||
function handleThemesChanged(themes) {
|
||||
savedThemes = themes;
|
||||
if (browser) {
|
||||
try {
|
||||
localStorage.setItem(THEMES_KEY, JSON.stringify(savedThemes));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleListening() {
|
||||
if (isListening) {
|
||||
visualizerComponent.stopListening();
|
||||
} else {
|
||||
visualizerComponent.startListening();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScreenshot() {
|
||||
const dataUrl = visualizerComponent.takeScreenshot();
|
||||
if (!dataUrl) return;
|
||||
const link = document.createElement("a");
|
||||
link.download = "visualizer-screenshot.png";
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
}
|
||||
|
||||
function handleFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(() => {});
|
||||
} else {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
selectedPreset = defaults.selectedPreset;
|
||||
logoUrl = defaults.logoUrl;
|
||||
title = defaults.title;
|
||||
subtitle = defaults.subtitle;
|
||||
bgImage = defaults.bgImage;
|
||||
bgColor = defaults.bgColor;
|
||||
customPrimary = defaults.customPrimary;
|
||||
customSecondary = defaults.customSecondary;
|
||||
useCustomColors = defaults.useCustomColors;
|
||||
vignette = defaults.vignette;
|
||||
audioSource = defaults.audioSource;
|
||||
vizMode = defaults.vizMode;
|
||||
barCount = defaults.barCount;
|
||||
sensitivity = defaults.sensitivity;
|
||||
smoothing = defaults.smoothing;
|
||||
bassEmphasis = defaults.bassEmphasis;
|
||||
particlesEnabled = defaults.particlesEnabled;
|
||||
particleDensity = defaults.particleDensity;
|
||||
shakeAmount = defaults.shakeAmount;
|
||||
zoomIntensity = defaults.zoomIntensity;
|
||||
glowIntensity = defaults.glowIntensity;
|
||||
logoSpin = defaults.logoSpin;
|
||||
logoSpinSpeed = defaults.logoSpinSpeed;
|
||||
particleImage = defaults.particleImage;
|
||||
particleSize = defaults.particleSize;
|
||||
particleRotation = defaults.particleRotation;
|
||||
titlePosition = defaults.titlePosition;
|
||||
}
|
||||
|
||||
function handleAudioFile(file) {
|
||||
if (file && visualizerComponent) {
|
||||
if (isListening) visualizerComponent.stopListening();
|
||||
audioSource = "file";
|
||||
visualizerComponent.setAudioFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilePause() {
|
||||
if (visualizerComponent) visualizerComponent.pauseAudio();
|
||||
}
|
||||
function handleFileResume() {
|
||||
if (visualizerComponent) visualizerComponent.resumeAudio();
|
||||
}
|
||||
function handleFileSeek(time) {
|
||||
if (visualizerComponent) visualizerComponent.seekAudio(time);
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (
|
||||
e.target.tagName === "INPUT" ||
|
||||
e.target.tagName === "TEXTAREA" ||
|
||||
e.target.isContentEditable
|
||||
)
|
||||
return;
|
||||
switch (e.code) {
|
||||
case "Space":
|
||||
e.preventDefault();
|
||||
toggleListening();
|
||||
break;
|
||||
case "KeyF":
|
||||
e.preventDefault();
|
||||
handleFullscreen();
|
||||
break;
|
||||
case "KeyH":
|
||||
e.preventDefault();
|
||||
handleToggleClick();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
});
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
});
|
||||
|
||||
const colorPresets = {
|
||||
cyanPink: {
|
||||
name: "Cyan/Pink",
|
||||
primary: "#00d4ff",
|
||||
secondary: "#ff006e",
|
||||
},
|
||||
purpleOrange: {
|
||||
name: "Purple/Orange",
|
||||
primary: "#8b5cf6",
|
||||
secondary: "#f97316",
|
||||
},
|
||||
greenYellow: {
|
||||
name: "Green/Yellow",
|
||||
primary: "#22c55e",
|
||||
secondary: "#eab308",
|
||||
},
|
||||
pinkBlue: {
|
||||
name: "Pink/Blue",
|
||||
primary: "#ec4899",
|
||||
secondary: "#3b82f6",
|
||||
},
|
||||
redOrange: {
|
||||
name: "Red/Orange",
|
||||
primary: "#ef4444",
|
||||
secondary: "#f97316",
|
||||
},
|
||||
white: { name: "White", primary: "#ffffff", secondary: "#888888" },
|
||||
};
|
||||
|
||||
let currentColors = $derived(
|
||||
useCustomColors
|
||||
? { primary: customPrimary, secondary: customSecondary }
|
||||
: colorPresets[selectedPreset],
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<main
|
||||
class="relative flex w-screen h-screen overflow-hidden select-none"
|
||||
style="background: {bgColor}"
|
||||
>
|
||||
{#if bgImage}
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center z-0"
|
||||
style="background-image: url({bgImage})"
|
||||
></div>
|
||||
{/if}
|
||||
{#if vignette}
|
||||
<div
|
||||
class="absolute inset-0 z-0"
|
||||
style="background: radial-gradient(ellipse at center, rgba(0,0,0,0.5) 0%, rgba(0,0,0,1) 100%)"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<div class="absolute inset-0 flex items-center justify-center z-[1]">
|
||||
<AudioVisualizer
|
||||
bind:this={visualizerComponent}
|
||||
colors={currentColors}
|
||||
{logoUrl}
|
||||
{audioSource}
|
||||
{vizMode}
|
||||
{barCount}
|
||||
{sensitivity}
|
||||
{smoothing}
|
||||
{bassEmphasis}
|
||||
{particlesEnabled}
|
||||
{particleDensity}
|
||||
{shakeAmount}
|
||||
{zoomIntensity}
|
||||
{glowIntensity}
|
||||
{logoSpin}
|
||||
{logoSpinSpeed}
|
||||
{particleImage}
|
||||
{particleSize}
|
||||
{particleRotation}
|
||||
bind:isListening
|
||||
bind:fileCurrentTime
|
||||
bind:fileDuration
|
||||
bind:filePlaying
|
||||
bind:fileName
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Title overlay -->
|
||||
<div class="title-overlay title-{titlePosition}">
|
||||
<h1
|
||||
class="text-[clamp(1.4rem,3.5vw,3rem)] font-light tracking-[0.2em] uppercase m-0"
|
||||
style="color: {currentColors.primary}"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<h2
|
||||
class="text-[clamp(1rem,2vw,1.8rem)] font-light tracking-[0.15em] mt-1 opacity-70 m-0"
|
||||
style="color: {currentColors.secondary}"
|
||||
>
|
||||
{subtitle}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar toggle -->
|
||||
<div
|
||||
role="region"
|
||||
aria-label="Sidebar toggle"
|
||||
class="fixed top-0 right-0 w-[50px] h-screen z-30 transition-opacity duration-500 {toggleHidden
|
||||
? 'opacity-0 hover:opacity-100'
|
||||
: 'opacity-100'}"
|
||||
onmouseenter={() => {
|
||||
toggleHidden = false;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="absolute top-1/2 -translate-y-1/2 bg-white/[0.08] border border-white/15 border-r-0 rounded-l-lg text-white py-3 px-1.5 cursor-pointer transition-all duration-300 hover:bg-white/15
|
||||
{sidebarOpen ? 'right-[320px]' : 'right-0'}"
|
||||
onclick={handleToggleClick}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
{#if sidebarOpen}
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
{:else}
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="fixed top-0 w-[320px] h-screen bg-[rgba(10,10,15,0.92)] backdrop-blur-xl border-l border-white/10 z-20 overflow-y-auto transition-all duration-300 p-5 box-border
|
||||
{sidebarOpen ? 'right-0' : '-right-[320px]'}"
|
||||
>
|
||||
<Controls
|
||||
bind:selectedPreset
|
||||
bind:logoUrl
|
||||
bind:title
|
||||
bind:subtitle
|
||||
bind:bgImage
|
||||
bind:bgColor
|
||||
bind:vignette
|
||||
bind:audioSource
|
||||
bind:customPrimary
|
||||
bind:customSecondary
|
||||
bind:useCustomColors
|
||||
bind:vizMode
|
||||
bind:barCount
|
||||
bind:sensitivity
|
||||
bind:smoothing
|
||||
bind:bassEmphasis
|
||||
bind:particlesEnabled
|
||||
bind:particleDensity
|
||||
bind:shakeAmount
|
||||
bind:zoomIntensity
|
||||
bind:glowIntensity
|
||||
bind:logoSpin
|
||||
bind:logoSpinSpeed
|
||||
bind:particleImage
|
||||
bind:particleSize
|
||||
bind:particleRotation
|
||||
bind:titlePosition
|
||||
bind:savedThemes
|
||||
{isListening}
|
||||
{colorPresets}
|
||||
onToggle={toggleListening}
|
||||
onscreenshot={handleScreenshot}
|
||||
onfullscreen={handleFullscreen}
|
||||
onreset={handleReset}
|
||||
onaudiofile={handleAudioFile}
|
||||
onfilepause={handleFilePause}
|
||||
onfileresume={handleFileResume}
|
||||
onfileseek={handleFileSeek}
|
||||
onthemeschanged={handleThemesChanged}
|
||||
{fileCurrentTime}
|
||||
{fileDuration}
|
||||
{filePlaying}
|
||||
{fileName}
|
||||
/>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.title-overlay {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
padding: 1.5rem 2.5rem;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0.7) 0%,
|
||||
rgba(0, 0, 0, 0.4) 50%,
|
||||
transparent 80%
|
||||
);
|
||||
text-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 1),
|
||||
0 0 30px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
.title-top {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.title-bottom {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.title-center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.title-top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.title-top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.title-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.title-bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.title-left {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
text-align: left;
|
||||
}
|
||||
.title-right {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
2
src/routes/+page.ts
Normal file
2
src/routes/+page.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
3
src/routes/layout.css
Normal file
3
src/routes/layout.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/forms';
|
||||
@plugin '@tailwindcss/typography';
|
||||
3
static/robots.txt
Normal file
3
static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
13
svelte.config.js
Normal file
13
svelte.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()]
|
||||
});
|
||||
12
vite.config.ts
Normal file
12
vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { paraglideVitePlugin } from '@inlang/paraglide-js';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
sveltekit(),
|
||||
paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' })
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user