feat: Implement DML Interactive Canvas & Dynamic Visual Physics and Math Simulators with live sliders for calculus and mechanics

This commit is contained in:
Hamza-Ayed
2026-08-28 02:06:26 +03:00
parent a99ff9a184
commit f0592466f6
+287 -7
View File
@@ -764,7 +764,7 @@ class StudentPortal
<!-- Problem Card -->
<div style="background: rgba(11, 19, 43, 0.85); border: 1px solid rgba(56, 189, 248, 0.3); border-radius: 18px; padding: 24px; margin-bottom: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.4);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px;">
<span style="font-size: 13px; font-weight: 800; color: var(--accent-gold);" id="steplab_prob_topic">الرياضيات العلمي — المعدلات المرتبطة بالزمن (توجيهي 2008)</span>
<span style="font-size: 11px; color: var(--text-muted);">المصدر: توماس كالكولوس + بنك التميز الوزاري</span>
</div>
@@ -772,6 +772,41 @@ class StudentPortal
خزان ماء على شكل مخروط دائري قائم مقلوب، رأسه إلى أسفل وقاعدته أفقية، نصف قطر قاعدته 2 متر وارتفاعه 6 أمتار. يُصب فيه الماء بمعدل ثابت (0.5 م³/دقيقة). جد معدل ارتفاع منسوب الماء في الخزان عندما يكون عمق الماء 3 أمتار.
</h4>
<!-- INTERACTIVE DML VISUAL SIMULATOR -->
<div style="background: rgba(0,0,0,0.6); border: 1px solid rgba(0,245,212,0.3); border-radius: 16px; padding: 18px; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; flex-wrap: wrap; gap: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 13.5px; font-weight: 800; color: var(--accent-cyan);">🎮 المحاكاة البصرية الحية وتغيير المعطيات (DML Interactive Canvas)</span>
<span style="font-size: 10.5px; background: rgba(0,245,212,0.15); color: var(--accent-cyan); padding: 2px 8px; border-radius: 4px; font-weight: 700;">تحريك حي بالثواني ⚡</span>
</div>
<div id="sim_live_readout" style="font-family: monospace; font-size: 13px; font-weight: 800; color: var(--accent-gold); background: rgba(245,158,11,0.1); padding: 4px 12px; border-radius: 8px; border: 1px solid rgba(245,158,11,0.3);">
dh/dt = 0.159 m/min
</div>
</div>
<div style="display: flex; flex-direction: column; align-items: center;">
<canvas id="steplab_sim_canvas" width="600" height="260" style="max-width: 100%; height: auto; background: #030712; border-radius: 12px; border: 1px solid rgba(255,255,255,0.08);"></canvas>
</div>
<!-- Interactive Sliders -->
<div id="sim_controls_container" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 14px; background: rgba(255,255,255,0.02); border-radius: 10px; padding: 12px;">
<div>
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px;">
<span id="slider_label_1" style="color: var(--text-secondary);">عمق الماء الحالي (h):</span>
<span id="slider_val_1" style="font-family: monospace; color: var(--accent-cyan); font-weight: 700;">3.0 m</span>
</div>
<input type="range" id="sim_slider_1" min="0.5" max="5.8" step="0.1" value="3.0" style="width: 100%; accent-color: var(--accent-cyan);" oninput="onSimSliderChange()">
</div>
<div>
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px;">
<span id="slider_label_2" style="color: var(--text-secondary);">معدل التدفق (dV/dt):</span>
<span id="slider_val_2" style="font-family: monospace; color: var(--accent-gold); font-weight: 700;">0.5 m³/min</span>
</div>
<input type="range" id="sim_slider_2" min="0.1" max="2.0" step="0.1" value="0.5" style="width: 100%; accent-color: var(--accent-gold);" oninput="onSimSliderChange()">
</div>
</div>
</div>
<!-- 1. LOCKED THINKING / STRUGGLE WINDOW (60s Timer) -->
<div id="steplab_struggle_box" style="background: rgba(0,0,0,0.5); border: 2px dashed rgba(245, 158, 11, 0.5); border-radius: 16px; padding: 20px; text-align: center; margin-top: 16px;">
<div style="font-size: 32px; margin-bottom: 6px;">⏳</div>
@@ -1446,6 +1481,9 @@ class StudentPortal
}
}
// Initialize interactive DML canvas simulation
initSimControls(idx);
// Start struggle timer
startStruggleTimer();
}
@@ -1501,12 +1539,254 @@ class StudentPortal
if (nextBtn) nextBtn.disabled = false;
}
function verifyStep(stepNum, isSuccess) {
if (isSuccess) {
showLuxuryToast('إنجاز ممتاز ✓', `أتقنت الخطوة ${stepNum} بنجاح!`);
playChimeNotification();
} else {
showLuxuryToast('نقطة تعلم 💡', `راجع تفاصيل الخطوة ${stepNum} لتثبيت الفهم.`);
// ==========================================
// DML Interactive Visual Simulation Engine (HTML5 Canvas)
// ==========================================
function initSimControls(idx) {
const s1 = document.getElementById('sim_slider_1');
const s2 = document.getElementById('sim_slider_2');
const l1 = document.getElementById('slider_label_1');
const l2 = document.getElementById('slider_label_2');
const v1 = document.getElementById('slider_val_1');
const v2 = document.getElementById('slider_val_2');
if (idx === 0) {
l1.textContent = 'عمق الماء الحالي (h):';
s1.min = '0.5'; s1.max = '5.8'; s1.step = '0.1'; s1.value = '3.0';
v1.textContent = '3.0 m';
l2.textContent = 'معدل تدفق الحجم (dV/dt):';
s2.min = '0.1'; s2.max = '2.0'; s2.step = '0.1'; s2.value = '0.5';
v2.textContent = '0.5 m³/min';
} else if (idx === 1) {
l1.textContent = 'بُعد الطرف السفلي عن الحائط (x):';
s1.min = '0.5'; s1.max = '4.8'; s1.step = '0.1'; s1.value = '3.0';
v1.textContent = '3.0 m';
l2.textContent = 'سرعة ابتعاد الطرف السفلي (dx/dt):';
s2.min = '0.5'; s2.max = '4.0'; s2.step = '0.1'; s2.value = '2.0';
v2.textContent = '2.0 m/s';
} else if (idx === 2) {
l1.textContent = 'زاوية إطلاق المقذوف (θ):';
s1.min = '10'; s1.max = '75'; s1.step = '1'; s1.value = '30';
v1.textContent = '30°';
l2.textContent = 'السرعة الابتدائية (v0):';
s2.min = '20'; s2.max = '70'; s2.step = '1'; s2.value = '50';
v2.textContent = '50 m/s';
}
onSimSliderChange();
}
function onSimSliderChange() {
const val1 = parseFloat(document.getElementById('sim_slider_1').value);
const val2 = parseFloat(document.getElementById('sim_slider_2').value);
const v1 = document.getElementById('slider_val_1');
const v2 = document.getElementById('slider_val_2');
if (currentStepLabIdx === 0) {
v1.textContent = `${val1.toFixed(1)} m`;
v2.textContent = `${val2.toFixed(1)} m³/min`;
} else if (currentStepLabIdx === 1) {
v1.textContent = `${val1.toFixed(1)} m`;
v2.textContent = `${val2.toFixed(1)} m/s`;
} else if (currentStepLabIdx === 2) {
v1.textContent = `${Math.round(val1)}°`;
v2.textContent = `${Math.round(val2)} m/s`;
}
renderCanvasSim(currentStepLabIdx, val1, val2);
}
function renderCanvasSim(idx, val1, val2) {
const canvas = document.getElementById('steplab_sim_canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
if (idx === 0) {
// Conical Tank Simulation
const h = val1;
const dV = val2;
const r = h / 3;
const dh_dt = dV / (Math.PI * Math.pow(r, 2));
document.getElementById('sim_live_readout').textContent = `dh/dt = ${dh_dt.toFixed(3)} m/min | r = ${r.toFixed(2)} m`;
const cx = W / 2;
const topY = 30;
const coneH = 190;
const coneTopR = 90;
const tipY = topY + coneH;
// Inverted Cone Outline
ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx - coneTopR, topY);
ctx.lineTo(cx, tipY);
ctx.lineTo(cx + coneTopR, topY);
ctx.stroke();
// Cone Top Ellipse
ctx.beginPath();
ctx.ellipse(cx, topY, coneTopR, 18, 0, 0, Math.PI * 2);
ctx.stroke();
// Water inside cone
const waterFraction = h / 6.0;
const waterH = coneH * waterFraction;
const waterY = tipY - waterH;
const waterR = coneTopR * waterFraction;
const grad = ctx.createLinearGradient(0, waterY, 0, tipY);
grad.addColorStop(0, 'rgba(0, 245, 212, 0.7)');
grad.addColorStop(1, 'rgba(2, 132, 199, 0.9)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(cx - waterR, waterY);
ctx.lineTo(cx, tipY);
ctx.lineTo(cx + waterR, waterY);
ctx.closePath();
ctx.fill();
// Water surface ellipse
ctx.fillStyle = 'rgba(0, 245, 212, 0.9)';
ctx.beginPath();
ctx.ellipse(cx, waterY, waterR, 10 * waterFraction, 0, 0, Math.PI * 2);
ctx.fill();
// Text labels on canvas
ctx.fillStyle = '#F59E0B';
ctx.font = 'bold 12px Alexandria, sans-serif';
ctx.fillText(`h = ${h.toFixed(1)} m`, cx + waterR + 12, waterY + 4);
ctx.fillStyle = '#00F5D4';
ctx.fillText(`r = ${(h/3).toFixed(2)} m`, cx - 20, waterY - 8);
} else if (idx === 1) {
// Sliding Ladder Simulation
const x = val1;
const dx_dt = val2;
const y = Math.sqrt(Math.max(0.1, 25 - x * x));
const dy_dt = -(x * dx_dt) / y;
document.getElementById('sim_live_readout').textContent = `dy/dt = ${dy_dt.toFixed(2)} m/s (انزلاق للأسفل) | y = ${y.toFixed(2)} m`;
const originX = 480;
const originY = 210;
const scale = 32;
// Wall and Ground
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(originX, 20);
ctx.lineTo(originX, originY);
ctx.lineTo(80, originY);
ctx.stroke();
// Ladder Endpoints
const ladderTopY = originY - (y * scale);
const ladderBottomX = originX - (x * scale);
// Ladder Bar
ctx.strokeStyle = '#F59E0B';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(originX, ladderTopY);
ctx.lineTo(ladderBottomX, originY);
ctx.stroke();
// Base velocity arrow (leftwards away from wall)
ctx.strokeStyle = '#38BDF8';
ctx.fillStyle = '#38BDF8';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(ladderBottomX, originY + 16);
ctx.lineTo(ladderBottomX - 35, originY + 16);
ctx.stroke();
ctx.fillText(`dx/dt = +${dx_dt.toFixed(1)} m/s`, ladderBottomX - 110, originY + 20);
// Top velocity arrow (downwards)
ctx.strokeStyle = '#EF4444';
ctx.fillStyle = '#EF4444';
ctx.beginPath();
ctx.moveTo(originX + 16, ladderTopY);
ctx.lineTo(originX + 16, ladderTopY + 35);
ctx.stroke();
ctx.fillText(`dy/dt = ${dy_dt.toFixed(2)} m/s`, originX + 26, ladderTopY + 20);
} else if (idx === 2) {
// Projectile Simulation
const rad = (val1 * Math.PI) / 180;
const v0 = val2;
const g = 10;
const y0 = 45;
const v0y = v0 * Math.sin(rad);
const v0x = v0 * Math.cos(rad);
const hMax = y0 + (v0y * v0y) / (2 * g);
// Flight time
const disc = Math.sqrt(v0y * v0y + 2 * g * y0);
const tFlight = (v0y + disc) / g;
const range = v0x * tFlight;
document.getElementById('sim_live_readout').textContent = `أقصى ارتفاع H = ${hMax.toFixed(1)} m | زمن التحليق = ${tFlight.toFixed(2)} s`;
const startX = 60;
const groundY = 220;
const towerH = 45 * 0.9;
const startY = groundY - towerH;
// Draw Tower
ctx.fillStyle = 'rgba(255,255,255,0.1)';
ctx.fillRect(startX - 20, startY, 20, towerH);
ctx.strokeStyle = '#475569';
ctx.strokeRect(startX - 20, startY, 20, towerH);
// Draw Ground
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(30, groundY);
ctx.lineTo(W - 30, groundY);
ctx.stroke();
// Draw Trajectory Parabola
ctx.strokeStyle = '#00F5D4';
ctx.lineWidth = 2.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(startX, startY);
const dt = tFlight / 60;
const scaleX = (W - 140) / range;
const scaleY = (groundY - 30) / hMax;
for (let t = 0; t <= tFlight; t += dt) {
const px = startX + (v0x * t) * scaleX;
const py = groundY - (y0 + v0y * t - 0.5 * g * t * t) * scaleY;
ctx.lineTo(px, py);
}
ctx.stroke();
ctx.setLineDash([]);
// Apex Marker
const apexT = v0y / g;
const apexX = startX + (v0x * apexT) * scaleX;
const apexY = groundY - hMax * scaleY;
ctx.fillStyle = '#F59E0B';
ctx.beginPath();
ctx.arc(apexX, apexY, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillText(`H_max = ${hMax.toFixed(1)} m`, apexX - 35, apexY - 10);
}
}