Генерация Фракталов методом хаоса, UI на ScalaFX

Ремарка

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

Метод Хауса

Метод хаоса, или «игра хаоса», является увлекательным и визуально впечатляющим подходом к созданию фрактальных структур. Этот метод основывается на случайном выборе точек внутри определенной фигуры и последовательном применении простых математических правил для генерации новых точек. В результате многократного применения этих правил могут формируются сложные и красивые узоры. Одним из наиболее известных примеров использования метода хаоса является создание фрактала Серпинского треугольника, что так же упомянуто в статье на википедии. Привлекательность метода хаоса заключается в его простоте и способности генерировать сложные структуры из простых алгоритмов, что делает его мощным инструментом для исследования фрактальной геометрии и визуализации хаоса.

что хочет создать автор

Целью проекта является создание оконного приложения на фреймворке ScalaFX, которое будет состоять из области для рисования фигур методом хаоса, панели настроек и кнопки, запускающей отрисовку фигуры. Приложение также должно иметь привлекательный внешний вид. Область для рисования представляет собой пространство, где после нажатия кнопки будет появляться фигура.

Панель настроек включает в себя две основные составляющие:

  1. Настройка веса точки, который определяет, насколько близко к предыдущей точке будет ставиться следующая. Также можно будет задать цвет точек и итоговой фигуры, радиус точек и их количество.

  2. Настройка аттракторов. Их координаты должны задаваться формульно отдельно для координат x и y. Также необходимо указать вероятностный вес аттрактора, от которого зависит выбор аттрактора на каждой итерации, и вес аттрактора, определяющий, насколько близко к нему будет располагаться точка. Дополнительно можно задать радиус, цвет и количество аттракторов (первые два параметра используются исключительно для визуализации).

Разметка и цветовая схема

недолго поразмыслив пришла к такой структуре:

8111941ae75d77774e25f21223ebd522.png

Цвета здесь выбраны произвольно, и их нужно подобрать согласно цветовой схеме. Так как зона отрисовки фигур останется белой, будем отталкиваться от белого цвета и использовать следующим сайтом

подбираю 6 следующих оттенков:

  • #f5f9ea

  • #c1d0a4

  • #46727d

  • #003248

  • #001933

  • #000000

переделаю схему с использованием следующих оттенков

2786f53cc7dd934a3129324aa937c18b.png

вроде уже симпотичнее, в общем пойдет

создание проекта и предворительная настройка sbt

добавим в корневую папуку файл build.sbt и определим сначала самый базис:

name := "ScalaFX_Fractal" //имя сего проекта
version := "1.0.0" // версия проекта


scalaVersion := "2.13.12" // версия скалы

// подключаем необходимые опции компилятора
scalacOptions ++= Seq("-unchecked", "-deprecation", "-Xcheckinit", "-encoding", "utf8", "-Ymacro-annotations")
  • -unchecked: Включает предупреждения о небезопасных или непроверенных операциях.

  • -deprecation: Включает предупреждения о том, что используются устаревшие элементы языка или библиотеки.

  • -Xcheckinit: Включает дополнительные проверки инициализации объектов.

  • -encoding utf8: Устанавливает кодировку исходных файлов на UTF-8. Это гарантирует правильное чтение и запись файлов, особенно если они содержат символы, не входящие в стандартный набор ASCII.

  • -Ymacro-annotations: Включает поддержку аннотаций макросов, которые позволяют генерировать код во время компиляции с использованием макросов. в данном проекте это важно, поскольку без него не будет ScalaFX, а конкретно та часть что парсит FXML

добавим зависемости для sclaaFX

lazy val scalaFxVersion = "16.0.0-R24"
lazy val scalaFxCoreVersion = "0.5"

lazy val scalaFxDependencies = Seq(
	"org.scalafx" %% "scalafx" % scalaFxVersion, //добовляет scalaFX в проект
	"org.scalafx" %% "scalafxml-core-sfx8" % scalaFxCoreVersion 
    /*
      дополнительное расщирение ScalaFX для связки его с FXML, 
      собственно для него и необходима опция -Ymacro-annotations,
      без которой не будет работать
    */
)

