Spaces:
Running
Running
Upload 2 files
Browse files- .gitattributes +1 -0
- index.html +727 -18
- rock-sprite.png +3 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
rock-sprite.png filter=lfs diff=lfs merge=lfs -text
|
index.html
CHANGED
@@ -1,19 +1,728 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
</html>
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
+
<title>Cloud Chamber Simulator</title>
|
7 |
+
<style>
|
8 |
+
body { margin: 0; overflow: hidden; background: #000; color: white; font-family: sans-serif; }
|
9 |
+
canvas { display: block; }
|
10 |
+
#rock-sprite {
|
11 |
+
position: absolute;
|
12 |
+
width: 80px;
|
13 |
+
height: 80px;
|
14 |
+
left: 50%;
|
15 |
+
top: 50%;
|
16 |
+
transform: translate(-50%, -50%);
|
17 |
+
cursor: grab;
|
18 |
+
z-index: 10;
|
19 |
+
user-select: none;
|
20 |
+
}
|
21 |
+
#rock-sprite:active {
|
22 |
+
cursor: grabbing;
|
23 |
+
}
|
24 |
+
#gui {
|
25 |
+
position: absolute;
|
26 |
+
top: 10px;
|
27 |
+
right: 10px;
|
28 |
+
background: rgba(0, 0, 0, 0.7);
|
29 |
+
padding: 10px;
|
30 |
+
border-radius: 8px;
|
31 |
+
font-size: 14px;
|
32 |
+
}
|
33 |
+
#performance {
|
34 |
+
position: absolute;
|
35 |
+
top: 10px;
|
36 |
+
left: 10px;
|
37 |
+
background: rgba(0, 0, 0, 0.7);
|
38 |
+
padding: 10px;
|
39 |
+
border-radius: 8px;
|
40 |
+
font-size: 14px;
|
41 |
+
font-family: monospace;
|
42 |
+
color: #0f0;
|
43 |
+
min-width: 120px;
|
44 |
+
}
|
45 |
+
#gui label {
|
46 |
+
display: block;
|
47 |
+
margin-top: 10px;
|
48 |
+
}
|
49 |
+
#gui input, #gui select {
|
50 |
+
width: 100%;
|
51 |
+
margin-top: 4px;
|
52 |
+
}
|
53 |
+
.slider-container {
|
54 |
+
display: flex;
|
55 |
+
align-items: center;
|
56 |
+
gap: 8px;
|
57 |
+
margin-top: 4px;
|
58 |
+
}
|
59 |
+
.slider-value {
|
60 |
+
font-size: 12px;
|
61 |
+
color: #ccc;
|
62 |
+
white-space: nowrap;
|
63 |
+
}
|
64 |
+
.slider-input {
|
65 |
+
flex: 1;
|
66 |
+
}
|
67 |
+
</style>
|
68 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/gl-matrix-min.js"></script>
|
69 |
+
</head>
|
70 |
+
<body>
|
71 |
+
<canvas id="glcanvas"></canvas>
|
72 |
+
<img id="rock-sprite" src="rock-sprite.png" alt="Radiation Source">
|
73 |
+
<div id="performance">
|
74 |
+
<div>FPS: <span id="fps-counter">0</span></div>
|
75 |
+
<div>Particles: <span id="particle-counter">0</span></div>
|
76 |
+
</div>
|
77 |
+
<div id="gui">
|
78 |
+
<label>Background Cosmic Intensity
|
79 |
+
<div class="slider-container">
|
80 |
+
<span class="slider-value">0</span>
|
81 |
+
<input type="range" id="intensity" class="slider-input" min="0" max="100" step="0.1" value="0">
|
82 |
+
<span class="slider-value">100</span>
|
83 |
+
</div>
|
84 |
+
<div class="slider-value">Current: <span id="intensity-value">0</span></div>
|
85 |
+
</label>
|
86 |
+
<label>Radiation Source Intensity
|
87 |
+
<div class="slider-container">
|
88 |
+
<span class="slider-value">0</span>
|
89 |
+
<input type="range" id="sourceIntensity" class="slider-input" min="0" max="10" step="0.1" value="2">
|
90 |
+
<span class="slider-value">10</span>
|
91 |
+
</div>
|
92 |
+
<div class="slider-value">Current: <span id="sourceIntensity-value">2</span></div>
|
93 |
+
</label>
|
94 |
+
<label>Particle Size
|
95 |
+
<div class="slider-container">
|
96 |
+
<span class="slider-value">0.05</span>
|
97 |
+
<input type="range" id="size" class="slider-input" min="0.05" max="2.0" step="0.05" value="0.6">
|
98 |
+
<span class="slider-value">2.0</span>
|
99 |
+
</div>
|
100 |
+
<div class="slider-value">Current: <span id="size-value">0.6</span></div>
|
101 |
+
</label>
|
102 |
+
<label>Heat Flow
|
103 |
+
<div class="slider-container">
|
104 |
+
<span class="slider-value">0</span>
|
105 |
+
<input type="range" id="heatFlow" class="slider-input" min="0" max="0.5" step="0.01" value="0.04">
|
106 |
+
<span class="slider-value">0.5</span>
|
107 |
+
</div>
|
108 |
+
<div class="slider-value">Current: <span id="heatFlow-value">0.04</span></div>
|
109 |
+
</label>
|
110 |
+
<label>Trail Lifetime
|
111 |
+
<div class="slider-container">
|
112 |
+
<span class="slider-value">0.01</span>
|
113 |
+
<input type="range" id="trailLife" class="slider-input" min="0.01" max="1.0" step="0.01" value="0.03">
|
114 |
+
<span class="slider-value">1.0</span>
|
115 |
+
</div>
|
116 |
+
<div class="slider-value">Current: <span id="trailLife-value">0.03</span></div>
|
117 |
+
</label>
|
118 |
+
<label>Central Source Material
|
119 |
+
<select id="radiationType">
|
120 |
+
<option value="Uranium-238">Uranium-238 (Alpha)</option>
|
121 |
+
<option value="Cesium-137">Cesium-137 (Beta/Gamma)</option>
|
122 |
+
<option value="Cobalt-60">Cobalt-60 (Gamma)</option>
|
123 |
+
<option value="Radium-226">Radium-226 (Alpha)</option>
|
124 |
+
<option value="Strontium-90">Strontium-90 (Beta)</option>
|
125 |
+
<option value="Americium-241">Americium-241 (Alpha)</option>
|
126 |
+
</select>
|
127 |
+
</label>
|
128 |
+
<label>Cosmic Particle Type
|
129 |
+
<select id="cosmicType">
|
130 |
+
<option value="Muons">Muons</option>
|
131 |
+
<option value="Protons">Protons</option>
|
132 |
+
<option value="Electrons">Electrons</option>
|
133 |
+
<option value="Pions">Pions</option>
|
134 |
+
<option value="Mixed">Mixed Cosmic Ray</option>
|
135 |
+
</select>
|
136 |
+
</label>
|
137 |
+
<label>Lift Force
|
138 |
+
<div class="slider-container">
|
139 |
+
<span class="slider-value">0</span>
|
140 |
+
<input type="range" id="liftForce" class="slider-input" min="0" max="0.1" step="0.005" value="0.05">
|
141 |
+
<span class="slider-value">0.1</span>
|
142 |
+
</div>
|
143 |
+
<div class="slider-value">Current: <span id="liftForce-value">0.05</span></div>
|
144 |
+
</label>
|
145 |
+
<label>Friction Variability
|
146 |
+
<div class="slider-container">
|
147 |
+
<span class="slider-value">0</span>
|
148 |
+
<input type="range" id="frictionVariance" class="slider-input" min="0" max="1.0" step="0.005" value="0.4">
|
149 |
+
<span class="slider-value">1.0</span>
|
150 |
+
</div>
|
151 |
+
<div class="slider-value">Current: <span id="frictionVariance-value">0.4</span></div>
|
152 |
+
</label>
|
153 |
+
<label>Particle Curvature
|
154 |
+
<div class="slider-container">
|
155 |
+
<span class="slider-value">0</span>
|
156 |
+
<input type="range" id="curvatureMultiplier" class="slider-input" min="0" max="5.0" step="0.1" value="1.0">
|
157 |
+
<span class="slider-value">5.0</span>
|
158 |
+
</div>
|
159 |
+
<div class="slider-value">Current: <span id="curvatureMultiplier-value">1.0</span></div>
|
160 |
+
</label>
|
161 |
+
<label>Trail Length Scale
|
162 |
+
<div class="slider-container">
|
163 |
+
<span class="slider-value">1.0</span>
|
164 |
+
<input type="range" id="trailLengthScale" class="slider-input" min="1.0" max="10.0" step="0.1" value="2.0">
|
165 |
+
<span class="slider-value">10.0</span>
|
166 |
+
</div>
|
167 |
+
<div class="slider-value">Current: <span id="trailLengthScale-value">2.0</span></div>
|
168 |
+
</label>
|
169 |
+
</div>
|
170 |
+
<script>
|
171 |
+
const params = {
|
172 |
+
intensity: 0,
|
173 |
+
sourceIntensity: 2,
|
174 |
+
size: 0.6,
|
175 |
+
heatFlow: 0.04,
|
176 |
+
trailLife: 0.03,
|
177 |
+
trailLengthScale: 2.0,
|
178 |
+
radiationType: 'Cesium-137',
|
179 |
+
cosmicType: 'Muons',
|
180 |
+
liftForce: 0.05,
|
181 |
+
frictionVariance: 0.4,
|
182 |
+
curvatureMultiplier: 1.0
|
183 |
+
};
|
184 |
+
|
185 |
+
for (const id in params) {
|
186 |
+
const el = document.getElementById(id);
|
187 |
+
el.addEventListener('input', () => {
|
188 |
+
params[id] = el.type === 'range' ? parseFloat(el.value) : el.value;
|
189 |
+
if (el.type === 'range') {
|
190 |
+
const valueSpan = document.getElementById(id + '-value');
|
191 |
+
if (valueSpan) valueSpan.textContent = el.value;
|
192 |
+
}
|
193 |
+
});
|
194 |
+
}
|
195 |
+
|
196 |
+
// Sprite dragging functionality
|
197 |
+
const rockSprite = document.getElementById('rock-sprite');
|
198 |
+
let isDragging = false;
|
199 |
+
let dragOffset = { x: 0, y: 0 };
|
200 |
+
let spritePosition = { x: 0, y: 0 }; // Position in world coordinates
|
201 |
+
|
202 |
+
function screenToWorld(screenX, screenY) {
|
203 |
+
const canvas = document.getElementById("glcanvas");
|
204 |
+
const rect = canvas.getBoundingClientRect();
|
205 |
+
|
206 |
+
// Convert to normalized device coordinates (-1 to 1)
|
207 |
+
const ndcX = ((screenX - rect.left) / rect.width) * 2 - 1;
|
208 |
+
const ndcY = -(((screenY - rect.top) / rect.height) * 2 - 1);
|
209 |
+
|
210 |
+
// Calculate world space dimensions at z=0 plane
|
211 |
+
// Camera is at z=-50, FOV = PI/3, distance to z=0 = 50
|
212 |
+
const fov = Math.PI / 3;
|
213 |
+
const distance = 50;
|
214 |
+
const halfHeight = Math.tan(fov / 2) * distance;
|
215 |
+
const halfWidth = halfHeight * (canvas.width / canvas.height);
|
216 |
+
|
217 |
+
// Convert NDC to world coordinates
|
218 |
+
const worldX = ndcX * halfWidth;
|
219 |
+
const worldY = ndcY * halfHeight;
|
220 |
+
|
221 |
+
return { x: worldX, y: worldY };
|
222 |
+
}
|
223 |
+
|
224 |
+
function worldToScreen(worldX, worldY) {
|
225 |
+
const canvas = document.getElementById("glcanvas");
|
226 |
+
const rect = canvas.getBoundingClientRect();
|
227 |
+
|
228 |
+
// Calculate world space dimensions at z=0 plane
|
229 |
+
const fov = Math.PI / 3;
|
230 |
+
const distance = 50;
|
231 |
+
const halfHeight = Math.tan(fov / 2) * distance;
|
232 |
+
const halfWidth = halfHeight * (canvas.width / canvas.height);
|
233 |
+
|
234 |
+
// Convert world coordinates to NDC
|
235 |
+
const ndcX = worldX / halfWidth;
|
236 |
+
const ndcY = worldY / halfHeight;
|
237 |
+
|
238 |
+
// Convert NDC to screen coordinates
|
239 |
+
const screenX = (ndcX + 1) * 0.5 * rect.width + rect.left;
|
240 |
+
const screenY = (-ndcY + 1) * 0.5 * rect.height + rect.top;
|
241 |
+
|
242 |
+
return { x: screenX, y: screenY };
|
243 |
+
}
|
244 |
+
|
245 |
+
rockSprite.addEventListener('mousedown', (e) => {
|
246 |
+
isDragging = true;
|
247 |
+
const rect = rockSprite.getBoundingClientRect();
|
248 |
+
dragOffset.x = e.clientX - rect.left - rect.width / 2;
|
249 |
+
dragOffset.y = e.clientY - rect.top - rect.height / 2;
|
250 |
+
e.preventDefault();
|
251 |
+
});
|
252 |
+
|
253 |
+
document.addEventListener('mousemove', (e) => {
|
254 |
+
if (isDragging) {
|
255 |
+
const newX = e.clientX - dragOffset.x;
|
256 |
+
const newY = e.clientY - dragOffset.y;
|
257 |
+
|
258 |
+
rockSprite.style.left = newX + 'px';
|
259 |
+
rockSprite.style.top = newY + 'px';
|
260 |
+
rockSprite.style.transform = 'translate(-50%, -50%)';
|
261 |
+
|
262 |
+
// Update world position
|
263 |
+
const worldPos = screenToWorld(newX, newY); // newX, newY is already the center due to CSS transform
|
264 |
+
spritePosition.x = worldPos.x;
|
265 |
+
spritePosition.y = worldPos.y;
|
266 |
+
}
|
267 |
+
});
|
268 |
+
|
269 |
+
document.addEventListener('mouseup', () => {
|
270 |
+
isDragging = false;
|
271 |
+
});
|
272 |
+
|
273 |
+
const canvas = document.getElementById("glcanvas");
|
274 |
+
const gl = canvas.getContext("webgl");
|
275 |
+
|
276 |
+
if (!gl) {
|
277 |
+
alert("WebGL not supported");
|
278 |
+
}
|
279 |
+
|
280 |
+
// Create matrices early so they can be used in resize function
|
281 |
+
const projection = glMatrix.mat4.create();
|
282 |
+
const modelView = glMatrix.mat4.create();
|
283 |
+
|
284 |
+
function resize() {
|
285 |
+
canvas.width = window.innerWidth;
|
286 |
+
canvas.height = window.innerHeight;
|
287 |
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
288 |
+
|
289 |
+
// Update projection matrix with new aspect ratio
|
290 |
+
glMatrix.mat4.perspective(projection, Math.PI / 3, canvas.width / canvas.height, 0.1, 100);
|
291 |
+
|
292 |
+
// Update sprite screen position to maintain world coordinates
|
293 |
+
if (spritePosition.x !== 0 || spritePosition.y !== 0) {
|
294 |
+
const screenPos = worldToScreen(spritePosition.x, spritePosition.y);
|
295 |
+
rockSprite.style.left = screenPos.x + 'px';
|
296 |
+
rockSprite.style.top = screenPos.y + 'px';
|
297 |
+
}
|
298 |
+
}
|
299 |
+
window.addEventListener('resize', resize);
|
300 |
+
resize();
|
301 |
+
|
302 |
+
const vsSource = `
|
303 |
+
attribute vec3 aPosition;
|
304 |
+
attribute float aLife;
|
305 |
+
attribute vec3 aColor;
|
306 |
+
attribute float aDensity;
|
307 |
+
uniform float uPointSize;
|
308 |
+
uniform mat4 uProjection;
|
309 |
+
uniform mat4 uModelView;
|
310 |
+
varying float vLife;
|
311 |
+
varying vec3 vColor;
|
312 |
+
varying float vDensity;
|
313 |
+
void main() {
|
314 |
+
gl_Position = uProjection * uModelView * vec4(aPosition, 1.0);
|
315 |
+
gl_PointSize = uPointSize * (0.5 + vDensity);
|
316 |
+
vLife = aLife;
|
317 |
+
vColor = aColor;
|
318 |
+
vDensity = aDensity;
|
319 |
+
}
|
320 |
+
`;
|
321 |
+
|
322 |
+
const fsSource = `
|
323 |
+
precision mediump float;
|
324 |
+
varying float vLife;
|
325 |
+
varying vec3 vColor;
|
326 |
+
varying float vDensity;
|
327 |
+
void main() {
|
328 |
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
329 |
+
float distance = length(coord);
|
330 |
+
|
331 |
+
float alpha = vLife * vDensity * (1.0 - smoothstep(0.0, 0.5, distance));
|
332 |
+
alpha *= exp(-distance * (2.0 + vDensity));
|
333 |
+
|
334 |
+
gl_FragColor = vec4(vColor, alpha);
|
335 |
+
}
|
336 |
+
`;
|
337 |
+
|
338 |
+
function compileShader(source, type) {
|
339 |
+
const shader = gl.createShader(type);
|
340 |
+
gl.shaderSource(shader, source);
|
341 |
+
gl.compileShader(shader);
|
342 |
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
343 |
+
console.error("Shader compile error:", gl.getShaderInfoLog(shader));
|
344 |
+
gl.deleteShader(shader);
|
345 |
+
return null;
|
346 |
+
}
|
347 |
+
return shader;
|
348 |
+
}
|
349 |
+
|
350 |
+
const vertexShader = compileShader(vsSource, gl.VERTEX_SHADER);
|
351 |
+
const fragmentShader = compileShader(fsSource, gl.FRAGMENT_SHADER);
|
352 |
+
const program = gl.createProgram();
|
353 |
+
gl.attachShader(program, vertexShader);
|
354 |
+
gl.attachShader(program, fragmentShader);
|
355 |
+
gl.linkProgram(program);
|
356 |
+
gl.useProgram(program);
|
357 |
+
|
358 |
+
gl.enable(gl.BLEND);
|
359 |
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
360 |
+
|
361 |
+
const aPosition = gl.getAttribLocation(program, "aPosition");
|
362 |
+
const aLife = gl.getAttribLocation(program, "aLife");
|
363 |
+
const aColor = gl.getAttribLocation(program, "aColor");
|
364 |
+
const aDensity = gl.getAttribLocation(program, "aDensity");
|
365 |
+
const uPointSize = gl.getUniformLocation(program, "uPointSize");
|
366 |
+
const uProjection = gl.getUniformLocation(program, "uProjection");
|
367 |
+
const uModelView = gl.getUniformLocation(program, "uModelView");
|
368 |
+
|
369 |
+
// Fixed particle pool system
|
370 |
+
const PARTICLE_POOL_SIZE = 40000;
|
371 |
+
const particles = [];
|
372 |
+
let activeParticleCount = 0;
|
373 |
+
|
374 |
+
// Initialize particle pool
|
375 |
+
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
|
376 |
+
particles.push({
|
377 |
+
position: [0, 0, 0],
|
378 |
+
age: 0,
|
379 |
+
totalLife: 0,
|
380 |
+
friction: 1.0,
|
381 |
+
color: [1.0, 1.0, 1.0],
|
382 |
+
source: 'cosmic',
|
383 |
+
density: 0.5,
|
384 |
+
particleType: 'mixed',
|
385 |
+
active: false
|
386 |
+
});
|
387 |
+
}
|
388 |
+
|
389 |
+
// Performance tracking
|
390 |
+
let frameCount = 0;
|
391 |
+
let lastTime = performance.now();
|
392 |
+
let fps = 0;
|
393 |
+
const fpsCounter = document.getElementById('fps-counter');
|
394 |
+
const particleCounter = document.getElementById('particle-counter');
|
395 |
+
|
396 |
+
// Particle recycling
|
397 |
+
function getInactiveParticle() {
|
398 |
+
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
|
399 |
+
if (!particles[i].active) {
|
400 |
+
return particles[i];
|
401 |
+
}
|
402 |
+
}
|
403 |
+
return null; // Pool is full
|
404 |
+
}
|
405 |
+
|
406 |
+
function activateParticle(particle, position, totalLife, friction, color, source, density, particleType) {
|
407 |
+
particle.position[0] = position[0];
|
408 |
+
particle.position[1] = position[1];
|
409 |
+
particle.position[2] = position[2];
|
410 |
+
particle.age = 0;
|
411 |
+
particle.totalLife = totalLife;
|
412 |
+
particle.friction = friction;
|
413 |
+
particle.color = color;
|
414 |
+
particle.source = source;
|
415 |
+
particle.density = density;
|
416 |
+
particle.particleType = particleType;
|
417 |
+
particle.active = true;
|
418 |
+
activeParticleCount++;
|
419 |
+
}
|
420 |
+
|
421 |
+
function deactivateParticle(particle) {
|
422 |
+
if (particle.active) {
|
423 |
+
particle.active = false;
|
424 |
+
activeParticleCount--;
|
425 |
+
}
|
426 |
+
}
|
427 |
+
|
428 |
+
function getRadiationProperties(material) {
|
429 |
+
const properties = {
|
430 |
+
'Uranium-238': {
|
431 |
+
length: [15, 25], spacing: 0.08, color: [1.0, 0.8, 0.6],
|
432 |
+
density: 0.9, curvature: 0.02, scattering: 0.1, type: 'alpha'
|
433 |
+
},
|
434 |
+
'Cesium-137': {
|
435 |
+
length: [20, 40], spacing: 0.06, color: [0.8, 1.0, 0.8],
|
436 |
+
density: 0.6, curvature: 0.05, scattering: 0.3, type: 'beta-gamma'
|
437 |
+
},
|
438 |
+
'Cobalt-60': {
|
439 |
+
length: [25, 50], spacing: 0.04, color: [0.6, 0.8, 1.0],
|
440 |
+
density: 0.3, curvature: 0.001, scattering: 0.05, type: 'gamma'
|
441 |
+
},
|
442 |
+
'Radium-226': {
|
443 |
+
length: [12, 20], spacing: 0.1, color: [1.0, 0.6, 0.8],
|
444 |
+
density: 0.95, curvature: 0.015, scattering: 0.08, type: 'alpha'
|
445 |
+
},
|
446 |
+
'Strontium-90': {
|
447 |
+
length: [30, 45], spacing: 0.05, color: [1.0, 1.0, 0.6],
|
448 |
+
density: 0.5, curvature: 0.08, scattering: 0.4, type: 'beta'
|
449 |
+
},
|
450 |
+
'Americium-241': {
|
451 |
+
length: [10, 18], spacing: 0.12, color: [0.8, 0.6, 1.0],
|
452 |
+
density: 0.85, curvature: 0.025, scattering: 0.12, type: 'alpha'
|
453 |
+
}
|
454 |
+
};
|
455 |
+
return properties[material] || properties['Uranium-238'];
|
456 |
+
}
|
457 |
+
|
458 |
+
function getCosmicProperties(type) {
|
459 |
+
const properties = {
|
460 |
+
'Muons': {
|
461 |
+
length: [200, 300], spacing: 0.03, penetration: 0.9,
|
462 |
+
density: 0.2, curvature: 0.001, scattering: 0.02, type: 'muon'
|
463 |
+
},
|
464 |
+
'Protons': {
|
465 |
+
length: [150, 250], spacing: 0.05, penetration: 0.7,
|
466 |
+
density: 0.6, curvature: 0.03, scattering: 0.15, type: 'proton'
|
467 |
+
},
|
468 |
+
'Electrons': {
|
469 |
+
length: [100, 180], spacing: 0.08, penetration: 0.3,
|
470 |
+
density: 0.15, curvature: 0.12, scattering: 0.6, type: 'electron'
|
471 |
+
},
|
472 |
+
'Pions': {
|
473 |
+
length: [180, 280], spacing: 0.06, penetration: 0.6,
|
474 |
+
density: 0.4, curvature: 0.04, scattering: 0.25, type: 'pion'
|
475 |
+
},
|
476 |
+
'Mixed': {
|
477 |
+
length: [150, 300], spacing: 0.04, penetration: 0.8,
|
478 |
+
density: 0.3, curvature: 0.06, scattering: 0.3, type: 'mixed'
|
479 |
+
}
|
480 |
+
};
|
481 |
+
return properties[type] || properties['Muons'];
|
482 |
+
}
|
483 |
+
|
484 |
+
function spawnCentralSourceTrail() {
|
485 |
+
const props = getRadiationProperties(params.radiationType);
|
486 |
+
const baseLength = props.length[0] + Math.floor(Math.random() * (props.length[1] - props.length[0]));
|
487 |
+
const length = Math.floor(baseLength * 5 * params.trailLengthScale * props.density);
|
488 |
+
|
489 |
+
let dir = [Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5];
|
490 |
+
const mag = Math.sqrt(dir[0]**2 + dir[1]**2 + dir[2]**2);
|
491 |
+
dir[0] /= mag; dir[1] /= mag; dir[2] /= mag;
|
492 |
+
|
493 |
+
let currentPos = [spritePosition.x, spritePosition.y, 0];
|
494 |
+
let currentDir = [...dir];
|
495 |
+
|
496 |
+
for (let i = 0; i < length; i++) {
|
497 |
+
if (Math.random() < props.scattering && i > 5) {
|
498 |
+
const scatterAngle = (Math.random() - 0.5) * props.scattering * 2;
|
499 |
+
const perpDir = [
|
500 |
+
Math.random() - 0.5,
|
501 |
+
Math.random() - 0.5,
|
502 |
+
Math.random() - 0.5
|
503 |
+
];
|
504 |
+
currentDir[0] += perpDir[0] * scatterAngle;
|
505 |
+
currentDir[1] += perpDir[1] * scatterAngle;
|
506 |
+
currentDir[2] += perpDir[2] * scatterAngle;
|
507 |
+
|
508 |
+
const newMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
|
509 |
+
currentDir[0] /= newMag; currentDir[1] /= newMag; currentDir[2] /= newMag;
|
510 |
+
}
|
511 |
+
|
512 |
+
const curveFactor = props.curvature * params.curvatureMultiplier * Math.sin(i * 0.1) * (1 + Math.random() * 0.5);
|
513 |
+
currentDir[0] += curveFactor * (Math.random() - 0.5);
|
514 |
+
currentDir[1] += curveFactor * (Math.random() - 0.5);
|
515 |
+
currentDir[2] += curveFactor * (Math.random() - 0.5);
|
516 |
+
|
517 |
+
const dirMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
|
518 |
+
currentDir[0] /= dirMag; currentDir[1] /= dirMag; currentDir[2] /= dirMag;
|
519 |
+
|
520 |
+
currentPos[0] += currentDir[0] * props.spacing;
|
521 |
+
currentPos[1] += currentDir[1] * props.spacing;
|
522 |
+
currentPos[2] += currentDir[2] * props.spacing;
|
523 |
+
|
524 |
+
const particle = getInactiveParticle();
|
525 |
+
if (particle) {
|
526 |
+
activateParticle(
|
527 |
+
particle,
|
528 |
+
[...currentPos],
|
529 |
+
params.trailLife + 1.0,
|
530 |
+
1.0 - Math.random() * params.frictionVariance,
|
531 |
+
props.color,
|
532 |
+
'central',
|
533 |
+
props.density,
|
534 |
+
props.type
|
535 |
+
);
|
536 |
+
}
|
537 |
+
}
|
538 |
+
}
|
539 |
+
|
540 |
+
function spawnCosmicTrail() {
|
541 |
+
const props = getCosmicProperties(params.cosmicType);
|
542 |
+
const baseLength = props.length[0] + Math.floor(Math.random() * (props.length[1] - props.length[0]));
|
543 |
+
const length = Math.floor(baseLength * 3 * params.trailLengthScale * props.density);
|
544 |
+
|
545 |
+
// Generate completely random direction for cosmic particles
|
546 |
+
const angle1 = Math.random() * Math.PI * 2;
|
547 |
+
const angle2 = Math.random() * Math.PI;
|
548 |
+
let currentDir = [
|
549 |
+
Math.sin(angle2) * Math.cos(angle1),
|
550 |
+
Math.sin(angle2) * Math.sin(angle1),
|
551 |
+
Math.cos(angle2)
|
552 |
+
];
|
553 |
+
|
554 |
+
// Start from a random position on the edge of the simulation space
|
555 |
+
const startDistance = 60 + Math.random() * 20;
|
556 |
+
const startAngle1 = Math.random() * Math.PI * 2;
|
557 |
+
const startAngle2 = Math.random() * Math.PI;
|
558 |
+
let currentPos = [
|
559 |
+
Math.sin(startAngle2) * Math.cos(startAngle1) * startDistance,
|
560 |
+
Math.sin(startAngle2) * Math.sin(startAngle1) * startDistance,
|
561 |
+
Math.cos(startAngle2) * startDistance
|
562 |
+
];
|
563 |
+
|
564 |
+
for (let i = 0; i < length; i++) {
|
565 |
+
if (Math.random() < props.scattering && i > 10) {
|
566 |
+
const scatterAngle = (Math.random() - 0.5) * props.scattering;
|
567 |
+
const perpDir = [
|
568 |
+
Math.random() - 0.5,
|
569 |
+
Math.random() - 0.5,
|
570 |
+
Math.random() - 0.5
|
571 |
+
];
|
572 |
+
currentDir[0] += perpDir[0] * scatterAngle;
|
573 |
+
currentDir[1] += perpDir[1] * scatterAngle;
|
574 |
+
currentDir[2] += perpDir[2] * scatterAngle;
|
575 |
+
|
576 |
+
const newMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
|
577 |
+
currentDir[0] /= newMag; currentDir[1] /= newMag; currentDir[2] /= newMag;
|
578 |
+
}
|
579 |
+
|
580 |
+
if (props.type === 'electron' || props.type === 'mixed') {
|
581 |
+
const tangleFactor = props.curvature * params.curvatureMultiplier * (1 + Math.random());
|
582 |
+
currentDir[0] += tangleFactor * (Math.random() - 0.5);
|
583 |
+
currentDir[1] += tangleFactor * (Math.random() - 0.5);
|
584 |
+
currentDir[2] += tangleFactor * (Math.random() - 0.5);
|
585 |
+
|
586 |
+
const dirMag = Math.sqrt(currentDir[0]**2 + currentDir[1]**2 + currentDir[2]**2);
|
587 |
+
currentDir[0] /= dirMag; currentDir[1] /= dirMag; currentDir[2] /= dirMag;
|
588 |
+
}
|
589 |
+
|
590 |
+
currentPos[0] += currentDir[0] * props.spacing;
|
591 |
+
currentPos[1] += currentDir[1] * props.spacing;
|
592 |
+
currentPos[2] += currentDir[2] * props.spacing;
|
593 |
+
|
594 |
+
const particle = getInactiveParticle();
|
595 |
+
if (particle) {
|
596 |
+
activateParticle(
|
597 |
+
particle,
|
598 |
+
[...currentPos],
|
599 |
+
(params.trailLife + 1.0) * props.penetration,
|
600 |
+
1.0 - Math.random() * params.frictionVariance * 0.5,
|
601 |
+
[1.0, 1.0, 1.0],
|
602 |
+
'cosmic',
|
603 |
+
props.density,
|
604 |
+
props.type
|
605 |
+
);
|
606 |
+
}
|
607 |
+
}
|
608 |
+
}
|
609 |
+
|
610 |
+
// Set up model view matrix (projection is handled in resize function)
|
611 |
+
glMatrix.mat4.translate(modelView, modelView, [0, 0, -50]);
|
612 |
+
|
613 |
+
const positionBuffer = gl.createBuffer();
|
614 |
+
const lifeBuffer = gl.createBuffer();
|
615 |
+
const colorBuffer = gl.createBuffer();
|
616 |
+
const densityBuffer = gl.createBuffer();
|
617 |
+
|
618 |
+
function render() {
|
619 |
+
// FPS calculation
|
620 |
+
frameCount++;
|
621 |
+
const currentTime = performance.now();
|
622 |
+
if (currentTime - lastTime >= 1000) {
|
623 |
+
fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
|
624 |
+
frameCount = 0;
|
625 |
+
lastTime = currentTime;
|
626 |
+
fpsCounter.textContent = fps;
|
627 |
+
}
|
628 |
+
|
629 |
+
// Update particle count
|
630 |
+
particleCounter.textContent = activeParticleCount;
|
631 |
+
|
632 |
+
gl.clearColor(0, 0, 0, 1);
|
633 |
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
634 |
+
|
635 |
+
if (Math.random() < 0.1 * params.intensity) {
|
636 |
+
spawnCosmicTrail();
|
637 |
+
}
|
638 |
+
|
639 |
+
if (Math.random() < 0.1 * params.sourceIntensity) {
|
640 |
+
spawnCentralSourceTrail();
|
641 |
+
}
|
642 |
+
|
643 |
+
// Update and deactivate particles
|
644 |
+
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
|
645 |
+
const p = particles[i];
|
646 |
+
if (!p.active) continue;
|
647 |
+
|
648 |
+
p.age += 0.01 * p.friction;
|
649 |
+
|
650 |
+
// Apply physics if particle is still in visible lifetime
|
651 |
+
if (p.age <= params.trailLife + 1.0) {
|
652 |
+
const f = params.heatFlow;
|
653 |
+
p.position[0] += (Math.random() - 0.5) * f * p.friction;
|
654 |
+
p.position[1] += (Math.random() - 0.5) * f * p.friction + params.liftForce * p.friction;
|
655 |
+
p.position[2] += (Math.random() - 0.5) * f * p.friction;
|
656 |
+
}
|
657 |
+
|
658 |
+
// Deactivate particles that have exceeded their lifetime or are out of bounds
|
659 |
+
if (p.age > p.totalLife ||
|
660 |
+
Math.abs(p.position[0]) > 100 ||
|
661 |
+
Math.abs(p.position[1]) > 100 ||
|
662 |
+
Math.abs(p.position[2]) > 100) {
|
663 |
+
deactivateParticle(p);
|
664 |
+
}
|
665 |
+
}
|
666 |
+
|
667 |
+
// Build arrays only for active particles
|
668 |
+
const posArray = new Float32Array(activeParticleCount * 3);
|
669 |
+
const lifeArray = new Float32Array(activeParticleCount);
|
670 |
+
const colorArray = new Float32Array(activeParticleCount * 3);
|
671 |
+
const densityArray = new Float32Array(activeParticleCount);
|
672 |
+
|
673 |
+
let activeIndex = 0;
|
674 |
+
for (let i = 0; i < PARTICLE_POOL_SIZE; i++) {
|
675 |
+
const p = particles[i];
|
676 |
+
if (!p.active) continue;
|
677 |
+
|
678 |
+
posArray.set(p.position, activeIndex * 3);
|
679 |
+
let alpha = 1.0;
|
680 |
+
const fadeStart = params.trailLife * 0.7;
|
681 |
+
if (p.age < fadeStart) {
|
682 |
+
alpha = 1.0;
|
683 |
+
} else if (p.age < p.totalLife) {
|
684 |
+
const fadeProgress = (p.age - fadeStart) / (p.totalLife - fadeStart);
|
685 |
+
alpha = 1.0 - Math.pow(fadeProgress, 0.8);
|
686 |
+
} else {
|
687 |
+
alpha = 0.0;
|
688 |
+
}
|
689 |
+
lifeArray[activeIndex] = Math.max(0.0, alpha);
|
690 |
+
colorArray.set(p.color || [1.0, 1.0, 1.0], activeIndex * 3);
|
691 |
+
densityArray[activeIndex] = p.density || 0.5;
|
692 |
+
activeIndex++;
|
693 |
+
}
|
694 |
+
|
695 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
696 |
+
gl.bufferData(gl.ARRAY_BUFFER, posArray, gl.DYNAMIC_DRAW);
|
697 |
+
gl.enableVertexAttribArray(aPosition);
|
698 |
+
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
|
699 |
+
|
700 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, lifeBuffer);
|
701 |
+
gl.bufferData(gl.ARRAY_BUFFER, lifeArray, gl.DYNAMIC_DRAW);
|
702 |
+
gl.enableVertexAttribArray(aLife);
|
703 |
+
gl.vertexAttribPointer(aLife, 1, gl.FLOAT, false, 0, 0);
|
704 |
+
|
705 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
|
706 |
+
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.DYNAMIC_DRAW);
|
707 |
+
gl.enableVertexAttribArray(aColor);
|
708 |
+
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
|
709 |
+
|
710 |
+
gl.bindBuffer(gl.ARRAY_BUFFER, densityBuffer);
|
711 |
+
gl.bufferData(gl.ARRAY_BUFFER, densityArray, gl.DYNAMIC_DRAW);
|
712 |
+
gl.enableVertexAttribArray(aDensity);
|
713 |
+
gl.vertexAttribPointer(aDensity, 1, gl.FLOAT, false, 0, 0);
|
714 |
+
|
715 |
+
gl.uniformMatrix4fv(uProjection, false, projection);
|
716 |
+
gl.uniformMatrix4fv(uModelView, false, modelView);
|
717 |
+
gl.uniform1f(uPointSize, 10.0 * params.size);
|
718 |
+
|
719 |
+
gl.drawArrays(gl.POINTS, 0, activeParticleCount);
|
720 |
+
|
721 |
+
requestAnimationFrame(render);
|
722 |
+
}
|
723 |
+
|
724 |
+
requestAnimationFrame(render);
|
725 |
+
</script>
|
726 |
+
</body>
|
727 |
</html>
|
728 |
+
|
rock-sprite.png
ADDED
![]() |
Git LFS Details
|