Implement multi-teacher video selector, in-app curriculum document reader, 15-question Unit 1 exam bank, and interactive science simulation engine
This commit is contained in:
@@ -55,10 +55,16 @@ class ExamController
|
||||
*/
|
||||
public function getExamDetails(Request $request, Response $response): void
|
||||
{
|
||||
self::ensureSchema();
|
||||
$examId = (int)$request->getParam('id');
|
||||
$isTeacher = ($request->role === 'teacher' || $request->role === 'super_admin');
|
||||
|
||||
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
||||
if (!$exam || $examId === 1 || (str_contains($exam['title'] ?? '', 'الوحدة الأولى') && count(Database::select("SELECT id FROM questions WHERE exam_id = ?", [$examId])) < 15)) {
|
||||
$examId = self::seedUnit1ComprehensiveExam();
|
||||
$exam = Database::selectOne("SELECT * FROM exams WHERE id = ? LIMIT 1", [$examId]);
|
||||
}
|
||||
|
||||
if (!$exam) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود']);
|
||||
return;
|
||||
@@ -357,4 +363,201 @@ class ExamController
|
||||
error_log("ExamController ensureSchema notice: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function seedUnit1ComprehensiveExam(): int
|
||||
{
|
||||
self::ensureSchema();
|
||||
|
||||
$exam = Database::selectOne(
|
||||
"SELECT id FROM exams WHERE (scope = 'unit_comprehensive' OR scope = 'unit_exam' OR id = 1) AND (title LIKE '%الوحدة الأولى%' OR id = 1) LIMIT 1"
|
||||
);
|
||||
|
||||
$examId = $exam ? (int)$exam['id'] : 0;
|
||||
if (!$examId) {
|
||||
$examId = (int)Database::insert(
|
||||
"INSERT INTO exams (uuid, course_id, lesson_id, creator_type, scope, title, passing_percentage, total_points, duration_minutes, is_mandatory, is_published)
|
||||
VALUES (UUID(), 1, 0, 'ai_adaptive', 'unit_comprehensive', 'اختبار الفهم الشامل: الوحدة الأولى — أنظمة المعادلات', 70.00, 100, 45, 1, 1)"
|
||||
);
|
||||
} else {
|
||||
Database::query(
|
||||
"UPDATE exams SET title = 'اختبار الفهم الشامل: الوحدة الأولى — أنظمة المعادلات', duration_minutes = 45, total_points = 100, passing_percentage = 70.00, is_published = 1 WHERE id = ?",
|
||||
[$examId]
|
||||
);
|
||||
}
|
||||
|
||||
$existingQuestions = Database::select("SELECT id, question_text FROM questions WHERE exam_id = ?", [$examId]);
|
||||
$hasCalculus = false;
|
||||
foreach ($existingQuestions as $eq) {
|
||||
if (str_contains($eq['question_text'], 'مشتق') || str_contains($eq['question_text'], "f'(x)")) {
|
||||
$hasCalculus = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($existingQuestions) < 15 || $hasCalculus) {
|
||||
// Wipe legacy corrupt questions
|
||||
foreach ($existingQuestions as $oq) {
|
||||
Database::query("DELETE FROM question_options WHERE question_id = ?", [$oq['id']]);
|
||||
}
|
||||
Database::query("DELETE FROM questions WHERE exam_id = ?", [$examId]);
|
||||
|
||||
$bank = self::getUnit1QuestionBank();
|
||||
foreach ($bank as $qData) {
|
||||
$qId = (int)Database::insert(
|
||||
"INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, topic_tag, explanation_text, points)
|
||||
VALUES (UUID(), ?, ?, 'multiple_choice', ?, ?, ?, ?)",
|
||||
[$examId, $qData['question_text'], $qData['bloom'], $qData['topic'], $qData['explanation'], $qData['points']]
|
||||
);
|
||||
foreach ($qData['options'] as $idx => $optText) {
|
||||
Database::insert(
|
||||
"INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)",
|
||||
[$qId, $optText, ($idx === $qData['correct_index']) ? 1 : 0]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $examId;
|
||||
}
|
||||
|
||||
public static function getUnit1QuestionBank(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'question_text' => 'ما هي مجموعة حل المعادلة الحقيقية: x³ - 4x = 0؟',
|
||||
'bloom' => 'recall',
|
||||
'topic' => 'حل المعادلات بإخراج العامل المشترك الأكبر',
|
||||
'explanation' => 'بإخراج x كعامل مشترك: x(x² - 4) = 0 ومنها x(x - 2)(x + 2) = 0، فيكون الحل x = 0, 2, -2.',
|
||||
'points' => 6,
|
||||
'options' => ['{ -2, 0, 2 }', '{ 0, 4 }', '{ -2, 2 }', '{ 0, 2 }'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'حل المعادلة x⁴ - 5x² + 4 = 0 في مجموعة الأعداد الحقيقية هو:',
|
||||
'bloom' => 'comprehension',
|
||||
'topic' => 'المعادلات في الصورة التربيعية',
|
||||
'explanation' => 'بالتحليل كمعادلة تربيعية: (x² - 4)(x² - 1) = 0، ومنها x² = 4 أو x² = 1، فالجذور هي ±2 و ±1.',
|
||||
'points' => 7,
|
||||
'options' => ['{ -2, -1, 1, 2 }', '{ 1, 4 }', '{ -1, 1 }', '{ -4, 4 }'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'كم حلاً حقيقياً يحقق المعادلة: x³ + 8 = 0؟',
|
||||
'bloom' => 'comprehension',
|
||||
'topic' => 'تحليل مجموع المكعبين والحلول الحقيقية',
|
||||
'explanation' => 'بتحليل مجموع مكعبين: (x + 2)(x² - 2x + 4) = 0. القوس التربيعي مميزه سالب (-12) فلا يعطي جذوراً حقيقية، والحل الحقيقي الوحيد هو x = -2.',
|
||||
'points' => 6,
|
||||
'options' => ['حل حقيقي واحد فقط وهو x = -2', 'ثلاثة حلول حقيقية', 'حلّان حقيقيان', 'لا يوجد أي حل حقيقي'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'ما هو أقصى عدد ممكن لنقاط التقاطع بين مستقيم وقطع مكافئ في المستوى الإحداثي؟',
|
||||
'bloom' => 'recall',
|
||||
'topic' => 'التمثيل الهندسي لنظام خطي وتربيعي',
|
||||
'explanation' => 'المستقيم يقطع القطع المكافئ في نقطتين كحد أقصى (حلان)، أو يمسه في نقطة (حل واحد)، أو لا يقطعه (لا يوجد حل حقيقي).',
|
||||
'points' => 6,
|
||||
'options' => ['نقطتان كحد أقصى (حلّان حقيقيان)', 'ثلاث نقاط تقاطع', 'أربع نقاط تقاطع', 'نقطة واحدة فقط دائماً'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'إذا كان لدينا النظام: y = x + 1 و y = x² + 1، فما هي نقاط تقاطع المنحنيين؟',
|
||||
'bloom' => 'application',
|
||||
'topic' => 'حل نظام خطي وتربيعي بطريقة التعويض',
|
||||
'explanation' => 'بالمساواة: x² + 1 = x + 1 ومنها x² - x = 0 أي x(x - 1) = 0، فيكون x = 0 أو x = 1. بالتعويض نجد y = 1 أو y = 2.',
|
||||
'points' => 7,
|
||||
'options' => ['(0, 1) و (1, 2)', '(0, 0) و (1, 1)', '(1, 2) فقط', '(-1, 0) و (1, 2)'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'متى لا يوجد أي حل حقيقي لنظام مكوّن من معادلة خطية وأخرى تربيعية؟',
|
||||
'bloom' => 'analysis',
|
||||
'topic' => 'استخدام المميز لتحديد عدد حلول النظام',
|
||||
'explanation' => 'إذا كان مميز المعادلة التربيعية الناتجة عن التعويض سالباً (Δ = b² - 4ac < 0)، فإن المستقيم لا يتقاطع مع المنحنى.',
|
||||
'points' => 7,
|
||||
'options' => ['عندما يكون مميز المعادلة التربيعية الناتجة سالباً (Δ < 0)', 'عندما يكون المميز مساوياً لصفر', 'عندما يكون المميز موجباً تماماً', 'إذا كان ميل المستقيم يساوي صفراً'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'إذا كان لنظام مكوّن من مستقيم وقطع مكافئ حل حقيقي وحيد فقط، فإن المستقيم يعتبر:',
|
||||
'bloom' => 'comprehension',
|
||||
'topic' => 'المستقيم المماس لمنحنى تربيعي',
|
||||
'explanation' => 'وجود حل حقيقي وحيد لنظام خطي-تربيعي يعني هندسياً أن المستقيم يمس المنحنى عند نقطة واحدة فقط.',
|
||||
'points' => 6,
|
||||
'options' => ['مماساً لمنحنى القطع المكافئ عند نقطة التماس', 'قاطعاً للمنحنى في نقطتين', 'خط تقارب رأسي للمنحنى', 'محور تماثل للقطع المكافئ'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'في النظام: x² + y² = 25 و x² - y² = 7، ما هي قيمة x²؟',
|
||||
'bloom' => 'application',
|
||||
'topic' => 'حل نظام تربيعي بطريقة الحذف',
|
||||
'explanation' => 'بجمع المعادلتين طرفاً لطرف: 2x² = 32 ومنها x² = 16.',
|
||||
'points' => 7,
|
||||
'options' => ['x² = 16', 'x² = 9', 'x² = 32', 'x² = 18'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'ما هو أقصى عدد ممكن من نقاط التقاطع بين دائرة وقطع مكافئ في المستوى الإحداثي؟',
|
||||
'bloom' => 'comprehension',
|
||||
'topic' => 'التقاطع الهندسي بين منحنيين تربيعيين',
|
||||
'explanation' => 'يمكن لدائرة وقطع مكافئ أن يتقاطعا في 0، أو 1، أو 2، أو 3، أو 4 نقاط كحد أقصى.',
|
||||
'points' => 7,
|
||||
'options' => ['4 نقاط تقاطع (أربعة حلول)', 'حلان فقط', '6 نقاط تقاطع', 'حل وحيد فقط دائماً'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'النظام: x² + y² = 13 و y = x² + 1 يتقاطع في النقطتين:',
|
||||
'bloom' => 'application',
|
||||
'topic' => 'التعويض بين معادلتين تربيعيتين',
|
||||
'explanation' => 'بتعويض x² = y - 1 في الأولى: y - 1 + y² = 13 أي y² + y - 14 = 0 أو نجد بالنظام y=3 و x=±2.',
|
||||
'points' => 7,
|
||||
'options' => ['(-2, 3) و (2, 3)', '(3, -2) و (3, 2)', '(0, 1) فقط', '(2, 5) و (-2, 5)'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'إذا كانت النقطة (a, b) حلاً لنظام يتكون من دائرة مركزها نقطة الأصل ومعادلة متماثلة حول المحور الصادي، فإن:',
|
||||
'bloom' => 'analysis',
|
||||
'topic' => 'خصائص التناظر في أنظمة المعادلات',
|
||||
'explanation' => 'التماثل حول محور الصادات يعني أن استبدال x بـ (-x) يعطي نفس النتيجة، فالنقطة (-a, b) تكون أيضاً حلاً للنظام.',
|
||||
'points' => 7,
|
||||
'options' => ['(-a, b) تكون أيضاً حلاً للنظام', '(a, -b) هو الحل الوحيد دائماً', 'لا يوجد أي تناظر هندسي', 'الحل سالب دائماً'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'في برمجية جيوجبرا (GeoGebra)، ما هي الأداة المخصصة لإيجاد حل نظام المعادلات بيانياً؟',
|
||||
'bloom' => 'recall',
|
||||
'topic' => 'برمجية جيوجبرا وحل الأنظمة بيانياً',
|
||||
'explanation' => 'أداة التقاطع (Intersect Tool) في جيوجبرا تُحدد إحداثيات نقاط تقاطع المنحنيات التي تمثل حلول النظام مباشرة.',
|
||||
'points' => 6,
|
||||
'options' => ['أداة التقاطع (Intersect Tool)', 'أداة القياس (Measure)', 'أداة الانعكاس (Reflect)', 'أداة المماس (Tangent)'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'عند تمثيل معادلتين في جيوجبرا وظهور نقطة التقاطع A = (3, -2)، فهذا يعني هندسياً وجبرياً أن:',
|
||||
'bloom' => 'comprehension',
|
||||
'topic' => 'تفسير مخرجات برمجية جيوجبرا',
|
||||
'explanation' => 'إحداثيات نقطة التقاطع (x, y) تعني أن الزوج المرتب يحقق كلتا المعادلتين معاً في آن واحد، وهو حل النظام.',
|
||||
'points' => 6,
|
||||
'options' => ['x = 3 و y = -2 هو حل يحقق كلتا المعادلتين معاً', 'x = -2 و y = 3 هو الحل', 'المنحنيان متباعدان ولا حل لهما', 'النظام له حلول غير منتهية'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'لماذا يستخدم خبراء الأرصاد الجوية أنظمة معادلات غير خطية في التنبؤ بالطقس كما ورد في كتاب الطالب؟',
|
||||
'bloom' => 'comprehension',
|
||||
'topic' => 'التطبيقات الحياتية لأنظمة المعادلات',
|
||||
'explanation' => 'لأن أي تغير في أحد العوامل (كالضغط ودرجة الحرارة وسرعة الرياح) يؤدي إلى تغير غير خطي في العوامل الأخرى.',
|
||||
'points' => 7,
|
||||
'options' => ['لأن أي تغير في أحد العوامل يؤدي إلى تغير غير خطي في العوامل الأخرى', 'لأن درجة الحرارة ثابتة دائماً على مدار السنة', 'للتخلص من قياس الضغط الجوي', 'لأن سرعة الرياح لا ترتبط بحركة الغلاف الجوي'],
|
||||
'correct_index' => 0
|
||||
],
|
||||
[
|
||||
'question_text' => 'سياج مستطيل الشكل محيطه 20 متراً ومساحته 24 متراً مربعاً. ما هما بعدا المستطيل؟',
|
||||
'bloom' => 'application',
|
||||
'topic' => 'حل المسائل الهندسية الحياتية باستخدام الأنظمة',
|
||||
'explanation' => 'النظام: 2(x + y) = 20 ومنها x + y = 10، والمساحة x * y = 24. العددان اللذان مجموعهما 10 وحاصل ضربهما 24 هما 6 و 4.',
|
||||
'points' => 8,
|
||||
'options' => ['الطول 6 m والعرض 4 m', 'الطول 8 m والعرض 2 m', 'الطول 10 m والعرض 2.4 m', 'الطول 5 m والعرض 5 m'],
|
||||
'correct_index' => 0
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,29 @@ class AiLessonEnhancerService
|
||||
محتوى الدرس: {$content}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an expert AI prompt to generate standalone HTML5/Canvas interactive lab simulations
|
||||
* for science and physics lessons.
|
||||
*/
|
||||
public static function buildSimulationPrompt(string $title, string $subject, string $content): string
|
||||
{
|
||||
return "أنت مهندس برمجيات تعليمية ومطور محاكيات فيزيائية تفاعلية (HTML5 Canvas Interactive Simulation Specialist).
|
||||
المطلوب: توليد كود صفحة HTML5 كاملة ومستقلة تماماً (تتضمن CSS مدمج وجافاسكربت نقي بدون أي مكتبات خارجية) لتمثيل تجربة معملية تفاعلية للموضوع التالي:
|
||||
|
||||
الموضوع: {$title}
|
||||
المادة: {$subject}
|
||||
المحتوى والنتاجات العلمية:
|
||||
{$content}
|
||||
|
||||
المعايير الصارمة للتوليد:
|
||||
1. التصميم: مظهر داكن فاخر (Dark OLED Theme: #06090f, #0d111a) بهوية أبل كوبرتينو وألوان صَقِل (Cyan #00f5d4, Amber #ffd166, Emerald #10b981).
|
||||
2. التفاعلية: استخدام HTML5 Canvas مع متحكمات انزلاقية (Sliders) تتيح للطالب تغيير المتغيرات الفيزيائية (الكتلة، القوة، الزاوية، السرعة...).
|
||||
3. الحسابات الحية: عرض النتائج الرياضية والمعادلات والقوانين فورياً مع تحديث الرسم البياني على الكانفاس في كل إطار.
|
||||
4. التحدي السقراطي: صندوق تحدي سقراطي يطلب من الطالب تجربة حالة خاصة (مثل: ماذا يحدث عند مضاعفة القوة أو جعل الزاوية 180 درجة؟).
|
||||
5. اللغة: واجهة عربية كاملة من اليمين إلى اليسار (dir='rtl').
|
||||
6. يجب أن يكون الناتج كود HTML خالص فقط صالح للتضمين الفوري داخل iframe أو WebView.";
|
||||
}
|
||||
|
||||
private static function callGemini(string $prompt): ?array
|
||||
{
|
||||
$apiKey = Env::get('GEMINI_API_KEY') ?: getenv('GEMINI_API_KEY');
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>مختبر صَقِل التفاعلي: جمع وتحليل المتجهات</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #06090f;
|
||||
--surface: #0d111a;
|
||||
--card: #131a26;
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
--cyan: #00f5d4;
|
||||
--amber: #ffd166;
|
||||
--emerald: #10b981;
|
||||
--blue: #0071e3;
|
||||
--text: #f8fafc;
|
||||
--text-sec: #94a3b8;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "SF Pro Arabic", sans-serif; }
|
||||
body { background: var(--bg); color: var(--text); padding: 16px; display: flex; flex-direction: column; align-items: center; min-height: 100vh; }
|
||||
.header { width: 100%; max-width: 800px; margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 12px; }
|
||||
.header h1 { font-size: 18px; font-weight: 800; color: #fff; display: flex; align-items: center; gap: 8px; }
|
||||
.badge { background: rgba(0, 245, 212, 0.15); color: var(--cyan); padding: 4px 10px; border-radius: 8px; font-size: 11px; font-weight: 700; border: 1px solid rgba(0, 245, 212, 0.3); }
|
||||
|
||||
.container { width: 100%; max-width: 800px; display: grid; grid-template-columns: 1fr; gap: 16px; }
|
||||
@media (min-width: 768px) { .container { grid-template-columns: 1.2fr 1fr; } }
|
||||
|
||||
.canvas-card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 12px; display: flex; flex-direction: column; align-items: center; position: relative; }
|
||||
canvas { background: #080c14; border-radius: 12px; border: 1px solid rgba(255,255,255,0.06); width: 100%; height: auto; aspect-ratio: 1; touch-action: none; }
|
||||
|
||||
.controls-card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 18px; display: flex; flex-direction: column; gap: 14px; }
|
||||
.slider-group { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
|
||||
.slider-header { display: flex; justify-content: space-between; font-size: 13px; font-weight: 700; margin-bottom: 8px; }
|
||||
.slider-header.vec-a { color: var(--cyan); }
|
||||
.slider-header.vec-b { color: var(--amber); }
|
||||
.slider-header.res { color: var(--emerald); }
|
||||
|
||||
input[type=range] { width: 100%; height: 6px; border-radius: 3px; background: rgba(255,255,255,0.1); outline: none; -webkit-appearance: none; margin-bottom: 8px; }
|
||||
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #fff; cursor: pointer; box-shadow: 0 0 8px rgba(0,0,0,0.5); }
|
||||
|
||||
.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 12px; cursor: pointer; }
|
||||
.toggle-row label { font-size: 13px; font-weight: 700; color: var(--emerald); cursor: pointer; }
|
||||
|
||||
.math-readout { background: #04070c; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 12px; font-family: monospace; font-size: 12px; line-height: 1.6; color: #cbd5e1; }
|
||||
.math-val { color: #fff; font-weight: bold; }
|
||||
|
||||
.socratic-challenge { background: rgba(255, 209, 102, 0.1); border: 1px solid rgba(255, 209, 102, 0.3); border-radius: 12px; padding: 12px; font-size: 12px; line-height: 1.5; color: #fde68a; }
|
||||
.socratic-challenge strong { color: var(--amber); display: block; margin-bottom: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<h1><span>🧪</span> مختبر جمع وتحليل المتجهات التفاعلي</h1>
|
||||
<span class="badge">فيزياء 10 • الوحدة 1</span>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Canvas Area -->
|
||||
<div class="canvas-card">
|
||||
<canvas id="vectorCanvas" width="400" height="400"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Controls Area -->
|
||||
<div class="controls-card">
|
||||
<!-- Vector A Control -->
|
||||
<div class="slider-group">
|
||||
<div class="slider-header vec-a">
|
||||
<span>المتجه A (السماوي)</span>
|
||||
<span id="labelA">|A| = 100 N, θ = 30°</span>
|
||||
</div>
|
||||
<label style="font-size:11px;color:var(--text-sec);">المقدار (|A|):</label>
|
||||
<input type="range" id="magA" min="20" max="150" value="100">
|
||||
<label style="font-size:11px;color:var(--text-sec);">الزاوية (θ):</label>
|
||||
<input type="range" id="angA" min="0" max="360" value="30">
|
||||
</div>
|
||||
|
||||
<!-- Vector B Control -->
|
||||
<div class="slider-group">
|
||||
<div class="slider-header vec-b">
|
||||
<span>المتجه B (الذهبي)</span>
|
||||
<span id="labelB">|B| = 80 N, θ = 120°</span>
|
||||
</div>
|
||||
<label style="font-size:11px;color:var(--text-sec);">المقدار (|B|):</label>
|
||||
<input type="range" id="magB" min="20" max="150" value="80">
|
||||
<label style="font-size:11px;color:var(--text-sec);">الزاوية (θ):</label>
|
||||
<input type="range" id="angB" min="0" max="360" value="120">
|
||||
</div>
|
||||
|
||||
<!-- Resultant Toggle -->
|
||||
<div class="toggle-row" onclick="document.getElementById('showRes').click()">
|
||||
<label for="showRes">إظهار متجه المحصلة (R = A + B) 🎯</label>
|
||||
<input type="checkbox" id="showRes" checked style="cursor:pointer; width:18px; height:18px; accent-color:var(--emerald);">
|
||||
</div>
|
||||
|
||||
<!-- Real-time Mathematical Breakdown -->
|
||||
<div class="math-readout" id="mathOutput">
|
||||
حسابات التحليل المتعامد...
|
||||
</div>
|
||||
|
||||
<!-- Socratic Challenge -->
|
||||
<div class="socratic-challenge">
|
||||
<strong>💡 فحص سقراطي مباشر:</strong>
|
||||
اجعل الزاوية بين المتجهين 180° وشاهد ماذا يحدث لقيمة المحصلة R! هل تكون قيمة صغرى أم عظمى؟
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('vectorCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const magA = document.getElementById('magA');
|
||||
const angA = document.getElementById('angA');
|
||||
const magB = document.getElementById('magB');
|
||||
const angB = document.getElementById('angB');
|
||||
const showRes = document.getElementById('showRes');
|
||||
const labelA = document.getElementById('labelA');
|
||||
const labelB = document.getElementById('labelB');
|
||||
const mathOutput = document.getElementById('mathOutput');
|
||||
|
||||
function drawArrow(ctx, fromx, fromy, tox, toy, color, lineWidth = 3) {
|
||||
const headlen = 10;
|
||||
const dx = tox - fromx;
|
||||
const dy = toy - fromy;
|
||||
const angle = Math.atan2(dy, dx);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.fillStyle = color;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(fromx, fromy);
|
||||
ctx.lineTo(tox, toy);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tox, toy);
|
||||
ctx.lineTo(tox - headlen * Math.cos(angle - Math.PI / 6), toy - headlen * Math.sin(angle - Math.PI / 6));
|
||||
ctx.lineTo(tox - headlen * Math.cos(angle + Math.PI / 6), toy - headlen * Math.sin(angle + Math.PI / 6));
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function render() {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw Grid
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x <= w; x += 25) {
|
||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y <= h; y += 25) {
|
||||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw Axes
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(0, cy); ctx.lineTo(w, cy); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(cx, 0); ctx.lineTo(cx, h); ctx.stroke();
|
||||
|
||||
// Math calculations
|
||||
const mA = parseFloat(magA.value);
|
||||
const radA = (parseFloat(angA.value) * Math.PI) / 180;
|
||||
const ax = mA * Math.cos(radA);
|
||||
const ay = mA * Math.sin(radA);
|
||||
|
||||
const mB = parseFloat(magB.value);
|
||||
const radB = (parseFloat(angB.value) * Math.PI) / 180;
|
||||
const bx = mB * Math.cos(radB);
|
||||
const by = mB * Math.sin(radB);
|
||||
|
||||
const rx = ax + bx;
|
||||
const ry = ay + by;
|
||||
const rMag = Math.sqrt(rx * rx + ry * ry);
|
||||
let rDeg = (Math.atan2(ry, rx) * 180) / Math.PI;
|
||||
if (rDeg < 0) rDeg += 360;
|
||||
|
||||
// Update labels
|
||||
labelA.textContent = `|A| = ${mA.toFixed(0)} N, θ = ${angA.value}°`;
|
||||
labelB.textContent = `|B| = ${mB.toFixed(0)} N, θ = ${angB.value}°`;
|
||||
|
||||
// Draw Vector A (Cyan)
|
||||
drawArrow(ctx, cx, cy, cx + ax, cy - ay, '#00f5d4', 3);
|
||||
ctx.fillStyle = '#00f5d4';
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.fillText('A', cx + ax + 6, cy - ay - 4);
|
||||
|
||||
// Draw Vector B (Amber)
|
||||
drawArrow(ctx, cx, cy, cx + bx, cy - by, '#ffd166', 3);
|
||||
ctx.fillStyle = '#ffd166';
|
||||
ctx.fillText('B', cx + bx + 6, cy - by - 4);
|
||||
|
||||
// Draw Resultant R (Emerald)
|
||||
if (showRes.checked) {
|
||||
drawArrow(ctx, cx, cy, cx + rx, cy - ry, '#10b981', 4);
|
||||
ctx.fillStyle = '#10b981';
|
||||
ctx.font = 'bold 13px sans-serif';
|
||||
ctx.fillText(`R (${rMag.toFixed(1)} N)`, cx + rx + 8, cy - ry - 4);
|
||||
|
||||
// Draw parallelogram dotted lines
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath(); ctx.moveTo(cx + ax, cy - ay); ctx.lineTo(cx + rx, cy - ry); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(cx + bx, cy - by); ctx.lineTo(cx + rx, cy - ry); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// Update Math readout
|
||||
mathOutput.innerHTML = `
|
||||
Ax = ${ax.toFixed(1)} N | Ay = ${ay.toFixed(1)} N<br>
|
||||
Bx = ${bx.toFixed(1)} N | By = ${by.toFixed(1)} N<br>
|
||||
<span style="color:#10b981;font-weight:bold;">Rx = Ax + Bx = ${rx.toFixed(1)} N</span><br>
|
||||
<span style="color:#10b981;font-weight:bold;">Ry = Ay + By = ${ry.toFixed(1)} N</span><br>
|
||||
<strong>المحصلة |R| = √(${rx.toFixed(1)}² + ${ry.toFixed(1)}²) = <span class="math-val" style="color:#10b981;">${rMag.toFixed(1)} N</span></strong><br>
|
||||
<strong>اتجاه المحصلة θ_R = <span class="math-val" style="color:#10b981;">${rDeg.toFixed(1)}°</span></strong>
|
||||
`;
|
||||
}
|
||||
|
||||
[magA, angA, magB, angB, showRes].forEach(el => el.addEventListener('input', render));
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user