Minor update
This commit is contained in:
65
README.md
65
README.md
@@ -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
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
```
|
||||
# Clone the repo
|
||||
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
|
||||
# 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
|
||||
# Start dev server
|
||||
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
|
||||
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
19
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^25.2.3",
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
@@ -1397,6 +1398,17 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
@@ -2410,6 +2422,13 @@
|
||||
"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": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^25.2.3",
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
let {
|
||||
@@ -38,14 +38,26 @@
|
||||
const MAX_PARTICLES = 600;
|
||||
const CANVAS_PAD = 2.5;
|
||||
|
||||
let canvas;
|
||||
let ctx;
|
||||
let animationId;
|
||||
let audioContext;
|
||||
let analyser;
|
||||
let currentStream = null;
|
||||
let audioElement = null;
|
||||
let audioSourceNode = null;
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
life: number;
|
||||
decay: number;
|
||||
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 bufferLength = 128;
|
||||
|
||||
@@ -59,11 +71,11 @@
|
||||
let smoothedValues = new Float32Array(Math.ceil(barCount / 2));
|
||||
let smoothedScale = MIN_SCALE;
|
||||
let smoothedLoudness = 0;
|
||||
let visualizerWrapper;
|
||||
let visualizerWrapper: HTMLDivElement;
|
||||
let blobPhase = 0;
|
||||
|
||||
// Particle image cache
|
||||
let pImg = $state(null);
|
||||
let pImg: HTMLImageElement | null = $state(null);
|
||||
$effect(() => {
|
||||
if (particleImage) {
|
||||
const img = new Image();
|
||||
@@ -75,7 +87,7 @@
|
||||
});
|
||||
|
||||
// Particle system
|
||||
let particles = [];
|
||||
let particles: Particle[] = [];
|
||||
|
||||
// Recalculate angles when barCount changes
|
||||
let prevBarCount = barCount;
|
||||
@@ -105,7 +117,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
function spawnParticles(loudness) {
|
||||
function spawnParticles(loudness: number) {
|
||||
if (!particlesEnabled) return;
|
||||
const count = Math.floor(loudness * 8 * particleDensity);
|
||||
const maxP = Math.floor(MAX_PARTICLES * particleDensity);
|
||||
@@ -113,7 +125,7 @@
|
||||
for (let j = 0; j < count; j++) {
|
||||
if (particles.length >= maxP) break;
|
||||
|
||||
let p;
|
||||
let p: Particle;
|
||||
switch (vizMode) {
|
||||
case "bars": {
|
||||
const halfBars = Math.ceil(barCount / 2);
|
||||
@@ -138,6 +150,8 @@
|
||||
life: 1.0,
|
||||
decay: 0.008 + Math.random() * 0.02,
|
||||
radius: (2 + Math.random() * 5) * particleSize,
|
||||
angle: 0,
|
||||
spin: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -161,6 +175,8 @@
|
||||
life: 1.0,
|
||||
decay: 0.006 + Math.random() * 0.012,
|
||||
radius: (2 + Math.random() * 4) * particleSize,
|
||||
angle: 0,
|
||||
spin: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -183,6 +199,8 @@
|
||||
life: 1.0,
|
||||
decay: 0.006 + Math.random() * 0.015,
|
||||
radius: (2 + Math.random() * 6) * particleSize,
|
||||
angle: 0,
|
||||
spin: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -199,6 +217,8 @@
|
||||
life: 1.0,
|
||||
decay: 0.005 + Math.random() * 0.015,
|
||||
radius: (3 + Math.random() * 7) * particleSize,
|
||||
angle: 0,
|
||||
spin: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -271,7 +291,8 @@
|
||||
}
|
||||
|
||||
async function initAudio() {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
audioContext = new (window.AudioContext ||
|
||||
(window as any).webkitAudioContext)();
|
||||
if (audioContext.state === "suspended") {
|
||||
await audioContext.resume();
|
||||
}
|
||||
@@ -298,7 +319,7 @@
|
||||
noiseSuppression: false,
|
||||
autoGainControl: false,
|
||||
systemAudio: "include",
|
||||
},
|
||||
} as MediaTrackConstraints,
|
||||
});
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
@@ -383,7 +404,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function drawCircular(halfBars, totalBars) {
|
||||
function drawCircular(halfBars: number, totalBars: number) {
|
||||
const maxBarHeight = size * 0.25;
|
||||
const barHeights = new Float32Array(totalBars);
|
||||
for (let i = 0; i < totalBars; i++) {
|
||||
@@ -433,7 +454,7 @@
|
||||
ctx.fill("evenodd");
|
||||
}
|
||||
|
||||
function drawBars(halfBars) {
|
||||
function drawBars(halfBars: number) {
|
||||
const totalBarsToShow = halfBars;
|
||||
const barGap = 3;
|
||||
const totalWidth = canvas.width;
|
||||
@@ -476,7 +497,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function drawWave(halfBars) {
|
||||
function drawWave(halfBars: number) {
|
||||
const totalWidth = canvas.width;
|
||||
const maxAmp = canvas.height * 0.3;
|
||||
const startX = 0;
|
||||
@@ -558,7 +579,7 @@
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
function drawBlob(halfBars) {
|
||||
function drawBlob(halfBars: number) {
|
||||
blobPhase += 0.008;
|
||||
const points = 64;
|
||||
const maxDeform = size * 0.2;
|
||||
@@ -668,7 +689,10 @@
|
||||
draw();
|
||||
} catch (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) {
|
||||
audioContext.close().catch(() => {});
|
||||
audioContext = null;
|
||||
@@ -697,7 +721,7 @@
|
||||
draw();
|
||||
}
|
||||
|
||||
export function setAudioFile(file) {
|
||||
export function setAudioFile(file: File) {
|
||||
if (audioElement) {
|
||||
audioElement.pause();
|
||||
URL.revokeObjectURL(audioElement.src);
|
||||
@@ -711,10 +735,10 @@
|
||||
fileCurrentTime = 0;
|
||||
fileDuration = 0;
|
||||
audioElement.addEventListener("loadedmetadata", () => {
|
||||
fileDuration = audioElement.duration;
|
||||
fileDuration = audioElement!.duration;
|
||||
});
|
||||
audioElement.addEventListener("timeupdate", () => {
|
||||
fileCurrentTime = audioElement.currentTime;
|
||||
fileCurrentTime = audioElement!.currentTime;
|
||||
});
|
||||
audioElement.addEventListener("play", () => {
|
||||
filePlaying = true;
|
||||
@@ -732,7 +756,7 @@
|
||||
if (audioElement) audioElement.play();
|
||||
}
|
||||
|
||||
export function seekAudio(time) {
|
||||
export function seekAudio(time: number) {
|
||||
if (audioElement) audioElement.currentTime = time;
|
||||
}
|
||||
|
||||
@@ -742,7 +766,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
ctx = canvas.getContext("2d");
|
||||
ctx = canvas.getContext("2d")!;
|
||||
precalculateAngles();
|
||||
updateSize();
|
||||
window.addEventListener("resize", updateSize);
|
||||
|
||||
@@ -1,7 +1,56 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages.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 {
|
||||
selectedPreset = $bindable("cyanPink"),
|
||||
logoUrl = $bindable(""),
|
||||
@@ -37,22 +86,22 @@
|
||||
particleSize = $bindable(1.0),
|
||||
particleRotation = $bindable(0),
|
||||
titlePosition = $bindable("top"),
|
||||
savedThemes = $bindable([]),
|
||||
savedThemes = $bindable<any[]>([]),
|
||||
onscreenshot = () => {},
|
||||
onfullscreen = () => {},
|
||||
onreset = () => {},
|
||||
onaudiofile = (file) => {},
|
||||
onaudiofile = (_file: File) => {},
|
||||
onfilepause = () => {},
|
||||
onfileresume = () => {},
|
||||
onfileseek = (time) => {},
|
||||
onthemeschanged = (themes) => {},
|
||||
} = $props();
|
||||
onfileseek = (_time: number) => {},
|
||||
onthemeschanged = (_themes: any[]) => {},
|
||||
}: Props = $props();
|
||||
|
||||
let fileInput = $state(null);
|
||||
let bgFileInput = $state(null);
|
||||
let audioFileInput = $state(null);
|
||||
let particleImgInput = $state(null);
|
||||
let themeFileInput = $state(null);
|
||||
let fileInput: HTMLInputElement | null = $state(null);
|
||||
let bgFileInput: HTMLInputElement | null = $state(null);
|
||||
let audioFileInput: HTMLInputElement | null = $state(null);
|
||||
let particleImgInput: HTMLInputElement | null = $state(null);
|
||||
let themeFileInput: HTMLInputElement | null = $state(null);
|
||||
let dragOver = $state(false);
|
||||
let newThemeName = $state("");
|
||||
|
||||
@@ -62,7 +111,7 @@
|
||||
const supportsTabAudio = !isFirefox;
|
||||
|
||||
const vizModeKeys = ["circular", "bars", "wave", "blob"];
|
||||
function vizModeName(key) {
|
||||
function vizModeName(key: string) {
|
||||
switch (key) {
|
||||
case "circular":
|
||||
return m.circular();
|
||||
@@ -77,48 +126,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
function readFileAsDataUrl(file, callback) {
|
||||
function readFileAsDataUrl(file: File, callback: (result: string) => void) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => callback(e.target.result);
|
||||
reader.onload = (e) => callback(e.target!.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
function formatTime(seconds: number) {
|
||||
if (!seconds || !isFinite(seconds)) return "0:00";
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = Math.floor(seconds % 60);
|
||||
return `${min}:${sec.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function handleFileUpload(e) {
|
||||
const file = e.target.files[0];
|
||||
function handleFileUpload(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) readFileAsDataUrl(file, (url) => (logoUrl = url));
|
||||
}
|
||||
function handleParticleImgUpload(e) {
|
||||
const file = e.target.files[0];
|
||||
function handleParticleImgUpload(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) readFileAsDataUrl(file, (url) => (particleImage = url));
|
||||
}
|
||||
function handleBgUpload(e) {
|
||||
const file = e.target.files[0];
|
||||
function handleBgUpload(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) readFileAsDataUrl(file, (url) => (bgImage = url));
|
||||
}
|
||||
function handleAudioFile(e) {
|
||||
const file = e.target.files[0];
|
||||
function handleAudioFile(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file && file.type.startsWith("audio/")) {
|
||||
audioSource = "file";
|
||||
onaudiofile(file);
|
||||
}
|
||||
}
|
||||
function handleDrop(e) {
|
||||
function handleDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
dragOver = false;
|
||||
const file = e.dataTransfer.files[0];
|
||||
const file = e.dataTransfer?.files[0];
|
||||
if (file && file.type.startsWith("audio/")) {
|
||||
audioSource = "file";
|
||||
onaudiofile(file);
|
||||
}
|
||||
}
|
||||
function handleDragOver(e) {
|
||||
function handleDragOver(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
dragOver = true;
|
||||
}
|
||||
@@ -132,7 +181,7 @@
|
||||
if (bgFileInput) bgFileInput.value = "";
|
||||
}
|
||||
|
||||
function selectPreset(key) {
|
||||
function selectPreset(key: string) {
|
||||
selectedPreset = key;
|
||||
useCustomColors = false;
|
||||
}
|
||||
@@ -140,8 +189,8 @@
|
||||
useCustomColors = true;
|
||||
}
|
||||
|
||||
function handleSeek(e) {
|
||||
onfileseek(parseFloat(e.target.value));
|
||||
function handleSeek(e: Event) {
|
||||
onfileseek(parseFloat((e.target as HTMLInputElement).value));
|
||||
}
|
||||
function toggleFilePlayback() {
|
||||
if (filePlaying) onfilepause();
|
||||
@@ -185,7 +234,7 @@
|
||||
onthemeschanged(savedThemes);
|
||||
}
|
||||
|
||||
function loadTheme(theme) {
|
||||
function loadTheme(theme: any) {
|
||||
selectedPreset = theme.selectedPreset ?? selectedPreset;
|
||||
logoUrl = theme.logoUrl ?? logoUrl;
|
||||
title = theme.title ?? title;
|
||||
@@ -214,12 +263,12 @@
|
||||
titlePosition = theme.titlePosition ?? titlePosition;
|
||||
}
|
||||
|
||||
function deleteTheme(id) {
|
||||
function deleteTheme(id: number) {
|
||||
savedThemes = savedThemes.filter((t) => t.id !== id);
|
||||
onthemeschanged(savedThemes);
|
||||
}
|
||||
|
||||
function exportTheme(theme) {
|
||||
function exportTheme(theme: any) {
|
||||
const data = JSON.stringify(theme, null, 2);
|
||||
const blob = new Blob([data], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -230,13 +279,13 @@
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handleThemeImport(e) {
|
||||
const file = e.target.files[0];
|
||||
function handleThemeImport(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
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$/, "");
|
||||
theme.id = Date.now();
|
||||
savedThemes = [...savedThemes, theme];
|
||||
@@ -246,7 +295,7 @@
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = "";
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -332,7 +381,7 @@
|
||||
'file'
|
||||
? 'bg-white/15 text-white'
|
||||
: 'bg-white/5 text-zinc-500 hover:bg-white/10'}"
|
||||
onclick={() => audioFileInput.click()}>{m.file()}</button
|
||||
onclick={() => audioFileInput?.click()}>{m.file()}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -927,7 +976,7 @@
|
||||
/>
|
||||
<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"
|
||||
onclick={() => themeFileInput.click()}
|
||||
onclick={() => themeFileInput?.click()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import AudioVisualizer from "$lib/AudioVisualizer.svelte";
|
||||
@@ -90,9 +90,9 @@
|
||||
let particleRotation = $state(saved.particleRotation);
|
||||
let titlePosition = $state(saved.titlePosition);
|
||||
let savedThemes = $state(loadThemes());
|
||||
let visualizerComponent = $state(null);
|
||||
let visualizerComponent: any = $state(null);
|
||||
let toggleHidden = $state(false);
|
||||
let hideTimer;
|
||||
let hideTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
let fileCurrentTime = $state(0);
|
||||
let fileDuration = $state(0);
|
||||
@@ -162,7 +162,7 @@
|
||||
} catch {}
|
||||
});
|
||||
|
||||
function handleThemesChanged(themes) {
|
||||
function handleThemesChanged(themes: any[]) {
|
||||
savedThemes = themes;
|
||||
if (browser) {
|
||||
try {
|
||||
@@ -173,14 +173,14 @@
|
||||
|
||||
function toggleListening() {
|
||||
if (isListening) {
|
||||
visualizerComponent.stopListening();
|
||||
visualizerComponent?.stopListening();
|
||||
} else {
|
||||
visualizerComponent.startListening();
|
||||
visualizerComponent?.startListening();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScreenshot() {
|
||||
const dataUrl = visualizerComponent.takeScreenshot();
|
||||
const dataUrl = visualizerComponent?.takeScreenshot();
|
||||
if (!dataUrl) return;
|
||||
const link = document.createElement("a");
|
||||
link.download = "visualizer-screenshot.png";
|
||||
@@ -226,7 +226,7 @@
|
||||
titlePosition = defaults.titlePosition;
|
||||
}
|
||||
|
||||
function handleAudioFile(file) {
|
||||
function handleAudioFile(file: File) {
|
||||
if (file && visualizerComponent) {
|
||||
if (isListening) visualizerComponent.stopListening();
|
||||
audioSource = "file";
|
||||
@@ -240,15 +240,16 @@
|
||||
function handleFileResume() {
|
||||
if (visualizerComponent) visualizerComponent.resumeAudio();
|
||||
}
|
||||
function handleFileSeek(time) {
|
||||
function handleFileSeek(time: number) {
|
||||
if (visualizerComponent) visualizerComponent.seekAudio(time);
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
e.target.tagName === "INPUT" ||
|
||||
e.target.tagName === "TEXTAREA" ||
|
||||
e.target.isContentEditable
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable
|
||||
)
|
||||
return;
|
||||
switch (e.code) {
|
||||
@@ -306,7 +307,7 @@
|
||||
let currentColors = $derived(
|
||||
useCustomColors
|
||||
? { primary: customPrimary, secondary: customSecondary }
|
||||
: colorPresets[selectedPreset],
|
||||
: colorPresets[selectedPreset as keyof typeof colorPresets],
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user