Проводим эксперимент Бюффона со спичками в браузере
easy

Проводим эксперимент Бюффона со спичками в браузере

Несложная визуализация красивого метода

Недавно мы рассказали про старый метод нахождения числа пи, который придумал в 18-м веке математик Карл Бюффон. Смысл в том, чтобы найти число пи, используя мировой хаос и энтропию. Как это сделать: 

  1. Берём ткань и рисуем на ней параллельные линии на расстоянии X друг от друга.
  2. Берём иглу длиной L, но так, чтобы её длина была меньше или равна расстоянию между линиями, то есть L <= X.
  3. Случайным образом бросаем иглу на ткань и смотрим, попала ли игла на одну из линий или нет.
  4. Считаем, сколько раз мы бросили иглу и сколько раз она попала на одну из линий.
  5. Отношение этих двух чисел даст нам число, похожее на пи. Чем больше бросков — тем результат будет ближе к пи.

Проводим эксперимент Бюффона со спичками в браузере

В статье мы давали код на Python, который всё быстро считал, но было непонятно, как ложились спички на поле для эксперимента. Сегодня сделаем визуальную часть — создадим страницу, на которой нарисуем линии и разбросаем спички, а потом посчитаем, сколько было пересечений и что получилось в итоге.

Вот как это будет выглядеть:

Проводим эксперимент Бюффона со спичками в браузере

Создаём страницу

За эксперимент будет отвечать скрипт, поэтому нам достаточно расположить на странице нужные блоки и подключить внешние файлы. Всего будет два блока: поле для бросания спичек и табличка с результатами в нижнем углу.

Что подключим из внешнего:

  • jQuery, чтобы проще было работать с элементами на странице;
  • CSS-файл со стилями;
  • JS-файл, в котором будет лежать наш скрипт.

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>"Эксперимент Бюффона"</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- подключаем стили -->
  <link rel="stylesheet" href="style.css">
  <!-- и jQuery -->
  <script type="text/javascript" src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
</head>
<body>
  <!-- блок с результатами -->
  <div class="bottom"> Пересечений: <span class="crossed"></span>
    <br>Число Пи: 
    <span class="answer"></span>
  </div>
  <!-- поле, куда будем бросать спички -->
  <canvas id="c"></canvas>
  <!-- подключаем скрипт -->
  <script  src="script.js"></script>
</body>
</html>
Проводим эксперимент Бюффона со спичками в браузере
Так как мы только создали блоки, но не настраивали их, на странице есть только простой текст

Добавляем стили

Для красоты сделаем так: пусть будет тёмный фон, на который мы будем бросать светлые спички. Чтобы блок с результатами не сливался с фоном, его сделаем чуть светлее и настроим отступы.

/* добавляем внешний шрифт */
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700,800);

/* цвет фона */
body {
  background: #000;
}

/* настройки блока с результатами */
.bottom {
  /* настройки текста */
  font-family: "Open Sans", sans-serif;
  font-weight: 300;
  /* устанавливаем абсолютное позиционирование и ставим блок внизу */
  position: absolute;
  bottom: 0;
  /* цвет текста */
  color: white;
  /* отступы */
  padding: 10px;
  /* цвет фона */
  background: #333;
}
Проводим эксперимент Бюффона со спичками в браузере
Стало лучше, но спичек и линий пока не видно

Рисуем поле с линиями

Чтобы скрипт выполнялся сразу при загрузке страницы, сделаем основную jQuery-функцию $(function() {}, а всё остальное напишем внутри неё.

Сразу предусмотрим изменение размеров окна — если оно станет другого размера, нужно перезапустить эксперимент, потому что пропорции линий и пересечений будут уже другими. Для этого используем свойство resize у основного окна:

// при изменении размеров браузера меняем размеры поля и перезапускаем эксперимент
$(window).on({
resize: function() {
c.width = window.innerWidth;
c.height = window.innerHeight;
run();
}
});

Теперь подготовим переменные, которые нам понадобятся в процессе эксперимента. Для доступа к полю с линиями используем jQuery — обратимся к нужному элементу и будем работать через отдельную переменную. Здесь же получим начальные размеры окна и создадим переменные, с которыми будем работать:

// получаем доступ к блоку с полем в HTML-файле
var c = $('#c')[0];
// будем рисовать на плоскости
var ctx = c.getContext('2d');
// получаем ширину и высоту окна
c.width = window.innerWidth;
c.height = window.innerHeight;
// создаём переменные, которые понадобятся для работы скрипта
var l, box, nLines, nMatches, matches, lines, crossed;
// запускаем эксперимент
run();

Для отрисовки поля с линиями сделаем отдельную функцию: она будет отвечать и за расчёт линий, и за их отрисовку. Чтобы было удобнее работать, внутри функции сделаем метод draw() — при вызове он отрисует поле заново:

// отрисовываем поле для эксперимента
function Box(w, h) {
    // устанавливаем размеры
    this.width = w * c.width;
    this.height = h * c.height;
    this.x = c.width * (1 - w) / 2;
    this.y = c.height * (1 - h) / 2;
    // здесь будут храниться линии
    lines = [];
    // метод отрисовки поля с линиями
    this.draw = function() {
        // тёмно-синие линии
        ctx.strokeStyle = '#00f';
        // ширина линии
        ctx.lineWidth = 1;
        // разбиваем поле линиями
        for (var x = box.x; x <= box.width + box.x + 1; x += l * 2) {
            // если очередная линия помещается на поле — добавляем её
            if (lines.length < nLines) lines.push(x);
            // рисуем линию на поле
            ctx.beginPath();
            ctx.moveTo(x, box.y);
            ctx.lineTo(x, box.height + box.y);
            ctx.closePath();
            ctx.stroke();
        }
    }
}

Последнее, что нам осталось здесь сделать — запустить эксперимент, чтобы на странице появилось уже готовое поле с линиями. Для этого создадим новую функцию, где пропишем:

  • количество линий и спичек;
  • пропорции поля для эксперимента;
  • расстояние между линиями;
  • стартовые значения переменных.

После этого мы можем вызывать метод draw(), который мы сделали выше, чтобы на странице появилось разлинованное поле.

// функция запуска эксперимента
function run() {
    // сколько будет линий и спичек
    nLines = 10;
    nMatches = 300;
    // на старте пока нет ни линий, ни спичек
    matches = lines = [];

    // высота и ширина поля для эксперимента
    box = new Box(0.9, 0.65);

    // расстояние между линиями
    l = box.width / nLines / 2;
    // рисуем поле
    box.draw();

    // количество пересечений, на старте — нулевое
    crossed = 0;
    
}

// основная функция
$(function() {

    // при изменении размеров браузера меняем размеры поля и перезапускаем эксперимент
    $(window).on({
        resize: function() {
            c.width = window.innerWidth;
            c.height = window.innerHeight;
            run();
        }
    });

    // отрисовываем поле для эксперимента
    function Box(w, h) {
        // устанавливаем размеры
        this.width = w * c.width;
        this.height = h * c.height;
        this.x = c.width * (1 - w) / 2;
        this.y = c.height * (1 - h) / 2;
        // здесь будут храниться линии
        lines = [];
        // метод отрисовки поля с линиями
        this.draw = function() {
            // тёмно-синие линии
            ctx.strokeStyle = '#00f';
            // ширина линии
            ctx.lineWidth = 1;
            // разбиваем поле линиями
            for (var x = box.x; x <= box.width + box.x + 1; x += l * 2) {
                // если очередная линия помещается на поле — добавляем её
                if (lines.length < nLines) lines.push(x);
                // рисуем линию на поле
                ctx.beginPath();
                ctx.moveTo(x, box.y);
                ctx.lineTo(x, box.height + box.y);
                ctx.closePath();
                ctx.stroke();
            }
        }
    }

    
    // функция запуска эксперимента
    function run() {
        // сколько будет линий и спичек
        nLines = 10;
        nMatches = 300;
        // на старте пока нет ни линий, ни спичек
        matches = lines = [];

        // высота и ширина поля для эксперимента
        box = new Box(0.9, 0.65);

        // длина спички
        l = box.width / nLines / 2;
        // рисуем поле
        box.draw();

        // количество пересечений, на старте — нулевое
        crossed = 0;
        
    }

    // получаем доступ к блоку с полем в HTML-файле
    var c = $('#c')[0];
    // будем рисовать на плоскости
    var ctx = c.getContext('2d');
    // получаем ширину и высоту окна
    c.width = window.innerWidth;
    c.height = window.innerHeight;
    // создаём переменные, которые понадобятся для работы скрипта
    var l, box, nLines, nMatches, matches, lines, crossed;
    // запускаем эксперимент
    run();
});

Проводим эксперимент Бюффона со спичками в браузере
Линии появились, значит, скрипт работает как нужно

Разбрасываем спички и считаем результат

Чтобы работать с массивом спичек, его нужно сначала заполнить пустыми элементами — сделаем для этого отдельную функцию:

// создаём много новых спичек
function createMatches(n) {
matches = [];
for (var i = 0; i < n; i++) matches.push(new Match());
}

Теперь напишем функцию, которая при вызове создаст одну виртуальную спичку:

  • случайным образом получит её координаты;
  • выберет угол поворота при броске;
  • рассчитает координаты начала и конца спички;
  • сразу проверит, есть пересечение с линией или нет;
  • нарисует эту спичку на поле.

Получается, в одной функции мы получим координаты, проверим пересечение и сразу всё нарисуем. Это удобно — нам не придётся потом перебирать несколько раз массив со спичками, чтобы сделать это поэтапно.

Код выглядит громоздко, но это из-за команд отрисовки: каждый элемент спички рисуем вручную по координатам.

// формируем спичку и сразу проверяем её на пересечение с линией
function Match() {
    // случайный угол поворота при броске
    this.angle = Math.random() * 2 * Math.PI;
    // и случайные координаты спички
    this.x = Math.random() * box.width + box.x;
    this.y = Math.random() * box.height + box.y;

    // получаем начальные и конечные координаты спички
    var x1 = this.x + l / 2 * Math.sin(this.angle);
    var x2 = this.x - l / 2 * Math.sin(this.angle);
    var y1 = this.y + l / 2 * Math.cos(this.angle);
    var y2 = this.y - l / 2 * Math.cos(this.angle);

    // перебираем все линии на поле
    for (var i = 0; i < nLines; i++) {
        // если координаты линии лежат между начальными и конечными координатами спички — считаем это как пересечение
        var check1 = x1 < lines[i] && lines[i] < x2;
        var check2 = x1 > lines[i] && lines[i] > x2;
        if (check1 || check2) crossed++;
    }
    
    // метод отрисовки спички 
    this.draw = function() {
        // рисуем тень от спички, чтобы они визуально не слипались друг с другом
        ctx.strokeStyle = '#000';
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.closePath();
        ctx.stroke();

        // рисуем спичку
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.closePath();
        ctx.stroke();

        // рисуем красную точку на вершине спички
        ctx.fillStyle = '#f00';
        ctx.beginPath();
        ctx.arc(x1, y1, 2, 0, Math.PI * 2);
        ctx.closePath();
        ctx.fill();
    }
}

Но эта функция сама по себе ничего не делает — для этого её нужно вызвать, а этого в коде у нас ещё нет. Исправим это:

// создаём и отрисовываем все спички сразу
function drawMatches(n) {
for (var i = 0; i < n; i++) matches[i].draw();
}

Последнее, что нам осталось сделать — добавить вызов этих функций в функцию run() и  там же рассчитать на основе результатов число пи:

// создаём и рисуем спички
createMatches(nMatches);
drawMatches(nMatches);
// считаем число Пи
var pi = (nMatches / crossed).toFixed(2);
// выводим результаты в нижний блок
$('.crossed').html(crossed);
$('.answer').html(pi);

Теперь всё готово: можно обновлять страницу и смотреть на результат. Если нужна точность побольше, увеличьте количество спичек:

Проводим эксперимент Бюффона со спичками в браузере

Посмотреть на эксперимент на странице проекта

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>"Эксперимент Бюффона"</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- подключаем стили -->
  <link rel="stylesheet" href="style.css">
  <!-- и jQuery -->
  <script type="text/javascript" src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
</head>
<body>
  <!-- блок с результатами -->
  <div class="bottom"> Пересечений: <span class="crossed"></span>
    <br>Число Пи: 
    <span class="answer"></span>
  </div>
  <!-- поле, куда будем бросать спички -->
  <canvas id="c"></canvas>
  <!-- подключаем скрипт -->
  <script  src="script.js"></script>
</body>
</html>

/* добавляем внешний шрифт */
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700,800);

/* цвет фона */
body {
  background: #000;
}

/* настройки блока с результатами */
.bottom {
  /* настройки текста */
  font-family: "Open Sans", sans-serif;
  font-weight: 300;
  /* устанавливаем абсолютное позиционирование и ставим блок внизу */
  position: absolute;
  bottom: 0;
  /* цвет текста */
  color: white;
  /* отступы */
  padding: 10px;
  /* цвет фона */
  background: #333;
}

// основная функция
$(function() {

    // при изменении размеров браузера меняем размеры поля и перезапускаем эксперимент
    $(window).on({
        resize: function() {
            c.width = window.innerWidth;
            c.height = window.innerHeight;
            run();
        }
    });

    // отрисовываем поле для эксперимента
    function Box(w, h) {
        // устанавливаем размеры
        this.width = w * c.width;
        this.height = h * c.height;
        this.x = c.width * (1 - w) / 2;
        this.y = c.height * (1 - h) / 2;
        // здесь будут храниться линии
        lines = [];
        // метод отрисовки поля с линиями
        this.draw = function() {
            // тёмно-синие линии
            ctx.strokeStyle = '#00f';
            // ширина линии
            ctx.lineWidth = 1;
            // разбиваем поле линиями
            for (var x = box.x; x <= box.width + box.x + 1; x += l * 2) {
                // если очередная линия помещается на поле — добавляем её
                if (lines.length < nLines) lines.push(x);
                // рисуем линию на поле
                ctx.beginPath();
                ctx.moveTo(x, box.y);
                ctx.lineTo(x, box.height + box.y);
                ctx.closePath();
                ctx.stroke();
            }
        }
    }

    // формируем спичку и сразу проверяем её на пересечение с линией
    function Match() {
        // случайный угол поворота при броске
        this.angle = Math.random() * 2 * Math.PI;
        // и случайные координаты спички
        this.x = Math.random() * box.width + box.x;
        this.y = Math.random() * box.height + box.y;

        // получаем начальные и конечные координаты спички
        var x1 = this.x + l / 2 * Math.sin(this.angle);
        var x2 = this.x - l / 2 * Math.sin(this.angle);
        var y1 = this.y + l / 2 * Math.cos(this.angle);
        var y2 = this.y - l / 2 * Math.cos(this.angle);

        // перебираем все линии на поле
        for (var i = 0; i < nLines; i++) {
            // если координаты линии лежат между начальными и конечными координатами спички — считаем это как пересечение
            var check1 = x1 < lines[i] && lines[i] < x2;
            var check2 = x1 > lines[i] && lines[i] > x2;
            if (check1 || check2) crossed++;
        }
        
        // метод отрисовки спички 
        this.draw = function() {
            // рисуем тень от спички, чтобы они визуально не слипались друг с другом
            ctx.strokeStyle = '#000';
            ctx.lineWidth = 4;
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.closePath();
            ctx.stroke();

            // рисуем спичку
            ctx.strokeStyle = '#fff';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.closePath();
            ctx.stroke();

            // рисуем красную точку на вершине спички
            ctx.fillStyle = '#f00';
            ctx.beginPath();
            ctx.arc(x1, y1, 2, 0, Math.PI * 2);
            ctx.closePath();
            ctx.fill();
        }
    }

    // создаём много новых спичек
    function createMatches(n) {
        matches = [];
        for (var i = 0; i < n; i++) matches.push(new Match());
    }

    // создаём и отрисовываем все спички сразу
    function drawMatches(n) {
        for (var i = 0; i < n; i++) matches[i].draw();
    }

    // функция запуска эксперимента
    function run() {
        // сколько будет линий и спичек
        nLines = 10;
        nMatches = 300;
        // на старте пока нет ни линий, ни спичек
        matches = lines = [];

        // высота и ширина поля для эксперимента
        box = new Box(0.9, 0.65);

        // длина спички
        l = box.width / nLines / 2;
        // рисуем поле
        box.draw();

        // количество пересечений, на старте — нулевое
        crossed = 0;
        // создаём и рисуем спички
        createMatches(nMatches);
        drawMatches(nMatches);
        // считаем число Пи
        var pi = (nMatches / crossed).toFixed(2);
        // выводим результаты в нижний блок
        $('.crossed').html(crossed);
        $('.answer').html(pi);
    }

    // получаем доступ к блоку с полем в HTML-файле
    var c = $('#c')[0];
    // будем рисовать на плоскости
    var ctx = c.getContext('2d');
    // получаем ширину и высоту окна
    c.width = window.innerWidth;
    c.height = window.innerHeight;
    // создаём переменные, которые понадобятся для работы скрипта
    var l, box, nLines, nMatches, matches, lines, crossed;
    // запускаем эксперимент
    run();

});

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Аня Соколова

Фронтенд-разработка — востребованная профессия
На новом курсе «Практикума» о фронтенде вас обучат самым востребованным технологиям: JS и TypeScript, Flexbox и Grid, React, Git, Bash и др. Это то, что нужно работодателям сегодня. Старт — бесплатно.
Попробовать бесплатно
Фронтенд-разработка — востребованная профессия Фронтенд-разработка — востребованная профессия Фронтенд-разработка — востребованная профессия Фронтенд-разработка — востребованная профессия
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
easy