Треугольник Серпинскогофрактал, математическое описание которого опубликовал польский математик Вацлав Серпинский в 1915 году.

В этом посте мы напишем рекурсивный алгоритм отрисовки данного известного фрактала в canvas с помощью JS

Какой кистью будем рисовать?

Основой всего будет HTML, в котором будет находиться дочерний элемент в виде canvas. Размером он будет в 1000x1000 пикселей, хотя также будет возможность увеличивать данное значение вплоть до 8к

В роли художника у нас выступает JavaScript. В нем мы напишем рекурсивный алгоритм, основывающийся на Игре Хаоса. Он будет размером всего-лишь чуть больше 20 строк.

Стили будут присутствовать, но разве что для красоты. Можно обойтись и без них.

Логика отрисовки:

У нас имеется белый холст в котором нужно обозначить три координаты — ABC (вершины треугольника)

Задаем начальную вершину. Допустим это будет C

Выбираем случайную вершину. Возьмем A

Проводим между ними линию и ставим точку по центру

Вновь выбираем случайную вершину. Пусть будет B

Проводим линию с предыдущей точки

Ставим новую точку по центру

Повторяем эти действия несколько сотен раз и мы получим первые отрисовки фрактала

Приступим к коду

Прописываем базовый HTML код, подключаем к нему .css и .js файл.

Задаем серый фон для body:

body {
    background-color: rgb(40, 40, 40); /* Чтоб красиво было :D */
}

Внутри body создаем элемент canvas, даем ему размеры 1000x1000 пикселей и белый фон:

<canvas width="1000" height="1000"></canvas>
canvas {
    background-color: white;
}

Чтоб мы знали нынешнюю итерацию и количество точек на экране, также пропишем следующее:

<span>Точек на экране: 0</span>
span {
    position: absolute; /* Прижимаем текст к левому-нижнему углу экрана */
    right: 0;
    bottom: 0;
    font-size: 30px; /* Задаем размер и цвет текста */
    color: white;
}

Пишем JS скрипт:

Для начала нужно получить сам canvas и его содержимое:

let canvas = document.querySelector('canvas') // Получаем canvas в виде DOM-элемента
let ctx = canvas.getContext('2d'); // Получаем его контекст (содержимое)

Обозначим вершины в виде матрицы (двумерного массива) с координатами:

Дабы не мучиться с координатами при изменении размера холста, лучше будем сами получать их с помощью .getBoundingClientRect()

let cornerCords = [ // Координаты в виде X = ширина, Y = высота
    [canvas.getBoundingClientRect().width / 2, 0], // Получаем ширину холста и делим ее на 2, тем самым находим центр (вершина А)
    [0, canvas.getBoundingClientRect().height], // Получаем только высоту холста и находим вершину B
    [canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height] // Получаем и высоту, и ширину холста, найдя вершину C
]

Если все переменные заменить на цифры, а холст будет размером в 1000х1000, то получитcя такой результат:

let cornerCords = [
    [500, 0], 
    [0, 1000], 
    [1000, 1000]
]

Теперь мы можем изменять размер холста как захотим, а треугольник несмотря на это, будет правильно отображаться

Переходим к рекурсии

Прописываем обычную функцию с именем RecursionDrawing:

function RecursionDrawing() {}

Как мы помним, перед началом нужно определиться с начальной вершиной, пусть будет A

Чтобы дать функции понять это, будем передавать параметр в виде массива, который достаем из матрицы координат:

function RecursionDrawing(previousDotCords) {
  // Функция теперь принимает координаты и обозначает их в виде переменной previousDotCords
} 

RecursionDrawing(cornerCords[0])

Внутрь функции прописываем setTimeout() (задержка), дабы не ловить ошибку:

function RecursionDrawing() {
  setTimeout(function () {}, 0) // Задержка 0мс, этого хватит
}

RecursionDrawing(cornerCords[0])

После обозначения начальной точки, нужно выбрать случайную вершину

Воспользуемся некоторыми манипуляциями со встроенной библиотекой Math:

function RecursionDrawing() {
  setTimeout(function () {
    let randomCorner = Math.floor(Math.random() * cornerCords.length)
    // Math.random() дает случайное дробное число (float) от 0 до 1
    // Умножая это значение на длину массива мы получаем случайное дробное число
    // Округляем с помощью Math.floor() и получаем случайное полное число (int)
    // Тем самым получаем случайное число (в нашем случае от 0 до 2, A-B-C)
    
  }, 0)
}

RecursionDrawing(cornerCords[0])

