All Downloads are FREE. Search and download functionalities are using the official Maven repository.

se.llbit.chunky.ui.GradientEditor Maven / Gradle / Ivy

There is a newer version: 1.4.5
Show newest version
/* Copyright (c) 2014-2016 Jesper Öqvist 
 *
 * This file is part of Chunky.
 *
 * Chunky 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.
 *
 * Chunky 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 Chunky.  If not, see .
 */
package se.llbit.chunky.ui;

import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import se.llbit.chunky.renderer.scene.Sky;
import se.llbit.chunky.ui.render.SkyTab;
import se.llbit.json.JsonParser;
import se.llbit.math.ColorUtil;
import se.llbit.math.Constants;
import se.llbit.math.Vector4;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.IntBuffer;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;

/**
 * A control for editing the sky color gradient.
 * The edited gradient does not use linear blending.
 */
public class GradientEditor extends VBox implements Initializable {
  private static final WritablePixelFormat PIXEL_FORMAT =
      PixelFormat.getIntArgbInstance();
  private final SkyTab sky;

  private String[][] presets = {
      {"Clear", "[{\"rgb\":\"0BABC7\",\"pos\":0.0},{\"rgb\":\"75AAFF\",\"pos\":1.0}]"},
      {"Desert", "[{\"rgb\":\"FF9966\",\"pos\":0.0},{\"rgb\":\"FFB77D\",\"pos\":0.19811320754716982},{\"rgb\":\"FFFFB3\",\"pos\":0.3867924528301887},{\"rgb\":\"D5ECEE\",\"pos\":0.7358490566037735},{\"rgb\":\"EBFCFD\",\"pos\":1.0}]"},
      {"The End", "[{\"rgb\":\"2F1234\",\"pos\":0.0},{\"rgb\":\"321237\",\"pos\":0.42924528301886794},{\"rgb\":\"110713\",\"pos\":0.6320754716981132},{\"rgb\":\"000000\",\"pos\":1.0}]"},
      {"Mountain", "[{\"rgb\":\"718A83\",\"pos\":0.0},{\"rgb\":\"E7E8E8\",\"pos\":0.41745283018867924},{\"rgb\":\"BBF1F4\",\"pos\":0.5801886792452831},{\"rgb\":\"72F0F7\",\"pos\":0.7735849056603774},{\"rgb\":\"58F0F9\",\"pos\":1.0}]"},
      {"The Nether", "[{\"rgb\":\"000000\",\"pos\":0.0},{\"rgb\":\"000000\",\"pos\":0.20047169811320756},{\"rgb\":\"B31A1A\",\"pos\":0.7240566037735849},{\"rgb\":\"B3281A\",\"pos\":0.8655660377358491},{\"rgb\":\"B3341A\",\"pos\":1.0}]"},
      {"Overcast", "[{\"rgb\":\"A5BECA\",\"pos\":0.0},{\"rgb\":\"BED6DD\",\"pos\":0.5259433962264151},{\"rgb\":\"D2E9EB\",\"pos\":0.7358490566037735},{\"rgb\":\"E3F3F4\",\"pos\":1.0}]"},
  };

  @FXML private MenuButton loadPreset;
  @FXML private Button prevBtn;
  @FXML private Button nextBtn;
  @FXML private Button removeBtn;
  @FXML private Button addBtn;
  @FXML private Button importBtn;
  @FXML private Button exportBtn;
  @FXML private Canvas canvas;
  @FXML private SimpleColorPicker colorPicker;

  private List gradient;
  int selected = 0;
  private ChangeListener colorListener = (observable, oldValue, newValue) -> {
    Vector4 stop = gradient.get(selected);
    stop.x = newValue.getRed();
    stop.y = newValue.getGreen();
    stop.z = newValue.getBlue();
    gradientChanged();
  };

  public GradientEditor(SkyTab sky) throws IOException {
    this.sky = sky;
    FXMLLoader loader = new FXMLLoader(getClass().getResource("GradientEditor.fxml"));
    loader.setRoot(this);
    loader.setController(this);
    loader.load();
  }

