Sfoglia il codice sorgente

new gradient system partly working

Nicolas Winkler 5 anni fa
parent
commit
ba338d0008

+ 39 - 10
Almond.cpp

@@ -20,8 +20,9 @@ Almond::Almond(QWidget* parent) :
     //mw = std::make_unique<MandelWidget>(mandelContext,
     //                                    &mandelContext.getDefaultGenerator(),
     //                                    ui.centralWidget);
-    fractalWidget = new FractalZoomWidget(this);
+    fractalWidget = new FractalWidget(this);
     fractalWidget->setGenerator(&mandelContext.getDefaultGenerator());
+    fractalWidget->setGradient(Gradient::defaultGradient());
     customGeneratorDialog = std::make_unique<CustomGenerator>(mandelContext);
     customGenerator = nullptr;
     customViewSave = mnd::MandelViewport::centerView();
@@ -58,6 +59,35 @@ Almond::Almond(QWidget* parent) :
     connect(gradientSm, &AlmondSubMenu::accepted, this, &Almond::gradientEditOk);
 
 
+    connect(gradientMenu, &GradientMenu::gradientChanged, [this] () {
+
+        std::vector<std::pair<RGBColor, float>> np;
+        const auto& points = gradientMenu->getGradient();
+        std::transform(points.begin(), points.end(), std::back_inserter(np),
+            [](auto& qp) -> std::pair<RGBColor, float> {
+            auto& [pos, col] = qp;
+            return { RGBColor{ uint8_t(col.red()), uint8_t(col.green()), uint8_t(col.blue()) },
+                pos };
+        });
+        std::sort(np.begin(), np.end(), [](auto& a, auto& b) { return a.second < b.second; });
+        if (!np.empty()) {
+            auto& first = np.at(0);
+            if (first.second > 0) {
+                np.insert(np.begin(), { first.first, 0.0f });
+            }
+            auto& last = np.at(np.size() - 1);
+            if (last.second < 1) {
+                np.insert(np.begin(), { last.first, 1.0f });
+            }
+        }
+
+        std::for_each(np.begin(), np.end(), [](auto& x) { x.second *= 300; });
+
+        Gradient grad{ np, true };
+        fractalWidget->setGradient(grad);
+    });
+
+
     /*QStatusBar* bar = new QStatusBar(this);
     bar->addWidget(new QLabel("ayay"));
     auto* p = new QPushButton("About");
@@ -225,7 +255,7 @@ void Almond::gradientEditOk(void)
     std::for_each(np.begin(), np.end(), [](auto& x) { x.second *= 300; });
 
     Gradient g{ np, true };
-    //mw->setGradient(std::move(g)); // TODO update
+    fractalWidget->setGradient(std::move(g));
     amw->showMainMenu();
 }
 
@@ -292,13 +322,13 @@ void Almond::backgroundTaskProgress(float percentage)
 
 void Almond::on_zoom_out_clicked()
 {
-    // TODO update mw->zoom(2);
+    fractalWidget->zoom(2);
 }
 
 
 void Almond::on_zoom_in_clicked()
 {
-    // TODO update mw->zoom(0.5);
+    fractalWidget->zoom(0.5);
 }
 
 
@@ -312,8 +342,7 @@ void Almond::on_maxIterations_editingFinished()
 
 void Almond::on_chooseGradient_clicked()
 {
-    /*
-    const auto& gradient = mw->getGradient(); // TODO update
+    const auto& gradient = fractalWidget->getGradient(); // TODO update
     auto points = gradient.getPoints();
     std::for_each(points.begin(), points.end(), [](auto& x) { x.second /= 300; });
 
@@ -323,7 +352,7 @@ void Almond::on_chooseGradient_clicked()
         auto& [col, pos] = qp;
         return { pos, QColor{ (col.r), (col.g), (col.b) } };
     });
-    this->gradientMenu->setGradient(std::move(np));*/
+    this->gradientMenu->setGradient(std::move(np));
     emit this->amw->showSubMenu(2);
     //gcd.exec();
     //auto gradient = gcd.getGradient();
@@ -374,10 +403,10 @@ void Almond::on_exportImage_clicked()
 void Almond::on_resetZoom_clicked()
 {
     if (currentView == MANDELBROT) {
-        // TODO update mw->setViewport(mnd::MandelViewport::standardView());
+        fractalWidget->setViewport(mnd::MandelViewport::standardView());
     }
     else {
-        // TODO update mw->setViewport(mnd::MandelViewport::centerView());
+        fractalWidget->setViewport(mnd::MandelViewport::centerView());
     }
 }
 
