Туторіал · Електромагнетизм · Canvas 2D · JavaScript
📅 Липень 2026 ⏱ ≈ 25 хв 🎯 Середній рівень

Симуляція електричного поля через 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;  // стала Кулона, перемасштабована під піксельні одиниці
Навіщо перемасштабувати K? справжня стала Кулона (≈ 8.99 × 10⁹ Н·м²/Кл²) не має сенсу в піксельних координатах. Оберіть значення, за якого стрілки й силові лінії мають зручний для читання розмір на вашій канві — кілька тисяч добре працюють для величини заряду ±1 на канві 800×600.

2. Суперпозиція за Кулоном у точці

Електричне поле в будь-якій точці — це векторна сума внесків від кожного заряду: закон Кулона, застосований для кожного заряду окремо, а потім складений:

E(P) = Σᵢ K · qᵢ · (P − Pᵢ) / |P − Pᵢ|³
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 };
}
Обмеження distSq важливе: без нього точка вибірки, що потрапляє точно на (чи дуже близько до) заряду, дає величину поля, що прямує до нескінченності, через що ця стрілка домінує над усім малюнком. Обмеження мінімальної квадратичної відстані тримає стрілки поблизу заряду великими, але скінченними.

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, не потрібна.