так как ScalaFx — это обвесок для JavaFx то и ScalaFx без него работать не будет, а следовательно добавляем JavaFx в проект добовляя строки в build.sbt

// Определяем зависимости для JavaFX в зависимости от операционной системы
lazy val javaFxDependencies = {
  // Определяем название операционной системы, чтобы выбрать соответствующий классификатор
  lazy val osName = System.getProperty("os.name") match {
    case n if n.startsWith("Linux") => "linux" // Если ОС Linux, используем классификатор "linux"
    case n if n.startsWith("Mac") => "mac"    // Если ОС macOS, используем классификатор "mac"
    case n if n.startsWith("Windows") => "win" // Если ОС Windows, используем классификатор "win"
    case _ => throw new Exception("Unknown platform!") // Если платформа не распознана, выбрасываем исключение
  }

  // Создаем последовательность зависимостей для JavaFX, используя полученный классификатор
  Seq("base", "controls", "fxml", "graphics", "media", "swing", "web") // Перечисляем модули JavaFX
    .map(m => "org.openjfx" % s"javafx-$m" % "16" classifier osName) // Формируем зависимость для каждого модуля с нужным классификатором
}

данные строки были взяты с сайта и не то чтобы прям вникала, думаю для легковестности стоило бы что то из этого вырезать, например web, но работаит и так сойдет.

ну и остаеться только добавить зависемости

libraryDependencies ++= scalaFxDependencies ++ javaFxDependencies

Делаем разметку FXML и подключаем отображение на окно

в ScalaFx для отображения первого окна нужно мейн классом от JFXApp3 и переписать метод start

// Главный объект приложения, наследуемся от JFXApp3
object Main extends JFXApp3 { 

  // Переопределяем метод start, который будет вызван при запуске приложения
  override def start(): Unit = { 
    // Устанавливаем первичную сцену (stage) приложения
    stage = new JFXApp3.PrimaryStage { 
      title = "ScalaFX" // Заголовок окна приложения
      // Устанавливаем сцену с корневым элементом GridPane и размерами 600x450 пикселей
      scene = new Scene(new GridPane(), 600, 450) //GridPane пока поставлен просто чтоб отобразилось хоть что то
    }
  }
}

и уже можно запускать приложие, я пока жму зеленый треугольник в Idea

dd62a58fe887b9618988906cb1060463.png

первое окно запущено .fxml файл где будет описан интерфейс
созаю файл main_window.fxml в папке resources со следующим содержанием








        


данный FAXML файл определяет такой же пустой GridPane что до этого делали через new GridPane с той лишь разницей что в первом был элемент из пространства scalafx, а не javafx, но дальше будет происходит автоматическая конвертация, об этом чуть позже

далее из файла нужно собрать обьект сцены, я решила это вынести из Mаin обьекта и вынести в отдельный trait MainWindowConfigure

// Трейт (trait), который содержит конфигурацию основного окна приложения
trait MainWindowConfigure {

  // Ленивая инициализация главного окна (PrimaryStage)
  lazy val mainWindow: JFXApp3.PrimaryStage = new JFXApp3.PrimaryStage {

    // Загружаем ресурс FXML-файла для главного окна
    val resource: URL = getClass.getResource("/FXMLs/main_window.fxml")
    // Проверяем, удалось ли загрузить ресурс
    if (resource == null) {
      // Если ресурс не найден, выбрасываем исключение
      throw new IOException(f"Cannot load resource: ${resource.toURI}")
    }

    // Создаем корневой элемент сцены (root) из FXML-файла, используя FXMLView и NoDependencyResolver
    val root: Parent = FXMLView(resource, NoDependencyResolver)

    // Устанавливаем заголовок окна
    title = "ScalaFX"
    // Устанавливаем сцену для главного окна с корневым элементом и размерами 600x450 пикселей
    scene = new Scene(root, 600, 450)
  }
}