@@ -409,7 +438,7 @@ void Almond::on_chooseGenerator_clicked()
             customGenerator = gen.get();
         }
         currentGenerator = gen.get();
-        // TODO update this->mw->setGenerator(currentGenerator);
+        this->fractalWidget->setGenerator(currentGenerator);
         adjustedGenerators.push_back(std::move(gen));
     }
     else {

+ 2 - 2
Almond.h

@@ -13,7 +13,7 @@
 #include "choosegenerators.h"
 #include "customgenerator.h"
 
-#include "FractalZoomWidget.h"
+#include "FractalWidget.h"
 
 #include "AlmondMenuWidget.h"
 #include "ExportImageMenu.h"
@@ -54,7 +54,7 @@ private:
     bool fullscreenMode = false;
     QWidget* cw;
 public:
-    FractalZoomWidget* fractalWidget;
+    FractalWidget* fractalWidget;
 private:
     //std::unique_ptr<BenchmarkDialog> benchmarkDialog;
     std::unique_ptr<CustomGenerator> customGeneratorDialog;

+ 2 - 0
Almond.pro

@@ -33,6 +33,7 @@ SOURCES += \
         EscapeTimeVisualWidget.cpp \
         ExportImageMenu.cpp \
         ExportVideoMenu.cpp \
+        FractalWidget.cpp \
         FractalWidgetUtils.cpp \
         FractalZoomWidget.cpp \
         GradientMenu.cpp \
@@ -53,6 +54,7 @@ HEADERS += \
         EscapeTimeVisualWidget.h \
         ExportImageMenu.h \
         ExportVideoMenu.h \
+        FractalWidget.h \
         FractalWidgetUtils.h \
         FractalZoomWidget.h \
         GradientMenu.h \

+ 2 - 2
Almond.ui

@@ -9,8 +9,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>1202</width>
-    <height>1192</height>
+    <width>1333</width>
+    <height>1141</height>
    </rect>
   </property>
   <property name="windowTitle">

+ 92 - 12
EscapeTimeVisualWidget.cpp

@@ -6,9 +6,11 @@
 #include <QOpenGLContext>
 #include <QOpenGLFunctions>
 
+#include <vector>
 
 
-ETVImage::ETVImage(EscapeTimeVisualWidget& owner) :
+ETVImage::ETVImage(EscapeTimeVisualWidget& owner,
+                   const Bitmap<float>& img) :
     owner{ owner }
 {
     auto& gl = *owner.context()->functions();
@@ -16,18 +18,20 @@ ETVImage::ETVImage(EscapeTimeVisualWidget& owner) :
     gl.glActiveTexture(GL_TEXTURE0);
     gl.glBindTexture(GL_TEXTURE_2D, textureId);
 
-    Bitmap<float> img{512, 512};
-    for (int i = 0; i < img.width; i++) {
-        for (int j = 0; j < img.height; j++) {
-            img.get(i, j) = (i ^ j);
+    {
+    /*Bitmap<float> img2 = img.map<float>([](float x) { return x; });
+    for (int i = 0; i < img2.width; i++) {
+        for (int j = 0; j < img2.height; j++) {
+            img2.get(i, j) = img.get(i, j) * i + j;
         }
-    }
+    }*/
     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.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
     gl.glBindTexture(GL_TEXTURE_2D, 0);
+    }
 }
 
 
@@ -89,8 +93,23 @@ void ETVImage::draw(float x, float y, float w, float h,
 
 
 EscapeTimeVisualWidget::EscapeTimeVisualWidget(QWidget* parent) :
-    QOpenGLWidget{ parent }
+    QOpenGLWidget{ parent },
+    gradientNeedsUpdate{ false }
+{
+}
+
+
+void EscapeTimeVisualWidget::setGradient(Gradient newGradient)
+{
+    this->gradient = newGradient;
+    gradientNeedsUpdate = true;
+    update();
+}
+
+
+const Gradient& EscapeTimeVisualWidget::getGradient(void)
 {
+    return gradient;
 }
 
 
@@ -158,10 +177,17 @@ void EscapeTimeVisualWidget::resizeGL(int w, int h)
 {
     auto& gl = *this->context()->functions();
     float pixelRatio = this->devicePixelRatioF();
-    gl.glViewport(0, 0, w * pixelRatio, h * pixelRatio);
+
+    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, w * pixelRatio, h * pixelRatio });
+    pmvMatrix.ortho(QRectF{ 0, 0, newW, newH });
     int matrixLocation = program->uniformLocation("matrix");
     program->setUniformValue(matrixLocation, pmvMatrix);
 }
@@ -169,11 +195,65 @@ void EscapeTimeVisualWidget::resizeGL(int w, int h)
 
 void EscapeTimeVisualWidget::paintGL(void)
 {
-    ETVImage etvi{ *this };
+    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);
+    etvi.draw(100, 100, 700, 700);*/
+}
+
+
+void EscapeTimeVisualWidget::updateGradient(void)
+{
+    auto& gl = *this->context()->functions();
+
+    const int len = 512;
+    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);
+
+    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;
 }

