Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Textbook-Style Electric Field lines</title>
<style>
body { margin: 0; overflow: hidden; background-color: #f0f0f0; font-family: 'Courier New', Courier, monospace; color: #333; }
/* Educational Sidebar (styled like the textbook) */
#textbook-panel {
position: absolute; top: 0; right: 0; width: 320px; height: 100%;
background: #fafafa; border-left: 2px solid #999; padding: 20px;
box-sizing: border-box; overflow-y: auto; box-shadow: -5px 0 15px rgba(0,0,0,0.1);
}
h2 { border-bottom: 2px solid #333; padding-bottom: 10px; margin-top: 0; }
h3 { margin-top: 25px; color: #444; text-transform: uppercase; font-size: 1em; }
p { line-height: 1.5; font-size: 0.95em; }
.figure-caption { font-style: italic; color: #555; margin-top: 5px; margin-bottom: 15px; display: block; font-size: 0.9em;}
/* Controls Area */
#controls {
position: absolute; bottom: 20px; left: 20px; width: 280px;
background: rgba(255, 255, 255, 0.9); padding: 15px;
border-radius: 8px; border: 1px solid #ccc; box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.control-group { margin-bottom: 15px; }
label { display: block; font-size: 0.9em; margin-bottom: 8px; font-weight: bold;}
button {
width: 100%; padding: 10px; margin-bottom: 8px; cursor: pointer;
background: #fff; border: 2px solid #333; color: #333; font-weight: bold;
text-transform: uppercase; transition: 0.2s; font-family: inherit;
}
button:hover { background: #333; color: #fff; }
button.active { background: #333; color: #fff; border-color: #333;}
.reset-btn { background: #fee; border-color: #a33; color: #a33; margin-top: 10px;}
.reset-btn:hover { background: #a33; color: #fff; }
input[type="range"] { width: 100%; cursor: pointer; accent-color: #333; }
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="textbook-panel">
<h2>PHYSICS: Electrostatics</h2>
<h3>Electric Field Patterns</h3>
<p>In order to draw the electric field lines, we place positive test charges at different places. The electric field created by the source charges will exert a force on these test charges. The lines of force (or electric field lines) are drawn tangent to the net force vector at every point.</p>
<p>Use the presets below to visualize the classic patterns from your textbook image:</p>
<div class="control-group">
<button id="btn-positive" onclick="loadPreset('positive')">(1) Single Positive Charge</button>
<span class="figure-caption">Field lines are directed radially outward.</span>
<button id="btn-negative" onclick="loadPreset('negative')">(2) Single Negative Charge</button>
<span class="figure-caption">Field lines are directed radially inward.</span>
<button id="btn-like" onclick="loadPreset('like')">(3) Two Like Charges (+, +)</button>
<span class="figure-caption">Field lines repel. Middle region shows a zero field spot (neutral zone).</span>
<button id="btn-unlike" onclick="loadPreset('unlike')">(4) Two Unlike Charges (+, -)</button>
<span class="figure-caption">Field lines start from positive and end on negative. Attractive force.</span>
</div>
<div class="control-group" style="border-top: 1px solid #ccc; pt: 15px;">
<label>Line Density (Drafting Precision)</label>
<input type="range" id="density" min="8" max="48" step="4" value="24" oninput="updateDensity(this.value)">
</div>
<button class="reset-btn" onclick="clearScene()">Clear Canvas</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// --- Core Configuration ---
let scene, camera, renderer, controls;
let charges = []; // {mesh, strength, originalY}
let fieldLineGroup;
let currentPreset = 'unlike';
let lineDensity = 24; // Lines per charge
// Physics constants adjusted for visual scaling
const k = 100;
const stepSize = 0.1; // Length of each line segment
const maxSteps = 1000; // Prevent infinite lines
const stopDistanceSq = 0.6 * 0.6; // Stop line near a charge
init();
animate();
function init() {
// 1. Setup Scene (White background, orthographic-like view)
scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0); // Textbook paper color
// Orthographic camera makes 2D diagrams easier
const aspect = (window.innerWidth - 320) / window.innerHeight; // Account for panel
const d = 20;
camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 1, 1000);
camera.position.set(0, 0, 50); // Look straight down the Z axis
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth - 320, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('canvas-container').appendChild(renderer.domElement);
// Minimalist grid (optional, but helps spatial awareness)
// scene.add(new THREE.GridHelper(100, 100, 0xdddddd, 0xeeeeee));
// 2. Objects Group
fieldLineGroup = new THREE.Group();
scene.add(fieldLineGroup);
// 3. Setup interaction (Pan and Zoom only)
controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = false; // Keep it 2D
controls.screenSpacePanning = true;
// 4. Load initial state
loadPreset('unlike');
window.addEventListener('resize', onWindowResize);
}
// --- Core Physics Logic (Field Line Tracing) ---
// Calculate Net E-Field Vector at a point
function calculateField(x, y) {
let netField = new THREE.Vector2(0, 0);
for (const charge of charges) {
let pos = charge.mesh.position;
let dx = x - pos.x;
let dy = y - pos.y;
let distSq = dx * dx + dy * dy;
if (distSq < 0.1) continue; // Avoid singularity
let mag = (k * charge.strength) / distSq;
let dir = new THREE.Vector2(dx, dy).normalize();
netField.add(dir.multiplyScalar(mag));
}
return netField;
}
// Trace a single field line from a starting point
function traceLine(startX, startY, strength) {
let points = [];
let currentPos = new THREE.Vector2(startX, startY);
points.push(new THREE.Vector3(currentPos.x, currentPos.y, 0));
let lineDirection = strength > 0 ? 1 : -1; // + charges trace outward, - charges inward
for (let i = 0; i < maxSteps; i++) {
let eField = calculateField(currentPos.x, currentPos.y);
if (eField.length() < 0.01) break; // Field too weak (neutral zone)
// Move stepAlong field
let step = eField.normalize().multiplyScalar(stepSize * lineDirection);
currentPos.add(step);
points.push(new THREE.Vector3(currentPos.x, currentPos.y, 0));
// Check if we hit another charge or went too far
let hitCharge = false;
for (const c of charges) {
if (currentPos.distanceToSquared(c.mesh.position) < stopDistanceSq) {
// Snap last point to charge surface visually
points[points.length-1].copy(c.mesh.position);
hitCharge = true;
break;
}
}
if (hitCharge || currentPos.length() > 100) break;
}
if (points.length < 2) return null; // Geometry needs 2 points
// Create the line mesh
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x333333, linewidth: 1 });
const line = new THREE.Line(geometry, material);
// Add Arrowhead like the diagrams
if (points.length > 10) { // Only add arrows to decent sized lines
// Place arrow roughly 3/4 along the line
const arrowIndex = Math.floor(points.length * 0.7);
const p1 = points[arrowIndex];
const p2 = points[arrowIndex + 1];
if(p1 && p2) {
const dir = new THREE.Vector3().subVectors(p2, p1).normalize();
// Color arrow same as charge (textbook style varies, let's keep it monochromatic for clarity)
const arrowHelper = new THREE.ArrowHelper(dir, p1, 0.8, 0x333333, 0.3, 0.3);
line.add(arrowHelper);
}
}
return line;
}
// --- Visualization Management ---
function rebuildScene() {
// Clear old lines
while (fieldLineGroup.children.length > 0) {
fieldLineGroup.remove(fieldLineGroup.children[0]);
}
// 1. Trace lines starting angularly around each charge
charges.forEach(charge => {
const numLines = lineDensity;
const radius = 0.6; // Start just outside the sphere surface
for (let i = 0; i < numLines; i++) {
const angle = (i / numLines) * Math.PI * 2;
const startX = charge.mesh.position.x + Math.cos(angle) * radius;
const startY = charge.mesh.position.y + Math.sin(angle) * radius;
const lineMesh = traceLine(startX, startY, charge.strength);
if (lineMesh) fieldLineGroup.add(lineMesh);
}
});
}
// --- Interaction & Presets ---
function createChargeMesh(strength) {
const geo = new THREE.SphereGeometry(0.5, 16, 16);
const mat = new THREE.MeshBasicMaterial({
color: strength > 0 ? 0xff4444 : 0x4444ff, // Red for +, Blue for -
});
const mesh = new THREE.Mesh(geo, mat);
// Add text label (+ or -) like in diagrams
const canvas = document.createElement('canvas');
canvas.width = 64; canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.font = 'Bold 40px Courier New';
ctx.fillStyle = 'white';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(strength > 0 ? '+' : '-', 32, 35);
const texture = new THREE.CanvasTexture(canvas);
const spriteMat = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(spriteMat);
sprite.scale.set(1.5, 1.5, 1);
mesh.add(sprite); // Attach label to sphere
return mesh;
}
function addCharge(strength, x, y) {
const mesh = createChargeMesh(strength);
mesh.position.set(x, y, 0);
scene.add(mesh);
charges.push({ mesh, strength });
}
// Global Access Functions for HTML Buttons
window.clearScene = function() {
charges.forEach(c => scene.remove(c.mesh));
charges = [];
// Remove active class from buttons
document.querySelectorAll('.control-group button').forEach(b=>b.classList.remove('active'));
rebuildScene();
}
window.updateDensity = function(val) {
lineDensity = parseInt(val);
rebuildScene();
}
window.loadPreset = function(name) {
window.clearScene();
currentPreset = name;
// UI feedback
document.getElementById(`btn-${name}`).classList.add('active');
switch(name) {
case 'positive':
addCharge(1, 0, 0);
break;
case 'negative':
addCharge(-1, 0, 0);
break;
case 'like':
addCharge(1, -6, 0);
addCharge(1, 6, 0);
break;
case 'unlike':
default:
addCharge(1, -6, 0);
addCharge(-1, 6, 0);
break;
}
rebuildScene();
}
// --- Main Loop ---
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
function onWindowResize() {
const width = window.innerWidth - 320;
const aspect = width / window.innerHeight;
const d = camera.top; // Keep vertical scale consistent
camera.left = -d * aspect;
camera.right = d * aspect;
camera.updateProjectionMatrix();
renderer.setSize(width, window.innerHeight);
}
</script>
</body>
</html>
3
2
272KB
1286KB
310.0ms
200.0ms
311.0ms