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:
@@ -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
@@ -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>
|
||||||
|
|||||||
+60
-17
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user