@jjlmoya/utils-science 1.36.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/double-slit-decoherence/bibliography.astro +14 -0
- package/src/tool/double-slit-decoherence/bibliography.ts +12 -0
- package/src/tool/double-slit-decoherence/component.astro +235 -0
- package/src/tool/double-slit-decoherence/double-slit-decoherence-simulator.css +344 -0
- package/src/tool/double-slit-decoherence/entry.ts +26 -0
- package/src/tool/double-slit-decoherence/i18n/de.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/en.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/es.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/fr.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/id.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/it.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/ja.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/ko.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/nl.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/pl.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/pt.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/ru.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/sv.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/tr.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/zh.ts +181 -0
- package/src/tool/double-slit-decoherence/index.ts +11 -0
- package/src/tool/double-slit-decoherence/logic.ts +77 -0
- package/src/tool/double-slit-decoherence/seo.astro +15 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { stellarHabitabilityZone } from '../tool/stellar-habitability-zone/index
|
|
|
11
11
|
import { radioactiveDecay } from '../tool/radioactive-decay/index';
|
|
12
12
|
import { naturalSelectionDrift } from '../tool/natural-selection-drift/index';
|
|
13
13
|
import { entropySecondLaw } from '../tool/entropy-second-law/index';
|
|
14
|
+
import { doubleSlitDecoherence } from '../tool/double-slit-decoherence/index';
|
|
14
15
|
import { phaseDiagramCriticalPoints } from '../tool/phase-diagram-critical-points/index';
|
|
15
16
|
import { twinParadoxVisualizer } from '../tool/twin-paradox-visualizer/index';
|
|
16
17
|
import { mandelbrotFractal } from '../tool/mandelbrot-fractal/index';
|
|
@@ -20,7 +21,7 @@ import { rocheLimitSatelliteDisruption } from '../tool/roche-limit-satellite-dis
|
|
|
20
21
|
|
|
21
22
|
export const scienceCategory: ScienceCategoryEntry = {
|
|
22
23
|
icon: 'mdi:flask',
|
|
23
|
-
tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption],
|
|
24
|
+
tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption],
|
|
24
25
|
i18n: {
|
|
25
26
|
es: () => import('./i18n/es').then((m) => m.content),
|
|
26
27
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry'
|
|
|
11
11
|
export { radioactiveDecay } from './tool/radioactive-decay/entry';
|
|
12
12
|
export { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
|
|
13
13
|
export { entropySecondLaw } from './tool/entropy-second-law/entry';
|
|
14
|
+
export { doubleSlitDecoherence } from './tool/double-slit-decoherence/entry';
|
|
14
15
|
export { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
|
|
15
16
|
export { twinParadoxVisualizer } from './tool/twin-paradox-visualizer/entry';
|
|
16
17
|
export { mandelbrotFractal } from './tool/mandelbrot-fractal/entry';
|
|
@@ -30,10 +31,11 @@ import { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry'
|
|
|
30
31
|
import { radioactiveDecay } from './tool/radioactive-decay/entry';
|
|
31
32
|
import { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
|
|
32
33
|
import { entropySecondLaw } from './tool/entropy-second-law/entry';
|
|
34
|
+
import { doubleSlitDecoherence } from './tool/double-slit-decoherence/entry';
|
|
33
35
|
import { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
|
|
34
36
|
import { twinParadoxVisualizer } from './tool/twin-paradox-visualizer/entry';
|
|
35
37
|
import { mandelbrotFractal } from './tool/mandelbrot-fractal/entry';
|
|
36
38
|
import { planetAtmosphereSurvival } from './tool/planet-atmosphere-survival/entry';
|
|
37
39
|
import { threeBodyProblem } from './tool/three-body-problem/entry';
|
|
38
40
|
import { rocheLimitSatelliteDisruption } from './tool/roche-limit-satellite-disruption/entry';
|
|
39
|
-
export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption];
|
|
41
|
+
export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, doubleSlitDecoherence, phaseDiagramCriticalPoints, twinParadoxVisualizer, mandelbrotFractal, planetAtmosphereSurvival, threeBodyProblem, rocheLimitSatelliteDisruption];
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { STELLAR_HABITABILITY_ZONE_TOOL } from './tool/stellar-habitability-zone
|
|
|
12
12
|
export { RADIOACTIVE_DECAY_TOOL } from './tool/radioactive-decay/index';
|
|
13
13
|
export { NATURAL_SELECTION_DRIFT_TOOL } from './tool/natural-selection-drift/index';
|
|
14
14
|
export { ENTROPY_SECOND_LAW_TOOL } from './tool/entropy-second-law/index';
|
|
15
|
+
export { DOUBLE_SLIT_DECOHERENCE_TOOL } from './tool/double-slit-decoherence/index';
|
|
15
16
|
export { PHASE_DIAGRAM_CRITICAL_POINTS_TOOL } from './tool/phase-diagram-critical-points/index';
|
|
16
17
|
export { TWIN_PARADOX_VISUALIZER_TOOL } from './tool/twin-paradox-visualizer/index';
|
|
17
18
|
export { MANDELBROT_FRACTAL_TOOL } from './tool/mandelbrot-fractal/index';
|
|
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 19 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(19);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('scienceCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { doubleSlitDecoherence } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await doubleSlitDecoherence.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Feynman Lectures on Physics, Volume III, Chapter 1: Quantum Behavior',
|
|
6
|
+
url: 'https://www.feynmanlectures.caltech.edu/III_01.html',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Decoherence, the measurement problem, and interpretations of quantum mechanics',
|
|
10
|
+
url: 'https://arxiv.org/abs/quant-ph/0312059',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './double-slit-decoherence-simulator.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="dslit-lab" id="dslit-lab">
|
|
12
|
+
<section class="dslit-stage" aria-label={ui.stage}>
|
|
13
|
+
<div class="dslit-apparatus">
|
|
14
|
+
<canvas id="dslit-canvas" width="860" height="520"></canvas>
|
|
15
|
+
<div class="dslit-source" aria-hidden="true"></div>
|
|
16
|
+
<div class="dslit-barrier" aria-hidden="true">
|
|
17
|
+
<span></span>
|
|
18
|
+
<i></i>
|
|
19
|
+
<span></span>
|
|
20
|
+
<i></i>
|
|
21
|
+
<span></span>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="dslit-screen" aria-hidden="true"></div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="dslit-mode-strip">
|
|
27
|
+
<span>{ui.unobserved}</span>
|
|
28
|
+
<strong id="dslit-state-label" data-wave={ui.waveMode} data-particle={ui.particleMode}>{ui.waveMode}</strong>
|
|
29
|
+
<span>{ui.observed}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
33
|
+
<section class="dslit-panel" aria-label={ui.controls}>
|
|
34
|
+
<div class="dslit-kicker">{ui.kicker}</div>
|
|
35
|
+
<label class="dslit-toggle">
|
|
36
|
+
<input id="dslit-detector-enabled" type="checkbox" />
|
|
37
|
+
<span>{ui.detectorToggle}</span>
|
|
38
|
+
</label>
|
|
39
|
+
|
|
40
|
+
<label class="dslit-field" for="dslit-detector">
|
|
41
|
+
<span>{ui.detectorStrength}</span>
|
|
42
|
+
<output id="dslit-detector-output">0%</output>
|
|
43
|
+
<input id="dslit-detector" type="range" min="0" max="100" step="1" value="0" />
|
|
44
|
+
</label>
|
|
45
|
+
|
|
46
|
+
<label class="dslit-field" for="dslit-separation">
|
|
47
|
+
<span>{ui.slitSeparation}</span>
|
|
48
|
+
<output id="dslit-separation-output">1.80</output>
|
|
49
|
+
<input id="dslit-separation" type="range" min="80" max="280" step="5" value="180" />
|
|
50
|
+
</label>
|
|
51
|
+
|
|
52
|
+
<label class="dslit-field" for="dslit-width">
|
|
53
|
+
<span>{ui.slitWidth}</span>
|
|
54
|
+
<output id="dslit-width-output">0.42</output>
|
|
55
|
+
<input id="dslit-width" type="range" min="16" max="90" step="2" value="42" />
|
|
56
|
+
</label>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section class="dslit-readout" aria-label={ui.results}>
|
|
60
|
+
<article>
|
|
61
|
+
<span>{ui.fringeVisibility}</span>
|
|
62
|
+
<strong id="dslit-visibility">1.00</strong>
|
|
63
|
+
</article>
|
|
64
|
+
<article>
|
|
65
|
+
<span>{ui.whichPath}</span>
|
|
66
|
+
<strong id="dslit-path">0%</strong>
|
|
67
|
+
</article>
|
|
68
|
+
<article>
|
|
69
|
+
<span>{ui.coherence}</span>
|
|
70
|
+
<strong id="dslit-coherence">100%</strong>
|
|
71
|
+
</article>
|
|
72
|
+
<p>{ui.readoutNote}</p>
|
|
73
|
+
</section>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
import { calculateDoubleSlitPattern, measurePattern } from './logic';
|
|
78
|
+
|
|
79
|
+
const canvas = document.getElementById('dslit-canvas') as HTMLCanvasElement | null;
|
|
80
|
+
const ctx = canvas?.getContext('2d');
|
|
81
|
+
const detectorToggle = document.getElementById('dslit-detector-enabled') as HTMLInputElement | null;
|
|
82
|
+
const detectorInput = document.getElementById('dslit-detector') as HTMLInputElement | null;
|
|
83
|
+
const separationInput = document.getElementById('dslit-separation') as HTMLInputElement | null;
|
|
84
|
+
const widthInput = document.getElementById('dslit-width') as HTMLInputElement | null;
|
|
85
|
+
const detectorOutput = document.getElementById('dslit-detector-output');
|
|
86
|
+
const separationOutput = document.getElementById('dslit-separation-output');
|
|
87
|
+
const widthOutput = document.getElementById('dslit-width-output');
|
|
88
|
+
const visibilityOutput = document.getElementById('dslit-visibility');
|
|
89
|
+
const pathOutput = document.getElementById('dslit-path');
|
|
90
|
+
const coherenceOutput = document.getElementById('dslit-coherence');
|
|
91
|
+
const stateLabel = document.getElementById('dslit-state-label');
|
|
92
|
+
|
|
93
|
+
const state = {
|
|
94
|
+
detectorStrength: 0,
|
|
95
|
+
slitSeparation: 1.8,
|
|
96
|
+
slitWidth: 0.42,
|
|
97
|
+
wavelength: 0.36,
|
|
98
|
+
screenDistance: 4.2,
|
|
99
|
+
phase: 0,
|
|
100
|
+
raf: 0,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function isDarkTheme() {
|
|
104
|
+
return document.documentElement.classList.contains('theme-dark') || document.body.classList.contains('theme-dark');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createBeamGradient(sourceX: number, screenX: number, isDark: boolean) {
|
|
108
|
+
const gradient = ctx!.createLinearGradient(sourceX, 0, screenX, 0);
|
|
109
|
+
gradient.addColorStop(0, isDark ? 'rgba(64, 224, 208, 0.1)' : 'rgba(0, 139, 130, 0.16)');
|
|
110
|
+
gradient.addColorStop(0.48, isDark ? 'rgba(64, 224, 208, 0.3)' : 'rgba(0, 139, 130, 0.42)');
|
|
111
|
+
gradient.addColorStop(1, isDark ? 'rgba(255, 207, 102, 0.2)' : 'rgba(181, 111, 0, 0.32)');
|
|
112
|
+
|
|
113
|
+
return gradient;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function drawInterferencePaths(
|
|
117
|
+
samples: ReturnType<typeof calculateDoubleSlitPattern>,
|
|
118
|
+
scene: { sourceX: number; slitX: number; screenX: number; centerY: number; slitGap: number },
|
|
119
|
+
isDark: boolean,
|
|
120
|
+
) {
|
|
121
|
+
ctx!.strokeStyle = createBeamGradient(scene.sourceX, scene.screenX, isDark);
|
|
122
|
+
ctx!.lineWidth = isDark ? 1.2 : 1.45;
|
|
123
|
+
ctx!.globalCompositeOperation = isDark ? 'lighter' : 'multiply';
|
|
124
|
+
|
|
125
|
+
samples.forEach((sample, index) => {
|
|
126
|
+
if (index % 2 !== 0) return;
|
|
127
|
+
const screenY = scene.centerY + sample.y * 68;
|
|
128
|
+
const glow = Math.max(0.04, sample.observedIntensity);
|
|
129
|
+
ctx!.globalAlpha = Math.min(isDark ? 0.62 : 0.74, glow * (isDark ? 0.85 : 1.05));
|
|
130
|
+
ctx!.beginPath();
|
|
131
|
+
ctx!.moveTo(scene.sourceX, scene.centerY + Math.sin(state.phase + index * 0.05) * 8);
|
|
132
|
+
ctx!.quadraticCurveTo(scene.slitX, scene.centerY - scene.slitGap / 2, scene.screenX, screenY);
|
|
133
|
+
ctx!.stroke();
|
|
134
|
+
ctx!.beginPath();
|
|
135
|
+
ctx!.moveTo(scene.sourceX, scene.centerY + Math.cos(state.phase + index * 0.04) * 8);
|
|
136
|
+
ctx!.quadraticCurveTo(scene.slitX, scene.centerY + scene.slitGap / 2, scene.screenX, screenY);
|
|
137
|
+
ctx!.stroke();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
ctx!.globalAlpha = 1;
|
|
141
|
+
ctx!.globalCompositeOperation = 'source-over';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function drawBarrier(scene: { slitX: number; centerY: number; slitGap: number; slitWidth: number }, isDark: boolean) {
|
|
145
|
+
ctx!.fillStyle = isDark ? 'rgba(249, 245, 234, 0.82)' : 'rgba(9, 18, 32, 0.86)';
|
|
146
|
+
ctx!.fillRect(scene.slitX - 12, 58, 24, scene.centerY - scene.slitGap / 2 - scene.slitWidth / 2 - 58);
|
|
147
|
+
ctx!.fillRect(scene.slitX - 12, scene.centerY - scene.slitGap / 2 + scene.slitWidth / 2, 24, scene.slitGap - scene.slitWidth);
|
|
148
|
+
ctx!.fillRect(scene.slitX - 12, scene.centerY + scene.slitGap / 2 + scene.slitWidth / 2, 24, canvas!.height - (scene.centerY + scene.slitGap / 2 + scene.slitWidth / 2) - 58);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function drawScreenPattern(
|
|
152
|
+
samples: ReturnType<typeof calculateDoubleSlitPattern>,
|
|
153
|
+
scene: { screenX: number; centerY: number },
|
|
154
|
+
isDark: boolean,
|
|
155
|
+
) {
|
|
156
|
+
samples.forEach((sample) => {
|
|
157
|
+
const y = scene.centerY + sample.y * 68;
|
|
158
|
+
const intensity = Math.min(1, sample.observedIntensity);
|
|
159
|
+
ctx!.fillStyle = isDark
|
|
160
|
+
? `rgba(255, ${Math.round(178 + intensity * 60)}, 92, ${0.12 + intensity * 0.8})`
|
|
161
|
+
: `rgba(${Math.round(170 + intensity * 30)}, ${Math.round(95 + intensity * 60)}, 0, ${0.22 + intensity * 0.72})`;
|
|
162
|
+
ctx!.fillRect(scene.screenX, y - 1.2 - intensity * 3, 34 + intensity * 30, 2.4 + intensity * 6);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function drawBeam(samples: ReturnType<typeof calculateDoubleSlitPattern>) {
|
|
167
|
+
if (!ctx || !canvas) return;
|
|
168
|
+
|
|
169
|
+
const isDark = isDarkTheme();
|
|
170
|
+
const scene = {
|
|
171
|
+
sourceX: 86,
|
|
172
|
+
slitX: 330,
|
|
173
|
+
screenX: 742,
|
|
174
|
+
centerY: canvas.height / 2,
|
|
175
|
+
slitGap: state.slitSeparation * 58,
|
|
176
|
+
slitWidth: state.slitWidth * 42,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
180
|
+
drawInterferencePaths(samples, scene, isDark);
|
|
181
|
+
drawBarrier(scene, isDark);
|
|
182
|
+
drawScreenPattern(samples, scene, isDark);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function update() {
|
|
186
|
+
const samples = calculateDoubleSlitPattern(state, 201);
|
|
187
|
+
const metrics = measurePattern(samples, state.detectorStrength);
|
|
188
|
+
|
|
189
|
+
detectorOutput!.textContent = `${Math.round(state.detectorStrength * 100)}%`;
|
|
190
|
+
separationOutput!.textContent = state.slitSeparation.toFixed(2);
|
|
191
|
+
widthOutput!.textContent = state.slitWidth.toFixed(2);
|
|
192
|
+
visibilityOutput!.textContent = metrics.fringeVisibility.toFixed(2);
|
|
193
|
+
pathOutput!.textContent = `${Math.round(metrics.whichPath * 100)}%`;
|
|
194
|
+
coherenceOutput!.textContent = `${Math.round(metrics.coherence * 100)}%`;
|
|
195
|
+
stateLabel!.textContent = state.detectorStrength > 0.55
|
|
196
|
+
? stateLabel!.getAttribute('data-particle') ?? 'Particle bands'
|
|
197
|
+
: stateLabel!.getAttribute('data-wave') ?? 'Interference';
|
|
198
|
+
|
|
199
|
+
detectorInput?.style.setProperty('--fill', `${state.detectorStrength * 100}%`);
|
|
200
|
+
separationInput?.style.setProperty('--fill', `${((state.slitSeparation * 100 - 80) / 200) * 100}%`);
|
|
201
|
+
widthInput?.style.setProperty('--fill', `${((state.slitWidth * 100 - 16) / 74) * 100}%`);
|
|
202
|
+
|
|
203
|
+
drawBeam(samples);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function animate() {
|
|
207
|
+
state.phase += 0.025 * (1 - state.detectorStrength * 0.55);
|
|
208
|
+
update();
|
|
209
|
+
state.raf = window.requestAnimationFrame(animate);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
detectorToggle?.addEventListener('change', () => {
|
|
213
|
+
state.detectorStrength = detectorToggle.checked ? Math.max(0.72, Number(detectorInput?.value ?? 0) / 100) : 0;
|
|
214
|
+
if (detectorInput) detectorInput.value = String(Math.round(state.detectorStrength * 100));
|
|
215
|
+
update();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
detectorInput?.addEventListener('input', () => {
|
|
219
|
+
state.detectorStrength = Number(detectorInput.value) / 100;
|
|
220
|
+
if (detectorToggle) detectorToggle.checked = state.detectorStrength > 0.02;
|
|
221
|
+
update();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
separationInput?.addEventListener('input', () => {
|
|
225
|
+
state.slitSeparation = Number(separationInput.value) / 100;
|
|
226
|
+
update();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
widthInput?.addEventListener('input', () => {
|
|
230
|
+
state.slitWidth = Number(widthInput.value) / 100;
|
|
231
|
+
update();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
animate();
|
|
235
|
+
</script>
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
.dslit-lab {
|
|
2
|
+
--dslit-ink: #141722;
|
|
3
|
+
--dslit-muted: rgba(20, 23, 34, 0.62);
|
|
4
|
+
--dslit-line: rgba(20, 23, 34, 0.12);
|
|
5
|
+
--dslit-cyan: #40e0d0;
|
|
6
|
+
--dslit-cyan-ink: #008b82;
|
|
7
|
+
--dslit-amber: #ffcf66;
|
|
8
|
+
--dslit-amber-ink: #a86600;
|
|
9
|
+
--dslit-rose: #ff6f91;
|
|
10
|
+
--dslit-panel: transparent;
|
|
11
|
+
--dslit-track: rgba(20, 23, 34, 0.18);
|
|
12
|
+
|
|
13
|
+
display: grid;
|
|
14
|
+
gap: 1rem;
|
|
15
|
+
width: min(100%, 1220px);
|
|
16
|
+
padding: clamp(0.9rem, 3vw, 2rem);
|
|
17
|
+
color: var(--dslit-ink);
|
|
18
|
+
background:
|
|
19
|
+
linear-gradient(120deg, rgba(64, 224, 208, 0.18), transparent 38%),
|
|
20
|
+
linear-gradient(300deg, rgba(255, 207, 102, 0.18), transparent 44%),
|
|
21
|
+
#f7f3ea;
|
|
22
|
+
border: 1px solid rgba(20, 23, 34, 0.08);
|
|
23
|
+
border-radius: 24px;
|
|
24
|
+
box-shadow: 0 28px 70px rgba(45, 37, 24, 0.14);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.theme-dark .dslit-lab {
|
|
28
|
+
--dslit-ink: #f9f5ea;
|
|
29
|
+
--dslit-muted: rgba(249, 245, 234, 0.66);
|
|
30
|
+
--dslit-line: rgba(249, 245, 234, 0.14);
|
|
31
|
+
--dslit-panel: rgba(10, 14, 24, 0.54);
|
|
32
|
+
--dslit-track: rgba(249, 245, 234, 0.18);
|
|
33
|
+
|
|
34
|
+
background:
|
|
35
|
+
linear-gradient(120deg, rgba(64, 224, 208, 0.16), transparent 38%),
|
|
36
|
+
linear-gradient(300deg, rgba(255, 111, 145, 0.14), transparent 44%),
|
|
37
|
+
#090e18;
|
|
38
|
+
border-color: rgba(255, 255, 255, 0.08);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dslit-stage,
|
|
42
|
+
.dslit-panel,
|
|
43
|
+
.dslit-readout {
|
|
44
|
+
min-width: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dslit-stage {
|
|
48
|
+
display: grid;
|
|
49
|
+
gap: 0.85rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dslit-apparatus {
|
|
53
|
+
position: relative;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
min-height: 320px;
|
|
56
|
+
border: 1px solid var(--dslit-line);
|
|
57
|
+
border-radius: 18px;
|
|
58
|
+
background:
|
|
59
|
+
repeating-linear-gradient(90deg, transparent 0 42px, rgba(20, 23, 34, 0.045) 42px 43px),
|
|
60
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.16), transparent);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.theme-dark .dslit-apparatus {
|
|
64
|
+
background:
|
|
65
|
+
repeating-linear-gradient(90deg, transparent 0 42px, rgba(249, 245, 234, 0.035) 42px 43px),
|
|
66
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#dslit-canvas {
|
|
70
|
+
display: block;
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: auto;
|
|
73
|
+
aspect-ratio: 43 / 26;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dslit-source,
|
|
77
|
+
.dslit-screen,
|
|
78
|
+
.dslit-barrier {
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: 11%;
|
|
81
|
+
bottom: 11%;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dslit-source {
|
|
86
|
+
left: 8%;
|
|
87
|
+
width: 10px;
|
|
88
|
+
border-radius: 999px;
|
|
89
|
+
background: linear-gradient(180deg, transparent, var(--dslit-cyan-ink), transparent);
|
|
90
|
+
box-shadow: 0 0 22px rgba(0, 139, 130, 0.42);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.theme-dark .dslit-source {
|
|
94
|
+
background: linear-gradient(180deg, transparent, var(--dslit-cyan), transparent);
|
|
95
|
+
box-shadow: 0 0 28px rgba(64, 224, 208, 0.72);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.dslit-screen {
|
|
99
|
+
right: 10%;
|
|
100
|
+
width: 12px;
|
|
101
|
+
border-radius: 999px;
|
|
102
|
+
background: linear-gradient(180deg, transparent, rgba(168, 102, 0, 0.9), transparent);
|
|
103
|
+
box-shadow: 0 0 24px rgba(168, 102, 0, 0.28);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.theme-dark .dslit-screen {
|
|
107
|
+
background: linear-gradient(180deg, transparent, rgba(255, 207, 102, 0.92), transparent);
|
|
108
|
+
box-shadow: 0 0 34px rgba(255, 207, 102, 0.46);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.dslit-barrier {
|
|
112
|
+
left: 38.2%;
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-rows: 1fr 34px 0.7fr 34px 1fr;
|
|
115
|
+
width: 18px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.dslit-barrier span {
|
|
119
|
+
display: block;
|
|
120
|
+
background: rgba(20, 23, 34, 0.84);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.theme-dark .dslit-barrier span {
|
|
124
|
+
background: rgba(249, 245, 234, 0.82);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.dslit-barrier i {
|
|
128
|
+
display: block;
|
|
129
|
+
background: rgba(64, 224, 208, 0.22);
|
|
130
|
+
box-shadow: inset 0 0 16px rgba(64, 224, 208, 0.32);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.dslit-mode-strip {
|
|
134
|
+
display: grid;
|
|
135
|
+
grid-template-columns: 1fr minmax(8.5rem, auto) 1fr;
|
|
136
|
+
gap: 0.8rem;
|
|
137
|
+
align-items: center;
|
|
138
|
+
min-height: 2.2rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dslit-mode-strip span,
|
|
142
|
+
.dslit-kicker,
|
|
143
|
+
.dslit-field span,
|
|
144
|
+
.dslit-readout span {
|
|
145
|
+
color: var(--dslit-muted);
|
|
146
|
+
font-size: 0.72rem;
|
|
147
|
+
font-weight: 700;
|
|
148
|
+
letter-spacing: 0.12em;
|
|
149
|
+
text-transform: uppercase;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.dslit-mode-strip span:last-child {
|
|
153
|
+
text-align: right;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.dslit-mode-strip strong {
|
|
157
|
+
align-self: center;
|
|
158
|
+
justify-self: center;
|
|
159
|
+
padding: 0.22rem 0;
|
|
160
|
+
border-bottom: 1px solid currentcolor;
|
|
161
|
+
color: var(--dslit-ink);
|
|
162
|
+
font-size: 0.78rem;
|
|
163
|
+
line-height: 1;
|
|
164
|
+
text-align: center;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.dslit-panel {
|
|
168
|
+
display: grid;
|
|
169
|
+
gap: 1rem;
|
|
170
|
+
padding: 0.25rem 0 0.25rem 1rem;
|
|
171
|
+
border-left: 1px solid var(--dslit-line);
|
|
172
|
+
background: transparent;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.theme-dark .dslit-panel {
|
|
176
|
+
padding: 1rem;
|
|
177
|
+
border: 1px solid var(--dslit-line);
|
|
178
|
+
border-radius: 18px;
|
|
179
|
+
background: var(--dslit-panel);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.dslit-toggle {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: 0.75rem;
|
|
185
|
+
align-items: center;
|
|
186
|
+
min-height: 48px;
|
|
187
|
+
color: var(--dslit-ink);
|
|
188
|
+
font-weight: 750;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.dslit-toggle input {
|
|
193
|
+
width: 46px;
|
|
194
|
+
height: 26px;
|
|
195
|
+
appearance: none;
|
|
196
|
+
border: 1px solid rgba(20, 23, 34, 0.18);
|
|
197
|
+
border-radius: 999px;
|
|
198
|
+
background: rgba(20, 23, 34, 0.045);
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.theme-dark .dslit-toggle input {
|
|
203
|
+
border-color: var(--dslit-line);
|
|
204
|
+
background: rgba(249, 245, 234, 0.12);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.dslit-toggle input::before {
|
|
208
|
+
display: block;
|
|
209
|
+
width: 20px;
|
|
210
|
+
height: 20px;
|
|
211
|
+
margin: 2px;
|
|
212
|
+
border-radius: 50%;
|
|
213
|
+
background: var(--dslit-cyan-ink);
|
|
214
|
+
transition: transform 180ms ease, background-color 180ms ease;
|
|
215
|
+
content: '';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.theme-dark .dslit-toggle input::before {
|
|
219
|
+
background: var(--dslit-ink);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.dslit-toggle input:checked::before {
|
|
223
|
+
background: var(--dslit-rose);
|
|
224
|
+
transform: translateX(20px);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.dslit-field {
|
|
228
|
+
display: grid;
|
|
229
|
+
gap: 0.55rem;
|
|
230
|
+
width: 100%;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.dslit-field output {
|
|
234
|
+
font-size: clamp(2rem, 9vw, 3.2rem);
|
|
235
|
+
font-weight: 760;
|
|
236
|
+
letter-spacing: 0;
|
|
237
|
+
line-height: 0.9;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.dslit-field input[type='range'] {
|
|
241
|
+
--fill: 0%;
|
|
242
|
+
|
|
243
|
+
width: 100%;
|
|
244
|
+
height: 24px;
|
|
245
|
+
margin: 0;
|
|
246
|
+
appearance: none;
|
|
247
|
+
background: transparent;
|
|
248
|
+
vertical-align: middle;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.dslit-field input[type='range']::-webkit-slider-runnable-track {
|
|
252
|
+
height: 3px;
|
|
253
|
+
border-radius: 999px;
|
|
254
|
+
background: linear-gradient(90deg, var(--dslit-cyan-ink) 0 var(--fill), var(--dslit-track) var(--fill) 100%);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.dslit-field input[type='range']::-webkit-slider-thumb {
|
|
258
|
+
width: 16px;
|
|
259
|
+
height: 16px;
|
|
260
|
+
margin-top: -6.5px;
|
|
261
|
+
appearance: none;
|
|
262
|
+
border: 2px solid #f7f3ea;
|
|
263
|
+
border-radius: 50%;
|
|
264
|
+
background: var(--dslit-cyan-ink);
|
|
265
|
+
box-shadow: 0 0 0 3px rgba(0, 139, 130, 0.08);
|
|
266
|
+
cursor: grab;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.theme-dark .dslit-field input[type='range']::-webkit-slider-runnable-track {
|
|
270
|
+
background: linear-gradient(90deg, var(--dslit-cyan) 0 var(--fill), var(--dslit-track) var(--fill) 100%);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.theme-dark .dslit-field input[type='range']::-webkit-slider-thumb {
|
|
274
|
+
border-color: #090e18;
|
|
275
|
+
background: var(--dslit-ink);
|
|
276
|
+
box-shadow: 0 0 0 3px rgba(64, 224, 208, 0.08);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.dslit-field input[type='range']::-moz-range-track {
|
|
280
|
+
height: 3px;
|
|
281
|
+
border-radius: 999px;
|
|
282
|
+
background: var(--dslit-track);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.dslit-field input[type='range']::-moz-range-progress {
|
|
286
|
+
height: 3px;
|
|
287
|
+
border-radius: 999px;
|
|
288
|
+
background: var(--dslit-cyan-ink);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.dslit-field input[type='range']::-moz-range-thumb {
|
|
292
|
+
width: 16px;
|
|
293
|
+
height: 16px;
|
|
294
|
+
border: 2px solid #f7f3ea;
|
|
295
|
+
border-radius: 50%;
|
|
296
|
+
background: var(--dslit-cyan-ink);
|
|
297
|
+
box-shadow: 0 0 0 3px rgba(0, 139, 130, 0.08);
|
|
298
|
+
cursor: grab;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.theme-dark .dslit-field input[type='range']::-moz-range-progress {
|
|
302
|
+
background: var(--dslit-cyan);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.theme-dark .dslit-field input[type='range']::-moz-range-thumb {
|
|
306
|
+
border-color: #090e18;
|
|
307
|
+
background: var(--dslit-ink);
|
|
308
|
+
box-shadow: 0 0 0 3px rgba(64, 224, 208, 0.08);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.dslit-readout {
|
|
312
|
+
display: grid;
|
|
313
|
+
gap: 0.75rem;
|
|
314
|
+
align-content: start;
|
|
315
|
+
padding: 0.25rem 0 0.25rem 1rem;
|
|
316
|
+
border-left: 1px solid var(--dslit-line);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.dslit-readout article {
|
|
320
|
+
display: grid;
|
|
321
|
+
gap: 0.32rem;
|
|
322
|
+
padding: 0.75rem 0;
|
|
323
|
+
border-bottom: 1px solid var(--dslit-line);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.dslit-readout strong {
|
|
327
|
+
font-size: clamp(2rem, 8vw, 3.7rem);
|
|
328
|
+
line-height: 0.86;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.dslit-readout p {
|
|
332
|
+
max-width: 42ch;
|
|
333
|
+
margin: 0;
|
|
334
|
+
color: var(--dslit-muted);
|
|
335
|
+
font-size: 0.88rem;
|
|
336
|
+
line-height: 1.58;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@media (min-width: 940px) {
|
|
340
|
+
.dslit-lab {
|
|
341
|
+
grid-template-columns: minmax(0, 1.55fr) minmax(250px, 0.62fr) minmax(230px, 0.54fr);
|
|
342
|
+
align-items: start;
|
|
343
|
+
}
|
|
344
|
+
}
|