Minor update
This commit is contained in:
@@ -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