feat: Implement DML Interactive Canvas & Dynamic Visual Physics and Math Simulators with live sliders for calculus and mechanics
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user