forked from Tadrisino/MathApp_frontend
play whole islands, make home the path itself, draw tutor diagrams, add tedi
- Home is now the island path of every unit with one clear Start button on the
current island; the six-card dashboard is gone.
- Start opens /nodes/{id}/quiz (4 questions) instead of one-question lessons, so
an island takes one round instead of ~11. Lessons stay one tap away in the sheet.
- Tutor answers render the backend's validated diagram (plane, number line,
fraction) as React SVG; function expressions go through a small parser, no eval.
- Tedi, the mobile-preview mascot, joins as the fourth companion with all six moods
and an onboarding slide.
- Light polish: 3D buttons, header gradients, warmer page background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* سهیار: سیگما (Σ)، دلتا (Δ) و پی (π) — ماسکوتهای اپ.
|
||||
* همراههای اپ: سهیار — سیگما (Σ)، دلتا (Δ) و پی (π) — و تدی.
|
||||
*
|
||||
* هر سه از دلِ یک دفترچهی بینهایت آمدهاند و هر کدام از جنسِ یک نمادِ ریاضیاند:
|
||||
* سیگما → راهنمای درس و آزمون دلتا → همتیمیِ تمرینِ سریع پی → دستیار و همراهِ دلگرمی
|
||||
@@ -8,13 +8,14 @@
|
||||
* ورودیها همه از همین فایلاند و هیچ متنِ کاربر واردِ SVG نمیشود.
|
||||
*/
|
||||
|
||||
export type SeyarChar = 'sigma' | 'delta' | 'pi';
|
||||
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 };
|
||||
@@ -122,19 +123,81 @@ function seyarPiSVG(mood: SeyarMood): string {
|
||||
</g></svg>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* تدی — شکلکِ زردِ پیشنمایشِ موبایل (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}
|
||||
</g></svg>`;
|
||||
}
|
||||
|
||||
export const SEYAR_SVG: Record<SeyarChar, (mood: SeyarMood) => string> = {
|
||||
sigma: seyarSigmaSVG, delta: seyarDeltaSVG, pi: seyarPiSVG,
|
||||
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) را به یکی از سهیار نگاشت میکند. */
|
||||
/** 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') return s;
|
||||
if (s === 'sigma' || s === 'delta' || s === 'pi' || s === 'tedi') return s;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user