и теперь в main обьекте подключаем сцену из трейта и теперь мейн обьект выглядит так, более к нему не вернемся.

package com.scalafx.fractal

import scalafx.application.JFXApp3

object Main extends JFXApp3 with MainWindowConfigure {

  override def start(): Unit = {
    stage = mainWindow
  }

}

и теперь при запуске открывается тоже самое окно что и раньше, только теперь оно собираеться из .fxml файла

делаем разметку FAXML

на данном этапе задача воспроезвести следующий макет

угольных интерфейсов

угольных интерфейсов

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

начнем с разметки сетки

 
    
    
    
    
  
    
    
    
    


 
    
    
    
    
  
    
    
    
    

    
    
    
    

все элементы которые должны размещаться в GridPane и других layout-ов распологаються в children

    

        
      
        
  1. GridPane.columnIndex: Устанавливает индекс столбца, в котором будет размещен элемент.

  2. GridPane.rowIndex: Устанавливает индекс строки, в которой будет размещен элемент.

  3. GridPane.columnSpan: Определяет, сколько столбцов будет занимать элемент.

  4. GridPane.rowSpan: Определяет, сколько строк будет занимать элемент.

  5. GridPane.vgrow: Определяет вертикальный рост элемента. ALWAYS позволяет элементу расти по вертикали, занимая доступное пространство.

  6. GridPane.hgrow: Определяет горизонтальный рост элемента. ALWAYS позволяет элементу расти по горизонтали, занимая доступное пространство.

  7. style: Позволяет установить CSS-стили для элемента, например, -fx-background-color для установки цвета фона.

  8. maxWidth и maxHeight: Устанавливают максимальные размеры элемента.

  9. prefWidth и prefHeight: Устанавливают предпочтительные размеры элемента.

Эти настройки позволяют гибко управлять расположением и размером элементов в GridPane, обеспечивая адаптивное и отзывчивое оформление интерфейса. и таким образом достигаю нужного эффекта. и вот что получаю следующие окно

f2f1b7f3b15bc634a66b3ee83a05e344.png

таким образом разметкак готова

наполняем FAML функциональными элементами

на данном этапе задача заменить все Label функциональными эментами.
первое что сделаю — это кнопка запуска
заменим соответствующий Label следующей структурой, которая определит кнопку

        

на данном этапе имеем следующие:

3f47c3d5598a6029fc27785848353278.png

сделаем панель настройки точек фигуры, добавив следующию структуру

        
            
            
            
            
        

на данный момент имеем следующие:

5317a80098279c8bd593c1f5250850d9.png

сделаем настроичную панель заменив Label на следующие

        
            

                
                    
                        

и имею следующие:

b7fe84d38a54e71f61697da69cb94238.png

и наконец осталась только отрисовочная панель

        
            
        

и того имеем:

c35c9488666c858a7f01f1d9dcbaee40.png

Стилизуем через .css

для стилизации элементов в ScalaFx можно использовать отдельный .css файл
создадим в resources файл styles.css и подключаем его к окну следующей строчко в трейте:
root.stylesheets += getClass.getResource("/styles.css").toExternalForm

создадим стиль для кнопки следующим кодом

.start-button {
  -fx-background-color: #003248; /* Цвет фона */
  -fx-text-fill: #f5f9ea; /* Цвет текста */
  -fx-border-color: #001933; /* Цвет бордюра */
  -fx-border-width: 2px; /* Ширина бордюра */
  -fx-font-size: 16px; /* Размер шрифта */
  -fx-padding: 10px; /* Отступы внутри кнопки */
  -fx-alignment: center; /* Выравнивает содержимое элемента по центру */
}

.start-button:hover {
  -fx-background-color: #001933; /* Цвет фона при наведении */
}

и чтобы применить этот стиль к нопке надо добавить к styleClass="start-button" к элементу Button

для остальных элементов делаем аналогичным образом и на .css более затрагиваться в статье не будет, но в нем есть стили для всех элементов и тгго имею следующий интерфейс:

7122ff52192a32f99ba9515ebe354542.png

Ну, вроде вышло симпотично

создаем контроллер для окна

что бы назначить контроллер нужно с начала создать класс и навесить на него анотацию @sfxml

@sfxml
class MainWindowController() {} //класс пока пустой 

и чтобы на окно назначить этот контроллер нужно добавить строку в FAXML

чтобы в контроллере можно было использовать элементы из сцены, этим элементам надо добавить fx:id вот на примере настроичной панели

для необходимых компонентов добовляю этот параметр, тут останавливаться не буду
и в конструктор по этому id добовляем элемент с соответсвующим типом

@sfxml
class MainWindowController(private val settingsVbox: VBox,
    					   private val drawCNS: Pane,
						   private val drawSP: ScrollPane,
						   private val divisionCoefficientTF: TextField,
						   private val radiusTF: TextField,
                           private val hexTF: TextField,
						   private val iterTF: TextField) {



}

тут есть момент что в .fxml мы описываем элементы из JavaFx, однако в конструктуре получаем аналогичные обьекты из ScalaFx, то есть анотация @sfxml производит неявную конвертацию что очень удобно.

делаем темплейт для панели настройки

добавим еще один .fxml файл attractor_settings_panel.fxml со следующим содержанием









    
        

это понадобиться далее, чтобы так же их .fxml создавать элементы динамически и не хардкодить все это

Функционал на кнопки +/-

чтобы назначит функционал на кнопку в .fxml нужно повесить параметр onAction
вот на примере Button

далее нужно только создать в контролле метод с тем же именем

@sfxml
class MainWindowController(private val settingsVbox: VBox,
    					   private val drawCNS: Pane,
						   private val drawSP: ScrollPane,
						   private val divisionCoefficientTF: TextField,
						   private val radiusTF: TextField,
                           private val hexTF: TextField,
						   private val iterTF: TextField) {

    // Загружаем ресурс FXML файла для панели настроек аттрактора
	lazy val resource: URL = getClass.getResource("/FXMLs/attractor_settings_panel.fxml")
	if (resource == null) {
		throw new IOException(f"Cannot load resource: ${resource.toURI}")
	}

    // Метод для обработки действия добавления новой панели настроек аттрактора
    def handleAddAction(): Unit = {
        // Создаем панель настроек из FXML ресурса
        val settingPanel = FXMLView(resource, NoDependencyResolver)
        // Добавляем новую панель перед последним элементом в settingsVbox (на последней распологаются кнопки +/-)
        settingsVbox.children.add(settingsVbox.children.size - 1, settingPanel)
    }

    // Метод для обработки действия удаления панели настроек аттрактора
    def handleremoveAction(): Unit = {
        // Удаляем предпоследнюю панель, если их больше одной (на последней распологаются кнопки +/-)
        if (settingsVbox.children.size > 1)
            settingsVbox.children.remove(settingsVbox.children.size - 2)
    }

}

и того мы можен добавлять новые панели настроичные для точек атрактора:

пример с двумя панелями

пример с двумя панелями

вроде как весьма симпотично.

рендеринг

последнее что осталось сделать — это функционал главной кнопки
но сначала определим 2 вспомогательных кейс класса, которые будут хранить информацию с панелей настроек аттракторов

package com.scalafx.fractal.model.DTO

//думаю тут особых пояснений не надо
case class AttractorSettingsPanelDTO(formulaX: String,
									 formulaY: String,
									 probabilityWeight: Int,
									 divisionCoefficient: Int,
									 radius: Int,
									 hex: String,
									 iterCount: Int)
package com.scalafx.fractal.model.DTO

//думаю тут пояснений не надо
case class PointSettingsDTO(divisionCoefficient: Int,
							radius: Int,
							hex: String,
							iterCount: Int)

А так же понадобятся классы для работы с арефметическими вырожениями, которые я реализовывала в одной из своих статей (в связи с чем не буду останавливаться на данном функционале) я буквально просто копию/вставляю все классы в папку model.AST

