Files
MathApp_frontend/src/mascots/seyar.ts

224 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* همراه‌های اپ: سیگما (Σ)، دلتا (Δ)، پی (π) و امگا (ω). «سه‌یار» نامِ قدیمیِ سه‌تای اول است؛ در متنِ
* کاربر تعدادِ همراه‌ها نوشته نمی‌شود، چون کاتالوگ از ادمین می‌آید و بزرگ می‌شود.
*
* هر سه از دلِ یک دفترچه‌ی بی‌نهایت آمده‌اند و هر کدام از جنسِ یک نمادِ ریاضی‌اند:
* سیگما → راهنمای درس و آزمون دلتا → هم‌تیمیِ تمرینِ سریع پی → دستیار و همراهِ دلگرمی
*
* خروجی رشته‌ی SVG است (نه JSX) تا هر سه با یک موتورِ چهره ساخته شوند.
* ورودی‌ها همه از همین فایل‌اند و هیچ متنِ کاربر واردِ SVG نمی‌شود.
*/
export type SeyarChar = 'sigma' | 'delta' | 'pi' | 'tedi';
export type SeyarMood = 'idle' | 'happy' | 'excited' | 'sad' | 'angry' | 'depressed';
const PALETTE: Record<SeyarChar, Record<string, string>> = {
sigma: { body: "#4C5FD5", inner: "#7A8CFF", belly: "#EAF0FF", ink: "#232355", cheek: "#FFB2C0", mouth: "#FF6B6B", tear: "#8EC9FF", badge: "#FFD23F" },
delta: { body: "#FF7A3D", inner: "#FFB25E", belly: "#FFE7C7", ink: "#6E2A12", cheek: "#FF8FA3", mouth: "#D6402A", tear: "#8EC9FF", spike: "#FFEB3B" },
pi: { body: "#9B7BFF", inner: "#C6B3FF", belly: "#F1ECFF", ink: "#3B2B66", cheek: "#FFC2D1", mouth: "#FF6F91", tear: "#5EC8E8" },
tedi: { body: "#FFC63D", inner: "#FF9E42", belly: "#FFE9B0", ink: "#4A3B32", cheek: "#FFAEB0", mouth: "#E7573F", tear: "#7EC8E3" },
};
const Y_SHIFT: Record<SeyarMood, number> = { idle: 0, happy: 0, excited: -4, sad: 3, angry: 0, depressed: 9 };
const ARM_DELTA: Record<SeyarMood, number> = { idle: 0, happy: 2, excited: 46, sad: -4, angry: -8, depressed: -10 };
function face(mood: SeyarMood, ex: number, ey: number, mx: number, my: number, ink: string, mouthC: string, tearC: string, cheekC: string, spread: number) {
const lx = ex, rx = ex + spread;
let eyes = "", mouth = "", cheeks = "", extra = "";
if (mood === "idle") {
eyes = `<circle cx="${lx}" cy="${ey}" r="9" fill="${ink}"/><circle cx="${rx}" cy="${ey}" r="9" fill="${ink}"/>`
+ `<circle cx="${lx + 3}" cy="${ey - 3}" r="3" fill="#fff"/><circle cx="${rx + 3}" cy="${ey - 3}" r="3" fill="#fff"/>`;
mouth = `<path d="M${mx - 14} ${my} q14 10 28 0" fill="none" stroke="${ink}" stroke-width="5" stroke-linecap="round"/>`;
} else if (mood === "happy") {
eyes = `<path d="M${lx - 11} ${ey} q11 -14 22 0" fill="none" stroke="${ink}" stroke-width="5.5" stroke-linecap="round"/>`
+ `<path d="M${rx - 11} ${ey} q11 -14 22 0" fill="none" stroke="${ink}" stroke-width="5.5" stroke-linecap="round"/>`;
mouth = `<path d="M${mx - 20} ${my - 4} q20 26 40 0 q-20 10 -40 0z" fill="${mouthC}" stroke="${ink}" stroke-width="4.5" stroke-linejoin="round"/>`;
cheeks = `<ellipse cx="${lx - 14}" cy="${my - 14}" rx="11" ry="7" fill="${cheekC}"/><ellipse cx="${rx + 14}" cy="${my - 14}" rx="11" ry="7" fill="${cheekC}"/>`;
} else if (mood === "excited") {
eyes = `<circle cx="${lx}" cy="${ey}" r="11" fill="${ink}"/><circle cx="${rx}" cy="${ey}" r="11" fill="${ink}"/>`
+ `<circle cx="${lx + 4}" cy="${ey - 4}" r="4" fill="#fff"/><circle cx="${rx + 4}" cy="${ey - 4}" r="4" fill="#fff"/>`
+ `<path d="M${lx - 16} ${ey - 16} l3 6 l6 -2 l-4 5 l4 5 l-6 -2 l-3 6 l-1 -6 l-6 1 l5 -4 l-5 -4 l6 1z" fill="${tearC}" opacity=".85"/>`;
mouth = `<ellipse cx="${mx}" cy="${my + 2}" rx="16" ry="14" fill="${mouthC}" stroke="${ink}" stroke-width="4.5"/>`;
cheeks = `<ellipse cx="${lx - 14}" cy="${my - 12}" rx="12" ry="8" fill="${cheekC}"/><ellipse cx="${rx + 14}" cy="${my - 12}" rx="12" ry="8" fill="${cheekC}"/>`;
extra = `<g stroke="${tearC}" stroke-width="4" stroke-linecap="round"><path d="M${lx - 30} ${ey - 24} l-7 -10"/><path d="M${rx + 30} ${ey - 24} l7 -10"/></g>`;
} else if (mood === "sad") {
eyes = `<circle cx="${lx}" cy="${ey + 2}" r="8" fill="${ink}"/><circle cx="${rx}" cy="${ey + 2}" r="8" fill="${ink}"/>`
+ `<path d="M${lx - 10} ${ey - 10} q9 -6 18 -2" fill="none" stroke="${ink}" stroke-width="5" stroke-linecap="round"/>`
+ `<path d="M${rx - 8} ${ey - 12} q9 -4 18 2" fill="none" stroke="${ink}" stroke-width="5" stroke-linecap="round"/>`;
mouth = `<path d="M${mx - 16} ${my + 8} q16 -14 32 0" fill="none" stroke="${ink}" stroke-width="5" stroke-linecap="round"/>`;
extra = `<path d="M${lx - 2} ${ey + 10} q-4 12 0 18 q4 -6 0 -18z" fill="${tearC}"/>`;
} else if (mood === "angry") {
eyes = `<path d="M${lx - 9} ${ey - 4} l14 4 l-4 8 z" fill="${ink}"/><path d="M${rx + 9} ${ey - 4} l-14 4 l4 8 z" fill="${ink}"/>`
+ `<path d="M${lx - 12} ${ey - 12} l16 4" stroke="${ink}" stroke-width="5" stroke-linecap="round"/>`
+ `<path d="M${rx + 12} ${ey - 12} l-16 4" stroke="${ink}" stroke-width="5" stroke-linecap="round"/>`;
mouth = `<path d="M${mx - 14} ${my + 4} l6 -6 l6 6 l6 -6 l6 6 l6 -6" fill="none" stroke="${ink}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>`;
cheeks = `<ellipse cx="${lx - 14}" cy="${my - 14}" rx="9" ry="6" fill="${mouthC}" opacity=".55"/><ellipse cx="${rx + 14}" cy="${my - 14}" rx="9" ry="6" fill="${mouthC}" opacity=".55"/>`;
extra = `<g stroke="${ink}" stroke-width="4" stroke-linecap="round" opacity=".8"><path d="M${lx - 34} ${ey - 30} q4 -8 -2 -14"/><path d="M${rx + 34} ${ey - 30} q-4 -8 2 -14"/></g>`;
} else if (mood === "depressed") {
eyes = `<path d="M${lx - 10} ${ey} q10 5 20 0" fill="none" stroke="${ink}" stroke-width="6" stroke-linecap="round"/>`
+ `<path d="M${rx - 10} ${ey} q10 5 20 0" fill="none" stroke="${ink}" stroke-width="6" stroke-linecap="round"/>`;
mouth = `<path d="M${mx - 12} ${my} q12 6 24 1" fill="none" stroke="${ink}" stroke-width="4.5" stroke-linecap="round"/>`;
extra = `<g><ellipse cx="${mx}" cy="${ey - 58}" rx="26" ry="10" fill="#B9C2CC" opacity=".9"/>`
+ `<ellipse cx="${mx - 16}" cy="${ey - 52}" rx="16" ry="8" fill="#B9C2CC" opacity=".9"/>`
+ `<ellipse cx="${mx + 18}" cy="${ey - 52}" rx="15" ry="8" fill="#B9C2CC" opacity=".9"/>`
+ `<g stroke="#8FA0B3" stroke-width="3.5" stroke-linecap="round" opacity=".8">`
+ `<line x1="${mx - 14}" y1="${ey - 42}" x2="${mx - 18}" y2="${ey - 32}"/>`
+ `<line x1="${mx}" y1="${ey - 40}" x2="${mx - 4}" y2="${ey - 28}"/>`
+ `<line x1="${mx + 14}" y1="${ey - 42}" x2="${mx + 10}" y2="${ey - 30}"/></g></g>`;
}
return { eyes, mouth, cheeks, extra };
}
function seyarSigmaSVG(mood: SeyarMood): string {
const C = PALETTE.sigma;
const f = face(mood, 80, 86, 100, 114, C.ink, C.mouth, C.tear, C.cheek, 40);
const a = 12 + ARM_DELTA[mood], dy = Y_SHIFT[mood];
return `<svg viewBox="-14 -34 228 250" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 ${dy})">
<line x1="100" y1="18" x2="100" y2="-8" stroke="${C.ink}" stroke-width="6" stroke-linecap="round"/>
<circle cx="100" cy="-14" r="9" fill="${C.badge}" stroke="${C.ink}" stroke-width="5"/>
<rect x="52" y="176" width="26" height="16" rx="8" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<rect x="122" y="176" width="26" height="16" rx="8" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<rect x="14" y="96" width="24" height="52" rx="12" fill="${C.body}" stroke="${C.ink}" stroke-width="5" transform="rotate(${a} 26 122)"/>
<rect x="162" y="96" width="24" height="52" rx="12" fill="${C.body}" stroke="${C.ink}" stroke-width="5" transform="rotate(${-a} 174 122)"/>
<rect x="20" y="70" width="14" height="34" rx="7" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<rect x="166" y="70" width="14" height="34" rx="7" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<rect x="34" y="24" width="132" height="150" rx="38" fill="${C.body}" stroke="${C.ink}" stroke-width="6"/>
<rect x="54" y="44" width="92" height="98" rx="24" fill="${C.belly}" stroke="${C.ink}" stroke-width="4"/>
${f.extra}<g>${f.eyes}</g>${f.cheeks}${f.mouth}
<circle cx="100" cy="160" r="14" fill="${C.badge}" stroke="${C.ink}" stroke-width="4"/>
<text x="100" y="167" font-size="18" text-anchor="middle" font-family="Georgia,serif" font-weight="bold" fill="${C.ink}">&#931;</text>
</g></svg>`;
}
/** نشانِ حرفِ یونانیِ هر همراه (مثلِ مدالِ Σِ سیگما): دایره‌ی کوچک روی شکم با نمادِ خودش. */
function letterBadge(cx: number, cy: number, entity: string, fill: string, ink: string): string {
return `<circle cx="${cx}" cy="${cy}" r="13" fill="${fill}" stroke="${ink}" stroke-width="4"/>`
+ `<text x="${cx}" y="${cy + 6.5}" font-size="18" text-anchor="middle" font-family="Georgia,serif" font-weight="bold" fill="${ink}">${entity}</text>`;
}
function seyarDeltaSVG(mood: SeyarMood): string {
const C = PALETTE.delta;
const f = face(mood, 84, 108, 100, 128, C.ink, C.mouth, C.tear, C.cheek, 32);
const a = 16 + ARM_DELTA[mood], dy = Y_SHIFT[mood];
return `<svg viewBox="-14 -34 228 250" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 ${dy})">
<ellipse cx="76" cy="182" rx="18" ry="12" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<ellipse cx="124" cy="182" rx="18" ry="12" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<rect x="18" y="106" width="22" height="46" rx="11" fill="${C.body}" stroke="${C.ink}" stroke-width="5" transform="rotate(${a} 29 129)"/>
<rect x="160" y="106" width="22" height="46" rx="11" fill="${C.body}" stroke="${C.ink}" stroke-width="5" transform="rotate(${-a} 171 129)"/>
<path d="M62 46 L48 16 L70 34 Z" fill="${C.spike}" stroke="${C.ink}" stroke-width="5" stroke-linejoin="round"/>
<path d="M138 46 L152 16 L130 34 Z" fill="${C.spike}" stroke="${C.ink}" stroke-width="5" stroke-linejoin="round"/>
<path d="M100 30 Q112 30 118 42 L160 156 Q166 172 150 174 L50 174 Q34 172 40 156 L82 42 Q88 30 100 30 Z" fill="${C.body}" stroke="${C.ink}" stroke-width="6" stroke-linejoin="round"/>
<path d="M100 74 Q108 74 112 84 L136 148 Q140 158 128 158 L72 158 Q60 158 64 148 L88 84 Q92 74 100 74 Z" fill="${C.belly}"/>
${f.extra}<g>${f.eyes}</g>${f.cheeks}${f.mouth}
${letterBadge(100, 160, '&#916;', '#FFD23F', C.ink)}
</g></svg>`;
}
function seyarPiSVG(mood: SeyarMood): string {
const C = PALETTE.pi;
const f = face(mood, 80, 112, 100, 138, C.ink, C.mouth, C.tear, C.cheek, 40);
const a = 18 + ARM_DELTA[mood], dy = Y_SHIFT[mood];
return `<svg viewBox="-14 -34 228 250" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 ${dy})">
<ellipse cx="76" cy="184" rx="19" ry="12" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<ellipse cx="124" cy="184" rx="19" ry="12" fill="${C.inner}" stroke="${C.ink}" stroke-width="5"/>
<rect x="16" y="112" width="22" height="48" rx="11" fill="${C.body}" stroke="${C.ink}" stroke-width="5" transform="rotate(${a} 27 136)"/>
<rect x="162" y="112" width="22" height="48" rx="11" fill="${C.body}" stroke="${C.ink}" stroke-width="5" transform="rotate(${-a} 173 136)"/>
<g stroke="${C.ink}" stroke-width="7" stroke-linecap="round">
<line x1="78" y1="22" x2="122" y2="22"/><line x1="86" y1="22" x2="84" y2="44"/><line x1="114" y1="22" x2="116" y2="44"/>
</g>
<ellipse cx="100" cy="118" rx="74" ry="72" fill="${C.body}" stroke="${C.ink}" stroke-width="6"/>
<ellipse cx="100" cy="134" rx="46" ry="46" fill="${C.belly}"/>
${f.extra}<g>${f.eyes}</g>${f.cheeks}${f.mouth}
${letterBadge(100, 170, '&#960;', '#FFD23F', C.ink)}
</g></svg>`;
}
/**
* امگا (ω؛ تا ۱۴۰۵-۰۷ «تدی»؛ کلیدِ داخلی‌اش هنوز `tedi` است چون slugِ سرور همین است) — شکلکِ زردِ پیش‌نمایشِ موبایل (mascot-pack، ریپوی بک‌اند، پوشه‌ی
* mobile-preview). بدن، گوش‌ها، پاها و دست‌ها عیناً از همان پک است؛ چهره‌ی
* «عصبانی» و «ناامید» را پک نداشت و با همان زبانِ طراحیِ سه‌یار اضافه شد.
*/
function tediSVG(mood: SeyarMood): string {
const C = PALETTE.tedi;
const S = `stroke="${C.ink}" stroke-width="5"`;
const dy = Y_SHIFT[mood];
const restArmL = `<rect x="20" y="112" width="24" height="46" rx="12" fill="${C.body}" ${S} transform="rotate(14 32 135)"/>`;
const restArmR = `<rect x="156" y="112" width="24" height="46" rx="12" fill="${C.body}" ${S} transform="rotate(-14 168 135)"/>`;
const upArmL = `<rect x="14" y="70" width="24" height="50" rx="12" fill="${C.body}" ${S} transform="rotate(28 26 95)"/>`;
const upArmR = `<rect x="162" y="70" width="24" height="50" rx="12" fill="${C.body}" ${S} transform="rotate(-28 174 95)"/>`;
const droopArmL = `<rect x="24" y="118" width="24" height="46" rx="12" fill="${C.body}" ${S} transform="rotate(4 36 141)"/>`;
const droopArmR = `<rect x="152" y="118" width="24" height="46" rx="12" fill="${C.body}" ${S} transform="rotate(-4 164 141)"/>`;
let arms = restArmL + restArmR, hands = '', eyes = '', mouth = '', cheeks = '', extra = '';
if (mood === 'happy' || mood === 'excited') {
eyes = `<path d="M66 100 q10 -13 20 0" fill="none" ${S} stroke-linecap="round"/><path d="M114 100 q10 -13 20 0" fill="none" ${S} stroke-linecap="round"/>`;
mouth = `<path d="M78 122 q22 34 44 0 q-22 12 -44 0z" fill="${C.mouth}" ${S} stroke-linejoin="round"/>`;
cheeks = `<ellipse cx="64" cy="122" rx="12" ry="8" fill="${C.cheek}"/><ellipse cx="136" cy="122" rx="12" ry="8" fill="${C.cheek}"/>`;
if (mood === 'excited') {
arms = upArmL + upArmR;
hands = `<circle cx="18" cy="66" r="12" fill="${C.body}" ${S}/><circle cx="182" cy="66" r="12" fill="${C.body}" ${S}/>`;
extra = `<g stroke="${C.inner}" stroke-width="5" stroke-linecap="round"><path d="M40 30 l-8 -12"/><path d="M160 30 l8 -12"/><path d="M100 8 v-12"/></g>`;
}
} else if (mood === 'sad') {
eyes = `<circle cx="78" cy="104" r="8" fill="${C.ink}"/><circle cx="122" cy="104" r="8" fill="${C.ink}"/>`
+ `<path d="M68 94 q10 -6 20 -2" fill="none" ${S} stroke-linecap="round"/><path d="M112 92 q10 -4 20 2" fill="none" ${S} stroke-linecap="round"/>`;
mouth = `<path d="M82 142 q18 -18 36 0" fill="none" ${S} stroke-linecap="round"/>`;
extra = `<path d="M78 112 q-4 12 0 18 q4 -6 0 -18z" fill="${C.tear}"/>`;
} else if (mood === 'angry') {
eyes = `<path d="M68 98 l16 5 l-5 9 z" fill="${C.ink}"/><path d="M132 98 l-16 5 l5 9 z" fill="${C.ink}"/>`
+ `<path d="M64 88 l22 7" ${S} stroke-linecap="round"/><path d="M136 88 l-22 7" ${S} stroke-linecap="round"/>`;
mouth = `<path d="M82 136 l6 -6 l6 6 l6 -6 l6 6 l6 -6 l6 6" fill="none" ${S} stroke-linecap="round" stroke-linejoin="round"/>`;
cheeks = `<ellipse cx="62" cy="124" rx="10" ry="6" fill="${C.mouth}" opacity=".5"/><ellipse cx="138" cy="124" rx="10" ry="6" fill="${C.mouth}" opacity=".5"/>`;
extra = `<g stroke="${C.ink}" stroke-width="4" stroke-linecap="round" opacity=".75"><path d="M30 20 q4 -8 -2 -14"/><path d="M170 20 q-4 -8 2 -14"/></g>`;
} else if (mood === 'depressed') {
arms = droopArmL + droopArmR;
eyes = `<path d="M68 104 q10 5 20 0" fill="none" stroke="${C.ink}" stroke-width="6" stroke-linecap="round"/><path d="M112 104 q10 5 20 0" fill="none" stroke="${C.ink}" stroke-width="6" stroke-linecap="round"/>`;
mouth = `<path d="M88 136 q12 6 24 1" fill="none" ${S} stroke-linecap="round"/>`;
extra = `<g><ellipse cx="100" cy="-6" rx="26" ry="10" fill="#B9C2CC" opacity=".9"/><ellipse cx="84" cy="0" rx="16" ry="8" fill="#B9C2CC" opacity=".9"/><ellipse cx="118" cy="0" rx="15" ry="8" fill="#B9C2CC" opacity=".9"/>`
+ `<g stroke="#8FA0B3" stroke-width="3.5" stroke-linecap="round" opacity=".8"><line x1="86" y1="10" x2="82" y2="20"/><line x1="100" y1="12" x2="96" y2="24"/><line x1="114" y1="10" x2="110" y2="22"/></g></g>`;
} else {
eyes = `<circle cx="78" cy="102" r="9" fill="${C.ink}"/><circle cx="122" cy="102" r="9" fill="${C.ink}"/>`
+ `<circle cx="81" cy="99" r="3" fill="#fff"/><circle cx="125" cy="99" r="3" fill="#fff"/>`;
mouth = `<path d="M84 128 q16 18 32 0" fill="none" ${S} stroke-linecap="round"/>`;
}
return `<svg viewBox="-14 -34 228 250" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 ${dy})">
${extra}
<ellipse cx="74" cy="185" rx="20" ry="13" fill="${C.inner}" ${S}/><ellipse cx="126" cy="185" rx="20" ry="13" fill="${C.inner}" ${S}/>
${arms}
<circle cx="56" cy="46" r="24" fill="${C.body}" ${S}/><circle cx="144" cy="46" r="24" fill="${C.body}" ${S}/>
<circle cx="56" cy="46" r="12" fill="${C.inner}"/><circle cx="144" cy="46" r="12" fill="${C.inner}"/>
<ellipse cx="100" cy="116" rx="72" ry="70" fill="${C.body}" ${S}/>
<ellipse cx="100" cy="133" rx="44" ry="45" fill="${C.belly}"/>
${hands}${cheeks}<g>${eyes}</g>${mouth}
${letterBadge(100, 166, '&#969;', '#fff', C.ink)}
</g></svg>`;
}
export const SEYAR_SVG: Record<SeyarChar, (mood: SeyarMood) => string> = {
sigma: seyarSigmaSVG, delta: seyarDeltaSVG, pi: seyarPiSVG, tedi: tediSVG,
};
export const SEYAR_INFO: Record<SeyarChar, { name: string; symbol: string; role: string }> = {
sigma: { name: "سیگما", symbol: "Σ", role: "راهنمای آزمون و کوییز" },
delta: { name: "دلتا", symbol: "Δ", role: "هم‌تیمی تمرین‌های سریع" },
pi: { name: "پی", symbol: "π", role: "همراه دلگرمی" },
tedi: { name: "امگا", symbol: "ω", role: "رفیق همیشگی تدریسینو ریاضی" },
};
/** slugِ کاراکترِ سرور (sigma/delta/pi/tedi) را به یکی از همراه‌ها نگاشت می‌کند. */
export function seyarFromSlug(slug: string | null | undefined): SeyarChar | null {
const s = (slug ?? '').toLowerCase();
if (s === 'sigma' || s === 'delta' || s === 'pi' || s === 'tedi') return s;
return null;
}
/** حالتِ ماسکوت که سرور می‌دهد («happy»، «encourage»، …) را به حالتِ سه‌یار نگاشت می‌کند. */
export function moodFromServer(server: string | null | undefined): SeyarMood {
const s = (server ?? '').toLowerCase();
if (s.includes('excited') || s.includes('celebrat')) return 'excited';
if (s.includes('happy') || s.includes('encourage')) return 'happy';
if (s.includes('sad')) return 'sad';
if (s.includes('angry')) return 'angry';
if (s.includes('depress') || s.includes('tired')) return 'depressed';
return 'idle';
}