Aerotech commited on
Commit
8ca6130
·
verified ·
1 Parent(s): 424aeaa

Create a web-based application that simulates real-time airflow over a NACA airfoil, with interactive controls for modifying airfoil shape (NACA 4-digit), angle of attack, and wind speed. The airflow visualization must be responsive in real time within a browser using WebGL. ✅ Functional Requirements: Dynamic NACA Airfoil Generation: User inputs a NACA 4-digit code (e.g., 2412). Airfoil geometry is generated dynamically using standard equations. Real-Time Airflow Visualization: Simulate 2D or 2.5D airflow around the airfoil using: Lattice Boltzmann Method (LBM) or potential flow (fast, browser-safe). Fragment shaders (GPU) for fluid advection (WebGL via GLSL). Show streamlines or particle advection using vector fields. User Interaction: Inputs: NACA code (text input) Angle of attack (slider) Wind speed (slider) Button: “Reset Flow” / “Pause Simulation” Visualization: Display pressure regions with color map (red/blue for high/low). Show lift and drag indicators. Optionally: graphs of lift coefficient vs angle of attack. 🔧 Technologies: Frontend: HTML + CSS + JS WebGL via Three.js or custom GLSL shaders UI: dat.GUI, React, or simple DOM elements Physics/Simulation: JSPF (JavaScript Potential Flow) Or use Navier-Stokes approximation with shaders Option: WebAssembly + Rust/Fortran for fast CFD kernel 🎯 Goal: Enable users to interactively explore how airflow behaves over different NACA airfoils in real time, all within the browser (no external app or backend). - Initial Deployment

Browse files
Files changed (2) hide show
  1. README.md +6 -4
  2. index.html +659 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Naca