3892ab5e729209bc2bd62d67a0b0c675.png

добовляю только допом StandartASTComponents.scala где соберу свои стандартные сущьности арефметические со следующим содержанием:

package com.scalafx.fractal.model.AST

import scala.util.Random

object StandartASTComponents {

	lazy val plus: UDO = UDO("+", 1, Option(0d), (left, right) => left + right)
	lazy val minus: UDO = UDO("-", 1, Option(0d), (left, right) => left - right)
	lazy val multiply: UDO = UDO("*", 2, Option(1d), (left, right) => left * right)
	lazy val division: UDO = UDO("/", 2, None, (left, right) => left / right)
	lazy val pow: UDO = UDO("^", 3, None, (left, right) => Math.pow(left, right))

	lazy val pi: UDC = UDC("pi", Math.PI)
	lazy val e: UDC = UDC("e", Math.E)

	val avg: UDF = UDF("avg", params => params.sum / params.size)
	val abs: UDF = UDF("abs", params => params.head.abs)
	val cos: UDF = UDF("cos", params => Math.cos(params.head))
	val sin: UDF = UDF("sin", params => Math.sin(params.head))
	val ln: UDF = UDF("ln", params => Math.log(params.head))
	val max: UDF = UDF("max", params => params.max)
	val min: UDF = UDF("min", params => params.min)
	val ran: UDF = UDF("ran", params => Random.nextDouble())

	lazy val standartUDOs: Set[UDO] = Set(plus, minus, multiply, division, pow)
	lazy val standartUDCs: Set[UDC] = Set(pi, e)
	lazy val standartUDFs: Set[UDF] = Set(avg, abs, cos, sin, ln, max, min, ran)

}

добавлю так же 2 вспомогательных метода в контроллер:

//небольшой метод, нужен на случай если пользователь ввел некоректно цветовую схему
//и если это так то возращает просто черный цвет и так же если поле было пустым
private def validHexColor(color: String): String= {
		Try {

			val hexColorPattern = "^#([A-Fa-f0-9]{6})$".r
			if (hexColorPattern.matches(color))
				color
			else
				"#000000"

		}.getOrElse("#000000")
}

//метод который мне парсит настроичные панели в мой DTO caseClass
//подробнее тут останавливаться не хочу
private def validAttractorDTO(node: javafx.scene.Node): Try[AttractorSettingsPanelDTO] = {

		Try {

			AttractorSettingsPanelDTO(
				formulaX = node.lookup("#formulaTFX").asInstanceOf[javafx.scene.control.TextField].getText,
				formulaY = node.lookup("#formulaTFY").asInstanceOf[javafx.scene.control.TextField].getText,
				probabilityWeight = Try(node.lookup("#probabilityWeightTF").asInstanceOf[javafx.scene.control.TextField].getText.toInt).getOrElse(1),
				divisionCoefficient = Try(node.lookup("#divisionCoefficientTF").asInstanceOf[javafx.scene.control.TextField].getText.toInt).getOrElse(1),
				radius = Try(node.lookup("#radiusTF").asInstanceOf[javafx.scene.control.TextField].getText.toInt).getOrElse(1),
				hex = validHexColor(node.lookup("#hexTF").asInstanceOf[javafx.scene.control.TextField].getText),
				iterCount = Try(node.lookup("#iterTF").asInstanceOf[javafx.scene.control.TextField].getText.toInt).getOrElse(1)
			)

		}

}

реализуем функционал кнопки главвной

к кнопке добовляю парметр onAction="#rendering" и соответсвующий метод в контроллер: def rendering(): Unit = { } и остается только его реализовать и функционал программы готов.

