Parcourir la source

Merge branch 'master' of https://git.winfor.ch/nicolas/Almond

Nicolas Winkler il y a 5 ans
Parent
commit
464d0c36f3
68 fichiers modifiés avec 5124 ajouts et 1577 suppressions
  1. 222 124
      Almond.cpp
  2. 11 0
      Almond.desktop
  3. 32 4
      Almond.h
  4. 0 0
      Almond.png
  5. 26 9
      Almond.pro
  6. 2 2
      Almond.qrc
  7. 82 70
      Almond.ui
  8. 229 0
      AlmondMenuWidget.cpp
  9. 67 0
      AlmondMenuWidget.h
  10. 15 8
      BackgroundTask.cpp
  11. 3 2
      BackgroundTask.h
  12. 13 4
      CMakeLists.txt
  13. 0 66
      CubicSpline.cpp
  14. 0 19
      CubicSpline.h
  15. 547 0
      EscapeTimeVisualWidget.cpp
  16. 72 0
      EscapeTimeVisualWidget.h
  17. 71 0
      ExportImageMenu.cpp
  18. 30 0
      ExportImageMenu.h
  19. 99 0
      ExportImageMenu.ui
  20. 123 0
      ExportVideoMenu.cpp
  21. 31 0
      ExportVideoMenu.h
  22. 252 0
      ExportVideoMenu.ui
  23. 325 0
      FractalWidget.cpp
  24. 63 0
      FractalWidget.h
  25. 133 0
      FractalWidgetUtils.cpp
  26. 138 0
      FractalWidgetUtils.h
  27. 479 0
      FractalZoomWidget.cpp
  28. 128 0
      FractalZoomWidget.h
  29. 0 164
      Gradient.cpp
  30. 0 38
      Gradient.h
  31. 41 0
      GradientMenu.cpp
  32. 32 0
      GradientMenu.h
  33. 58 0
      GradientMenu.ui
  34. 460 1
      GradientWidget.cpp
  35. 80 2
      GradientWidget.h
  36. 17 0
      LICENSE
  37. 234 56
      MandelWidget.cpp
  38. 64 19
      MandelWidget.h
  39. 8 0
      README.md
  40. 1 0
      customgenerator.cpp
  41. 112 0
      debian/copyright
  42. 19 4
      exportdialogs.cpp
  43. 1 1
      gradientchoosedialog.cpp
  44. 0 674
      installer/packages/almond/meta/gpl3.txt
  45. 1 1
      installer/packages/almond/meta/package.xml
  46. 17 0
      installer/packages/almond/meta/zlib.txt
  47. 16 4
      libalmond/CMakeLists.txt
  48. 1 1
      libalmond/include/CubicSpline.h
  49. 18 5
      libalmond/include/Gradient.h
  50. 21 3
      libalmond/include/ImageExport.h
  51. 21 4
      libalmond/src/CubicSpline.cpp
  52. 65 4
      libalmond/src/Gradient.cpp
  53. 313 2
      libalmond/src/ImageExport.cpp
  54. 23 20
      libmandel/CMakeLists.txt
  55. 14 12
      libmandel/asmjit/CMakeLists.txt
  56. 4 5
      libmandel/include/CpuGenerators.h
  57. 2 1
      libmandel/include/Mandel.h
  58. 17 1
      libmandel/include/Types.h
  59. 0 0
      libmandel/include_cl/CL/cl.hpp
  60. 0 0
      libmandel/include_cl/CL/cl2.hpp
  61. 2 0
      libmandel/src/ClGenerators.cpp
  62. 10 5
      libmandel/src/CpuGeneratorsAVXFMA.cpp
  63. 2 4
      libmandel/src/CpuGeneratorsNeon.cpp
  64. 16 7
      libmandel/src/Mandel.cpp
  65. 13 11
      libmandel/src/opencl/float.cl
  66. 212 210
      libmandel/src/opencl/float.h
  67. 2 0
      main.cpp
  68. 14 10
      mandelvid/src/main.cpp

+ 222 - 124
Almond.cpp

@@ -3,7 +3,9 @@
 #include <QIcon>
 #include <QFileDialog>
 #include <QMessageBox>
+#include <QStatusBar>
 #include <QGradient>
+#include <QWindow>
 #include "gradientchoosedialog.h"
 
 #include "GridFlowLayout.h"
@@ -11,35 +13,78 @@
 #include <cmath>
 
 Almond::Almond(QWidget* parent) :
-    QMainWindow{ parent },
+    QMainWindow{ parent, Qt::WindowFlags() },
     mandelContext{ mnd::initializeContext() }
 {
     ui.setupUi(this);
-    mw = std::make_unique<MandelWidget>(mandelContext,
-                                        &mandelContext.getDefaultGenerator(),
-                                        ui.centralWidget);
+    fractalWidget = new FractalWidget(this);
+    fractalWidget->setGenerator(&mandelContext.getDefaultGenerator());
+    fractalWidget->setGradient(Gradient::defaultGradient());
+    fractalWidget->setSmoothColoring(ui.smooth->isChecked());
+
+    connect(fractalWidget, &FractalWidget::pointSelected, this, &Almond::pointSelected);
+
     customGeneratorDialog = std::make_unique<CustomGenerator>(mandelContext);
     customGenerator = nullptr;
     customViewSave = mnd::MandelViewport::centerView();
 
     on_maxIterations_editingFinished();
-    mw->setSmoothColoring(ui.smooth->isChecked());
-
 
     currentView = MANDELBROT;
     mandelGenerator = &mandelContext.getDefaultGenerator();
-    mandelViewSave = mw->getViewport();
+    mandelViewSave = fractalWidget->getViewport();
 
-    QObject::connect(mw.get(), &MandelWidget::pointSelected, this, &Almond::pointSelected);
-    ui.mainContainer->addWidget(mw.get());
+    ui.mandel_container->addWidget(fractalWidget);
+    //ui.mandel_container->addWidget(mw.get());
     ui.maxIterations->setValidator(new QIntValidator(1, 1000000000, this));
-    ui.backgroundProgress->setVisible(false);
+
+    ui.backgroundProgress->setEnabled(false);
+    ui.cancelProgress->setEnabled(false);
+
+    amw = new AlmondMenuWidget(this);
+    amw->setMainMenu(ui.dockWidgetContents_2);
+    eim = new ExportImageMenu();
+    evm = new ExportVideoMenu();
+    gradientMenu = new GradientMenu();
+    AlmondSubMenu* imageSm = amw->addSubMenu(eim);
+    AlmondSubMenu* videoSm = amw->addSubMenu(evm);
+    AlmondSubMenu* gradientSm = amw->addSubMenu(gradientMenu);
+    ui.dockWidget_2->setWidget(amw);
+
+    connect(amw, &AlmondMenuWidget::submenuCancel, [this] (int) { amw->showMainMenu(); });
+    //connect(amw, &AlmondMenuWidget::submenuOK, this, &Almond::submenuOK);
+    connect(imageSm, &AlmondSubMenu::accepted, this, &Almond::imageExportOk);
+    connect(videoSm, &AlmondSubMenu::accepted, this, &Almond::videoExportOk);
+    connect(gradientSm, &AlmondSubMenu::accepted, this, &Almond::gradientEditOk);
+    connect(gradientSm, &AlmondSubMenu::cancelled, [this] () {
+        fractalWidget->setGradient(gradientMenu->getGradientBefore());
+    });
+    connect(gradientMenu, &GradientMenu::gradientChanged, [this] () {
+        fractalWidget->setGradient(gradientMenu->getGradient());
+    });
+
+
+    /*QStatusBar* bar = new QStatusBar(this);
+    bar->addWidget(new QLabel("ayay"));
+    auto* p = new QPushButton("About");
+    bar->addPermanentWidget(p);
+    QObject::connect(p, &QPushButton::clicked, [this]() {
+        toggleFullscreen();
+    });
+    bar->setFixedHeight(bar->sizeHint().height());
+    //ui.mainContainer->addWidget(bar);
+    this->setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
+    this->setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);*/
+
+    installEventFilter(this);
 
     backgroundTasks.setMaxThreadCount(1);
     QIcon icon{ ":/icons/icon" };
     icon.addFile(":/icons/icon@2x");
     this->setWindowIcon(icon);
 
+
+
     // replace vertical layout with gridflowlayout
     /*GridFlowLayout* gfl = new GridFlowLayout(nullptr);
     //ui.horizontalLayout_4->addItem(gfl);
@@ -62,12 +107,126 @@ void Almond::submitBackgroundTask(BackgroundTask* task)
 {
     QObject::connect(task, &BackgroundTask::finished, this, &Almond::backgroundTaskFinished);
     QObject::connect(task, &BackgroundTask::progress, this, &Almond::backgroundTaskProgress);
+    int running = backgroundTasks.activeThreadCount();
     backgroundTasks.start(task);
-    //if (taken) {
+    if (running == 0) {
         ui.backgroundProgress->setRange(0, 0);
-        ui.backgroundProgress->setVisible(true);
         ui.backgroundProgress->setFormat("");
-    //}
+        ui.backgroundProgress->setEnabled(true);
+        ui.cancelProgress->setEnabled(true);
+    }
+}
+
+
+void Almond::stopBackgroundTask(void)
+{
+    stoppingBackgroundTasks = true;
+}
+
+bool Almond::eventFilter(QObject *target, QEvent *event)
+{
+    if (event->type() == QEvent::KeyPress) {
+        QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+        if (keyEvent->key() == Qt::Key_F11) {
+            emit toggleFullscreen();
+        }
+    }
+    return QObject::eventFilter(target, event);
+}
+
+
+void Almond::submenuOK(int smIndex)
+{
+    switch(smIndex) {
+    case 0:
+        emit imageExportOk();
+        break;
+    case 1:
+        emit videoExportOk();
+        break;
+    }
+}
+
+void Almond::imageExportOk(void)
+{
+    mnd::MandelInfo mi;
+    mi.maxIter = eim->getMaxIterations();
+    mi.view = fractalWidget->getViewport();
+    mi.bWidth = eim->getWidth();
+    mi.bHeight = eim->getHeight();
+    mi.view.adjustAspectRatio(mi.bWidth, mi.bHeight);
+    mi.smooth = true;
+    if (currentView == JULIA) {
+        mi.julia = fractalWidget->getMandelInfo().julia;
+        mi.juliaX = fractalWidget->getMandelInfo().juliaX;
+        mi.juliaY = fractalWidget->getMandelInfo().juliaY;
+    }
+
+    mnd::MandelGenerator* currentGenerator = fractalWidget->getGenerator();
+    if (currentGenerator == nullptr)
+        currentGenerator = &mandelContext.getDefaultGenerator();
+
+    alm::ImageExportInfo iei;
+    iei.drawInfo = mi;
+    iei.generator = currentGenerator;
+    iei.gradient = fractalWidget->getGradient();
+    iei.path = eim->getPath().toStdString();
+    iei.options.jpegQuality = 95;
+    submitBackgroundTask(new ImageExportTask(iei, [this] () { return stoppingBackgroundTasks; }));
+    
+    amw->showMainMenu();
+}
+
+
+void Almond::videoExportOk(void)
+{
+    ExportVideoInfo evi = evm->getInfo();
+    evi.gradient = fractalWidget->getGradient();
+    evi.mi.smooth = fractalWidget->getMandelInfo().smooth;
+    if (currentView == JULIA) {
+        evi.mi.julia = fractalWidget->getMandelInfo().julia;
+        evi.mi.juliaX = fractalWidget->getMandelInfo().juliaX;
+        evi.mi.juliaY = fractalWidget->getMandelInfo().juliaY;
+    }
+    if (evi.path == "") {
+        QMessageBox errMsg = QMessageBox(QMessageBox::Icon::Critical, "Error", "No path specified.");
+        errMsg.exec();
+    }
+    else {
+        MandelVideoGenerator mvg(evi);
+        mnd::MandelGenerator& g = *fractalWidget->getGenerator();
+        //printf("wii: %ld\n", evi.mi.bWidth);
+        fflush(stdout);
+        submitBackgroundTask(new VideoExportTask(std::move(mvg), g));
+        amw->showMainMenu();
+    }
+}
+
+
+void Almond::gradientEditOk(void)
+{
+    fractalWidget->setGradient(gradientMenu->getGradient());
+    amw->showMainMenu();
+}
+
+
+void Almond::toggleFullscreen(void)
+{
+    /*
+    if (fullscreenMode) {
+        auto* m = this->takeCentralWidget();
+        ui.mandel_container->addWidget(m);
+        this->setCentralWidget(cw);
+        emit this->showNormal();
+        fullscreenMode = false;
+    }
+    else {
+        cw = this->takeCentralWidget();
+        this->setCentralWidget(mw.get());
+        emit this->showFullScreen();
+        fullscreenMode = true;
+    }
+    */
 }
 
 
@@ -84,8 +243,12 @@ void Almond::backgroundTaskFinished(bool succ, QString message)
         emit info.exec();
     }
 
-    ui.backgroundProgress->setVisible(false);
-    ui.backgroundProgress->setFormat("");
+    ui.backgroundProgress->setFormat(tr("Export Progress"));
+    ui.backgroundProgress->setEnabled(false);
+    ui.backgroundProgress->setRange(0, 100);
+    ui.backgroundProgress->setValue(0);
+    ui.cancelProgress->setEnabled(false);
+    stoppingBackgroundTasks = false;
 }
 
 
@@ -109,13 +272,13 @@ void Almond::backgroundTaskProgress(float percentage)
 
 void Almond::on_zoom_out_clicked()
 {
-    mw->zoom(2);
+    fractalWidget->zoom(2);
 }
 
 
 void Almond::on_zoom_in_clicked()
 {
-    mw->zoom(0.5);
+    fractalWidget->zoom(0.5);
 }
 
 
@@ -123,119 +286,51 @@ void Almond::on_maxIterations_editingFinished()
 {
     QString text = ui.maxIterations->text();
     int maxIter = text.toInt();
-    mw->setMaxIterations(maxIter);
+    fractalWidget->setMaxIterations(maxIter);
 }
 
 
 void Almond::on_chooseGradient_clicked()
 {
-    gcd.exec();
-    auto gradient = gcd.getGradient();
-    if (gradient)
-        mw->setGradient(std::move(*gradient));
+    this->gradientMenu->setGradient(fractalWidget->getGradient());
+    emit this->amw->showSubMenu(2);
 }
 
 
 void Almond::on_exportVideo_clicked()
 {
-    ExportVideoInfo evi;
-    evi.start = mnd::MandelViewport::standardView();
-    evi.end = mw->getViewport();
-    evi.gradient = mw->getGradient();
-    ExportVideoDialog dialog(this, evi);
-    //dialog.show();
-    auto response = dialog.exec();
-    printf("dialog executed\n"); fflush(stdout);
-    if (response == 1) {
-        mnd::MandelInfo mi;
-        evi = dialog.getExportVideoInfo();
-        MandelVideoGenerator mvg(evi);
-        mnd::MandelGenerator& g = *mw->getGenerator();
-        submitBackgroundTask(new VideoExportTask(std::move(mvg), g));
-        //if (exportVideo(evi)) {
-
-        //Video
-        /*mi.maxIter = dialog.getMaxIterations();
-        mi.view = mw->getViewport();
-        mi.bWidth = dialog.getWidth();
-        mi.bHeight = dialog.getHeight();
-        mi.view.adjustAspectRatio(mi.bWidth, mi.bHeight);
-        mnd::Generator& g = mandelContext.getDefaultGenerator();
-        auto fmap = Bitmap<float>(mi.bWidth, mi.bHeight);
-        g.generate(mi, fmap.pixels.get());
-        auto bitmap = fmap.map<RGBColor>([](float i) { return i < 0 ? RGBColor{ 0,0,0 } : RGBColor{ uint8_t(cos(i * 0.015f) * 127 + 127), uint8_t(sin(i * 0.01f) * 127 + 127), uint8_t(i) }; });//uint8_t(::sin(i * 0.01f) * 100 + 100), uint8_t(i) }; });
-        QImage img((unsigned char*)bitmap.pixels.get(), bitmap.width, bitmap.height, bitmap.width * 3, QImage::Format_RGB888);
-        img.save(dialog.getPath());*/
-    }
+    evm->setEndViewport(fractalWidget->getViewport());
+    emit this->amw->showSubMenu(1);
 }
 
 
 void Almond::on_smooth_stateChanged(int checked)
 {
-    this->mw->setSmoothColoring(checked != Qt::Unchecked);
+    fractalWidget->setSmoothColoring(checked != Qt::Unchecked);
 }
 
 
 void Almond::on_exportImage_clicked()
 {
-    ExportImageDialog dialog(this);
-    dialog.setMaxIterations(mw->getMaxIterations());
-    //dialog.show();
-    auto response = dialog.exec();
-    if (response == 1) {
-        mnd::MandelInfo mi;
-        mi.maxIter = dialog.getMaxIterations();
-        mi.view = mw->getViewport();
-        mi.bWidth = dialog.getWidth();
-        mi.bHeight = dialog.getHeight();
-        mi.view.adjustAspectRatio(mi.bWidth, mi.bHeight);
-        mi.smooth = mw->getSmoothColoring();
-        if (currentView == JULIA) {
-            mi.julia = mw->getMandelInfo().julia;
-            mi.juliaX = mw->getJuliaX();
-            mi.juliaY = mw->getJuliaY();
-        }
-        mnd::MandelGenerator* currentGenerator = mw->getGenerator();
-        mnd::MandelGenerator& g = currentGenerator ? *currentGenerator : mandelContext.getDefaultGenerator();
-
-        alm::ImageExportInfo iei;
-        iei.drawInfo = mi;
-        iei.generator = &g;
-        iei.gradient = mw->getGradient();
-        iei.path = dialog.getPath().toStdString();
-        submitBackgroundTask(new ImageExportTask(iei));
-
-        /*auto exprt = [iei, path = dialog.getPath().toStdString()]() {
-            alm::exportPng(path, iei);
-        };
-
-        submitBackgroundTask();*/
-        
-        /*auto fmap = Bitmap<float>(mi.bWidth, mi.bHeight);
-        g.generate(mi, fmap.pixels.get());
-        auto bitmap = fmap.map<RGBColor>([&mi, this] (float i) {
-            return i >= mi.maxIter ? RGBColor{ 0,0,0 } : mw->getGradient().get(i);
-        });
-        QImage img(reinterpret_cast<unsigned char*>(bitmap.pixels.get()), bitmap.width, bitmap.height, bitmap.width * 3, QImage::Format_RGB888);
-        img.save(dialog.getPath());*/
-    }
+    this->amw->showSubMenu(0);
+    return;
 }
 
 
 void Almond::on_resetZoom_clicked()
 {
     if (currentView == MANDELBROT) {
-        mw->setViewport(mnd::MandelViewport::standardView());
+        fractalWidget->setViewport(mnd::MandelViewport::standardView());
     }
     else {
-        mw->setViewport(mnd::MandelViewport::centerView());
+        fractalWidget->setViewport(mnd::MandelViewport::centerView());
     }
 }
 
 
 void Almond::on_displayInfo_stateChanged(int checked)
 {
-    this->mw->setDisplayInfo(checked != Qt::Unchecked);
+    this->fractalWidget->setDisplayInfo(checked != Qt::Unchecked);
 }
 
 
@@ -260,7 +355,7 @@ void Almond::on_chooseGenerator_clicked()
             customGenerator = gen.get();
         }
         currentGenerator = gen.get();
-        this->mw->setGenerator(currentGenerator);
+        this->fractalWidget->setGenerator(currentGenerator);
         adjustedGenerators.push_back(std::move(gen));
     }
     else {
@@ -276,20 +371,16 @@ void Almond::pointSelected(mnd::Real x, mnd::Real y)
 {
     if (currentView != JULIA) {
         saveView();
-        this->mw->setViewport(mnd::MandelViewport::centerView());
-        this->mw->setJuliaPos(x, y);
-        this->mw->getMandelInfo().julia = true;
-        this->mw->clearAll();
+        this->fractalWidget->setViewport(mnd::MandelViewport::centerView());
+        this->fractalWidget->getMandelInfo().julia = true;
+        this->fractalWidget->getMandelInfo().juliaX = x;
+        this->fractalWidget->getMandelInfo().juliaY = y;
+        this->fractalWidget->clearCells();
     }
     currentView = JULIA;
 }
 
 
-void Almond::on_groupBox_toggled(bool arg1)
-{
-    printf("arg1: %i\n", int(arg1)); fflush(stdout);
-}
-
 void Almond::on_wMandel_clicked()
 {
 
@@ -299,9 +390,9 @@ void Almond::on_wMandel_clicked()
 void Almond::saveView()
 {
     if (currentView == MANDELBROT)
-        mandelViewSave = mw->getViewport();
+        mandelViewSave = fractalWidget->getViewport();
     else if (currentView == CUSTOM)
-        customViewSave = mw->getViewport();
+        customViewSave = fractalWidget->getViewport();
 }
 
 
@@ -310,21 +401,21 @@ void Almond::setViewType(ViewType v)
     saveView();
     if (v == MANDELBROT) {
         currentGenerator = mandelGenerator;
-        emit this->mw->stopSelectingPoint();
-        this->mw->setViewport(mandelViewSave);
-        this->mw->setGenerator(currentGenerator);
-        this->mw->getMandelInfo().julia = false;
-        this->mw->clearAll();
+        emit this->fractalWidget->stopSelectingPoint();
+        this->fractalWidget->setViewport(mandelViewSave);
+        this->fractalWidget->setGenerator(currentGenerator);
+        this->fractalWidget->getMandelInfo().julia = false;
+        this->fractalWidget->clearCells();
         currentView = MANDELBROT;
     }
     else if (v == CUSTOM) {
         if (customGenerator != nullptr) {
             currentGenerator = customGenerator;
-            this->mw->setGenerator(currentGenerator);
-            emit this->mw->stopSelectingPoint();
-            this->mw->setViewport(customViewSave);
-            this->mw->getMandelInfo().julia = false;
-            this->mw->clearAll();
+            this->fractalWidget->setGenerator(currentGenerator);
+            emit this->fractalWidget->stopSelectingPoint();
+            this->fractalWidget->setViewport(customViewSave);
+            this->fractalWidget->getMandelInfo().julia = false;
+            this->fractalWidget->clearCells();
             currentView = CUSTOM;
         }
         else {
@@ -333,16 +424,16 @@ void Almond::setViewType(ViewType v)
     }
     else if (v == JULIA) {
         if (currentView == MANDELBROT) {
-            emit this->mw->selectPoint();
+            emit this->fractalWidget->selectJuliaPoint();
         }
         else {
             currentView = MANDELBROT;
             currentGenerator = mandelGenerator;
-            this->mw->setGenerator(currentGenerator);
-            this->mw->setViewport(mandelViewSave);
-            this->mw->getMandelInfo().julia = false;
-            this->mw->clearAll();
-            emit this->mw->selectPoint();
+            this->fractalWidget->setGenerator(currentGenerator);
+            this->fractalWidget->setViewport(mandelViewSave);
+            this->fractalWidget->getMandelInfo().julia = false;
+            this->fractalWidget->clearCells();
+            emit this->fractalWidget->selectJuliaPoint();
         }
     }
 }
@@ -378,6 +469,7 @@ void Almond::on_radioButton_2_toggled(bool checked)
     }
 }
 
+
 void Almond::on_createCustom_clicked()
 {
     auto response = customGeneratorDialog->exec();
@@ -391,3 +483,9 @@ void Almond::on_createCustom_clicked()
         setViewType(CUSTOM);
     }
 }
+
+
+void Almond::on_cancelProgress_clicked()
+{
+    stopBackgroundTask();
+}

+ 11 - 0
Almond.desktop

@@ -0,0 +1,11 @@
+[Desktop Entry]
+
+Type=Application
+Encoding=UTF-8
+Name=Almond
+Version=1.0
+Comment=Fractal Generation Software
+Exec=/usr/bin/Almond
+Icon=Almond.png
+Terminal=false
+Categories=Education;Art;

+ 32 - 4
Almond.h

@@ -1,4 +1,6 @@
 #pragma once
+#ifndef ALMOND_H
+#define ALMOND_H
 
 #include <QtWidgets/QMainWindow>
 #include "ui_Almond.h"
@@ -10,7 +12,14 @@
 #include "gradientchoosedialog.h"
 #include "choosegenerators.h"
 #include "customgenerator.h"
-//#include "benchmarkdialog.h"
+
+#include "FractalWidget.h"
+
+#include "AlmondMenuWidget.h"
+#include "ExportImageMenu.h"
+#include "ExportVideoMenu.h"
+#include "GradientMenu.h"
+
 
 #include <memory>
 
@@ -35,8 +44,17 @@ class Almond : public QMainWindow
 private:
     mnd::MandelContext mandelContext;
     QThreadPool backgroundTasks;
+    bool stoppingBackgroundTasks = false;
+
+    AlmondMenuWidget* amw;
+    ExportImageMenu* eim;
+    ExportVideoMenu* evm;
+    GradientMenu* gradientMenu;
+
+    bool fullscreenMode = false;
+    QWidget* cw;
 public:
-    std::unique_ptr<MandelWidget> mw;
+    FractalWidget* fractalWidget;
 private:
     //std::unique_ptr<BenchmarkDialog> benchmarkDialog;
     std::unique_ptr<CustomGenerator> customGeneratorDialog;
@@ -58,7 +76,16 @@ public:
     ~Almond(void);
 
     void submitBackgroundTask(BackgroundTask* task);
+    void stopBackgroundTask();
+
+    bool eventFilter(QObject *target, QEvent *event);
 
+    void submenuOK(int smIndex);
+    void imageExportOk(void);
+    void videoExportOk(void);
+    void gradientEditOk(void);
+public slots:
+    void toggleFullscreen(void);
 private slots:
     void on_zoom_out_clicked();
     void on_zoom_in_clicked();
@@ -80,8 +107,6 @@ private slots:
 
     void on_wMandel_toggled(bool checked);
 
-    void on_groupBox_toggled(bool arg1);
-
     void saveView(void);
     void setViewType(ViewType v);
 
@@ -89,8 +114,11 @@ private slots:
     void on_radioButton_toggled(bool checked);
     void on_radioButton_2_toggled(bool checked);
     void on_createCustom_clicked();
+    void on_cancelProgress_clicked();
 
 private:
     Ui::AlmondClass ui;
 };
 
+
+#endif // ALMOND_H

+ 0 - 0
icon.png → Almond.png


+ 26 - 9
Almond.pro

@@ -28,10 +28,16 @@ QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.12
 
 SOURCES += \
         Almond.cpp \
+        AlmondMenuWidget.cpp \
         BackgroundTask.cpp \
         Color.cpp \
-        CubicSpline.cpp \
-        Gradient.cpp \
+        EscapeTimeVisualWidget.cpp \
+        ExportImageMenu.cpp \
+        ExportVideoMenu.cpp \
+        FractalWidget.cpp \
+        FractalWidgetUtils.cpp \
+        FractalZoomWidget.cpp \
+        GradientMenu.cpp \
         GradientWidget.cpp \
         MandelWidget.cpp \
         choosegenerators.cpp \
@@ -43,10 +49,16 @@ SOURCES += \
 
 HEADERS += \
         Almond.h \
+        AlmondMenuWidget.h \
         BackgroundTask.h \
         Color.h \
-        CubicSpline.h \
-        Gradient.h \
+        EscapeTimeVisualWidget.h \
+        ExportImageMenu.h \
+        ExportVideoMenu.h \
+        FractalWidget.h \
+        FractalWidgetUtils.h \
+        FractalZoomWidget.h \
+        GradientMenu.h \
         GradientWidget.h \
         MandelWidget.h \
         choosegenerators.h \
@@ -57,6 +69,9 @@ HEADERS += \
 
 FORMS += \
         Almond.ui \
+        ExportImageMenu.ui \
+        ExportVideoMenu.ui \
+        GradientMenu.ui \
         choosegenerators.ui \
         customgenerator.ui \
         exportimagedialog.ui \
@@ -78,9 +93,8 @@ else:unix:QMAKE_LFLAGS+= -fopenmp
 LIBS += -fopenmp
 unix:LIBS += -lm -latomic
 
-win32:CONFIG(release, debug|release): LIBS += -L$$PWD/../libs/ffmpeg-20200216-8578433-win64-dev/lib/ -lavcodec
-else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../libs/ffmpeg-20200216-8578433-win64-dev/lib/ -lavcodec
-else:macx: LIBS += -L/usr/local/lib -lavcodec
+win32:CONFIG(release, debug|profile|release): LIBS += -L$$PWD/../libs/ffmpeg-20200216-8578433-win64-dev/lib/ -lavcodec
+else:win32:CONFIG(debug, debug|profile|release): LIBS += -L$$PWD/../libs/ffmpeg-20200216-8578433-win64-dev/lib/ -lavcodec
 else:unix: LIBS += -lavcodec
 
 win32:FFMPEGPATH = $$PWD/../libs/ffmpeg-20200216-8578433-win64-dev/lib/
@@ -90,6 +104,9 @@ win32:DEPENDPATH += $$PWD/../libs/ffmpeg-20200216-8578433-win64-dev/include
 win32:INCLUDEPATH += ../libs/boost_1_72_0
 DEFINES += WITH_BOOST=1
 
+win32:LIBS += -L$$PWD/../libs/libjpeg-turbo-2.0.4/libjpeg-turbo-2.0.4/build/Release
+win32:INCLUDEPATH += $$PWD/../libs/libjpeg-turbo-2.0.4/libjpeg-turbo-2.0.4
+
 #win32:CONFIG(release, debug|release): LIBS += -L$$PWD/'../../../../../Program Files (x86)/AMD APP SDK/3.0/lib/x86/' -lOpenCL
 #else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/'../../../../../Program Files (x86)/AMD APP SDK/3.0/lib/x86/' -lOpenCL
 #else:unix: LIBS += -lOpenCL
@@ -128,9 +145,9 @@ unix|win32: LIBS += -L$FFMPEGPATH -lswscale
 RESOURCES += Almond.qrc \
     splash.qrc
 
-win32:LIBS += -llibpng16_static -lzlibstatic
+win32:LIBS += -llibpng16_static -lzlibstatic -ljpeg
 unix|win32: LIBS += -L$$PWD/libmandel/ -L$$PWD/libalmond/ -lmandel -lqd -lasmjit -lalmond
-unix: LIBS += -lrt -lpng -lavcodec -lavdevice -lavformat -lavutil -lswscale -lavfilter
+unix: LIBS += -lrt -lpng -ljpeg -lavcodec -lavdevice -lavformat -lavutil -lswscale -lavfilter
 
 INCLUDEPATH += $$PWD/libmandel/include $$PWD/libmandel/qd-2.3.22/include $$PWD/libalmond/include
 DEPENDPATH += $$PWD/libmandel/include $$PWD/libmandel/qd-2.3.22/include $$PWD/libalmond/include

+ 2 - 2
Almond.qrc

@@ -1,7 +1,7 @@
 <RCC>
     <qresource prefix="/icons">
-        <file alias="icon">icon.png</file>
-        <file alias="icon@2x">icon.png</file>
+        <file alias="icon">Almond.png</file>
+        <file alias="icon@2x">Almond.png</file>
     </qresource>
     <qresource prefix="/gradients">
         <file alias="default">gradients/default.xml</file>

+ 82 - 70
Almond.ui

@@ -9,8 +9,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>1202</width>
-    <height>1188</height>
+    <width>1333</width>
+    <height>1141</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -19,16 +19,23 @@
   <widget class="QWidget" name="centralWidget">
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
-     <layout class="QVBoxLayout" name="mainContainer"/>
+     <layout class="QVBoxLayout" name="mainContainer">
+      <item>
+       <layout class="QHBoxLayout" name="mandel_container"/>
+      </item>
+     </layout>
     </item>
    </layout>
   </widget>
   <widget class="QDockWidget" name="dockWidget_2">
-   <property name="floating">
-    <bool>false</bool>
+   <property name="sizePolicy">
+    <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
+     <horstretch>0</horstretch>
+     <verstretch>0</verstretch>
+    </sizepolicy>
    </property>
    <property name="features">
-    <set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
+    <set>QDockWidget::AllDockWidgetFeatures</set>
    </property>
    <property name="allowedAreas">
     <set>Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea</set>
@@ -45,6 +52,12 @@
       <layout class="QVBoxLayout" name="verticalLayout_right">
        <item>
         <widget class="QGroupBox" name="grp_zoom">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="title">
           <string>Zoom</string>
          </property>
@@ -88,12 +101,25 @@
        </item>
        <item>
         <widget class="QGroupBox" name="grp_display_opts">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="title">
           <string>Display/Calculation Options</string>
          </property>
          <layout class="QVBoxLayout" name="verticalLayout_3">
           <item>
            <layout class="QFormLayout" name="formLayout">
+            <item row="0" column="0" colspan="2">
+             <widget class="QPushButton" name="chooseGradient">
+              <property name="text">
+               <string>Choose Gradient</string>
+              </property>
+             </widget>
+            </item>
             <item row="1" column="0">
              <widget class="QLabel" name="label">
               <property name="text">
@@ -101,14 +127,18 @@
               </property>
              </widget>
             </item>
+            <item row="1" column="1">
+             <widget class="QLineEdit" name="maxIterations">
+              <property name="text">
+               <string>500</string>
+              </property>
+              <property name="maxLength">
+               <number>32</number>
+              </property>
+             </widget>
+            </item>
             <item row="2" column="0" colspan="2">
              <widget class="QCheckBox" name="smooth">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
               <property name="text">
                <string>smooth coloring</string>
               </property>
@@ -119,12 +149,6 @@
             </item>
             <item row="3" column="0" colspan="2">
              <widget class="QCheckBox" name="displayInfo">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
               <property name="text">
                <string>display scale</string>
               </property>
@@ -137,35 +161,6 @@
               </property>
              </widget>
             </item>
-            <item row="1" column="1">
-             <widget class="QLineEdit" name="maxIterations">
-              <property name="minimumSize">
-               <size>
-                <width>70</width>
-                <height>0</height>
-               </size>
-              </property>
-              <property name="text">
-               <string>500</string>
-              </property>
-              <property name="maxLength">
-               <number>32</number>
-              </property>
-             </widget>
-            </item>
-            <item row="0" column="0" colspan="2">
-             <widget class="QPushButton" name="chooseGradient">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="text">
-               <string>Choose Gradient</string>
-              </property>
-             </widget>
-            </item>
            </layout>
           </item>
          </layout>
@@ -186,6 +181,12 @@
        </item>
        <item>
         <widget class="QGroupBox" name="grp_fractal">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="title">
           <string>Fractal</string>
          </property>
@@ -243,35 +244,21 @@
        </item>
        <item>
         <widget class="QGroupBox" name="grp_export_box">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
          <property name="title">
           <string>Export</string>
          </property>
