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
|
# Output
|
||||||
dist/
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
# Vite
|
# OS
|
||||||
.vite/
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea/
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Logs
|
# Env
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.*
|
||||||
.env.*.local
|
!.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",
|
"name": "audio-visualizer",
|
||||||
"version": "1.0.0",
|
"private": true,
|
||||||
"private": true,
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
},
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"devDependencies": {
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
"svelte": "^4.2.12",
|
},
|
||||||
"vite": "^5.2.0"
|
"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>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
|
||||||
export let colors = { primary: "#00d4ff", secondary: "#ff006e" };
|
let {
|
||||||
export let logoUrl = "";
|
colors = { primary: "#00d4ff", secondary: "#ff006e" },
|
||||||
export let isListening = false;
|
logoUrl = "",
|
||||||
export let audioSource = "mic"; // "mic", "tab", "link", "file"
|
isListening = $bindable(false),
|
||||||
|
audioSource = "mic",
|
||||||
// Visualization mode: "circular", "bars", "wave", "blob"
|
vizMode = "circular",
|
||||||
export let vizMode = "circular";
|
barCount = 48,
|
||||||
export let barCount = 48;
|
sensitivity = 1.0,
|
||||||
export let sensitivity = 1.0;
|
smoothing = 0.82,
|
||||||
export let smoothing = 0.82;
|
bassEmphasis = 0.25,
|
||||||
export let bassEmphasis = 0.25;
|
particlesEnabled = true,
|
||||||
export let particlesEnabled = true;
|
particleDensity = 1.0,
|
||||||
export let particleDensity = 1.0;
|
shakeAmount = 1.0,
|
||||||
export let shakeAmount = 1.0;
|
zoomIntensity = 1.0,
|
||||||
export let zoomIntensity = 1.0;
|
glowIntensity = 0.6,
|
||||||
export let glowIntensity = 0.6;
|
logoSpin = false,
|
||||||
export let logoSpin = false;
|
logoSpinSpeed = 5,
|
||||||
export let logoSpinSpeed = 5;
|
particleImage = "",
|
||||||
export let particleImage = "";
|
particleSize = 1.0,
|
||||||
export let particleSize = 1.0;
|
particleRotation = 0,
|
||||||
export let particleRotation = 0;
|
fileCurrentTime = $bindable(0),
|
||||||
|
fileDuration = $bindable(0),
|
||||||
|
filePlaying = $bindable(false),
|
||||||
|
fileName = $bindable(""),
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const DEFAULT_SIZE = 250;
|
const DEFAULT_SIZE = 250;
|
||||||
const LINE_LIFT = 8;
|
const LINE_LIFT = 8;
|
||||||
@@ -45,10 +49,10 @@
|
|||||||
let dataArray = new Uint8Array(128);
|
let dataArray = new Uint8Array(128);
|
||||||
let bufferLength = 128;
|
let bufferLength = 128;
|
||||||
|
|
||||||
let size = DEFAULT_SIZE;
|
let size = $state(DEFAULT_SIZE);
|
||||||
let centerX = DEFAULT_SIZE / 2;
|
let centerX = DEFAULT_SIZE / 2;
|
||||||
let centerY = 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 cosValues = new Float32Array(barCount);
|
||||||
let sinValues = new Float32Array(barCount);
|
let sinValues = new Float32Array(barCount);
|
||||||
@@ -59,26 +63,47 @@
|
|||||||
let blobPhase = 0;
|
let blobPhase = 0;
|
||||||
|
|
||||||
// Particle image cache
|
// Particle image cache
|
||||||
let pImg = null;
|
let pImg = $state(null);
|
||||||
$: if (particleImage) {
|
$effect(() => {
|
||||||
const img = new Image();
|
if (particleImage) {
|
||||||
img.src = particleImage;
|
const img = new Image();
|
||||||
pImg = img;
|
img.src = particleImage;
|
||||||
} else {
|
pImg = img;
|
||||||
pImg = null;
|
} else {
|
||||||
}
|
pImg = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Particle system
|
// Particle system
|
||||||
let particles = [];
|
let particles = [];
|
||||||
|
|
||||||
$: {
|
// Recalculate angles when barCount changes
|
||||||
|
let prevBarCount = barCount;
|
||||||
|
$effect(() => {
|
||||||
const totalBars = barCount;
|
const totalBars = barCount;
|
||||||
const halfBars = Math.ceil(totalBars / 2);
|
const halfBars = Math.ceil(totalBars / 2);
|
||||||
cosValues = new Float32Array(totalBars);
|
cosValues = new Float32Array(totalBars);
|
||||||
sinValues = new Float32Array(totalBars);
|
sinValues = new Float32Array(totalBars);
|
||||||
smoothedValues = new Float32Array(halfBars);
|
smoothedValues = new Float32Array(halfBars);
|
||||||
precalculateAngles();
|
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) {
|
function spawnParticles(loudness) {
|
||||||
if (!particlesEnabled) return;
|
if (!particlesEnabled) return;
|
||||||
@@ -91,7 +116,6 @@
|
|||||||
let p;
|
let p;
|
||||||
switch (vizMode) {
|
switch (vizMode) {
|
||||||
case "bars": {
|
case "bars": {
|
||||||
// Sparks rise from random bar tops
|
|
||||||
const halfBars = Math.ceil(barCount / 2);
|
const halfBars = Math.ceil(barCount / 2);
|
||||||
const cw = canvas ? canvas.width : window.innerWidth;
|
const cw = canvas ? canvas.width : window.innerWidth;
|
||||||
const ch = canvas ? canvas.height : window.innerHeight;
|
const ch = canvas ? canvas.height : window.innerHeight;
|
||||||
@@ -118,7 +142,6 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "wave": {
|
case "wave": {
|
||||||
// Particles float upward from the wave line
|
|
||||||
const halfBars = Math.ceil(barCount / 2);
|
const halfBars = Math.ceil(barCount / 2);
|
||||||
const cw = canvas ? canvas.width : window.innerWidth;
|
const cw = canvas ? canvas.width : window.innerWidth;
|
||||||
const ch = canvas ? canvas.height : window.innerHeight;
|
const ch = canvas ? canvas.height : window.innerHeight;
|
||||||
@@ -142,7 +165,6 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "blob": {
|
case "blob": {
|
||||||
// Particles swirl around the blob surface
|
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
const halfBars = Math.ceil(barCount / 2);
|
const halfBars = Math.ceil(barCount / 2);
|
||||||
const freqIdx =
|
const freqIdx =
|
||||||
@@ -166,7 +188,6 @@
|
|||||||
}
|
}
|
||||||
case "circular":
|
case "circular":
|
||||||
default: {
|
default: {
|
||||||
// Radiate outward from circle
|
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
const spawnR = baseRadius * 0.7 + Math.random() * baseRadius * 0.3;
|
const spawnR = baseRadius * 0.7 + Math.random() * baseRadius * 0.3;
|
||||||
const speed = 1.5 + Math.random() * 5 * loudness;
|
const speed = 1.5 + Math.random() * 5 * loudness;
|
||||||
@@ -195,9 +216,7 @@
|
|||||||
const p = particles[i];
|
const p = particles[i];
|
||||||
p.x += p.vx;
|
p.x += p.vx;
|
||||||
p.y += p.vy;
|
p.y += p.vy;
|
||||||
// Gravity for bars mode (sparks fall slightly)
|
|
||||||
if (vizMode === "bars") p.vy += 0.04;
|
if (vizMode === "bars") p.vy += 0.04;
|
||||||
// Slight drag for blob mode (swirl slows)
|
|
||||||
if (vizMode === "blob") {
|
if (vizMode === "blob") {
|
||||||
p.vx *= 0.995;
|
p.vx *= 0.995;
|
||||||
p.vy *= 0.995;
|
p.vy *= 0.995;
|
||||||
@@ -237,16 +256,12 @@
|
|||||||
function precalculateAngles() {
|
function precalculateAngles() {
|
||||||
const totalBars = barCount;
|
const totalBars = barCount;
|
||||||
const halfBars = Math.ceil(totalBars / 2);
|
const halfBars = Math.ceil(totalBars / 2);
|
||||||
// Mirrored halves: right side goes bottom→top, left side mirrors
|
|
||||||
const angleStep = Math.PI / halfBars;
|
const angleStep = Math.PI / halfBars;
|
||||||
const halfStep = angleStep / 2;
|
const halfStep = angleStep / 2;
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
// Right half: top → bottom
|
|
||||||
const rightAngle = -Math.PI / 2 + halfStep + i * angleStep;
|
const rightAngle = -Math.PI / 2 + halfStep + i * angleStep;
|
||||||
cosValues[i] = Math.cos(rightAngle);
|
cosValues[i] = Math.cos(rightAngle);
|
||||||
sinValues[i] = Math.sin(rightAngle);
|
sinValues[i] = Math.sin(rightAngle);
|
||||||
|
|
||||||
// Left half: top → bottom (mirrored)
|
|
||||||
if (halfBars + i < totalBars) {
|
if (halfBars + i < totalBars) {
|
||||||
const leftAngle = -Math.PI / 2 - halfStep - i * angleStep;
|
const leftAngle = -Math.PI / 2 - halfStep - i * angleStep;
|
||||||
cosValues[halfBars + i] = Math.cos(leftAngle);
|
cosValues[halfBars + i] = Math.cos(leftAngle);
|
||||||
@@ -257,12 +272,9 @@
|
|||||||
|
|
||||||
async function initAudio() {
|
async function initAudio() {
|
||||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
|
||||||
// Browsers may suspend AudioContext until a user gesture resumes it
|
|
||||||
if (audioContext.state === "suspended") {
|
if (audioContext.state === "suspended") {
|
||||||
await audioContext.resume();
|
await audioContext.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
analyser = audioContext.createAnalyser();
|
analyser = audioContext.createAnalyser();
|
||||||
analyser.fftSize = 8192;
|
analyser.fftSize = 8192;
|
||||||
analyser.smoothingTimeConstant = smoothing;
|
analyser.smoothingTimeConstant = smoothing;
|
||||||
@@ -272,7 +284,6 @@
|
|||||||
let stream;
|
let stream;
|
||||||
|
|
||||||
if (audioSource === "file" && audioElement) {
|
if (audioSource === "file" && audioElement) {
|
||||||
// Direct Web Audio pipeline — best quality, no mic needed
|
|
||||||
audioSourceNode = audioContext.createMediaElementSource(audioElement);
|
audioSourceNode = audioContext.createMediaElementSource(audioElement);
|
||||||
audioSourceNode.connect(analyser);
|
audioSourceNode.connect(analyser);
|
||||||
analyser.connect(audioContext.destination);
|
analyser.connect(audioContext.destination);
|
||||||
@@ -280,7 +291,6 @@
|
|||||||
isListening = true;
|
isListening = true;
|
||||||
return;
|
return;
|
||||||
} else if (audioSource === "tab") {
|
} else if (audioSource === "tab") {
|
||||||
// Tab audio capture via getDisplayMedia (Chrome/Edge only)
|
|
||||||
stream = await navigator.mediaDevices.getDisplayMedia({
|
stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
video: { width: 1, height: 1, frameRate: 1 },
|
video: { width: 1, height: 1, frameRate: 1 },
|
||||||
audio: {
|
audio: {
|
||||||
@@ -290,8 +300,6 @@
|
|||||||
systemAudio: "include",
|
systemAudio: "include",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify audio tracks exist (user must check "Share tab audio")
|
|
||||||
const audioTracks = stream.getAudioTracks();
|
const audioTracks = stream.getAudioTracks();
|
||||||
if (audioTracks.length === 0) {
|
if (audioTracks.length === 0) {
|
||||||
stream.getTracks().forEach((t) => t.stop());
|
stream.getTracks().forEach((t) => t.stop());
|
||||||
@@ -299,19 +307,12 @@
|
|||||||
'No audio captured. Make sure to check "Share tab audio" in the sharing dialog.',
|
'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());
|
stream.getVideoTracks().forEach((t) => t.stop());
|
||||||
|
|
||||||
// Handle stream ending (user clicks "Stop sharing")
|
|
||||||
stream.addEventListener("inactive", () => stopListening());
|
stream.addEventListener("inactive", () => stopListening());
|
||||||
audioTracks.forEach((track) =>
|
audioTracks.forEach((track) =>
|
||||||
track.addEventListener("ended", () => stopListening()),
|
track.addEventListener("ended", () => stopListening()),
|
||||||
);
|
);
|
||||||
} else {
|
} 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({
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
echoCancellation: false,
|
echoCancellation: false,
|
||||||
@@ -327,15 +328,10 @@
|
|||||||
isListening = true;
|
isListening = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (analyser) {
|
|
||||||
analyser.smoothingTimeConstant = smoothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProcessedData() {
|
function getProcessedData() {
|
||||||
const totalBars = barCount;
|
const totalBars = barCount;
|
||||||
const halfBars = Math.ceil(totalBars / 2);
|
const halfBars = Math.ceil(totalBars / 2);
|
||||||
const usableBins = (bufferLength * FREQ_RANGE) | 0;
|
const usableBins = (bufferLength * FREQ_RANGE) | 0;
|
||||||
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
const t = i / (halfBars - 1 || 1);
|
const t = i / (halfBars - 1 || 1);
|
||||||
@@ -347,16 +343,12 @@
|
|||||||
(dataArray[binIdx] || 0) +
|
(dataArray[binIdx] || 0) +
|
||||||
(dataArray[hi] || 0)) /
|
(dataArray[hi] || 0)) /
|
||||||
3;
|
3;
|
||||||
// Normalize and apply sensitivity
|
|
||||||
const normalized = Math.min(1, (raw * sensitivity) / 255);
|
const normalized = Math.min(1, (raw * sensitivity) / 255);
|
||||||
// Gentle curve to keep activity spread evenly
|
|
||||||
const curved = Math.pow(normalized, 8);
|
const curved = Math.pow(normalized, 8);
|
||||||
// Optional bass emphasis as additive boost for lower bars
|
|
||||||
const bassBoost = (1 - t) * bassEmphasis * normalized * 0.3;
|
const bassBoost = (1 - t) * bassEmphasis * normalized * 0.3;
|
||||||
const spiked = Math.min(1, curved + bassBoost) * 255;
|
const spiked = Math.min(1, curved + bassBoost) * 255;
|
||||||
smoothedValues[i] += (spiked - smoothedValues[i]) * SMOOTH_FACTOR;
|
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 pass = 0; pass < 2; pass++) {
|
||||||
for (let i = 1; i < halfBars - 1; i++) {
|
for (let i = 1; i < halfBars - 1; i++) {
|
||||||
smoothedValues[i] =
|
smoothedValues[i] =
|
||||||
@@ -366,27 +358,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < halfBars; i++) sum += smoothedValues[i];
|
for (let i = 0; i < halfBars; i++) sum += smoothedValues[i];
|
||||||
|
|
||||||
const avgLoudness = (sum / halfBars) * INV_255;
|
const avgLoudness = (sum / halfBars) * INV_255;
|
||||||
smoothedLoudness += (avgLoudness - smoothedLoudness) * 0.2;
|
smoothedLoudness += (avgLoudness - smoothedLoudness) * 0.2;
|
||||||
|
|
||||||
return { halfBars, totalBars, sum };
|
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() {
|
function applyWrapperEffects() {
|
||||||
const shakeIntensity = 12 * shakeAmount;
|
const shakeIntensity = 12 * shakeAmount;
|
||||||
const zoom = isFinite(zoomIntensity) ? zoomIntensity : 1.0;
|
const zoom = isFinite(zoomIntensity) ? zoomIntensity : 1.0;
|
||||||
const targetScale =
|
const targetScale =
|
||||||
MIN_SCALE + smoothedLoudness * (MAX_SCALE - MIN_SCALE) * zoom;
|
MIN_SCALE + smoothedLoudness * (MAX_SCALE - MIN_SCALE) * zoom;
|
||||||
smoothedScale += (targetScale - smoothedScale) * SCALE_SMOOTH;
|
smoothedScale += (targetScale - smoothedScale) * SCALE_SMOOTH;
|
||||||
|
|
||||||
const shakeX = (Math.random() - 0.5) * shakeIntensity * smoothedLoudness;
|
const shakeX = (Math.random() - 0.5) * shakeIntensity * smoothedLoudness;
|
||||||
const shakeY = (Math.random() - 0.5) * shakeIntensity * smoothedLoudness;
|
const shakeY = (Math.random() - 0.5) * shakeIntensity * smoothedLoudness;
|
||||||
const bloom = 1 + smoothedLoudness * glowIntensity;
|
const bloom = 1 + smoothedLoudness * glowIntensity;
|
||||||
@@ -404,14 +386,11 @@
|
|||||||
function drawCircular(halfBars, totalBars) {
|
function drawCircular(halfBars, totalBars) {
|
||||||
const maxBarHeight = size * 0.25;
|
const maxBarHeight = size * 0.25;
|
||||||
const barHeights = new Float32Array(totalBars);
|
const barHeights = new Float32Array(totalBars);
|
||||||
|
|
||||||
for (let i = 0; i < totalBars; i++) {
|
for (let i = 0; i < totalBars; i++) {
|
||||||
const dataIdx = i < halfBars ? i : i - halfBars;
|
const dataIdx = i < halfBars ? i : i - halfBars;
|
||||||
barHeights[i] =
|
barHeights[i] =
|
||||||
baseRadius + 5 + smoothedValues[dataIdx] * maxBarHeight * INV_255;
|
baseRadius + 5 + smoothedValues[dataIdx] * maxBarHeight * INV_255;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build outer points for mirrored halves
|
|
||||||
const outerPoints = [];
|
const outerPoints = [];
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
const r = barHeights[i] + LINE_LIFT;
|
const r = barHeights[i] + LINE_LIFT;
|
||||||
@@ -427,8 +406,6 @@
|
|||||||
y: centerY + sinValues[i] * r,
|
y: centerY + sinValues[i] * r,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw filled area from circle edge to outer line
|
|
||||||
const len = outerPoints.length;
|
const len = outerPoints.length;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(outerPoints[0].x, outerPoints[0].y);
|
ctx.moveTo(outerPoints[0].x, outerPoints[0].y);
|
||||||
@@ -440,12 +417,8 @@
|
|||||||
ctx.quadraticCurveTo(curr.x, curr.y, mx, my);
|
ctx.quadraticCurveTo(curr.x, curr.y, mx, my);
|
||||||
}
|
}
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
// Cut out the inner circle
|
|
||||||
ctx.moveTo(centerX + baseRadius, centerY);
|
ctx.moveTo(centerX + baseRadius, centerY);
|
||||||
ctx.arc(centerX, centerY, baseRadius, 0, Math.PI * 2, true);
|
ctx.arc(centerX, centerY, baseRadius, 0, Math.PI * 2, true);
|
||||||
|
|
||||||
// Gradient fill
|
|
||||||
const grad = ctx.createRadialGradient(
|
const grad = ctx.createRadialGradient(
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
@@ -471,7 +444,6 @@
|
|||||||
const maxBarHeight = canvas.height * 0.7;
|
const maxBarHeight = canvas.height * 0.7;
|
||||||
const startX = 0;
|
const startX = 0;
|
||||||
const bottomY = canvas.height;
|
const bottomY = canvas.height;
|
||||||
|
|
||||||
const grad = ctx.createLinearGradient(
|
const grad = ctx.createLinearGradient(
|
||||||
0,
|
0,
|
||||||
bottomY - maxBarHeight,
|
bottomY - maxBarHeight,
|
||||||
@@ -481,13 +453,11 @@
|
|||||||
grad.addColorStop(0, colors.primary);
|
grad.addColorStop(0, colors.primary);
|
||||||
grad.addColorStop(1, colors.secondary);
|
grad.addColorStop(1, colors.secondary);
|
||||||
ctx.fillStyle = grad;
|
ctx.fillStyle = grad;
|
||||||
|
|
||||||
for (let i = 0; i < totalBarsToShow; i++) {
|
for (let i = 0; i < totalBarsToShow; i++) {
|
||||||
const val = smoothedValues[i] * INV_255;
|
const val = smoothedValues[i] * INV_255;
|
||||||
const h = Math.max(2, val * maxBarHeight);
|
const h = Math.max(2, val * maxBarHeight);
|
||||||
const x = startX + i * (barWidth + barGap);
|
const x = startX + i * (barWidth + barGap);
|
||||||
const radius = Math.min(barWidth / 2, 4);
|
const radius = Math.min(barWidth / 2, 4);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x + radius, bottomY - h);
|
ctx.moveTo(x + radius, bottomY - h);
|
||||||
ctx.lineTo(x + barWidth - radius, bottomY - h);
|
ctx.lineTo(x + barWidth - radius, bottomY - h);
|
||||||
@@ -511,8 +481,6 @@
|
|||||||
const maxAmp = canvas.height * 0.3;
|
const maxAmp = canvas.height * 0.3;
|
||||||
const startX = 0;
|
const startX = 0;
|
||||||
const midY = canvas.height * 0.5;
|
const midY = canvas.height * 0.5;
|
||||||
|
|
||||||
// Primary wave
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(startX, midY);
|
ctx.moveTo(startX, midY);
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
@@ -533,18 +501,14 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Close to form filled area
|
|
||||||
ctx.lineTo(startX + totalWidth, midY);
|
ctx.lineTo(startX + totalWidth, midY);
|
||||||
ctx.lineTo(startX, midY);
|
ctx.lineTo(startX, midY);
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
const grad = ctx.createLinearGradient(0, midY - maxAmp, 0, midY);
|
const grad = ctx.createLinearGradient(0, midY - maxAmp, 0, midY);
|
||||||
grad.addColorStop(0, colors.primary + "cc");
|
grad.addColorStop(0, colors.primary + "cc");
|
||||||
grad.addColorStop(1, colors.primary + "11");
|
grad.addColorStop(1, colors.primary + "11");
|
||||||
ctx.fillStyle = grad;
|
ctx.fillStyle = grad;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Stroke the wave line
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
const t = i / (halfBars - 1);
|
const t = i / (halfBars - 1);
|
||||||
@@ -560,8 +524,6 @@
|
|||||||
ctx.shadowBlur = 10 * glowIntensity;
|
ctx.shadowBlur = 10 * glowIntensity;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
// Mirror wave (secondary color, below)
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
const t = i / (halfBars - 1);
|
const t = i / (halfBars - 1);
|
||||||
@@ -574,13 +536,11 @@
|
|||||||
ctx.lineTo(startX + totalWidth, midY);
|
ctx.lineTo(startX + totalWidth, midY);
|
||||||
ctx.lineTo(startX, midY);
|
ctx.lineTo(startX, midY);
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
const grad2 = ctx.createLinearGradient(0, midY, 0, midY + maxAmp * 0.6);
|
const grad2 = ctx.createLinearGradient(0, midY, 0, midY + maxAmp * 0.6);
|
||||||
grad2.addColorStop(0, colors.secondary + "11");
|
grad2.addColorStop(0, colors.secondary + "11");
|
||||||
grad2.addColorStop(1, colors.secondary + "88");
|
grad2.addColorStop(1, colors.secondary + "88");
|
||||||
ctx.fillStyle = grad2;
|
ctx.fillStyle = grad2;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let i = 0; i < halfBars; i++) {
|
for (let i = 0; i < halfBars; i++) {
|
||||||
const t = i / (halfBars - 1);
|
const t = i / (halfBars - 1);
|
||||||
@@ -602,14 +562,11 @@
|
|||||||
blobPhase += 0.008;
|
blobPhase += 0.008;
|
||||||
const points = 64;
|
const points = 64;
|
||||||
const maxDeform = size * 0.2;
|
const maxDeform = size * 0.2;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let i = 0; i <= points; i++) {
|
for (let i = 0; i <= points; i++) {
|
||||||
const angle = (i / points) * Math.PI * 2;
|
const angle = (i / points) * Math.PI * 2;
|
||||||
// Map angle to frequency bin
|
|
||||||
const freqIdx = Math.floor((i / points) * halfBars) % halfBars;
|
const freqIdx = Math.floor((i / points) * halfBars) % halfBars;
|
||||||
const val = smoothedValues[freqIdx] * INV_255;
|
const val = smoothedValues[freqIdx] * INV_255;
|
||||||
// Perlin-like noise via layered sin
|
|
||||||
const noise =
|
const noise =
|
||||||
Math.sin(angle * 3 + blobPhase) * 0.3 +
|
Math.sin(angle * 3 + blobPhase) * 0.3 +
|
||||||
Math.sin(angle * 5 - blobPhase * 1.3) * 0.2 +
|
Math.sin(angle * 5 - blobPhase * 1.3) * 0.2 +
|
||||||
@@ -621,7 +578,6 @@
|
|||||||
else ctx.lineTo(x, y);
|
else ctx.lineTo(x, y);
|
||||||
}
|
}
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
const grad = ctx.createRadialGradient(
|
const grad = ctx.createRadialGradient(
|
||||||
centerX,
|
centerX,
|
||||||
centerY,
|
centerY,
|
||||||
@@ -638,8 +594,6 @@
|
|||||||
ctx.shadowBlur = 20 * glowIntensity;
|
ctx.shadowBlur = 20 * glowIntensity;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
// Inner glow circle
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(centerX, centerY, baseRadius * 0.95, 0, Math.PI * 2);
|
ctx.arc(centerX, centerY, baseRadius * 0.95, 0, Math.PI * 2);
|
||||||
const innerGrad = ctx.createRadialGradient(
|
const innerGrad = ctx.createRadialGradient(
|
||||||
@@ -658,7 +612,6 @@
|
|||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const useFullViewport = vizMode === "bars" || vizMode === "wave";
|
const useFullViewport = vizMode === "bars" || vizMode === "wave";
|
||||||
const cw = useFullViewport
|
const cw = useFullViewport
|
||||||
? window.innerWidth
|
? window.innerWidth
|
||||||
@@ -671,11 +624,9 @@
|
|||||||
canvas.height = ch;
|
canvas.height = ch;
|
||||||
}
|
}
|
||||||
ctx.clearRect(0, 0, cw, ch);
|
ctx.clearRect(0, 0, cw, ch);
|
||||||
|
|
||||||
if (analyser) {
|
if (analyser) {
|
||||||
analyser.getByteFrequencyData(dataArray);
|
analyser.getByteFrequencyData(dataArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useFullViewport) {
|
if (useFullViewport) {
|
||||||
centerX = cw / 2;
|
centerX = cw / 2;
|
||||||
centerY = ch / 2;
|
centerY = ch / 2;
|
||||||
@@ -684,13 +635,9 @@
|
|||||||
centerX = offset + size / 2;
|
centerX = offset + size / 2;
|
||||||
centerY = offset + size / 2;
|
centerY = offset + size / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { halfBars, totalBars } = getProcessedData();
|
const { halfBars, totalBars } = getProcessedData();
|
||||||
|
|
||||||
applyWrapperEffects();
|
applyWrapperEffects();
|
||||||
spawnParticles(smoothedLoudness);
|
spawnParticles(smoothedLoudness);
|
||||||
|
|
||||||
// Draw based on selected mode
|
|
||||||
switch (vizMode) {
|
switch (vizMode) {
|
||||||
case "bars":
|
case "bars":
|
||||||
drawBars(halfBars);
|
drawBars(halfBars);
|
||||||
@@ -706,10 +653,7 @@
|
|||||||
drawCircular(halfBars, totalBars);
|
drawCircular(halfBars, totalBars);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw particles on top
|
|
||||||
updateAndDrawParticles();
|
updateAndDrawParticles();
|
||||||
|
|
||||||
animationId = requestAnimationFrame(draw);
|
animationId = requestAnimationFrame(draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,7 +669,6 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to start audio:", err);
|
console.error("Failed to start audio:", err);
|
||||||
alert(err.message || "Failed to start audio. Check browser permissions.");
|
alert(err.message || "Failed to start audio. Check browser permissions.");
|
||||||
// Clean up partial state
|
|
||||||
if (audioContext) {
|
if (audioContext) {
|
||||||
audioContext.close().catch(() => {});
|
audioContext.close().catch(() => {});
|
||||||
audioContext = null;
|
audioContext = null;
|
||||||
@@ -754,11 +697,6 @@
|
|||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
export let fileCurrentTime = 0;
|
|
||||||
export let fileDuration = 0;
|
|
||||||
export let filePlaying = false;
|
|
||||||
export let fileName = "";
|
|
||||||
|
|
||||||
export function setAudioFile(file) {
|
export function setAudioFile(file) {
|
||||||
if (audioElement) {
|
if (audioElement) {
|
||||||
audioElement.pause();
|
audioElement.pause();
|
||||||
@@ -772,7 +710,6 @@
|
|||||||
filePlaying = false;
|
filePlaying = false;
|
||||||
fileCurrentTime = 0;
|
fileCurrentTime = 0;
|
||||||
fileDuration = 0;
|
fileDuration = 0;
|
||||||
|
|
||||||
audioElement.addEventListener("loadedmetadata", () => {
|
audioElement.addEventListener("loadedmetadata", () => {
|
||||||
fileDuration = audioElement.duration;
|
fileDuration = audioElement.duration;
|
||||||
});
|
});
|
||||||
@@ -788,21 +725,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pauseAudio() {
|
export function pauseAudio() {
|
||||||
if (audioElement) {
|
if (audioElement) audioElement.pause();
|
||||||
audioElement.pause();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resumeAudio() {
|
export function resumeAudio() {
|
||||||
if (audioElement) {
|
if (audioElement) audioElement.play();
|
||||||
audioElement.play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function seekAudio(time) {
|
export function seekAudio(time) {
|
||||||
if (audioElement) {
|
if (audioElement) audioElement.currentTime = time;
|
||||||
audioElement.currentTime = time;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function takeScreenshot() {
|
export function takeScreenshot() {
|
||||||
@@ -852,7 +783,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visualizer-wrapper {
|
.visualizer-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -862,11 +792,9 @@
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container {
|
.logo-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -882,17 +810,14 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container img {
|
.logo-container img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-container.spinning {
|
.logo-container.spinning {
|
||||||
animation: logo-spin var(--spin-duration, 4s) linear infinite;
|
animation: logo-spin var(--spin-duration, 4s) linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes -global-logo-spin {
|
@keyframes -global-logo-spin {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
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