  @Override public void initialize(URL location, ResourceBundle resources) {
    for (String[] preset : presets) {
      MenuItem menuItem = new MenuItem(preset[0]);
      menuItem.setOnAction(e -> importGradient(preset[1]));
      loadPreset.getItems().add(menuItem);
    }
    prevBtn.setTooltip(new Tooltip("Select the previous stop."));
    prevBtn.setOnAction(e -> {
      selectStop(Math.max(selected - 1, 0));
      draw();
    });
    nextBtn.setTooltip(new Tooltip("Select the next stop."));
    nextBtn.setOnAction(e -> {
      selectStop(Math.min(selected + 1, gradient.size() - 1));
      draw();
    });
    addBtn.setTooltip(new Tooltip("Add a new stop after the selected stop."));
    addBtn.setOnAction(e -> {
      selectStop(addStopAfter(selected));
      gradientChanged();
    });
    removeBtn.setTooltip(new Tooltip("Delete the selected stop."));
    removeBtn.setDisable(true);
    removeBtn.setOnAction(e -> {
      if (removeStop(selected)) {
        selectStop(Math.min(selected, gradient.size() - 1));
        nextBtn.setDisable(selected == gradient.size() - 1);
        gradientChanged();
      }
    });
    importBtn.setOnAction(e -> {
      javafx.scene.control.TextInputDialog dialog = new TextInputDialog();
      dialog.setTitle("Import Gradient");
      dialog.setHeaderText("Gradient Import");
      dialog.setContentText("Graident JSON:");
      Optional result = dialog.showAndWait();
      if (result.isPresent()) {
        importGradient(result.get());
      }
    });
    exportBtn.setOnAction(e -> {
      javafx.scene.control.TextInputDialog dialog =
          new TextInputDialog(Sky.gradientJson(gradient).toCompactString());
      dialog.setTitle("Gradient Export");
      dialog.setHeaderText("Gradient Export");
      dialog.setContentText("Gradient JSON:");
      dialog.showAndWait();
    });
    canvas.setOnMouseDragged(e -> {
      double pos = e.getX() / canvas.getWidth();
      if (selected > 0 && selected + 1 < gradient.size()) {
        pos = Math.max(gradient.get(selected - 1).w, pos);
        pos = Math.min(gradient.get(selected + 1).w, pos);
        gradient.get(selected).w = pos;
        gradientChanged();
      }
    });
    canvas.setOnMousePressed(e -> {
      // Select closest stop.
      double pos = e.getX() / canvas.getWidth();
      double closest = Double.MAX_VALUE;
      int stop = 0;
      int index = 0;
      for (Vector4 m : gradient) {
        double distance = Math.abs(m.w - pos);
        if (distance < closest) {
          stop = index;
          closest = distance;
        }
        index += 1;
      }
      selectStop(stop);
      draw();
    });
    canvas.setOnMouseClicked(e -> {
      if (e.getClickCount() == 2) {
        // Add new stop after the last stop before click position.
        double pos = e.getX() / canvas.getWidth();
        int stop = 0;
        int index = 0;
        for (Vector4 m : gradient) {
          if (pos >= m.w) {
            stop = index;
          } else {
            break;
          }
          index += 1;
        }
        int added = addStopAfter(stop, pos);
        gradient.get(added).w = pos;
        selectStop(added);
        gradientChanged();
      }
    });
    colorPicker.colorProperty().addListener(colorListener);
  }

  private void importGradient(String data) {
    JsonParser parser = new JsonParser(new ByteArrayInputStream(data.getBytes()));
    try {
      List newGradient = Sky.gradientFromJson(parser.parse().array());
      if (newGradient != null) {
        setGradientNoUpdate(newGradient);
      }
    } catch (IOException | JsonParser.SyntaxError ignored) {
      // Ignored.
    }
  }

  private Color toFxColor(Vector4 color) {
    return new Color(color.x, color.y, color.z, 1);
  }

  private void gradientChanged() {
    removeBtn.setDisable(gradient.size() == 2);
    draw();
    sky.gradientChanged(gradient);
  }

  /**
   * @return the index of the added stop
   */
  private int addStopAfter(int stop) {
    int i0;
    int i1;
    if (stop == gradient.size() - 1) {
      i0 = stop - 1;
      i1 = stop;
    } else {
      i0 = stop;
      i1 = stop + 1;
    }
    gradient.add(i1, blend(gradient.get(i0), gradient.get(i1), 0.5));
    nextBtn.setDisable(selected == gradient.size() - 1);
    return i1;
  }

  /**
   * @return the index of the added stop
   */
  private int addStopAfter(int stop, double pos) {
    int i0;
    int i1;
    if (stop == gradient.size() - 1) {
      i0 = stop - 1;
      i1 = stop;
    } else {
      i0 = stop;
      i1 = stop + 1;
    }
    Vector4 s0 = gradient.get(i0);
    Vector4 s1 = gradient.get(i1);
    gradient.add(i1, blend(s0, s1, (pos - s0.w) / (s1.w - s0.w)));
    nextBtn.setDisable(selected == gradient.size() - 1);
    return i1;
  }

  /**
   * @return {@code true} if a stop was deleted.
   */
  public boolean removeStop(int stop) {
    if (gradient.size() > 2) {
      gradient.remove(stop);
      if (stop == 0) {
        gradient.get(0).w = 0;
      } else if (stop == gradient.size()) {
        gradient.get(stop - 1).w = 1;
      }
      return true;
    }
    return false;
  }

