الكود المصدري

21 ملفًا كاملًا في 5 أقسام · JavaScript

رجوع إلى المحاكي

محرك البصريات

src/lib/optics.js · 206 سطرًا

سنيل، فرينل، العدسة الرقيقة، المرايا، المنشور، شقّان.

1/**
2 * محرك البصريات — دوال فيزيائية خالصة (بدون أي قيم ثابتة مزيفة).
3 * جميع الوحدات: الزوايا بالدرجات عند الحدود، وأطوال الموجة بالنانومتر،
4 * والأطوال بالمتر ما لم يُذكر خلاف ذلك.
5 */
6
7export const C_LIGHT = 299792458; // m/s
8export const DEG2RAD = Math.PI / 180;
9export const RAD2DEG = 180 / Math.PI;
10
11export const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
12
13export const dot = (a, b) => a.x * b.x + a.y * b.y;
14
15export function normalize(v) {
16 const L = Math.hypot(v.x, v.y) || 1;
17 return { x: v.x / L, y: v.y / L };
18}
19
20/** الزاوية الحرجة (درجات) — تُعرَّف فقط عند الانتقال من وسط أكثر كثافة ضوئية إلى أقل. */
21export function criticalAngle(n1, n2) {
22 if (!(n1 > 0) || !(n2 > 0)) return null;
23 if (!(n1 > n2)) return null;
24 const s = n2 / n1;
25 if (s >= 1) return null;
26 return Math.asin(s) * RAD2DEG;
27}
28
29/** قانون سنيل: n1·sinθ1 = n2·sinθ2 */
30export function refraction(n1, n2, theta1Deg) {
31 const theta1 = theta1Deg * DEG2RAD;
32 const sin1 = Math.sin(theta1);
33 const sin2 = (n1 / n2) * sin1;
34 const tc = criticalAngle(n1, n2);
35 if (Math.abs(sin2) > 1) {
36 return { tir: true, theta1Deg, theta1, sin1, sin2, theta2: null, theta2Deg: null, criticalAngleDeg: tc };
37 }
38 const theta2 = Math.asin(sin2);
39 return {
40 tir: false,
41 theta1Deg,
42 theta1,
43 sin1,
44 sin2,
45 theta2,
46 theta2Deg: theta2 * RAD2DEG,
47 criticalAngleDeg: tc,
48 };
49}
50
51/** معاملات فرينل لضوء غير مستقطب + التحقق من حفظ الطاقة */
52export function fresnel(n1, n2, theta1Deg) {
53 const t1 = theta1Deg * DEG2RAD;
54 const r = refraction(n1, n2, theta1Deg);
55 if (r.tir) return { R: 1, T: 0, Rs: 1, Rp: 1, tir: true, theta2Deg: null };
56 const c1 = Math.cos(t1);
57 const c2 = Math.cos(r.theta2);
58 const rs = (n1 * c1 - n2 * c2) / (n1 * c1 + n2 * c2);
59 const rp = (n2 * c1 - n1 * c2) / (n2 * c1 + n1 * c2);
60 const Rs = rs * rs;
61 const Rp = rp * rp;
62 return { Rs, Rp, R: (Rs + Rp) / 2, T: 1 - (Rs + Rp) / 2, tir: false, theta2Deg: r.theta2Deg };
63}
64
65/** معادلة العدسة الرقيقة: 1/f = 1/do + 1/di ، والتكبير M = -di/do */
66export function thinLens(doCm, fCm) {
67 if (!Number.isFinite(doCm) || !Number.isFinite(fCm) || doCm <= 0 || fCm === 0) {
68 return { di: null, m: null, type: 'undefined', atFocus: false, f: fCm };
69 }
70 const inv = 1 / fCm - 1 / doCm;
71 if (Math.abs(inv) < 1e-12) {
72 return { di: Infinity, m: null, type: 'at-focus', atFocus: true, f: fCm };
73 }
74 const di = 1 / inv;
75 const m = -di / doCm;
76 return { di, m, type: di > 0 ? 'real' : 'virtual', atFocus: false, f: fCm };
77}
78
79/** معادلة المرآة الكروية: 1/f = 1/do + 1/di مع f = R/2 (R>0 مقعرة، R<0 محدبة) */
80export function sphericalMirror(doCm, RCm) {
81 const f = Number.isFinite(RCm) ? RCm / 2 : Infinity;
82 if (!Number.isFinite(doCm) || doCm <= 0) return { di: null, m: null, type: 'undefined', f, atFocus: false };
83 const inv = (f === Infinity ? 0 : 1 / f) - 1 / doCm;
84 if (Math.abs(inv) < 1e-12) return { di: Infinity, m: null, type: 'at-focus', f, atFocus: true };
85 const di = 1 / inv;
86 return { di, m: -di / doCm, type: di > 0 ? 'real' : 'virtual', f, atFocus: false };
87}
88
89/** معاملات كوشي التقريبية: n(λ) = A + B/λ² ، λ بالميكرومتر */
90export const GLASSES = {
91 bk7: { id: 'bk7', name: 'زجاج بوروسيليكات BK7', A: 1.5046, B: 0.0042 },
92 crown: { id: 'crown', name: 'زجاج كراون', A: 1.522, B: 0.0046 },
93 flint: { id: 'flint', name: 'زجاج فلينت', A: 1.6034, B: 0.00895 },
94 acrylic: { id: 'acrylic', name: 'أكريليك', A: 1.4831, B: 0.0032 },
95 diamond: { id: 'diamond', name: 'ألماس', A: 2.38, B: 0.0106 },
96};
97
98export function cauchyN(lambdaNm, glass) {
99 const g = glass || GLASSES.bk7;
100 const um = lambdaNm / 1000;
101 return g.A + g.B / (um * um);
102}
103
104/** انحراف المنشور: r1 = sin⁻¹(sin i1 / n) ، r2 = A − r1 ، i2 = sin⁻¹(n·sin r2) ، δ = i1 + i2 − A */
105export function prismDeviation(apexDeg, incidenceDeg, n) {
106 const A = apexDeg * DEG2RAD;
107 const i1 = incidenceDeg * DEG2RAD;
108 const s1 = Math.sin(i1) / n;
109 if (Math.abs(s1) > 1) {
110 return { valid: false, tir: false, i1Deg: incidenceDeg, r1Deg: null, r2Deg: null, i2Deg: null, deltaDeg: null };
111 }
112 const r1 = Math.asin(s1);
113 const r2 = A - r1;
114 const s2 = n * Math.sin(r2);
115 const base = { valid: true, i1Deg: incidenceDeg, r1Deg: r1 * RAD2DEG, r2Deg: r2 * RAD2DEG };
116 if (Math.abs(s2) > 1) {
117 return { ...base, tir: true, i2Deg: null, deltaDeg: null };
118 }
119 const i2 = Math.asin(s2);
120 const delta = i1 + i2 - A;
121 return { ...base, tir: false, i2Deg: i2 * RAD2DEG, deltaDeg: delta * RAD2DEG };
122}
123
124/** زاوية الانحراف الصغرى: δmin = 2·sin⁻¹(n·sin(A/2)) − A */
125export function minDeviation(apexDeg, n) {
126 const A = apexDeg * DEG2RAD;
127 const s = n * Math.sin(A / 2);
128 if (s > 1) return null;
129 return (2 * Math.asin(s) - A) * RAD2DEG;
130}
131
132/** تجربة يونغ: تباعد الأهداب Δy = λL/d */
133export function fringeSpacing({ lambdaNm, slitMm, screenM }) {
134 const lambda = lambdaNm * 1e-9;
135 const d = slitMm * 1e-3;
136 const L = screenM;
137 const dyM = (lambda * L) / d;
138 return {
139 lambda,
140 d,
141 L,
142 dyM,
143 dyMm: dyM * 1000,
144 firstOrderAngleDeg: Math.asin(clamp(lambda / d, 0, 1)) * RAD2DEG,
145 maxOrder: Math.floor(d / lambda),
146 };
147}
148
149/** شدة التداخل لشقين (إهمال عرض الشق): I = cos²(π·d·sinθ/λ) */
150export function twoSlitIntensity(yMm, { lambdaNm, slitMm, screenM }) {
151 const lambda = lambdaNm * 1e-9;
152 const d = slitMm * 1e-3;
153 const y = yMm * 1e-3;
154 const theta = Math.atan2(y, screenM);
155 const delta = d * Math.sin(theta);
156 return Math.cos((Math.PI * delta) / lambda) ** 2;
157}
158
159export const mediumSpeed = (n) => C_LIGHT / n;
160export const waveFrequency = (lambdaNm) => C_LIGHT / (lambdaNm * 1e-9);
161export const wavelengthInMedium = (lambdaNm, n) => lambdaNm / n;
162
163/** انكسار متجهي: I اتجاه ساقط، N عمودي على السطح نحو جهة السقوط. يعيد null عند الانعكاس الكلي. */
164export function refractVector(I, N, n1, n2) {
165 const eta = n1 / n2;
166 const cosi = -dot(I, N);
167 const k = 1 - eta * eta * (1 - cosi * cosi);
168 if (k < 0) return null;
169 const s = eta * cosi - Math.sqrt(k);
170 return normalize({ x: eta * I.x + s * N.x, y: eta * I.y + s * N.y });
171}
172
173export function reflectVector(I, N) {
174 const d = dot(I, N);
175 return normalize({ x: I.x - 2 * d * N.x, y: I.y - 2 * d * N.y });
176}
177
178/** تقريب لون الطول الموجي (Bruton) لعرض الطيف */
179export function wavelengthToRGB(nm) {
180 let r = 0;
181 let g = 0;
182 let b = 0;
183 if (nm >= 380 && nm < 440) {
184 r = -(nm - 440) / 60;
185 b = 1;
186 } else if (nm >= 440 && nm < 490) {
187 g = (nm - 440) / 50;
188 b = 1;
189 } else if (nm >= 490 && nm < 510) {
190 g = 1;
191 b = -(nm - 510) / 20;
192 } else if (nm >= 510 && nm < 580) {
193 r = (nm - 510) / 70;
194 g = 1;
195 } else if (nm >= 580 && nm < 645) {
196 r = 1;
197 g = -(nm - 645) / 65;
198 } else if (nm >= 645) {
199 r = 1;
200 }
201 let f = 1;
202 if (nm >= 380 && nm < 420) f = 0.35 + (0.65 * (nm - 380)) / 40;
203 else if (nm > 700) f = 0.35 + (0.65 * (780 - nm)) / 80;
204 const c = (v) => Math.round(255 * Math.pow(clamp(v * f, 0, 1), 0.8));
205 return `rgb(${c(r)}, ${c(g)}, ${c(b)})`;
206}

الأكواد معروضة كما هي في المشروع تمامًا، وتتحدث تلقائيًا مع أي تعديل لاحق.