Ремарка
В данной статье подробно разбирается, как автор создал оконное приложение с пользовательским интерфейсом для рисования фракталов методом хаоса. Однако, автор не утверждает, что выбранный стек технологий и методология являются наиболее подходящими или оптимальными для данной задачи или аналогичных проектов. Кроме того, в программе используется функционал, ранее описанный в предыдущей статье, поэтому аспекты обработки арифметических выражений будут упомянуты лишь вскользь с ссылкой на ту статью. Следует также отметить, что автор впервые сталкивается с использованием CSS в этом проекте и потому скорее всего весьма топорно и некрасиво оформил файл.
Метод Хауса
Метод хаоса, или "игра хаоса", является увлекательным и визуально впечатляющим подходом к созданию фрактальных структур. Этот метод основывается на случайном выборе точек внутри определенной фигуры и последовательном применении простых математических правил для генерации новых точек. В результате многократного применения этих правил могут формируются сложные и красивые узоры. Одним из наиболее известных примеров использования метода хаоса является создание фрактала Серпинского треугольника, что так же упомянуто в статье на википедии. Привлекательность метода хаоса заключается в его простоте и способности генерировать сложные структуры из простых алгоритмов, что делает его мощным инструментом для исследования фрактальной геометрии и визуализации хаоса.
что хочет создать автор
Целью проекта является создание оконного приложения на фреймворке ScalaFX, которое будет состоять из области для рисования фигур методом хаоса, панели настроек и кнопки, запускающей отрисовку фигуры. Приложение также должно иметь привлекательный внешний вид. Область для рисования представляет собой пространство, где после нажатия кнопки будет появляться фигура.
Панель настроек включает в себя две основные составляющие:
Настройка веса точки, который определяет, насколько близко к предыдущей точке будет ставиться следующая. Также можно будет задать цвет точек и итоговой фигуры, радиус точек и их количество.
Настройка аттракторов. Их координаты должны задаваться формульно отдельно для координат x и y. Также необходимо указать вероятностный вес аттрактора, от которого зависит выбор аттрактора на каждой итерации, и вес аттрактора, определяющий, насколько близко к нему будет располагаться точка. Дополнительно можно задать радиус, цвет и количество аттракторов (первые два параметра используются исключительно для визуализации).
Разметка и цветовая схема
недолго поразмыслив пришла к такой структуре:
![](https://habrastorage.org/getpro/habr/upload_files/811/194/1ae/8111941ae75d77774e25f21223ebd522.png)
Цвета здесь выбраны произвольно, и их нужно подобрать согласно цветовой схеме. Так как зона отрисовки фигур останется белой, будем отталкиваться от белого цвета и использовать следующим сайтом
подбираю 6 следующих оттенков:
#f5f9ea
#c1d0a4
#46727d
#003248
#001933
#000000
переделаю схему с использованием следующих оттенков
![](https://habrastorage.org/getpro/habr/upload_files/278/6f5/3cc/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
![](https://habrastorage.org/getpro/habr/upload_files/dd6/2a5/8fe/dd62a58fe887b9618988906cb1060463.png)
первое окно запущено .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
на данном этапе задача воспроезвести следующий макет
![угольных интерфейсов угольных интерфейсов](https://habrastorage.org/getpro/habr/upload_files/f28/1bc/6aa/f281bc6aa095ec458b4b7dac2c4cb5f6.png)
для этого я выбираю 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
, обеспечивая адаптивное и отзывчивое оформление интерфейса. и таким образом достигаю нужного эффекта. и вот что получаю следующие окно
![](https://habrastorage.org/getpro/habr/upload_files/f2f/1b7/f3b/f2f1b7f3b15bc634a66b3ee83a05e344.png)
таким образом разметкак готова
наполняем 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"/>
на данном этапе имеем следующие:
![](https://habrastorage.org/getpro/habr/upload_files/3f4/7c3/d55/3f47c3d5598a6029fc27785848353278.png)
сделаем панель настройки точек фигуры, добавив следующию структуру
<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>
на данный момент имеем следующие:
![](https://habrastorage.org/getpro/habr/upload_files/531/7a8/009/5317a80098279c8bd593c1f5250850d9.png)
сделаем настроичную панель заменив 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>
и имею следующие:
![](https://habrastorage.org/getpro/habr/upload_files/b7f/e84/d38/b7fe84d38a54e71f61697da69cb94238.png)
и наконец осталась только отрисовочная панель
<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>
и того имеем:
![](https://habrastorage.org/getpro/habr/upload_files/c35/c94/886/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
<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 более затрагиваться в статье не будет, но в нем есть стили для всех элементов и тгго имею следующий интерфейс:
![](https://habrastorage.org/getpro/habr/upload_files/712/2ff/521/7122ff52192a32f99ba9515ebe354542.png)
Ну, вроде вышло симпотично
создаем контроллер для окна
что бы назначить контроллер нужно с начала создать класс и навесить на него анотацию @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)
}
}
и того мы можен добавлять новые панели настроичные для точек атрактора:
![пример с двумя панелями пример с двумя панелями](https://habrastorage.org/getpro/habr/upload_files/456/830/f62/456830f62f8172691f1ee4165d542fad.png)
вроде как весьма симпотично.
рендеринг
последнее что осталось сделать - это функционал главной кнопки
но сначала определим 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
![](https://habrastorage.org/getpro/habr/upload_files/389/2ab/5e7/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)
}
собственно приложение готово
Рисуем фракталы
![вот я рисую треугольник серпинского вот я рисую треугольник серпинского](https://habrastorage.org/getpro/habr/upload_files/dfc/55f/a27/dfc55fa27f348485aa369373ff58c969.png)
![другой фрактал у него вроде тоже есть название другой фрактал у него вроде тоже есть название](https://habrastorage.org/getpro/habr/upload_files/148/291/7dc/1482917dc76735bb54cd6ee3a39577a9.png)
![и ещё и ещё](https://habrastorage.org/getpro/habr/upload_files/8a2/8be/238/8a28be238ce538833e1b3271ae6e5cad.png)
![бубновый фрактал бубновый фрактал](https://habrastorage.org/getpro/habr/upload_files/a19/ac6/a5f/a19ac6a5f409650c95f0b1f23eef763e.png)
![](https://habrastorage.org/getpro/habr/upload_files/d33/7be/837/d337be8371ef50f9e14a8d85f5690727.png)
![последний из серии этой с ic = 6, тут прикольно что в центре еще фрактал снежинка образуется из пустоты последний из серии этой с ic = 6, тут прикольно что в центре еще фрактал снежинка образуется из пустоты](https://habrastorage.org/getpro/habr/upload_files/8da/7ba/c80/8da7bac808cbaea2c174e94f87a49ba7.png)
![фрактал из превью к статье, обращу внимание что это не треугольник серпинского фрактал из превью к статье, обращу внимание что это не треугольник серпинского](https://habrastorage.org/getpro/habr/upload_files/276/0af/0e2/2760af0e2f2fbe5d821532a68e3f92c0.png)
![фрактал "космический корабль" (авторское название) фрактал "космический корабль" (авторское название)](https://habrastorage.org/getpro/habr/upload_files/448/38f/5c5/44838f5c5d3bd087f97dd17ae832bc8b.png)
![ковер серпинского ковер серпинского](https://habrastorage.org/getpro/habr/upload_files/1ab/ff2/693/1abff2693ae7920286ebbd12b4eeef61.png)
![снежинка снежинка](https://habrastorage.org/getpro/habr/upload_files/9c5/dd1/ec0/9c5dd1ec0441340ed13a8b2beb5bad07.png)
![последний последний](https://habrastorage.org/getpro/habr/upload_files/ae0/91b/6df/ae091b6dfbb8710ebad5df50442ba1a6.png)
снова лезем в 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
А так, пишите/жмите что думаете.
mike_aksarin
привет! Спасибо за пост. Давно ли вы начали писать на скале и почему ею заинтересовались? Интересно было почитать ваш пример использования ScalaFХ, как у вас впечатление о нем в целом, можете ли его рекомендовать для построения UI?
PicoPicoRobotWoman Автор
привет! не сказать что бы прям очень давно, к скале пришла через работу с
Apache Spark
,и теперь это мой любимый ЯП. про ScalFX стоит понимать что это обвесок
JavaFX, и в общем на скала мне приятнее с FX работать чем на java.
впечетление хорошее, напоминает WPF, в общем тот же подход: описываем
интерфейс через XML, а все остальное кодом. так как был опыт WPF мне
было не сложно саирентироваться. то есть если вы пишете на Scala/Java то
пожалуй FX лучший вариант чем swing или swt которые так же на джавой
платформе работают.
Anarchist
Нда, на Скалу подсаживаемся, как на наркоту. :)