Initial commit
This commit is contained in:
591
src/App.svelte
Normal file
591
src/App.svelte
Normal file
@@ -0,0 +1,591 @@
|
||||
<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,
|
||||
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 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,
|
||||
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;
|
||||
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}
|
||||
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: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>
|
||||
Reference in New Issue
Block a user