Ремарка
В данной статье подробно разбирается, как автор создал оконное приложение с пользовательским интерфейсом для рисования фракталов методом хаоса. Однако, автор не утверждает, что выбранный стек технологий и методология являются наиболее подходящими или оптимальными для данной задачи или аналогичных проектов. Кроме того, в программе используется функционал, ранее описанный в предыдущей статье, поэтому аспекты обработки арифметических выражений будут упомянуты лишь вскользь с ссылкой на ту статью. Следует также отметить, что автор впервые сталкивается с использованием CSS в этом проекте и потому скорее всего весьма топорно и некрасиво оформил файл.
Метод Хауса
Метод хаоса, или «игра хаоса», является увлекательным и визуально впечатляющим подходом к созданию фрактальных структур. Этот метод основывается на случайном выборе точек внутри определенной фигуры и последовательном применении простых математических правил для генерации новых точек. В результате многократного применения этих правил могут формируются сложные и красивые узоры. Одним из наиболее известных примеров использования метода хаоса является создание фрактала Серпинского треугольника, что так же упомянуто в статье на википедии. Привлекательность метода хаоса заключается в его простоте и способности генерировать сложные структуры из простых алгоритмов, что делает его мощным инструментом для исследования фрактальной геометрии и визуализации хаоса.
что хочет создать автор
Целью проекта является создание оконного приложения на фреймворке ScalaFX, которое будет состоять из области для рисования фигур методом хаоса, панели настроек и кнопки, запускающей отрисовку фигуры. Приложение также должно иметь привлекательный внешний вид. Область для рисования представляет собой пространство, где после нажатия кнопки будет появляться фигура.
Панель настроек включает в себя две основные составляющие:
-
Настройка веса точки, который определяет, насколько близко к предыдущей точке будет ставиться следующая. Также можно будет задать цвет точек и итоговой фигуры, радиус точек и их количество.
-
Настройка аттракторов. Их координаты должны задаваться формульно отдельно для координат 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>
-
GridPane.columnIndex: Устанавливает индекс столбца, в котором будет размещен элемент.
-
GridPane.rowIndex: Устанавливает индекс строки, в которой будет размещен элемент.
-
GridPane.columnSpan: Определяет, сколько столбцов будет занимать элемент.
-
GridPane.rowSpan: Определяет, сколько строк будет занимать элемент.
-
GridPane.vgrow: Определяет вертикальный рост элемента.
ALWAYSпозволяет элементу расти по вертикали, занимая доступное пространство. -
GridPane.hgrow: Определяет горизонтальный рост элемента.
ALWAYSпозволяет элементу расти по горизонтали, занимая доступное пространство. -
style: Позволяет установить CSS-стили для элемента, например,
-fx-background-colorдля установки цвета фона. -
maxWidth и maxHeight: Устанавливают максимальные размеры элемента.
-
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) }
собственно приложение готово
Рисуем фракталы

снова лезем в 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/
Добавить комментарий