+ 20 - 2
EscapeTimeVisualWidget.h

@@ -2,10 +2,11 @@
 #define ESCAPETIMEVISUALWIDGET_H
 
 #include <QOpenGLWidget>
+#include "Bitmap.h"
+#include "Gradient.h"
 
 class QOpenGLShaderProgram;
 
-
 class EscapeTimeVisualWidget;
 
 
@@ -14,7 +15,8 @@ class ETVImage
     GLuint textureId;
     EscapeTimeVisualWidget& owner;
 public:
-    ETVImage(EscapeTimeVisualWidget& owner);
+    ETVImage(EscapeTimeVisualWidget& owner,
+             const Bitmap<float>& img);
     ~ETVImage(void);
 
     void draw(float x, float y, float w, float h,
@@ -31,12 +33,28 @@ class EscapeTimeVisualWidget :
     friend class ETVImage;
     QOpenGLShaderProgram* program;
     GLuint gradientTextureId;
+    Gradient gradient;
+    bool gradientNeedsUpdate;
+
+    float resolutionX;
+    float resolutionY;
+
 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 setResolutionX(int w);
+    void setResolutionY(int h);
+    int getResolutionX(void) const;
+    int getResolutionY(void) const;
+private:
+    void updateGradient(void);
 };
 
 #endif // ESCAPETIMEVISUALWIDGET_H

+ 218 - 0
FractalWidget.cpp

@@ -0,0 +1,218 @@
+#include "FractalWidget.h"
+#include <QMouseEvent>
+
+
+
+Q_DECLARE_METATYPE(mnd::MandelViewport)
+
+
+ViewportAnimation::ViewportAnimation(QObject* parent) :
+    QPropertyAnimation{ parent }
+{
+}
+
+
+QVariant ViewportAnimation::interpolated(const QVariant& from, const QVariant& to,
+                      qreal progress) const
+{
+    const mnd::MandelViewport& a = from.value<mnd::MandelViewport>();
+    const mnd::MandelViewport& b = to.value<mnd::MandelViewport>();
+
+    auto retVal = mnd::MandelViewport {
+        a.x * (1 - progress) + b.x * progress,
+        a.y * (1 - progress) + b.y * progress,
+        a.width * (1 - progress) + b.width * progress,
+        a.height * (1 - progress) + b.height * progress,
+    };
+
+    return QVariant::fromValue(retVal);
+}
+
+
+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;
+        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 (selectingPoint) {
+        pointX = me->x();
+        pointY = me->y();
+        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();
+
+        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) {
+        selectingPoint = false;
+        this->setMouseTracking(false);
+        /*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);*/
+        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::resizeGL(int w, int h)
+{
+    FractalZoomWidget::resizeGL(w, h);
+    targetViewport.adjustAspectRatio(w, h);
+}
+
+
+void FractalWidget::paintGL(void)
+{
+    FractalZoomWidget::paintGL();
+    updateAnimations();
+}
+
+
+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();
+    }
+}

+ 55 - 0
FractalWidget.h

@@ -0,0 +1,55 @@
+#ifndef FRACTALWIDGET_H
+#define FRACTALWIDGET_H
+
+#include "FractalZoomWidget.h"
+#include <QPropertyAnimation>
+#include <chrono>
+
+class ViewportAnimation :
+    public QPropertyAnimation
+{
+public:
+    ViewportAnimation(QObject* parent = nullptr);
+
+    QVariant interpolated(const QVariant& from, const QVariant& to,
+                          qreal progress) const override;
+};
+
+class FractalWidget :
+    public FractalZoomWidget
+{
+    bool rubberbanding = false;
+    QRectF rubberband;
+
+    bool dragging = false;
+    float dragX, dragY;
+
+    bool selectingPoint = false;
+    float pointX, pointY;
+
+    /// 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;
+
+    virtual void resizeGL(int w, int h) override;
+    virtual void paintGL(void) override;
+
+private:
+    void newAnimation(void);
+    void updateAnimations(void);
+};
+
+#endif // FRACTALWIDGET_H

