<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>