  private Vector4 blend(Vector4 s0, Vector4 s1, double d) {
    double xx = 0.5 * (Math.sin(Math.PI * d - Constants.HALF_PI) + 1);
    double a = 1 - xx;
    double b = xx;
    return new Vector4(a * s0.x + b * s1.x, a * s0.y + b * s1.y, a * s0.z + b * s1.z,
        a * s0.w + b * s1.w);
  }

  private synchronized void selectStop(int stop) {
    if (stop != selected) {
      selected = stop;
      updateSelectedColor();
    }
  }

  private void updateSelectedColor() {
    colorPicker.colorProperty().removeListener(colorListener);
    colorPicker.setColor(toFxColor(gradient.get(selected)));
    colorPicker.colorProperty().addListener(colorListener);
    prevBtn.setDisable(selected == 0);
    nextBtn.setDisable(selected == gradient.size() - 1);
  }

  private void draw() {
    int width = (int) canvas.getWidth();
    int height = (int) canvas.getHeight();
    Image image = drawGradient(width, height, gradient);
    GraphicsContext gc = canvas.getGraphicsContext2D();
    gc.drawImage(image, 0, 0);
    gc.setFill(Color.WHITE);
    gc.fillRect(0, 0, width, 15);

    int min = 0, max = width;
    if (selected > 0) {
      min = (int) (gradient.get(selected - 1).w * width);
    }
    if (selected < gradient.size() - 1) {
      max = (int) (gradient.get(selected + 1).w * width);
    }
    gc.setFill(Color.LIGHTGRAY);
    gc.fillRect(min, 0, max - min, 15);
    int index = 0;
    for (Vector4 stop : gradient) {
      if (index == selected) {
        index += 1;
        continue;
      }

      gc.setFill(Color.WHITE);
      int x = Math.min(width - 1, (int) (stop.w * width));
      double[] xPoints = {x - 5, x, x + 5};
      double[] yPoints = {0, 15, 0};
      gc.fillPolygon(xPoints, yPoints, 3);

      boolean isEndpoint = index == 0 || index == gradient.size() - 1;
      if (isEndpoint) {
        gc.setStroke(Color.GRAY);
      } else {
        gc.setStroke(Color.BLACK);
      }
      gc.strokeLine(x - 5, 0, x, 15);
      gc.strokeLine(x, 15, x + 5, 0);
      gc.strokeLine(x - 5, 0, x + 5, 0);
      index += 1;
    }
    gc.setLineWidth(2);
    Vector4 stop = gradient.get(selected);
    boolean isEndpoint = selected == 0 || selected == gradient.size() - 1;
    if (isEndpoint) {
      gc.setFill(Color.GRAY);
    } else {
      gc.setFill(Color.BLACK);
    }
    int x = Math.min(width - 1, (int) (stop.w * width));
    double[] xPoints = {x - 5, x, x + 5};
    double[] yPoints = {0, 15, 0};
    gc.fillPolygon(xPoints, yPoints, 3);

    gc.setStroke(Color.WHITE);
    gc.strokeLine(x - 5, 0, x, 15);
    gc.strokeLine(x, 15, x + 5, 0);
    gc.strokeLine(x - 5, 0, x + 5, 0);
  }

  protected static Image drawGradient(int width, int height, List gradient) {
    int[] pixels = new int[width * height];
    if (width <= 0 || height <= 0 || gradient.size() < 2) {
      throw new IllegalArgumentException();
    }
    int x = 0;
    // Fill the first row.
    for (int i = 0; i < width; ++i) {
      double weight = i / (double) width;
      Vector4 c0 = gradient.get(x);
      Vector4 c1 = gradient.get(x + 1);
      double xx = (weight - c0.w) / (c1.w - c0.w);
      while (x + 2 < gradient.size() && xx > 1) {
        x += 1;
        c0 = gradient.get(x);
        c1 = gradient.get(x + 1);
        xx = (weight - c0.w) / (c1.w - c0.w);
      }
      xx = 0.5 * (Math.sin(Math.PI * xx - Constants.HALF_PI) + 1);
      double a = 1 - xx;
      double b = xx;
      int argb = ColorUtil
          .getArgb(a * c0.x + b * c1.x, a * c0.y + b * c1.y, a * c0.z + b * c1.z, 1);
      pixels[i] = argb;
    }
    // Copy top row to the rest of the image.
    for (int j = 1; j < height; ++j) {
      System.arraycopy(pixels, 0, pixels, j * width, width);
    }
    WritableImage image = new WritableImage(width, height);
    image.getPixelWriter().setPixels(0, 0, width, height, PIXEL_FORMAT, pixels, 0, width);
    return image;
  }

  private void setGradientNoUpdate(List gradient) {
    this.gradient = gradient;
    selected = Math.min(selected, gradient.size() - 1);
    updateSelectedColor();
    gradientChanged();
  }

  public void setGradient(List gradient) {
    this.gradient = gradient;
    selected = Math.min(selected, gradient.size() - 1);
    updateSelectedColor();
    draw();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy