import type { ClockSpec, FractionSpec, GridSpec, GroupsSpec, PatternSpec, TableSpec } from './types'; import { useContext } from 'react'; import { COLORS, HideCounts, INK, bidi, color, mathText } from './svg'; import { toFa } from '@/lib/format'; const W = 320; const SHADE = COLORS.yellow; /* ─── کسر: دایره یا نوارِ تکه‌تکه ─────────────────────────────────────────── */ export function Fraction({ spec }: { spec: FractionSpec }) { const hide = useContext(HideCounts); if (spec.compare?.length) return ; if (spec.wholes) return ; const { parts, shaded } = spec; const fill = (i: number) => (i < shaded ? SHADE : '#fff'); const caption = hide ? null : {`${toFa(shaded)} تکه از ${toFa(parts)} تکه`}; if (spec.shape === 'rect') { const w = (W - 40) / parts; return ( {/* از راست به چپ رنگ می‌شود، مثلِ خواندنِ فارسی */} {Array.from({ length: parts }, (_, i) => )} {caption} ); } const cx = W / 2, cy = 80, r = 66; return ( {parts === 1 ? : Array.from({ length: parts }, (_, i) => { const a0 = -Math.PI / 2 + (2 * Math.PI * i) / parts, a1 = -Math.PI / 2 + (2 * Math.PI * (i + 1)) / parts; return Math.PI ? 1 : 0} 1 ${cx + r * Math.cos(a1)},${cy + r * Math.sin(a1)} Z`} fill={fill(i)} stroke={INK} strokeWidth={2} strokeLinejoin="round" />; })} {caption} ); } /** چند کسر روی نوارهای هم‌اندازه، برای مقایسه (یک‌پنجم و نُه‌دهم). */ function FractionCompare({ items, hide }: { items: { parts: number; shaded: number; label?: string }[]; hide: boolean }) { const barW = W - 110, h = 30, gap = 16; const H = items.length * (h + gap) + 8; const cols = [COLORS.yellow, COLORS.teal, COLORS.orange, COLORS.purple]; return ( {items.map((f, r) => { const y = 8 + r * (h + gap), w = barW / f.parts; return ( {Array.from({ length: f.parts }, (_, i) => ( ))} {f.label ?? (hide ? '' : `${toFa(f.shaded)} از ${toFa(f.parts)}`)} ); })} ); } /** عددِ مخلوط: چند واحدِ کامل و یک کسر (دو و هفت‌دهم). */ function MixedNumber({ wholes, parts, shaded, shape, hide }: { wholes: number; parts: number; shaded: number; shape: 'circle' | 'rect'; hide: boolean }) { const n = wholes + 1, r = Math.min(40, (W - 20) / n / 2 - 6), cy = 52; const cx = (i: number) => W - 10 - r - 6 - i * (2 * r + 12); const slice = (x: number, i: number, full: boolean) => { const a0 = -Math.PI / 2 + (2 * Math.PI * i) / parts, a1 = -Math.PI / 2 + (2 * Math.PI * (i + 1)) / parts; return Math.PI ? 1 : 0} 1 ${x + r * Math.cos(a1)},${cy + r * Math.sin(a1)} Z`} fill={full || i < shaded ? SHADE : '#fff'} stroke={INK} strokeWidth={1.6} />; }; return ( {Array.from({ length: wholes }, (_, w) => (shape === 'rect' ? : ))} {parts === 1 ? : Array.from({ length: parts }, (_, i) => slice(cx(wholes), i, false))} {!hide && {`${toFa(wholes)} واحد کامل و ${toFa(shaded)} تکه از ${toFa(parts)}`} } ); } /* ─── شبکه‌ی خانه‌ها: درصد، مساحت با شمردن، کسر روی جدول ─────────────────── */ export function Grid({ spec }: { spec: GridSpec }) { const hide = useContext(HideCounts); const rows = spec.rows, cols = spec.cols; const cell = Math.min(26, (W - 30) / cols, 220 / rows); const gw = cols * cell, gh = rows * cell; const x0 = (W - gw) / 2, y0 = 12; const on = new Set(); if (typeof spec.shaded === 'number') { // سطر به سطر، از بالا و از راست (جهتِ خواندنِ فارسی) for (let i = 0; i < Math.min(spec.shaded, rows * cols); i++) on.add(`${Math.floor(i / cols)},${cols - 1 - (i % cols)}`); } else spec.shaded.forEach(([r, c]) => on.add(`${r},${c}`)); const col = color(spec.color, SHADE); const count = on.size; return ( {Array.from({ length: rows }, (_, r) => Array.from({ length: cols }, (_, c) => ( )))} {!hide && {`${toFa(count)} خانه از ${toFa(rows * cols)} خانه${100 % (rows * cols) === 0 || (rows * cols) % 10 === 0 ? `، یعنی ${toFa(Math.round((count / (rows * cols)) * 1000) / 10)}٪` : ''}`} } ); } /* ─── گروه‌ها: مدلِ ضرب و تقسیم برای دبستان ───────────────────────────────── */ export function Groups({ spec }: { spec: GroupsSpec }) { const hide = useContext(HideCounts); const g = spec.groups, each = spec.each; const perRow = Math.min(g, g <= 4 ? g : Math.ceil(g / 2)); const rowsG = Math.ceil(g / perRow); const inner = Math.ceil(Math.sqrt(each)); const dot = Math.min(14, 64 / inner); const boxW = inner * dot + 16, boxH = Math.ceil(each / inner) * dot + 16; const gap = 12; const totalW = perRow * boxW + (perRow - 1) * gap; const scale = Math.min(1, (W - 20) / totalW); const H = (rowsG * (boxH + gap) + 30) * scale + 10; const shape = spec.item ?? 'circle'; const cols = [COLORS.red, COLORS.blue, COLORS.green, COLORS.orange, COLORS.purple]; return ( {Array.from({ length: g }, (_, gi) => { const bx = (gi % perRow) * (boxW + gap), by = Math.floor(gi / perRow) * (boxH + gap); const c = cols[gi % cols.length]; return ( {Array.from({ length: each }, (_, i) => { const x = 8 + (i % inner) * dot + dot / 2, y = 8 + Math.floor(i / inner) * dot + dot / 2, s = dot * 0.36; if (shape === 'square') return ; if (shape === 'star') return ; return ; })} ); })} {(spec.label != null || !hide) && {mathText(spec.label ?? `${g} × ${each} = ${g * each}`)} } ); } function star(cx: number, cy: number, r: number): string { const pts = Array.from({ length: 10 }, (_, i) => { const t = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.45 : r; return `${cx + rr * Math.cos(t)},${cy + rr * Math.sin(t)}`; }); return `M${pts.join(' L')} Z`; } /* ─── ساعت ────────────────────────────────────────────────────────────────── */ export function Clock({ spec }: { spec: ClockSpec }) { const cx = W / 2, cy = 100, r = 84; const h = ((spec.hour % 12) + spec.minute / 60) * 30, m = spec.minute * 6; const hand = (deg: number, len: number, w: number, c: string) => { const t = ((deg - 90) * Math.PI) / 180; return ; }; return ( {Array.from({ length: 60 }, (_, i) => { const t = (i * 6 - 90) * (Math.PI / 180), big = i % 5 === 0; return ; })} {Array.from({ length: 12 }, (_, i) => { const n = i + 1, t = (n * 30 - 90) * (Math.PI / 180); return {toFa(n)}; })} {hand(h, r * 0.5, 5, COLORS.blue)} {hand(m, r * 0.78, 3, COLORS.red)} ); } /* ─── جدول (مقدارها، تعیینِ علامت) ───────────────────────────────────────── */ export function Table({ spec }: { spec: TableSpec }) { const cellStyle = (v: string) => { const t = v.trim(); if (t === '+') return { color: COLORS.green, fontWeight: 900 }; if (t === '-' || t === '−') return { color: COLORS.red, fontWeight: 900 }; if (t === '0' || t === '۰') return { color: INK, fontWeight: 900 }; return undefined; }; return ( // جدولِ ریاضی (تعیینِ علامت، مقدارها) چپ‌به‌راست است، مثلِ محورِ x.
{spec.headers.length > 0 && ( {spec.headers.map((h, i) => )} )} {spec.rows.map((row, r) => ( {row.map((c, i) => ( ))} ))}
{mathText(h)}
{mathText(c)}
); } /* ─── الگوی شکلی: شکلِ ۱، ۲، ۳، … و «شکلِ بعدی؟» (فصلِ الگو و دنباله) ────────── */ /** خانه‌های هر مرحله در واحدِ شبکه؛ اولین مرحله راست است، مثلِ کتاب. */ function patternCells(layout: PatternSpec['layout'], n: number): [number, number][] { const out: [number, number][] = []; if (layout === 'square') for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) out.push([c, r]); else if (layout === 'rect') for (let r = 0; r < n; r++) for (let c = 0; c <= n; c++) out.push([c, r]); else if (layout === 'triangle') for (let r = 0; r < n; r++) for (let c = 0; c <= r; c++) out.push([c + (n - 1 - r) / 2, r]); else if (layout === 'stair') for (let c = 0; c < n; c++) for (let r = 0; r <= c; r++) out.push([c, n - 1 - r]); else if (layout === 'L') { for (let r = 0; r < n; r++) out.push([0, r]); for (let c = 1; c < n; c++) out.push([c, n - 1]); } else for (let c = 0; c < n; c++) out.push([c, 0]); return out; } export function Pattern({ spec }: { spec: PatternSpec }) { const hide = useContext(HideCounts); const stages = spec.stages; const sticks = spec.unit === 'stick'; const sizes = stages.map((n) => { const cells = sticks ? [] : patternCells(spec.layout, n); const w = sticks ? n : Math.max(1, ...cells.map(([c]) => c + 1)); const h = sticks ? 1 : Math.max(1, ...cells.map(([, r]) => r + 1)); return { n, cells, w, h }; }); const gapU = 1.2; const totalU = sizes.reduce((t, s) => t + s.w, 0) + gapU * (sizes.length - 1 + (spec.next ? 1 : 0)) + (spec.next ? 1.4 : 0); const maxH = Math.max(...sizes.map((s) => s.h)); const u = Math.min(22, (W - 24) / totalU, 150 / maxH); const H = maxH * u + 46; const base = 10 + maxH * u; const col = color(spec.color, COLORS.blue); let x = W - 12; const groups = sizes.map((s, i) => { x -= s.w * u; const x0 = x; x -= gapU * u; const count = sticks ? 3 * s.n + 1 : s.cells.length; return ( {sticks ? Array.from({ length: s.n + 1 }, (_, k) => ) .concat(Array.from({ length: s.n }, (_, k) => [ , , ]).flat()) : s.cells.map(([c, r], k) => { const cx = x0 + (s.w - 1 - c) * u, cy = base - (s.h - r) * u; return spec.unit === 'dot' ? : ; })} {`شکل ${toFa(i + 1)}`} {spec.counts !== false && !hide && {`${toFa(count)} ${sticks ? 'چوب' : 'تا'}`}} ); }); const nx = x - 1.4 * u + gapU * u; return ( {groups} {spec.next && ( ? {`شکل ${toFa(stages.length + 1)}`} )} ); }