-         <layout class="QVBoxLayout" name="verticalLayout_2">
-          <item>
-           <widget class="QPushButton" name="exportVideo">
-            <property name="text">
-             <string>Export Video</string>
-            </property>
-           </widget>
-          </item>
-          <item>
-           <widget class="QPushButton" name="exportImage">
-            <property name="text">
-             <string>Export Image</string>
-            </property>
-           </widget>
-          </item>
-          <item>
+         <layout class="QGridLayout" name="gridLayout">
+          <item row="2" column="0">
            <widget class="QProgressBar" name="backgroundProgress">
             <property name="enabled">
              <bool>true</bool>
             </property>
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
             <property name="maximum">
              <number>1</number>
             </property>
@@ -282,7 +269,32 @@
              <set>Qt::AlignCenter</set>
             </property>
             <property name="format">
-             <string>Background Tasks</string>
+             <string>Export Progress</string>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="1">
+           <widget class="QPushButton" name="cancelProgress">
+            <property name="text">
+             <string/>
+            </property>
+            <property name="icon">
+             <iconset theme="cancel">
+              <normaloff>.</normaloff>.</iconset>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="0" colspan="2">
+           <widget class="QPushButton" name="exportVideo">
+            <property name="text">
+             <string>Export Video</string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0" colspan="2">
+           <widget class="QPushButton" name="exportImage">
+            <property name="text">
+             <string>Export Image</string>
             </property>
            </widget>
           </item>

+ 229 - 0
AlmondMenuWidget.cpp

@@ -0,0 +1,229 @@
+#include "AlmondMenuWidget.h"
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QResizeEvent>
+#include <QPropertyAnimation>
+#include <QParallelAnimationGroup>
+
+AlmondSubMenu::AlmondSubMenu(QWidget* widget) :
+    w{ widget }
+{
+}
+
+
+QWidget* AlmondSubMenu::widget(void)
+{
+    return w;
+}
+
+
+AlmondMenuWidget::AlmondMenuWidget(QWidget* parent) :
+    QFrame{ parent },
+    mainMenu{ nullptr }
+{
+    rightWidget = new QWidget(this);
+    subMenuContainer = new QStackedWidget(rightWidget);
+    subMenuContainer->setContentsMargins(0, 0, 0, 0);
+    rightOK = new QPushButton("OK", rightWidget);
+    rightCancel = new QPushButton("Cancel", rightWidget);
+
+    leftWidget = new QWidget(this);
+    QVBoxLayout* mainLayout = new QVBoxLayout(leftWidget);
+    mainLayout->setMargin(0);
+    mainLayout->setContentsMargins(0, 0, 0, 0);
+
+    QVBoxLayout* smlayout = new QVBoxLayout(rightWidget);
+    smlayout->addWidget(subMenuContainer, 1);
+    smlayout->addWidget(rightOK);
+    smlayout->addWidget(rightCancel);
+
+    connect(rightOK, &QPushButton::clicked,
+            this, &AlmondMenuWidget::clickedRightOK);
+    connect(rightCancel, &QPushButton::clicked,
+            this, &AlmondMenuWidget::clickedRightCancel);
+
+    //layout->addWidget(rightWidget);
+    //layout->addWidget(leftWidget);
+    rightWidget->setVisible(false);
+
+    /*states = new QStateMachine(this);
+    QState* mainMenuShow = new QState(states);
+    QState* subMenuShow = new QState(states);
+
+    mainMenuShow->assignProperty(leftWidget, "visible", true);
+    mainMenuShow->assignProperty(rightWidget, "visible", false);
+
+    subMenuShow->assignProperty(leftWidget, "visible", false);
+    subMenuShow->assignProperty(leftWidget, "pos", QPoint(100, 100));
+    subMenuShow->assignProperty(rightWidget, "visible", true);
+
+    mainMenuShow->addTransition(this, SIGNAL(showLeft()), subMenuShow);
+    subMenuShow->addTransition(this, SIGNAL(showRight()), mainMenuShow);
+    states->start();*/
+}
+
+
+AlmondMenuWidget::~AlmondMenuWidget(void)
+{
+    for (auto& a : subMenus) {
+        delete a;
+    }
+}
+
+
+void AlmondMenuWidget::setMainMenu(QWidget* mainMenu)
+{
+    //mainMenu->setParent(this);
+    this->mainMenu = mainMenu;
+    leftWidget->layout()->addWidget(mainMenu);
+}
+
+
+AlmondSubMenu* AlmondMenuWidget::addSubMenu(QWidget* subMenu)
+{
+    subMenuContainer->addWidget(subMenu);
+    AlmondSubMenu* almsm = new AlmondSubMenu(subMenu);
+    this->subMenus.append(almsm);
+    return almsm;
+}
+
+
+QSize AlmondMenuWidget::sizeHint(void) const
+{
+    QSize hint{ 0, 0 };
+    hint = leftWidget->sizeHint();
+    for (auto& subMenu : subMenus) {
+        const auto& widget = subMenu->widget();
+        QSize widgetHint = widget->sizeHint();
+        if (hint.width() < widgetHint.width())
+            hint.setWidth(widgetHint.width());
+        if (hint.height() < widgetHint.height())
+            hint.setHeight(widgetHint.height());
+    }
+    return hint;
+}
+
+
+QSize AlmondMenuWidget::minimumSizeHint(void) const
+{
+    QSize hint{ 0, 0 };
+    hint = leftWidget->minimumSizeHint();
+    for (auto& subMenu : subMenus) {
+        const auto& widget = subMenu->widget();
+        QSize widgetHint = widget->minimumSizeHint();
+        if (hint.width() < widgetHint.width())
+            hint.setWidth(widgetHint.width());
+        if (hint.height() < widgetHint.height())
+            hint.setHeight(widgetHint.height());
+    }
+    return hint;
+}
+
+
+void AlmondMenuWidget::resizeEvent(QResizeEvent* event)
+{
+    QRect leftGeom = contentsRect();
+    QRect rightGeom = contentsRect();
+    //leftGeom.setSize(event->size());
+    //rightGeom.setSize(event->size());
+    leftWidget->setGeometry(leftGeom);
+    rightWidget->setGeometry(rightGeom);
+}
+
+
+void AlmondMenuWidget::clickedRightOK(void)
+{
+    int index = subMenuContainer->currentIndex();
+    emit submenuOK(index);
+    emit subMenus.at(index)->accepted();
+}
+
+
+void AlmondMenuWidget::clickedRightCancel(void)
+{
+    int index = subMenuContainer->currentIndex();
+    emit submenuCancel(subMenuContainer->currentIndex());
+    emit subMenus.at(index)->cancelled();
+}
+
+
+void AlmondMenuWidget::showMainMenu(void)
+{
+    emit showLeft();
+    if (!mainMenu)
+        return;
+    QParallelAnimationGroup* ag = new QParallelAnimationGroup(this);
+    QPropertyAnimation* mm = new QPropertyAnimation(leftWidget, "geometry", this);
+    QPropertyAnimation* rw = new QPropertyAnimation(rightWidget, "geometry", this);
+
+    mm->setStartValue(contentsRect().adjusted(-width(), 0, -width(), 0));
+    mm->setEndValue(contentsRect());
+
+    rw->setEndValue(contentsRect().adjusted(width(), 0, width(), 0));
+    rw->setStartValue(contentsRect());
+
+    mm->setEasingCurve(QEasingCurve::InOutSine);
+    rw->setEasingCurve(QEasingCurve::InOutSine);
+    mm->setDuration(200);
+    rw->setDuration(200);
+
+    ag->addAnimation(mm);
+    ag->addAnimation(rw);
+
+    connect(ag, &QParallelAnimationGroup::finished, [this] () {
+        emit rightWidget->hide();
+        leftWidget->setGeometry(contentsRect());
+    });
+
+    leftWidget->setGeometry(rightWidget->geometry().adjusted(-width(), 0, -width(), 0));
+    emit leftWidget->show();
+    emit ag->start(QAbstractAnimation::DeleteWhenStopped);
+}
+
+
+void AlmondMenuWidget::showSubMenu(int index)
+{
+    subMenuContainer->setCurrentIndex(index);
+    /*while (subMenuContainer->layout()->count() > 0)
+        subMenuContainer->layout()->takeAt(0);
+    subMenuContainer->layout()->update();
+    rightWidget->setGeometry(leftWidget->geometry().adjusted(width(), 0, width(), 0));
+    //subMenuContainer->layout()->addWidget(subMenus.at(index));
+    subMenuContainer->layout()->update();*/
+
+    showSubMenu();
+}
+
+
+void AlmondMenuWidget::showSubMenu(void)
+{
+    emit showRight();
+    if (!mainMenu)
+    return;
+    QParallelAnimationGroup* ag = new QParallelAnimationGroup(this);
+    QPropertyAnimation* mm = new QPropertyAnimation(leftWidget, "geometry", this);
+    QPropertyAnimation* rw = new QPropertyAnimation(rightWidget, "geometry", this);
+
+    mm->setStartValue(contentsRect());
+    mm->setEndValue(contentsRect().adjusted(-width(), 0, -width(), 0));
+
+    rw->setStartValue(contentsRect().adjusted(width(), 0, width(), 0));
+    rw->setEndValue(contentsRect());
+
+    mm->setEasingCurve(QEasingCurve::InOutSine);
+    rw->setEasingCurve(QEasingCurve::InOutSine);
+    mm->setDuration(200);
+    rw->setDuration(200);
+
+    ag->addAnimation(mm);
+    ag->addAnimation(rw);
+
+    connect(ag, &QParallelAnimationGroup::finished, [this] () {
+        leftWidget->hide();
+        rightWidget->setGeometry(contentsRect());
+    });
+
+    rightWidget->setGeometry(mainMenu->geometry().adjusted(width(), 0, width(), 0));
+    emit rightWidget->setVisible(true);
+    emit ag->start(QAbstractAnimation::DeleteWhenStopped);
+}

+ 67 - 0
AlmondMenuWidget.h

@@ -0,0 +1,67 @@
+#ifndef ALMONDMENUWIDGET_H
+#define ALMONDMENUWIDGET_H
+
+#include <QFrame>
+#include <QList>
+#include <QStackedWidget>
+#include <QPushButton>
+#include <QStateMachine>
+
+class AlmondSubMenu :
+    public QObject
+{
+    Q_OBJECT
+
+    QWidget* w;
+public:
+    AlmondSubMenu(QWidget* widget);
+    QWidget* widget(void);
+
+signals:
+    void accepted(void);
+    void cancelled(void);
+};
+
+class AlmondMenuWidget :
+    public QFrame
+{
+    Q_OBJECT
+        
+    QWidget* mainMenu;
+    QWidget* rightWidget;
+    QWidget* leftWidget;
+    QPushButton* rightOK;
+    QPushButton* rightCancel;
+    QStackedWidget* subMenuContainer;
+    QList<AlmondSubMenu*> subMenus;
+    QStateMachine* states;
+public:
+    AlmondMenuWidget(QWidget* parent = nullptr);
+    ~AlmondMenuWidget(void);
+
+    void setMainMenu(QWidget* mainMenu);
+    AlmondSubMenu* addSubMenu(QWidget* subMenu);
+
+    virtual QSize sizeHint(void) const override;
+    virtual QSize minimumSizeHint(void) const override;
+protected:
+    virtual void resizeEvent(QResizeEvent* event) override;
+
+protected slots:
+    void clickedRightOK(void);
+    void clickedRightCancel(void);
+
+public slots:
+    void showMainMenu(void);
+    void showSubMenu(int index);
+    void showSubMenu(void);
+
+signals:
+    void submenuOK(int index);
+    void submenuCancel(int index);
+
+    void showLeft();
+    void showRight();
+};
+
+#endif // ALMONDMENUWIDGET_H

+ 15 - 8
BackgroundTask.cpp

@@ -1,13 +1,14 @@
 #include "BackgroundTask.h"
 
-BackgroundTask::BackgroundTask(const std::string& shortDescription) :
-    shortDescription{ shortDescription }
+BackgroundTask::BackgroundTask(const std::string& shortDescription, std::function<bool(void)> stopCallback) :
+    shortDescription{ shortDescription },
+    stopCallback{ std::move(stopCallback) }
 {
 }
 
 
-ImageExportTask::ImageExportTask(const alm::ImageExportInfo& iei) :
-    BackgroundTask{ "Exporting Image" },
+ImageExportTask::ImageExportTask(const alm::ImageExportInfo& iei, std::function<bool(void)> stopCallback) :
+    BackgroundTask{ "Exporting Image", std::move(stopCallback) },
     iei{ iei }
 {
 }
@@ -16,10 +17,13 @@ ImageExportTask::ImageExportTask(const alm::ImageExportInfo& iei) :
 void ImageExportTask::run(void)
 {
     try {
-        alm::exportPng(iei, [this](float percentage) {
+        alm::exportImage(iei, [this](float percentage) {
             emit progress(percentage);
-        });
-        emit finished(true, "Image successfully exported.");
+        }, stopCallback);
+        if (!stopCallback())
+            emit finished(true, "Image successfully exported.");
+        else
+            emit finished(false, "Image export cancelled.");
     }
     catch (alm::ImageExportException& ex) {
         emit finished(false, QString("Error during image export: ") + ex.what());
@@ -45,7 +49,10 @@ void VideoExportTask::run(void)
             emit progress(mvpi.progress);
         });
         mvg.generate(generator);
-        emit finished(true, "Video successfully exported.");
+        if (!stopCallback())
+            emit finished(true, "Video successfully exported.");
+        else
+            emit finished(false, "Video export cancelled.");
     }
     catch (alm::VideoExportException& ex) {
         emit finished(false, QString("Error during video export: ") + ex.what());

+ 3 - 2
BackgroundTask.h

@@ -13,8 +13,9 @@ class BackgroundTask : public QObject, public QRunnable
     Q_OBJECT
 protected:
     std::string shortDescription;
+    std::function<bool(void)> stopCallback;
 public:
-    BackgroundTask(const std::string& shortDescription);
+    BackgroundTask(const std::string& shortDescription, std::function<bool(void)> stopCallback = [] () { return false; });
 
     void run(void) = 0;
 
@@ -32,7 +33,7 @@ class ImageExportTask : public BackgroundTask
 private:
     const alm::ImageExportInfo iei;
 public:
-    ImageExportTask(const alm::ImageExportInfo& iei);
+    ImageExportTask(const alm::ImageExportInfo& iei, std::function<bool(void)> stopCallback = [] () { return false; });
 
     void run(void) override;
 };

+ 13 - 4
CMakeLists.txt

@@ -24,11 +24,13 @@ FILE(GLOB AlmondHeaders *.h)
 
 
 IF (WIN32)
-    add_executable(Almond WIN32 ${AlmondSources} Almond.qrc splash.qrc icon.rc)
+    add_executable(Almond WIN32 ${AlmondSources} Almond.qrc splash.qrc icon.rc ${AlmondHeaders})
 ELSE()
-    add_executable(Almond ${AlmondSources} Almond.qrc splash.qrc)
+    add_executable(Almond ${AlmondSources} Almond.qrc splash.qrc ${AlmondHeaders})
 ENDIF()
 
+target_include_directories(Almond PUBLIC .)
+
 add_subdirectory(libalmond)
 
 target_include_directories(Almond SYSTEM PUBLIC ${FFMPEG_INCLUDE_DIRS})
@@ -69,11 +71,18 @@ IF (WIN32)
 
 ELSEIF (UNIX AND NOT APPLE)
     install(TARGETS Almond RUNTIME DESTINATION "bin")
+    install(FILES Almond.desktop DESTINATION "share/applications")
+    install(FILES Almond.png DESTINATION "share/pixmaps")
+    install(FILES debian/copyright DESTINATION "share/doc/almond/" RENAME "copyright")
     set(CPACK_GENERATOR "DEB")
+    set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_SOURCE_DIR}/LICENSE)
+    set(CPACK_STRIP_FILES TRUE)
     set(CPACK_SOURCE_GENERATOR "DEB")
     set(CPACK_COMPONENTS_ALL Almond)
-    set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Nicolas Winkler")
-    set(CPACK_DEBIAN_PACKAGE_DEPENDS "qt5-default,libavformat58,libavdevice58,libavfilter7,libavutil56,libswscale5,libgl1,ocl-icd-libopencl1,libpng16-16")
+    set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "A fractal viewer")
+    set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE amd64)
+    set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Nicolas Winkler <nicolas.winkler@gmx.ch>")
+    set(CPACK_DEBIAN_PACKAGE_DEPENDS "qtbase5-dev,libavformat58,libavdevice58,libavfilter7,libavutil56,libswscale5,ocl-icd-libopencl1,libpng16-16,libc6")
     set(CPACK_SET_DESTDIR True)
     set(CPACK_INSTALL_PREFIX "/usr")
     include(CPack)

+ 0 - 66
CubicSpline.cpp

@@ -1,66 +0,0 @@
-#include "CubicSpline.h"
-
-CubicSpline::CubicSpline(const std::vector<std::pair<float, float> >& dataPoints, bool useSlopes) :
-    useSlopes{ useSlopes }
-{
-    if (dataPoints.size() < 2) {
-        return;
-    }
-
-    points.push_back({ dataPoints[0].first, dataPoints[0].second,
-                       (dataPoints[1].second - dataPoints[0].second) /
-                       (dataPoints[1].first - dataPoints[0].first) });
-    for (size_t i = 1; i < dataPoints.size() - 1; i++) {
-
-        auto& dp1 = dataPoints[i - 1];
-        auto& dp2 = dataPoints[i];
-        auto& dp3 = dataPoints[i + 1];
-
-        float w1 = dp2.first - dp1.first;
-        float w2 = dp3.first - dp2.first;
-        float h1 = dp2.second - dp1.second;
-        float h2 = dp3.second - dp2.second;
-
-        float s1 = h1 / w1;
-        float s2 = h2 / w2;
-
-        float avgSlope = (s1 + s2) / 2;
-        points.push_back({ dp2.first, dp2.second, avgSlope });
-    }
-    points.push_back({ dataPoints[dataPoints.size() - 1].first, dataPoints[dataPoints.size() - 1].second,
-                       (dataPoints[dataPoints.size() - 2].second - dataPoints[dataPoints.size() - 1].second) /
-                       (dataPoints[dataPoints.size() - 2].first - dataPoints[dataPoints.size() - 1].first) });
-}
-
-
-float CubicSpline::interpolateAt(float x)
-{
-    const static auto h00 = [] (float t) { return (1 + 2 * t) * (1 - t) * (1 - t); };
-    const static auto h01 = [] (float t) { return t * t * (3 - 2 * t); };
-    const static auto h10 = [] (float t) { return t * (1 - t) * (1 - t); };
-    const static auto h11 = [] (float t) { return t * t * (t - 1); };
-    for (auto it = points.begin(); it != points.end() && (it + 1) != points.end(); ++it) {
-        auto& left = *it;
-        auto& right = *(it + 1);
-        float xleft = std::get<0>(left);
-        float xright = std::get<0>(right);
-        if (xleft < x && xright >= x) {
-            float w = (xright - xleft);
-            float t = (x - xleft) / w;
-            float yleft = std::get<1>(left);
-            float yright = std::get<1>(right);
-            float sleft = std::get<2>(left);
-            float sright = std::get<2>(right);
-
-            float inter = h00(t) * yleft +
-                          h01(t) * yright;
-
-            if (useSlopes)
-                inter += h10(t) * w * sleft +
-                         h11(t) * w * sright;
-
-            return inter;
-        }
-    }
-    return std::get<1>(points[points.size() - 1]);
-}

+ 0 - 19
CubicSpline.h

@@ -1,19 +0,0 @@
-#ifndef CUBICSPLINE_H
-#define CUBICSPLINE_H
-
-#include <vector>
-#include <utility>
-#include <tuple>
-
-class CubicSpline
-{
-    /// contains x, y and y' of each interpolation point
-    std::vector<std::tuple<float, float, float>> points;
-    bool useSlopes;
-public:
-    CubicSpline(const std::vector<std::pair<float, float>>& dataPoints, bool useSlopes);
-
-    float interpolateAt(float x);
-};
-
-#endif // CUBICSPLINE_H

+ 547 - 0
EscapeTimeVisualWidget.cpp

@@ -0,0 +1,547 @@
+#include "EscapeTimeVisualWidget.h"
+
+#include "Bitmap.h"
+
+#include <QOpenGLShaderProgram>
+#include <QOpenGLContext>
+#include <QOpenGLFunctions>
+#include <QOpenGLExtraFunctions>
+#include <QOpenGLFunctions_3_0>
+#include <QOpenGLFunctions_4_0_Core>
+
+#include <vector>
+
+
+ETVImage::ETVImage(EscapeTimeVisualWidget& owner,
+                   const Bitmap<float>& img) :
+    owner{ owner }
+{
+    auto& gl = *owner.context()->functions();
+    gl.glGenTextures(1, &textureId);
+    gl.glActiveTexture(GL_TEXTURE0);
+    gl.glBindTexture(GL_TEXTURE_2D, textureId);
+
+    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, int(img.width), int(img.height), 0, GL_RED, GL_FLOAT, img.pixels.get());
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    gl.glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+
+ETVImage::~ETVImage(void)
+{
+    auto& gl = *owner.context()->functions();
+    gl.glDeleteTextures(1, &textureId);
+}
+
+
+void ETVImage::draw(float x, float y, float w, float h,
+                    float tx, float ty, float tw, float th)
+{
+    auto& gl = *owner.context()->functions();
+    auto& gle = *owner.context()->extraFunctions();
+
+    GLfloat const fbVertices[] = {
+        0, 0,  0.0f,
+        0, 256, 0.0f,
+        256, 0, 0.0f,
+        256, 256, 0.0f,
+    };
+
+    GLfloat const vertices[] = {
+        x, y,  0.0f,
+        x, y + h, 0.0f,
+        x + w, y, 0.0f,
+        x + w, y + h, 0.0f,
+    };
+
+    GLfloat const texCoords[] = {
+        tx,      ty,
+        tx,      ty + th,
+        tx + tw, ty,
+        tx + tw, ty + th,
+    };
+
+    GLfloat const fullTexCoords[] = {
+        0, 0,
+        0, 1,
+        1, 0,
+        1, 1,
+    };
+
+    QColor color{ 255, 255, 255 };
+    auto& program = owner.program;
+    program->bind();
+    int vertexLoc = program->attributeLocation("vertex");
+    int texCoordsLoc = program->attributeLocation("texCoord");
+    int colorLocation = program->uniformLocation("color");
+    int texLoc = program->uniformLocation("tex");
+    int gradLoc = program->uniformLocation("gradient");
+    int gradientScaler = program->uniformLocation("gradientScaler");
+    int maxIterations = program->uniformLocation("maxIterations");
+    program->setAttributeArray(vertexLoc, vertices, 3);
+    program->setAttributeArray(texCoordsLoc, texCoords, 2);
+    program->enableAttributeArray(vertexLoc);
+    program->enableAttributeArray(texCoordsLoc);
+    program->setUniformValue(colorLocation, color);
+    program->setUniformValue(gradientScaler, 1.0f / float(owner.gradientTextureMax));
+    program->setUniformValue(maxIterations, float(owner.maxIterations));
+
+
+    QMatrix4x4 pmvMatrix;
+    pmvMatrix.ortho(QRect{ 0, 0, owner.getResolutionX(), owner.getResolutionY() });
+    int matrixLocation = program->uniformLocation("matrix");
+    program->setUniformValue(matrixLocation, pmvMatrix);
+
+    gl.glEnable(GL_TEXTURE_2D);
+
+    //GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0 };
+    //gle.glDrawBuffers(1, drawBuffers);
+    gl.glBindFramebuffer(GL_FRAMEBUFFER, owner.tileFramebuffer);
+    gl.glDisable(GL_DEPTH_TEST);
+    if(gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+        printf("error intitializing framebuffer\n");
+    }
+    gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, owner.tileTexture, 0);
+    //gl.glViewport(0, 0, 256, 256);
+
+    gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    gl.glUniform1i(texLoc, 0);
+    gl.glUniform1i(gradLoc, 2);
+
+    gl.glActiveTexture(GL_TEXTURE0);
+    gl.glBindTexture(GL_TEXTURE_2D, textureId);
+    gl.glActiveTexture(GL_TEXTURE2);
+    gl.glBindTexture(GL_TEXTURE_2D, owner.gradientTextureId);
+
+    gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+    program->disableAttributeArray(vertexLoc);
+    program->disableAttributeArray(texCoordsLoc);
+
+    /*owner.renderTextures->bind();
+    gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    //gl.glViewport(0, 0, owner.getResolutionX(), owner.getResolutionY());
+    int rtVertexLoc = owner.renderTextures->attributeLocation("vertex");
+    int rtTexCoordsLoc = owner.renderTextures->attributeLocation("texCoord");
+    int rtTexLoc = owner.renderTextures->attributeLocation("tex");
+
+    gl.glActiveTexture(GL_TEXTURE0);
+    gl.glUniform1i(rtTexLoc, 0);
+    owner.renderTextures->setAttributeArray(rtVertexLoc, vertices, 3);
+    owner.renderTextures->setAttributeArray(rtTexCoordsLoc, fullTexCoords, 2);
+    owner.renderTextures->enableAttributeArray(rtVertexLoc);
+    owner.renderTextures->enableAttributeArray(rtTexCoordsLoc);
+    gl.glBindTexture(GL_TEXTURE_2D, owner.tileTexture);
+    //if (rand() % 2)
+    //gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+    owner.renderTextures->disableAttributeArray(rtVertexLoc);
+    owner.renderTextures->disableAttributeArray(rtTexCoordsLoc);
+    */
+    gl.glActiveTexture(GL_TEXTURE0);
+}
+
+
+EscapeTimeVisualWidget::EscapeTimeVisualWidget(QWidget* parent) :
+    QOpenGLWidget{ parent },
+    gradientTextureId{ 0 },
+    gradientTextureMax{ 1.0f },
+    gradientNeedsUpdate{ false }
+{
+}
+
+
+void EscapeTimeVisualWidget::setGradient(Gradient newGradient)
+{
+    this->gradient = newGradient;
+    gradientNeedsUpdate = true;
+    update();
+}
+
+
+const Gradient& EscapeTimeVisualWidget::getGradient(void)
+{
+    return gradient;
+}
+
+
+void EscapeTimeVisualWidget::initializeGL(void)
+{
+    auto& gl = *this->context()->functions();
+    gl.glClearColor(0, 0, 0, 0);
+
+    gl.glDisable(GL_DEPTH_TEST);
+
+    fprintf(stdout, "version: %s\n", gl.glGetString(GL_VERSION));
+    fflush(stdout);
+
+    // looks not even better
+    //gl.glEnable(GL_FRAMEBUFFER_SRGB);
+
+    //glShadeModel(GL_SMOOTH);
+
+    renderTextures = new QOpenGLShaderProgram{ this->context() };
+    renderTextures->addShaderFromSourceCode(QOpenGLShader::Vertex,
+        "attribute highp vec4 vertex;\n"
+        "attribute highp vec2 texCoord;\n"
+        "uniform highp mat4 matrix;\n"
+        "varying highp vec2 texc;\n"
+        "void main(void)\n"
+        "{\n"
+        "   gl_Position = matrix * vertex;\n"
+        "   texc = texCoord;\n"
+        "}");
+    renderTextures->addShaderFromSourceCode(QOpenGLShader::Fragment,
+//        "#version 110\n"
+        "uniform sampler2D tex;\n"
+        "varying highp vec2 texc;\n"
+        "void main(void)\n"
+        "{\n"
+        "    gl_FragColor = texture2D(tex, texc);\n"
+        "}");
+
+    renderTextures->link();
+
+    juliaPreviewer = new QOpenGLShaderProgram{ this->context() };
+    juliaPreviewer->addShaderFromSourceCode(QOpenGLShader::Vertex,
+        "attribute highp vec4 vertex;\n"
+        "attribute highp vec2 texCoord;\n"
+        "uniform highp mat4 matrix;\n"
+        "varying highp vec2 texc;\n"
+        "void main(void)\n"
+        "{\n"
+        "   gl_Position = matrix * vertex;\n"
+        "   texc = texCoord;\n"
+        "}");
+    juliaPreviewer->addShaderFromSourceCode(QOpenGLShader::Fragment,
+//    "#version 110\n"
+    "uniform sampler2D gradient;\n"
+    "uniform highp float gradientScaler;\n"
+    "const highp float maxIterations = 350.0;\n"
+    "varying highp vec2 texc;\n"
+    "uniform highp float juliaX;\n"
+    "uniform highp float juliaY;\n"
+    "const highp float left = -1.5;\n"
+    "const highp float right = 1.5;\n"
+    "const highp float top = -1.5;\n"
+    "const highp float bottom = 1.5;\n"
+    "highp float map(highp float a, highp float b, highp float v) {\n"
+    "    return (1.0 - v) * a + b * v;\n"
+    "}\n"
+    "highp float iterate(highp float x, highp float y, highp float ca, highp float cb) {\n"
+    "    int k = 0;\n"
+    "    highp float a = x;\n"
+    "    highp float b = y;\n"
+    "    while(k <= int(maxIterations)) {\n"
+    "        highp float aa = a * a;\n"
+    "        highp float bb = b * b;\n"
+    "        highp float abab = 2.0 * a * b;\n"
+    "        a = aa - bb + ca;\n"
+    "        b = abab + cb;\n"
+    "        if (aa + bb >= 16.0) break;\n"
+    "        k = k + 1;\n"
+    "    }\n"
+    "    return float(k) + 1 - log2(log(a * a + b * b) * 0.5);\n"
+    "}\n"
+    "void main(void)\n"
+    "{\n"
+    "    highp float x = map(left, right, texc.x);\n"
+    "    highp float y = map(top, bottom, texc.y);\n"
+    "    highp float v = iterate(x, y, juliaX, juliaY);\n"
+    "    if (v >= maxIterations) {\n"
+    "        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
+    "    } else {\n"
+    "        highp float vnorm = v * gradientScaler;\n"
+    "        gl_FragColor = texture2D(gradient, vec2(vnorm, 0.0));\n"
+    "    }\n"
+    //"    gl_FragColor = vec4(vnorm, 0.0, 0.0, 0.0);\n"
+    "}");
+    juliaPreviewer->link();
+
+    program = new QOpenGLShaderProgram{ this->context() };
+    bool vert = program->addShaderFromSourceCode(QOpenGLShader::Vertex,
+    "attribute highp vec4 vertex;\n"
+    "attribute highp vec2 texCoord;\n"
+    "uniform highp mat4 matrix;\n"
+    "varying highp vec2 texc;\n"
+    "void main(void)\n"
+    "{\n"
+    "   gl_Position = matrix * vertex;\n"
+    "   texc = texCoord;\n"
+    "}");
+
+    // TODO rewrite this monster
+    if (!context()->isOpenGLES() &&
+        context()->versionFunctions<QOpenGLFunctions_4_0_Core>() != nullptr) {
+        bool frag = program->addShaderFromSourceCode(QOpenGLShader::Fragment,
+        "#version 400\n"
+        "uniform sampler2D gradient;\n"
+        "uniform sampler2D tex;\n"
+        "uniform mediump vec4 color;\n"
+        "uniform highp float gradientScaler;\n"
+        "uniform highp float maxIterations;\n"
+        "varying highp vec2 texc;\n"
+        "vec4 colorize(float pos) {\n"
+        "    if (pos >= maxIterations) {\n"
+        "        return vec4(0.0, 0.0, 0.0, 1.0);\n"
+        "    } else {\n"
+        "        return texture2D(gradient, vec2(pos * gradientScaler, 0.0));\n"
+        "    }\n"
+        "}\n"
+        "void main(void)\n"
+        "{\n"
+        "   vec2 size = textureSize(tex, 0);\n"
+        "   size = vec2(256.0, 256.0);\n"
+        "   vec2 accPoint = texc * size;\n"
+        "   vec2 ip = floor(accPoint);\n"
+        "   vec2 fp = fract(accPoint);\n"
+        "   vec4 inter = textureGather(tex, ip / size, 0);\n"
+        "   vec4 col1 = colorize(inter.x);\n"
+        "   vec4 col2 = colorize(inter.y);\n"
+        "   vec4 col3 = colorize(inter.z);\n"
+        "   vec4 col4 = colorize(inter.w);\n"
+        "   vec4 col = mix(mix(col4, col3, fp.x), mix(col1, col2, fp.x), fp.y);\n"
+        "   gl_FragColor = col;\n"
+    //    "   gl_FragColor = gl_FragColor * texture2D(tex, texc);\n"
+    //    "   float v = texture2D(tex, texc).r;\n"
+    //    "   gl_FragColor = vec4(v, 1.0 - v, v*v, 1);\n"
+    //    "   gl_FragColor.g = 0.3;\n"
+        "}");
+    }
+    else {
+        bool frag = program->addShaderFromSourceCode(QOpenGLShader::Fragment,
+//        "#version 110\n"
+        "uniform sampler2D gradient;\n"
+        "uniform sampler2D tex;\n"
+        "uniform mediump vec4 color;\n"
+        "uniform highp float gradientScaler;\n"
+        "uniform highp float maxIterations;\n"
+        "varying highp vec2 texc;\n"
+        "void main(void)\n"
+        "{\n"
+        "   highp float v = texture2D(tex, texc).r;\n"
+        "   if (v >= maxIterations) {\n"
+        "       gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
+        "   } else {\n"
+        "       gl_FragColor = texture2D(gradient, vec2(v * gradientScaler, 0.0));\n"
+        "   }\n"
+    //    "   gl_FragColor = gl_FragColor * texture2D(tex, texc);\n"
+    //    "   float v = texture2D(tex, texc).r;\n"
+    //    "   gl_FragColor = vec4(v, 1.0 - v, v*v, 1);\n"
+    //    "   gl_FragColor.g = 0.3;\n"
+        "}");
+    }
+
+    //program.link();
+    bool bound = program->bind();
+    bound = renderTextures->bind();
+
+    int vertexLoc = program->attributeLocation("vertex");
+    int texCoordsLoc = program->attributeLocation("texCoord");
+    int colorLocation = program->uniformLocation("color");
+    int texLoc = program->uniformLocation("tex");
+    int gradLoc = program->uniformLocation("gradient");
+    int gradientScaler = program->uniformLocation("gradientScaler");
+    int maxIterations = program->uniformLocation("maxIterations");
+    program->setUniformValue(gradientScaler, 0.005f);
+    program->setUniformValue(maxIterations, 250.0f);
+
+    auto& gle = *this->context()->extraFunctions();
+
+    gl.glGenTextures(1, &tileTexture);
+    gl.glBindTexture(GL_TEXTURE_2D, tileTexture);
+    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    gl.glBindTexture(GL_TEXTURE_2D, 0);
+
+    gl.glGenFramebuffers(1, &tileFramebuffer);
+    gl.glBindFramebuffer(GL_FRAMEBUFFER, tileFramebuffer);
+    gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tileTexture, 0);
+    GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0 };
+    gle.glDrawBuffers(1, drawBuffers);
+    if(gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+        printf("error intitializing framebuffer\n");
+    }
+    gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+    unsigned char pix[] = { 255, 0, 0, 0, 255, 0, 0, 0, 255 };
+
+    GLuint id;
+    gl.glEnable(GL_TEXTURE_2D);
+    gl.glGenTextures(1, &id);
+    gl.glBindTexture(GL_TEXTURE_2D, id);
+
+    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 3, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, reinterpret_cast<char*> (pix));
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    gl.glBindTexture(GL_TEXTURE_2D, 0);
+
+    gradientTextureId = id;
+
+    gl.glDisable(GL_DEPTH_TEST);
+    //gl3.glBindSampler(0, id);
+}
+
+
+void EscapeTimeVisualWidget::resizeGL(int w, int h)
+{
+    auto& gl = *this->context()->functions();
+    float pixelRatio = this->devicePixelRatioF();
+    //pixelRatio = 1.0 / 32;
+
+    float newW = w * pixelRatio;
+    float newH = h * pixelRatio;
+
+    setResolutionX(newW);
+    setResolutionY(newH);
+
+    gl.glViewport(0, 0, newW, newH);
+
+    QMatrix4x4 pmvMatrix;
+    pmvMatrix.ortho(QRectF{ 0, 0, newW, newH });
+    int matrixLocation = program->uniformLocation("matrix");
+    int rtMatrixLocation = renderTextures->uniformLocation("matrix");
+    int jMatrixLocation = juliaPreviewer->uniformLocation("matrix");
+    program->setUniformValue(matrixLocation, pmvMatrix);
+    renderTextures->setUniformValue(rtMatrixLocation, pmvMatrix);
+    juliaPreviewer->setUniformValue(jMatrixLocation, pmvMatrix);
+}
+
+
+void EscapeTimeVisualWidget::paintGL(void)
+{
+    if (gradientNeedsUpdate)
+        updateGradient();
+
+    /*ETVImage etvi{ *this };
+
+    auto& gl = *this->context()->functions();
+    gl.glClearColor(0.0, 0.2, 0.0, 1.0);
+    gl.glClear(GL_COLOR_BUFFER_BIT);
+
+    etvi.draw(100, 100, 700, 700);*/
+}
+
+
+void EscapeTimeVisualWidget::drawJulia(float jx, float jy, QRectF area)
+{
+    juliaPreviewer->bind();
+    int gradLoc = juliaPreviewer->uniformLocation("gradient");
+    int gradientScaler = juliaPreviewer->uniformLocation("gradientScaler");
+    int juliaX = juliaPreviewer->uniformLocation("juliaX");
+    int juliaY = juliaPreviewer->uniformLocation("juliaY");
+    int vertexLoc = juliaPreviewer->attributeLocation("vertex");
+    int texCoordsLoc = juliaPreviewer->attributeLocation("texCoord");
+    int maxIterLoc = juliaPreviewer->attributeLocation("maxIterations");
+
+    const float x = area.x();
+    const float y = area.y();
+    const float w = area.width();
+    const float h = area.height();
+    GLfloat const vertices[] = {
+        x, y,  0.0f,
+        x, y + h, 0.0f,
+        x + w, y, 0.0f,
+        x + w, y + h, 0.0f,
+    };
+
+    GLfloat const texCoords[] = {
+        0, 0,
+        0, 1,
+        1, 0,
+        1, 1,
+    };
+
+    auto& gl = *this->context()->functions();
+    gl.glEnable(GL_TEXTURE_2D);
+    gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    juliaPreviewer->setAttributeArray(vertexLoc, vertices, 3);
+    juliaPreviewer->setAttributeArray(texCoordsLoc, texCoords, 2);
+    juliaPreviewer->enableAttributeArray(vertexLoc);
+    juliaPreviewer->enableAttributeArray(texCoordsLoc);
+    juliaPreviewer->setUniformValue(gradientScaler, 1.0f / float(gradientTextureMax));
+    juliaPreviewer->setUniformValue(maxIterLoc, float(250));
+    juliaPreviewer->setUniformValue(juliaX, float(jx));
+    juliaPreviewer->setUniformValue(juliaY, float(jy));
+
+    gl.glUniform1i(gradLoc, 0);
+
+    gl.glActiveTexture(GL_TEXTURE0);
+    gl.glBindTexture(GL_TEXTURE_2D, gradientTextureId);
+
+    gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+    juliaPreviewer->disableAttributeArray(vertexLoc);
+    juliaPreviewer->disableAttributeArray(texCoordsLoc);
+    //juliaPreviewer->release();
+    //program->bind();
+}
+
+
+void EscapeTimeVisualWidget::setMaxIterationCutoff(float maxIter)
+{
+    this->maxIterations = maxIter;
+}
+
+
+void EscapeTimeVisualWidget::updateGradient(void)
+{
+    auto& gl = *this->context()->functions();
+
+    int len = 512;
+    if (gradient.getPoints().size() > 25) {
+        len = 2048;
+    }
+    else if (gradient.getPoints().size() > 7) {
+        len = 1024;
+    }
+    std::unique_ptr<uint8_t[]> pixels = std::make_unique<uint8_t[]>(len * 3);
+
+    for (int i = 0; i < len; i++) {
+        RGBColor c = gradient.get(gradient.getMax() * i / len);
+        pixels[i * 3] = c.r;
+        pixels[i * 3 + 1] = c.g;
+        pixels[i * 3 + 2] = c.b;
+    }
+
+    gl.glEnable(GL_TEXTURE_2D);
+    gl.glBindTexture(GL_TEXTURE_2D, gradientTextureId);
+
+    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, len, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, reinterpret_cast<unsigned char*> (pixels.get()));
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    gl.glBindTexture(GL_TEXTURE_2D, 0);
+    this->gradientTextureMax = gradient.getMax();
+
+    gradientNeedsUpdate = false;
+}
+
+
+void EscapeTimeVisualWidget::setResolutionX(int w)
+{
+    resolutionX = w;
+}
+
+
+void EscapeTimeVisualWidget::setResolutionY(int h)
+{
+    resolutionY = h;
+}
+
+
+int EscapeTimeVisualWidget::getResolutionX(void) const
+{
+    return resolutionX;
+}
+
+
+int EscapeTimeVisualWidget::getResolutionY(void) const
+{
+    return resolutionY;
+}

