<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<canvas id="canvas" width="700" height="500"></canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
function vec3_add(a, b) {
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}
function vec3_sub(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
function vec3_scale(v, s) {
return [v[0] * s, v[1] * s, v[2] * s];
}
function vec3_magnitude(v) {
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
function vec3_normalize(v) {
const mag = vec3_magnitude(v);
if (mag === 0) return [0, 0, 0];
return [v[0] / mag, v[1] / mag, v[2] / mag];
}
const charges = [{
pos: [250, 250, 0],
id: "negative"
},
{
pos: [450, 250, 0],
id: "positive"
}
];
const lines = 16;
const step_size = 4;
const max_steps = 1000;
function calculate_field_at_point(point, charges) {
let field = [0, 0, 0];
for (let charge of charges) {
let dir = vec3_sub(charge.pos, point);
let dist = vec3_magnitude(dir);
if (dist < 5) continue;
let strength = charge.id.includes("negative") ? 1 : -1;
strength /= (dist * dist);
field = vec3_add(field, vec3_scale(dir, strength));
}
return vec3_normalize(field);
}
function rk4_step(point, charges) {
let k1 = calculate_field_at_point(point, charges);
let temp = vec3_add(point, vec3_scale(k1, step_size * 0.5));
let k2 = calculate_field_at_point(temp, charges);
temp = vec3_add(point, vec3_scale(k2, step_size * 0.5));
let k3 = calculate_field_at_point(temp, charges);
temp = vec3_add(point, vec3_scale(k3, step_size));
let k4 = calculate_field_at_point(temp, charges);
return vec3_scale(
vec3_add(
vec3_add(
vec3_scale(k1, 1 / 6),
vec3_scale(k2, 1 / 3)
),
vec3_add(
vec3_scale(k3, 1 / 3),
vec3_scale(k4, 1 / 6)
)
),
step_size
);
}
function generate_circle_points(center, radius, num_points) {
let points = [];
for (let i = 0; i < num_points; i++) {
let angle = (i / num_points) * Math.PI * 2.0;
let x = center[0] + radius * Math.cos(angle);
let y = center[1] + radius * Math.sin(angle);
points.push([x, y, 0]);
}
return points;
}
function integrate_field_line(start_point, charges) {
let points = [start_point];
let current_point = [...start_point];
for (let i = 0; i < max_steps; i++) {
let step = rk4_step(current_point, charges);
if (vec3_magnitude(step) < 0.1) break;
current_point = vec3_add(current_point, step);
points.push([...current_point]);
let too_close = charges.some(charge => vec3_magnitude(vec3_sub(current_point, charge.pos)) < 15);
if (too_close) break;
if (current_point[0] < 0 || current_point[0] > canvas.width ||
current_point[1] < 0 || current_point[1] > canvas.height) break;
}
return points;
}
const positive_charges = charges.filter(c => c.id.includes("positive"));
for (let charge of positive_charges) {
let start_points = generate_circle_points(charge.pos, 35, lines);
for (let start_point of start_points) {
let points = integrate_field_line(start_point, charges);
if (points.length > 5) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
ctx.beginPath();
ctx.arc(charges[0].pos[0], charges[0].pos[1], 30, 0, Math.PI * 2);
ctx.fillStyle = "blue";
ctx.fill();
ctx.lineWidth = 4;
ctx.strokeStyle = "black";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(charges[0].pos[0] - 15, charges[0].pos[1]);
ctx.lineTo(charges[0].pos[0] + 15, charges[0].pos[1]);
ctx.lineWidth = 5;
ctx.strokeStyle = "black";
ctx.stroke();
ctx.beginPath();
ctx.arc(charges[1].pos[0], charges[1].pos[1], 30, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
ctx.lineWidth = 4;
ctx.strokeStyle = "black";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(charges[1].pos[0] - 15, charges[1].pos[1]);
ctx.lineTo(charges[1].pos[0] + 15, charges[1].pos[1]);
ctx.moveTo(charges[1].pos[0], charges[1].pos[1] - 15);
ctx.lineTo(charges[1].pos[0], charges[1].pos[1] + 15);
ctx.lineWidth = 5;
ctx.strokeStyle = "black";
ctx.stroke();
</script>
</body>
</html>
I am trying to simulate an electric field between two charged particles.
To do I sample points around the positive charge, take a series of small steps and approximate the direction of the electric field each time using Runge–Kutta.
However in my snippet the field lines cannot reach behind the negative charge on the left.
I tried generating lines from negative to positive charges but lines start colliding and it looks ugly.



