{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", width)
.style("border", "1px solid black");
const charge = { x: width / 2, y: width / 2 };
const dots = [];
const speed = 2;
const emissionInterval = 500;
const updateInterval = 30;
const n = 20;
const maxDots = 500;
const maxChargeSpeed = speed * 0.9;
let dragging = false;
let targetPosition = { x: charge.x, y: charge.y };
let lastUpdateTime = 0;
function updateTargetPosition(event) {
const now = Date.now();
if (dragging && now - lastUpdateTime > updateInterval) {
targetPosition.x = d3.pointer(event)[0];
targetPosition.y = d3.pointer(event)[1];
lastUpdateTime = now;
}
}
function moveCharge() {
const dx = targetPosition.x - charge.x;
const dy = targetPosition.y - charge.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > maxChargeSpeed) {
const scale = maxChargeSpeed / distance;
charge.x += dx * scale;
charge.y += dy * scale;
} else {
charge.x = targetPosition.x;
charge.y = targetPosition.y;
}
}
function emitDots() {
console.log("Emitting dots");
const velocityMag = Math.sqrt(targetPosition.x * targetPosition.x + targetPosition.y * targetPosition.y);
const beta = velocityMag / speed;
const gamma = 1 / Math.sqrt(1 - beta * beta);
for (let i = 0; i < n; i++) {
const angle = (2 * i * Math.PI) / n;
const adjustedAngle = Math.atan2(Math.sin(angle), gamma * (Math.cos(angle) + beta));
const velocity = {
x: Math.cos(adjustedAngle) * speed,
y: Math.sin(adjustedAngle) * speed
};
const color = (i % 2 === 0) ? "blue" : "orange";
dots.push({ x: charge.x, y: charge.y, vx: velocity.x, vy: velocity.y, color: color, direction: i });
}
while (dots.length > maxDots) {
dots.shift();
}
}
function updateDots() {
console.log("Updating dots");
dots.forEach(dot => {
dot.x += dot.vx;
dot.y += dot.vy;
});
for (let i = dots.length - 1; i >= 0; i--) {
if (dots[i].x < 0 || dots[i].x > width || dots[i].y < 0 || dots[i].y > width) {
dots.splice(i, 1);
}
}
}
function draw() {
console.log("Drawing");
const lines = svg.selectAll(".line").data(d3.range(n));
lines.exit().remove();
lines.enter()
.append("path")
.attr("class", "line")
.merge(lines)
.attr("d", d => {
const points = dots.filter(dot => dot.direction === d);
if (points.length < 1) return null;
const path = d3.line()
.x(dot => dot.x)
.y(dot => dot.y);
return path([...points, charge]);
})
.attr("stroke", d => (d % 2 === 0) ? "blue" : "orange")
.attr("stroke-width", 1)
.attr("fill", "none");
const circles = svg.selectAll(".dot").data(dots);
circles.exit().remove();
circles.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 2)
.merge(circles)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("fill", d => d.color);
svg.select("#charge").remove();
svg.append("circle")
.attr("id", "charge")
.attr("cx", charge.x)
.attr("cy", charge.y)
.attr("r", 5)
.attr("fill", "red");
}
svg.on("mousedown", () => {
dragging = true;
targetPosition = { x: charge.x, y: charge.y };
}).on("mouseup", () => {
dragging = false;
}).on("mousemove", updateTargetPosition);
setInterval(() => {
emitDots();
}, emissionInterval);
setInterval(() => {
if (dragging) {
moveCharge();
}
updateDots();
draw();
}, updateInterval);
return svg.node();
}