+ 72 - 0
EscapeTimeVisualWidget.h

@@ -0,0 +1,72 @@
+#ifndef ESCAPETIMEVISUALWIDGET_H
+#define ESCAPETIMEVISUALWIDGET_H
+
+#include <QOpenGLWidget>
+#include "Bitmap.h"
+#include "Gradient.h"
+
+class QOpenGLShaderProgram;
+
+class EscapeTimeVisualWidget;
+
+
+class ETVImage
+{
+    GLuint textureId;
+    EscapeTimeVisualWidget& owner;
+public:
+    ETVImage(EscapeTimeVisualWidget& owner,
+             const Bitmap<float>& img);
+    ~ETVImage(void);
+
+    void draw(float x, float y, float w, float h,
+              float tx = 0.0f, float ty = 0.0f,
+              float tw = 1.0f, float th = 1.0f);
+};
+
+
+class EscapeTimeVisualWidget :
+    public QOpenGLWidget
+{
+    Q_OBJECT
+
+    friend class ETVImage;
+protected:
+    QOpenGLShaderProgram* program;
+    QOpenGLShaderProgram* renderTextures;
+    QOpenGLShaderProgram* juliaPreviewer;
+    GLuint gradientTextureId;
+    float gradientTextureMax;
+    float maxIterations;
+    Gradient gradient;
+    bool gradientNeedsUpdate;
+
+    float resolutionX;
+    float resolutionY;
+
+    GLuint tileFramebuffer;
+    GLuint tileTexture;
+
+public:
+    EscapeTimeVisualWidget(QWidget* parent = nullptr);
+
+    void setGradient(Gradient newGradient);
+    const Gradient& getGradient(void);
+
+    virtual void initializeGL(void) override;
+    virtual void resizeGL(int w, int h) override;
+    virtual void paintGL(void) override;
+    void drawJulia(float jx, float jy, QRectF area);
+
+    void setMaxIterationCutoff(float maxIter);
+
+
+    void setResolutionX(int w);
+    void setResolutionY(int h);
+    int getResolutionX(void) const;
+    int getResolutionY(void) const;
+private:
+    void updateGradient(void);
+};
+
+#endif // ESCAPETIMEVISUALWIDGET_H

+ 71 - 0
ExportImageMenu.cpp

@@ -0,0 +1,71 @@
+#include "ExportImageMenu.h"
+#include "ui_ExportImageMenu.h"
+
+#include "ImageExport.h"
+
+#include <QIntValidator>
+#include <QFileDialog>
+
+ExportImageMenu::ExportImageMenu(QWidget *parent) :
+    QWidget(parent),
+    ui(new Ui::ExportImageMenu)
+{
+    ui->setupUi(this);
+    ui->maxIterTxt->setValidator(new QIntValidator(1, 1000000000, this));
+    ui->widthTxt->setValidator(new QIntValidator(1, 10000000, this));
+    ui->heightTxt->setValidator(new QIntValidator(1, 10000000, this));
+}
+
+
+ExportImageMenu::~ExportImageMenu()
+{
+    delete ui;
+}
+
+
+int ExportImageMenu::getMaxIterations(void) const
+{
+    return ui->maxIterTxt->text().toInt();
+}
+
+
+int ExportImageMenu::getWidth(void) const
+{
+    return ui->widthTxt->text().toInt();
+}
+
+
+int ExportImageMenu::getHeight(void) const
+{
+    return ui->heightTxt->text().toInt();
+}
+
+
+QString ExportImageMenu::getPath(void) const
+{
+    return ui->pathTxt->text();
+}
+
+
+void ExportImageMenu::on_pathBtn_clicked()
+{
+    std::string formatString = "";
+    if (alm::supportsImageFormat(alm::ImageFormat::PNG)) {
+        formatString += "PNG image (*.png);;";
+    }
+    if (alm::supportsImageFormat(alm::ImageFormat::JPEG)) {
+        formatString += "JPEG image (*.jpg *.jpeg);;";
+    }
+    if (alm::supportsImageFormat(alm::ImageFormat::BMP)) {
+        formatString += "BMP image (*.bmp *.dib);;";
+    }
+    if (!formatString.empty()) {
+        formatString.erase(formatString.end() - 2, formatString.end());
+    }
+
+    QString saveAs = QFileDialog::getSaveFileName(this,
+            tr("Save exported image"), "",
+            tr(formatString.c_str()));
+    if(!saveAs.isEmpty() && !saveAs.isNull())
+        ui->pathTxt->setText(saveAs);
+}

+ 30 - 0
ExportImageMenu.h

@@ -0,0 +1,30 @@
+#ifndef EXPORTIMAGEMENU_H
+#define EXPORTIMAGEMENU_H
+
+#include <QWidget>
+
+namespace Ui {
+class ExportImageMenu;
+}
+
+class ExportImageMenu : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit ExportImageMenu(QWidget *parent = nullptr);
+    ~ExportImageMenu();
+
+    int getMaxIterations(void) const;
+    int getWidth(void) const;
+    int getHeight(void) const;
+    QString getPath(void) const;
+
+private slots:
+    void on_pathBtn_clicked();
+
+private:
+    Ui::ExportImageMenu *ui;
+};
+
+#endif // EXPORTIMAGEMENU_H

+ 99 - 0
ExportImageMenu.ui

@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportImageMenu</class>
+ <widget class="QWidget" name="ExportImageMenu">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>285</width>
+    <height>330</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item row="0" column="0" colspan="2">
+    <widget class="QLabel" name="titleLbl">
+     <property name="font">
+      <font>
+       <pointsize>14</pointsize>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Export Image</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="maxIterLbl">
+     <property name="text">
+      <string>Max. Iterations</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QLineEdit" name="maxIterTxt">
+     <property name="text">
+      <string>5000</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="widthLbl">
+     <property name="text">
+      <string>Width</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QLineEdit" name="widthTxt">
+     <property name="text">
+      <string>1920</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <widget class="QLabel" name="heightLbl">
+     <property name="text">
+      <string>Height</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="QLineEdit" name="heightTxt">
+     <property name="text">
+      <string>1080</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0">
+    <widget class="QPushButton" name="pathBtn">
+     <property name="text">
+      <string>Save As:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="1">
+    <widget class="QLineEdit" name="pathTxt"/>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 123 - 0
ExportVideoMenu.cpp

@@ -0,0 +1,123 @@
+#include "ExportVideoMenu.h"
+#include "ui_ExportVideoMenu.h"
+#include <QIntValidator>
+#include <QDoubleValidator>
+#include <QFileDialog>
+
+ExportVideoMenu::ExportVideoMenu(QWidget *parent) :
+    QWidget{ parent },
+    ui{ new Ui::ExportVideoMenu }
+{
+    ui->setupUi(this);
+    this->adjustSize();
+    ui->maxIterTxt->setValidator(new QIntValidator(1, 1000000000, this));
+    ui->widthTxt->setValidator(new QIntValidator(1, 10000000, this));
+    ui->heightTxt->setValidator(new QIntValidator(1, 10000000, this));
+    ui->bitrate->setValidator(new QIntValidator(1, 10000000, this));
+    ui->fps->setValidator(new QIntValidator(1, 8000, this));
+    ui->zoomSpeed->setValidator(new QDoubleValidator(0.0, 100.0, -1, this));
+
+
+    /*
+    evd.startX->setText(QString::fromStdString(evi.start.x.str()));
+    evd.startY->setText(QString::fromStdString(evi.start.y.str()));
+    evd.startW->setText(QString::fromStdString(evi.start.width.str()));
+    evd.startH->setText(QString::fromStdString(evi.start.height.str()));
+
+    evd.endX->setText(QString::fromStdString(evi.end.x.str()));
+    evd.endY->setText(QString::fromStdString(evi.end.y.str()));
+    evd.endW->setText(QString::fromStdString(evi.end.width.str()));
+    evd.endH->setText(QString::fromStdString(evi.end.height.str()));
+    */
+
+
+    const auto presets = {
+        "ultrafast",
+        "superfast",
+        "veryfast",
+        "faster",
+        "fast",
+        "medium",
+        "slow",
+        "slower",
+        "veryslow",
+    };
+    for (auto& preset : presets) {
+        ui->encodingPresetBox->addItem(preset);
+    }
+    ui->encodingPresetBox->setCurrentText("medium");
+    setStartViewport(mnd::MandelViewport::standardView());
+}
+
+
+ExportVideoMenu::~ExportVideoMenu()
+{
+    delete ui;
+}
+
+
+ExportVideoInfo ExportVideoMenu::getInfo(void) const
+{
+    ExportVideoInfo evi;
+    /*if (ui->pathTxt->text() == "") {
+        QMessageBox* msgBox = new QMessageBox;
+        msgBox->setText("Please specify a path.");
+        msgBox->exec();
+        emit reject();
+    }*/
+
+    evi.path = ui->pathTxt->text().toStdString();
+
+    evi.mi.bWidth = ui->widthTxt->text().toInt();
+    evi.mi.bHeight = ui->heightTxt->text().toInt();
+    evi.mi.maxIter = ui->maxIterTxt->text().toInt();
+
+    evi.bitrate = ui->bitrate->text().toInt();
+    evi.preset = ui->encodingPresetBox->currentText().toStdString();
+    evi.fps = ui->fps->text().toInt();
+    evi.zoomSpeed = QLocale::system().toDouble(ui->zoomSpeed->text());
+    evi.start = mnd::MandelViewport {
+        mnd::Real(ui->startX->text().toStdString().c_str()),
+        mnd::Real(ui->startY->text().toStdString().c_str()),
+        mnd::Real(ui->startW->text().toStdString().c_str()),
+        mnd::Real(ui->startH->text().toStdString().c_str())
+    };
+    evi.end = mnd::MandelViewport {
+        mnd::Real(ui->endX->text().toStdString().c_str()),
+        mnd::Real(ui->endY->text().toStdString().c_str()),
+        mnd::Real(ui->endW->text().toStdString().c_str()),
+        mnd::Real(ui->endH->text().toStdString().c_str())
+    };
+
+    evi.start.adjustAspectRatio(evi.mi.bWidth, evi.mi.bHeight);
+    evi.end.adjustAspectRatio(evi.mi.bWidth, evi.mi.bHeight);
+
+    return evi;
+}
+
+
+void ExportVideoMenu::setStartViewport(const mnd::MandelViewport& mv)
+{
+    ui->startX->setText(QString::fromStdString(mnd::toString(mv.x)));
+    ui->startY->setText(QString::fromStdString(mnd::toString(mv.y)));
+    ui->startW->setText(QString::fromStdString(mnd::toString(mv.width)));
+    ui->startH->setText(QString::fromStdString(mnd::toString(mv.height)));
+}
+
+
+void ExportVideoMenu::setEndViewport(const mnd::MandelViewport& mv)
+{
+    ui->endX->setText(QString::fromStdString(mnd::toString(mv.x)));
+    ui->endY->setText(QString::fromStdString(mnd::toString(mv.y)));
+    ui->endW->setText(QString::fromStdString(mnd::toString(mv.width)));
+    ui->endH->setText(QString::fromStdString(mnd::toString(mv.height)));
+}
+
+
+void ExportVideoMenu::on_pathBtn_clicked()
+{
+    QString saveAs = QFileDialog::getSaveFileName(this,
+            tr("Save exported image"), "",
+            tr("AVI video (*.avi);;MP4 video (*.mp4);;All Files (*)"));
+    ui->pathTxt->setText(saveAs);
+}

+ 31 - 0
ExportVideoMenu.h

@@ -0,0 +1,31 @@
+#ifndef EXPORTVIDEOMENU_H
+#define EXPORTVIDEOMENU_H
+
+#include <QWidget>
+
+#include "MandelVideoGenerator.h"
+
+namespace Ui
+{
+    class ExportVideoMenu;
+}
+
+class ExportVideoMenu : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit ExportVideoMenu(QWidget *parent = nullptr);
+    ~ExportVideoMenu();
+
+    ExportVideoInfo getInfo(void) const;
+    void setStartViewport(const mnd::MandelViewport& mv);
+    void setEndViewport(const mnd::MandelViewport& mv);
+private slots:
+    void on_pathBtn_clicked();
+
+private:
+    Ui::ExportVideoMenu *ui;
+};
+
+#endif // EXPORTVIDEOMENU_H

+ 252 - 0
ExportVideoMenu.ui

@@ -0,0 +1,252 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportVideoMenu</class>
+ <widget class="QWidget" name="ExportVideoMenu">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>614</width>
+    <height>543</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Frame</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QLabel" name="titleLbl">
+     <property name="font">
+      <font>
+       <pointsize>14</pointsize>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Export Video</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QToolBox" name="toolBox">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>1</verstretch>
+      </sizepolicy>
+     </property>
+     <widget class="QWidget" name="toolBoxPage1">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>586</width>
+        <height>640</height>
+       </rect>
+      </property>
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="label">
+       <string>General</string>
+      </attribute>
+      <layout class="QFormLayout" name="formLayout_2">
+       <item row="0" column="0">
+        <widget class="QLabel" name="maxIterLbl">
+         <property name="text">
+          <string>Max. Iterations</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QLineEdit" name="maxIterTxt">
+         <property name="text">
+          <string>5000</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="0">
+        <widget class="QLabel" name="widthLbl">
+         <property name="text">
+          <string>Video Width</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1">
+        <widget class="QLineEdit" name="widthTxt">
+         <property name="text">
+          <string>1920</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="0">
+        <widget class="QLabel" name="heightLbl">
+         <property name="text">
+          <string>Video Height</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="1">
+        <widget class="QLineEdit" name="heightTxt">
+         <property name="text">
+          <string>1080</string>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="0" colspan="2">
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Start View</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout_2">
+          <item row="1" column="0">
+           <widget class="QLineEdit" name="startX"/>
+          </item>
+          <item row="0" column="0">
+           <widget class="QLineEdit" name="startW"/>
+          </item>
+          <item row="1" column="1">
+           <widget class="QLineEdit" name="startY"/>
+          </item>
+          <item row="0" column="1">
+           <widget class="QLineEdit" name="startH"/>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item row="4" column="0" colspan="2">
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>End View</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout">
+          <item row="0" column="0">
+           <widget class="QLineEdit" name="endX"/>
+          </item>
+          <item row="1" column="0">
+           <widget class="QLineEdit" name="endW"/>
+          </item>
+          <item row="1" column="1">
+           <widget class="QLineEdit" name="endH"/>
+          </item>
+          <item row="0" column="1">
+           <widget class="QLineEdit" name="endY"/>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item row="5" column="0">
+        <widget class="QPushButton" name="pathBtn">
+         <property name="text">
+          <string>Save As</string>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="1">
+        <widget class="QLineEdit" name="pathTxt"/>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="toolBoxPage2">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>614</width>
+        <height>373</height>
+       </rect>
+      </property>
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <attribute name="label">
+       <string>Encoding</string>
+      </attribute>
+      <layout class="QFormLayout" name="formLayout_4">
+       <item row="0" column="0">
+        <widget class="QLabel" name="label_6">
+         <property name="text">
+          <string>Bitrate [kbps]</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QLineEdit" name="bitrate">
+         <property name="text">
+          <string>5000</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="0">
+        <widget class="QLabel" name="label_7">
+         <property name="text">
+          <string>Encoding preset</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1">
+        <widget class="QComboBox" name="encodingPresetBox">
+         <property name="editable">
+          <bool>false</bool>
+         </property>
+         <property name="currentText">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="0">
+        <widget class="QLabel" name="label_8">
+         <property name="text">
+          <string>Fps</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="1">
+        <widget class="QLineEdit" name="fps">
+         <property name="text">
+          <string>60</string>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="0">
+        <widget class="QLabel" name="label_9">
+         <property name="text">
+          <string>Zoom Speed</string>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="1">
+        <widget class="QLineEdit" name="zoomSpeed">
+         <property name="text">
+          <string>1</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 325 - 0
FractalWidget.cpp

@@ -0,0 +1,325 @@
+#include "FractalWidget.h"
+#include <QMouseEvent>
+
+#include <QOpenGLShaderProgram>
+#include <QPainter>
+
+
+
+
+FractalWidget::FractalWidget(QWidget* parent) :
+    FractalZoomWidget{ parent }
+{
+}
+
+
+void FractalWidget::mousePressEvent(QMouseEvent* me)
+{
+    QOpenGLWidget::mousePressEvent(me);
+    if (me->button() == Qt::RightButton) {
+        rubberbanding = true;
+        rubberband.setCoords(me->x(), me->y(), me->x(), me->y());
+        update();
+        me->accept();
+    }
+    else if (me->button() == Qt::LeftButton) {
+        dragging = true;
+        didDrag = false;
+        dragX = me->x();
+        dragY = me->y();
+        me->accept();
+    }
+}
+
+
+void FractalWidget::mouseMoveEvent(QMouseEvent* me)
+{
+    QOpenGLWidget::mouseMoveEvent(me);
+    if (rubberbanding) {
+        QRectF& rect = rubberband;
+        double aspect = double(geometry().width()) / geometry().height();
+        rect.setBottomRight(QPoint(me->x(), me->y()));
+        if (rect.width() > rect.height() * aspect)
+            rect.setHeight(rect.width() / aspect);
+        else
+            rect.setWidth(rect.height() * aspect);
+
+        update();
+    }
+    else if (dragging) {
+        double deltaX = me->x() - dragX;
+        double deltaY = me->y() - dragY;
+
+        auto& viewport = mandelInfo.view;
+        viewport.x -= deltaX * viewport.width / this->width();
+        viewport.y -= deltaY * viewport.height / this->height();
+        targetViewport = viewport;
+        dragX = me->x(); dragY = me->y();
+        didDrag = true;
+
+        update();
+    }
+
+
+    if (selectingPoint) {
+        pointX = me->x();
+        pointY = me->y();
+        update();
+    }
+
+    me->accept();
+}
+
+
+void FractalWidget::mouseReleaseEvent(QMouseEvent* me)
+{
+    QOpenGLWidget::mouseReleaseEvent(me);
+    if (rubberbanding) {
+        QRect rect = rubberband.toRect();
+        if(rect.width() != 0 && rect.height() != 0) {
+            QRect full = this->geometry();
+
+            auto& viewport = targetViewport;
+            viewport.x += mnd::Real(rect.left()) * viewport.width / full.width();
+            viewport.y += mnd::Real(rect.top()) * viewport.height / full.height();
+            viewport.width *= mnd::Real(rect.width()) / full.width();
+            viewport.height *= mnd::Real(rect.height()) / full.height();
+            viewport.normalize();
+            viewport.adjustAspectRatio(getResolutionX(), getResolutionY());
+            newAnimation();
+            //currentViewport = viewport;
+        }
+        update();
+        rubberbanding = false;
+    }
+    else if (selectingPoint && !didDrag) {
+        selectingPoint = false;
+        this->setMouseTracking(false);
+        const auto& vp = getViewport();
+        mnd::Real x = vp.x + vp.width * (float(me->pos().x()) / this->width());
+        mnd::Real y = vp.y + vp.height * (float(me->pos().y()) / this->height());
+        emit pointSelected(x, y);
+        update();
+    }
+    dragging = false;
+}
+
+
+void FractalWidget::wheelEvent(QWheelEvent* we)
+{
+    QOpenGLWidget::wheelEvent(we);
+    float x = float(we->x()) / this->width();
+    float y = float(we->y()) / this->height();
+    float scale = ::powf(0.9975f, we->angleDelta().y());
+    //mandelInfo.view.zoom(scale, x, y);
+    zoom(scale, x, y);
+    //if (!we->pixelDelta().isNull())
+    //    this->currentViewport = this->viewport;
+    we->accept();
+}
+
+
+void FractalWidget::zoom(float factor)
+{
+    targetViewport.zoomCenter(factor);
+    newAnimation();
+    update();
+}
+
+
+void FractalWidget::zoom(float factor, float fx, float fy)
+{
+    targetViewport.zoom(factor, fx, fy);
+    newAnimation();
+    update();
+    /*viewportSmoother = new ViewportAnimation(this);
+    viewportSmoother->setStartValue(QVariant::fromValue(getViewport()));
+    viewportSmoother->setEndValue(QVariant::fromValue(newVp));
+    viewportSmoother->setTargetObject(this);
+    viewportSmoother->setPropertyName("viewport");
+    viewportSmoother->setDuration(200);
+    viewportSmoother->setEasingCurve(QEasingCurve::OutExpo);
+    viewportSmoother->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped);*/
+}
+
+
+void FractalWidget::setViewport(const mnd::MandelViewport& viewport)
+{
+    FractalZoomWidget::setViewport(viewport);
+    targetViewport = mandelInfo.view;
+    update();
+}
+
+
+const mnd::MandelViewport& FractalWidget::getViewport(void) const
+{
+    return mandelInfo.view;
+}
+
+
+void FractalWidget::setDisplayInfo(bool displayInfo)
+{
+    if (displayInfo != this->displayInfo) {
+        this->displayInfo = displayInfo;
+        update();
+    }
+}
+
+
+void FractalWidget::selectJuliaPoint(void)
+{
+    this->selectingPoint = true;
+    this->setMouseTracking(true);
+    update();
+}
+
+
+void FractalWidget::stopSelectingPoint(void)
+{
+    this->selectingPoint = false;
+    this->setMouseTracking(false);
+    update();
+}
+
+
+void FractalWidget::resizeGL(int w, int h)
+{
+    FractalZoomWidget::resizeGL(w, h);
+    targetViewport.height = targetViewport.width * h / w;
+}
+
+
+void FractalWidget::paintGL(void)
+{
+    updateAnimations();
+    EscapeTimeVisualWidget::program->bind();
+    FractalZoomWidget::paintGL();
+    EscapeTimeVisualWidget::juliaPreviewer->bind();
+
+    if (selectingPoint) {
+        drawSelectingPoint();
+
+        const auto& vp = getViewport();
+        float jx = float(vp.x) + float(vp.width) * pointX / this->width();
+        float jy = float(vp.y) + float(vp.height) * pointY / this->height();
+
+        float minRes = getResolutionX();
+        if (getResolutionY() < minRes)
+            minRes = getResolutionY();
+        QRectF area{
+            60, 60,
+            minRes * 0.3, minRes * 0.3
+        };
+
+        EscapeTimeVisualWidget::drawJulia(jx, jy, area);
+
+        QPainter framePainter{ this };
+        QPen pen{ QColor{ 255, 255, 255 } };
+        pen.setWidth(2);
+        framePainter.setPen(pen);
+        framePainter.drawRect(area);
+    }
+    if (rubberbanding)
+        drawRubberband();
+    if (displayInfo)
+        drawDisplayInfo();
+}
+
+
+void FractalWidget::drawDisplayInfo(void)
+{
+    QPainter infoPainter{ this };
+
+    const float DIST_FROM_BORDER = 15;
+    float maxWidth = this->width() - 2 * DIST_FROM_BORDER;
+    mnd::Real distPerPixel = getViewport().width / this->width();
+    float log10 = (mnd::convert<float>(mnd::log(distPerPixel)) + ::logf(maxWidth)) / ::logf(10);
+    mnd::Real displayDist = mnd::pow(mnd::Real(10), ::floor(log10));
+    float pixels = mnd::convert<float>(displayDist / distPerPixel);
+    int factor = 1;
+    for (int i = 9; i > 1; i--) {
+        if (pixels * i < maxWidth) {
+            factor *= i;
+            pixels *= i;
+            displayDist *= i;
+            break;
+        }
+    }
+
+    std::stringstream dis;
+    if (::abs(log10) < 3) {
+        dis << mnd::convert<float>(displayDist);
+    }
+    else {
+        dis << factor << "e" << int(::floor(log10));
+    }
+
+    if (maxWidth > 400) {
+        dis << "; per pixel: " << distPerPixel;
+    }
+
+    float lineY = this->height() - DIST_FROM_BORDER;
+    float lineXEnd = DIST_FROM_BORDER + pixels;
+
+    infoPainter.setPen(Qt::white);
+    infoPainter.setFont(QFont("Arial", 12));
+    infoPainter.drawLine(QPointF{ DIST_FROM_BORDER, lineY }, QPointF{ lineXEnd, lineY });
+    infoPainter.drawLine(QPointF{ DIST_FROM_BORDER, lineY }, QPointF{ DIST_FROM_BORDER, lineY - 5 });
+    infoPainter.drawLine(QPointF{ lineXEnd, lineY }, QPointF{ lineXEnd, lineY - 5 });
+    infoPainter.drawText(int(DIST_FROM_BORDER), int(lineY - 20), int(lineXEnd - DIST_FROM_BORDER), 20,
+                         Qt::AlignCenter, QString::fromStdString(dis.str()));
+}
+
+
+void FractalWidget::drawSelectingPoint(void)
+{
+    QPainter pointPainter{ this };
+    pointPainter.setPen(QColor{ 255, 255, 255 });
+    pointPainter.drawLine(0, pointY, width(), pointY);
+    pointPainter.drawLine(pointX, 0, pointX, height());
+}
+
+
+void FractalWidget::drawRubberband(void)
+{
+    QPainter rubberbandPainter{ this };
+    rubberbandPainter.fillRect(rubberband, QColor{ 125, 140, 225, 120 });
+
+    QPen pen{ QColor{ 100, 115, 200 } };
+    pen.setWidth(2);
+    rubberbandPainter.setPen(pen);
+
+    rubberbandPainter.drawRect(rubberband);
+}
+
+
+void FractalWidget::newAnimation(void)
+{
+    auto now = std::chrono::high_resolution_clock::now();
+    lastAnimUpdate = now;
+}
+
+
+void FractalWidget::updateAnimations(void)
+{
+    auto& currentViewport = mandelInfo.view;
+    if (mnd::abs(currentViewport.width / targetViewport.width - 1.0) < 1e-3
+            && mnd::abs(currentViewport.height / targetViewport.height - 1.0) < 1e-3) {
+        // animation finished
+        currentViewport = targetViewport;
+    }
+    else {
+        auto now = std::chrono::high_resolution_clock::now();
+        auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastAnimUpdate).count();
+        const mnd::Real factor = mnd::Real(::pow(0.97, millis));
+        const mnd::Real one(1.0);
+
+        currentViewport.x = currentViewport.x * factor + targetViewport.x * (one - factor);
+        currentViewport.y = currentViewport.y * factor + targetViewport.y * (one - factor);
+        currentViewport.width = currentViewport.width * factor + targetViewport.width * (one - factor);
+        currentViewport.height = currentViewport.height * factor + targetViewport.height * (one - factor);
+
+        lastAnimUpdate = now;
+        emit update();
+    }
+}

+ 63 - 0
FractalWidget.h

@@ -0,0 +1,63 @@
+#ifndef FRACTALWIDGET_H
+#define FRACTALWIDGET_H
+
+#include "FractalZoomWidget.h"
+#include <QPropertyAnimation>
+#include <chrono>
+
+
+class FractalWidget :
+    public FractalZoomWidget
+{
+    Q_OBJECT
+
+    bool rubberbanding = false;
+    QRectF rubberband;
+
+    bool dragging = false;
+    bool didDrag = false;
+    float dragX, dragY;
+
+    bool selectingPoint = false;
+    float pointX, pointY;
+
+    bool displayInfo = false;
+
+    /// the target of an ongoing animation
+    mnd::MandelViewport targetViewport;
+    std::chrono::time_point<std::chrono::high_resolution_clock> lastAnimUpdate;
+public:
+    FractalWidget(QWidget* parent = nullptr);
+
+    void mousePressEvent(QMouseEvent* me) override;
+    void mouseMoveEvent(QMouseEvent* me) override;
+    void mouseReleaseEvent(QMouseEvent* me) override;
+    void wheelEvent(QWheelEvent* we) override;
+
+    virtual void zoom(float factor) override;
+    virtual void zoom(float factor, float fx, float fy);
+
+    virtual void setViewport(const mnd::MandelViewport& viewport) override;
+    virtual const mnd::MandelViewport& getViewport(void) const override;
+
+    void setDisplayInfo(bool displayInfo);
+
+    void selectJuliaPoint(void);
+    void stopSelectingPoint(void);
+
+    virtual void resizeGL(int w, int h) override;
+    virtual void paintGL(void) override;
+
+    void drawDisplayInfo(void);
+    void drawSelectingPoint(void);
+    void drawRubberband(void);
+
+signals:
+    void pointSelected(mnd::Real x, mnd::Real y);
+
+private:
+    void newAnimation(void);
+    void updateAnimations(void);
+};
+
+#endif // FRACTALWIDGET_H