3
- emoji: 🌖
4
- colorFrom: gray
5
  colorTo: yellow
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: naca
3
+ emoji: 🐳
4
+ colorFrom: green
5
  colorTo: yellow
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,659 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
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>NACA Airfoil Flow Simulator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/dat.gui@0.7.7/build/dat.gui.min.js"></script>
10
+ <style>
11
+ .pressure-gradient {
12
+ background: linear-gradient(90deg, #0000ff, #ffffff, #ff0000);
13
+ }
14
+ canvas {
15
+ display: block;
16
+ width: 100% !important;
17
+ height: auto !important;
18
+ }
19
+ .tooltip {
20
+ position: relative;
21
+ display: inline-block;
22
+ }
23
+ .tooltip .tooltiptext {
24
+ visibility: hidden;
25
+ width: 200px;
26
+ background-color: #333;
27
+ color: #fff;
28
+ text-align: center;
29
+ border-radius: 6px;
30
+ padding: 5px;
31
+ position: absolute;
32
+ z-index: 1;
33
+ bottom: 125%;
34
+ left: 50%;
35
+ margin-left: -100px;
36
+ opacity: 0;
37
+ transition: opacity 0.3s;
38
+ }
39
+ .tooltip:hover .tooltiptext {
40
+ visibility: visible;
41
+ opacity: 1;
42
+ }
43
+ #renderCanvas {
44
+ touch-action: none;
45
+ }
46
+ </style>
47
+ </head>
48
+ <body class="bg-gray-100 font-sans">
49
+ <div class="container mx-auto px-4 py-8">
50
+ <header class="mb-8">
51
+ <h1 class="text-4xl font-bold text-center text-blue-800 mb-2">NACA Airfoil Flow Simulator</h1>
52
+ <p class="text-center text-gray-600">Interactive real-time airflow visualization over NACA airfoils</p>
53
+ </header>
54
+
55
+ <div class="flex flex-col lg:flex-row gap-8">
56
+ <!-- Controls Panel -->
57
+ <div class="w-full lg:w-1/4 bg-white rounded-lg shadow-lg p-6">
58
+ <h2 class="text-2xl font-semibold mb-4 text-gray-800">Controls</h2>
59
+
60
+ <div class="mb-6">
61
+ <label for="nacaCode" class="block text-sm font-medium text-gray-700 mb-1">NACA 4-digit Code</label>
62
+ <div class="flex">
63
+ <input type="text" id="nacaCode" value="2412" maxlength="4"
64
+ class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500">
65
+ <button id="updateAirfoil" class="bg-blue-600 text-white px-4 py-2 rounded-r-md hover:bg-blue-700 transition">
66
+ Update
67
+ </button>
68
+ </div>
69
+ <p class="text-xs text-gray-500 mt-1">Enter a 4-digit NACA code (e.g., 2412, 0012)</p>
70
+ </div>
71
+
72
+ <div class="mb-6">
73
+ <label for="aoaSlider" class="block text-sm font-medium text-gray-700 mb-1">
74
+ Angle of Attack: <span id="aoaValue">5</span>°
75
+ </label>
76
+ <input type="range" id="aoaSlider" min="-15" max="15" value="5" step="0.5"
77
+ class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
78
+ </div>
79
+
80
+ <div class="mb-6">
81
+ <label for="windSpeedSlider" class="block text-sm font-medium text-gray-700 mb-1">
82
+ Wind Speed: <span id="windSpeedValue">10</span> m/s
83
+ </label>
84
+ <input type="range" id="windSpeedSlider" min="1" max="30" value="10" step="1"
85
+ class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
86
+ </div>
87
+
88
+ <div class="mb-6">
89
+ <label class="block text-sm font-medium text-gray-700 mb-1">Flow Visualization</label>
90
+ <select id="visualizationMode" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
91
+ <option value="streamlines">Streamlines</option>
92
+ <option value="particles">Particles</option>
93
+ <option value="pressure">Pressure Field</option>
94
+ </select>
95
+ </div>
96
+
97
+ <div class="flex space-x-3 mb-6">
98
+ <button id="resetFlow" class="flex-1 bg-gray-600 text-white px-4 py-2 rounded-md hover:bg-gray-700 transition">
99
+ Reset Flow
100
+ </button>
101
+ <button id="pauseSim" class="flex-1 bg-yellow-600 text-white px-4 py-2 rounded-md hover:bg-yellow-700 transition">
102
+ Pause
103
+ </button>
104
+ </div>
105
+
106
+ <div class="bg-gray-100 p-4 rounded-lg">
107
+ <h3 class="font-medium text-gray-800 mb-2">Performance Metrics</h3>
108
+ <div class="grid grid-cols-2 gap-2">
109
+ <div>
110
+ <p class="text-xs text-gray-600">Lift Coefficient (C<sub>L</sub>):</p>
111
+ <p id="liftCoeff" class="font-bold">0.75</p>
112
+ </div>
113
+ <div>
114
+ <p class="text-xs text-gray-600">Drag Coefficient (C<sub>D</sub>):</p>
115
+ <p id="dragCoeff" class="font-bold">0.02</p>
116
+ </div>
117
+ <div>
118
+ <p class="text-xs text-gray-600">Reynolds Number:</p>
119
+ <p id="reynoldsNumber" class="font-bold">6.7×10<sup>5</sup></p>
120
+ </div>
121
+ <div>
122
+ <p class="text-xs text-gray-600">FPS:</p>
123
+ <p id="fpsCounter" class="font-bold">60</p>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="mt-4">
129
+ <div class="flex items-center justify-between mb-1">
130
+ <span class="text-sm text-gray-700">Pressure Gradient</span>
131
+ </div>
132
+ <div class="pressure-gradient h-4 rounded-md"></div>
133
+ <div class="flex justify-between text-xs text-gray-600 mt-1">
134
+ <span>Low</span>
135
+ <span>High</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Visualization Canvas -->
141
+ <div class="w-full lg:w-3/4">
142
+ <div class="bg-white rounded-lg shadow-lg overflow-hidden">
143
+ <div id="renderCanvas" class="w-full h-96 lg:h-[32rem]"></div>
144
+ </div>
145
+
146
+ <div class="mt-4 bg-white rounded-lg shadow-lg p-4">
147
+ <h3 class="font-medium text-gray-800 mb-2">About NACA Airfoils</h3>
148
+ <p class="text-sm text-gray-600">
149
+ The NACA 4-digit series defines airfoil geometry using 4 digits (e.g., 2412):
150
+ <span class="tooltip">
151
+ <span class="font-bold">[?]</span>
152
+ <span class="tooltiptext">
153
+ First digit: maximum camber (% of chord)<br>
154
+ Second digit: camber position (tenths of chord)<br>
155
+ Last two digits: maximum thickness (% of chord)
156
+ </span>
157
+ </span>
158
+ </p>
159
+ <div class="mt-2 flex justify-center">
160
+ <button id="showClGraph" class="bg-blue-100 text-blue-800 px-4 py-2 rounded-md hover:bg-blue-200 transition text-sm">
161
+ Show C<sub>L</sub> vs Angle of Attack
162
+ </button>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Graph Modal -->
170
+ <div id="graphModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
171
+ <div class="bg-white rounded-lg p-6 w-11/12 max-w-3xl">
172
+ <div class="flex justify-between items-center mb-4">
173
+ <h3 class="text-xl font-semibold">Lift Coefficient vs Angle of Attack</h3>
174
+ <button id="closeModal" class="text-gray-500 hover:text-gray-700">
175
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
176
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
177
+ </svg>
178
+ </button>
179
+ </div>
180
+ <div class="border border-gray-200 rounded-lg p-4">
181
+ <canvas id="clGraphCanvas" class="w-full h-64"></canvas>
182
+ </div>
183
+ <div class="mt-4 text-sm text-gray-600">
184
+ <p>This graph shows the theoretical lift coefficient variation with angle of attack for the current NACA airfoil.</p>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <script>
190
+ // Main simulation variables
191
+ let scene, camera, renderer, airfoilMesh, flowParticles, flowField;
192
+ let simulationPaused = false;
193
+ let lastTimestamp = 0;
194
+ let frameCount = 0;
195
+ let lastFpsUpdate = 0;
196
+ let currentNacaCode = "2412";
197
+ let currentAoa = 5;
198
+ let currentWindSpeed = 10;
199
+ let currentVisualization = "streamlines";
200
+
201
+ // Initialize Three.js scene
202
+ function initScene() {
203
+ const canvas = document.getElementById('renderCanvas');
204
+
205
+ // Scene setup
206
+ scene = new THREE.Scene();
207
+ scene.background = new THREE.Color(0xf0f0f0);
208
+
209
+ // Camera setup
210
+ camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
211
+ camera.position.set(0, 0, 20);
212
+ camera.lookAt(0, 0, 0);
213
+
214
+ // Renderer setup
215
+ renderer = new THREE.WebGLRenderer({ antialias: true });
216
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
217
+ canvas.appendChild(renderer.domElement);
218
+
219
+ // Lighting
220
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
221
+ scene.add(ambientLight);
222
+
223
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
224
+ directionalLight.position.set(1, 1, 1);
225
+ scene.add(directionalLight);
226
+
227
+ // Grid helper
228
+ const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0xcccccc);
229
+ scene.add(gridHelper);
230
+
231
+ // Create initial airfoil
232
+ createAirfoil(currentNacaCode);
233
+
234
+ // Create flow visualization
235
+ createFlowVisualization();
236
+
237
+ // Handle window resize
238
+ window.addEventListener('resize', onWindowResize);
239
+
240
+ // Start animation loop
241
+ animate();
242
+ }
243
+
244
+ // Create NACA airfoil geometry
245
+ function createAirfoil(nacaCode) {
246
+ // Remove existing airfoil if present
247
+ if (airfoilMesh) {
248
+ scene.remove(airfoilMesh);
249
+ }
250
+
251
+ // Parse NACA code
252
+ const m = parseInt(nacaCode[0]) / 100; // Maximum camber
253
+ const p = parseInt(nacaCode[1]) / 10; // Position of maximum camber
254
+ const t = parseInt(nacaCode.substring(2)) / 100; // Thickness
255
+
256
+ // Generate airfoil points
257
+ const points = [];
258
+ const chordLength = 10;
259
+ const resolution = 100;
260
+
261
+ for (let i = 0; i <= resolution; i++) {
262
+ const x = i / resolution;
263
+
264
+ // Thickness distribution
265
+ const yt = 5 * t * (0.2969 * Math.sqrt(x) - 0.1260 * x - 0.3516 * x*x + 0.2843 * x*x*x - 0.1015 * x*x*x*x);
266
+
267
+ // Camber line
268
+ let yc, dyc;
269
+ if (x < p) {
270
+ yc = m * (2 * p * x - x * x) / (p * p);
271
+ dyc = 2 * m * (p - x) / (p * p);
272
+ } else {
273
+ yc = m * ((1 - 2 * p) + 2 * p * x - x * x) / ((1 - p) * (1 - p));
274
+ dyc = 2 * m * (p - x) / ((1 - p) * (1 - p));
275
+ }
276
+
277
+ // Upper and lower surfaces
278
+ const theta = Math.atan(dyc);
279
+ const xu = x - yt * Math.sin(theta);
280
+ const yu = yc + yt * Math.cos(theta);
281
+ const xl = x + yt * Math.sin(theta);
282
+ const yl = yc - yt * Math.cos(theta);
283
+
284
+ points.push(new THREE.Vector3((xu - 0.5) * chordLength, yu * chordLength, 0));
285
+ if (i > 0 && i < resolution) {
286
+ points.push(new THREE.Vector3((xl - 0.5) * chordLength, yl * chordLength, 0));
287
+ }
288
+ }
289
+
290
+ // Create geometry
291
+ const shape = new THREE.Shape();
292
+ shape.moveTo(points[0].x, points[0].y);
293
+ for (let i = 1; i < points.length; i++) {
294
+ shape.lineTo(points[i].x, points[i].y);
295
+ }
296
+ shape.lineTo(points[0].x, points[0].y);
297
+
298
+ const extrudeSettings = {
299
+ steps: 1,
300
+ depth: 0.5,
301
+ bevelEnabled: false
302
+ };
303
+
304
+ const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
305
+
306
+ // Create mesh
307
+ const material = new THREE.MeshPhongMaterial({
308
+ color: 0x3b82f6,
309
+ specular: 0x111111,
310
+ shininess: 30,
311
+ side: THREE.DoubleSide
312
+ });
313
+
314
+ airfoilMesh = new THREE.Mesh(geometry, material);
315
+ airfoilMesh.rotation.z = -currentAoa * Math.PI / 180;
316
+ scene.add(airfoilMesh);
317
+
318
+ // Update metrics
319
+ updateMetrics();
320
+ }
321
+
322
+ // Create flow visualization
323
+ function createFlowVisualization() {
324
+ // Remove existing flow if present
325
+ if (flowParticles) {
326
+ scene.remove(flowParticles);
327
+ }
328
+
329
+ // Create particle system for flow visualization
330
+ const particleCount = currentVisualization === "particles" ? 500 : 100;
331
+ const particles = new THREE.BufferGeometry();
332
+ const positions = new Float32Array(particleCount * 3);
333
+ const colors = new Float32Array(particleCount * 3);
334
+ const sizes = new Float32Array(particleCount);
335
+
336
+ // Initialize particles
337
+ for (let i = 0; i < particleCount; i++) {
338
+ // Random positions in front of the airfoil
339
+ positions[i * 3] = (Math.random() - 0.5) * 20;
340
+ positions[i * 3 + 1] = (Math.random() - 0.5) * 10 - 5;
341
+ positions[i * 3 + 2] = 0;
342
+
343
+ // Colors based on velocity (blue = slow, red = fast)
344
+ colors[i * 3] = 0.2 + Math.random() * 0.8;
345
+ colors[i * 3 + 1] = 0.2;
346
+ colors[i * 3 + 2] = 0.8 - Math.random() * 0.6;
347
+
348
+ sizes[i] = currentVisualization === "particles" ? 0.1 : 0.05;
349
+ }
350
+
351
+ particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));
352
+ particles.setAttribute('color', new THREE.BufferAttribute(colors, 3));
353
+ particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
354
+
355
+ // Particle material
356
+ const particleMaterial = new THREE.PointsMaterial({
357
+ size: 0.1,
358
+ vertexColors: true,
359
+ transparent: true,
360
+ opacity: 0.8,
361
+ sizeAttenuation: true
362
+ });
363
+
364
+ flowParticles = new THREE.Points(particles, particleMaterial);
365
+ scene.add(flowParticles);
366
+
367
+ // Create flow field for simulation
368
+ flowField = {
369
+ particles: positions,
370
+ velocities: new Float32Array(particleCount * 3),
371
+ ages: new Float32Array(particleCount)
372
+ };
373
+
374
+ // Initialize velocities and ages
375
+ for (let i = 0; i < particleCount; i++) {
376
+ flowField.velocities[i * 3] = currentWindSpeed * 0.05;
377
+ flowField.velocities[i * 3 + 1] = 0;
378
+ flowField.velocities[i * 3 + 2] = 0;
379
+
380
+ flowField.ages[i] = Math.random() * 100;
381
+ }
382
+ }
383
+
384
+ // Update flow simulation
385
+ function updateFlow(deltaTime) {
386
+ if (simulationPaused) return;
387
+
388
+ const positions = flowParticles.geometry.attributes.position.array;
389
+ const colors = flowParticles.geometry.attributes.color.array;
390
+ const particleCount = positions.length / 3;
391
+
392
+ for (let i = 0; i < particleCount; i++) {
393
+ const idx = i * 3;
394
+
395
+ // Update age
396
+ flowField.ages[i] += deltaTime;
397
+
398
+ // Reset particles that are too old or out of bounds
399
+ if (flowField.ages[i] > 100 ||
400
+ positions[idx] < -12 || positions[idx] > 12 ||
401
+ positions[idx + 1] < -8 || positions[idx + 1] > 8) {
402
+
403
+ positions[idx] = (Math.random() - 0.5) * 20 - 8;
404
+ positions[idx + 1] = (Math.random() - 0.5) * 10;
405
+ positions[idx + 2] = 0;
406
+
407
+ flowField.velocities[idx] = currentWindSpeed * 0.05;
408
+ flowField.velocities[idx + 1] = 0;
409
+ flowField.velocities[idx + 2] = 0;
410
+
411
+ flowField.ages[i] = 0;
412
+
413
+ // Set initial color
414
+ colors[idx] = 0.2 + Math.random() * 0.8;
415
+ colors[idx + 1] = 0.2;
416
+ colors[idx + 2] = 0.8 - Math.random() * 0.6;
417
+ }
418
+
419
+ // Simple flow simulation (potential flow approximation)
420
+ const x = positions[idx];
421
+ const y = positions[idx + 1];
422
+
423
+ // Distance to airfoil center
424
+ const dx = x - airfoilMesh.position.x;
425
+ const dy = y - airfoilMesh.position.y;
426
+ const distSq = dx * dx + dy * dy;
427
+
428
+ // Basic flow around a cylinder approximation
429
+ if (distSq < 16) {
430
+ // Near the airfoil, add some disturbance
431
+ const angle = Math.atan2(dy, dx);
432
+ const radius = Math.sqrt(distSq);
433
+
434
+ // Tangential velocity increases closer to the airfoil
435
+ const tangentialFactor = (1 / (radius * radius)) * 2;
436
+
437
+ // Add angle of attack effect
438
+ const aoaEffect = Math.sin(angle - airfoilMesh.rotation.z) * currentAoa * 0.01;
439
+
440
+ // Update velocity
441
+ flowField.velocities[idx] = currentWindSpeed * 0.05 * (Math.cos(angle) - tangentialFactor * Math.sin(angle) + aoaEffect);
442
+ flowField.velocities[idx + 1] = currentWindSpeed * 0.05 * (Math.sin(angle) + tangentialFactor * Math.cos(angle) + aoaEffect);
443
+
444
+ // Update color based on velocity magnitude
445
+ const velMag = Math.sqrt(
446
+ flowField.velocities[idx] * flowField.velocities[idx] +
447
+ flowField.velocities[idx + 1] * flowField.velocities[idx + 1]
448
+ );
449
+
450
+ // Map velocity to color (blue = low, red = high)
451
+ colors[idx] = 0.2 + Math.min(velMag * 10, 0.8);
452
+ colors[idx + 1] = 0.2;
453
+ colors[idx + 2] = 0.8 - Math.min(velMag * 5, 0.6);
454
+ } else {
455
+ // Far from airfoil, maintain free stream velocity
456
+ flowField.velocities[idx] = currentWindSpeed * 0.05;
457
+ flowField.velocities[idx + 1] = 0;
458
+ }
459
+
460
+ // Update position
461
+ positions[idx] += flowField.velocities[idx] * deltaTime * 60;
462
+ positions[idx + 1] += flowField.velocities[idx + 1] * deltaTime * 60;
463
+ }
464
+
465
+ // Mark attributes as needing update
466
+ flowParticles.geometry.attributes.position.needsUpdate = true;
467
+ flowParticles.geometry.attributes.color.needsUpdate = true;
468
+ }
469
+
470
+ // Update performance metrics
471
+ function updateMetrics() {
472
+ // Simple lift and drag coefficient calculations based on thin airfoil theory
473
+ const aoaRad = currentAoa * Math.PI / 180;
474
+ const cl = 2 * Math.PI * aoaRad + 0.1 * currentAoa; // Rough approximation
475
+ const cd = 0.01 + 0.1 * aoaRad * aoaRad; // Parasite drag + induced drag
476
+
477
+ document.getElementById('liftCoeff').textContent = cl.toFixed(3);
478
+ document.getElementById('dragCoeff').textContent = cd.toFixed(3);
479
+
480
+ // Reynolds number calculation (Re = ρVL/μ)
481
+ const chordLength = 1; // meters (reference length)
482
+ const airDensity = 1.225; // kg/m³
483
+ const dynamicViscosity = 1.8e-5; // Pa·s
484
+ const re = (airDensity * currentWindSpeed * chordLength) / dynamicViscosity;
485
+
486
+ document.getElementById('reynoldsNumber').textContent = (re / 1e5).toFixed(1) + "×10<sup>5</sup>";
487
+ }
488
+
489
+ // Animation loop
490
+ function animate(timestamp = 0) {
491
+ requestAnimationFrame(animate);
492
+
493
+ // Calculate delta time for smooth animation
494
+ const deltaTime = (timestamp - lastTimestamp) / 1000;
495
+ lastTimestamp = timestamp;
496
+
497
+ // Update FPS counter
498
+ frameCount++;
499
+ if (timestamp - lastFpsUpdate >= 1000) {
500
+ document.getElementById('fpsCounter').textContent = frameCount;
501
+ frameCount = 0;
502
+ lastFpsUpdate = timestamp;
503
+ }
504
+
505
+ // Update flow simulation
506
+ updateFlow(deltaTime);
507
+
508
+ // Rotate airfoil if angle of attack changed
509
+ airfoilMesh.rotation.z = -currentAoa * Math.PI / 180;
510
+
511
+ // Render scene
512
+ renderer.render(scene, camera);
513
+ }
514
+
515
+ // Handle window resize
516
+ function onWindowResize() {
517
+ const canvas = document.getElementById('renderCanvas');
518
+ camera.aspect = canvas.clientWidth / canvas.clientHeight;
519
+ camera.updateProjectionMatrix();
520
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
521
+ }
522
+
523
+ // Initialize UI event listeners
524
+ function initUI() {
525
+ // NACA code input
526
+ document.getElementById('updateAirfoil').addEventListener('click', () => {
527
+ const newCode = document.getElementById('nacaCode').value;
528
+ if (/^\d{4}$/.test(newCode)) {
529
+ currentNacaCode = newCode;
530
+ createAirfoil(currentNacaCode);
531
+ } else {
532
+ alert("Please enter a valid 4-digit NACA code (e.g., 2412)");
533
+ }
534
+ });
535
+
536
+ // Angle of attack slider
537
+ document.getElementById('aoaSlider').addEventListener('input', (e) => {
538
+ currentAoa = parseFloat(e.target.value);
539
+ document.getElementById('aoaValue').textContent = currentAoa;
540
+ updateMetrics();
541
+ });
542
+
543
+ // Wind speed slider
544
+ document.getElementById('windSpeedSlider').addEventListener('input', (e) => {
545
+ currentWindSpeed = parseFloat(e.target.value);
546
+ document.getElementById('windSpeedValue').textContent = currentWindSpeed;
547
+ updateMetrics();
548
+ });
549
+
550
+ // Visualization mode
551
+ document.getElementById('visualizationMode').addEventListener('change', (e) => {
552
+ currentVisualization = e.target.value;
553
+ createFlowVisualization();
554
+ });
555
+
556
+ // Reset flow button
557
+ document.getElementById('resetFlow').addEventListener('click', () => {
558
+ createFlowVisualization();
559
+ });
560
+
561
+ // Pause simulation button
562
+ document.getElementById('pauseSim').addEventListener('click', () => {
563
+ simulationPaused = !simulationPaused;
564
+ document.getElementById('pauseSim').textContent = simulationPaused ? "Resume" : "Pause";
565
+ document.getElementById('pauseSim').className = simulationPaused ?
566
+ "flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 transition" :
567
+ "flex-1 bg-yellow-600 text-white px-4 py-2 rounded-md hover:bg-yellow-700 transition";
568
+ });
569
+
570
+ // Show CL graph button
571
+ document.getElementById('showClGraph').addEventListener('click', showClGraph);
572
+ document.getElementById('closeModal').addEventListener('click', () => {
573
+ document.getElementById('graphModal').classList.add('hidden');
574
+ });
575
+ }
576
+
577
+ // Show CL vs AoA graph
578
+ function showClGraph() {
579
+ const modal = document.getElementById('graphModal');
580
+ modal.classList.remove('hidden');
581
+
582
+ // Create graph if not already exists
583
+ if (!window.clGraph) {
584
+ const canvas = document.getElementById('clGraphCanvas');
585
+ const ctx = canvas.getContext('2d');
586
+
587
+ // Generate data points
588
+ const data = [];
589
+ for (let aoa = -15; aoa <= 15; aoa += 0.5) {
590
+ const aoaRad = aoa * Math.PI / 180;
591
+ const cl = 2 * Math.PI * aoaRad + 0.1 * aoa; // Rough approximation
592
+ data.push({x: aoa, y: cl});
593
+ }
594
+
595
+ // Draw graph
596
+ window.clGraph = new Chart(ctx, {
597
+ type: 'line',
598
+ data: {
599
+ datasets: [{
600
+ label: 'Lift Coefficient (CL)',
601
+ data: data,
602
+ borderColor: 'rgb(59, 130, 246)',
603
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
604
+ borderWidth: 2,
605
+ pointRadius: 0,
606
+ fill: true
607
+ }]
608
+ },
609
+ options: {
610
+ responsive: true,
611
+ scales: {
612
+ x: {
613
+ type: 'linear',
614
+ position: 'center',
615
+ title: {
616
+ display: true,
617
+ text: 'Angle of Attack (°)'
618
+ },
619
+ min: -15,
620
+ max: 15,
621
+ ticks: {
622
+ stepSize: 5
623
+ }
624
+ },
625
+ y: {
626
+ title: {
627
+ display: true,
628
+ text: 'Lift Coefficient (CL)'
629
+ },
630
+ min: -1.5,
631
+ max: 1.5
632
+ }
633
+ },
634
+ plugins: {
635
+ legend: {
636
+ position: 'top',
637
+ },
638
+ tooltip: {
639
+ callbacks: {
640
+ label: function(context) {
641
+ return `CL: ${context.parsed.y.toFixed(3)} at ${context.parsed.x}°`;
642
+ }
643
+ }
644
+ }
645
+ }
646
+ }
647
+ });
648
+ }
649
+ }
650
+
651
+ // Initialize everything when DOM is loaded
652
+ document.addEventListener('DOMContentLoaded', () => {
653
+ initScene();
654
+ initUI();
655
+ });
656
+ </script>
657
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
658
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Aerotech/naca" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
659
+ </html>