def rendering(): Unit = {
  
        // первое что делаю отчищаю панель для отрисовки фигур
        drawCNS.children.clear()
  
        //вычисляю координаты центра 
		val centerX: Double = drawSP.getWidth / 2
		val centerY: Double = drawSP.getHeight / 2

        //ну и считываю настройки с панели настроект точек фигуры 
        //(понадобиться когда буду отрисовывать фигуру))
		val pointSettingsDTO = PointSettingsDTO(
			divisionCoefficient = Try( divisionCoefficientTF.getText.toInt ).getOrElse(1),
			radius = Try( radiusTF.getText.toInt ).getOrElse(1),
			hex = validHexColor(hexTF.getText),
			iterCount = Try( iterTF.getText.toInt ).getOrElse(10000)
		)

      

  }

следующий шаг это расчитать все точки атрактры, ну то есмь их координаты и к ним сразу настройку добавить

val attractorPoints = settingsVbox //обращаемся к панели настроек
  .children // берем ее "детей"
  .init // убераю последнего "ребенка" (последний это +/- кнопочки)
  .map(validAttractorDTO) // преобразую через вспомогательный метод все в DTO с параметрами с панелей 
  .filter(_.isSuccess) // оставляю только те где все корректно спарсилось
  .map(_.get) // получаю сами DTO
  .filter(_.formulaY.nonEmpty) // отсеиваю те где пустой параметр formulaY
  .filter(_.formulaX.nonEmpty) // отсеиваю те где пустой параметр formulaX
  .flatMap { attractor => //и таки работаю с информацией с панели
    val coordinates = (0 until attractor.iterCount) //создаю с 0 по количеству интераций список
      .map { i => //действие на итерацию 
        Try {
          //тут к стандартным сущьностям добовляю только 2 константы, с итерацие i и c количеством итераций ic
          implicit val udcs: Set[UDC] = StandartASTComponents.standartUDCs + UDC("i", i) + UDC("ic", attractor.iterCount)
          implicit val udfs: Set[UDF] = StandartASTComponents.standartUDFs
          implicit val udos: Set[UDO] = StandartASTComponents.standartUDOs

          val x = attractor.formulaX.parseAST().calc() // вычисляю координату x
          val y = attractor.formulaY.parseAST().calc() // вычисляю координату y

          ((x, y), attractor) // сама точка со своими настройками
        }
      }

    //возращаю ничего если хоть на одной из итераций посчиталось некоректно
    if (!coordinates.exists(_.isFailure)) {
      coordinates.map(_.get)
    } else {
      List.empty
    }
  }

теперь нарисую эти точки атракторы

attractorPoints
  .foreach { //беру каждую расчитаную точку и рисую по координатам их положение на фигуре которую рисую
  case ((x, y), attractor) =>
    //единственное что делаю это паралеьный перенос к центру
    val circeCenterX = centerX + x
    val circeCenterY = centerY + y 
    val circeColor = Color.web(attractor.hex)

    val circle = new Circle {
      centerX = circeCenterX
      centerY = circeCenterY
      radius = attractor.radius
      fill = circeColor
    }

    drawCNS.children.add(circle)
}

расчитываю точки фигуры итоговой

// Рассчитываем суммарный вес аттракторов
val totalAttractorWeight = attractorPoints.map(_._2.probabilityWeight).sum