+ 133 - 0
FractalWidgetUtils.cpp

@@ -0,0 +1,133 @@
+#include "FractalWidgetUtils.h"
+#include "FractalZoomWidget.h"
+
+
+
+CellImage::~CellImage(void)
+{
+}
+
+ImageClip::ImageClip(std::shared_ptr<ETVImage> tex,
+                 float tx, float ty, float tw, float th) :
+    etvImage{ std::move(tex) },
+    tx{ tx }, ty{ ty }, tw{ tw }, th{ th }
+{
+}
+
+
+ImageClip::~ImageClip(void)
+{
+}
+
+
+void ImageClip::drawRect(float x, float y, float width, float height)
+{
+    etvImage->draw(x, y, width, height, tx, ty, tw, th);
+}
+
+
+ImageClip ImageClip::clip(float x, float y, float w, float h)
+{
+    float tx = this->tx + x * this->tw;
+    float ty = this->ty + y * this->th;
+    float tw = this->tw * w;
+    float th = this->th * h;
+    return ImageClip{ this->etvImage, tx, ty, tw, th };
+}
+
+
+std::shared_ptr<CellImage> ImageClip::clip(short i, short j)
+{
+    return std::make_shared<ImageClip>(clip(i * 0.5f, j * 0.5f, 0.5f, 0.5f));
+}
+
+
+int ImageClip::getRecalcPriority() const
+{
+    return int(1.0f / tw);
+}
+
+
+QuadImage::QuadImage(std::shared_ptr<CellImage> i00,
+                 std::shared_ptr<CellImage> i01,
+                 std::shared_ptr<CellImage> i10,
+                 std::shared_ptr<CellImage> i11) :
+    cells{ { std::move(i00), std::move(i01) },
+           { std::move(i10), std::move(i11) } }
+{
+}
+
+
+QuadImage::~QuadImage(void)
+{
+}
+
+
+void QuadImage::drawRect(float x, float y, float width, float height)
+{
+    for (int i = 0; i < 2; i++) {
+        for (int j = 0; j < 2; j++) {
+            this->cells[i][j]->drawRect(x + i * 0.5f * width,
+                                        y + j * 0.5f * height,
+                                        width * 0.5f,
+                                        height * 0.5f);
+        }
+    }
+}
+
+
+std::shared_ptr<CellImage> QuadImage::clip(short i, short j)
+{
+    return cells[i][j];
+}
+
+
+int QuadImage::getRecalcPriority() const
+{
+    return 1;
+}
+
+
+void CalcJob::run(void)
+{
+    auto [absX, absY] = grid->getPositions(i, j);
+    mnd::Real gw = grid->dpp * FractalZoomWidget::chunkSize;
+
+    Bitmap<float> f{ FractalZoomWidget::chunkSize, FractalZoomWidget::chunkSize };
+    mnd::MandelInfo mi = owner.getMandelInfo();
+    mi.view.x = absX;
+    mi.view.y = absY;
+    mi.view.width = mi.view.height = gw;
+    mi.bWidth = mi.bHeight = FractalZoomWidget::chunkSize;
+    try {
+        generator->generate(mi, f.pixels.get());
+        emit done(level, i, j, new Bitmap<float>(std::move(f)));
+    }
+    catch(std::exception& ex) {
+        emit failed(level, i, j, ex.what());
+    }
+    catch(...) {
+        emit failed(level, i, j, tr("unknown error"));
+    }
+}
+
+
+size_t IndexPairHash::operator()(const std::pair<GridIndex, GridIndex>& p) const
+{
+    const auto& [a, b] = p;
+    size_t truncA = std::hash<GridIndex>{}(a);
+    size_t truncB = std::hash<GridIndex>{}(b);
+    boost::hash_combine(truncA, truncB);
+    return truncA;
+}
+
+
+size_t IndexTripleHash::operator()(const std::tuple<int, GridIndex, GridIndex>& p) const
+{
+    const auto& [i, a, b] = p;
+    size_t truncA = std::hash<GridIndex>{}(a);
+    size_t truncB = std::hash<GridIndex>{}(b);
+    boost::hash_combine(truncA, truncB);
+    boost::hash_combine(truncA, i);
+    return truncA;
+}

+ 138 - 0
FractalWidgetUtils.h

