<html lang="en">
<head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Interactive Toroidal Vector Field Simulator</title>
   
   <script src="https://cdn.tailwindcss.com"></script>
   
   <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
   
   <script type="importmap">
       {
           "imports": {
               "three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js",
               "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/"
           }
       }
   </script>
   <link rel="preconnect" href="https://fonts.googleapis.com" />
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
   <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Fira+Code&display=swap" rel="stylesheet" />
   <style>
       body {
           font-family: 'Inter', sans-serif;
       }
       .fira-code {
           font-family: 'Fira Code', monospace;
       }
       /* Custom radio button styles */
       input[type="radio"]:checked + label {
           background-color: #4F46E5; /* indigo-600 */
           border-color: #6366F1; /* indigo-500 */
           color: white;
       }
   </style>
</head>
<body class="bg-gray-900 text-gray-200 flex flex-col items-center justify-center min-h-screen p-4">

   <div class="flex flex-col items-center">
       <div id="simulation-container" class="w-[1000px] h-[1000px] bg-black rounded-t-lg shadow-2xl overflow-hidden relative border-2 border-gray-700">
       </div>

       <div id="ui-container" class="w-[1000px] h-[300px] bg-gray-800/50 backdrop-blur-sm rounded-b-lg shadow-2xl p-6 flex flex-col justify-between border-x-2 border-b-2 border-gray-700">
           
           <div class="flex justify-between items-start">
               <div>
                   <h1 class="text-2xl font-bold text-white">Interactive Toroidal Vector Field</h1>
                   <p class="text-gray-400">Adjust the flow type, direction, and geometry of the torus.</p>
               </div>
                <div class="flex gap-4">
                   
                   <fieldset class="flex gap-2 items-center">
                       <legend class="text-sm text-gray-400 mb-1">Flow Type</legend>
                       <div class="flex rounded-lg border border-gray-600">
                           <input type="radio" id="flow-toroidal" name="flow-type" value="toroidal" class="sr-only" checked />
                           <label for="flow-toroidal" class="cursor-pointer py-2 px-4 rounded-l-md bg-gray-700 hover:bg-gray-600 transition-colors">Toroidal</label>
                           
                           <input type="radio" id="flow-poloidal" name="flow-type" value="poloidal" class="sr-only" />
                           <label for="flow-poloidal" class="cursor-pointer py-2 px-4 rounded-r-md bg-gray-700 hover:bg-gray-600 transition-colors border-l border-gray-600">Poloidal</label>
                       </div>
                   </fieldset>
                    
                   <fieldset class="flex gap-2 items-center">
                       <legend class="text-sm text-gray-400 mb-1">Flow Direction</legend>
                        <div class="flex rounded-lg border border-gray-600">
                           <input type="radio" id="flow-dir-normal" name="flow-direction" value="normal" class="sr-only" checked />
                           <label for="flow-dir-normal" class="cursor-pointer py-2 px-4 rounded-l-md bg-gray-700 hover:bg-gray-600 transition-colors">Normal</label>
                           
                           <input type="radio" id="flow-dir-reverse" name="flow-direction" value="reverse" class="sr-only" />
                           <label for="flow-dir-reverse" class="cursor-pointer py-2 px-4 rounded-r-md bg-gray-700 hover:bg-gray-600 transition-colors border-l border-gray-600">Reverse</label>
                       </div>
                   </fieldset>
               </div>
           </div>

           
           <div class="flex-grow flex items-center justify-between gap-8">
               <div class="grid grid-cols-2 gap-x-8 gap-y-2">
                   <div>
                       <label for="major-radius" class="text-sm text-gray-400">Major Radius (R): <span id="major-radius-val" class="font-mono bg-gray-700 px-2 py-1 rounded"></span></label>
                       <input id="major-radius" type="range" min="2" max="10" value="6" step="0.1" class="w-56" />
                   </div>
                   <div>
                       <label for="minor-radius" class="text-sm text-gray-400">Minor Radius (r): <span id="minor-radius-val" class="font-mono bg-gray-700 px-2 py-1 rounded"></span></label>
                       <input id="minor-radius" type="range" min="1" max="5" value="2" step="0.1" class="w-56" />
                   </div>
                   <div>
                       <label for="density" class="text-sm text-gray-400">Vector Density: <span id="density-val" class="font-mono bg-gray-700 px-2 py-1 rounded"></span></label>
                       <input id="density" type="range" min="4" max="25" value="12" class="w-56" />
                   </div>
                    <div>
                       <label for="scale" class="text-sm text-gray-400">Vector Scale: <span id="scale-val" class="font-mono bg-gray-700 px-2 py-1 rounded"></span></label>
                       <input id="scale" type="range" min="0.1" max="4" value="1.2" step="0.1" class="w-56" />
                   </div>
               </div>
               <div id="math-display" class="bg-gray-900/50 border border-gray-700 p-3 rounded-lg flex-grow h-full">
                   <h3 class="text-sm text-gray-400 border-b border-gray-600 pb-1 mb-2">Vector Field Equation: T(u,v)</h3>
                   <div class="fira-code text-sm text-cyan-300 space-y-1">
                       <p>i = <span id="i-eq"></span></p>
                       <p>j = <span id="j-eq"></span></p>
                       <p>k = <span id="k-eq"></span></p>
                   </div>
               </div>
           </div>

           
           <div class="flex justify-end items-end h-10">
               <button id="simulate-button" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-lg transition-colors shadow-lg text-lg">
                   Simulate
               </button>
           </div>
       </div>
   </div>

   <script type="module">
       import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

       let scene, camera, renderer, controls, vectorFieldGroup, torusMesh;

       const simContainer = document.getElementById('simulation-container');
       const majorRadiusSlider = document.getElementById('major-radius');
       const minorRadiusSlider = document.getElementById('minor-radius');
       const densitySlider = document.getElementById('density');
       const scaleSlider = document.getElementById('scale');
       const simulateButton = document.getElementById('simulate-button');
       const majorRadiusVal = document.getElementById('major-radius-val');
       const minorRadiusVal = document.getElementById('minor-radius-val');
       const densityVal = document.getElementById('density-val');
       const scaleVal = document.getElementById('scale-val');
       const iEq = document.getElementById('i-eq');
       const jEq = document.getElementById('j-eq');
       const kEq = document.getElementById('k-eq');

       function updateSliderLabels() {
           majorRadiusVal.textContent = parseFloat(majorRadiusSlider.value).toFixed(1);
           minorRadiusVal.textContent = parseFloat(minorRadiusSlider.value).toFixed(1);
           densityVal.textContent = densitySlider.value;
           scaleVal.textContent = parseFloat(scaleSlider.value).toFixed(1);
       }

       function updateMathDisplay() {
           const flowType = document.querySelector('input[name="flow-type"]:checked').value;
           const flowDirection = document.querySelector('input[name="flow-direction"]:checked').value;
           const directionSign = (flowDirection === 'reverse') ? -1 : 1;

           let iStr, jStr, kStr;

           if (flowType === 'toroidal') {
               iStr = (directionSign * -1 > 0 ? '' : '-') + "(R + r·cos(v))·sin(u)";
               jStr = (directionSign * 1 > 0 ? '' : '-') + "(R + r·cos(v))·cos(u)";
               kStr = "0";
           } else { // poloidal
               iStr = (directionSign * -1 > 0 ? '' : '-') + "r·sin(v)·cos(u)";
               jStr = (directionSign * -1 > 0 ? '' : '-') + "r·sin(v)·sin(u)";
               kStr = (directionSign * 1 > 0 ? '' : '-') + "r·cos(v)";
           }

           iEq.textContent = iStr;
           jEq.textContent = jStr;
           kEq.textContent = kStr;
       }

       function initialize() {
           init3D();
           setupEventListeners();
           updateSliderLabels();
           updateMathDisplay(); // Initial display
           createVectorField();
       }

       function setupEventListeners() {
           simulateButton.addEventListener('click', createVectorField);
           window.addEventListener('resize', onWindowResize);
           [majorRadiusSlider, minorRadiusSlider, densitySlider, scaleSlider].forEach(slider => {
               slider.addEventListener('input', updateSliderLabels);
           });
           document.querySelectorAll('input[name="flow-type"], input[name="flow-direction"]').forEach(radio => {
               radio.addEventListener('change', updateMathDisplay);
           });
       }
       
       function init3D() {
           const width = simContainer.clientWidth;
           const height = simContainer.clientHeight;
           scene = new THREE.Scene();
           scene.background = new THREE.Color(0x000000);
           camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
           camera.position.set(12, 12, 12);
           renderer = new THREE.WebGLRenderer({ antialias: true });
           renderer.setSize(width, height);
           simContainer.appendChild(renderer.domElement);
           scene.add(new THREE.AmbientLight(0xcccccc, 1.0));
           const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
           directionalLight.position.set(20, 30, 10);
           scene.add(directionalLight);
           const axesHelper = new THREE.AxesHelper(15);
           scene.add(axesHelper);
           controls = new OrbitControls(camera, renderer.domElement);
           controls.enableDamping = true;
           animate3D();
       }
       
       function onWindowResize() {
           const width = simContainer.clientWidth;
           const height = simContainer.clientHeight;
           camera.aspect = width / height;
           camera.updateProjectionMatrix();
           renderer.setSize(width, height);
       }

       function createVectorField() {
           if (vectorFieldGroup) scene.remove(vectorFieldGroup);
           if (torusMesh) scene.remove(torusMesh);
           vectorFieldGroup = new THREE.Group();

           const R = parseFloat(majorRadiusSlider.value);
           const r = parseFloat(minorRadiusSlider.value);
           if (r > R) {
               minorRadiusSlider.value = R;
               updateSliderLabels();
               return createVectorField();
           }

           const torusGeometry = new THREE.TorusGeometry(R, r, 24, 100);
           const torusMaterial = new THREE.MeshStandardMaterial({
               color: 0x667788, transparent: true, opacity: 0.25, metalness: 0.5, roughness: 0.6
           });
           torusMesh = new THREE.Mesh(torusGeometry, torusMaterial);
           scene.add(torusMesh);

           const density = parseInt(densitySlider.value);
           const scale = parseFloat(scaleSlider.value);
           const flowType = document.querySelector('input[name="flow-type"]:checked').value;
           const flowDirection = document.querySelector('input[name="flow-direction"]:checked').value;
           
           const uSteps = flowType === 'toroidal' ? density * 2.5 : density * 1.5;
           const vSteps = flowType === 'toroidal' ? density : density * 2;
           const color = new THREE.Color();

           for (let i = 0; i < uSteps; i++) {
               const u = (i / uSteps) * 2 * Math.PI;
               for (let j = 0; j < vSteps; j++) {
                   const v = (j / vSteps) * 2 * Math.PI;

                   const pos_x = (R + r * Math.cos(v)) * Math.cos(u);
                   const pos_y = (R + r * Math.cos(v)) * Math.sin(u);
                   const pos_z = r * Math.sin(v);
                   const pos = new THREE.Vector3(pos_x, pos_y, pos_z);

                   let dir;
                   if (flowType === 'toroidal') {
                       const dir_x = -(R + r * Math.cos(v)) * Math.sin(u);
                       const dir_y =  (R + r * Math.cos(v)) * Math.cos(u);
                       dir = new THREE.Vector3(dir_x, dir_y, 0);
                       color.setHSL(u / (2 * Math.PI), 1.0, 0.5);
                   } else { // Poloidal
                       const dir_x = -r * Math.sin(v) * Math.cos(u);
                       const dir_y = -r * Math.sin(v) * Math.sin(u);
                       const dir_z = r * Math.cos(v);
                       dir = new THREE.Vector3(dir_x, dir_y, dir_z);
                       color.setHSL(v / (2 * Math.PI), 0.8, 0.6);
                   }

                   if (flowDirection === 'reverse') dir.negate();
                   
                   const arrow = new THREE.ArrowHelper(dir.clone().normalize(), pos, scale, color.getHex());
                   vectorFieldGroup.add(arrow);
               }
           }
           scene.add(vectorFieldGroup);
       }

       function animate3D() {
           requestAnimationFrame(animate3D);
           if (controls) controls.update();
           if (renderer && scene && camera) renderer.render(scene, camera);
       }
       
       initialize();
   </script>
</body>
</html>

 

We need your consent to load the translations

We use a third-party service to translate the website content that may collect data about your activity. Please review the details in the privacy policy and accept the service to view the translations.