Симуляція електричного поля через canvas 2D з нуля
Візуалізатор електричного поля точкових зарядів — одна з найзадоволеніших маленьких симуляцій для побудови: жменька зарядів, одна формула (закон Кулона) і канва, повна стрілок або вигнутих силових ліній, що миттєво реагують, коли ви перетягуєте заряд. Цей урок будує все — векторну сітку, трасування силових ліній та перетягування — менш ніж за 150 рядків звичайного Canvas 2D JavaScript, без потреби у WebGL.
1. Модель даних заряду
Кожен точковий заряд потребує лише позицію та знакову величину (додатну чи від'ємну). Це весь стан, який має відстежувати симуляція — усе інше (стрілки, силові лінії, кольори) виводиться з цього масиву щокадру або за потреби.
const charges = [
{ x: 250, y: 300, q: +1 }, // додатний заряд
{ x: 550, y: 300, q: -1 }, // від'ємний заряд (пара диполя)
];
const K = 8000; // стала Кулона, перемасштабована під піксельні одиниці
2. Суперпозиція за Кулоном у точці
Електричне поле в будь-якій точці — це векторна сума внесків від кожного заряду: закон Кулона, застосований для кожного заряду окремо, а потім складений:
function fieldAt(x, y, charges) {
let ex = 0, ey = 0;
for (const c of charges) {
const dx = x - c.x, dy = y - c.y;
const distSq = Math.max(dx*dx + dy*dy, 36); // обмеження поблизу самого заряду
const dist = Math.sqrt(distSq);
const strength = K * c.q / distSq;
ex += strength * (dx / dist);
ey += strength * (dy / dist);
}
return { x: ex, y: ey };
}
3. Малювання сітки векторних стрілок
Найпростіша візуалізація вибирає fieldAt() на грубій
сітці по всій канві та малює коротку лінію (зі стрілочкою) у
напрямку поля в кожній точці вибірки, масштабовану так, щоб не
перевантажувати сусідні стрілки:
function drawArrowGrid(ctx, charges, spacing = 40) {
for (let y = spacing / 2; y < ctx.canvas.height; y += spacing) {
for (let x = spacing / 2; x < ctx.canvas.width; x += spacing) {
const e = fieldAt(x, y, charges);
const mag = Math.hypot(e.x, e.y);
if (mag < 1e-6) continue;
const len = Math.min(spacing * 0.4, mag * 0.02); // обмеження довжини стрілки
const ux = e.x / mag, uy = e.y / mag;
const x2 = x + ux * len, y2 = y + uy * len;
ctx.strokeStyle = 'rgba(251,191,36,0.85)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
// маленька стрілочка
const ang = Math.atan2(uy, ux);
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - 5 * Math.cos(ang - 0.4), y2 - 5 * Math.sin(ang - 0.4));
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - 5 * Math.cos(ang + 0.4), y2 - 5 * Math.sin(ang + 0.4));
ctx.stroke();
}
}
}
4. Трасування силових ліній методом RK4
Силові лінії — плавніші й інформативніші за сітку стрілок: вони показують реальний шлях, яким рухався б маленький пробний заряд. Трасуйте одну, інтегруючи (нормалізований) напрямок поля крок за кроком, починаючи трохи назовні від додатного заряду. Метод Рунге-Кутти 4-го порядку дає значно плавніші, менш "тремтливі" криві, ніж наївний крок Ейлера:
function traceFieldLine(startX, startY, charges, stepSize = 4, maxSteps = 400) {
const path = [{ x: startX, y: startY }];
let x = startX, y = startY;
const dir = (px, py) => {
const e = fieldAt(px, py, charges);
const mag = Math.max(Math.hypot(e.x, e.y), 1e-9);
return { x: e.x / mag, y: e.y / mag };
};
for (let s = 0; s < maxSteps; s++) {
// класичний RK4 на одиничному векторі напрямку поля
const k1 = dir(x, y);
const k2 = dir(x + k1.x * stepSize / 2, y + k1.y * stepSize / 2);
const k3 = dir(x + k2.x * stepSize / 2, y + k2.y * stepSize / 2);
const k4 = dir(x + k3.x * stepSize, y + k3.y * stepSize);
x += (stepSize / 6) * (k1.x + 2*k2.x + 2*k3.x + k4.x);
y += (stepSize / 6) * (k1.y + 2*k2.y + 2*k3.y + k4.y);
path.push({ x, y });
if (x < 0 || x > 800 || y < 0 || y > 600) break; // вийшла за межі канви
// зупинитись, якщо лінія дійшла до від'ємного заряду ("стоку")
if (charges.some(c => c.q < 0 && Math.hypot(x - c.x, y - c.y) < 10)) break;
}
return path;
}
Задайте кілька стартових точок навколо кожного додатного заряду
(наприклад, 12 точок, рівномірно розподілених на малому колі),
трасуйте одну силову лінію на кожну точку старту, і намалюйте
отриманий масив path як ламану. Силові лінії
починаються на додатних зарядах і закінчуються на від'ємних
(або йдуть у нескінченність, тобто до краю канви).
5. Кольорове кодування величини поля
Сам напрямок не показує силу поля. Швидкий спосіб
додати цей вимір без повного проходу теплокарти — розфарбувати
кожну стрілку (або фонову сітку маленьких точок) за
log(величина), оскільки сира величина охоплює
багато порядків розміру поблизу заряду:
function magnitudeColor(mag) {
// стискаємо величезний динамічний діапазон поблизу заряду за допомогою log()
const t = Math.min(Math.log(1 + mag) / 8, 1);
const hue = (1 - t) * 220; // синій (слабке) -> червоний (сильне)
return `hsl(${hue}, 90%, 55%)`;
}
strokeStyle у drawArrowGrid() на
magnitudeColor(mag), або намалюйте окремий
низько-роздільний фоновий шар кольорових точок під стрілками для
теплокарти "з першого погляду".
6. Інтерактивне перетягування зарядів
Дозвіл перетягувати заряди перетворює статичну діаграму на
експеримент. Шаблон повторює будь-яку взаємодію перетягування в
Canvas 2D: знайти найближчий заряд при mousedown,
слідувати за мишею при mousemove, відпустити при
mouseup:
let dragged = null;
canvas.addEventListener('mousedown', (e) => {
const { x, y } = getCanvasCoords(e);
dragged = charges.find(c => Math.hypot(c.x - x, c.y - y) < 14) || null;
});
canvas.addEventListener('mousemove', (e) => {
if (!dragged) return;
const { x, y } = getCanvasCoords(e);
dragged.x = x; dragged.y = y;
redraw(); // перетрасувати силові лінії та перемалювати сітку стрілок
});
addEventListener('mouseup', () => { dragged = null; });
Невелике покращення зручності: при кліку правою кнопкою миші (або клавіші-модифікаторі + клік) інвертуйте знак заряду під курсором, щоб користувачі могли перемикати додатний на від'ємний без окремого елемента керування UI.
7. Продуктивність: перемальовка лише за потреби
На відміну від фізичної симуляції з рухомими тілами, статичне електростатичне поле змінюється лише коли рухається заряд. Перерахування повної сітки стрілок і перетрасування кожної силової лінії 60 разів на секунду, коли нічого не змінилось, марнує процесорний час без візуальної користі:
let dirty = true; // встановлюйте true щоразу, коли заряд рухається, додається чи видаляється
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const lines = charges
.filter(c => c.q > 0)
.flatMap(c => seedPointsAround(c).map(p => traceFieldLine(p.x, p.y, charges)));
lines.forEach(path => strokePath(ctx, path));
drawArrowGrid(ctx, charges);
charges.forEach(c => drawCharge(ctx, c));
dirty = false;
}
function animate() {
if (dirty) redraw();
requestAnimationFrame(animate);
}
animate();
Часті запитання
Чого я навчуся в цьому уроці?
Створіть 2D симулятор електричного поля на звичайному Canvas: точкові заряди, суперпозиція за законом Кулона, сітка векторних стрілок, трасування силових ліній методом RK4 та демонстрація з перетягуванням заряду — менш ніж за 150 рядків JavaScript.
Які теми розглядаються в цьому уроці?
Цей урок охоплює такі теми: Модель даних заряду, Суперпозиція за Кулоном у точці, Малювання сітки векторних стрілок, Трасування силових ліній методом RK4, Кольорове кодування величини поля, Інтерактивне перетягування зарядів, Продуктивність: перемальовка лише за потреби.
Скільки часу займає цей урок?
Цей урок займає приблизно 25 хв.
Які попередні знання потрібні?
Це урок рівня «Середній рівень» — окрема попередня підготовка, крім базового JavaScript, не потрібна.