add favicon, major performance optimizations
This commit is contained in:
@@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<title>Hear On Out - Listen to the Weather</title>
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="%sveltekit.assets%/favicon-96x96.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|||||||
@@ -31,5 +31,5 @@ export function createGain(volume: number): Tone.Gain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAnalyser(): Tone.Analyser {
|
export function createAnalyser(): Tone.Analyser {
|
||||||
return new Tone.Analyser('fft', 512);
|
return new Tone.Analyser('fft', 256); // Reduced from 512 for better performance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,14 @@
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
let particles: Particle[] = [];
|
let particles: Particle[] = [];
|
||||||
const numParticles = 60;
|
const numParticles = 40; // Reduced from 60 for better performance with two visualizations
|
||||||
let audioData: Float32Array | null = null;
|
let audioData: Float32Array | null = null;
|
||||||
|
|
||||||
|
// Cache frequently used calculations
|
||||||
|
let bassRange = 0;
|
||||||
|
let midRange = 0;
|
||||||
|
let trebleStart = 0;
|
||||||
|
|
||||||
class Particle {
|
class Particle {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -42,11 +47,13 @@
|
|||||||
this.x += this.vx;
|
this.x += this.vx;
|
||||||
this.y += this.vy;
|
this.y += this.vy;
|
||||||
|
|
||||||
// Subtle audio reactive displacement
|
// Subtle audio reactive displacement (only every 3rd frame for performance)
|
||||||
|
if (p.frameCount % 3 === 0) {
|
||||||
const displacement = audioLevel * 20;
|
const displacement = audioLevel * 20;
|
||||||
const angle = p.noise(this.x * 0.01, this.y * 0.01, p.frameCount * 0.01) * p.TWO_PI;
|
const angle = p.noise(this.x * 0.01, this.y * 0.01, p.frameCount * 0.01) * p.TWO_PI;
|
||||||
this.x += p.cos(angle) * displacement * 0.05;
|
this.x += p.cos(angle) * displacement * 0.05;
|
||||||
this.y += p.sin(angle) * displacement * 0.05;
|
this.y += p.sin(angle) * displacement * 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
// Subtle audio reactive size
|
// Subtle audio reactive size
|
||||||
this.size = p.map(bass + mid, 0, 2, 3, 10);
|
this.size = p.map(bass + mid, 0, 2, 3, 10);
|
||||||
@@ -79,6 +86,7 @@
|
|||||||
const sketch = (p: p5) => {
|
const sketch = (p: p5) => {
|
||||||
p.setup = () => {
|
p.setup = () => {
|
||||||
p.createCanvas(width, height);
|
p.createCanvas(width, height);
|
||||||
|
p.frameRate(30); // Reduce from 60fps to 30fps for better performance
|
||||||
p.background(0);
|
p.background(0);
|
||||||
|
|
||||||
// Initialize particles
|
// Initialize particles
|
||||||
@@ -89,12 +97,23 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
p.draw = () => {
|
p.draw = () => {
|
||||||
|
// Pause visualization when document is hidden (tab not focused)
|
||||||
|
if (document.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
p.background(0, 30); // Fade effect
|
p.background(0, 30); // Fade effect
|
||||||
|
|
||||||
if (isPlaying && analyser) {
|
if (isPlaying && analyser) {
|
||||||
// Get FFT data (frequency analysis)
|
// Get FFT data (frequency analysis)
|
||||||
audioData = analyser.getValue() as Float32Array;
|
audioData = analyser.getValue() as Float32Array;
|
||||||
|
|
||||||
|
// Initialize range values on first run
|
||||||
|
if (bassRange === 0) {
|
||||||
|
bassRange = Math.floor(audioData.length * 0.15); // Low frequencies
|
||||||
|
midRange = Math.floor(audioData.length * 0.4); // Mid frequencies
|
||||||
|
trebleStart = midRange;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate audio metrics from FFT data
|
// Calculate audio metrics from FFT data
|
||||||
// FFT values are in decibels (negative values, typically -100 to 0)
|
// FFT values are in decibels (negative values, typically -100 to 0)
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
@@ -102,9 +121,6 @@
|
|||||||
let mid = 0;
|
let mid = 0;
|
||||||
let treble = 0;
|
let treble = 0;
|
||||||
|
|
||||||
const bassRange = Math.floor(audioData.length * 0.15); // Low frequencies
|
|
||||||
const midRange = Math.floor(audioData.length * 0.4); // Mid frequencies
|
|
||||||
|
|
||||||
for (let i = 0; i < audioData.length; i++) {
|
for (let i = 0; i < audioData.length; i++) {
|
||||||
// Convert from decibels to linear scale (0-1)
|
// Convert from decibels to linear scale (0-1)
|
||||||
// FFT returns values from -100 to 0 dB
|
// FFT returns values from -100 to 0 dB
|
||||||
@@ -124,7 +140,7 @@
|
|||||||
let audioLevel = (sum / audioData.length) * 2;
|
let audioLevel = (sum / audioData.length) * 2;
|
||||||
bass = (bass / bassRange) * 2.5;
|
bass = (bass / bassRange) * 2.5;
|
||||||
mid = (mid / (midRange - bassRange)) * 2;
|
mid = (mid / (midRange - bassRange)) * 2;
|
||||||
treble = (treble / (audioData.length - midRange)) * 1.5;
|
treble = (treble / (audioData.length - trebleStart)) * 1.5;
|
||||||
|
|
||||||
// Clamp values
|
// Clamp values
|
||||||
audioLevel = p.constrain(audioLevel, 0, 1);
|
audioLevel = p.constrain(audioLevel, 0, 1);
|
||||||
@@ -139,12 +155,17 @@
|
|||||||
for (let i = 0; i < particles.length; i++) {
|
for (let i = 0; i < particles.length; i++) {
|
||||||
particles[i].update(p, audioLevel, bass, mid);
|
particles[i].update(p, audioLevel, bass, mid);
|
||||||
particles[i].display(p, audioLevel);
|
particles[i].display(p, audioLevel);
|
||||||
|
}
|
||||||
|
|
||||||
// Connect nearby particles (non-reactive distance)
|
// Draw connections less frequently (every other frame) for better performance
|
||||||
for (let j = i + 1; j < particles.length; j++) {
|
if (p.frameCount % 2 === 0) {
|
||||||
|
for (let i = 0; i < particles.length; i++) {
|
||||||
|
// Only check next 5 particles instead of all, reduces O(n²) significantly
|
||||||
|
for (let j = i + 1; j < Math.min(i + 6, particles.length); j++) {
|
||||||
particles[i].connect(p, particles[j], connectionDist);
|
particles[i].connect(p, particles[j], connectionDist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (!isPlaying) {
|
} else if (!isPlaying) {
|
||||||
// Static state when not playing
|
// Static state when not playing
|
||||||
p.fill(255, 50);
|
p.fill(255, 50);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
pm25 = 0,
|
pm25 = 0,
|
||||||
relativeHumidity2m = 50,
|
relativeHumidity2m = 50,
|
||||||
windSpeed10m = 0,
|
windSpeed10m = 0,
|
||||||
volume = -15
|
volume = -12
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// Component state
|
// Component state
|
||||||
@@ -65,6 +65,9 @@
|
|||||||
// Initialize audio components
|
// Initialize audio components
|
||||||
const initializeAudio = async (): Promise<void> => {
|
const initializeAudio = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
// Optimize audio scheduling for better stability
|
||||||
|
Tone.getContext().lookAhead = 0.1; // Keep default 100ms lookahead
|
||||||
|
|
||||||
// Create instruments
|
// Create instruments
|
||||||
noiseSynth = createNoiseSynth(volume);
|
noiseSynth = createNoiseSynth(volume);
|
||||||
|
|
||||||
@@ -105,20 +108,22 @@
|
|||||||
loop = null;
|
loop = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a loop that triggers at random intervals
|
// Create a loop that triggers at intervals with variation
|
||||||
|
const updateLoopInterval = () => {
|
||||||
|
if (loop) {
|
||||||
|
const randomFactor = 0.8 + Math.random() * 0.4; // 0.8x to 1.2x variation
|
||||||
|
loop.interval = burstInterval * randomFactor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
loop = new Tone.Loop((time) => {
|
loop = new Tone.Loop((time) => {
|
||||||
if (noiseSynth) {
|
if (noiseSynth) {
|
||||||
// Trigger noise burst
|
// Trigger noise burst at the scheduled time
|
||||||
noiseSynth.triggerAttackRelease('16n', time);
|
noiseSynth.triggerAttackRelease('16n', time);
|
||||||
|
// Schedule interval update for after this callback completes
|
||||||
// Schedule next burst with randomization
|
Tone.Draw.schedule(() => {
|
||||||
const baseInterval = burstInterval;
|
updateLoopInterval();
|
||||||
const randomFactor = 0.5 + Math.random(); // 0.5x to 1.5x variation
|
}, time);
|
||||||
const nextBurstTime = baseInterval * randomFactor;
|
|
||||||
|
|
||||||
if (loop) {
|
|
||||||
loop.interval = nextBurstTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, burstInterval);
|
}, burstInterval);
|
||||||
|
|
||||||
@@ -158,9 +163,14 @@
|
|||||||
// Reactive updates for environmental parameters
|
// Reactive updates for environmental parameters
|
||||||
// Note: Reverb wet is fixed at 0.8 for spacious sound, not reactive to humidity
|
// Note: Reverb wet is fixed at 0.8 for spacious sound, not reactive to humidity
|
||||||
|
|
||||||
|
// Memoize delay time conversion to avoid repeated calculations
|
||||||
|
const delayTimeSeconds = $derived.by(() => {
|
||||||
|
return Tone.Time(delayTime).toSeconds();
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (delay && isInitialized) {
|
if (delay && isInitialized) {
|
||||||
delay.delayTime.value = delayTime;
|
delay.delayTime.rampTo(delayTimeSeconds, 0.5);
|
||||||
delay.feedback.rampTo(delayFeedback, 0.5);
|
delay.feedback.rampTo(delayFeedback, 0.5);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -151,6 +151,9 @@
|
|||||||
// Initialize audio components
|
// Initialize audio components
|
||||||
const initializeAudio = async (): Promise<void> => {
|
const initializeAudio = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
// Optimize audio scheduling for better stability
|
||||||
|
Tone.getContext().lookAhead = 0.1; // Keep default 100ms lookahead
|
||||||
|
|
||||||
// Create instruments
|
// Create instruments
|
||||||
synth = createPadSynth(isDay);
|
synth = createPadSynth(isDay);
|
||||||
arpSynth = createArpSynth(arpVolume);
|
arpSynth = createArpSynth(arpVolume);
|
||||||
@@ -219,10 +222,13 @@
|
|||||||
arpSequence = new Tone.Sequence(
|
arpSequence = new Tone.Sequence(
|
||||||
(time: number, chord) => {
|
(time: number, chord) => {
|
||||||
if (arpSynth && chord && chord.notes) {
|
if (arpSynth && chord && chord.notes) {
|
||||||
|
// Capture synth reference for TypeScript
|
||||||
|
const synth = arpSynth;
|
||||||
|
// Calculate time between notes based on 16th notes
|
||||||
|
const sixteenthNote = Tone.Time('16n').toSeconds();
|
||||||
// Play arpeggio pattern through the chord notes
|
// Play arpeggio pattern through the chord notes
|
||||||
chord.notes.forEach((note: string, index: number) => {
|
chord.notes.forEach((note: string, index: number) => {
|
||||||
const noteTime = time + index * 0.15; // 150ms between notes
|
synth.triggerAttackRelease(note, '16n', time + index * sixteenthNote);
|
||||||
arpSynth!.triggerAttackRelease(note, '16n', noteTime);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,13 +287,12 @@
|
|||||||
const octave = parseInt(rootNote.slice(-1)); // e.g., 4 from 'C4'
|
const octave = parseInt(rootNote.slice(-1)); // e.g., 4 from 'C4'
|
||||||
const bassNote = noteName + (octave - 2); // e.g., 'C2'
|
const bassNote = noteName + (octave - 2); // e.g., 'C2'
|
||||||
|
|
||||||
// Randomize release time: half to full chord duration
|
// Randomize note duration: half to full chord duration (2n to 4n)
|
||||||
// Quarter note = 1 beat, so random between 0.5 and 1.0 beats
|
const randomDuration = 0.5 + Math.random() * 0.5;
|
||||||
const randomRelease = 0.5 + Math.random() * 0.5;
|
const noteDuration = Tone.Time('4n').toSeconds() * randomDuration;
|
||||||
bassSynth.envelope.release = randomRelease * (60 / bpm);
|
|
||||||
|
|
||||||
// Trigger bass note
|
// Trigger bass note at the scheduled time
|
||||||
bassSynth.triggerAttackRelease(bassNote, '4n', time);
|
bassSynth.triggerAttackRelease(bassNote, noteDuration, time);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentProgression,
|
currentProgression,
|
||||||
@@ -346,9 +351,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Memoize delay time conversion to avoid repeated calculations
|
||||||
|
const delayTimeSeconds = $derived.by(() => {
|
||||||
|
return Tone.Time(delayTime).toSeconds();
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (delay && isInitialized) {
|
if (delay && isInitialized) {
|
||||||
delay.delayTime.value = delayTime;
|
delay.delayTime.rampTo(delayTimeSeconds, 0.5);
|
||||||
delay.feedback.rampTo(delayFeedback, 0.5);
|
delay.feedback.rampTo(delayFeedback, 0.5);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
static/favicon-16x16.png
Normal file
BIN
static/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 277 B |
BIN
static/favicon-32x32.png
Normal file
BIN
static/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 505 B |
BIN
static/favicon-96x96.png
Normal file
BIN
static/favicon-96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 505 B |
Reference in New Issue
Block a user