+ 7 - 7
FractalWidgetUtils.cpp

@@ -101,13 +101,13 @@ void CalcJob::run(void)
     mi.bWidth = mi.bHeight = FractalZoomWidget::chunkSize;
     try {
         generator->generate(mi, f.pixels.get());
-        emit done(new Bitmap<float>(std::move(f)));
+        emit done(level, i, j, new Bitmap<float>(std::move(f)));
     }
     catch(std::exception& ex) {
-        emit failed(ex.what());
+        emit failed(level, i, j, ex.what());
     }
     catch(...) {
-        emit failed(tr("unknown error"));
+        emit failed(level, i, j, tr("unknown error"));
     }
 }
 
@@ -115,8 +115,8 @@ void CalcJob::run(void)
 size_t IndexPairHash::operator()(const std::pair<GridIndex, GridIndex>& p) const
 {
     const auto& [a, b] = p;
-    size_t truncA = static_cast<size_t>(a);
-    size_t truncB = static_cast<size_t>(b);
+    size_t truncA = std::hash<GridIndex>{}(a);
+    size_t truncB = std::hash<GridIndex>{}(b);
     boost::hash_combine(truncA, truncB);
     return truncA;
 }
@@ -125,8 +125,8 @@ size_t IndexPairHash::operator()(const std::pair<GridIndex, GridIndex>& p) const
 size_t IndexTripleHash::operator()(const std::tuple<int, GridIndex, GridIndex>& p) const
 {
     const auto& [i, a, b] = p;
-    size_t truncA = static_cast<size_t>(a);
-    size_t truncB = static_cast<size_t>(b);
+    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;

+ 2 - 2
FractalWidgetUtils.h

@@ -119,8 +119,8 @@ public:
 
     void run() override;
 signals:
-    void done(Bitmap<float>* bmp);
-    void failed(QString err);
+    void done(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp);
+    void failed(int level, GridIndex i, GridIndex j, QString err);
 };
 
 

+ 271 - 20
FractalZoomWidget.cpp

@@ -122,28 +122,21 @@ void Calcer::notFinished(int level, GridIndex i, GridIndex j)
 }
 
 
-void Calcer::jobFailed(void)
+void Calcer::jobFailed(int level, GridIndex i, GridIndex j)
 {
-
+    jobsMutex.lock();
+    jobs.erase({ level, i, j });
+    jobsMutex.unlock();
 }
 
 
-void Calcer::redirect(Bitmap<float>* bmp)
+void Calcer::redirect(int level, GridIndex i, GridIndex j, Bitmap<float>* bmp)
 {
-    QObject* s = sender();
-    if (CalcJob* cj = qobject_cast<CalcJob*>(s)) {
-        int level = cj->level;
-        GridIndex i = cj->i;
-        GridIndex j = cj->j;
-        jobsMutex.lock();
-        jobs.erase({ level, i, j });
-        jobsMutex.unlock();
-        if (this->calcState == calcState) {
-            emit done(level, i, j, bmp);
-        }
-        else {
-            delete bmp;
-        }
+    jobsMutex.lock();
+    jobs.erase({ level, i, j });
+    jobsMutex.unlock();
+    if (this->calcState == calcState) {
+        emit done(level, i, j, bmp);
     }
     else {
         delete bmp;
@@ -151,19 +144,172 @@ void Calcer::redirect(Bitmap<float>* bmp)
 }
 
 
+const int FractalZoomWidget::chunkSize = 256;
+
+
 FractalZoomWidget::FractalZoomWidget(QWidget* parent) :
     EscapeTimeVisualWidget{ parent },
     calcer{ *this }
 {
+    qMetaTypeId<GridIndex>();
+    connect(&calcer, &Calcer::done, this, &FractalZoomWidget::cellReady);
+    mandelInfo.maxIter = 250;
 }
 
 
-mnd::Real FractalZoomWidget::getDpp(int level)
+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
+{
+    return int(mnd::log2(dpp / chunkSize));
+}
+
+
+mnd::Real FractalZoomWidget::getDpp(int level) const
 {
     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();
+    }
+}
+
+
+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;
@@ -172,7 +318,16 @@ const mnd::MandelInfo& FractalZoomWidget::getMandelInfo(void) const
 
 void FractalZoomWidget::setGenerator(mnd::MandelGenerator* gen)
 {
+    bool changed = true;
+    if (this->generator == gen)
+        changed = false;
+
     this->generator = gen;
+
+    if (changed) {
+        clearCells();
+        update();
+    }
 }
 
 
@@ -182,13 +337,109 @@ mnd::MandelGenerator* FractalZoomWidget::getGenerator(void) const
 }
 
 