Начинаем работу с canvas:

function RecursionDrawing(previousDotCords) {
    setTimeout(function () {
        let randomCorner = Math.floor(Math.random() * cornerCords.length)

        ctx.beginPath() // Открываем путь 
        ctx.fillStyle = "black" // Цвет заполнения, в нашем случае черный (также поддерживает hsl, rgb, hex)
        
        ctx.closePath() // Закрываем путь
      }, 0)
  }

Теперь рисуем саму точку:

X = (X-случайной вершины + X-предыдущей точки) / 2

Y = (Y-случайной вершины + Y-предыдущей точки) / 2

Также укажем размеры точки: (1, 1)

// В JS будет выглядеть так
(cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2, 1, 1)

Вставляем это в основной код:

function RecursionDrawing(previousDotCords) {
    setTimeout(function () {
        let randomCorner = Math.floor(Math.random() * cornerCords.length)

        ctx.beginPath()
        ctx.fillStyle = "black"
      
        // ctx.fillRect() — Заполняет указанную область принимая: (x, y, width, height)
        ctx.fillRect((cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2, 1, 1)
      
        ctx.closePath()
      }, 0)
  }

Запускаем рекурсию, указывая в конце кода саму функцию и передавая координаты новой точки:

function RecursionDrawing(previousDotCords) {
    setTimeout(function () {
        let randomCorner = Math.floor(Math.random() * cornerCords.length)

        ctx.beginPath()
        ctx.fillStyle = "black"
        ctx.fillRect((cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2, 1, 1)
        ctx.closePath()

        // Передаем тоже самое, что и в ctx.fillRect(), но без width и height (1, 1)
        RecursionDrawing([(cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2])
      }, 0)
  }

Осталось лишь посчитать кол-во точек

Вне функции объявляем переменную iteration:

let iteration = 0

Теперь до вызова рекурсии мы должны прописать следующее:

let iteration = 0
function RecursionDrawing(previousDotCords) {
    setTimeout(function () {
        let randomCorner = Math.floor(Math.random() * cornerCords.length)

        ctx.beginPath()
        ctx.fillStyle = "black"
        ctx.fillRect((cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2, 1, 1)
        ctx.closePath()

        iteration++ // К iteration прибавляем +1
        // Получаем наш span и заменяем его содержимое
        document.querySelector('span').innerText = `Точек на экране: ${iteration}` 
      
        RecursionDrawing([(cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2])
      }, 0)
  }

Чтож, осталось лишь собрать все вместе

Весь код:

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <canvas width="1000" height="1000"></canvas>
    <span>Точек на экране: 0</span>

    <script src="script.js"></script>
</body>
</html>

CSS:

body {
    background-color: rgb(40, 40, 40);
}
canvas {
    background-color: white;
}
span {
    position: absolute; 
    right: 0;
    bottom: 0;
    font-size: 30px;
    color: rgb(255, 255, 255);    
}

JS:

let canvas = document.querySelector('canvas')
let ctx = canvas.getContext('2d');

let cornerCords = [
    [canvas.getBoundingClientRect().width / 2, 0], 
    [0, canvas.getBoundingClientRect().height], 
    [canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height]
]

let iteration = 0
function RecursionDrawing(previousDotCords) {
    setTimeout(function () {
        let randomCorner = Math.floor(Math.random() * cornerCords.length)

        ctx.beginPath()
        ctx.fillStyle = "black"
        ctx.fillRect((cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2, 1, 1)
        ctx.closePath()

        iteration++
        document.querySelector('span').innerText = `Точек на экране: ${iteration}`

        RecursionDrawing([(cornerCords[randomCorner][0] + previousDotCords[0]) / 2, (cornerCords[randomCorner][1] + previousDotCords[1]) / 2])
    }, 0)
}

RecursionDrawing(cornerCords[0])

Спасибо за прочтение данного поста.

CodePen: https://codepen.io/Saman2789/pen/vYQEGNV

Буду благодарен, если посетите мой телеграм канал: https://t.me/blg_projects

Комментарии (3)


  1. longclaps
    08.06.2023 10:20
    +4

    Серпинский просил передать, что A, B и C - вершины треугольника ABC, а вовсе не углы, как Вы упорно повторяете [/зануда офф].

    А от себя поинтересуюсь: при чем тут векторная графика? И в чем, на Ваш взгляд, ценность Вашего экзерсиса?


    1. samanwirst Автор
      08.06.2023 10:20

      Исправил.


  1. storoj
    08.06.2023 10:20

    Recourse ????