@@ -0,0 +1,138 @@
+#ifndef FRACTALWIDGETUTILS_H
+#define FRACTALWIDGETUTILS_H
+
+#include "EscapeTimeVisualWidget.h"
+#include "Mandel.h"
+#include "Bitmap.h"
+#include <unordered_map>
+#include <QMetaType>
+#include <QObject>
+#include <QRunnable>
+
+#include <utility>
+
+
+class SliceGrid;
+class FractalZoomWidget;
+
+
+using GridIndex = mnd::Integer;
+Q_DECLARE_METATYPE(GridIndex)
+Q_DECLARE_METATYPE(mnd::Real)
+
+
+class CellImage
+{
+public:
+    CellImage(void) = default;
+    CellImage(CellImage&& b) = default;
+    CellImage(const CellImage& b) = delete;
+    virtual ~CellImage(void);
+
+    virtual void drawRect(float x, float y, float width, float height) = 0;
+    virtual std::shared_ptr<CellImage> clip(short i, short j) = 0;
+    virtual int getRecalcPriority(void) const = 0;
+};
+
+
+class ImageClip :
+    public CellImage
+{
+    std::shared_ptr<ETVImage> etvImage;
+    float tx, ty, tw, th;
+public:
+    ImageClip(std::shared_ptr<ETVImage> tex,
+              float tx, float ty, float tw, float th);
+
+    inline ImageClip(std::shared_ptr<ETVImage> tex) :
+        ImageClip{ tex, 0.0f, 0.0f, 1.0f, 1.0f }
+    {}
+
+    ImageClip(ImageClip&&) = default;
+    ImageClip(const ImageClip&) = delete;
+
+    virtual ~ImageClip(void);
+
+    void drawRect(float x, float y, float width, float height) override;
+
+    ImageClip clip(float x, float y, float w, float h);
+    std::shared_ptr<CellImage> clip(short i, short j) override;
+    int getRecalcPriority(void) const override;
+};
+
+
+class QuadImage :
+    public CellImage
+{
+    std::shared_ptr<CellImage> cells[2][2];
+public:
+    QuadImage(std::shared_ptr<CellImage> i00,
+              std::shared_ptr<CellImage> i01,
+              std::shared_ptr<CellImage> i10,
+              std::shared_ptr<CellImage> i11);
+
+    QuadImage(QuadImage&&) = default;
+    QuadImage(const QuadImage&) = delete;
+
+    virtual ~QuadImage(void);
+
+    void drawRect(float x, float y, float width, float height) override;
+    std::shared_ptr<CellImage> clip(short i, short j) override;
+    int getRecalcPriority(void) const override;
+};
+
+
+struct GridElement
+{
+    bool enoughResolution;
+    std::shared_ptr<CellImage> img;
+    inline GridElement(bool enoughResolution, std::shared_ptr<CellImage> img) :
+        enoughResolution{ enoughResolution },
+        img{ std::move(img) }
+    {}
+};
+
+
+class CalcJob : public QObject, public QRunnable
+{
+    Q_OBJECT
+public:
+    FractalZoomWidget& owner;
+    mnd::MandelGenerator* generator;
+    SliceGrid* grid;
+    int level;
+    GridIndex i, j;
+    long calcState = 0;
+
+    inline CalcJob(FractalZoomWidget& owner,
+        mnd::MandelGenerator* generator,
+        SliceGrid* grid, int level,
+        GridIndex i, GridIndex j,
+        long calcState) :
+        owner{ owner },
+        generator{ generator },
+        grid{ grid },
+        level{ level },
+        i{ i }, j{ j },
+        calcState{ calcState }
+    {}
+
+    void run() override;
+signals:
+    void done(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
+    void failed(int level, GridIndex i, GridIndex j, QString err);
+};
+
+
+struct IndexPairHash
+{
+    size_t operator()(const std::pair<GridIndex, GridIndex>& p) const;
+};
+
+
+struct IndexTripleHash
+{
+    size_t operator()(const std::tuple<int, GridIndex, GridIndex>& p) const;
+};
+
+#endif // FRACTALWIDGETUTILS_H

+ 479 - 0
FractalZoomWidget.cpp

@@ -0,0 +1,479 @@
+#include "FractalZoomWidget.h"
+
+#include <QOpenGLContext>
+#include <QOpenGLFunctions>
+
+SliceGrid::SliceGrid(FractalZoomWidget& owner, int level) :
+    owner{ owner },
+    level{ level },
+    dpp{ owner.getDpp(level) }
+{
+}
+
+
+std::pair<GridIndex, GridIndex> SliceGrid::getCellIndices(mnd::Real x, mnd::Real y)
+{
+    return {
+        GridIndex(mnd::floor(x / dpp / FractalZoomWidget::chunkSize)),
+        GridIndex(mnd::floor(y / dpp / FractalZoomWidget::chunkSize))
+    };
+}
+
+
+std::pair<mnd::Real, mnd::Real> SliceGrid::getPositions(GridIndex x, GridIndex y)
+{
+    return {
+        mnd::Real(x) * dpp * FractalZoomWidget::chunkSize,
+        mnd::Real(y) * dpp * FractalZoomWidget::chunkSize
+    };
+}
+
+
+GridElement* SliceGrid::getCell(GridIndex i, GridIndex j)
+{
+    auto cIt = cells.find({i, j});
+    if (cIt != cells.end()) {
+        return cIt->second.get();
+    }
+    else {
+        return nullptr;
+    }
+}
+
+
+void SliceGrid::setCell(GridIndex i, GridIndex j, std::unique_ptr<GridElement> tex)
+{
+    cells[{i, j}] = std::move(tex);
+}
+
+
+void SliceGrid::clearCells(void)
+{
+    cells.clear();
+}
+
+
+void SliceGrid::clearUncleanCells(void)
+{
+    for (auto it = cells.begin(); it != cells.end();) {
+        if (it->second->img->getRecalcPriority() > 1)
+            cells.erase(it++);
+        else ++it;
+    }
+}
+
+
+Calcer::Calcer(FractalZoomWidget& owner) :
+    jobsMutex{ QMutex::Recursive },
+    threadPool{ new QThreadPool(this) },
+    owner{ owner }
+{
+    threadPool->setMaxThreadCount(1);
+}
+
+void Calcer::clearAll(void)
+{
+    this->threadPool->clear();
+}
+
+
+void Calcer::calc(SliceGrid& grid, int level, GridIndex i, GridIndex j, int priority)
+{
+    jobsMutex.lock();
+    if (jobs.find({ level, i, j }) == jobs.end()) {
+        CalcJob* job = new CalcJob(owner, owner.getGenerator(), &grid, level, i, j, calcState);
+        connect(job, &CalcJob::done, this, &Calcer::redirect);
+        connect(job, &CalcJob::failed, this, &Calcer::jobFailed);
+        connect(job, &QObject::destroyed, this, [this, level, i, j] () { this->notFinished(level, i, j); });
+        jobs.emplace(std::tuple{ level, i, j }, job);
+        threadPool->start(job, priority);
+    }
+    jobsMutex.unlock();
+}
+
+
+void Calcer::setCurrentLevel(int level)
+{
+    if (this->currentLevel != level) {
+        this->currentLevel = level;
+        std::vector<QRunnable*> toCancel;
+        jobsMutex.lock();
+        for (auto&[tup, job] : jobs) {
+            auto& [level, i, j] = tup;
+            if(level != currentLevel) {
+                toCancel.push_back(job);
+            }
+        }
+        jobsMutex.unlock();
+        for (auto* job : toCancel) {
+            if (threadPool->tryTake(job)) {
+                delete job;
+            }
+        }
+    }
+}
+
+
+void Calcer::notFinished(int level, GridIndex i, GridIndex j)
+{
+    jobsMutex.lock();
+    jobs.erase({ level, i, j });
+    jobsMutex.unlock();
+}
+
+
+void Calcer::jobFailed(int level, GridIndex i, GridIndex j)
+{
+    jobsMutex.lock();
+    jobs.erase({ level, i, j });
+    jobsMutex.unlock();
+}
+
+
+void Calcer::redirect(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp)
+{
+    jobsMutex.lock();
+    jobs.erase({ level, i, j });
+    jobsMutex.unlock();
+    if (this->calcState == calcState) { // TODO remove invalid results correctly
+        emit done(level, i, j, bmp);
+    }
+    else {
+        delete bmp;
+    }
+}
+
+
+const int FractalZoomWidget::chunkSize = 256;
+
+
+FractalZoomWidget::FractalZoomWidget(QWidget* parent) :
+    EscapeTimeVisualWidget{ parent },
+    calcer{ *this }
+{
+    qMetaTypeId<GridIndex>();
+    connect(&calcer, &Calcer::done, this, &FractalZoomWidget::cellReady);
+    setMaxIterations(250);
+}
+
+
+void FractalZoomWidget::setViewport(const mnd::MandelViewport& viewport)
+{
+    mandelInfo.view = viewport;
+    mandelInfo.view.adjustAspectRatio(getResolutionX(), getResolutionY());
+    update();
+}
+
+
+const mnd::MandelViewport& FractalZoomWidget::getViewport(void) const
+{
+    return mandelInfo.view;
+}
+
+
+int FractalZoomWidget::getLevel(const mnd::Real& dpp) const
+{
+    int exponent = (dpp / chunkSize).backend().exponent();
+    // replace log2 with cheaper operation
+    //return int(mnd::log2(dpp / chunkSize));
+    return exponent + 1;
+}
+
+
+mnd::Real FractalZoomWidget::getDpp(int level) const
+{
+    mnd::Real a = 1;
+    a.backend().exponent() += level;
+    return a * chunkSize;
+    // return mnd::pow(mnd::Real(2), mnd::Real(level)) * chunkSize;
+}
+
+
+SliceGrid& FractalZoomWidget::getGrid(int level)
+{
+    auto it = levels.find(level);
+    if (it != levels.end()) {
+        return it->second;
+    }
+    else {
+        levels.insert(std::pair<int, SliceGrid>{ level, SliceGrid{ *this, level } });
+        return levels.at(level);
+    }
+}
+
+
+void FractalZoomWidget::clearCells(void)
+{
+    for(auto& [level, grid] : this->levels) {
+        grid.clearCells();
+    }
+    calcer.changeState();
+    calcer.clearAll();
+    update();
+}
+
+
+void FractalZoomWidget::garbageCollect(int level, const GridIndex& /*i*/, const GridIndex& /*j*/)
+{
+    for(auto& [l, grid] : levels) {
+        int dist = ::abs(l - level);
+
+        if (dist == 1) {
+            grid.clearUncleanCells();
+        }
+
+        if (dist > 20) {
+            grid.clearCells();
+        }
+        else if (dist > 10) {
+            if (grid.countAllocatedCells() > 50)
+                grid.clearCells();
+        }
+        else if (dist > 3) {
+            if (grid.countAllocatedCells() > 150)
+                grid.clearCells();
+        }
+        else if (dist > 0) {
+            if (grid.countAllocatedCells() > 350)
+                grid.clearCells();
+        }
+        else {
+            if (grid.countAllocatedCells() > 2500)
+                grid.clearCells();
+        }
+    }
+}
+
+
+GridElement* FractalZoomWidget::searchAbove(int level,
+        const GridIndex &i, const GridIndex &j, int recursionLevel)
+{
+    auto& grid = getGrid(level);
+    auto& gridAbove = getGrid(level + 1);
+
+    GridIndex ai = (i < 0 ? (i - 1) : i) / 2;
+    GridIndex aj = (j < 0 ? (j - 1) : j) / 2;
+
+    GridElement* above = gridAbove.getCell(ai, aj);
+
+    if (above == nullptr && recursionLevel > 0) {
+        auto abFound = searchAbove(level + 1, ai, aj, recursionLevel - 1);
+        if (abFound)
+            above = abFound;
+    }
+
+    if (above != nullptr) {
+        auto newElement = std::make_unique<GridElement>(
+            false, above->img->clip(short(i & 1), short(j & 1))
+        );
+        GridElement* ret = newElement.get();
+        grid.setCell(i, j, std::move(newElement));
+        return ret;
+    }
+    else {
+        return nullptr;
+    }
+}
+
+
+GridElement* FractalZoomWidget::searchUnder(int level,
+        const GridIndex &i, const GridIndex &j, int recursionLevel)
+{
+    if (recursionLevel == 0)
+        return nullptr;
+
+    auto& grid = getGrid(level);
+    auto& gridUnder = getGrid(level - 1);
+
+    GridIndex ai = i * 2;
+    GridIndex aj = j * 2;
+
+    GridElement* u00 = gridUnder.getCell(ai, aj);
+    GridElement* u01 = gridUnder.getCell(ai, aj + 1);
+    GridElement* u10 = gridUnder.getCell(ai + 1, aj);
+    GridElement* u11 = gridUnder.getCell(ai + 1, aj + 1);
+
+    /*if (   u00 == nullptr
+        || u01 == nullptr
+        || u10 == nullptr
+        || u11 == nullptr) {
+        auto abFound = searchUnder(level + 1, ai, aj, recursionLevel - 1);
+        if (abFound)
+            above = abFound;
+    }*/
+
+    if (   u00 != nullptr
+        && u01 != nullptr
+        && u10 != nullptr
+        && u11 != nullptr) {
+        auto newElement = std::make_unique<GridElement>(
+            false, std::make_shared<QuadImage>(u00->img, u01->img, u10->img, u11->img)
+        );
+        GridElement* ret = newElement.get();
+        grid.setCell(i, j, std::move(newElement));
+        return ret;
+    }
+    else {
+        return nullptr;
+    }
+}
+
+
+const mnd::MandelInfo& FractalZoomWidget::getMandelInfo(void) const
+{
+    return mandelInfo;
+}
+
+
+mnd::MandelInfo& FractalZoomWidget::getMandelInfo(void)
+{
+    return mandelInfo;
+}
+
+
+void FractalZoomWidget::setGenerator(mnd::MandelGenerator* gen)
+{
+    bool changed = true;
+    if (this->generator == gen)
+        changed = false;
+
+    this->generator = gen;
+
+    if (changed) {
+        clearCells();
+        update();
+    }
+}
+
+
+mnd::MandelGenerator* FractalZoomWidget::getGenerator(void) const
+{
+    return generator;
+}
+
+
+void FractalZoomWidget::zoom(float factor)
+{
+    mandelInfo.view.zoomCenter(factor);
+    update();
+}
+
+
+void FractalZoomWidget::setSmoothColoring(bool smooth)
+{
+    if (mandelInfo.smooth != smooth) {
+        mandelInfo.smooth = smooth;
+        clearCells();
+        update();
+    }
+}
+
+
+void FractalZoomWidget::setMaxIterations(int maxIterations)
+{
+    if (mandelInfo.maxIter != maxIterations) {
+        mandelInfo.maxIter = maxIterations;
+        EscapeTimeVisualWidget::setMaxIterationCutoff(float(maxIterations));
+        clearCells();
+        update();
+    }
+}
+
+
+void FractalZoomWidget::initializeGL(void)
+{
+    EscapeTimeVisualWidget::initializeGL();
+    Bitmap<float> empty{ 1, 1 };
+    empty.get(0, 0) = 0.0f;
+    emptyImage = new ETVImage(*this, empty);
+}
+
+
+void FractalZoomWidget::resizeGL(int w, int h)
+{
+    EscapeTimeVisualWidget::resizeGL(w, h);
+    mandelInfo.view.height = mandelInfo.view.width * h / w;
+}
+
+
+void FractalZoomWidget::paintGL(void)
+{
+    EscapeTimeVisualWidget::paintGL();
+    /*ETVImage etvi{ *this };
+
+    auto& gl = *this->context()->functions();
+    gl.glClearColor(0.0, 0.2, 0.0, 1.0);
+    gl.glClear(GL_COLOR_BUFFER_BIT);
+
+    etvi.draw(100, 100, 700, 700);*/
+
+
+    ////////////////////
+
+    int width = getResolutionX();
+    int height = getResolutionY();
+
+    auto& mvp = mandelInfo.view;
+    mnd::Real dpp = mvp.width / width;
+    int level = getLevel(dpp) - 1;
+    auto& grid = getGrid(level);
+    mnd::Real gw = getDpp(level) * chunkSize;
+    auto [left, top] = grid.getCellIndices(mvp.x, mvp.y);
+    auto [right, bottom] = grid.getCellIndices(mvp.right(), mvp.bottom());
+
+    garbageCollect(level, (left + right) / 2, (top + bottom) / 2);
+    emit calcer.setCurrentLevel(level);
+
+    mnd::Real w = width * gw / mvp.width;
+
+    auto [realXLeft, realYTop] = grid.getPositions(left, top);
+    realXLeft = ((realXLeft - mvp.x) * mnd::Real(width)) / mvp.width;
+    realYTop = ((realYTop - mvp.y) * mnd::Real(height)) / mvp.height;
+    for(GridIndex i = left; i <= right; i++) {
+        for(GridIndex j = top; j <= bottom; j++) {
+            mnd::Real x = w * int(i - left) + realXLeft;
+            mnd::Real y = w * int(j - top) + realYTop;
+
+            GridElement* t = grid.getCell(i, j);
+
+            if (t == nullptr) {
+                auto under = searchUnder(level, i, j, 1);
+                if (under) {
+                    t = under;
+                }
+                else {
+                    auto above = searchAbove(level, i, j, 3);
+                    if (above) {
+                        t = above;
+                    }
+                }
+            }
+
+            if (t != nullptr) {
+                if (!t->enoughResolution) {
+                    auto under = searchUnder(level, i, j, 1);
+                    if (under) {
+                        t = under;
+                    }
+                    calcer.calc(grid, level, i, j, t->img->getRecalcPriority());
+                }
+
+                t->img->drawRect(float(x), float(y), float(w), float(w));
+            }
+            else {
+                calcer.calc(grid, level, i, j, 1000);
+                if (emptyImage)
+                    emptyImage->draw(float(x), float(y), float(w), float(w));
+            }
+        }
+    }
+}
+
+
+void FractalZoomWidget::cellReady(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp)
+{
+    this->getGrid(level).setCell(i, j,
+        std::make_unique<GridElement>(true, std::make_shared<ImageClip>(std::make_shared<ETVImage>(*this, *bmp))));
+    delete bmp;
+    update();
+}

+ 128 - 0
FractalZoomWidget.h

@@ -0,0 +1,128 @@
+#ifndef FRACTALZOOMWIDGET_H
+#define FRACTALZOOMWIDGET_H
+
+#include "EscapeTimeVisualWidget.h"
+#include "FractalWidgetUtils.h"
+#include "Bitmap.h"
+
+#include <QThreadPool>
+#include <QMutex>
+
+class FractalZoomWidget;
+
+
+
+///
+/// \brief represents a grid of images at a certain depth
+///        in the fractal.
+///
+class SliceGrid
+{
+public:
+    FractalZoomWidget& owner;
+    int level;
+    mnd::Real dpp;
+    std::unordered_map<std::pair<GridIndex, GridIndex>, std::unique_ptr<GridElement>, IndexPairHash> cells;
+public:
+    SliceGrid(FractalZoomWidget& owner, int level);
+
+    std::pair<GridIndex, GridIndex> getCellIndices(mnd::Real x, mnd::Real y);
+    std::pair<mnd::Real, mnd::Real> getPositions(GridIndex i, GridIndex j);
+    GridElement* getCell(GridIndex i, GridIndex j);
+    void setCell(GridIndex i, GridIndex j, std::unique_ptr<GridElement> tex);
+
+    inline size_t countAllocatedCells(void) const { return cells.size(); }
+    void clearCells(void);
+    void clearUncleanCells(void);
+};
+
+
+class Calcer : public QObject
+{
+    Q_OBJECT
+
+    /// tuple contains level, i, j of the job
+    std::unordered_map<std::tuple<int, GridIndex, GridIndex>, CalcJob*, IndexTripleHash> jobs;
+    QMutex jobsMutex;
+    QThreadPool* threadPool;
+    FractalZoomWidget& owner;
+    int currentLevel;
+
+    volatile unsigned int calcState = 0;
+public:
+    Calcer(FractalZoomWidget& owner);
+    void clearAll(void);
+
+    inline void changeState(void) { calcState++; }
+
+public slots:
+    void calc(SliceGrid& grid, int level, GridIndex i, GridIndex j, int priority);
+    void setCurrentLevel(int level);
+    void notFinished(int level, GridIndex i, GridIndex j);
+    void jobFailed(int level, GridIndex i, GridIndex j);
+    void redirect(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
+signals:
+    void done(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
+};
+
+
+class FractalZoomWidget :
+    public EscapeTimeVisualWidget
+{
+    Q_OBJECT
+
+    // a grid should not be deleted once constructed.
+    // to free up memory one can call SliceGrid::clearCells()
+    std::unordered_map<int, SliceGrid> levels;
+    mnd::MandelGenerator* generator;
+    Calcer calcer;
+
+    ETVImage* emptyImage;
+protected:
+    mnd::MandelInfo mandelInfo;
+
+public:
+    static const int chunkSize;
+
+    FractalZoomWidget(QWidget* parent = nullptr);
+
+    Q_PROPERTY(mnd::MandelViewport viewport READ getViewport WRITE setViewport)
+    virtual void setViewport(const mnd::MandelViewport& viewport);
+    virtual const mnd::MandelViewport& getViewport(void) const;
+
+    int getLevel(const mnd::Real& dpp) const;
+    mnd::Real getDpp(int level) const;
+    SliceGrid& getGrid(int level);
+
+    void clearCells(void);
+
+    void garbageCollect(int level,
+         const GridIndex& i, const GridIndex& j);
+
+    GridElement* searchAbove(int level,
+                 const GridIndex& i, const GridIndex& j,
+                 int recursionLevel);
+    GridElement* searchUnder(int level,
+                 const GridIndex& i, const GridIndex& j,
+                 int recursionLevel);
+
+    const mnd::MandelInfo& getMandelInfo(void) const;
+    mnd::MandelInfo& getMandelInfo(void);
+
+    void setGenerator(mnd::MandelGenerator*);
+    mnd::MandelGenerator* getGenerator(void) const;
+
+    virtual void zoom(float factor);
+
+    virtual void setSmoothColoring(bool smooth);
+    virtual void setMaxIterations(int maxIterations);
+
+    virtual void initializeGL(void) override;
+    virtual void resizeGL(int w, int h) override;
+    virtual void paintGL(void) override;
+protected slots:
+
+    void cellReady(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
+};
+
+#endif // FRACTALZOOMWIDGET_H

+ 0 - 164
Gradient.cpp

@@ -1,164 +0,0 @@
-#include "Gradient.h"
-
-#include "CubicSpline.h"
-
-#include <cmath>
-#include <algorithm>
-#include <functional>
-#include <QtXml/QDomDocument>
-#include <QFile>
-
-
-Gradient::Gradient(void) :
-    max{ 1.0 }
-{
-}
-
-
-Gradient::Gradient(std::vector<std::pair<RGBColor, float>> colors, bool repeat, int precalcSteps) :
-    repeat{ repeat }
-{
-    if(colors.empty() || colors.size() < 2)
-        return;
-    std::sort(colors.begin(), colors.end(),
-        [] (const auto& a, const auto& b) {
-            return a.second < b.second;
-        });
-
-    max = colors.at(colors.size() - 1).second;
-
-    std::vector<std::pair<RGBColorf, float>> linearColors;
-    std::transform(colors.begin(), colors.end(), std::back_inserter(linearColors),
-                   [] (auto c) { return c; });
-
-    std::vector<std::pair<float, float>> rs;
-    std::vector<std::pair<float, float>> gs;
-    std::vector<std::pair<float, float>> bs;
-
-    std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(rs),
-                   [] (auto p) { return std::pair{ p.second, p.first.r }; });
-    std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(gs),
-                   [] (auto p) { return std::pair{ p.second, p.first.g }; });
-    std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(bs),
-                   [] (auto p) { return std::pair{ p.second, p.first.b }; });
-
-    CubicSpline rsp(rs, false);
-    CubicSpline gsp(gs, false);
-    CubicSpline bsp(bs, false);
-
-    if(precalcSteps <= 0) {
-        precalcSteps = int(max * 15) + 10;
-    }
-
-    for (int i = 0; i < precalcSteps; i++) {
-        float position = i * max / precalcSteps;
-        RGBColorf at = {
-            rsp.interpolateAt(position),
-            gsp.interpolateAt(position),
-            bsp.interpolateAt(position)
-        };
-        this->colors.push_back(at);
-    }
-}
-
-
-Gradient Gradient::defaultGradient(void)
-{
-    QFile res(":/gradients/default");
-    res.open(QIODevice::ReadOnly);
-    QString str = QString::fromUtf8(res.readAll());
-    return readXml(str);
-}
-
-Gradient Gradient::readXml(const QString& xml)
-{
-    QDomDocument xsr;
-    xsr.setContent(xml);
-    auto elem = xsr.documentElement();
-    auto repeatAttr = elem.attributeNode("repeat");
-    bool repeat = !repeatAttr.isNull() && repeatAttr.value().toLower() == "true";
-    auto colors = xsr.elementsByTagName("color");
-    std::vector<std::pair<RGBColor, float>> colorArr;
-    for (int i = 0; i < colors.length(); ++i) {
-        auto child = colors.item(i).toElement();
-        uint8_t r = uint8_t(child.attributeNode("r").value().toInt());
-        uint8_t g = uint8_t(child.attributeNode("g").value().toInt());
-        uint8_t b = uint8_t(child.attributeNode("b").value().toInt());
-        float p = child.attributeNode("p").value().toInt();
-
-        //printf("rgb (%s): %d, %d, %d\n", child.text().toUtf8().data(), r, g, b);
-        colorArr.push_back({ { r, g, b }, p });
-    }
-
-    return Gradient(std::move(colorArr), repeat);
-}
-
-
-RGBColor Gradient::get(float x) const
-{
-    if (colors.empty() || std::isnan(x) || std::isinf(x))
-        return RGBColor();
-    /*const auto [left, right, lerp] = getNeighbors(x);
-    RGBColor lerped = lerpColors(left, right, lerp);
-    return lerped;*/
-
-    if (x < 0)
-        return colors[0];
-    if (x > this->max) {
-        if (repeat)
-            x = ::fmodf(x, this->max);
-        else
-            x = this->max;
-    }
-    float pos = x * colors.size() / max;
-    if (pos < 0) {
-        pos = 0;
-    }
-    if (pos > colors.size() - 1) {
-        pos = colors.size() - 1;
-    }
-
-    int left = int(pos);
-    int right = int(pos + 1);
-    float lerp = pos - left;
-
-    if (lerp < 1e-5f) {
-        return colors[left];
-    }
-    else {
-        return lerpColors(colors[left], colors[right], lerp);
-    }
-}
-
-
-RGBColorf Gradient::lerpColors(RGBColorf a, RGBColorf b, float val)
-{
-    return RGBColorf {
-        b.r * val + a.r * (1 - val),
-        b.g * val + a.g * (1 - val),
-        b.b * val + a.b * (1 - val)
-    };
-}
-
-
-RGBColor Gradient::lerpColors(RGBColor a, RGBColor b, float val)
-{
-    return RGBColor{ lerpColors(RGBColorf{ a }, RGBColorf{ b }, val) };
-}
-
-
-/*std::tuple<RGBColor, RGBColor, float> Gradient::getNeighbors(float x) const
-{
-    for (auto it = colors.begin(); it != colors.end(); ++it) {
-        if (it->second > x) {
-            if (it == colors.begin()) {
-                return { it->first, it->first, 0 };
-            }
-            else {
-                float lerp = (x - (it - 1)->second) / (it->second - (it - 1)->second);
-                return { (it - 1)->first, it->first, lerp };
-            }
-        }
-    }
-    return { (colors.end() - 1)->first, (colors.end() - 1)->first, 0 };
-}*/

+ 0 - 38
Gradient.h

@@ -1,38 +0,0 @@
-#ifndef GRADIENT_H
-#define GRADIENT_H
-
-#include <QString>
-#include <vector>
-#include "Color.h"
-#include <tuple>
-#include <cinttypes>
-
-
-class Gradient
-{
-    /// the colors of this gradient stored in linear RGB format
-    /// so they can be easily interpolated
-    std::vector<RGBColorf> colors;
-    float max;
-    bool repeat;
-public:
-    Gradient(void);
-    Gradient(std::vector<std::pair<RGBColor, float>> colors, bool repeat = false, int precalcSteps = -1);
-
-    static Gradient defaultGradient(void);
-
-    static Gradient readXml(const QString& xml);
-
-    /*!
-     * \brief get a color at a specific position in this gradient
-     * \param x the position
-     * \return the color in sRGB format
-     */
-    RGBColor get(float x) const;
-private:
-    static RGBColorf lerpColors(RGBColorf a, RGBColorf b, float val);
-    static RGBColor lerpColors(RGBColor a, RGBColor b, float val);
-    std::tuple<RGBColor, RGBColor, float> getNeighbors(float x) const;
-};
-
-#endif // GRADIENT_H

+ 41 - 0
GradientMenu.cpp

@@ -0,0 +1,41 @@
+#include "GradientMenu.h"
+#include "ui_GradientMenu.h"
+
+GradientMenu::GradientMenu(QWidget *parent) :
+    QWidget(parent),
+    ui(new Ui::GradientMenu)
+{
+    ui->setupUi(this);
+    ui->gradientWidget->setGradient(
+        std::vector<std::pair<RGBColor, float>> {
+            { RGBColor{ 10, 200, 20 }, 0.1 },
+            { RGBColor{ 100, 20, 120 }, 0.7 }
+        }
+    );
+    connect(ui->gradientWidget, &GradientWidget::gradientChanged, this, &GradientMenu::gradientChanged);
+}
+
+
+GradientMenu::~GradientMenu()
+{
+    delete ui;
+}
+
+
+const Gradient& GradientMenu::getGradient(void)
+{
+    return ui->gradientWidget->getGradient();
+}
+
+
+const Gradient& GradientMenu::getGradientBefore(void) const
+{
+    return before;
+}
+
+
+void GradientMenu::setGradient(Gradient grad)
+{
+    before = grad;
+    ui->gradientWidget->setGradient(std::move(grad));
+}

+ 32 - 0
GradientMenu.h

@@ -0,0 +1,32 @@
+#ifndef GRADIENTMENU_H
+#define GRADIENTMENU_H
+
+#include <QWidget>
+#include <QVector>
+#include <QPair>
+
+#include "Gradient.h"
+
+namespace Ui {
+class GradientMenu;
+}
+
+class GradientMenu : public QWidget
+{
+    Q_OBJECT
+
+    Ui::GradientMenu *ui;
+    Gradient before;
+public:
+    explicit GradientMenu(QWidget *parent = nullptr);
+    ~GradientMenu(void);
+
+    const Gradient& getGradient(void);
+    const Gradient& getGradientBefore(void) const;
+    void setGradient(Gradient grad);
+
+signals:
+    void gradientChanged(void);
+};
+
+#endif // GRADIENTMENU_H

+ 58 - 0
GradientMenu.ui

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>GradientMenu</class>
+ <widget class="QWidget" name="GradientMenu">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>302</width>
+    <height>508</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Select Gradient</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout" stretch="0,1">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QLabel" name="titleLbl">
+     <property name="font">
+      <font>
+       <pointsize>14</pointsize>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Select Gradient</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="GradientWidget" name="gradientWidget" native="true"/>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>GradientWidget</class>
+   <extends>QWidget</extends>
+   <header>GradientWidget.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>

+ 460 - 1
GradientWidget.cpp

@@ -1,6 +1,465 @@
 #include "GradientWidget.h"
 
-GradientWidget::GradientWidget(QWidget *parent) : QWidget(parent)
+#include <QPaintEvent>
+#include <QPainter>
+#include <QStyleOptionSlider>
+#include <QColorDialog>
+#include <QStyle>
+
+#include <algorithm>
+#include <cmath>
+
+GradientWidget::GradientWidget(QWidget* parent) :
+    QWidget{ parent }
+{
+    dragging = false;
+    selectedHandle = -1;
+    mouseOver = -1;
+
+    maxValue = 1.0f;
+
+    colorPicker = new QColorDialog(this);
+    colorPicker->setOption(QColorDialog::NoButtons);
+    connect(colorPicker, &QColorDialog::currentColorChanged, this, &GradientWidget::selectedColorChanged);
+
+    setMouseTracking(true);
+}
+
+
+const Gradient& GradientWidget::getGradient(void) const
+{
+    return gradient;
+}
+
+
+void GradientWidget::setGradient(Gradient gr)
+{
+    gradient = std::move(gr);
+    points = gradient.getPoints();
+    maxValue = gradient.getMax();
+}
+
+
+void GradientWidget::updateGradient(void)
+{
+    gradient = Gradient{ points, maxValue };
+}
+
+
+QColor GradientWidget::colorAtY(float y)
+{
+    float v = handleYToGradVal(y);
+    return fromRGB(gradient.get(v));
+    /*float v = handleYToGradVal(y);
+    QColor up = QColor(QColor::Invalid);
+    QColor down = QColor(QColor::Invalid);
+    float upv = 0;
+    float downv = 1;
+    for (const auto& [color, val] : points) {
+        if (val >= upv && val < v) {
+            upv = val;
+            up = QColor(color.r, color.g, color.b);
+        }
+        if (val <= downv && val > v) {
+            downv = val;
+            down = QColor(color.r, color.g, color.b);
+        }
+    }
+
+    if (!up.isValid())
+        return down;
+    if (!down.isValid())
+        return up;
+    return lerp(up, down, (v - upv) / (downv - upv));*/
+}
+
+
+void GradientWidget::paintEvent(QPaintEvent* e)
+{
+    QPainter painter{ this };
+
+    QRect gradientRect = getGradientRect();
+    QStyleOption frameOptions;
+    frameOptions.init(this);
+    frameOptions.rect = gradientRect;
+    frameOptions.state |= QStyle::State_Sunken;
+    style()->drawPrimitive(
+                QStyle::PrimitiveElement::PE_Frame, &frameOptions, &painter, this);
+
+    int fhmargins = style()->pixelMetric(QStyle::PixelMetric::PM_FocusFrameHMargin);
+    int fvmargins = style()->pixelMetric(QStyle::PixelMetric::PM_FocusFrameVMargin);
+    if (fhmargins == -1) {
+        fhmargins = fvmargins = 3;
+    }
+
+    QLinearGradient linGrad;
+    linGrad.setStart(0, gradientRect.top());
+    linGrad.setFinalStop(0, gradientRect.bottom());
+
+    const int stops = this->height() / 5;
+    /*for (int i = 0; i < stops; i++) {
+        auto col = gradient.get(float(i) / stops * gradient.getMax());
+        linGrad.setColorAt(float(i) / stops, QColor{ col.r, col.g, col.b });
+    }*/
+
+    for (const auto& [col, at] : points) {
+        linGrad.setColorAt(at / maxValue, QColor{ col.r, col.g, col.b });
+    }
+
+    // adjust rect to have small margins, so the frame
+    // around the gradient is visible
+    gradientRect.adjust(fhmargins, fvmargins, -fhmargins, -fvmargins);
+    float lastPoint = 0;
+    QColor lastColor = QColor{ 0, 0, 0 };
+
+
+
+    /*std::vector<int> orderedIndices(points.size());
+    for (int i = 0; i < points.size(); i++)
+        orderedIndices.push_back(i);
+
+    std::sort(orderedIndices.begin(), orderedIndices.end(),
+        [this] (int l, int r) {
+        return points[l].first < points[r].first;
+    });
+
+    // traverse gradient in order and interpolate in linear
+    // RGB space to avoid interpolating in sRGB
+    for (int i = 0; i < orderedIndices.size(); i++) {
+        int index = orderedIndices[i];
+        auto& [point, color] = points[index];
+        int m = 17;
+        if (i > 0) {
+            for (int i = 0; i < m; i++) {
+                float v = float(i) / m;
+                gradient.setColorAt(lastPoint + (point - lastPoint) / m * i,
+                                    lerp(lastColor, color, v));
+            }
+        }
+        gradient.setColorAt(point, color);
+        lastPoint = point;
+        lastColor = color;
+    }*/
+
+    QBrush brush{ linGrad };
+    painter.fillRect(gradientRect, brush);
+
+    int index = 0;
+    for (auto& [color, point] : points) {
+        QRect r = getHandleRect(index);
+        /*QStyleOptionButton so;
+        so.init(this);
+        so.rect = r;
+        if (dragging && selectedHandle == index)
+            so.state |= QStyle::State_Sunken;
+        else if (selectedHandle == index)
+            so.state |= QStyle::State_HasFocus;
+        else
+            so.state &= ~QStyle::State_Sunken & ~QStyle::State_HasFocus;
+        if (mouseOver == index)
+            so.state |= QStyle::State_MouseOver;
+        else
+            so.state &= ~QStyle::State_MouseOver;
+        
+        so.palette.setColor(QPalette::ColorRole::Button, color);
+        style()->drawControl(QStyle::ControlElement::CE_PushButton, &so, &painter, this);*/
+        int hs = HandleState::HANDLE_NORMAL;
+        if (dragging && selectedHandle == index)
+            hs |= HANDLE_DOWN;
+        if (mouseOver == index)
+            hs |= HANDLE_MOUSEOVER;
+        if (selectedHandle == index)
+            hs |= HANDLE_SELECTED;
+        paintHandle(painter, r, fromRGB(color), hs);
+        index++;
+    }
+    /*for (auto&[point, color] : points) {
+        QStyleOptionSlider qsos;
+        qsos.rect = QRect{ 100, static_cast<int>(point * height() - 10), 20, 20 };
+        qsos.orientation = Qt::Vertical;
+        qsos.subControls = QStyle::SC_SliderHandle;
+        if (selectedHandle == index) {
+            qsos.state |= QStyle::State_Sunken;
+        }
+        else {
+            qsos.state &= ~QStyle::State_Sunken;
+        }
+        style()->drawComplexControl(QStyle::CC_Slider, &qsos, &painter, this);
+        index++;
+    }*/
+    /*
+
+    QPen pen(Qt::red);
+    pen.setWidth(10);
+    painter.setPen(pen);
+    painter.drawRect(30, 30, 50, 50);
+
+    painter.fillRect(QRect(0, 0, width(), height()), QColor{ 100, 20, 20 });
+    qDebug(std::to_string(width()).c_str());
+    */
+}
+
+
+void GradientWidget::paintHandle(QPainter& painter, const QRectF& pos,
+                                 QColor c, int handleState)
+{
+    const float lineWidth = 2;
+    QPainterPath qpp = createSlideHandle(pos.width() - lineWidth, pos.height() - lineWidth);
+    qpp.translate(pos.x() + lineWidth / 2, pos.y() + lineWidth / 2);
+    if (handleState & HANDLE_SELECTED) {
+        QColor absLighter;
+        absLighter.setHsvF(c.hueF(), c.saturationF(), c.valueF() > 0.3 ? c.valueF() : 0.3);
+        painter.setPen(QPen(QBrush(absLighter.lighter(130)), lineWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
+    } else
+        painter.setPen(QPen(QBrush(c.darker(200)), lineWidth / 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
+
+    painter.setRenderHint(QPainter::Antialiasing);
+    QLinearGradient bevel{ 0, pos.top(), 0, pos.bottom() }; // top down linear gradient
+
+    if (handleState & HANDLE_DOWN) {
+        bevel.setColorAt(0, c.darker(120));
+        bevel.setColorAt(1, c.lighter(120));
+    }
+    else if (handleState & HANDLE_MOUSEOVER) {
+        bevel.setColorAt(0, c.lighter(130));
+        bevel.setColorAt(1, c.darker(110));
+    }
+    else {
+        bevel.setColorAt(0, c.lighter(120));
+        bevel.setColorAt(1, c.darker(120));
+    }
+    painter.fillPath(qpp, QBrush(bevel));
+    painter.drawPath(qpp);
+}
+
+
+void GradientWidget::mousePressEvent(QMouseEvent* e)
+{
+    int handle = handleAtPos(e->pos());
+
+    if (handle != -1) {
+        selectedHandle = handle;
+        dragging = true;
+        selectOffsetY = e->y() - gradValToHandleY(
+                    points[handle].second);
+        update();
+        e->accept();
+    }
+    else {
+        e->ignore();
+    }
+}
+
+
+void GradientWidget::mouseReleaseEvent(QMouseEvent* e)
+{
+    if (dragging) {
+        dragging = false;
+        update();
+        e->accept();
+    }
+    else {
+        e->ignore();
+    }
+}
+
+
+void GradientWidget::mouseMoveEvent(QMouseEvent* e)
+{
+    if (dragging) {
+        float newVal = handleYToGradVal(e->y() - selectOffsetY);
+        newVal = std::clamp(newVal, 0.0f, maxValue);
+        points[selectedHandle].second = newVal;
+        updateGradient();
+        update();
+        emit gradientChanged();
+        e->accept();
+    }
+    else {
+        int handle = handleAtPos(e->pos());
+        bool needsUpdate = false;
+        if (mouseOver != handle)
+            needsUpdate = true;
+        mouseOver = handle;
+        e->accept();
+        if (needsUpdate)
+            update();
+    }
+}
+
+
+void GradientWidget::mouseDoubleClickEvent(QMouseEvent* e)
+{
+    auto torgb = [](const QColor& c) {
+        return RGBColor{
+            uint8_t(c.red()), uint8_t(c.green()), uint8_t(c.blue())
+        };
+    };
+    QRect handleArea = getHandleArea();
+    int handle = handleAtPos(e->pos());
+    if (handle != -1) {
+        RGBColor current = points.at(handle).first;
+        /*QColor newColor = QColorDialog::getColor(current,
+                                                 this,
+                                                 tr("Pick Color"));*/
+        selectedHandle = handle;
+        colorPicker->setCurrentColor(fromRGB(current));
+        colorPicker->exec();
+        /*if (newColor.isValid()) {
+            points[handle].first = torgb(newColor);
+            update();
+            emit gradientChanged();
+        }*/
+        e->accept();
+    }
+    else if (handleArea.contains(e->pos())) {
+        float v = handleYToGradVal(e->pos().y());
+        points.emplace_back(torgb(colorAtY(e->pos().y())), v);
+        e->accept();
+        updateGradient();
+        update();
+    }
+    else {
+        e->ignore();
+    }
+}
+
+
+QSize GradientWidget::minimumSizeHint(void) const
+{
+    int spacing = this->style()->pixelMetric(
+                QStyle::PM_LayoutHorizontalSpacing);
+    if (spacing == -1) {
+        spacing = this->style()->layoutSpacing(
+                    QSizePolicy::Frame,
+                    QSizePolicy::PushButton,
+                    Qt::Horizontal);
+    }
+    return QSize{ int(handleWidth * 1.5 + spacing), handleHeight * 5 };
+}
+
+
+QSize GradientWidget::sizeHint(void) const
+{
+    int spacing = this->style()->pixelMetric(
+                QStyle::PM_LayoutHorizontalSpacing);
+    if (spacing == -1) {
+        spacing = this->style()->layoutSpacing(
+                    QSizePolicy::Frame,
+                    QSizePolicy::PushButton,
+                    Qt::Horizontal);
+    }
+    return QSize{ int(handleWidth * 1.1 + spacing), handleHeight };
+}
+
+
+void GradientWidget::selectedColorChanged(const QColor& newColor)
+{
+    if (points.size() > selectedHandle) {
+        points.at(selectedHandle).first = RGBColor {
+            uint8_t(newColor.red()),
+            uint8_t(newColor.green()),
+            uint8_t(newColor.blue())
+        };
+        updateGradient();
+        update();
+        emit gradientChanged();
+    }
+}
+
+
+QRect GradientWidget::getGradientRect(void) const
 {
+    QMargins cm = contentsMargins();
+    int top = cm.top();
+    int bottom = cm.bottom();
+    int left = cm.left();
+    int right = cm.right();
 
+    int spacing = this->style()->pixelMetric(
+                QStyle::PM_LayoutHorizontalSpacing);
+    if (spacing == -1) {
+        spacing = this->style()->layoutSpacing(
+                    QSizePolicy::Frame,
+                    QSizePolicy::PushButton,
+                    Qt::Horizontal);
+    }
+    top += handleHeight / 2;
+    bottom += handleHeight / 2;
+    return  QRect{ left, top,
+                width() - left - right - handleWidth - spacing,
+                height() - bottom - top };
 }
+
+
+QRect GradientWidget::getHandleRect(int index) const
+{
+    QRect handleArea = getHandleArea();
+    float y = handleArea.top() + points.at(index).second / maxValue * handleArea.height();
+    return QRect {
+        handleArea.x(), int(y - handleHeight / 2),
+        handleWidth, handleHeight
+    };
+}
+
+
+QRect GradientWidget::getHandleArea(void) const
+{
+    QMargins cm = contentsMargins();
+    int top = cm.top();
+    int bottom = cm.bottom();
+    int left = cm.left();
+    int right = cm.right();
+
+    top += handleHeight / 2;
+    bottom += handleHeight / 2;
+    float y = top;
+    float x = width() - handleWidth - right;
+    return QRect {
+        int(x), top, handleWidth, height() - top - bottom
+    };
+}
+
+
+int GradientWidget::handleAtPos(QPoint pos) const
+{
+    for (int i = points.size() - 1; i >= 0; i--) {
+        QRect rect = getHandleRect(i);
+        if (rect.contains(pos)) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+float GradientWidget::handleYToGradVal(float y) const
+{
+    QRect area = getHandleArea();
+    return maxValue * (y - area.top()) / area.height();
+}
+
+
+float GradientWidget::gradValToHandleY(float v) const
+{
+    QRect area = getHandleArea();
+    return area.top() + v / maxValue * area.height();
+}
+
+
+QPainterPath GradientWidget::createSlideHandle(float w, float h)
+{
+    const float rounding = 4;
+    QPainterPath qpp;
+    QPolygonF qpf;
+    qpf << QPointF{ 0, 0.5 * h }
+        << QPointF{ 0.3 * w, h };
+    qpp.moveTo(0, 0.5 * h);
+    qpp.lineTo(0.3 * w, h);
+    qpp.arcTo(w - rounding, h - rounding, rounding, rounding, -90, 90);
+    qpp.arcTo(w - rounding, 0, rounding, rounding, 0, 90);
+    qpp.lineTo(0.3 * w, 0);
+    qpp.lineTo(0, 0.5 * h);
+
+    return qpp;
+}
+

+ 80 - 2
GradientWidget.h

@@ -2,15 +2,93 @@
 #define GRADIENTWIDGET_H
 
 #include <QWidget>
+#include <QPainterPath>
+#include <QColorDialog>
+#include <QVector>
+#include <QPair>
 
-class GradientWidget : public QWidget
+#include "Gradient.h"
+
+class GradientWidget :
+    public QWidget
 {
     Q_OBJECT
+
+    std::vector<std::pair<RGBColor, float>> points;
+    Gradient gradient;
+    float maxValue;
+
+    QColorDialog* colorPicker;
+
+    bool dragging;
+    int selectedHandle;
+    float selectOffsetY;
+
+    int mouseOver;
+
+    int handleWidth = 40;
+    int handleHeight = 24;
 public:
+
+    enum HandleState
+    {
+        HANDLE_NORMAL = 0x00,
+        HANDLE_MOUSEOVER = 0x01,
+        HANDLE_DOWN = 0x02,
+        HANDLE_SELECTED = 0x04
+    };
+
+
     explicit GradientWidget(QWidget *parent = nullptr);
 
-signals:
+    const Gradient& getGradient(void) const;
+    void setGradient(Gradient gradient);
+
+private:
+    void updateGradient(void);
+    inline QColor fromRGB(const RGBColor& rgb)
+    {
+        return QColor{ rgb.r, rgb.g, rgb.b };
+    }
+public:
 
+    QColor colorAtY(float y);
+
+    virtual void paintEvent(QPaintEvent* e) override;
+    virtual void paintHandle(QPainter& painter, const QRectF &pos, QColor c, int handleState);
+
+    virtual void mousePressEvent(QMouseEvent* e) override;
+    virtual void mouseReleaseEvent(QMouseEvent* e) override;
+    virtual void mouseMoveEvent(QMouseEvent* e) override;
+    virtual void mouseDoubleClickEvent(QMouseEvent* e) override;
+
+    QSize minimumSizeHint(void) const override;
+    QSize sizeHint(void) const override;
+
+public slots:
+    void selectedColorChanged(const QColor& newColor);
+
+protected:
+    /// \brief the area in which the gradient is displayed
+    QRect getGradientRect(void) const;
+
+    /// \brief the area in which the handle with index
+    /// \c index is displayed
+    QRect getHandleRect(int index) const;
+
+    /// \brief the area in which the handles can move around
+    QRect getHandleArea(void) const;
+
+    int handleAtPos(QPoint pos) const;
+
+    float handleYToGradVal(float y) const;
+    float gradValToHandleY(float v) const;
+
+private:
+    static QPainterPath createSlideHandle(float w, float h);
+signals:
+    void gradientChanged(void);
 };
 
 #endif // GRADIENTWIDGET_H
+

+ 17 - 0
LICENSE

@@ -0,0 +1,17 @@
+Copyright (c) 2020 Nicolas Winkler
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+   claim that you wrote the original software. If you use this software
+   in a product, an acknowledgment in the product documentation would be
+   appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+   misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.

+ 234 - 56
MandelWidget.cpp

@@ -1,7 +1,14 @@
+#if 0
+
 #include "MandelWidget.h"
 #include <cmath>
 #include <sstream>
 
+#include <QStyle>
+#include <QStyleOption>
+#include <QOpenGLShader>
+#include <QOpenGLFunctions_3_0>
+
 using namespace mnd;
 
 #include <QPainter>
@@ -10,12 +17,16 @@ using namespace mnd;
 #include <cstdio>
 
 
-Texture::Texture(QOpenGLFunctions_2_0& gl, const Bitmap<RGBColor>& bitmap, GLint param) :
+Texture::Texture(QOpenGLFunctions& gl, const Bitmap<float>& bitmap, GLint param) :
     gl{ gl }
 {
     gl.glGenTextures(1, &id);
+    gl.glActiveTexture(GL_TEXTURE0);
     gl.glBindTexture(GL_TEXTURE_2D, id);
 
+    Bitmap<float> copy = bitmap.map<float>([](float x) { return x / 200; });
+    Bitmap<RGBColor> rgbs = bitmap.map<RGBColor>([](float x) { return RGBColor{ 100, uint8_t(::sin(x * 0.01) * 127 + 127), 20 }; });
+
     //int lineLength = (bitmap.width * 3 + 3) & ~3;
 
     /*std::unique_ptr<unsigned char[]> pixels = std::make_unique<unsigned char[]>(lineLength * bitmap.height);
@@ -28,11 +39,12 @@ Texture::Texture(QOpenGLFunctions_2_0& gl, const Bitmap<RGBColor>& bitmap, GLint
             pixels[index + 2] = c.b;
         }
     }*/
-    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, int(bitmap.width), int(bitmap.height), 0, GL_RGB, GL_UNSIGNED_BYTE, reinterpret_cast<char*> (bitmap.pixels.get()));
+    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, int(bitmap.width), int(bitmap.height), 0, GL_RGB, GL_UNSIGNED_BYTE, rgbs.pixels.get());
     gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
     gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
     gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, param);
     gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, param);
+    gl.glBindTexture(GL_TEXTURE_2D, 0);
 }
 
 
@@ -51,23 +63,66 @@ Texture::Texture(Texture&& other) :
 }
 
 
-Texture& Texture::operator=(Texture&& other)
-{
-    this->id = other.id;
-    this->gl = other.gl;
-    other.id = 0;
-    return *this;
-}
-
-
 void Texture::bind(void) const
 {
+    gl.glActiveTexture(GL_TEXTURE0);
     gl.glBindTexture(GL_TEXTURE_2D, id);
 }
 
 
-void Texture::drawRect(float x, float y, float width, float height)
+static GLuint gradId;
+void Texture::drawRect(QOpenGLShaderProgram* program,
+                       float x, float y, float width, float height,
+                       float tx, float ty, float tw, float th)
 {
+#if 1
+    GLfloat const vertices[] = {
+        x, y,  0.0f,
+        x, y + height, 0.0f,
+        x + width, y + height, 0.0f,
+        x + width, y, 0.0f,
+    };
+
+    GLfloat const texCoords[] = {
+        tx,      ty,
+        tx,      ty + th,
+        tx + tw, ty + th,
+        tx + tw, ty,
+    };
+
+    QColor color(255, 255, 255);
+
+    int vertexLoc = program->attributeLocation("vertex");
+    int texCoordsLoc = program->attributeLocation("texCoord");
+    int colorLocation = program->uniformLocation("color");
+    int texLoc = program->uniformLocation("tex");
+    int gradLoc = program->uniformLocation("gradient");
+    program->setAttributeArray(vertexLoc, vertices, 3);
+    program->setAttributeArray(texCoordsLoc, texCoords, 2);
+    program->enableAttributeArray(vertexLoc);
+    program->enableAttributeArray(texCoordsLoc);
+    program->setUniformValue(colorLocation, color);
+
+
+    auto& gl3 = *QOpenGLContext::currentContext()->functions();
+    gl3.glEnable(GL_TEXTURE_2D);
+
+    gl3.glUniform1i(texLoc, GL_TEXTURE0);
+    gl3.glUniform1i(gradLoc, GL_TEXTURE2);
+
+    gl3.glActiveTexture(GL_TEXTURE0);
+    gl3.glBindTexture(GL_TEXTURE_2D, id);
+
+    gl3.glActiveTexture(GL_TEXTURE2);
+    gl3.glBindTexture(GL_TEXTURE_2D, gradId);
+    gl3.glActiveTexture(GL_TEXTURE0);
+
+    gl3.glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+    program->disableAttributeArray(vertexLoc);
+    program->disableAttributeArray(texCoordsLoc);
+    gl3.glActiveTexture(GL_TEXTURE0);
+#else
     gl.glColor3ub(255, 255, 255);
     gl.glEnable(GL_TEXTURE_2D);
     bind();
@@ -82,6 +137,7 @@ void Texture::drawRect(float x, float y, float width, float height)
     gl.glVertex2f(x + width, y + height);
     gl.glEnd();
     gl.glDisable(GL_TEXTURE_2D);
+#endif
 }
 
 
@@ -95,8 +151,11 @@ TextureClip::~TextureClip(void)
 {
 }
 
-void TextureClip::drawRect(float x, float y, float width, float height)
+
+void TextureClip::drawRect(QOpenGLShaderProgram* program,
+        float x, float y, float width, float height)
 {
+    /*
     auto& gl = texture->gl;
     gl.glColor3ub(255, 255, 255);
     gl.glEnable(GL_TEXTURE_2D);
@@ -111,7 +170,9 @@ void TextureClip::drawRect(float x, float y, float width, float height)
     gl.glTexCoord2f(tx + tw, ty + th);
     gl.glVertex2f(x + width, y + height);
     gl.glEnd();
-    gl.glDisable(GL_TEXTURE_2D);
+    gl.glDisable(GL_TEXTURE_2D);*/
+    texture->drawRect(program, x, y, width, height,
+                      tx, ty, tw, th);
 }
 
 
@@ -142,11 +203,13 @@ QuadImage::~QuadImage(void)
 }
 
 
-void QuadImage::drawRect(float x, float y, float width, float height)
+void QuadImage::drawRect(QOpenGLShaderProgram* program,
+        float x, float y, float width, float height)
 {
     for (int i = 0; i < 2; i++) {
         for (int j = 0; j < 2; j++) {
-            this->cells[i][j]->drawRect(x + i * 0.5f * width,
+            this->cells[i][j]->drawRect(program,
+                                        x + i * 0.5f * width,
                                         y + j * 0.5f * height,
                                         width * 0.5f,
                                         height * 0.5f);
@@ -225,18 +288,18 @@ void Job::run(void)
     auto [absX, absY] = grid->getPositions(i, j);
     mnd::Real gw = grid->dpp * MandelView::chunkSize;
 
-    Bitmap<float> f(MandelView::chunkSize, MandelView::chunkSize);
+    Bitmap<float>* f = new Bitmap<float>(MandelView::chunkSize, MandelView::chunkSize);
     mnd::MandelInfo mi = owner.getMandelInfo();
     mi.view.x = absX;
     mi.view.y = absY;
     mi.view.width = mi.view.height = gw;
     mi.bWidth = mi.bHeight = MandelView::chunkSize;
     try {
-        generator->generate(mi, f.pixels.get());
-        auto* rgb = new Bitmap<RGBColor>(f.map<RGBColor>([&mi, this] (float i) {
+        generator->generate(mi, f->pixels.get());
+        /*auto* rgb = new Bitmap<RGBColor>(f.map<RGBColor>([&mi, this] (float i) {
             return i >= mi.maxIter ? RGBColor{ 0, 0, 0 } : gradient.get(i);
-        }));
-        emit done(level, i, j, calcState, rgb);
+        }));*/
+        emit done(level, i, j, calcState, f);
     }
     catch(std::exception& ex) {
         printf("wat: %s?!\n", ex.what()); fflush(stdout);
@@ -310,7 +373,7 @@ void Calcer::notFinished(int level, GridIndex i, GridIndex j)
 }
 
 
-void Calcer::redirect(int level, GridIndex i, GridIndex j, long calcState, Bitmap<RGBColor>* bmp)
+void Calcer::redirect(int level, GridIndex i, GridIndex j, long calcState, Bitmap<float>* bmp)
 {
     jobsMutex.lock();
     jobs.erase({ level, i, j });
@@ -329,7 +392,9 @@ const int MandelView::chunkSize = 256;
 MandelView::MandelView(mnd::MandelGenerator* generator, MandelWidget& owner) :
     generator{ generator },
     calcer{ generator, owner },
-    owner{ owner }
+    owner{ owner },
+    width{ 0 },
+    height{ 0 }
 {
     /*Bitmap<RGBColor> emp(8, 8);
     for(auto i = 0; i < emp.width; i++) {
@@ -342,9 +407,9 @@ MandelView::MandelView(mnd::MandelGenerator* generator, MandelWidget& owner) :
             }
         }
     }*/
-    Bitmap<RGBColor> emp(1, 1);
-    emp.get(0, 0) = RGBColor{ 0, 0, 0 };
-    auto& gl = *QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_2_0>();
+    Bitmap<float> emp(1, 1);
+    emp.get(0, 0) = 0.0f;
+    auto& gl = *QOpenGLContext::currentContext()->functions();
     empty = std::make_unique<Texture>(gl, emp, GL_NEAREST);
     connect(&calcer, &Calcer::done, this, &MandelView::cellReady);
 }
@@ -498,8 +563,8 @@ GridElement* MandelView::searchUnder(int level, GridIndex i, GridIndex j, int re
 }
 
 
-void MandelView::paint(const mnd::MandelViewport& mvp, QPainter& qp)
-{
+void MandelView::paint(const mnd::MandelViewport& mvp)
+{    
     mnd::Real dpp = mvp.width / width;
     int level = getLevel(dpp) - 1;
     auto& grid = getGrid(level);
@@ -536,7 +601,11 @@ void MandelView::paint(const mnd::MandelViewport& mvp, QPainter& qp)
             }
 
             if (t != nullptr) {
-                t->img->drawRect(float(x), float(y), float(w), float(w));
+
+                auto& gl3 = *QOpenGLContext::currentContext()->functions();
+                gl3.glActiveTexture(GL_TEXTURE2);
+                gl3.glBindTexture(GL_TEXTURE_2D, owner.gradientTexture);
+                t->img->drawRect(this->owner.program,float(x), float(y), float(w), float(w));
                 /*glBegin(GL_LINE_LOOP);
                 glVertex2f(float(x), float(y));
                 glVertex2f(float(x) + float(w), float(y));
@@ -550,15 +619,17 @@ void MandelView::paint(const mnd::MandelViewport& mvp, QPainter& qp)
             }
             else {
                 calcer.calc(grid, level, i, j, 1000);
-                this->empty->drawRect(float(x), float(y), float(w), float(w));
+                this->empty->drawRect(this->owner.program,
+                            float(x), float(y), float(w), float(w));
             }
         }
     }
 }
 
-void MandelView::cellReady(int level, GridIndex i, GridIndex j, Bitmap<RGBColor>* bmp)
+
+void MandelView::cellReady(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp)
 {
-    auto& gl = *QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_2_0>();
+    auto& gl = *QOpenGLContext::currentContext()->functions();
     this->getGrid(level).setCell(i, j,
         std::make_unique<GridElement>(true, std::make_shared<TextureClip>(std::make_shared<Texture>(gl, *bmp))));
     delete bmp;
@@ -572,7 +643,7 @@ MandelWidget::MandelWidget(mnd::MandelContext& ctxt, mnd::MandelGenerator* gener
     generator{ generator },
     gradient{ Gradient::defaultGradient() }
 {
-    this->setContentsMargins(0, 0, 0, 0);
+    //this->setContentsMargins(0, 0, 0, 0);
     this->setSizePolicy(QSizePolicy::Expanding,
         QSizePolicy::Expanding);
     qRegisterMetaType<GridIndex>("GridIndex");
@@ -587,12 +658,46 @@ MandelWidget::~MandelWidget()
 
 void MandelWidget::setGradient(Gradient g)
 {
-    this->gradient = std::move(g);
+    /*this->gradient = std::move(g);
     if (mandelView) {
         mandelView->clearCells();
         mandelView->calcer.changeState();
     }
-    emit update();
+    emit update();*/
+
+    auto& gl = *this->context()->functions();
+    this->gradient = std::move(g);
+    int width = 1024;
+
+    std::vector<unsigned char> pixels(size_t(width * 3));
+    for (int i = 0; i < width; i++) {
+        float pos = i * this->gradient.getMax() / width;
+        RGBColor c = gradient.get(pos);
+        pixels.push_back(c.r);
+        pixels.push_back(c.g);
+        pixels.push_back(c.b);
+    }
+
+    unsigned char pix[] = { 255, 0, 0, 0, 255, 0, 0, 0, 255 };
+
+    GLuint id;
+    gl.glEnable(GL_TEXTURE_2D);
+    gl.glActiveTexture(GL_TEXTURE2);
+    gl.glGenTextures(1, &id);
+    gl.glBindTexture(GL_TEXTURE_2D, id);
+
+    gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 3, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, reinterpret_cast<char*> (pix));
+    gl.glUniform1i(this->program->uniformLocation("gradient"), GL_TEXTURE2);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    gl.glBindTexture(GL_TEXTURE_2D, 0);
+
+
+    gradientTexture = id;
+    gradId = id;
+    update();
 }
 
 
@@ -662,7 +767,6 @@ void MandelWidget::initializeGL(void)
 {
     auto& gl = *this->context()->functions();
     gl.glClearColor(0, 0, 0, 0);
-    this->context()->makeCurrent(nullptr);
 
     gl.glDisable(GL_DEPTH_TEST);
 
@@ -671,7 +775,39 @@ void MandelWidget::initializeGL(void)
 
     //glShadeModel(GL_SMOOTH);
 
+
+    program = new QOpenGLShaderProgram{ this->context() };
+    bool vert = program->addShaderFromSourceCode(QOpenGLShader::Vertex,
+    "attribute highp vec4 vertex;\n"
+    "attribute highp vec2 texCoord;\n"
+    "uniform highp mat4 matrix;\n"
+    "varying highp vec2 texc;\n"
+    "void main(void)\n"
+    "{\n"
+    "   gl_Position = matrix * vertex;\n"
+    "   texc = texCoord;\n"
+    "}");
+    bool frag = program->addShaderFromSourceCode(QOpenGLShader::Fragment,
+    "uniform sampler2D gradient;\n"
+    "uniform sampler2D tex;\n"
+    "uniform mediump vec4 color;\n"
+    "varying highp vec2 texc;\n"
+    "void main(void)\n"
+    "{\n"
+    "   float v = texture2D(tex, texc).r;\n"
+    "   gl_FragColor = texture2D(gradient, texc);\n"
+//    "   gl_FragColor = gl_FragColor * texture2D(tex, texc);\n"
+//    "   float v = texture2D(tex, texc).r;\n"
+//    "   gl_FragColor = vec4(v, 1.0 - v, v*v, 1);\n"
+//    "   gl_FragColor.g = 0.3;\n"
+    "}");
+    //program.link();
+    bool bound = program->bind();
+
+    //gl3.glBindSampler(0, id);
+
     mandelView = nullptr;
+    setGradient(gradient);
     requestRecalc();
 }
 
@@ -694,12 +830,15 @@ void MandelWidget::resizeGL(int w, int h)
 
 void MandelWidget::paintGL(void)
 {
-    auto& gl = *QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_2_0>();
+    //auto& gl = *QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_2_0>();
     if (mandelView == nullptr) {
         mandelView = std::make_unique<MandelView>(generator, *this);
         QObject::connect(mandelView.get(), &MandelView::redrawRequested, this, static_cast<void(QOpenGLWidget::*)(void)>(&QOpenGLWidget::update));
     }
+    //if (program)
+        //program->bind();
 
+    /*
     int width = this->width();
     int height = this->height();
     float pixelRatio = this->devicePixelRatioF();
@@ -712,32 +851,71 @@ void MandelWidget::paintGL(void)
 #ifdef QT_OPENGL_ES_1
     gl.glOrthof(0, width * pixelRatio, height * pixelRatio, 0, -1.0, 1.0);
 #else
-    gl.glOrtho(0, width * pixelRatio, height * pixelRatio, 0, -1.0, 1.0);
+    gl.glOrtho(0, double(width) * pixelRatio, double(height) * pixelRatio, 0, -1.0, 1.0);
 #endif
     gl.glMatrixMode(GL_MODELVIEW);
     gl.glLoadIdentity();
 
     gl.glClear(GL_COLOR_BUFFER_BIT);
 
-    updateAnimations();
 
     QPainter painter{ this };
 
     mandelView->paint(this->currentViewport, painter);
 
     if (rubberbanding)
-        drawRubberband();
+    drawRubberband();
+    if (displayInfo)
+    drawInfo();
+    if (selectingPoint)
+    drawPoint();*/
+    //QPainter painter{ this };
+
+
+    updateAnimations();
+
+    mandelView->paint(this->currentViewport);
+
+    static GLfloat const triangleVertices[] = {
+        0.0,  20,  0.0f,
+        49, 50, 0.0f,
+        -60,  70, 0.0f
+    };
+
+    QColor color(0, 255, 0);
+
+    QMatrix4x4 pmvMatrix;
+    pmvMatrix.ortho(rect());
+
+    int vertexLocation = program->attributeLocation("vertex");
+    int matrixLocation = program->uniformLocation("matrix");
+    int colorLocation = program->uniformLocation("color");
+    program->enableAttributeArray(vertexLocation);
+    program->setAttributeArray(vertexLocation, triangleVertices, 3);
+    program->setUniformValue(matrixLocation, pmvMatrix);
+    program->setUniformValue(colorLocation, color);
+    
+    auto& gl3 = *QOpenGLContext::currentContext()->functions();
+    gl3.glDrawArrays(GL_TRIANGLES, 0, 3);
+
+    program->disableAttributeArray(vertexLocation);
+
+    /*QPainter painter{ this };
+    painter.beginNativePainting();
+    painter.endNativePainting();*/
+    /*if (rubberbanding)
+        drawRubberband(painter);
     if (displayInfo)
-        drawInfo();
+        drawInfo(painter);
     if (selectingPoint)
-        drawPoint();
+        drawPoint(painter);*/
 }
 
 
 void MandelWidget::updateAnimations(void)
 {
-    if (mnd::abs(currentViewport.width / targetViewport.width - 1.0) < 0.1e-5
-            && mnd::abs(currentViewport.height / targetViewport.height - 1.0) < 0.1e-5) {
+    if (mnd::abs(currentViewport.width / targetViewport.width - 1.0) < 1e-3
+            && mnd::abs(currentViewport.height / targetViewport.height - 1.0) < 1e-3) {
         // animation finished
         currentViewport = targetViewport;
     }
@@ -759,9 +937,8 @@ void MandelWidget::updateAnimations(void)
 }
 
 
-void MandelWidget::drawRubberband(void)
+void MandelWidget::drawRubberband(QPainter& rubberbandPainter)
 {
-    QPainter rubberbandPainter{ this };
     rubberbandPainter.fillRect(rubberband, QColor{ 125, 140, 225, 120 });
 
     QPen pen{ QColor{ 100, 115, 200 } };
@@ -769,10 +946,12 @@ void MandelWidget::drawRubberband(void)
     rubberbandPainter.setPen(pen);
 
     rubberbandPainter.drawRect(rubberband);
+    //QStyleOption so;
+    //style()->drawControl(QStyle::CE_RubberBand, &so, &rubberbandPainter, this);
 }
 
 
-void MandelWidget::drawInfo(void)
+void MandelWidget::drawInfo(QPainter& infoPainter)
 {
     const float DIST_FROM_BORDER = 15;
     float maxWidth = this->width() - 2 * DIST_FROM_BORDER;
@@ -805,7 +984,6 @@ void MandelWidget::drawInfo(void)
     float lineY = this->height() - DIST_FROM_BORDER;
     float lineXEnd = DIST_FROM_BORDER + pixels;
 
-    QPainter infoPainter{ this };
     infoPainter.setPen(Qt::white);
     infoPainter.setFont(QFont("Arial", 12));
     infoPainter.drawLine(QPointF{ DIST_FROM_BORDER, lineY }, QPointF{ lineXEnd, lineY });
@@ -813,13 +991,11 @@ void MandelWidget::drawInfo(void)
     infoPainter.drawLine(QPointF{ lineXEnd, lineY }, QPointF{ lineXEnd, lineY - 5 });
     infoPainter.drawText(int(DIST_FROM_BORDER), int(lineY - 20), int(lineXEnd - DIST_FROM_BORDER), 20,
                          Qt::AlignCenter, QString::fromStdString(dis.str()));
-    infoPainter.end();
 }
 
 
-void MandelWidget::drawPoint(void)
+void MandelWidget::drawPoint(QPainter& pointPainter)
 {
-    QPainter pointPainter{ this };
     pointPainter.setPen(QColor{ 255, 255, 255 });
     pointPainter.drawLine(0, pointY, width(), pointY);
     pointPainter.drawLine(pointX, 0, pointX, height());
@@ -898,7 +1074,7 @@ void MandelWidget::mousePressEvent(QMouseEvent* me)
     if (me->button() == Qt::RightButton) {
         rubberbanding = true;
         rubberband.setCoords(me->x(), me->y(), me->x(), me->y());
-        emit repaint();
+        update();
         me->accept();
     }
     else if (me->button() == Qt::LeftButton) {
@@ -923,12 +1099,12 @@ void MandelWidget::mouseMoveEvent(QMouseEvent* me)
         else
             rect.setWidth(rect.height() * aspect);
 
-        emit repaint();
+        update();
     }
     else if (selectingPoint) {
         pointX = me->x();
         pointY = me->y();
-        emit repaint();
+        update();
     }
     else if (dragging) {
         double deltaX = me->x() - dragX;
@@ -939,7 +1115,7 @@ void MandelWidget::mouseMoveEvent(QMouseEvent* me)
         targetViewport = currentViewport;
         dragX = me->x(); dragY = me->y();
 
-        emit repaint();
+        update();
     }
     me->accept();
 }
@@ -969,7 +1145,7 @@ void MandelWidget::mouseReleaseEvent(QMouseEvent* me)
         mnd::Real x = currentViewport.x + currentViewport.width * mnd::convert<mnd::Real>(float(me->x()) / width());
         mnd::Real y = currentViewport.y + currentViewport.height * mnd::convert<mnd::Real>(float(me->y()) / height());
         emit pointSelected(x, y);
-        emit repaint();
+        update();
     }
     dragging = false;
 
@@ -996,3 +1172,5 @@ void MandelWidget::wheelEvent(QWheelEvent* we)
         emit repaint();
     }
 }*/
+
+#endif

+ 64 - 19
MandelWidget.h

@@ -1,4 +1,5 @@
 #pragma once
+#if 0
 
 #include <QGLWidget>
 #include <QOpenGLWidget>
@@ -7,6 +8,7 @@
 #include <QOpenGLContext>
 #include <QOpenGLFunctions>
 #include <QOpenGLFunctions_2_0>
+#include <QOpenGLShaderProgram>
 #include <QMutex>
 #include <QPainter>
 //#include <qopengl.h>
@@ -35,27 +37,61 @@ class MandelWidget;
 
 class Texture
 {
+protected:
     GLuint id;
+    QOpenGLFunctions& gl;
+    inline Texture(QOpenGLFunctions& gl) : gl{ gl } {}
 public:
-    QOpenGLFunctions_2_0& gl;
-    Texture(QOpenGLFunctions_2_0& gl, const Bitmap<RGBColor>& pict, GLint param = GL_LINEAR);
+    Texture(QOpenGLFunctions& gl, const Bitmap<float>& pict, GLint param = GL_LINEAR);
     ~Texture(void);
 
     Texture(const Texture& other) = delete;
     Texture& operator=(const Texture& other) = delete;
 
     Texture(Texture&& other);
-    Texture& operator=(Texture&& other);
+    Texture& operator=(Texture&& other) = delete;
 
 private:
     void bind(void) const;
 public:
     inline GLuint getId(void) const { return id; }
 
-    void drawRect(float x, float y, float width, float height);
+    void drawRect(QOpenGLShaderProgram* program,
+                  float x, float y, float width, float height,
+                  float tx = 0.0f, float ty = 0.0f,
+                  float tw = 1.0f, float th = 1.0f);
 };
 
 
+///
+/// \brief The FloatTexture class
+///
+/*class FloatTexture
+{
+    GLuint id;
+    QOpenGLFunctions& gl;
+public:
+    FloatTexture(QOpenGLFunctions& gl, const Bitmap<float>& pict, GLint param = GL_LINEAR);
+    ~Texture(void);
+
+    Texture(const Texture& other) = delete;
+    Texture& operator=(const Texture& other) = delete;
+
+    Texture(Texture&& other);
+    Texture& operator=(Texture&& other) = delete;
+
+private:
+    void bind(void) const;
+public:
+    inline GLuint getId(void) const { return id; }
+
+    void drawRect(QOpenGLShaderProgram* program,
+                  float x, float y, float width, float height,
+                  float tx = 0.0f, float ty = 0.0f,
+                  float tw = 1.0f, float th = 1.0f);
+};*/
+
+
 class CellImage
 {
 public:
@@ -64,7 +100,8 @@ public:
     CellImage(const CellImage& b) = delete;
     virtual ~CellImage(void);
 
-    virtual void drawRect(float x, float y, float width, float height) = 0;
+    virtual void drawRect(QOpenGLShaderProgram* program,
+                          float x, float y, float width, float height) = 0;
     virtual std::shared_ptr<CellImage> clip(short i, short j) = 0;
     virtual int getRecalcPriority(void) const = 0;
 };
@@ -90,11 +127,12 @@ public:
 
     virtual ~TextureClip(void);
 
-    void drawRect(float x, float y, float width, float height);
+    void drawRect(QOpenGLShaderProgram* program,
+                  float x, float y, float width, float height) override;
 
     TextureClip clip(float x, float y, float w, float h);
-    std::shared_ptr<CellImage> clip(short i, short j);
-    int getRecalcPriority(void) const;
+    std::shared_ptr<CellImage> clip(short i, short j) override;
+    int getRecalcPriority(void) const override;
 };
 
 
@@ -114,9 +152,10 @@ public:
 
     virtual ~QuadImage(void);
 
-    void drawRect(float x, float y, float width, float height);
-    std::shared_ptr<CellImage> clip(short i, short j);
-    int getRecalcPriority(void) const;
+    void drawRect(QOpenGLShaderProgram* program,
+                  float x, float y, float width, float height) override;
+    std::shared_ptr<CellImage> clip(short i, short j) override;
+    int getRecalcPriority(void) const override;
 };
 
 
@@ -203,7 +242,7 @@ public:
 
     void run() override;
 signals:
-    void done(int level, GridIndex i, GridIndex j, long calcState, Bitmap<RGBColor>* bmp);
+    void done(int level, GridIndex i, GridIndex j, long calcState, Bitmap<float>* bmp);
 };
 
 
@@ -231,9 +270,9 @@ public slots:
     void calc(TexGrid& grid, int level, GridIndex i, GridIndex j, int priority);
     void setCurrentLevel(int level);
     void notFinished(int level, GridIndex i, GridIndex j);
-    void redirect(int level, GridIndex i, GridIndex j, long calcState, Bitmap<RGBColor>* bmp);
+    void redirect(int level, GridIndex i, GridIndex j, long calcState, Bitmap<float>* bmp);
 signals:
-    void done(int level, GridIndex i, GridIndex j, Bitmap<RGBColor>* bmp);
+    void done(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
 };
 
 
@@ -266,9 +305,9 @@ public:
     void garbageCollect(int level, GridIndex i, GridIndex j);
     GridElement* searchAbove(int level, GridIndex i, GridIndex j, int recursionLevel);
     GridElement* searchUnder(int level, GridIndex i, GridIndex j, int recursionLevel);
-    void paint(const mnd::MandelViewport& mvp, QPainter& qp);
+    void paint(const mnd::MandelViewport& mvp);
 public slots:
-    void cellReady(int level, GridIndex i, GridIndex j, Bitmap<RGBColor>* bmp);
+    void cellReady(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
 signals:
     void redrawRequested(void);
 };
@@ -277,6 +316,8 @@ signals:
 class MandelWidget : public QOpenGLWidget
 {
     Q_OBJECT
+
+    friend class MandelView;
 private:
     mnd::MandelContext& mndContext;
     mnd::MandelGenerator* generator;
@@ -302,6 +343,9 @@ private:
     std::chrono::time_point<std::chrono::high_resolution_clock> lastAnimUpdate;
 
     std::unique_ptr<MandelView> mandelView;
+
+    QOpenGLShaderProgram* program;
+    GLuint gradientTexture;
 public:
     MandelWidget(mnd::MandelContext& ctxt, mnd::MandelGenerator* generator, QWidget* parent = nullptr);
     ~MandelWidget(void) override;
@@ -336,9 +380,9 @@ public:
 private:
     void updateAnimations(void);
 
-    void drawRubberband(void);
-    void drawInfo(void);
-    void drawPoint(void);
+    void drawRubberband(QPainter& p);
+    void drawInfo(QPainter& p);
+    void drawPoint(QPainter& p);
 public:
 
     void zoom(float scale, float x = 0.5f, float y = 0.5f);
@@ -360,3 +404,4 @@ signals:
     void pointSelected(mnd::Real x, mnd::Real y);
 };
 
+#endif

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+# Almond
+
+Almond is a fast and simple fractal viewer.
+
+## Building
+
+The preferred way of building Almond is using cmake.
+

+ 1 - 0
customgenerator.cpp

@@ -13,6 +13,7 @@ CustomGenerator::CustomGenerator(mnd::MandelContext& mndCtxt, QWidget *parent) :
     ui->setupUi(this);
 }
 
+
 CustomGenerator::~CustomGenerator()
 {
     delete ui;

+ 112 - 0
debian/copyright

@@ -0,0 +1,112 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: Almond
+Source: https://git.winfor.ch/nicolas/Almond
+
+Files: *
+Copyright: 2019-2020 Nicolas Winkler
+License: Zlib
+
+Files: libmandel/asmjit/*
+Copyright: 2008-2020 The AsmJit Authors
+License: Zlib
+
+Files: libmandel/qd-2.3.22/*
+Copyright: 
+License: BSD-LBNL
+
+Files: libalmond/lpng1637/*
+Copyright: 
+License: libpng
+
+Files: libalmond/zlib-1.2.11/
+Copyright: 1995-2017 Jean-loup Gailly and Mark Adler
+License: Zlib
+
+License: Zlib
+This software is provided 'as-is', without any express or implied warranty. In
+no event will the authors be held liable for any damages arising from the use
+of this software.
+
+Permission is granted to anyone to use this software for any purpose, including
+commercial applications, and to alter it and redistribute it freely, subject to
+the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not claim
+   that you wrote the original software. If you use this software in a product,
+   an acknowledgment in the product documentation would be appreciated but is
+   not required.
+2. Altered source versions must be plainly marked as such, and must not be
+   misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+
+License: BSD-LBNL
+1. Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+(1) Redistributions of source code must retain the copyright notice, this list
+of conditions and the following disclaimer.
+
+(2) Redistributions in binary form must reproduce the copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+(3) Neither the name of the University of California, Lawrence Berkeley
+National Laboratory, U.S. Dept. of Energy nor the names of its contributors may
+be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+2. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+3. You are under no obligation whatsoever to provide any bug fixes, patches, or
+upgrades to the features, functionality or performance of the source code
+("Enhancements") to anyone; however, if you choose to make your Enhancements
+available either publicly, or directly to Lawrence Berkeley National
+Laboratory, without imposing a separate written license agreement for such
+Enhancements, then you hereby grant the following license: a non-exclusive,
+royalty-free perpetual license to install, use, modify, prepare derivative
+works, incorporate into other computer software, distribute, and sublicense
+such enhancements or derivative works thereof, in binary and source code form.
+
+License: libpng
+ * Copyright (c) 1995-2019 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2019 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement.  In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+    must not claim that you wrote the original software.  If you
+    use this software in a product, an acknowledgment in the product
+    documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+    source or altered source distribution.
+
+

+ 19 - 4
exportdialogs.cpp

@@ -52,10 +52,23 @@ QString ExportImageDialog::getPath(void) const
 
 void ExportImageDialog::on_pushButton_clicked()
 {
+    std::string formatString = "";
+    if (alm::supportsImageFormat(alm::ImageFormat::PNG)) {
+        formatString += "PNG image (*.png);;";
+    }
+    if (alm::supportsImageFormat(alm::ImageFormat::JPEG)) {
+        formatString += "JPEG image (*.jpg *.jpeg);;";
+    }
+    if (alm::supportsImageFormat(alm::ImageFormat::BMP)) {
+        formatString += "BMP image (*.bmp *.dib);;";
+    }
+    if (!formatString.empty()) {
+        formatString.erase(formatString.end() - 2, formatString.end());
+    }
+
     QString saveAs = QFileDialog::getSaveFileName(this,
             tr("Save exported image"), "",
-            //tr("PNG image (*.png);;JPEG image (*.jpg);;All Files (*)"));
-            tr("PNG image (*.png)"));
+            tr(formatString.c_str()));
     if(!saveAs.isEmpty() && !saveAs.isNull())
         eid.savePath->setText(saveAs);
     this->repaint();
@@ -133,6 +146,7 @@ const ExportVideoInfo& ExportVideoDialog::getExportVideoInfo(void) const
 
 void ExportVideoDialog::on_buttonBox_accepted()
 {
+    /*
     if (evd.savePath->text() == "") {
         QMessageBox* msgBox = new QMessageBox;
         msgBox->setText("Please specify a path.");
@@ -163,14 +177,15 @@ void ExportVideoDialog::on_buttonBox_accepted()
         evd.endW->text().toDouble(),
         evd.endH->text().toDouble(),
     };*/
-
+    /*
     evi.start.adjustAspectRatio(evi.mi.bWidth, evi.mi.bHeight);
     evi.end.adjustAspectRatio(evi.mi.bWidth, evi.mi.bHeight);
     evi.gradient = almond->mw->getGradient();
-
+    */
     //}
 }
 
+
 void ExportVideoDialog::on_pushButton_clicked()
 {
     QString saveAs = QFileDialog::getSaveFileName(this,

+ 1 - 1
gradientchoosedialog.cpp

@@ -23,7 +23,7 @@ GradientChooseDialog::GradientChooseDialog()
 
 void GradientChooseDialog::on_buttonBox_accepted()
 {
-    chosenGradient = std::make_unique<Gradient>(Gradient::readXml(gcd.plainTextEdit->toPlainText()));
+    //chosenGradient = std::make_unique<Gradient>(Gradient::readXml(gcd.plainTextEdit->toPlainText()));
 }
 
 

+ 0 - 674
installer/packages/almond/meta/gpl3.txt

@@ -1,674 +0,0 @@
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.  We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors.  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights.  Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received.  You must make sure that they, too, receive
-or can get the source code.  And you must show them these terms so they
-know their rights.
-
-  Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
-  For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software.  For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
-  Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so.  This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software.  The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable.  Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products.  If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
-  Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary.  To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Use with the GNU Affero General Public License.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
-    <program>  Copyright (C) <year>  <name of author>
-    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
-
-  The GNU General Public License does not permit incorporating your program
-into proprietary programs.  If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.  But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 1 - 1
installer/packages/almond/meta/package.xml

@@ -5,7 +5,7 @@
     <Version>1.0.0</Version>
     <ReleaseDate>2020-05-04</ReleaseDate>
     <Licenses>
-        <License name="GNU General Public License 3" file="gpl3.txt" />
+        <License name="zlib/libpng License" file="zlib.txt" />
     </Licenses>
     <Default>true</Default>
     <!--<Default>script</Default>

+ 17 - 0
installer/packages/almond/meta/zlib.txt

@@ -0,0 +1,17 @@
+Copyright (c) 2020 Nicolas Winkler
+
+This software is provided 'as-is', without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+   claim that you wrote the original software. If you use this software
+   in a product, an acknowledgment in the product documentation would be
+   appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+   misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.

+ 16 - 4
libalmond/CMakeLists.txt

@@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.13)
 
 project(libalmond VERSION 1.0.0 DESCRIPTION "almond functionality")
 
+option(LIBALMOND_LIBJPEG "use libjpeg to export jpeg images" ON)
+option(LIBALMOND_LIBPNG "use libpng to export png images" ON)
+
 set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../CMakeModules)
 
 find_package(FFmpeg COMPONENTS AVCODEC AVDEVICE AVFORMAT AVUTIL SWSCALE REQUIRED)
@@ -12,6 +15,7 @@ add_subdirectory(../libmandel ./libmandel)
 set(Boost_USE_STATIC_LIBS ON)
 find_package(Boost 1.65 REQUIRED)
 find_package(PNG)
+find_package (JPEG)
 
 set(CMAKE_CXX_STANDARD 17)
 
@@ -27,10 +31,10 @@ set_target_properties(libalmond PROPERTIES OUTPUT_NAME almond)
 target_include_directories(libalmond SYSTEM PUBLIC ${FFMPEG_INCLUDE_DIRS})
 target_link_libraries(libalmond PUBLIC ${FFMPEG_LIBRARIES})
 
-if (PNG_FOUND)
-    target_include_directories(libalmond SYSTEM PUBLIC ${PNG_INCLUDE_DIRS})
-    target_link_libraries(libalmond PUBLIC ${PNG_LIBRARIES})
-else()
+if (PNG_FOUND AND LIBALMOND_LIBPNG)
+    target_link_libraries(libalmond PUBLIC PNG::PNG)
+    target_compile_definitions(libalmond PUBLIC WITH_LIBPNG)
+elseif(LIBALMOND_LIBPNG)
     set(PNG_BUILD_ZLIB ON CACHE BOOL "build zlib ourselves")
     add_subdirectory(zlib-1.2.11)
     set(ZLIB_LIBRARY zlibstatic)
@@ -46,8 +50,16 @@ else()
         get_filename_component(the_incluude ${header} DIRECTORY)
         list(APPEND PNG_PUB_INCLUDE ${the_incluude})
     endforeach()
+    #target_link_libraries(png_static PRIVATE zlibstatic)
     target_include_directories(libalmond PRIVATE ${PNG_PUB_INCLUDE})
     target_include_directories(libalmond PRIVATE ${ZLIB_INCLUDE_DIR})
     target_link_libraries(libalmond PRIVATE png_static)
+    target_compile_definitions(libalmond PUBLIC WITH_LIBPNG)
+endif()
+
+if (JPEG_FOUND AND LIBALMOND_LIBJPEG)
+    target_link_libraries(libalmond PUBLIC JPEG::JPEG)
+    target_compile_definitions(libalmond PUBLIC WITH_LIBJPEG)
 endif()
 
+

+ 1 - 1
libalmond/include/CubicSpline.h

@@ -11,7 +11,7 @@ class CubicSpline
     std::vector<std::tuple<float, float, float>> points;
     bool useSlopes;
 public:
-    CubicSpline(const std::vector<std::pair<float, float>>& dataPoints, bool useSlopes);
+    CubicSpline(const std::vector<std::pair<float, float>>& dataPoints, bool useSlopes, bool minSlopes);
 
     float interpolateAt(float x);
 };

+ 18 - 5
libalmond/include/Gradient.h

@@ -10,6 +10,7 @@
 
 class Gradient
 {
+    std::vector<std::pair<RGBColor, float>> points;
     /// the colors of this gradient stored in linear RGB format
     /// so they can be easily interpolated
     std::vector<RGBColorf> colors;
@@ -18,16 +19,28 @@ class Gradient
 public:
     Gradient(void);
     Gradient(std::vector<std::pair<RGBColor, float>> colors, bool repeat = false, int precalcSteps = -1);
+    Gradient(std::vector<std::pair<RGBColor, float>> colors, float maxValue, bool repeat = false, int precalcSteps = -1);
+
+    const std::vector<std::pair<RGBColor, float>>& getPoints(void) const;
 
     static Gradient defaultGradient(void);
 
     //static Gradient readXml(const std::string& xml);
 
-    /*!
-     * \brief get a color at a specific position in this gradient
-     * \param x the position
-     * \return the color in sRGB format
-     */
+    ///
+    /// \brief get the maximum value this gradient accepts
+    ///
+    /// If \link Gradient::get(float) is called with a value
+    /// greater than the one returned by this function, the
+    /// value will either get clamped or wrapped around.
+    ///
+    float getMax(void) const;
+
+    ///
+    /// \brief get a color at a specific position in this gradient
+    /// \param x the position
+    /// \return the color in sRGB format
+    ///
     RGBColor get(float x) const;
 private:
     static RGBColorf lerpColors(RGBColorf a, RGBColorf b, float val);

+ 21 - 3
libalmond/include/ImageExport.h

@@ -4,15 +4,30 @@
 #include "Mandel.h"
 #include "Gradient.h"
 #include <functional>
+#include <vector>
 #include <stdexcept>
 
 namespace alm
 {
+    enum class ImageFormat
+    {
+        BMP,
+        PNG,
+        JPEG
+    };
+    bool supportsImageFormat(ImageFormat imgf);
+
+    struct ImageOptions
+    {
+        int jpegQuality = 80;
+    };
+
     struct ImageExportInfo
     {
         mnd::MandelInfo drawInfo;
         mnd::MandelGenerator* generator;
         Gradient gradient;
+        ImageOptions options;
         std::string path;
     };
 
@@ -23,15 +38,18 @@ namespace alm
     };
 
     /**
-     * \brief generates and saves a fractal image in png format.
+     * \brief generates and saves a fractal image. The format
+     *        will be guessed by the file extension
      * 
      * \param iei               info to generate the image
      * \param progressCallback  optional function that is called to
      *                          report progress; the float parameter
      *                          contains a value from 0 to 100
      */
-    void exportPng(const ImageExportInfo& iei,
-        std::function<void(float)> progressCallback = [](float){});
+    void exportImage(const ImageExportInfo& iei,
+        std::function<void(float)> progressCallback = [](float){},
+        std::function<bool(void)> cancelCallback = [](void){ return false; }
+        );
 }
 
 

+ 21 - 4
libalmond/src/CubicSpline.cpp

@@ -1,6 +1,7 @@
 #include "CubicSpline.h"
+#include <cmath>
 
-CubicSpline::CubicSpline(const std::vector<std::pair<float, float> >& dataPoints, bool useSlopes) :
+CubicSpline::CubicSpline(const std::vector<std::pair<float, float> >& dataPoints, bool useSlopes, bool minSlopes) :
     useSlopes{ useSlopes }
 {
     if (dataPoints.size() < 2) {
@@ -24,8 +25,15 @@ CubicSpline::CubicSpline(const std::vector<std::pair<float, float> >& dataPoints
         float s1 = h1 / w1;
         float s2 = h2 / w2;
 
-        float avgSlope = (s1 + s2) / 2;
-        points.push_back({ dp2.first, dp2.second, avgSlope });
+        float slope;
+        if (minSlopes) {
+            if (fabs(s1) > fabs(s2))
+                slope = s2;
+            else
+                slope = s1;
+        } else
+            slope = (s1 + s2) / 2;
+        points.push_back({ dp2.first, dp2.second, slope });
     }
     points.push_back({ dataPoints[dataPoints.size() - 1].first, dataPoints[dataPoints.size() - 1].second,
                        (dataPoints[dataPoints.size() - 2].second - dataPoints[dataPoints.size() - 1].second) /
@@ -39,12 +47,21 @@ float CubicSpline::interpolateAt(float x)
     const static auto h01 = [] (float t) { return t * t * (3 - 2 * t); };
     const static auto h10 = [] (float t) { return t * (1 - t) * (1 - t); };
     const static auto h11 = [] (float t) { return t * t * (t - 1); };
+
+    if (points.empty()) {
+        return 0.0f;
+    }
+
+    if(std::get<0>(points[0]) > x) {
+        return std::get<1>(points[0]);
+    }
+
     for (auto it = points.begin(); it != points.end() && (it + 1) != points.end(); ++it) {
         auto& left = *it;
         auto& right = *(it + 1);
         float xleft = std::get<0>(left);
         float xright = std::get<0>(right);
-        if (xleft < x && xright >= x) {
+        if (xleft <= x && xright >= x) {
             float w = (xright - xleft);
             float t = (x - xleft) / w;
             float yleft = std::get<1>(left);

+ 65 - 4
libalmond/src/Gradient.cpp

@@ -23,6 +23,7 @@ Gradient::Gradient(std::vector<std::pair<RGBColor, float>> colors, bool repeat,
             return a.second < b.second;
         });
 
+    points = colors;
     max = colors.at(colors.size() - 1).second;
 
     std::vector<std::pair<RGBColorf, float>> linearColors;
@@ -40,9 +41,9 @@ Gradient::Gradient(std::vector<std::pair<RGBColor, float>> colors, bool repeat,
     std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(bs),
                    [] (auto p) { return std::pair{ p.second, p.first.b }; });
 
-    CubicSpline rsp(rs, false);
-    CubicSpline gsp(gs, false);
-    CubicSpline bsp(bs, false);
+    CubicSpline rsp(rs, false, true);
+    CubicSpline gsp(gs, false, true);
+    CubicSpline bsp(bs, false, true);
 
     if(precalcSteps <= 0) {
         precalcSteps = int(max * 15) + 10;
@@ -60,6 +61,60 @@ Gradient::Gradient(std::vector<std::pair<RGBColor, float>> colors, bool repeat,
 }
 
 
+Gradient::Gradient(std::vector<std::pair<RGBColor, float>> colors, float maxVal, bool repeat, int precalcSteps) :
+    repeat{ repeat }
+{
+    if(colors.empty() || colors.size() < 2)
+        return;
+    std::sort(colors.begin(), colors.end(),
+        [] (const auto& a, const auto& b) {
+            return a.second < b.second;
+        });
+
+    points = colors;
+    max = maxVal;
+
+    std::vector<std::pair<RGBColorf, float>> linearColors;
+    std::transform(colors.begin(), colors.end(), std::back_inserter(linearColors),
+                   [] (auto c) { return c; });
+
+    std::vector<std::pair<float, float>> rs;
+    std::vector<std::pair<float, float>> gs;
+    std::vector<std::pair<float, float>> bs;
+
+    std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(rs),
+                   [] (auto p) { return std::pair{ p.second, p.first.r }; });
+    std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(gs),
+                   [] (auto p) { return std::pair{ p.second, p.first.g }; });
+    std::transform(linearColors.begin(), linearColors.end(), std::back_inserter(bs),
+                   [] (auto p) { return std::pair{ p.second, p.first.b }; });
+
+    CubicSpline rsp(rs, false, true);
+    CubicSpline gsp(gs, false, true);
+    CubicSpline bsp(bs, false, true);
+
+    if(precalcSteps <= 0) {
+        precalcSteps = int(max * 7) + 10;
+    }
+
+    for (int i = 0; i < precalcSteps; i++) {
+        float position = i * max / precalcSteps;
+        RGBColorf at = {
+            rsp.interpolateAt(position),
+            gsp.interpolateAt(position),
+            bsp.interpolateAt(position)
+        };
+        this->colors.push_back(at);
+    }
+}
+
+
+const std::vector<std::pair<RGBColor, float>>& Gradient::getPoints(void) const
+{
+    return points;
+}
+
+
 Gradient Gradient::defaultGradient(void)
 {
     /*QFile res(":/gradients/default");
@@ -77,11 +132,17 @@ Gradient Gradient::defaultGradient(void)
         { RGBColor{ 20, 30, 180 }, 210.0f },
         { RGBColor{ 20, 190, 30 }, 240.0f },
         { RGBColor{ 120, 240, 120 }, 270.0f },
-        { RGBColor{ 40, 40, 40 }, 300.0f },
+        { RGBColor{ 0, 0, 0 }, 300.0f },
     }, true);
 }
 
 
+float Gradient::getMax(void) const
+{
+    return this->max;
+}
+
+
 RGBColor Gradient::get(float x) const
 {
     if (colors.empty() || std::isnan(x) || std::isinf(x))

+ 313 - 2
libalmond/src/ImageExport.cpp

@@ -1,17 +1,100 @@
 #include "ImageExport.h"
 #include "Bitmap.h"
-#include <png.h>
+#include <boost/endian/buffers.hpp>
 #include <cstdio>
 
+#ifdef WITH_LIBPNG
+extern "C" {
+#   include <png.h>
+}
+#endif // WITH_LIBPNG
+
+#ifdef WITH_LIBJPEG
+extern "C" {
+#   include <jpeglib.h>
+}
+#endif // WITH_LIBJPEG
+
+
 namespace alm
 {
 
+#ifdef WITH_LIBPNG
+void exportPng(const ImageExportInfo& iei,
+    std::function<void(float)> progressCallback,
+    std::function<bool(void)> cancelCallback);
+#endif // WITH_LIBPNG
+
+#ifdef WITH_LIBJPEG
+void exportJpeg(const ImageExportInfo& iei,
+    std::function<void(float)> progressCallback,
+    std::function<bool(void)> cancelCallback);
+#endif // WITH_LIBJPEG
+
+void exportBmp(const ImageExportInfo& iei,
+    std::function<void(float)> progressCallback,
+    std::function<bool(void)> cancelCallback);
+
+
+const std::vector<ImageFormat> supportedImageFormats = {
+    ImageFormat::BMP,
+#ifdef WITH_LIBPNG
+    ImageFormat::PNG,
+#endif // WITH_LIBPNG
+#ifdef WITH_LIBJPEG
+    ImageFormat::JPEG,
+#endif // WITH_LIBJPEG
+};
+
+bool supportsImageFormat(ImageFormat imgf)
+{
+    for (auto f : supportedImageFormats) {
+        if (imgf == f)
+            return true;
+    }
+    return false;
+}
+
 ImageExportException::ImageExportException(const std::string& err) :
     std::runtime_error{ err }
 {
 }
 
-void exportPng(const ImageExportInfo& iei, std::function<void(float)> progressCallback)
+void exportImage(const ImageExportInfo& iei,
+                 std::function<void(float)> progressCallback,
+                 std::function<bool(void)> cancelCallback)
+{
+    auto hasSuffix = [] (const std::string& path, const std::string& suffix) -> bool
+    {
+        return path.size() >= suffix.size() &&
+               path.compare(path.size() - suffix.size(),
+                        suffix.size(), suffix) == 0;
+    };
+    if (hasSuffix(iei.path, ".bmp") ||
+        hasSuffix(iei.path, ".dib")) {
+        exportBmp(iei, progressCallback, cancelCallback);
+    }
+#ifdef WITH_LIBPNG
+    else if (hasSuffix(iei.path, ".png")) {
+        exportPng(iei, progressCallback, cancelCallback);
+    }
+#endif // WITH_LIBPNG
+#ifdef WITH_LIBJPEG
+    else if (hasSuffix(iei.path, ".jpg") ||
+             hasSuffix(iei.path, ".jpeg")) {
+        exportJpeg(iei, progressCallback, cancelCallback);
+    }
+#endif // WITH_LIBJPEG
+    else {
+        throw ImageExportException{ "Could not guess image format" };
+    }
+}
+
+
+#ifdef WITH_LIBPNG
+void exportPng(const ImageExportInfo& iei,
+               std::function<void(float)> progressCallback,
+               std::function<bool(void)> cancelCallback)
 {
     if (iei.generator == nullptr) {
         throw "no generator";
@@ -79,6 +162,12 @@ void exportPng(const ImageExportInfo& iei, std::function<void(float)> progressCa
         png_write_rows(png, &rowPointers[0], tmpHeight);
         if (chunkY < height)
             progressCallback(100.0f * chunkY / height);
+
+        if (cancelCallback()) {
+            fclose(file);
+            png_destroy_write_struct(&png, &info);
+            return;
+        }
     }
 
     png_write_end(png, NULL);
@@ -88,8 +177,230 @@ void exportPng(const ImageExportInfo& iei, std::function<void(float)> progressCa
     png_destroy_write_struct(&png, &info);
     progressCallback(100.0f);
 }
+#endif // WITH_LIBPNG
+
+
+#ifdef WITH_LIBJPEG
+void exportJpeg(const ImageExportInfo& iei,
+                std::function<void(float)> progressCallback,
+                std::function<bool(void)> cancelCallback)
+{
+    if (iei.generator == nullptr) {
+        throw "no generator";
+    }
+
+    progressCallback(0.0f);
+
+    mnd::MandelGenerator& generator = *iei.generator;
+    FILE* file = fopen(iei.path.c_str(), "wb");
+    if(!file)
+        throw ImageExportException{ std::string("could not open file '") + iei.path + "'" };
+
+    const long width = iei.drawInfo.bWidth;
+    const long height = iei.drawInfo.bHeight;
+
+    struct jpeg_compress_struct jpegInfo;
+    struct jpeg_error_mgr jerr;
+
+    int rowLength = width * 3;
+
+    jpegInfo.err = jpeg_std_error(&jerr);
+    jerr.error_exit = [](j_common_ptr cinfo) {
+        auto errmsg = std::make_unique<char[]>(JMSG_LENGTH_MAX);
+        (cinfo->err->format_message)(cinfo, errmsg.get());
+        throw ImageExportException{ std::string("libjpeg error: ") + errmsg.get() };
+    };
+    jpeg_create_compress(&jpegInfo);
+
+    jpeg_stdio_dest(&jpegInfo, file);
+
+    jpegInfo.image_width = width;
+    jpegInfo.image_height = height;
+    jpegInfo.input_components = 3;
+    jpegInfo.in_color_space = JCS_RGB;
+
+    jpeg_set_defaults(&jpegInfo);
+    jpeg_set_quality(&jpegInfo, iei.options.jpegQuality, TRUE);
+    jpeg_start_compress(&jpegInfo, TRUE);
+
+
+    long chunkHeight = height / 20;
+    if (chunkHeight < 1)
+        chunkHeight = 1;
+    while (width * chunkHeight > 512 * 512)
+        chunkHeight /= 2;
+
+    auto rowPointers = std::make_unique<JSAMPROW[]>(chunkHeight);
+    for (long chunkY = 0; chunkY < height; chunkY += chunkHeight) {
+        auto minimum = [] (const auto& a, const auto& b) { return a < b ? a : b; };
+        long tmpHeight = minimum(chunkHeight, height - chunkY);
+        mnd::MandelInfo chunkInfo = iei.drawInfo;
+        chunkInfo.bHeight = tmpHeight;
+        chunkInfo.view.y += chunkInfo.view.height * chunkY / height;
+        chunkInfo.view.height *= mnd::Real(tmpHeight) / height;
+
+        Bitmap<float> chunk(width, tmpHeight);
+        generator.generate(chunkInfo, chunk.pixels.get());
+        Bitmap<RGBColor> coloredChunk = chunk.map<RGBColor>([&iei] (float i) {
+            return i >= iei.drawInfo.maxIter ? RGBColor{ 0, 0, 0 } : iei.gradient.get(i);
+        });
+        for (long i = 0; i < tmpHeight; i++) {
+            rowPointers[i] = reinterpret_cast<JSAMPROW>(&coloredChunk.get(0, i));
+        }
+        int written = jpeg_write_scanlines(&jpegInfo, rowPointers.get(), tmpHeight);
+        if (written < tmpHeight) {
+            throw ImageExportException{ "error writing jpeg rows" };
+        }
+        if (chunkY < height)
+            progressCallback(100.0f * chunkY / height);
+
+        if (cancelCallback()) {
+            fclose(file);
+            jpeg_destroy_compress(&jpegInfo);
+            return;
+        }
+    }
+
+    jpeg_finish_compress(&jpegInfo);
+    fclose(file);
+    jpeg_destroy_compress(&jpegInfo);
+    progressCallback(100.0f);
+}
+
+#endif // WITH_LIBJPEG
+
+
+void exportBmp(const ImageExportInfo& iei,
+               std::function<void(float)> progressCallback,
+               std::function<bool(void)> cancelCallback)
+{
+    if (iei.generator == nullptr) {
+        throw "no generator";
+    }
+
+    progressCallback(0.0f);
+
+    mnd::MandelGenerator& generator = *iei.generator;
+    FILE* file = fopen(iei.path.c_str(), "wb");
+    if(!file)
+        throw ImageExportException{ std::string("could not open file '") + iei.path + "'" };
+
+    const long width = iei.drawInfo.bWidth;
+    const long height = iei.drawInfo.bHeight;
+
+    // bmp needs rows to be 4-byte-aligned
+    const long rowLength = (width * 3 + 3) & ~0x03;
+
+    using namespace boost::endian;
 
+    const int fileHeaderSize = 14;
+    const int infoHeaderSize = 40;
+    const int pixelSize = rowLength * height;
 
+    // file header
+    little_int8_buf_t fhType[2];
+    little_uint32_buf_t fhSize;
+    little_uint16_buf_t fhReserved1;
+    little_uint16_buf_t fhReserved2;
+    little_uint32_buf_t fhOffset;
+    fhType[0] = 0x42; // 'B'
+    fhType[1] = 0x4D; // 'M'
+    fhSize = fileHeaderSize + infoHeaderSize + pixelSize;
+    fhReserved1 = 0;
+    fhReserved1 = 2;
+    fhOffset = fileHeaderSize + infoHeaderSize;
+
+    // info header
+    little_uint32_buf_t ihSize;
+    little_int32_buf_t ihWidth;
+    little_int32_buf_t ihHeight; // for top-down, height must be negative
+    little_uint16_buf_t ihPlanes;
+    little_uint16_buf_t ihBitCount;
+    little_uint32_buf_t ihCompression; // 0 = no compression
+    little_uint32_buf_t ihSizeImage; // for uncompressed bmps 0 can be set here
+    little_int32_buf_t ihXPixelsPerMeter;
+    little_int32_buf_t ihYPixelsPerMeter;
+    little_uint32_buf_t ihClrUsed;
+    little_uint32_buf_t ihClrImportant;
+    ihSize = infoHeaderSize;
+    ihWidth = width;
+    ihHeight = -height; // for top-down, height must be negative
+    ihPlanes = 1;
+    ihBitCount = 24;
+    ihCompression = 0; // 0 = no compression
+    ihSizeImage = 0; // for uncompressed bmps 0 can be set here
+    ihXPixelsPerMeter = 0;
+    ihYPixelsPerMeter = 0;
+    ihClrUsed = 0;
+    ihClrImportant = 0;
+    
+    auto writeCmp = [&file] (auto& buf) {
+        std::fwrite(&buf, sizeof(buf), 1, file);
+    };
+
+    writeCmp(fhType[0]);
+    writeCmp(fhType[1]);
+    writeCmp(fhSize);
+    writeCmp(fhReserved1);
+    writeCmp(fhReserved2);
+    writeCmp(fhOffset);
+
+    writeCmp(ihSize);
+    writeCmp(ihWidth);
+    writeCmp(ihHeight);
+    writeCmp(ihPlanes);
+    writeCmp(ihBitCount);
+    writeCmp(ihCompression);
+    writeCmp(ihSizeImage);
+    writeCmp(ihXPixelsPerMeter);
+    writeCmp(ihYPixelsPerMeter);
+    writeCmp(ihClrUsed);
+    writeCmp(ihClrImportant);
+
+    long chunkHeight = height / 20;
+    if (chunkHeight < 1)
+        chunkHeight = 1;
+    while (width * chunkHeight > 512 * 512)
+        chunkHeight /= 2;
+
+    for (long chunkY = 0; chunkY < height; chunkY += chunkHeight) {
+        auto minimum = [] (const auto& a, const auto& b) { return a < b ? a : b; };
+        long tmpHeight = minimum(chunkHeight, height - chunkY);
+        mnd::MandelInfo chunkInfo = iei.drawInfo;
+        chunkInfo.bHeight = tmpHeight;
+        chunkInfo.view.y += chunkInfo.view.height * chunkY / height;
+        chunkInfo.view.height *= mnd::Real(tmpHeight) / height;
+
+        Bitmap<float> chunk(width, tmpHeight);
+        generator.generate(chunkInfo, chunk.pixels.get());
+        for (long i = 0; i < tmpHeight; i++) {
+            for (long j = 0; j < width; j++) {
+                float iters = chunk.get(j, i);
+                RGBColor color =
+                    iters >= iei.drawInfo.maxIter ?
+                        RGBColor{ 0, 0, 0 } :
+                        iei.gradient.get(iters);
+                uint8_t bgr[3] = { color.b, color.g, color.r };
+                fwrite(bgr, 1, 3, file);
+            }
+
+            // write line padding
+            const uint8_t zeroes[] = { 0, 0, 0, 0 };
+            fwrite(zeroes, 1, rowLength - width * 3, file);
+        }
+        if (chunkY < height)
+            progressCallback(100.0f * chunkY / height);
+
+        if (cancelCallback()) {
+            fclose(file);
+            return;
+        }
+    }
+
+    fflush(file);
+    fclose(file);
+    progressCallback(100.0f);
+}
 }
 
 

+ 23 - 20
libmandel/CMakeLists.txt

@@ -2,16 +2,21 @@ cmake_minimum_required(VERSION 3.12)
 
 include(CheckCXXCompilerFlag)
 
-set(ARCH "X86_64" CACHE STRING "Target Architecture")
-option(AVX512 "generate code that can make use of avx-512-instructions" ON)
-option(WITH_ASMJIT "use just-in-time-compilation capabilities of asmjit" ON)
+project(mandel VERSION 1.0.0 DESCRIPTION "library for mandelbrot calculations")
+
+if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64")
+    set(MANDEL_TARGET_ARCHITECTURE "aarch64" CACHE STRING "Target Architecture")
+else()
+    set(MANDEL_TARGET_ARCHITECTURE "x86_64" CACHE STRING "Target Architecture")
+endif()
+option(MANDEL_AVX512 "generate code that can make use of avx-512-instructions" ON)
+option(MANDEL_ASMJIT "use just-in-time-compilation library asmjit" ON)
 option(MANDEL_BUILD_NATIVE
 	"use the -march=native flags if supported WARNING: when compiling with this flag, the binary might not run on machines other than the one it was compiled on"
 	OFF)
 
 #message(CMAKE_SYSTEM_PROCESSOR)
 
-project(mandel VERSION 1.0.0 DESCRIPTION "library for mandelbrot calculations")
 
 find_package(OpenCL REQUIRED)
 find_package(OpenMP)
@@ -42,12 +47,12 @@ SET(MandelSources
 )
 FILE(GLOB MandelHeaders include/*.h)
 
-if (ARCH STREQUAL "X86_64" OR ARCH STREQUAL "X86")
+if (MANDEL_TARGET_ARCHITECTURE STREQUAL "x86_64" OR MANDEL_TARGET_ARCHITECTURE STREQUAL "x86")
     list(APPEND MandelSources src/CpuGeneratorsAVX.cpp src/CpuGeneratorsAVXFMA.cpp src/CpuGeneratorsSSE2.cpp)
-    if (AVX512)
+    if (MANDEL_AVX512)
         list(APPEND MandelSources src/CpuGeneratorsAVX512.cpp)
     endif()
-elseif(ARCH STREQUAL "ARM")
+elseif(MANDEL_TARGET_ARCHITECTURE STREQUAL "aarch64")
     list(APPEND MandelSources src/CpuGeneratorsNeon.cpp)
 endif()
 
@@ -71,27 +76,25 @@ FILE(GLOB QdSources qd-2.3.22/src/*.cpp)
 
 target_compile_definitions(mandel PUBLIC WITH_QD)
 add_library(qd STATIC ${QdSources})
+target_include_directories(mandel PUBLIC "include")
 target_include_directories(qd PUBLIC qd-2.3.22/include qd-2.3.22)
 
 target_link_libraries(mandel PUBLIC qd)
 
-if(WITH_ASMJIT)
-	add_subdirectory(asmjit)
-	target_compile_definitions(mandel PUBLIC WITH_ASMJIT)
-	target_link_libraries(mandel PUBLIC asmjit)
-endif(WITH_ASMJIT)
+if(MANDEL_ASMJIT)
+    add_subdirectory(asmjit)
+    target_compile_definitions(mandel PUBLIC MANDEL_ASMJIT)
+    target_link_libraries(mandel PUBLIC AsmJit::AsmJit)
+endif(MANDEL_ASMJIT)
 
 
 if(OPENCL_FOUND)
     target_compile_definitions(mandel PUBLIC WITH_OPENCL)
-    target_include_directories(mandel SYSTEM PUBLIC
-        ${OpenCL_INCLUDE_DIRS}
-    )
-    target_include_directories(mandel PUBLIC "include")
+    target_include_directories(mandel SYSTEM PUBLIC ${OpenCL_INCLUDE_DIRS})
+    target_include_directories(mandel SYSTEM PUBLIC "include_cl")
     link_directories(${OpenCL_LIBRARY})
     target_link_libraries(mandel PUBLIC OpenCL::OpenCL)
 else(OPENCL_FOUND)
-    include_directories("include")
 endif(OPENCL_FOUND)
 
 if (APPLE AND OpenCL_FOUND)
@@ -109,8 +112,8 @@ if(Boost_FOUND)
     target_link_libraries(mandel PRIVATE ${Boost_LIBRARIES})
 endif(Boost_FOUND)
 
-if (ARCH STREQUAL "X86_64" OR ARCH STREQUAL "X86")
-    if (AVX512)
+if (MANDEL_TARGET_ARCHITECTURE STREQUAL "x86_64" OR MANDEL_TARGET_ARCHITECTURE STREQUAL "x86")
+    if (MANDEL_AVX512)
         target_compile_definitions(mandel PUBLIC WITH_AVX512)
         if (MSVC)
             set_source_files_properties(src/CpuGeneratorsAVX512.cpp PROPERTIES COMPILE_FLAGS /arch:AVX512F)
@@ -129,6 +132,6 @@ if (ARCH STREQUAL "X86_64" OR ARCH STREQUAL "X86")
         set_source_files_properties(src/CpuGeneratorsSSE2.cpp PROPERTIES COMPILE_FLAGS -msse2)
     endif(MSVC)
 
-elseif(ARCH STREQUAL "ARM")
+elseif(MANDEL_TARGET_ARCHITECTURE STREQUAL "aarch64")
     set_source_files_properties(src/CpuGeneratorsNeon.cpp PROPERTIES COMPILE_FLAGS -march=armv8-a+simd)
 endif()

+ 14 - 12
libmandel/asmjit/CMakeLists.txt

@@ -403,18 +403,20 @@ if (NOT ASMJIT_EMBED)
   # Add AsmJit::AsmJit target (alias to asmjit).
   add_library(AsmJit::AsmJit ALIAS asmjit)
 
-  # Install 'asmjit' target (shared or static).
-  install(TARGETS asmjit RUNTIME DESTINATION "bin"
-                         LIBRARY DESTINATION "lib${LIB_SUFFIX}"
-                         ARCHIVE DESTINATION "lib${LIB_SUFFIX}")
-
-  # Install 'asmjit' header files (private headers are filtered out).
-  foreach(_src_file ${ASMJIT_SRC_LIST})
-    if ("${_src_file}" MATCHES "\\.h$" AND NOT "${_src_file}" MATCHES "_p\\.h$")
-      get_filename_component(_src_dir ${_src_file} PATH)
-      install(FILES "${ASMJIT_DIR}/src/${_src_file}" DESTINATION "include/${_src_dir}")
-    endif()
-  endforeach()
+  if (MANDEL_INSTALL_ASMJIT)
+    # Install 'asmjit' target (shared or static).
+    install(TARGETS asmjit RUNTIME DESTINATION "bin"
+                           LIBRARY DESTINATION "lib${LIB_SUFFIX}"
+                           ARCHIVE DESTINATION "lib${LIB_SUFFIX}")
+  
+    # Install 'asmjit' header files (private headers are filtered out).
+    foreach(_src_file ${ASMJIT_SRC_LIST})
+      if ("${_src_file}" MATCHES "\\.h$" AND NOT "${_src_file}" MATCHES "_p\\.h$")
+        get_filename_component(_src_dir ${_src_file} PATH)
+        install(FILES "${ASMJIT_DIR}/src/${_src_file}" DESTINATION "include/${_src_dir}")
+      endif()
+    endforeach()
+  endif(MANDEL_INSTALL_ASMJIT)
 
   # Add 'asmjit' tests.
   if (ASMJIT_TEST)

+ 4 - 5
libmandel/include/CpuGenerators.h

@@ -162,12 +162,11 @@ public:
     virtual void generate(const MandelInfo& info, float* data);
 };
 
-#else //if defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) 
+#elif defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) 
 template<bool parallel>
-class mnd::CpuGenerator<float, mnd::ARM_NEON, parallel> : public Generator
+class mnd::CpuGenerator<float, mnd::ARM_NEON, parallel> : public MandelGenerator
 {
 public:
-    CpuGenerator(void);
     inline CpuGenerator(void) :
         MandelGenerator{ mnd::Precision::FLOAT, mnd::ARM_NEON }
     {
@@ -177,7 +176,7 @@ public:
 
 
 template<bool parallel>
-class mnd::CpuGenerator<double, mnd::ARM_NEON, parallel> : public Generator
+class mnd::CpuGenerator<double, mnd::ARM_NEON, parallel> : public MandelGenerator
 {
 public:
     inline CpuGenerator(void) :
@@ -189,7 +188,7 @@ public:
 
 
 template<bool parallel>
-class mnd::CpuGenerator<DoubleDouble, mnd::ARM_NEON, parallel> : public Generator
+class mnd::CpuGenerator<mnd::DoubleDouble, mnd::ARM_NEON, parallel> : public MandelGenerator
 {
 public:
     inline CpuGenerator(void) :

+ 2 - 1
libmandel/include/Mandel.h

@@ -43,6 +43,7 @@ class mnd::MandelDevice
 private:
     friend class MandelContext;
 
+    std::string platformName;
     std::string vendor;
     std::string name;
     std::string extensions;
@@ -51,7 +52,7 @@ private:
     std::map<GeneratorType, std::unique_ptr<MandelGenerator>> mandelGenerators;
 
 public:
-    MandelDevice(ClDeviceWrapper);
+    MandelDevice(ClDeviceWrapper, const std::string& platformName);
     MandelDevice(const MandelDevice&) = delete;
     MandelDevice(MandelDevice&&) = default;
     MandelDevice& operator=(const MandelDevice&) = delete;

+ 17 - 1
libmandel/include/Types.h

@@ -84,6 +84,22 @@ namespace mnd
 
     using Real = Float512;
     using Integer = boost::multiprecision::int512_t;
+    /*boost::multiprecision::number<
+        boost::multiprecision::backends::cpp_bin_float<
+            1500, boost::multiprecision::backends::digit_base_2, void, boost::int16_t, -16382, 16383>,
+            boost::multiprecision::et_off>;
+
+    inline Real abs(const Real& x) { return boost::multiprecision::abs(x); }
+    inline Real sqrt(const Real& x) { return boost::multiprecision::sqrt(x); }
+    inline Real floor(const Real& x) { return boost::multiprecision::floor(x); }
+    inline Real log(const Real& x) { return boost::multiprecision::log(x); }
+    inline Real log2(const Real& x) { return boost::multiprecision::log2(x); }
+    inline Real pow(const Real& x, const Real& y) { return boost::multiprecision::pow(x, y); }
+    inline Real atan2(const Real& y, const Real& x) { return boost::multiprecision::atan2(y, x); }
+    inline Real cos(const Real& x) { return boost::multiprecision::cos(x); }
+    inline Real sin(const Real& x) { return boost::multiprecision::sin(x); }
+    inline Real exp(const Real& x) { return boost::multiprecision::exp(x); }
+*/
 #else
     using Real = double;
     using Integer = int64_t;
@@ -201,7 +217,7 @@ namespace mnd
     template<>
     inline Fixed64 convert<Fixed64, Real>(const Real& x)
     {
-        return Fixed64(double(x));
+        return Fixed64{ static_cast<int64_t>(x * 0xFFFFFFFFFFFFLL), true };
     }
 
     template<>

+ 0 - 0
libmandel/include/CL/cl.hpp → libmandel/include_cl/CL/cl.hpp


+ 0 - 0
libmandel/include/CL/cl2.hpp → libmandel/include_cl/CL/cl2.hpp


+ 2 - 0
libmandel/src/ClGenerators.cpp

@@ -116,6 +116,8 @@ ClGeneratorFloat::ClGeneratorFloat(mnd::MandelDevice& device, const std::string&
 {
     const cl::Device& dev = device.getClDevice().device;
     useVec = dev.getInfo<CL_DEVICE_PREFERRED_VECTOR_WIDTH_FLOAT>() >= 4;
+    // often still slower than non-vec variation
+    useVec = false;
     kernel = Kernel(program, useVec ? "iterate_vec4" : "iterate");
 }
 

+ 10 - 5
libmandel/src/CpuGeneratorsAVXFMA.cpp

@@ -98,6 +98,9 @@ void CpuGenerator<float, mnd::X86_AVX_FMA, parallel>::generate(const mnd::Mandel
                     __m256 ab = _mm256_mul_ps(a, b);
                     __m256 ab2 = _mm256_mul_ps(a2, b2);
                     __m256 ab3 = _mm256_mul_ps(a3, b3);
+                    __m256 olda = a;
+                    __m256 olda2 = a2;
+                    __m256 olda3 = a3;
                     a = _mm256_add_ps(_mm256_fmsub_ps(a, a, bb), cx);
                     a2 = _mm256_add_ps(_mm256_fmsub_ps(a2, a2, bb2), cx2);
                     a3 = _mm256_add_ps(_mm256_fmsub_ps(a3, a3, bb3), cx3);
@@ -116,9 +119,9 @@ void CpuGenerator<float, mnd::X86_AVX_FMA, parallel>::generate(const mnd::Mandel
                     resultsb2 = _mm256_blendv_ps(resultsb2, b2, cmp2);
                     resultsa3 = _mm256_blendv_ps(resultsa3, a3, cmp3);
                     resultsb3 = _mm256_blendv_ps(resultsb3, b3, cmp3);
-                    cmp = _mm256_cmp_ps(_mm256_fmadd_ps(a, a, bb), threshold, _CMP_LE_OQ);
-                    cmp2 = _mm256_cmp_ps(_mm256_fmadd_ps(a2, a2, bb2), threshold, _CMP_LE_OQ);
-                    cmp3 = _mm256_cmp_ps(_mm256_fmadd_ps(a3, a3, bb3), threshold, _CMP_LE_OQ);
+                    cmp = _mm256_cmp_ps(_mm256_fmadd_ps(olda, olda, bb), threshold, _CMP_LE_OQ);
+                    cmp2 = _mm256_cmp_ps(_mm256_fmadd_ps(olda2, olda2, bb2), threshold, _CMP_LE_OQ);
+                    cmp3 = _mm256_cmp_ps(_mm256_fmadd_ps(olda3, olda3, bb3), threshold, _CMP_LE_OQ);
                     adder = _mm256_and_ps(adder, cmp);
                     counter = _mm256_add_ps(counter, adder);
                     adder2 = _mm256_and_ps(adder2, cmp2);
@@ -254,8 +257,10 @@ void CpuGenerator<double, mnd::X86_AVX_FMA, parallel>::generate(const mnd::Mande
             __m256d cmp = _mm256_cmp_pd(threshold, threshold, _CMP_LE_OQ);
             __m256d cmp2 = _mm256_cmp_pd(threshold, threshold, _CMP_LE_OQ);
             for (int k = 0; k < info.maxIter; k++) {
+                __m256d aa = _mm256_mul_pd(a, a);
                 __m256d ab = _mm256_mul_pd(a, b);
                 __m256d bb = _mm256_mul_pd(b, b);
+                __m256d aa2 = _mm256_mul_pd(a2, a2);
                 __m256d ab2 = _mm256_mul_pd(a2, b2);
                 __m256d bb2 = _mm256_mul_pd(b2, b2);
                 a = _mm256_fmsub_pd(a, a, bb);
@@ -270,8 +275,8 @@ void CpuGenerator<double, mnd::X86_AVX_FMA, parallel>::generate(const mnd::Mande
                     resultsa2 = _mm256_blendv_pd(resultsa2, a2, cmp2);
                     resultsb2 = _mm256_blendv_pd(resultsb2, b2, cmp2);
                 }
-                cmp = _mm256_cmp_pd(_mm256_fmadd_pd(a, a, bb), threshold, _CMP_LE_OQ);
-                cmp2 = _mm256_cmp_pd(_mm256_fmadd_pd(a2, a2, bb2), threshold, _CMP_LE_OQ);
+                cmp = _mm256_cmp_pd(_mm256_add_pd(aa, bb), threshold, _CMP_LE_OQ);
+                cmp2 = _mm256_cmp_pd(_mm256_add_pd(aa2, bb2), threshold, _CMP_LE_OQ);
                 adder = _mm256_and_pd(adder, cmp);
                 adder2 = _mm256_and_pd(adder2, cmp2);
                 counter = _mm256_add_pd(counter, adder);

+ 2 - 4
libmandel/src/CpuGeneratorsNeon.cpp

@@ -35,8 +35,7 @@ void CpuGenerator<float, mnd::ARM_NEON, parallel>::generate(const mnd::MandelInf
 #endif
     for (long j = 0; j < info.bHeight; j++) {
         T y = T(view.y) + T(j) * T(view.height / info.bHeight);
-        long i = 0;
-        for (i; i < info.bWidth; i += 4) {
+        for (long i = 0; i < info.bWidth; i += 4) {
             float xsvals[] = {
                 float(view.x + float(i) * view.width / info.bWidth),
                 float(view.x + float(i + 1) * view.width / info.bWidth),
@@ -128,8 +127,7 @@ void CpuGenerator<double, mnd::ARM_NEON, parallel>::generate(const mnd::MandelIn
 #endif
     for (long j = 0; j < info.bHeight; j++) {
         T y = T(view.y) + T(j) * T(view.height / info.bHeight);
-        long i = 0;
-        for (i; i < info.bWidth; i += 2) {
+        for (long i = 0; i < info.bWidth; i += 2) {
             double xsvals[] = {
                 double(view.x + double(i) * view.width / info.bWidth),
                 double(view.x + double(i + 1) * view.width / info.bWidth),

+ 16 - 7
libmandel/src/Mandel.cpp

@@ -83,8 +83,9 @@ MandelContext mnd::initializeContext(void)
 }
 
 
-MandelDevice::MandelDevice(mnd::ClDeviceWrapper device) :
-    clDevice{ std::make_unique<ClDeviceWrapper>(std::move(device)) }
+MandelDevice::MandelDevice(mnd::ClDeviceWrapper device, const std::string& platformName) :
+    clDevice{ std::make_unique<ClDeviceWrapper>(std::move(device)) },
+    platformName{ platformName }
 {
     extensions = clDevice->device.getInfo<CL_DEVICE_EXTENSIONS>();
     name = clDevice->device.getInfo<CL_DEVICE_NAME>();
@@ -272,17 +273,25 @@ std::vector<std::unique_ptr<MandelDevice>> MandelContext::createDevices(void)
     //platforms.erase(platforms.begin() + 1);
 
     for (auto& platform : platforms) {
-        std::string name = platform.getInfo<CL_PLATFORM_NAME>();
+        std::string platformName = platform.getInfo<CL_PLATFORM_NAME>();
         std::string profile = platform.getInfo<CL_PLATFORM_PROFILE>();
 
-        //printf("using opencl platform: %s\n", name.c_str());
+        //printf("using opencl platform: %s\n", platformName.c_str());
 
-        //std::string ext = platform.getInfo<CL_PLATFORM_EXTENSIONS>();
+        std::string ext = platform.getInfo<CL_PLATFORM_EXTENSIONS>();
         //printf("Platform extensions: %s\n", ext.c_str());
-        //printf("Platform: %s, %s\n", name.c_str(), profile.c_str());
+        //printf("Platform: %s, %s\n", platformName.c_str(), profile.c_str());
 
         std::vector<cl::Device> devices;
         platform.getDevices(CL_DEVICE_TYPE_GPU, &devices);
+        auto onError = [] (const char* errinfo, 
+            const void* private_info,
+            size_t cb, 
+            void* user_data) {
+                printf("opencl error: %s\n", errinfo);
+        };
+        
+        cl::Context context{ devices, nullptr, onError };
         for (auto& device : devices) {
             //printf("Device: %s\n", device.getInfo<CL_DEVICE_NAME>().c_str());
             //printf("preferred float width: %d\n", device.getInfo<CL_DEVICE_PREFERRED_VECTOR_WIDTH_FLOAT>());
@@ -291,7 +300,7 @@ std::vector<std::unique_ptr<MandelDevice>> MandelContext::createDevices(void)
 
             //printf("Device extensions: %s\n", ext.c_str());
             auto mandelDevice = std::make_unique<mnd::MandelDevice>(
-                ClDeviceWrapper{ device, cl::Context{ device } });
+                ClDeviceWrapper{ device, context }, platformName);
             MandelDevice& md = *mandelDevice;
 
             auto supportsDouble = md.supportsDouble();

+ 13 - 11
libmandel/src/opencl/float.cl

@@ -38,9 +38,9 @@ __kernel void iterate_vec4(__global float* A, const int width, float xl, float y
    float4 b = (float4) (y * pixelScaleY + yt);
    float4 ca = julia ? ((float4)(juliaX)) : a;
    float4 cb = julia ? ((float4)(juliaY)) : b;
-   float4 resa = a;
-   float4 resb = b;
-   int4 count = (int4)(0);
+   float4 resa = (float4)(0);
+   float4 resb = (float4)(0);
+   float4 count = (float4)(0);
 
    int n = 0;
    if (smooth) {
@@ -54,7 +54,7 @@ __kernel void iterate_vec4(__global float* A, const int width, float xl, float y
            resb = as_float4((as_int4(b) & cmp) | (as_int4(resb) & ~cmp));
            cmp = isless(cmpVal, (float4)(16.0f));
            if (!any(cmp)) break;
-           count += cmp & (int4)(1);
+           count += as_float4(cmp & as_int4((float4)(1)));
            n++;
        }
    }
@@ -66,14 +66,16 @@ __kernel void iterate_vec4(__global float* A, const int width, float xl, float y
            b = fma(2, ab, cb);
            int4 cmp = isless(cmpVal, (float4)(16.0f));
            if (!any(cmp)) break;
-           count += cmp & (int4)(1);
+           count += as_float4(cmp & as_int4((float4)(1)));
            n++;
        }
     }
-   for (int i = 0; i < 4 && i + x < width; i++) {
-       if (smooth != 0)
-           A[index + i] = ((float) count[i]) + 1 - log(log(fma(resa[i], resa[i], resb[i] * resb[i])) / 2) / log(2.0f);
-      else
-          A[index + i] = ((float) count[i]);
+    for (int i = 0; i < 4 && i + x < width; i++) {
+    if (smooth != 0) {
+        if (count[i] >= 0)
+            A[index + i] = ((float) count[i]) + 1 - log(log(fma(resa[i], resa[i], resb[i] * resb[i])) / 2) / log(2.0f);
+    }
+    else
+        A[index + i] = ((float) count[i]);
    }
-}
+}

+ 212 - 210
libmandel/src/opencl/float.h

@@ -1,101 +1,98 @@
 unsigned char float_cl[] = {
-  0x0d, 0x0a, 0x5f, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x76,
-  0x6f, 0x69, 0x64, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x65, 0x28,
-  0x5f, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x20, 0x66, 0x6c, 0x6f,
-  0x61, 0x74, 0x2a, 0x20, 0x41, 0x2c, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x74,
-  0x20, 0x69, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x2c, 0x20,
-  0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x78, 0x6c, 0x2c, 0x20, 0x66, 0x6c,
-  0x6f, 0x61, 0x74, 0x20, 0x79, 0x74, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65,
-  0x58, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x70, 0x69, 0x78,
-  0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x59, 0x2c, 0x20, 0x69, 0x6e,
-  0x74, 0x20, 0x6d, 0x61, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x73,
-  0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6a,
-  0x75, 0x6c, 0x69, 0x61, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20,
-  0x6a, 0x75, 0x6c, 0x69, 0x61, 0x58, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x59, 0x29, 0x20, 0x7b, 0x0d,
-  0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x64, 0x65,
-  0x78, 0x20, 0x3d, 0x20, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x6c, 0x6f, 0x62,
-  0x61, 0x6c, 0x5f, 0x69, 0x64, 0x28, 0x30, 0x29, 0x3b, 0x0d, 0x0a, 0x20,
-  0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e,
-  0x64, 0x65, 0x78, 0x20, 0x25, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x79, 0x20, 0x3d,
-  0x20, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x2f, 0x20, 0x77, 0x69, 0x64,
-  0x74, 0x68, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x20, 0x61, 0x20, 0x3d, 0x20, 0x78, 0x20, 0x2a, 0x20, 0x70, 0x69,
-  0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x58, 0x20, 0x2b, 0x20,
-  0x78, 0x6c, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x20, 0x62, 0x20, 0x3d, 0x20, 0x79, 0x20, 0x2a, 0x20, 0x70, 0x69,
-  0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x59, 0x20, 0x2b, 0x20,
-  0x79, 0x74, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x20, 0x63, 0x61, 0x20, 0x3d, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61,
-  0x20, 0x21, 0x3d, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x6a, 0x75, 0x6c, 0x69,
-  0x61, 0x58, 0x20, 0x3a, 0x20, 0x61, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20,
-  0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x63, 0x62, 0x20, 0x3d, 0x20, 0x6a,
-  0x75, 0x6c, 0x69, 0x61, 0x20, 0x21, 0x3d, 0x20, 0x30, 0x20, 0x3f, 0x20,
-  0x6a, 0x75, 0x6c, 0x69, 0x61, 0x59, 0x20, 0x3a, 0x20, 0x62, 0x3b, 0x0d,
-  0x0a, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6e, 0x20,
-  0x3d, 0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x77, 0x68, 0x69,
-  0x6c, 0x65, 0x20, 0x28, 0x6e, 0x20, 0x3c, 0x20, 0x6d, 0x61, 0x78, 0x20,
-  0x2d, 0x20, 0x31, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20,
+  0x0a, 0x5f, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x76, 0x6f,
+  0x69, 0x64, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x65, 0x28, 0x5f,
+  0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x20, 0x66, 0x6c, 0x6f, 0x61,
+  0x74, 0x2a, 0x20, 0x41, 0x2c, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20,
+  0x69, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x2c, 0x20, 0x66,
+  0x6c, 0x6f, 0x61, 0x74, 0x20, 0x78, 0x6c, 0x2c, 0x20, 0x66, 0x6c, 0x6f,
+  0x61, 0x74, 0x20, 0x79, 0x74, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74,
+  0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x58,
+  0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x70, 0x69, 0x78, 0x65,
+  0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x59, 0x2c, 0x20, 0x69, 0x6e, 0x74,
+  0x20, 0x6d, 0x61, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x73, 0x6d,
+  0x6f, 0x6f, 0x74, 0x68, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6a, 0x75,
+  0x6c, 0x69, 0x61, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x6a,
+  0x75, 0x6c, 0x69, 0x61, 0x58, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74,
+  0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x59, 0x29, 0x20, 0x7b, 0x0a, 0x20,
+  0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20,
+  0x3d, 0x20, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c,
+  0x5f, 0x69, 0x64, 0x28, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x69,
+  0x6e, 0x74, 0x20, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x78,
+  0x20, 0x25, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x69, 0x6e, 0x74, 0x20, 0x79, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x64,
+  0x65, 0x78, 0x20, 0x2f, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x61, 0x20, 0x3d,
+  0x20, 0x78, 0x20, 0x2a, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63,
+  0x61, 0x6c, 0x65, 0x58, 0x20, 0x2b, 0x20, 0x78, 0x6c, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x62, 0x20, 0x3d, 0x20,
+  0x79, 0x20, 0x2a, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61,
+  0x6c, 0x65, 0x59, 0x20, 0x2b, 0x20, 0x79, 0x74, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x63, 0x61, 0x20, 0x3d, 0x20,
+  0x6a, 0x75, 0x6c, 0x69, 0x61, 0x20, 0x21, 0x3d, 0x20, 0x30, 0x20, 0x3f,
+  0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x58, 0x20, 0x3a, 0x20, 0x61, 0x3b,
+  0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x63, 0x62,
+  0x20, 0x3d, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x20, 0x21, 0x3d, 0x20,
+  0x30, 0x20, 0x3f, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x59, 0x20, 0x3a,
+  0x20, 0x62, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x20,
+  0x6e, 0x20, 0x3d, 0x20, 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x77, 0x68,
+  0x69, 0x6c, 0x65, 0x20, 0x28, 0x6e, 0x20, 0x3c, 0x20, 0x6d, 0x61, 0x78,
+  0x20, 0x2d, 0x20, 0x31, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20,
   0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x61, 0x61, 0x20,
-  0x3d, 0x20, 0x61, 0x20, 0x2a, 0x20, 0x61, 0x3b, 0x0d, 0x0a, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x62,
-  0x62, 0x20, 0x3d, 0x20, 0x62, 0x20, 0x2a, 0x20, 0x62, 0x3b, 0x0d, 0x0a,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74,
-  0x20, 0x61, 0x62, 0x20, 0x3d, 0x20, 0x61, 0x20, 0x2a, 0x20, 0x62, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x20, 0x3d,
-  0x20, 0x61, 0x61, 0x20, 0x2d, 0x20, 0x62, 0x62, 0x20, 0x2b, 0x20, 0x63,
-  0x61, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62,
-  0x20, 0x3d, 0x20, 0x61, 0x62, 0x20, 0x2b, 0x20, 0x61, 0x62, 0x20, 0x2b,
-  0x20, 0x63, 0x62, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x69, 0x66, 0x20, 0x28, 0x61, 0x61, 0x20, 0x2b, 0x20, 0x62, 0x62,
-  0x20, 0x3e, 0x20, 0x31, 0x36, 0x29, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b,
-  0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6e, 0x2b,
-  0x2b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20,
-  0x20, 0x69, 0x66, 0x20, 0x28, 0x6e, 0x20, 0x3e, 0x3d, 0x20, 0x6d, 0x61,
-  0x78, 0x20, 0x2d, 0x20, 0x31, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x41, 0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78,
-  0x5d, 0x20, 0x3d, 0x20, 0x6d, 0x61, 0x78, 0x3b, 0x0d, 0x0a, 0x20, 0x20,
-  0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73, 0x65, 0x20,
-  0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66,
+  0x3d, 0x20, 0x61, 0x20, 0x2a, 0x20, 0x61, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x62, 0x62,
+  0x20, 0x3d, 0x20, 0x62, 0x20, 0x2a, 0x20, 0x62, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x61,
+  0x62, 0x20, 0x3d, 0x20, 0x61, 0x20, 0x2a, 0x20, 0x62, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x20, 0x3d, 0x20, 0x61, 0x61,
+  0x20, 0x2d, 0x20, 0x62, 0x62, 0x20, 0x2b, 0x20, 0x63, 0x61, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x20, 0x3d, 0x20, 0x61,
+  0x62, 0x20, 0x2b, 0x20, 0x61, 0x62, 0x20, 0x2b, 0x20, 0x63, 0x62, 0x3b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28,
+  0x61, 0x61, 0x20, 0x2b, 0x20, 0x62, 0x62, 0x20, 0x3e, 0x20, 0x31, 0x36,
+  0x29, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x6e, 0x2b, 0x2b, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x7d, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x6e, 0x20, 0x3e,
+  0x3d, 0x20, 0x6d, 0x61, 0x78, 0x20, 0x2d, 0x20, 0x31, 0x29, 0x20, 0x7b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x41, 0x5b, 0x69, 0x6e,
+  0x64, 0x65, 0x78, 0x5d, 0x20, 0x3d, 0x20, 0x6d, 0x61, 0x78, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73, 0x65,
+  0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66,
   0x20, 0x28, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x20, 0x21, 0x3d, 0x20,
-  0x30, 0x29, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x41, 0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5d, 0x20,
-  0x3d, 0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29, 0x6e, 0x29,
-  0x20, 0x2b, 0x20, 0x31, 0x20, 0x2d, 0x20, 0x6c, 0x6f, 0x67, 0x28, 0x6c,
-  0x6f, 0x67, 0x28, 0x61, 0x20, 0x2a, 0x20, 0x61, 0x20, 0x2b, 0x20, 0x62,
-  0x20, 0x2a, 0x20, 0x62, 0x29, 0x20, 0x2f, 0x20, 0x32, 0x29, 0x20, 0x2f,
-  0x20, 0x6c, 0x6f, 0x67, 0x28, 0x32, 0x2e, 0x30, 0x66, 0x29, 0x3b, 0x0d,
-  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73, 0x65,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x41, 0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5d, 0x20, 0x3d, 0x20,
-  0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29, 0x6e, 0x29, 0x3b, 0x0d,
-  0x0a, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x7d, 0x0d, 0x0a, 0x0d, 0x0a,
-  0x0d, 0x0a, 0x5f, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x76,
-  0x6f, 0x69, 0x64, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x65, 0x5f,
-  0x76, 0x65, 0x63, 0x34, 0x28, 0x5f, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61,
-  0x6c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x2a, 0x20, 0x41, 0x2c, 0x20,
-  0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x77, 0x69,
-  0x64, 0x74, 0x68, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x78,
-  0x6c, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x79, 0x74, 0x2c,
-  0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c,
-  0x53, 0x63, 0x61, 0x6c, 0x65, 0x58, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65,
-  0x59, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6d, 0x61, 0x78, 0x2c, 0x20,
-  0x69, 0x6e, 0x74, 0x20, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x2c, 0x20,
-  0x69, 0x6e, 0x74, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x2c, 0x20, 0x66,
-  0x6c, 0x6f, 0x61, 0x74, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x58, 0x2c,
-  0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61,
-  0x59, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74,
-  0x20, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x3d, 0x20, 0x67, 0x65, 0x74,
-  0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x28, 0x30,
-  0x29, 0x20, 0x2a, 0x20, 0x34, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69,
-  0x6e, 0x74, 0x20, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x78,
-  0x20, 0x25, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b, 0x0d, 0x0a, 0x20,
-  0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x79, 0x20, 0x3d, 0x20, 0x69, 0x6e,
-  0x64, 0x65, 0x78, 0x20, 0x2f, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20,
+  0x30, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x41, 0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5d, 0x20, 0x3d,
+  0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29, 0x6e, 0x29, 0x20,
+  0x2b, 0x20, 0x31, 0x20, 0x2d, 0x20, 0x6c, 0x6f, 0x67, 0x28, 0x6c, 0x6f,
+  0x67, 0x28, 0x61, 0x20, 0x2a, 0x20, 0x61, 0x20, 0x2b, 0x20, 0x62, 0x20,
+  0x2a, 0x20, 0x62, 0x29, 0x20, 0x2f, 0x20, 0x32, 0x29, 0x20, 0x2f, 0x20,
+  0x6c, 0x6f, 0x67, 0x28, 0x32, 0x2e, 0x30, 0x66, 0x29, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73, 0x65, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x41, 0x5b,
+  0x69, 0x6e, 0x64, 0x65, 0x78, 0x5d, 0x20, 0x3d, 0x20, 0x28, 0x28, 0x66,
+  0x6c, 0x6f, 0x61, 0x74, 0x29, 0x6e, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x7d, 0x0a, 0x7d, 0x0a, 0x0a, 0x0a, 0x5f, 0x5f, 0x6b, 0x65, 0x72, 0x6e,
+  0x65, 0x6c, 0x20, 0x76, 0x6f, 0x69, 0x64, 0x20, 0x69, 0x74, 0x65, 0x72,
+  0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x63, 0x34, 0x28, 0x5f, 0x5f, 0x67,
+  0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x2a,
+  0x20, 0x41, 0x2c, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20, 0x69, 0x6e,
+  0x74, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x2c, 0x20, 0x66, 0x6c, 0x6f,
+  0x61, 0x74, 0x20, 0x78, 0x6c, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74,
+  0x20, 0x79, 0x74, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x70,
+  0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x58, 0x2c, 0x20,
+  0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53,
+  0x63, 0x61, 0x6c, 0x65, 0x59, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6d,
+  0x61, 0x78, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x73, 0x6d, 0x6f, 0x6f,
+  0x74, 0x68, 0x2c, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6a, 0x75, 0x6c, 0x69,
+  0x61, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x6a, 0x75, 0x6c,
+  0x69, 0x61, 0x58, 0x2c, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x6a,
+  0x75, 0x6c, 0x69, 0x61, 0x59, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20,
+  0x69, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x3d, 0x20,
+  0x67, 0x65, 0x74, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x69,
+  0x64, 0x28, 0x30, 0x29, 0x20, 0x2a, 0x20, 0x34, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x69, 0x6e, 0x74, 0x20, 0x78, 0x20, 0x3d, 0x20, 0x69, 0x6e, 0x64,
+  0x65, 0x78, 0x20, 0x25, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x79, 0x20, 0x3d, 0x20, 0x69,
+  0x6e, 0x64, 0x65, 0x78, 0x20, 0x2f, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68,
+  0x3b, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20,
   0x61, 0x20, 0x3d, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29,
   0x20, 0x28, 0x78, 0x20, 0x2a, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53,
   0x63, 0x61, 0x6c, 0x65, 0x58, 0x20, 0x2b, 0x20, 0x78, 0x6c, 0x2c, 0x20,
@@ -106,123 +103,128 @@ unsigned char float_cl[] = {
   0x58, 0x20, 0x2b, 0x20, 0x78, 0x6c, 0x2c, 0x20, 0x28, 0x78, 0x20, 0x2b,
   0x20, 0x33, 0x29, 0x20, 0x2a, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53,
   0x63, 0x61, 0x6c, 0x65, 0x58, 0x20, 0x2b, 0x20, 0x78, 0x6c, 0x29, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20,
-  0x62, 0x20, 0x3d, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29,
-  0x20, 0x28, 0x79, 0x20, 0x2a, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53,
-  0x63, 0x61, 0x6c, 0x65, 0x59, 0x20, 0x2b, 0x20, 0x79, 0x74, 0x29, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20,
-  0x63, 0x61, 0x20, 0x3d, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x20, 0x3f,
-  0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x6a,
-  0x75, 0x6c, 0x69, 0x61, 0x58, 0x29, 0x29, 0x20, 0x3a, 0x20, 0x61, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20,
-  0x63, 0x62, 0x20, 0x3d, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x20, 0x3f,
-  0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x6a,
-  0x75, 0x6c, 0x69, 0x61, 0x59, 0x29, 0x29, 0x20, 0x3a, 0x20, 0x62, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20,
-  0x72, 0x65, 0x73, 0x61, 0x20, 0x3d, 0x20, 0x61, 0x3b, 0x0d, 0x0a, 0x20,
-  0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x72, 0x65, 0x73,
-  0x62, 0x20, 0x3d, 0x20, 0x62, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69,
-  0x6e, 0x74, 0x34, 0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x3d, 0x20,
-  0x28, 0x69, 0x6e, 0x74, 0x34, 0x29, 0x28, 0x30, 0x29, 0x3b, 0x0d, 0x0a,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x20, 0x6e, 0x20, 0x3d,
-  0x20, 0x30, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28,
-  0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x34, 0x20, 0x63,
-  0x6d, 0x70, 0x20, 0x3d, 0x20, 0x69, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x28,
-  0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x31, 0x36, 0x2e,
-  0x30, 0x66, 0x29, 0x2c, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34,
-  0x29, 0x28, 0x31, 0x36, 0x2e, 0x30, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x68, 0x69, 0x6c, 0x65,
-  0x20, 0x28, 0x6e, 0x20, 0x3c, 0x20, 0x6d, 0x61, 0x78, 0x29, 0x20, 0x7b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x62,
+  0x20, 0x3d, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x20,
+  0x28, 0x79, 0x20, 0x2a, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63,
+  0x61, 0x6c, 0x65, 0x59, 0x20, 0x2b, 0x20, 0x79, 0x74, 0x29, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x63, 0x61,
+  0x20, 0x3d, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x20, 0x3f, 0x20, 0x28,
+  0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x6a, 0x75, 0x6c,
+  0x69, 0x61, 0x58, 0x29, 0x29, 0x20, 0x3a, 0x20, 0x61, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x63, 0x62, 0x20,
+  0x3d, 0x20, 0x6a, 0x75, 0x6c, 0x69, 0x61, 0x20, 0x3f, 0x20, 0x28, 0x28,
+  0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x6a, 0x75, 0x6c, 0x69,
+  0x61, 0x59, 0x29, 0x29, 0x20, 0x3a, 0x20, 0x62, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x72, 0x65, 0x73, 0x61,
+  0x20, 0x3d, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28,
+  0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74,
+  0x34, 0x20, 0x72, 0x65, 0x73, 0x62, 0x20, 0x3d, 0x20, 0x28, 0x66, 0x6c,
+  0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x30, 0x29, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x63, 0x6f, 0x75, 0x6e,
+  0x74, 0x20, 0x3d, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29,
+  0x28, 0x30, 0x29, 0x3b, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74,
+  0x20, 0x6e, 0x20, 0x3d, 0x20, 0x30, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x69,
+  0x66, 0x20, 0x28, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x29, 0x20, 0x7b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x34,
+  0x20, 0x63, 0x6d, 0x70, 0x20, 0x3d, 0x20, 0x69, 0x73, 0x6c, 0x65, 0x73,
+  0x73, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x31,
+  0x36, 0x2e, 0x30, 0x66, 0x29, 0x2c, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61,
+  0x74, 0x34, 0x29, 0x28, 0x31, 0x36, 0x2e, 0x30, 0x66, 0x29, 0x29, 0x3b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x68, 0x69, 0x6c,
+  0x65, 0x20, 0x28, 0x6e, 0x20, 0x3c, 0x20, 0x6d, 0x61, 0x78, 0x29, 0x20,
+  0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
   0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x61, 0x62, 0x20, 0x3d,
-  0x20, 0x61, 0x20, 0x2a, 0x20, 0x62, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20,
+  0x20, 0x61, 0x20, 0x2a, 0x20, 0x62, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74,
+  0x34, 0x20, 0x63, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x20, 0x3d, 0x20, 0x66,
+  0x6d, 0x61, 0x28, 0x61, 0x2c, 0x20, 0x61, 0x2c, 0x20, 0x62, 0x20, 0x2a,
+  0x20, 0x62, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x61, 0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61, 0x28,
+  0x61, 0x2c, 0x20, 0x61, 0x2c, 0x20, 0x2d, 0x66, 0x6d, 0x61, 0x28, 0x62,
+  0x2c, 0x20, 0x62, 0x2c, 0x20, 0x2d, 0x63, 0x61, 0x29, 0x29, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62,
+  0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61, 0x28, 0x32, 0x2c, 0x20, 0x61, 0x62,
+  0x2c, 0x20, 0x63, 0x62, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x73, 0x61, 0x20, 0x3d,
+  0x20, 0x61, 0x73, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x28, 0x28,
+  0x61, 0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28, 0x61, 0x29, 0x20, 0x26,
+  0x20, 0x63, 0x6d, 0x70, 0x29, 0x20, 0x7c, 0x20, 0x28, 0x61, 0x73, 0x5f,
+  0x69, 0x6e, 0x74, 0x34, 0x28, 0x72, 0x65, 0x73, 0x61, 0x29, 0x20, 0x26,
+  0x20, 0x7e, 0x63, 0x6d, 0x70, 0x29, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x73, 0x62,
+  0x20, 0x3d, 0x20, 0x61, 0x73, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34,
+  0x28, 0x28, 0x61, 0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28, 0x62, 0x29,
+  0x20, 0x26, 0x20, 0x63, 0x6d, 0x70, 0x29, 0x20, 0x7c, 0x20, 0x28, 0x61,
+  0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28, 0x72, 0x65, 0x73, 0x62, 0x29,
+  0x20, 0x26, 0x20, 0x7e, 0x63, 0x6d, 0x70, 0x29, 0x29, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6d,
+  0x70, 0x20, 0x3d, 0x20, 0x69, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x28, 0x63,
+  0x6d, 0x70, 0x56, 0x61, 0x6c, 0x2c, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61,
+  0x74, 0x34, 0x29, 0x28, 0x31, 0x36, 0x2e, 0x30, 0x66, 0x29, 0x29, 0x3b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x69, 0x66, 0x20, 0x28, 0x21, 0x61, 0x6e, 0x79, 0x28, 0x63, 0x6d, 0x70,
+  0x29, 0x29, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x75,
+  0x6e, 0x74, 0x20, 0x2b, 0x3d, 0x20, 0x61, 0x73, 0x5f, 0x66, 0x6c, 0x6f,
+  0x61, 0x74, 0x34, 0x28, 0x63, 0x6d, 0x70, 0x20, 0x26, 0x20, 0x61, 0x73,
+  0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74,
+  0x34, 0x29, 0x28, 0x31, 0x29, 0x29, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6e, 0x2b, 0x2b, 0x3b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20,
+  0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73, 0x65, 0x20,
+  0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x77, 0x68, 0x69,
+  0x6c, 0x65, 0x20, 0x28, 0x6e, 0x20, 0x3c, 0x20, 0x6d, 0x61, 0x78, 0x29,
+  0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x61, 0x62, 0x20,
+  0x3d, 0x20, 0x61, 0x20, 0x2a, 0x20, 0x62, 0x3b, 0x0a, 0x20, 0x20, 0x20,
   0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61,
   0x74, 0x34, 0x20, 0x63, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x20, 0x3d, 0x20,
   0x66, 0x6d, 0x61, 0x28, 0x61, 0x2c, 0x20, 0x61, 0x2c, 0x20, 0x62, 0x20,
-  0x2a, 0x20, 0x62, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x20, 0x3d, 0x20, 0x66, 0x6d,
-  0x61, 0x28, 0x61, 0x2c, 0x20, 0x61, 0x2c, 0x20, 0x2d, 0x66, 0x6d, 0x61,
-  0x28, 0x62, 0x2c, 0x20, 0x62, 0x2c, 0x20, 0x2d, 0x63, 0x61, 0x29, 0x29,
-  0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x62, 0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61, 0x28, 0x32, 0x2c,
-  0x20, 0x61, 0x62, 0x2c, 0x20, 0x63, 0x62, 0x29, 0x3b, 0x0d, 0x0a, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65,
-  0x73, 0x61, 0x20, 0x3d, 0x20, 0x61, 0x73, 0x5f, 0x66, 0x6c, 0x6f, 0x61,
-  0x74, 0x34, 0x28, 0x28, 0x61, 0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28,
-  0x61, 0x29, 0x20, 0x26, 0x20, 0x63, 0x6d, 0x70, 0x29, 0x20, 0x7c, 0x20,
-  0x28, 0x61, 0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28, 0x72, 0x65, 0x73,
-  0x61, 0x29, 0x20, 0x26, 0x20, 0x7e, 0x63, 0x6d, 0x70, 0x29, 0x29, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x72, 0x65, 0x73, 0x62, 0x20, 0x3d, 0x20, 0x61, 0x73, 0x5f, 0x66,
-  0x6c, 0x6f, 0x61, 0x74, 0x34, 0x28, 0x28, 0x61, 0x73, 0x5f, 0x69, 0x6e,
-  0x74, 0x34, 0x28, 0x62, 0x29, 0x20, 0x26, 0x20, 0x63, 0x6d, 0x70, 0x29,
-  0x20, 0x7c, 0x20, 0x28, 0x61, 0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28,
-  0x72, 0x65, 0x73, 0x62, 0x29, 0x20, 0x26, 0x20, 0x7e, 0x63, 0x6d, 0x70,
-  0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x63, 0x6d, 0x70, 0x20, 0x3d, 0x20, 0x69, 0x73,
-  0x6c, 0x65, 0x73, 0x73, 0x28, 0x63, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x2c,
-  0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x31, 0x36,
-  0x2e, 0x30, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x21,
-  0x61, 0x6e, 0x79, 0x28, 0x63, 0x6d, 0x70, 0x29, 0x29, 0x20, 0x62, 0x72,
-  0x65, 0x61, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x20, 0x2b,
-  0x3d, 0x20, 0x63, 0x6d, 0x70, 0x20, 0x26, 0x20, 0x28, 0x69, 0x6e, 0x74,
-  0x34, 0x29, 0x28, 0x31, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6e, 0x2b, 0x2b, 0x3b, 0x0d,
-  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20,
-  0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73,
-  0x65, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x77, 0x68, 0x69, 0x6c, 0x65, 0x20, 0x28, 0x6e, 0x20, 0x3c, 0x20, 0x6d,
-  0x61, 0x78, 0x29, 0x20, 0x7b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34,
-  0x20, 0x61, 0x62, 0x20, 0x3d, 0x20, 0x61, 0x20, 0x2a, 0x20, 0x62, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34, 0x20, 0x63, 0x6d, 0x70, 0x56,
-  0x61, 0x6c, 0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61, 0x28, 0x61, 0x2c, 0x20,
-  0x61, 0x2c, 0x20, 0x62, 0x20, 0x2a, 0x20, 0x62, 0x29, 0x3b, 0x0d, 0x0a,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61,
-  0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61, 0x28, 0x61, 0x2c, 0x20, 0x61, 0x2c,
-  0x20, 0x2d, 0x66, 0x6d, 0x61, 0x28, 0x62, 0x2c, 0x20, 0x62, 0x2c, 0x20,
-  0x2d, 0x63, 0x61, 0x29, 0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x62, 0x20, 0x3d, 0x20, 0x66,
-  0x6d, 0x61, 0x28, 0x32, 0x2c, 0x20, 0x61, 0x62, 0x2c, 0x20, 0x63, 0x62,
-  0x29, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x34, 0x20, 0x63, 0x6d, 0x70, 0x20,
-  0x3d, 0x20, 0x69, 0x73, 0x6c, 0x65, 0x73, 0x73, 0x28, 0x63, 0x6d, 0x70,
-  0x56, 0x61, 0x6c, 0x2c, 0x20, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x34,
-  0x29, 0x28, 0x31, 0x36, 0x2e, 0x30, 0x66, 0x29, 0x29, 0x3b, 0x0d, 0x0a,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69,
-  0x66, 0x20, 0x28, 0x21, 0x61, 0x6e, 0x79, 0x28, 0x63, 0x6d, 0x70, 0x29,
-  0x29, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0d, 0x0a, 0x20, 0x20,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x75,
-  0x6e, 0x74, 0x20, 0x2b, 0x3d, 0x20, 0x63, 0x6d, 0x70, 0x20, 0x26, 0x20,
-  0x28, 0x69, 0x6e, 0x74, 0x34, 0x29, 0x28, 0x31, 0x29, 0x3b, 0x0d, 0x0a,
-  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6e,
-  0x2b, 0x2b, 0x3b, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x7d, 0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x20, 0x20,
-  0x20, 0x66, 0x6f, 0x72, 0x20, 0x28, 0x69, 0x6e, 0x74, 0x20, 0x69, 0x20,
-  0x3d, 0x20, 0x30, 0x3b, 0x20, 0x69, 0x20, 0x3c, 0x20, 0x34, 0x20, 0x26,
-  0x26, 0x20, 0x69, 0x20, 0x2b, 0x20, 0x78, 0x20, 0x3c, 0x20, 0x77, 0x69,
-  0x64, 0x74, 0x68, 0x3b, 0x20, 0x69, 0x2b, 0x2b, 0x29, 0x20, 0x7b, 0x0d,
-  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28,
-  0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x20, 0x21, 0x3d, 0x20, 0x30, 0x29,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x20, 0x41, 0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x2b, 0x20, 0x69,
-  0x5d, 0x20, 0x3d, 0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29,
-  0x20, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5b, 0x69, 0x5d, 0x29, 0x20, 0x2b,
-  0x20, 0x31, 0x20, 0x2d, 0x20, 0x6c, 0x6f, 0x67, 0x28, 0x6c, 0x6f, 0x67,
-  0x28, 0x66, 0x6d, 0x61, 0x28, 0x72, 0x65, 0x73, 0x61, 0x5b, 0x69, 0x5d,
-  0x2c, 0x20, 0x72, 0x65, 0x73, 0x61, 0x5b, 0x69, 0x5d, 0x2c, 0x20, 0x72,
-  0x65, 0x73, 0x62, 0x5b, 0x69, 0x5d, 0x20, 0x2a, 0x20, 0x72, 0x65, 0x73,
-  0x62, 0x5b, 0x69, 0x5d, 0x29, 0x29, 0x20, 0x2f, 0x20, 0x32, 0x29, 0x20,
-  0x2f, 0x20, 0x6c, 0x6f, 0x67, 0x28, 0x32, 0x2e, 0x30, 0x66, 0x29, 0x3b,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73, 0x65,
-  0x0d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
-  0x41, 0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x2b, 0x20, 0x69, 0x5d,
-  0x20, 0x3d, 0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29, 0x20,
-  0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5b, 0x69, 0x5d, 0x29, 0x3b, 0x0d, 0x0a,
-  0x20, 0x20, 0x20, 0x7d, 0x0d, 0x0a, 0x7d
+  0x2a, 0x20, 0x62, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61,
+  0x28, 0x61, 0x2c, 0x20, 0x61, 0x2c, 0x20, 0x2d, 0x66, 0x6d, 0x61, 0x28,
+  0x62, 0x2c, 0x20, 0x62, 0x2c, 0x20, 0x2d, 0x63, 0x61, 0x29, 0x29, 0x3b,
+  0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x62, 0x20, 0x3d, 0x20, 0x66, 0x6d, 0x61, 0x28, 0x32, 0x2c, 0x20, 0x61,
+  0x62, 0x2c, 0x20, 0x63, 0x62, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6e, 0x74, 0x34, 0x20,
+  0x63, 0x6d, 0x70, 0x20, 0x3d, 0x20, 0x69, 0x73, 0x6c, 0x65, 0x73, 0x73,
+  0x28, 0x63, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x2c, 0x20, 0x28, 0x66, 0x6c,
+  0x6f, 0x61, 0x74, 0x34, 0x29, 0x28, 0x31, 0x36, 0x2e, 0x30, 0x66, 0x29,
+  0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x21, 0x61, 0x6e, 0x79, 0x28, 0x63,
+  0x6d, 0x70, 0x29, 0x29, 0x20, 0x62, 0x72, 0x65, 0x61, 0x6b, 0x3b, 0x0a,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63,
+  0x6f, 0x75, 0x6e, 0x74, 0x20, 0x2b, 0x3d, 0x20, 0x61, 0x73, 0x5f, 0x66,
+  0x6c, 0x6f, 0x61, 0x74, 0x34, 0x28, 0x63, 0x6d, 0x70, 0x20, 0x26, 0x20,
+  0x61, 0x73, 0x5f, 0x69, 0x6e, 0x74, 0x34, 0x28, 0x28, 0x66, 0x6c, 0x6f,
+  0x61, 0x74, 0x34, 0x29, 0x28, 0x31, 0x29, 0x29, 0x29, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6e, 0x2b,
+  0x2b, 0x3b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a,
+  0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x6f,
+  0x72, 0x20, 0x28, 0x69, 0x6e, 0x74, 0x20, 0x69, 0x20, 0x3d, 0x20, 0x30,
+  0x3b, 0x20, 0x69, 0x20, 0x3c, 0x20, 0x34, 0x20, 0x26, 0x26, 0x20, 0x69,
+  0x20, 0x2b, 0x20, 0x78, 0x20, 0x3c, 0x20, 0x77, 0x69, 0x64, 0x74, 0x68,
+  0x3b, 0x20, 0x69, 0x2b, 0x2b, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20,
+  0x20, 0x69, 0x66, 0x20, 0x28, 0x73, 0x6d, 0x6f, 0x6f, 0x74, 0x68, 0x20,
+  0x21, 0x3d, 0x20, 0x30, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x63, 0x6f, 0x75, 0x6e,
+  0x74, 0x5b, 0x69, 0x5d, 0x20, 0x3e, 0x3d, 0x20, 0x30, 0x29, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x41,
+  0x5b, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x2b, 0x20, 0x69, 0x5d, 0x20,
+  0x3d, 0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29, 0x20, 0x63,
+  0x6f, 0x75, 0x6e, 0x74, 0x5b, 0x69, 0x5d, 0x29, 0x20, 0x2b, 0x20, 0x31,
+  0x20, 0x2d, 0x20, 0x6c, 0x6f, 0x67, 0x28, 0x6c, 0x6f, 0x67, 0x28, 0x66,
+  0x6d, 0x61, 0x28, 0x72, 0x65, 0x73, 0x61, 0x5b, 0x69, 0x5d, 0x2c, 0x20,
+  0x72, 0x65, 0x73, 0x61, 0x5b, 0x69, 0x5d, 0x2c, 0x20, 0x72, 0x65, 0x73,
+  0x62, 0x5b, 0x69, 0x5d, 0x20, 0x2a, 0x20, 0x72, 0x65, 0x73, 0x62, 0x5b,
+  0x69, 0x5d, 0x29, 0x29, 0x20, 0x2f, 0x20, 0x32, 0x29, 0x20, 0x2f, 0x20,
+  0x6c, 0x6f, 0x67, 0x28, 0x32, 0x2e, 0x30, 0x66, 0x29, 0x3b, 0x0a, 0x20,
+  0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x6c, 0x73,
+  0x65, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x41, 0x5b,
+  0x69, 0x6e, 0x64, 0x65, 0x78, 0x20, 0x2b, 0x20, 0x69, 0x5d, 0x20, 0x3d,
+  0x20, 0x28, 0x28, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x29, 0x20, 0x63, 0x6f,
+  0x75, 0x6e, 0x74, 0x5b, 0x69, 0x5d, 0x29, 0x3b, 0x0a, 0x20, 0x20, 0x20,
+  0x7d, 0x0a, 0x7d, 0x0a
 };
-unsigned int float_cl_len = 2695;
+unsigned int float_cl_len = 2716;

+ 2 - 0
main.cpp

@@ -8,6 +8,8 @@
 int main(int argc, char *argv[])
 {
     QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+    QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+    QApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
     QApplication a(argc, argv);
 
     QSize screenDim = QGuiApplication::screens()[0]->size();

+ 14 - 10
mandelvid/src/main.cpp

@@ -27,7 +27,7 @@ int main() {
     //evi.end.zoomCenter(1.0e+27);
     evi.gradient = Gradient::defaultGradient();
 
-    evi.mi.bWidth = 1280;
+    /*evi.mi.bWidth = 1280;
     evi.mi.bHeight = 720;
     evi.mi.maxIter = 1000;
     evi.fps = 60;
@@ -40,24 +40,28 @@ int main() {
 
     MandelVideoGenerator mvg(evi);
 
-    mvg.generate(mndCtxt.getDefaultGenerator());
-    //
+    mvg.generate(mndCtxt.getDefaultGenerator());*/
     
 
-/*
     mnd::MandelContext mc = mnd::initializeContext();
     mnd::MandelInfo mi;
     mi.view = evi.start;
-    mi.bWidth = 8000;
-    mi.bHeight = 8000;
-    mi.maxIter = 100;
+    mi.bWidth = 12000;
+    mi.bHeight = 12000;
+    mi.maxIter = 800;
     mi.smooth = true;
     alm::ImageExportInfo iei;
     iei.drawInfo = mi;
-    iei.gradient = &evi.gradient;
+    iei.gradient = evi.gradient;
     iei.generator = &mc.getDefaultGenerator();
-    alm::exportPng("file.png", iei);
-*/
+    iei.options.jpegQuality = 100;
+    iei.path = "file.jpg";
+    try {
+        alm::exportImage(iei);
+    } catch (alm::ImageExportException& iee) {
+        printf("%s\n", iee.what());
+        return 1;
+    }
 
     return 0;
 }