+void FractalZoomWidget::zoom(float factor)
+{
+    mandelInfo.view.zoomCenter(factor);
+    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.adjustAspectRatio(w, h);
+}
+
+
 void FractalZoomWidget::paintGL(void)
 {
-    ETVImage etvi{ *this };
+    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);
+    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) {
+                t->img->drawRect(float(x), float(y), float(w), float(w));
+                /*glBegin(GL_LINE_LOOP);
+                glVertex2f(float(x), float(y));
+                glVertex2f(float(x) + float(w), float(y));
+                glVertex2f(float(x) + float(w), float(y) + float(w));
+                glVertex2f(float(x), float(y) + float(w));
+                glEnd();*/
+
+                if (!t->enoughResolution) {
+                    calcer.calc(grid, level, i, j, t->img->getRecalcPriority());
+                }
+            }
+            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();
 }

+ 33 - 5
FractalZoomWidget.h

@@ -59,8 +59,8 @@ 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(void);
-    void redirect(Bitmap<float>* bmp);
+    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);
 };
@@ -77,20 +77,48 @@ class FractalZoomWidget :
     mnd::MandelGenerator* generator;
     Calcer calcer;
 
+    ETVImage* emptyImage;
+protected:
     mnd::MandelInfo mandelInfo;
 
 public:
-    static const int chunkSize = 256;
+    static const int chunkSize;
 
     FractalZoomWidget(QWidget* parent = nullptr);
 
-    mnd::Real getDpp(int level);
+    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;
 
     void setGenerator(mnd::MandelGenerator*);
     mnd::MandelGenerator* getGenerator(void) const;
 
-    void paintGL(void) override;
+    virtual void zoom(float factor);
+
+    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

+ 1 - 0
GradientMenu.cpp

@@ -12,6 +12,7 @@ GradientMenu::GradientMenu(QWidget *parent) :
             { 0.7, QColor{ 100, 20, 120 } }
         }
     );
+    connect(ui->gradientWidget, &GradientWidget::gradientChanged, this, &GradientMenu::gradientChanged);
 }
 
 

+ 3 - 2
GradientMenu.h

@@ -13,6 +13,7 @@ class GradientMenu : public QWidget
 {
     Q_OBJECT
 
+    Ui::GradientMenu *ui;
 public:
     explicit GradientMenu(QWidget *parent = nullptr);
     ~GradientMenu(void);
@@ -20,8 +21,8 @@ public:
     const QVector<QPair<float, QColor>>& getGradient(void);
     void setGradient(QVector<QPair<float, QColor>> grad);
 
-private:
-    Ui::GradientMenu *ui;
+signals:
+    void gradientChanged(void);
 };
 
 #endif // GRADIENTMENU_H

+ 2 - 0
GradientWidget.cpp

@@ -277,6 +277,7 @@ void GradientWidget::mouseMoveEvent(QMouseEvent* e)
         newVal = std::clamp(newVal, 0.0f, 1.0f);
         points[selectedHandle].first = newVal;
         update();
+        emit gradientChanged();
         e->accept();
     }
     else {
@@ -304,6 +305,7 @@ void GradientWidget::mouseDoubleClickEvent(QMouseEvent* e)
         if (newColor.isValid()) {
             points[handle].second = newColor;
             update();
+            emit gradientChanged();
         }
         e->accept();
     }

+ 1 - 1
GradientWidget.h

@@ -69,7 +69,7 @@ protected:
 private:
     static QPainterPath createSlideHandle(float w, float h);
 signals:
-
+    void gradientChanged(void);
 };
 
 #endif // GRADIENTWIDGET_H

+ 1 - 0
customgenerator.cpp

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

+ 3 - 3
libalmond/src/Gradient.cpp

@@ -41,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, true, true);
-    CubicSpline gsp(gs, true, true);
-    CubicSpline bsp(bs, true, true);
+    CubicSpline rsp(rs, false, true);
+    CubicSpline gsp(gs, false, true);
+    CubicSpline bsp(bs, false, true);
 
     if(precalcSteps <= 0) {
         precalcSteps = int(max * 15) + 10;