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
+34
View File
@@ -78,3 +78,37 @@ body {
linear-gradient(-45deg, rgba(212, 175, 55, 0.04) 25%, transparent 25%); linear-gradient(-45deg, rgba(212, 175, 55, 0.04) 25%, transparent 25%);
background-size: 22px 22px; background-size: 22px 22px;
} }
/* Gentle float for the hero logo. */
@keyframes float-y {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
.float-y {
animation: float-y 5.5s ease-in-out infinite;
}
/* Soft gold halo behind the hero logo. */
.gold-halo {
background: radial-gradient(circle, rgba(212, 175, 55, 0.28), transparent 62%);
filter: blur(8px);
}
/* Card-suit accent for hero/section glyphs. */
.suit {
color: var(--gold);
opacity: 0.16;
user-select: none;
line-height: 1;
}
/* Thin gold hairline divider. */
.rule-gold {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.5), transparent);
}
@media (prefers-reduced-motion: reduce) {
.float-y { animation: none; }
html { scroll-behavior: auto; }
}
+21 -2
View File
@@ -2,6 +2,7 @@ import {
Users, Bot, Trophy, Gift, MessageCircle, Globe, ShieldCheck, Zap, Crown, Star, Users, Bot, Trophy, Gift, MessageCircle, Globe, ShieldCheck, Zap, Crown, Star,
} from "lucide-react"; } from "lucide-react";
import { DownloadButtons } from "@/components/DownloadButtons"; import { DownloadButtons } from "@/components/DownloadButtons";
import { Logo } from "@/components/Logo";
import { BRAND } from "@/lib/site"; import { BRAND } from "@/lib/site";
const FEATURES = [ const FEATURES = [
@@ -29,8 +30,22 @@ export default function Home() {
return ( return (
<> <>
{/* Hero */} {/* Hero */}
<section className="felt card-pattern"> <section className="felt card-pattern relative overflow-hidden">
{/* decorative card suits floating in the backdrop */}
<span className="suit pointer-events-none absolute right-[6%] top-16 text-8xl"></span>
<span className="suit pointer-events-none absolute left-[8%] top-40 text-7xl"></span>
<span className="suit pointer-events-none absolute left-[14%] bottom-12 text-6xl"></span>
<span className="suit pointer-events-none absolute right-[12%] bottom-20 text-7xl"></span>
<div className="mx-auto max-w-6xl px-4 py-16 text-center sm:py-24"> <div className="mx-auto max-w-6xl px-4 py-16 text-center sm:py-24">
{/* card-fan brand mark */}
<div className="relative mx-auto mb-8 grid h-40 w-40 place-items-center sm:h-48 sm:w-48">
<div className="gold-halo absolute inset-0 rounded-full" />
<div className="float-y relative">
<Logo size={160} glow />
</div>
</div>
<span className="inline-flex items-center gap-1.5 rounded-full glass px-3 py-1 text-xs text-gold-soft"> <span className="inline-flex items-center gap-1.5 rounded-full glass px-3 py-1 text-xs text-gold-soft">
<Star size={13} /> بازی حکمِ ایرانی، حرفهایتر از همیشه <Star size={13} /> بازی حکمِ ایرانی، حرفهایتر از همیشه
</span> </span>
@@ -48,7 +63,7 @@ export default function Home() {
<div className="mx-auto mt-12 grid max-w-3xl gap-3 sm:grid-cols-3"> <div className="mx-auto mt-12 grid max-w-3xl gap-3 sm:grid-cols-3">
{STATS.map((s) => ( {STATS.map((s) => (
<div key={s.label} className="glass rounded-2xl px-4 py-4"> <div key={s.label} className="glass rounded-2xl px-4 py-4 transition hover:border-gold/40">
<s.icon className="mx-auto text-teal" size={22} /> <s.icon className="mx-auto text-teal" size={22} />
<div className="mt-2 text-sm font-bold text-cream">{s.label}</div> <div className="mt-2 text-sm font-bold text-cream">{s.label}</div>
<div className="text-xs text-cream/55">{s.value}</div> <div className="text-xs text-cream/55">{s.value}</div>
@@ -66,6 +81,7 @@ export default function Home() {
<p className="mx-auto mt-3 max-w-xl text-center text-cream/60"> <p className="mx-auto mt-3 max-w-xl text-center text-cream/60">
همهٔ چیزی که یک بازی حکم بینقص لازم دارد، در یک اپ. همهٔ چیزی که یک بازی حکم بینقص لازم دارد، در یک اپ.
</p> </p>
<div className="rule-gold mx-auto mt-6 max-w-xs" />
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map((f) => ( {FEATURES.map((f) => (
<div key={f.title} className="glass rounded-2xl p-6 transition hover:border-gold/40"> <div key={f.title} className="glass rounded-2xl p-6 transition hover:border-gold/40">
@@ -97,6 +113,9 @@ export default function Home() {
{/* Final CTA */} {/* Final CTA */}
<section className="mx-auto max-w-4xl px-4 py-16 text-center"> <section className="mx-auto max-w-4xl px-4 py-16 text-center">
<div className="mx-auto mb-6 w-fit">
<Logo size={64} glow />
</div>
<h2 className="text-3xl font-black sm:text-4xl"> <h2 className="text-3xl font-black sm:text-4xl">
همین حالا <span className="gold-text">حکم</span> را شروع کن همین حالا <span className="gold-text">حکم</span> را شروع کن
</h2> </h2>
+59 -16
View File
@@ -1,24 +1,67 @@
export function Logo({ size = 36 }: { size?: number }) { /**
* Brand mark — the app's card-fan icon (mirrors public/icon.svg): three gold-edged
* playing cards fanned out, a spade on the face card. Scales cleanly from the nav
* (≈34px) to the hero (≈160px). `glow` adds a soft gold halo for hero use.
*/
export function Logo({ size = 36, glow = false }: { size?: number; glow?: boolean }) {
return ( return (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none" aria-hidden> <svg
<rect x="6" y="6" width="88" height="88" rx="22" fill="url(#lg)" stroke="#d4af37" strokeWidth="3" /> width={size}
<text height={size}
x="50" viewBox="0 0 512 512"
y="62" fill="none"
textAnchor="middle" xmlns="http://www.w3.org/2000/svg"
fontSize="46" aria-hidden
fontWeight="900" style={glow ? { filter: "drop-shadow(0 10px 36px rgba(212,175,55,0.35))" } : undefined}
fill="#d4af37"
fontFamily="Vazirmatn Variable, sans-serif"
> >
و
</text>
<defs> <defs>
<linearGradient id="lg" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse"> <radialGradient id="frbg" cx="50%" cy="36%" r="78%">
<stop stopColor="#111a33" /> <stop offset="0" stopColor="#16284f" />
<stop offset="1" stopColor="#070b18" /> <stop offset="0.62" stopColor="#0a142e" />
<stop offset="1" stopColor="#060c1f" />
</radialGradient>
<linearGradient id="frgold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#f6e4a0" />
<stop offset="0.5" stopColor="#d4af37" />
<stop offset="1" stopColor="#b8860b" />
</linearGradient>
<linearGradient id="frface" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#fffdf7" />
<stop offset="1" stopColor="#f1e6cd" />
</linearGradient>
<linearGradient id="frnavy" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#1d356a" />
<stop offset="1" stopColor="#0a142e" />
</linearGradient> </linearGradient>
</defs> </defs>
<rect width="512" height="512" rx="116" fill="url(#frbg)" />
<circle cx="256" cy="196" r="185" fill="#2dd4bf" opacity="0.07" />
<rect x="30" y="30" width="452" height="452" rx="100" fill="none" stroke="url(#frgold)" strokeWidth="6" opacity="0.6" />
<g transform="rotate(-25 256 396)">
<rect x="182" y="180" width="148" height="210" rx="16" fill="url(#frnavy)" stroke="url(#frgold)" strokeWidth="4" />
<rect x="198" y="196" width="116" height="178" rx="10" fill="none" stroke="#d4af37" strokeWidth="2" opacity="0.45" />
<path d="M256 250 l16 35 -16 35 -16 -35 z" fill="#d4af37" opacity="0.75" />
</g>
<g transform="rotate(25 256 396)">
<rect x="182" y="180" width="148" height="210" rx="16" fill="url(#frnavy)" stroke="url(#frgold)" strokeWidth="4" />
<rect x="198" y="196" width="116" height="178" rx="10" fill="none" stroke="#d4af37" strokeWidth="2" opacity="0.45" />
<path d="M256 250 l16 35 -16 35 -16 -35 z" fill="#d4af37" opacity="0.75" />
</g>
<g transform="translate(0 -24)">
<rect x="181" y="178" width="150" height="212" rx="16" fill="url(#frface)" stroke="url(#frgold)" strokeWidth="5" />
<rect x="193" y="190" width="126" height="188" rx="10" fill="none" stroke="#d4af37" strokeWidth="2" />
<g transform="translate(256 268) scale(1.45)">
<path
d="M0,-44 C0,-44 -42,-6 -42,16 C-42,30 -31,40 -18,40 C-11,40 -5,37 0,32 C-2,44 -10,52 -20,55 L20,55 C10,52 2,44 0,32 C5,37 11,40 18,40 C31,40 42,30 42,16 C42,-6 0,-44 0,-44 Z"
fill="url(#frgold)"
stroke="#7a5a00"
strokeWidth="1.5"
/>
</g>
</g>
</svg> </svg>
); );
} }
+44 -3
View File
@@ -1,4 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="6" width="88" height="88" rx="22" fill="#0b1226" stroke="#d4af37" stroke-width="4"/> <defs>
<text x="50" y="64" text-anchor="middle" font-size="48" font-weight="900" fill="#d4af37" font-family="Tahoma, sans-serif">و</text> <radialGradient id="bg" cx="50%" cy="36%" r="78%">
<stop offset="0" stop-color="#16284f"/>
<stop offset="0.62" stop-color="#0a142e"/>
<stop offset="1" stop-color="#060c1f"/>
</radialGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f6e4a0"/>
<stop offset="0.5" stop-color="#d4af37"/>
<stop offset="1" stop-color="#b8860b"/>
</linearGradient>
<linearGradient id="face" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fffdf7"/>
<stop offset="1" stop-color="#f1e6cd"/>
</linearGradient>
<linearGradient id="navy" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#1d356a"/>
<stop offset="1" stop-color="#0a142e"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="116" fill="url(#bg)"/>
<circle cx="256" cy="196" r="185" fill="#2dd4bf" opacity="0.07"/>
<rect x="30" y="30" width="452" height="452" rx="100" fill="none" stroke="url(#gold)" stroke-width="6" opacity="0.6"/>
<g transform="rotate(-25 256 396)">
<rect x="182" y="180" width="148" height="210" rx="16" fill="url(#navy)" stroke="url(#gold)" stroke-width="4"/>
<rect x="198" y="196" width="116" height="178" rx="10" fill="none" stroke="#d4af37" stroke-width="2" opacity="0.45"/>
<path d="M256 250 l16 35 -16 35 -16 -35 z" fill="#d4af37" opacity="0.75"/>
</g>
<g transform="rotate(25 256 396)">
<rect x="182" y="180" width="148" height="210" rx="16" fill="url(#navy)" stroke="url(#gold)" stroke-width="4"/>
<rect x="198" y="196" width="116" height="178" rx="10" fill="none" stroke="#d4af37" stroke-width="2" opacity="0.45"/>
<path d="M256 250 l16 35 -16 35 -16 -35 z" fill="#d4af37" opacity="0.75"/>
</g>
<g transform="translate(0 -24)">
<rect x="181" y="178" width="150" height="212" rx="16" fill="url(#face)" stroke="url(#gold)" stroke-width="5"/>
<rect x="193" y="190" width="126" height="188" rx="10" fill="none" stroke="#d4af37" stroke-width="2"/>
<g transform="translate(256 268) scale(1.45)">
<path d="M0,-44 C0,-44 -42,-6 -42,16 C-42,30 -31,40 -18,40 C-11,40 -5,37 0,32 C-2,44 -10,52 -20,55 L20,55 C10,52 2,44 0,32 C5,37 11,40 18,40 C31,40 42,30 42,16 C42,-6 0,-44 0,-44 Z" fill="url(#gold)" stroke="#7a5a00" stroke-width="1.5"/>
</g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 2.4 KiB

+33 -10
View File
@@ -38,7 +38,7 @@ class SoundManager {
sfxEnabled = loadBool(LS_SFX); sfxEnabled = loadBool(LS_SFX);
musicEnabled = loadBool(LS_MUSIC); musicEnabled = loadBool(LS_MUSIC);
musicTrack: MusicTrack = 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. */ /** Must be called from a user gesture to unlock audio. */
init() { init() {
@@ -198,11 +198,22 @@ class SoundManager {
// • playful — bouncy major-pentatonic staccato loop (UNO-like). // • playful — bouncy major-pentatonic staccato loop (UNO-like).
private TRACKS: Record< private TRACKS: Record<
MusicTrack, 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: { santoor: {
notes: [293.66, 311.13, 392, 440, 466.16, 392, 311.13, 293.66], // Dastgah-e Shur on D — a calm phrase that rises then settles, looping
gap: 900, type: "sine", attack: 0.3, dur: 1.6, peak: 0.5, fifth: true, // 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: { playful: {
notes: [523.25, 659.25, 784, 659.25, 587.33, 698.46, 880, 698.46, 587.33, 523.25], 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; if (!this.ctx || !this.musicGain) return;
const cfg = this.TRACKS[this.musicTrack]; const cfg = this.TRACKS[this.musicTrack];
const now = this.ctx.currentTime; 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++; 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 osc = this.ctx!.createOscillator();
const g = this.ctx!.createGain(); const g = this.ctx!.createGain();
osc.type = cfg.type; osc.type = type;
osc.frequency.value = f; osc.frequency.value = f;
g.gain.setValueAtTime(0.0001, now); g.gain.setValueAtTime(0.0001, now);
g.gain.linearRampToValueAtTime(cfg.peak * scale, now + cfg.attack); g.gain.linearRampToValueAtTime(cfg.peak * scale, now + attack);
g.gain.exponentialRampToValueAtTime(0.0001, now + cfg.dur); g.gain.exponentialRampToValueAtTime(0.0001, now + dur);
osc.connect(g); osc.connect(g);
g.connect(this.musicGain!); g.connect(this.musicGain!);
osc.start(now); osc.start(now);
osc.stop(now + cfg.dur + 0.05); osc.stop(now + dur + 0.05);
}; };
voice(freq); voice(freq);
if (cfg.fifth) voice(freq * 1.5, 0.45); // soft fifth above 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(); tick();
this.musicTimer = setInterval(tick, this.TRACKS[this.musicTrack].gap); this.musicTimer = setInterval(tick, this.TRACKS[this.musicTrack].gap);