// Генерация точек фигуры
val fractalPoints: List[(Double, Double)] = Try {
  // Итерируемся по количеству итераций, заданных в pointSettingsDTO
  (0 until pointSettingsDTO.iterCount).foldLeft(List.empty[(Double, Double)]) {
    // Инициализация списка точек случайной точкой в центре области рисования
    case (Nil, _) =>
      List((Random.nextDouble() * centerX * 2, Random.nextDouble() * centerY * 2))

    // Добавляем новые точки на основе предыдущих
    case (prePoints, _) =>
      val lastPoint = prePoints.last

      // Выбираем случайный аттрактор на основе его веса
      val randomValue = Random.nextDouble() * totalAttractorWeight
      val randomAttractPoint: ((Double, Double), AttractorSettingsPanelDTO) = attractorPoints
        .foldLeft((0.0, Option.empty[((Double, Double), AttractorSettingsPanelDTO)])) {
          case ((cumulativeWeight, selectedPoint), point) =>
            val newCumulativeWeight = cumulativeWeight + point._2.probabilityWeight
            if (randomValue <= newCumulativeWeight && selectedPoint.isEmpty) {
              (newCumulativeWeight, Some(point))
            } else {
              (newCumulativeWeight, selectedPoint)
            }
        }
        ._2
        .get

      // Рассчитываем новую точку на основе предыдущей и выбранного аттрактора
      val totalCoefficient = pointSettingsDTO.divisionCoefficient + randomAttractPoint._2.divisionCoefficient
      val newPoint = (
        (lastPoint._1 * pointSettingsDTO.divisionCoefficient + randomAttractPoint._1._1 * randomAttractPoint._2.divisionCoefficient) / totalCoefficient,
        (lastPoint._2 * pointSettingsDTO.divisionCoefficient + randomAttractPoint._1._2 * randomAttractPoint._2.divisionCoefficient) / totalCoefficient
      )

      prePoints :+ newPoint
  }
}.getOrElse(List.empty) //возращаю пустой список если хоть где то произошла ошибка

и наконец рисую сами точки фигуры

// Итерация по всем точкам фрактала и их отрисовка
fractalPoints.foreach {
  case (x, y) =>

    // Вычисление центра окружности для каждой точки фрактала
    val circleCenterX = centerX + x
    val circleCenterY = centerY + y
    val circleColor = Color.web(pointSettingsDTO.hex)

    // Создание новой окружности с заданными параметрами
    val circle = new Circle {
      centerX = circleCenterX
      centerY = circleCenterY
      radius = pointSettingsDTO.radius
      fill = circleColor
    }

    // Добавление окружности на панель для рисования
    drawCNS.children.add(circle)
}

собственно приложение готово

Рисуем фракталы

вот я рисую треугольник серпинского

вот я рисую треугольник серпинского

другой фрактал у него вроде тоже есть название

другой фрактал у него вроде тоже есть название

и ещё

и ещё

бубновый фрактал

бубновый фрактал

d337be8371ef50f9e14a8d85f5690727.pngпоследний из серии этой с ic = 6, тут прикольно что в центре еще фрактал снежинка образуется из пустоты

последний из серии этой с ic = 6, тут прикольно что в центре еще фрактал снежинка образуется из пустоты

фрактал из превью к статье, обращу внимание что это не треугольник серпинского

фрактал из превью к статье, обращу внимание что это не треугольник серпинского

фрактал

фрактал «космический корабль» (авторское название)

ковер серпинского

ковер серпинского

снежинка

снежинка

последний

последний

снова лезем в sbt

так как я хочу чтобы условно любой мог запустить мое приложение на условный двойной щелчок мыши, то нужно настроить сборку .jar файла
добовляем в plugins.sbt следующию строку, для плагина assembly который поможет собирать джарник

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")

В build.sbt добовляем следующие строки:

// Указание главного класса для сборки в JAR файл
mainClass in assembly := Some("com.scalafx.fractal.Main")

// Стратегия слияния для SBT Assembly при сборке JAR файла
assemblyMergeStrategy in assembly := {
  // Игнорировать файлы с именем module-info.class
  case x if x.endsWith("module-info.class") => MergeStrategy.discard
  // Фильтрация строк в файлах META-INF/services/ для избежания конфликтов
  case x if x.startsWith("META-INF/services/") => MergeStrategy.filterDistinctLines
  // Игнорировать все файлы внутри директории META-INF/
  case x if x.startsWith("META-INF/") => MergeStrategy.discard
  // Игнорировать файлы с расширением .html
  case x if x.endsWith(".html") => MergeStrategy.discard
  // Для всех остальных файлов использовать первую версию файла
  case x => MergeStrategy.first
}

// Задание имени для результирующего JAR файла
assemblyJarName in assembly := f"${name.value}.jar"

и теперь для сборки .jar файла достаточно вызвать команду sbt assembly

P.S. проект можно глянулянуть на github
А так, пишите/жмите что думаете.

© Habrahabr.ru