Minor update

This commit is contained in:
AlacrisDevs
2026-02-18 10:02:20 +02:00
parent 2d1e3a401f
commit 21df667646
6 changed files with 213 additions and 104 deletions

View File

@@ -1,42 +1,57 @@
# sv # Audio Visualizer
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). A real-time audio visualizer built with SvelteKit, Canvas API, and Web Audio API. Capture audio from your microphone, browser tab, or a local file and watch it come to life with reactive visuals, particles, and customizable themes.
## Creating a project ## Features
If you're seeing this, you've probably already done this step. Congrats! - **4 Visualization Modes** — Circular, Bars, Wave, and Blob
- **3 Audio Sources** — Microphone, Browser Tab (Chrome/Edge), and Local File
- **Particle System** — Configurable density, size, rotation, and custom particle images
- **Audio Reactivity** — Sensitivity, smoothing, and bass emphasis controls
- **Visual Effects** — Shake, zoom, and glow intensity sliders
- **Color Presets & Custom Colors** — 6 built-in presets or pick your own gradient
- **Branding** — Custom logo with optional spin, title, and subtitle with positioning
- **Background** — Custom image, color picker, and vignette toggle
- **Theme System** — Save, load, import, and export themes as `.vslzr` files
- **Screenshot Export** — Save the current visualization as a PNG
- **Fullscreen Mode** — Immersive fullscreen with keyboard shortcut
- **i18n** — English and Estonian via Paraglide.js
- **Persistent Settings** — All settings saved to localStorage
## Quick Start
```sh ```sh
# create a new project # Clone the repo
npx sv create my-app git clone https://git.lapikud.ee/sass/audio-visualizer.git
``` cd audio-visualizer
To recreate this project with the same configuration: # Install dependencies
npm install
```sh # Start dev server
# 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 npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
``` ```
## Building Open [http://localhost:5173](http://localhost:5173) in your browser.
To create a production version of your app: ## Keyboard Shortcuts
| Key | Action |
| --- | --- |
| `Space` | Play / Stop |
| `F` | Toggle Fullscreen |
| `H` | Hide / Show Sidebar |
## Build
```sh ```sh
npm run build npm run build
npm run preview
``` ```
You can preview the production build with `npm run preview`. ## Tech Stack
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. - **SvelteKit** + **Svelte 5** (runes)
- **Tailwind CSS v4**
- **Canvas API** + **Web Audio API**
- **Paraglide.js** for i18n

