feat(audio,site): calm santoor default music + card-fan logo site redesign
- audio: default background music is now the santoor track (calm Persian), rebuilt as a real plucked-santoor loop — fast metallic attack, shimmer overtones, soft tonic drone, longer Dastgah-e-Shur phrase - site: marketing logo is now the app's card-fan icon (Logo.tsx + icon.svg); hero features the big logo with gold halo, floating suit motifs, and polished section dividers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-10
@@ -38,7 +38,7 @@ class SoundManager {
|
||||
sfxEnabled = loadBool(LS_SFX);
|
||||
musicEnabled = loadBool(LS_MUSIC);
|
||||
musicTrack: MusicTrack =
|
||||
(typeof window !== "undefined" && (localStorage.getItem(LS_TRACK) as MusicTrack)) || "playful";
|
||||
(typeof window !== "undefined" && (localStorage.getItem(LS_TRACK) as MusicTrack)) || "santoor";
|
||||
|
||||
/** Must be called from a user gesture to unlock audio. */
|
||||
init() {
|
||||
@@ -198,11 +198,22 @@ class SoundManager {
|
||||
// • playful — bouncy major-pentatonic staccato loop (UNO-like).
|
||||
private TRACKS: Record<
|
||||
MusicTrack,
|
||||
{ notes: number[]; gap: number; type: OscillatorType; attack: number; dur: number; peak: number; fifth: boolean }
|
||||
{
|
||||
notes: number[]; gap: number; type: OscillatorType; attack: number; dur: number;
|
||||
peak: number; fifth: boolean;
|
||||
shimmer?: boolean; // bright metallic overtones — santoor pluck character
|
||||
bassHz?: number; // soft sustained tonic drone underneath
|
||||
}
|
||||
> = {
|
||||
santoor: {
|
||||
notes: [293.66, 311.13, 392, 440, 466.16, 392, 311.13, 293.66],
|
||||
gap: 900, type: "sine", attack: 0.3, dur: 1.6, peak: 0.5, fifth: true,
|
||||
// Dastgah-e Shur on D — a calm phrase that rises then settles, looping
|
||||
// seamlessly. Plucked (fast attack, long decay) with metallic shimmer.
|
||||
notes: [
|
||||
293.66, 349.23, 392.0, 440.0, 392.0, 349.23, 311.13, 293.66,
|
||||
349.23, 392.0, 466.16, 440.0, 392.0, 349.23, 311.13, 293.66,
|
||||
],
|
||||
gap: 470, type: "triangle", attack: 0.004, dur: 1.15, peak: 0.4,
|
||||
fifth: false, shimmer: true, bassHz: 73.42, // D2 drone
|
||||
},
|
||||
playful: {
|
||||
notes: [523.25, 659.25, 784, 659.25, 587.33, 698.46, 880, 698.46, 587.33, 523.25],
|
||||
@@ -218,23 +229,35 @@ class SoundManager {
|
||||
if (!this.ctx || !this.musicGain) return;
|
||||
const cfg = this.TRACKS[this.musicTrack];
|
||||
const now = this.ctx.currentTime;
|
||||
const freq = cfg.notes[this.step % cfg.notes.length];
|
||||
const idx = this.step % cfg.notes.length;
|
||||
const freq = cfg.notes[idx];
|
||||
this.step++;
|
||||
const voice = (f: number, scale = 1) => {
|
||||
const voice = (f: number, scale = 1, type = cfg.type, attack = cfg.attack, dur = cfg.dur) => {
|
||||
const osc = this.ctx!.createOscillator();
|
||||
const g = this.ctx!.createGain();
|
||||
osc.type = cfg.type;
|
||||
osc.type = type;
|
||||
osc.frequency.value = f;
|
||||
g.gain.setValueAtTime(0.0001, now);
|
||||
g.gain.linearRampToValueAtTime(cfg.peak * scale, now + cfg.attack);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, now + cfg.dur);
|
||||
g.gain.linearRampToValueAtTime(cfg.peak * scale, now + attack);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, now + dur);
|
||||
osc.connect(g);
|
||||
g.connect(this.musicGain!);
|
||||
osc.start(now);
|
||||
osc.stop(now + cfg.dur + 0.05);
|
||||
osc.stop(now + dur + 0.05);
|
||||
};
|
||||
voice(freq);
|
||||
if (cfg.fifth) voice(freq * 1.5, 0.45); // soft fifth above
|
||||
if (cfg.shimmer) {
|
||||
// Metallic santoor overtones: a brighter octave + a faint third partial,
|
||||
// both with quick decay so the pluck shimmers then fades.
|
||||
voice(freq * 2, 0.16, "sine", 0.003, cfg.dur * 0.55);
|
||||
voice(freq * 3, 0.06, "sine", 0.003, cfg.dur * 0.4);
|
||||
}
|
||||
// Soft sustained tonic drone re-struck once per phrase half.
|
||||
if (cfg.bassHz && idx % (cfg.notes.length / 2) === 0) {
|
||||
const span = (cfg.gap / 1000) * (cfg.notes.length / 2);
|
||||
voice(cfg.bassHz, 0.5, "sine", 0.25, span * 0.95);
|
||||
}
|
||||
};
|
||||
tick();
|
||||
this.musicTimer = setInterval(tick, this.TRACKS[this.musicTrack].gap);
|
||||
|
||||
Reference in New Issue
Block a user