feat(audio,site): calm santoor default music + card-fan logo site redesign
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m0s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m18s

- 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:
soroush.asadi
2026-06-16 21:48:59 +03:30
parent 6aa4f37642
commit 9901c5e6d4
5 changed files with 192 additions and 32 deletions
+33 -10
View File
@@ -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);