19
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/node": "^25.2.3",
"svelte": "^5.49.2", "svelte": "^5.49.2",
"svelte-check": "^4.3.6", "svelte-check": "^4.3.6",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
@@ -1397,6 +1398,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "25.2.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -2410,6 +2422,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/unplugin": { "node_modules/unplugin": {
"version": "2.3.11", "version": "2.3.11",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",

View File

@@ -19,6 +19,7 @@
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/node": "^25.2.3",
"svelte": "^5.49.2", "svelte": "^5.49.2",
"svelte-check": "^4.3.6", "svelte-check": "^4.3.6",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",

View File

@@ -1,4 +1,4 @@
<script> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
let { let {
@@ -38,14 +38,26 @@
const MAX_PARTICLES = 600; const MAX_PARTICLES = 600;
const CANVAS_PAD = 2.5; const CANVAS_PAD = 2.5;
let canvas; interface Particle {
let ctx; x: number;
let animationId; y: number;
let audioContext; vx: number;
let analyser; vy: number;
let currentStream = null; life: number;
let audioElement = null; decay: number;
let audioSourceNode = null; radius: number;
angle: number;
spin: number;
}
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
let animationId: number | undefined;
let audioContext: AudioContext | null = null;
let analyser: AnalyserNode | null = null;
let currentStream: MediaStream | null = null;
let audioElement: HTMLAudioElement | null = null;
let audioSourceNode: MediaElementAudioSourceNode | null = null;
let dataArray = new Uint8Array(128); let dataArray = new Uint8Array(128);
let bufferLength = 128; let bufferLength = 128;
@@ -59,11 +71,11 @@
let smoothedValues = new Float32Array(Math.ceil(barCount / 2)); let smoothedValues = new Float32Array(Math.ceil(barCount / 2));
let smoothedScale = MIN_SCALE; let smoothedScale = MIN_SCALE;
let smoothedLoudness = 0; let smoothedLoudness = 0;
let visualizerWrapper; let visualizerWrapper: HTMLDivElement;
let blobPhase = 0; let blobPhase = 0;
// Particle image cache // Particle image cache
let pImg = $state(null); let pImg: HTMLImageElement | null = $state(null);
$effect(() => { $effect(() => {
if (particleImage) { if (particleImage) {
const img = new Image(); const img = new Image();
@@ -75,7 +87,7 @@
}); });
// Particle system // Particle system
let particles = []; let particles: Particle[] = [];
// Recalculate angles when barCount changes // Recalculate angles when barCount changes
let prevBarCount = barCount; let prevBarCount = barCount;
@@ -105,7 +117,7 @@
} }
}); });
function spawnParticles(loudness) { function spawnParticles(loudness: number) {
if (!particlesEnabled) return; if (!particlesEnabled) return;
const count = Math.floor(loudness * 8 * particleDensity); const count = Math.floor(loudness * 8 * particleDensity);
const maxP = Math.floor(MAX_PARTICLES * particleDensity); const maxP = Math.floor(MAX_PARTICLES * particleDensity);
@@ -113,7 +125,7 @@
for (let j = 0; j < count; j++) { for (let j = 0; j < count; j++) {
if (particles.length >= maxP) break; if (particles.length >= maxP) break;
let p; let p: Particle;
switch (vizMode) { switch (vizMode) {
case "bars": { case "bars": {
const halfBars = Math.ceil(barCount / 2); const halfBars = Math.ceil(barCount / 2);
@@ -138,6 +150,8 @@
life: 1.0, life: 1.0,
decay: 0.008 + Math.random() * 0.02, decay: 0.008 + Math.random() * 0.02,
radius: (2 + Math.random() * 5) * particleSize, radius: (2 + Math.random() * 5) * particleSize,
angle: 0,
spin: 0,
}; };
break; break;
} }
@@ -161,6 +175,8 @@
life: 1.0, life: 1.0,
decay: 0.006 + Math.random() * 0.012, decay: 0.006 + Math.random() * 0.012,
radius: (2 + Math.random() * 4) * particleSize, radius: (2 + Math.random() * 4) * particleSize,
angle: 0,
spin: 0,
}; };
break; break;
} }
@@ -183,6 +199,8 @@
life: 1.0, life: 1.0,
decay: 0.006 + Math.random() * 0.015, decay: 0.006 + Math.random() * 0.015,
radius: (2 + Math.random() * 6) * particleSize, radius: (2 + Math.random() * 6) * particleSize,
angle: 0,
spin: 0,
}; };
break; break;
} }
@@ -199,6 +217,8 @@
life: 1.0, life: 1.0,
decay: 0.005 + Math.random() * 0.015, decay: 0.005 + Math.random() * 0.015,
radius: (3 + Math.random() * 7) * particleSize, radius: (3 + Math.random() * 7) * particleSize,
angle: 0,
spin: 0,
}; };
break; break;
} }
@@ -271,7 +291,8 @@
} }
async function initAudio() { async function initAudio() {
audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
if (audioContext.state === "suspended") { if (audioContext.state === "suspended") {
await audioContext.resume(); await audioContext.resume();
} }
@@ -298,7 +319,7 @@
noiseSuppression: false, noiseSuppression: false,
autoGainControl: false, autoGainControl: false,
systemAudio: "include", systemAudio: "include",
}, } as MediaTrackConstraints,
}); });
const audioTracks = stream.getAudioTracks(); const audioTracks = stream.getAudioTracks();
if (audioTracks.length === 0) { if (audioTracks.length === 0) {
@@ -383,7 +404,7 @@
} }
} }
function drawCircular(halfBars, totalBars) { function drawCircular(halfBars: number, totalBars: number) {
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++) {
@@ -433,7 +454,7 @@
ctx.fill("evenodd"); ctx.fill("evenodd");
} }
function drawBars(halfBars) { function drawBars(halfBars: number) {
const totalBarsToShow = halfBars; const totalBarsToShow = halfBars;
const barGap = 3; const barGap = 3;
const totalWidth = canvas.width; const totalWidth = canvas.width;
@@ -476,7 +497,7 @@
} }
} }
function drawWave(halfBars) { function drawWave(halfBars: number) {
const totalWidth = canvas.width; const totalWidth = canvas.width;
const maxAmp = canvas.height * 0.3; const maxAmp = canvas.height * 0.3;
const startX = 0; const startX = 0;
@@ -558,7 +579,7 @@
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
} }
function drawBlob(halfBars) { function drawBlob(halfBars: number) {
blobPhase += 0.008; blobPhase += 0.008;
const points = 64; const points = 64;
const maxDeform = size * 0.2; const maxDeform = size * 0.2;
@@ -668,7 +689,10 @@
draw(); draw();
} 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 as Error).message ||
"Failed to start audio. Check browser permissions.",
);
if (audioContext) { if (audioContext) {
audioContext.close().catch(() => {}); audioContext.close().catch(() => {});
audioContext = null; audioContext = null;
@@ -697,7 +721,7 @@
draw(); draw();
} }
export function setAudioFile(file) { export function setAudioFile(file: File) {
if (audioElement) { if (audioElement) {
audioElement.pause(); audioElement.pause();
URL.revokeObjectURL(audioElement.src); URL.revokeObjectURL(audioElement.src);
@@ -711,10 +735,10 @@
fileCurrentTime = 0; fileCurrentTime = 0;
fileDuration = 0; fileDuration = 0;
audioElement.addEventListener("loadedmetadata", () => { audioElement.addEventListener("loadedmetadata", () => {
fileDuration = audioElement.duration; fileDuration = audioElement!.duration;
}); });
audioElement.addEventListener("timeupdate", () => { audioElement.addEventListener("timeupdate", () => {
fileCurrentTime = audioElement.currentTime; fileCurrentTime = audioElement!.currentTime;
}); });
audioElement.addEventListener("play", () => { audioElement.addEventListener("play", () => {
filePlaying = true; filePlaying = true;
@@ -732,7 +756,7 @@
if (audioElement) audioElement.play(); if (audioElement) audioElement.play();
} }
export function seekAudio(time) { export function seekAudio(time: number) {
if (audioElement) audioElement.currentTime = time; if (audioElement) audioElement.currentTime = time;
} }
@@ -742,7 +766,7 @@
} }
onMount(() => { onMount(() => {
ctx = canvas.getContext("2d"); ctx = canvas.getContext("2d")!;
precalculateAngles(); precalculateAngles();
updateSize(); updateSize();
window.addEventListener("resize", updateSize); window.addEventListener("resize", updateSize);

View File

@@ -1,7 +1,56 @@
<script> <script lang="ts">
import * as m from "$lib/paraglide/messages.js"; import * as m from "$lib/paraglide/messages.js";
import { getLocale, setLocale, locales } from "$lib/paraglide/runtime.js"; import { getLocale, setLocale, locales } from "$lib/paraglide/runtime.js";
interface Props {
selectedPreset?: string;
logoUrl?: string;
title?: string;
subtitle?: string;
bgImage?: string;
bgColor?: string;
vignette?: boolean;
audioSource?: string;
customPrimary?: string;
customSecondary?: string;
useCustomColors?: boolean;
isListening?: boolean;
colorPresets?: Record<
string,
{ name: string; primary: string; secondary: string }
>;
onToggle?: () => void;
fileCurrentTime?: number;
fileDuration?: number;
filePlaying?: boolean;
fileName?: string;
vizMode?: string;
barCount?: number;
sensitivity?: number;
smoothing?: number;
bassEmphasis?: number;
particlesEnabled?: boolean;
particleDensity?: number;
shakeAmount?: number;
zoomIntensity?: number;
glowIntensity?: number;
logoSpin?: boolean;
logoSpinSpeed?: number;
particleImage?: string;
particleSize?: number;
particleRotation?: number;
titlePosition?: string;
savedThemes?: any[];
onscreenshot?: () => void;
onfullscreen?: () => void;
onreset?: () => void;
onaudiofile?: (file: File) => void;
onfilepause?: () => void;
onfileresume?: () => void;
onfileseek?: (time: number) => void;
onthemeschanged?: (themes: any[]) => void;
}
let { let {
selectedPreset = $bindable("cyanPink"), selectedPreset = $bindable("cyanPink"),
logoUrl = $bindable(""), logoUrl = $bindable(""),
@@ -37,22 +86,22 @@
particleSize = $bindable(1.0), particleSize = $bindable(1.0),
particleRotation = $bindable(0), particleRotation = $bindable(0),
titlePosition = $bindable("top"), titlePosition = $bindable("top"),
savedThemes = $bindable([]), savedThemes = $bindable<any[]>([]),
onscreenshot = () => {}, onscreenshot = () => {},
onfullscreen = () => {}, onfullscreen = () => {},
onreset = () => {}, onreset = () => {},
onaudiofile = (file) => {}, onaudiofile = (_file: File) => {},
onfilepause = () => {}, onfilepause = () => {},
onfileresume = () => {}, onfileresume = () => {},
onfileseek = (time) => {}, onfileseek = (_time: number) => {},
onthemeschanged = (themes) => {}, onthemeschanged = (_themes: any[]) => {},
} = $props(); }: Props = $props();
let fileInput = $state(null); let fileInput: HTMLInputElement | null = $state(null);
let bgFileInput = $state(null); let bgFileInput: HTMLInputElement | null = $state(null);
let audioFileInput = $state(null); let audioFileInput: HTMLInputElement | null = $state(null);
let particleImgInput = $state(null); let particleImgInput: HTMLInputElement | null = $state(null);
let themeFileInput = $state(null); let themeFileInput: HTMLInputElement | null = $state(null);
let dragOver = $state(false); let dragOver = $state(false);
let newThemeName = $state(""); let newThemeName = $state("");
@@ -62,7 +111,7 @@
const supportsTabAudio = !isFirefox; const supportsTabAudio = !isFirefox;
const vizModeKeys = ["circular", "bars", "wave", "blob"]; const vizModeKeys = ["circular", "bars", "wave", "blob"];
function vizModeName(key) { function vizModeName(key: string) {
switch (key) { switch (key) {
case "circular": case "circular":
return m.circular(); return m.circular();
@@ -77,48 +126,48 @@
} }
} }
function readFileAsDataUrl(file, callback) { function readFileAsDataUrl(file: File, callback: (result: string) => void) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => callback(e.target.result); reader.onload = (e) => callback(e.target!.result as string);
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
function formatTime(seconds) { function formatTime(seconds: number) {
if (!seconds || !isFinite(seconds)) return "0:00"; if (!seconds || !isFinite(seconds)) return "0:00";
const min = Math.floor(seconds / 60); const min = Math.floor(seconds / 60);
const sec = Math.floor(seconds % 60); const sec = Math.floor(seconds % 60);
return `${min}:${sec.toString().padStart(2, "0")}`; return `${min}:${sec.toString().padStart(2, "0")}`;
} }
function handleFileUpload(e) { function handleFileUpload(e: Event) {
const file = e.target.files[0]; const file = (e.target as HTMLInputElement).files?.[0];
if (file) readFileAsDataUrl(file, (url) => (logoUrl = url)); if (file) readFileAsDataUrl(file, (url) => (logoUrl = url));
} }
function handleParticleImgUpload(e) { function handleParticleImgUpload(e: Event) {
const file = e.target.files[0]; const file = (e.target as HTMLInputElement).files?.[0];
if (file) readFileAsDataUrl(file, (url) => (particleImage = url)); if (file) readFileAsDataUrl(file, (url) => (particleImage = url));
} }
function handleBgUpload(e) { function handleBgUpload(e: Event) {
const file = e.target.files[0]; const file = (e.target as HTMLInputElement).files?.[0];
if (file) readFileAsDataUrl(file, (url) => (bgImage = url)); if (file) readFileAsDataUrl(file, (url) => (bgImage = url));
} }
function handleAudioFile(e) { function handleAudioFile(e: Event) {
const file = e.target.files[0]; const file = (e.target as HTMLInputElement).files?.[0];
if (file && file.type.startsWith("audio/")) { if (file && file.type.startsWith("audio/")) {
audioSource = "file"; audioSource = "file";
onaudiofile(file); onaudiofile(file);
} }
} }
function handleDrop(e) { function handleDrop(e: DragEvent) {
e.preventDefault(); e.preventDefault();
dragOver = false; dragOver = false;
const file = e.dataTransfer.files[0]; const file = e.dataTransfer?.files[0];
if (file && file.type.startsWith("audio/")) { if (file && file.type.startsWith("audio/")) {
audioSource = "file"; audioSource = "file";
onaudiofile(file); onaudiofile(file);
} }
} }
function handleDragOver(e) { function handleDragOver(e: DragEvent) {
e.preventDefault(); e.preventDefault();
dragOver = true; dragOver = true;
} }
@@ -132,7 +181,7 @@
if (bgFileInput) bgFileInput.value = ""; if (bgFileInput) bgFileInput.value = "";
} }
function selectPreset(key) { function selectPreset(key: string) {
selectedPreset = key; selectedPreset = key;
useCustomColors = false; useCustomColors = false;
} }
@@ -140,8 +189,8 @@
useCustomColors = true; useCustomColors = true;
} }
function handleSeek(e) { function handleSeek(e: Event) {
onfileseek(parseFloat(e.target.value)); onfileseek(parseFloat((e.target as HTMLInputElement).value));
} }
function toggleFilePlayback() { function toggleFilePlayback() {
if (filePlaying) onfilepause(); if (filePlaying) onfilepause();
@@ -185,7 +234,7 @@
onthemeschanged(savedThemes); onthemeschanged(savedThemes);
} }
function loadTheme(theme) { function loadTheme(theme: any) {
selectedPreset = theme.selectedPreset ?? selectedPreset; selectedPreset = theme.selectedPreset ?? selectedPreset;
logoUrl = theme.logoUrl ?? logoUrl; logoUrl = theme.logoUrl ?? logoUrl;
title = theme.title ?? title; title = theme.title ?? title;
@@ -214,12 +263,12 @@
titlePosition = theme.titlePosition ?? titlePosition; titlePosition = theme.titlePosition ?? titlePosition;
} }
function deleteTheme(id) { function deleteTheme(id: number) {
savedThemes = savedThemes.filter((t) => t.id !== id); savedThemes = savedThemes.filter((t) => t.id !== id);
onthemeschanged(savedThemes); onthemeschanged(savedThemes);
} }
function exportTheme(theme) { function exportTheme(theme: any) {
const data = JSON.stringify(theme, null, 2); const data = JSON.stringify(theme, null, 2);
const blob = new Blob([data], { type: "application/json" }); const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -230,13 +279,13 @@
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
function handleThemeImport(e) { function handleThemeImport(e: Event) {
const file = e.target.files[0]; const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (ev) => { reader.onload = (ev) => {
try { try {
const theme = JSON.parse(ev.target.result); const theme = JSON.parse(ev.target!.result as string);
if (!theme.name) theme.name = file.name.replace(/\.vslzr$/, ""); if (!theme.name) theme.name = file.name.replace(/\.vslzr$/, "");
theme.id = Date.now(); theme.id = Date.now();
savedThemes = [...savedThemes, theme]; savedThemes = [...savedThemes, theme];
@@ -246,7 +295,7 @@
} }
}; };
reader.readAsText(file); reader.readAsText(file);
e.target.value = ""; (e.target as HTMLInputElement).value = "";
} }
</script> </script>
@@ -332,7 +381,7 @@
'file' 'file'
? 'bg-white/15 text-white' ? 'bg-white/15 text-white'
: 'bg-white/5 text-zinc-500 hover:bg-white/10'}" : 'bg-white/5 text-zinc-500 hover:bg-white/10'}"
onclick={() => audioFileInput.click()}>{m.file()}</button onclick={() => audioFileInput?.click()}>{m.file()}</button
> >
</div> </div>
@@ -927,7 +976,7 @@
/> />
<button <button
class="flex-1 px-3 py-1.5 bg-white/[0.06] border border-white/15 rounded-lg text-zinc-400 text-xs hover:bg-white/10 hover:text-white transition-all" class="flex-1 px-3 py-1.5 bg-white/[0.06] border border-white/15 rounded-lg text-zinc-400 text-xs hover:bg-white/10 hover:text-white transition-all"
onclick={() => themeFileInput.click()} onclick={() => themeFileInput?.click()}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<script> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import AudioVisualizer from "$lib/AudioVisualizer.svelte"; import AudioVisualizer from "$lib/AudioVisualizer.svelte";
@@ -90,9 +90,9 @@
let particleRotation = $state(saved.particleRotation); let particleRotation = $state(saved.particleRotation);
let titlePosition = $state(saved.titlePosition); let titlePosition = $state(saved.titlePosition);
let savedThemes = $state(loadThemes()); let savedThemes = $state(loadThemes());
let visualizerComponent = $state(null); let visualizerComponent: any = $state(null);
let toggleHidden = $state(false); let toggleHidden = $state(false);
let hideTimer; let hideTimer: ReturnType<typeof setTimeout>;
let fileCurrentTime = $state(0); let fileCurrentTime = $state(0);
let fileDuration = $state(0); let fileDuration = $state(0);
@@ -162,7 +162,7 @@
} catch {} } catch {}
}); });
function handleThemesChanged(themes) { function handleThemesChanged(themes: any[]) {
savedThemes = themes; savedThemes = themes;
if (browser) { if (browser) {
try { try {
@@ -173,14 +173,14 @@
function toggleListening() { function toggleListening() {
if (isListening) { if (isListening) {
visualizerComponent.stopListening(); visualizerComponent?.stopListening();
} else { } else {
visualizerComponent.startListening(); visualizerComponent?.startListening();
} }
} }
function handleScreenshot() { function handleScreenshot() {
const dataUrl = visualizerComponent.takeScreenshot(); const dataUrl = visualizerComponent?.takeScreenshot();
if (!dataUrl) return; if (!dataUrl) return;
const link = document.createElement("a"); const link = document.createElement("a");
link.download = "visualizer-screenshot.png"; link.download = "visualizer-screenshot.png";
@@ -226,7 +226,7 @@
titlePosition = defaults.titlePosition; titlePosition = defaults.titlePosition;
} }
function handleAudioFile(file) { function handleAudioFile(file: File) {
if (file && visualizerComponent) { if (file && visualizerComponent) {
if (isListening) visualizerComponent.stopListening(); if (isListening) visualizerComponent.stopListening();
audioSource = "file"; audioSource = "file";
@@ -240,15 +240,16 @@
function handleFileResume() { function handleFileResume() {
if (visualizerComponent) visualizerComponent.resumeAudio(); if (visualizerComponent) visualizerComponent.resumeAudio();
} }
function handleFileSeek(time) { function handleFileSeek(time: number) {
if (visualizerComponent) visualizerComponent.seekAudio(time); if (visualizerComponent) visualizerComponent.seekAudio(time);
} }
function handleKeydown(e) { function handleKeydown(e: KeyboardEvent) {
const target = e.target as HTMLElement;
if ( if (
e.target.tagName === "INPUT" || target.tagName === "INPUT" ||
e.target.tagName === "TEXTAREA" || target.tagName === "TEXTAREA" ||
e.target.isContentEditable target.isContentEditable
) )
return; return;
switch (e.code) { switch (e.code) {
@@ -306,7 +307,7 @@
let currentColors = $derived( let currentColors = $derived(
useCustomColors useCustomColors
? { primary: customPrimary, secondary: customSecondary } ? { primary: customPrimary, secondary: customSecondary }
: colorPresets[selectedPreset], : colorPresets[selectedPreset as keyof typeof colorPresets],
); );
</script> </script>