Недавно мы рассказали про старый метод нахождения числа пи, который придумал в 18-м веке математик Карл Бюффон. Смысл в том, чтобы найти число пи, используя мировой хаос и энтропию. Как это сделать:
- Берём ткань и рисуем на ней параллельные линии на расстоянии X друг от друга.
- Берём иглу длиной L, но так, чтобы её длина была меньше или равна расстоянию между линиями, то есть L <= X.
- Случайным образом бросаем иглу на ткань и смотрим, попала ли игла на одну из линий или нет.
- Считаем, сколько раз мы бросили иглу и сколько раз она попала на одну из линий.
- Отношение этих двух чисел даст нам число, похожее на пи. Чем больше бросков — тем результат будет ближе к пи.
В статье мы давали код на 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();
});