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

от автора

Ремарка

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

Метод Хауса

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

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

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

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

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

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

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

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

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

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

  • #f5f9ea

  • #c1d0a4

  • #46727d

  • #003248

  • #001933

  • #000000

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

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

создание проекта и предворительная настройка 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

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

<?xml version="1.0" encoding="UTF-8"?> <!-- Определение версии XML и кодировки документа -->  <?import javafx.scene.layout.GridPane?> <!-- Импортируем класс GridPane из JavaFX, чтобы использовать его в этом файле -->  <GridPane         xmlns:fx="http://javafx.com/fxml"/>         <!-- Определяем пространство имен для элементов JavaFX -->  <!-- Определяем корневой элемент GridPane, который будет служить контейнером для других элементов интерфейса.      В данном примере он пока пустой и не содержит дочерних элементов. --> 

данный 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 нужно сделать разметку сетки и далее в ячейки этой сетки помещать различные элементы.

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

<columnConstraints> <!-- настраиваем колонки -->     <!-- Устанавливаем ограничения для первой колонки -->     <ColumnConstraints minWidth="300" maxWidth="300" hgrow="NEVER" halignment="CENTER"/>     <!-- Первая колонка будет иметь фиксированную ширину 300 пикселей и не будет изменяться в размере -->     <!-- тут будет настроечная панель она не должна меняться в размере -->        <!-- Устанавливаем ограничения для второй колонки -->     <ColumnConstraints fx:id="columnWith" minWidth="0" maxWidth="Infinity" hgrow="ALWAYS" halignment="CENTER"/>     <!-- Вторая колонка будет гибкой и может изменяться в ширину, занимая оставшееся пространство -->     <!-- в этой колонке будет пространство атрисовки я она должна растягиваться вместе с окном --> </columnConstraints>  <rowConstraints> <!--настраиваем строки-->     <!-- Устанавливаем ограничения для первой строки -->     <RowConstraints fx:id="rowHeight" minHeight="0" maxHeight="Infinity" vgrow="ALWAYS" valignment="CENTER"/>     <!-- Первая строка будет гибкой и может изменяться в высоту, занимая оставшееся пространство -->     <!-- тут будет настроечная панель и яхочу чтоб она растягивалась вместе с окнов в высоте своей -->        <!-- Устанавливаем ограничения для второй строки -->     <RowConstraints minHeight="0" maxHeight="25" vgrow="NEVER" valignment="CENTER"/>     <!-- Вторая строка будет фиксированной высотой 25 пикселей и не будет изменяться в размере -->     <!-- тут будет настройки точек фигуры и я не хочу чтоб они менялись с растягиванием окна -->      <!-- Устанавливаем ограничения для третьей строки -->     <RowConstraints minHeight="50" maxHeight="50" vgrow="NEVER" valignment="CENTER"/>     <!-- Третья строка будет фиксированной высотой 50 пикселей и не будет изменяться в размере -->     <!-- тут будет кнопка и я не хочу чтоб она менялась с растягиванием окна --> </rowConstraints> 

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

    <children>          <!-- пока я временно использую лейблы,             потому что им удобнее всего оказалось рисовать разметку,             но дальши заменим их на кнопки и прочие.             -->                <Label text="панель настройки" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS" style="-fx-background-color: #c1d0a4;" maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0"/>          <Label fx:id="startBTN" text="кнопака запуска"                GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS"                style="-fx-background-color: #003248;"                maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0" />          <Label                 text="пространство отрисовки фигуры" style="-fx-background-color: white;" GridPane.columnIndex="1"                 GridPane.rowIndex="0" GridPane.columnSpan="1" GridPane.rowSpan="3" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS" maxWidth="Infinity" maxHeight="Infinity"/>      </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, обеспечивая адаптивное и отзывчивое оформление интерфейса. и таким образом достигаю нужного эффекта. и вот что получаю следующие окно

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

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

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

        <Button text="Start ScalaFx"                 GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS"                 maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0"/>

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

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

        <HBox GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS">             <TextField promptText="divisionCoefficient y"/>             <TextField promptText="radius"/>             <TextField promptText="hex"/>             <TextField promptText="iter count"/>         </HBox>

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

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

        <ScrollPane fitToWidth="true" fitToHeight="true" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS">             <VBox maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0">                  <children>                     <HBox alignment="CENTER" spacing="5">                         <Button text="-" HBox.hgrow="ALWAYS" maxWidth="Infinity" prefWidth="0" prefHeight="10" maxHeight="10" />                         <Button text="+" HBox.hgrow="ALWAYS" maxWidth="Infinity" prefWidth="0" prefHeight="10" maxHeight="10" />                     </HBox>                 </children>             </VBox>         </ScrollPane>

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

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

        <ScrollPane fitToWidth="true" fitToHeight="true" GridPane.columnIndex="1" GridPane.rowIndex="0" GridPane.columnSpan="1" GridPane.rowSpan="3" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS">             <Pane xmlns:fx="http://javafx.com/fxml"                   GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS"                   minWidth="00" maxWidth="0"/>         </ScrollPane>

и того имеем:

Стилизуем через .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

<Button text="Start ScalaFx"      GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS"      styleClass="start-button" //подключаем стиль      maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0"/>

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

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

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

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

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

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

<GridPane         xmlns:fx="http://javafx.com/fxml"         fx:controller="com.scalafx.fractal.controller.MainWindowController" //добавляем эту строку         styleClass="grid-pane">

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

<VBox fx:id="settingsVbox" //собственно вот                   maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0"                   styleClass="settings-panel">

для необходимых компонентов добовляю этот параметр, тут останавливаться не буду
и в конструктор по этому 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 со следующим содержанием

<?xml version="1.0" encoding="UTF-8"?>  <?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.control.Label?> <?import javafx.scene.control.TextField?>  <VBox xmlns:fx="http://javafx.com/fxml/1">     <children>         <Label text="Attractor Panel" styleClass="attractor-panel-label"/>         <TextField fx:id="formulaTFX" promptText="formula x"/>         <TextField fx:id="formulaTFY" promptText="formula y"/>         <HBox>             <TextField fx:id="probabilityWeightTF" promptText="probability Weight"/>             <TextField fx:id="divisionCoefficientTF" promptText="divisionCoefficient y"/>         </HBox>         <HBox>             <TextField fx:id="radiusTF" promptText="radius"/>             <TextField fx:id="hexTF" promptText="hex"/>             <TextField fx:id="iterTF" promptText="iter count"/>         </HBox>     </children> </VBox> 

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

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

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

<Button text="Start ScalaFx"       GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="1" GridPane.vgrow="ALWAYS" GridPane.hgrow="ALWAYS"       styleClass="start-button"       maxWidth="Infinity" maxHeight="Infinity" prefWidth="0" prefHeight="0"       onAction="#rendering"/> //тут назначаем 

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

@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

добовляю только допом 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) } 

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

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

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

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

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

и ещё
бубновый фрактал

бубновый фрактал
последний из серии этой с 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
А так, пишите/жмите что думаете.


ссылка на оригинал статьи https://habr.com/ru/articles/832872/