Skip to content

Instantly share code, notes, and snippets.

@KrabCode
Last active March 27, 2020 15:08
Show Gist options
  • Select an option

  • Save KrabCode/817b39c61a7f55e3b91c371eda6de6e8 to your computer and use it in GitHub Desktop.

Select an option

Save KrabCode/817b39c61a7f55e3b91c371eda6de6e8 to your computer and use it in GitHub Desktop.
void setup() {
size(800, 800, P2D);
background(0);
}
void draw() {
group("global"); //collapsible groups
background(picker("background", 0).clr()); // instead of the whole clr() you can query hue, sat, br and alpha separately in the 0-1 range
PVector translate = sliderXY("translate");
translate(translate.x, translate.y);
group("shape");
float size = slider("size", 150); // setting a default value is useful, there's more parameters if you want min/max
strokeWeight(slider("stroke weight", 5));
stroke(picker("stroke").clr());
fill(picker("fill").clr());
if (toggle("noFill")) { // this keeps the current state until changed, default is false
noFill();
}
String option = options("rectangle", "ellipse");
if (option.equals("rectangle")) {
rectMode(CENTER);
rect(width*.5f, height*.5f, size, size);
} else if (option.equals("ellipse")) {
ellipse(width*.5f, height*.5f, size, size);
}
if (button("capture")) { // this is true only for 1 frame when the button is pressed
saveFrame("####.jpg");
}
rec(); // records the next 360 frames on 'k' press, change the number of frames to record by changing frameRecordingDuration in setup
gui(); // displays and updates the gui
}
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import static java.lang.System.currentTimeMillis;
private static final String STATE_BEGIN = "STATE_BEGIN";
private static final String STATE_END = "STATE_END";
private static final String SEPARATOR = "§";
private static final String UNDO_PREFIX = "UNDO";
private static final String REDO_PREFIX = "REDO";
private static final String GROUP_PREFIX = "GROUP";
private static final String ACTION_UP = "UP";
private static final String ACTION_DOWN = "DOWN";
private static final String ACTION_LEFT = "LEFT";
private static final String ACTION_RIGHT = "RIGHT";
private static final String ACTION_PRECISION_ZOOM_IN = "PRECISION_ZOOM_IN";
private static final String ACTION_PRECISION_ZOOM_OUT = "PRECISION_ZOOM_OUT";
private static final String ACTION_RESET = "RESET";
private static final String ACTION_CONTROL = "CONTROL";
private static final String ACTION_ALT = "ALT";
private static final String ACTION_ACTIVATE = "ACTIVATE";
private static final String ACTION_HIDE = "HIDE";
private static final String ACTION_UNDO = "UNDO";
private static final String ACTION_REDO = "REDO";
private static final String ACTION_SAVE = "SAVE";
private static final String ACTION_LOAD = "load";
private static final int MENU_BUTTON_COUNT = 4;
private static final String MENU_BUTTON_HIDE = "hide";
private static final String MENU_BUTTON_UNDO = "undo";
private static final String MENU_BUTTON_REDO = "redo";
private static final String MENU_BUTTON_SAVE = "save";
private static final String SATURATION = "saturation";
private static final String BRIGHTNESS = "brightness";
private static final String HUE = "hue";
private static final float BACKGROUND_ALPHA = .9f;
private static final float GRAYSCALE_GRID = .3f;
private static final float GRAYSCALE_TEXT_DARK = .5f;
private static final float GRAYSCALE_TEXT_SELECTED = 1;
private static final float INT_PRECISION_MAXIMUM = 10000;
private static final float INT_PRECISION_MINIMUM = 10f;
private static final float FLOAT_PRECISION_MAXIMUM = 10000;
private static final float FLOAT_PRECISION_MINIMUM = .1f;
private static final float ALPHA_PRECISION_MINIMUM = .005f;
private static final float ALPHA_PRECISION_MAXIMUM = 10;
private static final float UNDERLINE_TRAY_ANIMATION_DURATION = 10;
private static final float UNDERLINE_TRAY_ANIMATION_EASING = 3;
private static final float SLIDER_EDGE_DARKEN_EASING = 3;
private static final float SLIDER_REVEAL_DURATION = 15;
private static final float SLIDER_REVEAL_START_SKIP = SLIDER_REVEAL_DURATION * .25f;
private static final float SLIDER_REVEAL_EASING = 1;
private static final float PICKER_REVEAL_DURATION = 15;
private static final float PICKER_REVEAL_EASING = 1;
private static final float PICKER_REVEAL_START_SKIP = PICKER_REVEAL_DURATION * .25f;
private static final int KEY_REPEAT_DELAY_FIRST = 300;
private static final int KEY_REPEAT_DELAY = 30;
private static final float MENU_ROTATION_DURATION = 20;
private static final float MENU_ROTATION_EASING = 2;
private static final float DESELECTION_FADEOUT_DURATION = 10;
private static final float DESELECTION_FADEOUT_EASING = 1;
private static final float CHECK_ANIMATION_DURATION = 10;
private static final float CHECK_ANIMATION_EASING = 1;
private static final int GROUP_TOGGLE_ANIMATION_DURATION = 10;
private static final float GROUP_TOGGLE_ANIMATION_EASING = 1;
private final boolean onWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
private final float textSize = onWindows ? 24 : 48;
private final float cell = onWindows ? 40 : 80;
private final float hideButtonWidth = cell * 2;
private final float menuButtonSize = cell * 1.5f;
private final float previewTrayBoxWidth = cell * .375f;
private final float previewTrayBoxMargin = cell * .125f;
private final float previewTrayBoxOffsetY = -cell * .025f;
private final float minimumTrayWidth = hideButtonWidth + (MENU_BUTTON_COUNT - 1) * menuButtonSize;
private final float sliderHeight = cell * 2;
protected String captureDir;
protected String id = regenIdAndCaptureDir();
protected float t;
protected boolean mousePressedOutsideGui = false;
protected int frameRecordingStarted = 0;
protected int frameRecordingDuration = 360; // assuming t += radians(1) per frame for a perfect loop
private float trayWidthWhenExtended = minimumTrayWidth;
private float trayWidth = minimumTrayWidth;
private ArrayList<ArrayList<String>> undoStack = new ArrayList<ArrayList<String>>();
private ArrayList<ArrayList<String>> redoStack = new ArrayList<ArrayList<String>>();
private ArrayList<ArrayList<String>> replayStack = new ArrayList<ArrayList<String>>();
private boolean replayStackRecordingPaused = true;
private boolean captureScreenshot = false;
private int screenshotsAlreadyCaptured = 0;
private ArrayList<Group> groups = new ArrayList<Group>();
private Group currentGroup = null; // do not assign to nor read directly!
private ArrayList<Key> keyboardKeys = new ArrayList<Key>();
private ArrayList<Key> keyboardKeysToRemove = new ArrayList<Key>();
private ArrayList<String> actions = new ArrayList<String>();
private ArrayList<String> previousActions = new ArrayList<String>();
private boolean pMousePressed = false;
private int keyboardSelectionIndex = MENU_BUTTON_COUNT;
private boolean keyboardActive = true;
private boolean trayVisible = true;
private boolean overlayVisible;
private boolean horizontalOverlayVisible;
private boolean verticalOverlayVisible;
private boolean pickerOverlayVisible;
private boolean zOverlayVisible;
private Element overlayOwner = null; // do not assign directly!
private float underlineTrayAnimationStarted = -UNDERLINE_TRAY_ANIMATION_DURATION;
private float undoRotationStarted = -MENU_ROTATION_DURATION;
private float redoRotationStarted = -MENU_ROTATION_DURATION;
private float hideRotationStarted = -MENU_ROTATION_DURATION;
private float saveAnimationStarted = -MENU_ROTATION_DURATION;
private int undoHoldDuration = 0;
private int redoHoldDuration = 0;
private int menuButtonHoldThreshold = 60;
private float trayScrollOffset = 0;
private ArrayList<Float> scrollOffsetHistory = new ArrayList<Float>();
private ArrayList<ShaderSnapshot> snapshots = new ArrayList<ShaderSnapshot>();
private int shaderRefreshRateInMillis = 36;
// INTERFACE
protected int sliderInt() {
return floor(sliderInt("x"));
}
protected int sliderInt(String name) {
return floor(sliderInt(name, 0));
}
protected int sliderInt(String name, int defaultValue) {
return sliderInt(name, defaultValue, numberOfDigitsInFlooredNumber(defaultValue) * 100);
}
protected int sliderInt(String name, int max, boolean defaultMax) {
return sliderInt(name, defaultMax ? 0 : max, numberOfDigitsInFlooredNumber(max) * 100);
}
protected int sliderInt(String name, int defaultValue, int precision) {
return floor(slider(name, defaultValue, precision, false, -Float.MAX_VALUE, Float.MAX_VALUE, true));
}
protected int sliderInt(String name, int min, int max, int defaultValue) {
return floor(slider(name, defaultValue, numberOfDigitsInFlooredNumber(max) * 100, true, min, max, true));
}
protected int sliderInt(String name, int defaultValue, int precision, boolean constrained, int min, int max) {
return floor(slider(name, defaultValue, precision, constrained, min, max, true));
}
protected float slider() {
return slider("x", 0);
}
protected float slider(String name) {
return slider(name, 0);
}
protected float slider(String name, float defaultValue) {
return slider(name, defaultValue, numberOfDigitsInFlooredNumber(defaultValue) * 10);
}
protected float slider(String name, float max, boolean defaultMax) {
return slider(name, defaultMax ? 0 : max, max * .5f);
}
protected float slider(String name, float defaultValue, float precision) {
return slider(name, defaultValue, precision, false, -Float.MAX_VALUE, Float.MAX_VALUE, false);
}
protected float slider(String name, float min, float max, float defaultValue) {
return slider(name, defaultValue, max - min, true, min, max, false);
}
protected float slider(String name, float defaultValue, float precision, boolean constrained, float min,
float max, boolean floored) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(name, currentGroup.name)) {
SliderFloat newElement = new SliderFloat(currentGroup, name, defaultValue, precision,
constrained, min, max, floored);
currentGroup.elements.add(newElement);
}
SliderFloat slider = (SliderFloat) findElement(name, currentGroup.name);
return slider.value;
}
protected PVector sliderXY() {
return sliderXY("xy");
}
protected PVector sliderXY(String name) {
return sliderXY(name, 0, 0, 100);
}
protected PVector sliderXY(String name, float defaultX, float defaultY, float precision) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(name, currentGroup.name)) {
SliderXY newElement = new SliderXY(currentGroup, name, defaultX, defaultY, precision);
currentGroup.elements.add(newElement);
}
SliderXY slider = (SliderXY) findElement(name, currentGroup.name);
return slider.value;
}
protected PVector sliderXYZ(String name, float value, float precision) {
return sliderXYZ(name, value, value, value, precision);
}
protected PVector sliderXYZ() {
return sliderXYZ("xyz", 0, 0, 0, 100);
}
protected PVector sliderXYZ(String name) {
return sliderXYZ(name, 100);
}
protected PVector sliderXYZ(String name, float precision) {
return sliderXYZ(name, 0, 0, 0, precision);
}
protected PVector sliderXYZ(String name, float x, float y, float z, float precision) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(name, currentGroup.name)) {
SliderXYZ newElement = new SliderXYZ(currentGroup, name, x, y, z, precision);
currentGroup.elements.add(newElement);
}
SliderXYZ slider = (SliderXYZ) findElement(name, currentGroup.name);
return slider.value;
}
protected HSBA picker() {
return picker("color");
}
protected HSBA picker(String name) {
return picker(name, 0, 0, .5f, 1);
}
protected HSBA picker(String name, int grayscale) {
return picker(name, 0, 0, grayscale);
}
protected HSBA picker(String name, float grayscale, float alpha) {
return picker(name, 0, 0, grayscale, alpha);
}
protected HSBA picker(String name, float hue, float sat, float br) {
return picker(name, hue, sat, br, 1);
}
protected HSBA picker(String name, float hue, float sat, float br, float alpha) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(name, currentGroup.name)) {
ColorPicker newElement = new ColorPicker(currentGroup, name, hue, sat, br, alpha);
currentGroup.elements.add(newElement);
}
ColorPicker picker = (ColorPicker) findElement(name, currentGroup.name);
if (picker != null) {
return picker.getHSBA();
}
return new HSBA();
}
protected String optionsABC() {
return options("A", "B", "C");
}
protected String options(String defaultValue, String... otherValues) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(defaultValue, currentGroup.name)) {
Element newElement = new Radio(currentGroup, defaultValue, otherValues);
currentGroup.elements.add(newElement);
}
Radio radio = (Radio) findElement(defaultValue, currentGroup.name);
return radio.options.get(radio.valueIndex);
}
protected boolean button() {
return button("button");
}
protected boolean button(String name) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(name, currentGroup.name)) {
Button newElement = new Button(currentGroup, name);
currentGroup.elements.add(newElement);
}
Button button = (Button) findElement(name, currentGroup.name);
return button.value;
}
protected boolean toggle() {
return toggle("toggle");
}
protected boolean toggle(String name) {
return toggle(name, false);
}
protected boolean toggle(String name, boolean defaultState) {
Group currentGroup = getCurrentGroup();
if (elementDoesntExist(name, currentGroup.name)) {
Toggle newElement = new Toggle(currentGroup, name, defaultState);
currentGroup.elements.add(newElement);
}
Toggle toggle = (Toggle) findElement(name, currentGroup.name);
return toggle.checked;
}
protected void gui() {
gui(true);
}
protected void gui(boolean defaultVisibility) {
t += radians(1 / (frameRecordingDuration / 360f));
guiSetup(defaultVisibility);
updateFps();
updateKeyboardInput();
updateMouseState();
pushStyle();
pushMatrix();
strokeCap(SQUARE);
colorMode(HSB, 1, 1, 1, 1);
resetMatrixInAnyRenderer();
updateTrayBackground();
updateMenuButtons();
updateScrolling();
updateGroupsAndTheirElements();
if (overlayVisible && trayVisible) {
overlayOwner.updateOverlay();
}
popStyle();
popMatrix();
pMousePressed = mousePressed;
if (frameCount == 1) {
trayVisible = elementCount() != 0;
}
}
private void updateScrolling() {
if (!(trayVisible && isMousePressedHere(0, 0, trayWidth, height))) {
return;
}
scrollOffsetHistory.add(trayScrollOffset);
int scrollOffsetHistorySize = 3;
while (scrollOffsetHistory.size() > scrollOffsetHistorySize) {
scrollOffsetHistory.remove(0);
}
if (abs(pmouseY - mouseY) > 2) {
trayScrollOffset += mouseY - pmouseY;
}
}
private boolean trayHasntMovedInAWhile() {
for (Float historicalTrayOffset : scrollOffsetHistory) {
if (historicalTrayOffset != trayScrollOffset) {
return false;
}
}
return true;
}
private void updateMouseState() {
mousePressedOutsideGui = mousePressed && isMouseOutsideGui() && !overlayVisible;
}
private void guiSetup(boolean defaultVisibility) {
if (frameCount == 1) {
trayVisible = defaultVisibility;
textSize(textSize * 2);
} else if (frameCount == 3) {
loadLastStateFromFile(true);
}
}
// UTILS
protected void resetGui() {
for (Group group : groups) {
for (Element el : group.elements) {
el.reset();
}
}
}
public void rec() {
rec(g);
}
public void rec(PGraphics pg) {
saveReplay();
savePGraphics(pg);
}
private void savePGraphics(PGraphics pg) {
if (captureScreenshot) {
captureScreenshot = false;
screenshotsAlreadyCaptured++;
String filename = captureDir + "screenshot_" + screenshotsAlreadyCaptured + ".jpg";
println(filename + " saved");
pg.save(filename);
}
int frameRecordingEnd = frameRecordingStarted + frameRecordingDuration + 1;
if (!replayStack.isEmpty()) {
frameRecordingEnd = min(frameRecordingEnd, frameCount + replayStack.size());
}
if (frameRecordingStarted > 0 && frameCount < frameRecordingEnd) {
int frameNumber = frameCount - frameRecordingStarted + 1;
println(frameNumber, "/", frameRecordingEnd - frameRecordingStarted - 1, "saved");
pg.save(captureDir + frameNumber + ".jpg");
loadReplay(frameNumber);
}
}
private void saveReplay() {
if (!replayStackRecordingPaused) {
replayStack.add(getGuiState());
}
}
private void loadReplay(int frameNumber) {
if (replayStack.isEmpty()) {
return;
}
if (frameNumber < replayStack.size()) {
println("setting state for frame", frameNumber);
setGuiState(replayStack.get(frameNumber));
}
if (frameNumber >= replayStack.size()) {
replayStack.clear();
}
}
int numberOfDigitsInFlooredNumber(float inputNumber) {
return String.valueOf(floor(inputNumber)).length();
}
public String regenIdAndCaptureDir() {
String newId = year() + nf(month(), 2) + nf(day(), 2) + "-" + nf(hour(), 2) + nf(minute(), 2) + nf(second(),
2) + "_" + this.getClass().getSimpleName();
captureDir = "out/capture/" + newId + "/";
return newId;
}
public String randomImageUrl(float width, float height) {
return "https://picsum.photos/" + floor(width) + "/" + floor(height) + ".jpg";
}
public String randomImageUrl(float size) {
return "https://picsum.photos/" + floor(size) + ".jpg";
}
protected boolean isPointInRect(float px, float py, float rx, float ry, float rw, float rh) {
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
private float easedAnimation(float startFrame, float duration, float easingFactor) {
return easedAnimation(startFrame, duration, easingFactor, 0, 1);
}
private float easedAnimation(float startFrame, float duration, float easingFactor, float constrainMin,
float constrainMax) {
float animationNormalized = constrain(norm(frameCount, startFrame,
startFrame + duration), constrainMin, constrainMax);
return ease(animationNormalized, easingFactor);
}
protected float ease(float p, float g) {
if (p < 0.5)
return 0.5f * pow(2 * p, g);
else
return 1 - 0.5f * pow(2 * (1 - p), g);
}
// TRAY
protected void spiralSphere(PGraphics pg) {
pg.beginShape(POINTS);
pg.stroke(picker("stroke").clr());
pg.strokeWeight(slider("weight", 5));
pg.noFill();
float N = slider("count", 3000);
float s = 3.6f / sqrt(N);
float dz = 2.0f / N;
float lon = 0;
float z = 1 - dz / 2;
float scl = slider("scale", 260);
for (int k = 0; k < N; k++) {
float r = sqrt(1 - z * z);
pg.vertex(cos(lon) * r * scl, sin(lon) * r * scl, z * scl);
z = z - dz;
lon = lon + s / r;
}
pg.endShape();
pg.noStroke();
if (!toggle("hollow")) {
pg.fill(0);
pg.sphereDetail(floor(slider("detail", 20)));
pg.sphere(slider("scale") - slider("core", 5));
}
}
private void updateFps() {
int nonFlickeringFrameRate = floor(frameRate > 55 ? 60 : frameRate);
String fps = nonFlickeringFrameRate + " fps";
surface.setTitle(this.getClass().getSimpleName() + " " + fps);
}
private void updateMenuButtons() {
float x = 0;
float y = 0;
float size = menuButtonSize;
updateMenuButtonHide(x, y, hideButtonWidth, size);
if (!trayVisible) {
return;
}
x += hideButtonWidth;
updateMenuButtonUndo(x, y, size, size);
x += size;
updateMenuButtonRedo(x, y, size, size);
x += size;
updateMenuButtonSave(x, y, size, size);
}
private void updateMenuButtonHide(float x, float y, float w, float h) {
if (hideActivated(x, y, w, h)) {
trayVisible = !trayVisible;
trayWidth = trayVisible ? trayWidthWhenExtended : 0;
keyboardSelectionIndex = 0;
hideRotationStarted = frameCount;
}
float grayscale = (keyboardSelected(MENU_BUTTON_HIDE) || isMouseOver(x, y, w, h)) ? GRAYSCALE_TEXT_SELECTED :
GRAYSCALE_TEXT_DARK;
strokeWeight(2);
fill(grayscale);
stroke(grayscale);
float rotation = easedAnimation(hideRotationStarted, MENU_ROTATION_DURATION, MENU_ROTATION_EASING);
if (trayVisible) {
rotation += 1;
}
if (isMouseOver(x, y, w, h) || trayVisible) {
displayMenuButtonHideShow(x, y, w, h, rotation * PI);
}
}
private void updateMenuButtonUndo(float x, float y, float w, float h) {
float rotation = easedAnimation(undoRotationStarted, MENU_ROTATION_DURATION, MENU_ROTATION_EASING);
rotation -= constrain(norm(undoHoldDuration, 0, menuButtonHoldThreshold), 0, 1);
displayStateButton(x, y, w, h, rotation * TWO_PI, false, MENU_BUTTON_UNDO, undoStack.size());
boolean canUndo = undoStack.size() > 0;
if (canUndo && trayVisible) {
if (actions.contains(ACTION_UNDO) || isMousePressedHere(x, y, w, h)) {
undoHoldDuration++;
} else if (!isMouseOver(x, y, w, h)) {
undoHoldDuration = 0;
}
if (mouseJustReleasedHere(x, y, w, h) || actionJustReleased(ACTION_UNDO)) {
if (undoHoldDuration < menuButtonHoldThreshold) {
pushCurrentStateToRedo();
popUndoToCurrentState();
} else {
while (!undoStack.isEmpty()) {
pushCurrentStateToRedo();
popUndoToCurrentState();
}
}
undoRotationStarted = frameCount;
undoHoldDuration = 0;
}
}
}
private void updateMenuButtonRedo(float x, float y, float w, float h) {
float rotation = easedAnimation(redoRotationStarted, MENU_ROTATION_DURATION, MENU_ROTATION_EASING);
rotation -= constrain(norm(redoHoldDuration, 0, menuButtonHoldThreshold), 0, 1);
displayStateButton(x, y, w, h, rotation * TWO_PI, true, MENU_BUTTON_REDO, redoStack.size());
boolean canRedo = redoStack.size() > 0;
if (canRedo && trayVisible) {
if (actions.contains(ACTION_REDO) || isMousePressedHere(x, y, w, h)) {
redoHoldDuration++;
} else if (!isMouseOver(x, y, w, h)) {
redoHoldDuration = 0;
}
if (mouseJustReleasedHere(x, y, w, h) || actionJustReleased(ACTION_REDO)) {
if (redoHoldDuration < menuButtonHoldThreshold) {
pushCurrentStateToUndoWithoutClearingRedo();
popRedoToCurrentState();
} else {
while (!redoStack.isEmpty()) {
pushCurrentStateToUndoWithoutClearingRedo();
popRedoToCurrentState();
}
}
redoRotationStarted = frameCount;
redoHoldDuration = 0;
}
}
}
private void displayMenuButtonHideShow(float x, float y, float w, float h, float rotation) {
pushMatrix();
translate(x + w * .5f, y + h * .5f);
rotate(rotation);
float arrowWidth = w * .22f;
line(-arrowWidth, 0, w * .2f, 0);
strokeWeight(2);
beginShape();
vertex(-arrowWidth * .5f, h * .05f);
vertex(-arrowWidth, 0);
vertex(-arrowWidth * .5f, -h * .05f);
endShape(CLOSE);
popMatrix();
}
private void updateMenuButtonSave(float x, float y, float w, float h) {
if (activated(MENU_BUTTON_SAVE, x, y, w, h) || actions.contains(ACTION_SAVE)) {
saveAnimationStarted = frameCount;
saveStateToFile();
}
rectMode(CENTER);
float animation = 1 - easedAnimation(saveAnimationStarted, MENU_ROTATION_DURATION, 3);
if (animation == 0) {
animation = 1;
}
displayMenuButtonSave(x, y, w, h, animation);
}
private void displayMenuButtonSave(float x, float y, float w, float h, float animation) {
float grayscale = (keyboardSelected(MENU_BUTTON_SAVE) || isMouseOver(x, y, w, h)) ?
GRAYSCALE_TEXT_SELECTED : GRAYSCALE_TEXT_DARK;
stroke(grayscale);
strokeWeight(2);
noFill();
rect(x + w * .5f, y + h * .5f, w * .5f * animation, h * .5f * animation);
rect(x + w * .5f, y + h * .5f - animation * h * .12f, w * .25f * animation, h * .25f * animation);
}
private void displayStateButton(float x, float y, float w, float h, float rotation,
boolean direction, String menuButtonType, int stackSize) {
textSize(textSize);
textAlign(CENTER, CENTER);
float grayscale = (keyboardSelected(menuButtonType) || isMouseOver(x, y, w, h)) ?
GRAYSCALE_TEXT_SELECTED : GRAYSCALE_TEXT_DARK;
fill(grayscale);
pushMatrix();
translate(x + w * .5f, y + h * .5f);
rotate(PI + (direction ? rotation : -rotation));
float margin = 0;
noFill();
stroke(grayscale);
strokeWeight(2);
if (stackSize == 0) {
float crossSize = .08f;
line(-w * crossSize, -h * crossSize, w * crossSize, h * crossSize);
line(-w * crossSize, h * crossSize, w * crossSize, -h * crossSize);
}
float radiusMultiplier = .5f;
arc(0, 0, w * radiusMultiplier, h * radiusMultiplier, margin, PI - margin);
fill(grayscale);
stroke(grayscale);
beginShape();
vertex((direction ? -1 : 1) * w * radiusMultiplier * .4f, h * .1f);
vertex((direction ? -1 : 1) * w * radiusMultiplier * .5f, 0);
vertex((direction ? -1 : 1) * w * radiusMultiplier * .55f, h * .1f);
endShape();
popMatrix();
}
protected void group(String name) {
Group group = findGroup(name);
if (!groupExists(name)) {
group = new Group(name);
groups.add(group);
}
setCurrentGroup(group);
}
private void updateGroupsAndTheirElements() {
float x = cell * .5f;
float y = cell * 2.5f;
pushMatrix();
translate(0, trayScrollOffset);
if (actions.contains(ACTION_ACTIVATE) && !trayVisible && overlayVisible) {
overlayVisible = false;
while (actions.contains(ACTION_ACTIVATE)) {
actions.remove(ACTION_ACTIVATE);
}
}
for (Group group : groups) {
if (group.elements.isEmpty()) {
continue;
}
group.update(y);
if (trayVisible) {
group.displayInTray(x, y);
}
if (group.expanded) {
x += cell * .5f;
for (Element el : group.elements) {
y += cell;
if (el.equals(overlayOwner)) {
el.handleKeyboardInput();
}
updateElement(group, el, y);
if (trayVisible) {
displayElement(group, el, x, y, group.elementAlpha);
}
}
x -= cell * .5f;
}
y += cell;
}
popMatrix();
}
private void updateElement(Group group, Element el, float y) {
el.update();
if (activated(group.name + el.name, 0, y - cell, trayWidth, cell)) {
if (!el.canHaveOverlay()) {
el.onActivationWithoutOverlay(0, y - cell, trayWidth, cell);
return;
}
if (!overlayVisible) {
setOverlayOwner(el);
} else if (!el.equals(overlayOwner)) {
setOverlayOwner(el);
} else if (el.equals(overlayOwner)) {
overlayVisible = false;
}
}
}
private void displayElement(Group group, Element el, float x, float y, float alpha) {
boolean isSelected = keyboardSelected(group.name + el.name) ||
isMouseOverScrollAware(0, y - cell, trayWidth, cell);
float grayScale;
if (isSelected) {
el.lastSelected = frameCount;
grayScale = GRAYSCALE_TEXT_SELECTED;
} else {
float deselectionFadeout = easedAnimation(el.lastSelected, DESELECTION_FADEOUT_DURATION,
DESELECTION_FADEOUT_EASING);
grayScale = lerp(GRAYSCALE_TEXT_DARK, GRAYSCALE_TEXT_SELECTED, 1 - deselectionFadeout);
}
fill(grayScale, alpha);
stroke(grayScale, alpha);
el.displayOnTray(x, y);
}
private void updateTrayBackground() {
if (!trayVisible) {
return;
}
textSize(textSize);
trayWidthWhenExtended = max(minimumTrayWidth, findLongestNameWidth() + cell * 2);
noStroke();
fill(0, BACKGROUND_ALPHA);
rectMode(CORNER);
rect(0, 0, trayWidth, height);
}
private void displayTrayGrid() {
stroke(GRAYSCALE_GRID);
for (float x = cell; x < trayWidth; x += cell) {
line(x, 0, x, height);
}
for (float y = cell; y < height; y += cell) {
line(0, y, trayWidth, y);
}
}
private void resetMatrixInAnyRenderer() {
if (sketchRenderer().equals(P3D)) {
camera();
} else {
resetMatrix();
}
}
private void setOverlayOwner(Element overlayOwnerToSet) {
this.overlayOwner = overlayOwnerToSet;
this.overlayOwner.onOverlayShown();
overlayVisible = true;
underlineTrayAnimationStarted = frameCount;
}
// INPUT
private boolean hideActivated(float x, float y, float w, float h) {
return actions.contains(ACTION_HIDE) || mouseJustReleasedHere(x, y, w, h);
}
private boolean activated(String query, float x, float y, float w, float h) {
return mouseJustReleasedHereScrollAware(x, y, w, h) || keyboardActivated(query);
}
private boolean mouseJustReleasedHereScrollAware(float x, float y, float w, float h) {
return mouseJustReleasedHere(x, y + trayScrollOffset, w, h) && trayHasntMovedInAWhile();
}
private boolean mouseJustReleasedHere(float x, float y, float w, float h) {
return mouseJustReleased() && isPointInRect(mouseX, mouseY, x, y, w, h);
}
private boolean keyboardActivated(String query) {
return (overlayOwner != null && (overlayOwner.group + overlayOwner.name).equals(query) || keyboardSelected(query))
&& actions.contains(ACTION_ACTIVATE);
}
private boolean mouseJustReleased() {
return pMousePressed && !mousePressed;
}
private boolean isMousePressedHere(float x, float y, float w, float h) {
return mousePressed && isPointInRect(mouseX, mouseY, x, y, w, h);
}
private boolean mouseJustPressed() {
return !pMousePressed && mousePressed;
}
protected boolean mouseJustPressedOutsideGui() {
return !pMousePressed && mousePressed && isMouseOutsideGui();
}
private boolean isMouseOutsideGui() {
return !trayVisible || !isPointInRect(mouseX, mouseY, 0, 0, trayWidth, height);
}
private boolean isMouseOverScrollAware(float x, float y, float w, float h) {
return isMouseOver(x, y + trayScrollOffset, w, h);
}
private boolean isMouseOver(float x, float y, float w, float h) {
return frameCount > 1 && isPointInRect(mouseX, mouseY, x, y, w, h);
}
public void mouseWheel(MouseEvent event) {
float direction = event.getCount();
if (direction < 0) {
actions.add(ACTION_PRECISION_ZOOM_IN);
} else if (direction > 0) {
actions.add(ACTION_PRECISION_ZOOM_OUT);
}
}
private boolean isAnyGroupKeyboardSelected() {
return findKeyboardSelectedGroup() != null;
}
private boolean isAnyElementKeyboardSelected() {
return findKeyboardSelectedElement() != null;
}
private boolean keyboardSelected(String query) {
if (!keyboardActive || !trayVisible) {
return false;
}
if ((query.equals(MENU_BUTTON_HIDE) && keyboardSelectionIndex == 0)
|| (query.equals(MENU_BUTTON_UNDO) && keyboardSelectionIndex == 1)
|| (query.equals(MENU_BUTTON_REDO) && keyboardSelectionIndex == 2)
|| (query.equals(MENU_BUTTON_SAVE) && keyboardSelectionIndex == 3)) {
return true;
}
int i = MENU_BUTTON_COUNT;
for (Group group : groups) {
if (group.name.equals(query) && keyboardSelectionIndex == i) {
return true;
}
i++;
for (Element el : group.elements) {
if ((group.name + el.name).equals(query) && keyboardSelectionIndex == i) {
if (upAndDownArrowsControlOverlay() && !el.equals(overlayOwner)) {
return false;
}
if (el.equals(overlayOwner)) {
return true;
}
return true;
}
i++;
}
}
return false;
}
private int keyboardSelectionLength() {
int elementCount = 0;
for (Group group : groups) {
elementCount += group.elements.size();
}
return MENU_BUTTON_COUNT + groups.size() + elementCount;
}
public void mousePressed() {
if (!upAndDownArrowsControlOverlay()) {
keyboardActive = false;
}
}
public void keyPressed() {
// println((key == CODED ? "code: " + keyCode : "key: " + key));
keyboardActive = true;
if (key == CODED) {
if (keyboardKeysDoesntContain(keyCode, true)) {
keyboardKeys.add(new Key(keyCode, true));
}
} else {
if (keyboardKeysDoesntContain(key, false)) {
keyboardKeys.add(new Key((int) key, false));
}
}
if (key == 'j') {
replayStackRecordingPaused = !replayStackRecordingPaused;
println(replayStackRecordingPaused ? "replay recording paused" : "replay recording resumed");
}
if (key == 'k') {
replayStackRecordingPaused = true;
println("replay recording paused");
frameRecordingStarted = frameCount + 1;
id = regenIdAndCaptureDir();
}
if (key == 'i') {
captureScreenshot = true;
id = regenIdAndCaptureDir();
}
}
private boolean keyboardKeysDoesntContain(int keyCode, boolean coded) {
for (Key kk : keyboardKeys) {
if (kk.character == keyCode && kk.coded == coded) {
return false;
}
}
return true;
}
public void keyReleased() {
if (key == CODED) {
removeKey(keyCode, true);
} else {
removeKey(key, false);
}
}
private void removeKey(int keyCodeToRemove, boolean coded) {
keyboardKeysToRemove.clear();
for (Key kk : keyboardKeys) {
if (kk.coded == coded && kk.character == keyCodeToRemove) {
keyboardKeysToRemove.add(kk);
}
}
keyboardKeys.removeAll(keyboardKeysToRemove);
}
private void updateKeyboardInput() {
previousActions.clear();
previousActions.addAll(actions);
actions.clear();
for (Key kk : keyboardKeys) {
if (kk.coded) {
if (kk.character == UP) {
actions.add(ACTION_UP);
if (!upAndDownArrowsControlOverlay() && kk.repeatCheck() && trayVisible) {
if (keyboardSelectionIndex == MENU_BUTTON_COUNT) {
keyboardSelectionIndex = 0;
} else {
int skipped = hiddenElementCount(false);
keyboardSelectionIndex -= skipped;
keyboardSelectionIndex--;
}
}
}
if (kk.character == DOWN) {
actions.add(ACTION_DOWN);
if (!upAndDownArrowsControlOverlay() && kk.repeatCheck() && trayVisible) {
if (keyboardSelectionIndex < MENU_BUTTON_COUNT) {
keyboardSelectionIndex = MENU_BUTTON_COUNT;
} else {
int skipped = hiddenElementCount(true);
keyboardSelectionIndex += skipped;
keyboardSelectionIndex++;
}
}
}
if (kk.character == LEFT) {
if (isAnyGroupKeyboardSelected() && findKeyboardSelectedGroup().expanded) {
Group keyboardSelected = findKeyboardSelectedGroup();
keyboardSelected.expanded = !keyboardSelected.expanded;
} else {
actions.add(ACTION_LEFT);
}
}
if (kk.character == RIGHT) {
if (isAnyGroupKeyboardSelected() && !findKeyboardSelectedGroup().expanded) {
Group keyboardSelected = findKeyboardSelectedGroup();
keyboardSelected.expanded = !keyboardSelected.expanded;
} else {
actions.add(ACTION_RIGHT);
}
}
if (kk.character == CONTROL) {
actions.add(ACTION_CONTROL);
}
if (kk.character == ALT) {
actions.add(ACTION_ALT);
}
} else if (!kk.coded) {
if (kk.character == 'z' || kk.character == 26) {
actions.add(ACTION_UNDO);
}
if (kk.character == 'y' || kk.character == 25) {
actions.add(ACTION_REDO);
}
if (!kk.justPressed) {
continue;
}
if (kk.character == '*' || kk.character == '+') {
actions.add(ACTION_PRECISION_ZOOM_IN);
}
if (kk.character == '/' || kk.character == '-') {
actions.add(ACTION_PRECISION_ZOOM_OUT);
}
if (kk.character == ' ' || kk.character == ENTER) {
actions.add(ACTION_ACTIVATE);
}
if (kk.character == 'r') {
actions.add(ACTION_RESET);
}
if (kk.character == 'h') {
actions.add(ACTION_HIDE);
}
if (kk.character == 19) {
// CTRL + S
actions.add(ACTION_SAVE);
}
if (kk.character == 12) {
// CTRL + L
actions.add(ACTION_LOAD);
}
}
kk.justPressed = false;
}
if (keyboardSelectionIndex >= keyboardSelectionLength()) {
keyboardSelectionIndex = MENU_BUTTON_COUNT;
}
if (keyboardSelectionIndex < MENU_BUTTON_COUNT) {
Group lastGroup = getLastGroup();
if (lastGroup != null) {
if (!lastGroup.expanded) {
keyboardSelectionIndex = keyboardSelectionLength() - lastGroup.elements.size() - 1;
} else {
keyboardSelectionIndex = keyboardSelectionLength() - 1;
}
}
}
/* //debug
if (lastIndex != keyboardSelectionIndex) {
Element el = findKeyboardSelectedElement();
Group group = findKeyboardSelectedGroup();
String name = "-";
if(el != null){
name = el.name;
}
if(group != null){
name = group.name;
}
println("before", lastIndex,
"after", keyboardSelectionIndex,
"length", keyboardSelectionLength(),
"current ", name);
}
*/
}
private boolean actionJustReleased(String action) {
return previousActions.contains(action) && !actions.contains(action);
}
private boolean upAndDownArrowsControlOverlay() {
return overlayVisible && (verticalOverlayVisible || pickerOverlayVisible);
}
private float findLongestNameWidth() {
float longestNameWidth = 0;
for (Group group : groups) {
for (Element el : group.elements) {
if (el.trayTextWidth() > longestNameWidth) {
longestNameWidth = el.trayTextWidth();
}
}
}
return longestNameWidth;
}
// GROUP AND ELEMENT HANDLING
private int hiddenElementCount(boolean forwardFacing) {
Group group = findKeyboardSelectedGroup();
if (previousActions.contains(ACTION_ALT)) {
if (isAnyElementKeyboardSelected()) {
Element el = findKeyboardSelectedElement();
group = el.group;
if (forwardFacing) {
return group.elements.size() - group.elements.indexOf(el) - 1;
} else {
return group.elements.indexOf(el);
}
} else {
if (forwardFacing) {
return group.elements.size();
} else {
if (group != null) {
Group previous = findPreviousGroup(group.name);
if (previous != null) {
return previous.elements.size();
}
}
return 0;
}
}
} else if (group != null) {
if (forwardFacing && !group.expanded) {
return group.elements.size();
}
if (!forwardFacing) {
Group previous = findPreviousGroup(group.name);
if (previous.expanded) {
return 0;
}
return previous.elements.size();
}
}
return 0;
}
private int elementCount() {
int sum = 0;
for (Group group : groups) {
sum += group.elements.size();
}
return sum;
}
private Group getCurrentGroup() {
if (currentGroup == null) {
if (groups.isEmpty()) {
Group anonymous = new Group(this.getClass().getSimpleName());
groups.add(anonymous);
currentGroup = anonymous;
} else {
return groups.get(0);
}
}
return currentGroup;
}
private void setCurrentGroup(Group currentGroup) {
this.currentGroup = currentGroup;
}
private Group getLastGroup() {
if (groups.isEmpty()) {
return null;
}
return groups.get(groups.size() - 1);
}
private Group findGroup(String name) {
for (Group group : groups) {
if (group.name.equals(name)) {
return group;
}
}
return null;
}
private boolean groupExists(String name) {
return findGroup(name) != null;
}
private Group findPreviousGroup(String query) {
for (Group group : groups) {
if (group.name.equals(query)) {
int index = groups.indexOf(group);
if (index > 0) {
return groups.get(index - 1);
} else {
return null;
}
}
}
return null;
}
private Group findKeyboardSelectedGroup() {
for (Group group : groups) {
if (keyboardSelected(group.name)) {
return group;
}
}
return null;
}
private Element findKeyboardSelectedElement() {
for (Group group : groups) {
for (Element el : group.elements)
if (keyboardSelected(group.name + el.name)) {
return el;
}
}
return null;
}
private boolean elementDoesntExist(String elementName, String groupName) {
return findElement(elementName, groupName) == null;
}
private Element findElement(String elementName, String groupName) {
for (Group g : groups) {
for (Element el : g.elements) {
if (g.name.equals(groupName) && el.name.equals(elementName)) {
return el;
}
}
}
return null;
}
// STATE
private void pushCurrentStateToRedo() {
redoStack.add(getGuiState());
}
private void pushStateToUndo(ArrayList<String> state) {
setGuiState(state);
pushCurrentStateToUndo();
}
private void pushCurrentStateToUndo() {
redoStack.clear();
undoStack.add(getGuiState());
}
private void pushStateToRedo(ArrayList<String> state) {
redoStack.add(state);
}
private void pushCurrentStateToUndoWithoutClearingRedo() {
undoStack.add(getGuiState());
}
private void popUndoToCurrentState() {
if (undoStack.isEmpty()) {
return;
}
setGuiState(undoStack.remove(undoStack.size() - 1));
}
private void popRedoToCurrentState() {
if (redoStack.isEmpty()) {
return;
}
setGuiState(redoStack.remove(redoStack.size() - 1));
}
private ArrayList<String> getGuiState() {
ArrayList<String> states = new ArrayList<String>();
for (Group group : groups) {
states.add(group.getState());
for (Element el : group.elements) {
states.add(el.getState());
}
}
return states;
}
private void setGuiState(ArrayList<String> statesToSet) {
for (String state : statesToSet) {
String[] splitState = state.split(SEPARATOR);
if (state.startsWith(GROUP_PREFIX)) {
Group group = findGroup(splitState[1]);
if (group == null) {
continue;
}
group.setState(state);
} else {
Element el = findElement(splitState[1], splitState[0]);
if (el == null) {
// println("element does not exist", splitState[0], splitState[1]);
continue;
}
try {
el.setState(state);
}
catch(Exception ex) {
println(ex.getMessage());
}
}
}
}
void saveStateToFile() {
pushCurrentStateToUndo();
File file = dataFile(settingsPath());
ArrayList<String> save = new ArrayList<String>(Arrays.asList(loadLastStateFromFile(false)));
save.add(STATE_BEGIN);
save.add(UNDO_PREFIX);
save.addAll(undoStack.get(undoStack.size() - 1));
save.add(STATE_END);
String[] saveArray = arrayListToStringArray(save);
saveStrings(file, saveArray);
}
private String[] arrayListToStringArray(ArrayList<String> input) {
String[] array = new String[input.size()];
for (int i = 0; i < input.size(); i++) {
array[i] = input.get(i);
}
return array;
}
protected String[] loadLastStateFromFile(boolean alsoPush) {
File file = dataFile(settingsPath());
if (!file.exists()) {
return new String[0];
}
String[] lines = loadStrings(file);
if (alsoPush) {
redoStack.clear();
undoStack.clear();
boolean pushingUndo = false;
ArrayList<String> runningState = new ArrayList<String>();
for (String line : lines) {
if (line.startsWith(UNDO_PREFIX)) {
pushingUndo = true;
} else if (line.startsWith(REDO_PREFIX)) {
pushingUndo = false;
} else if (line.startsWith(STATE_BEGIN)) {
runningState.clear();
} else if (line.startsWith(STATE_END)) {
if (pushingUndo) {
// println("pushing ", concat(runningState));
pushStateToUndo(runningState);
runningState.clear();
} else {
// println("pushing ", concat(runningState));
pushStateToRedo(runningState);
runningState.clear();
}
} else {
runningState.add(line);
}
}
popUndoToCurrentState();
}
return lines;
}
private String settingsPath() {
return "gui\\" + this.getClass().getSimpleName() + ".txt";
}
// SHADERS
public PShader uniform(String fragPath) {
ShaderSnapshot snapshot = findSnapshotByPath(fragPath);
snapshot = initIfNull(snapshot, fragPath, null);
return snapshot.compiledShader;
}
public PShader uniform(String fragPath, String vertPath) {
ShaderSnapshot snapshot = findSnapshotByPath(fragPath);
snapshot = initIfNull(snapshot, fragPath, vertPath);
return snapshot.compiledShader;
}
public void hotFilter(String path, PGraphics canvas) {
hotShader(path, null, true, canvas);
}
public void hotFilter(String path) {
hotShader(path, null, true, g);
}
public void hotShader(String fragPath, String vertPath, PGraphics canvas) {
hotShader(fragPath, vertPath, false, canvas);
}
public void hotShader(String fragPath, String vertPath) {
hotShader(fragPath, vertPath, false, g);
}
public void hotShader(String fragPath, PGraphics canvas) {
hotShader(fragPath, null, false, canvas);
}
public void hotShader(String fragPath) {
hotShader(fragPath, null, false, g);
}
private void hotShader(String fragPath, String vertPath, boolean filter, PGraphics canvas) {
ShaderSnapshot snapshot = findSnapshotByPath(fragPath);
snapshot = initIfNull(snapshot, fragPath, vertPath);
snapshot.update(filter, canvas);
}
private ShaderSnapshot initIfNull(ShaderSnapshot snapshot, String fragPath, String vertPath) {
if (snapshot == null) {
snapshot = new ShaderSnapshot(fragPath, vertPath);
snapshots.add(snapshot);
}
return snapshot;
}
private ShaderSnapshot findSnapshotByPath(String path) {
for (ShaderSnapshot snapshot : snapshots) {
if (snapshot.fragPath.equals(path)) {
return snapshot;
}
}
return null;
}
protected float hueModulo(float hue) {
while (hue < 0) {
hue += 1;
}
hue %= 1;
return hue;
}
// CLASSES
private class ShaderSnapshot {
String fragPath;
String vertPath;
File fragFile;
File vertFile;
PShader compiledShader;
long fragLastKnownModified, vertLastKnownModified, lastChecked;
boolean compiledAtLeastOnce = false;
long lastKnownUncompilable = -shaderRefreshRateInMillis;
ShaderSnapshot(String fragPath, String vertPath) {
if (vertPath != null) {
compiledShader = loadShader(fragPath, vertPath);
vertFile = dataFile(vertPath);
vertLastKnownModified = vertFile.lastModified();
if (!vertFile.isFile()) {
println("Could not find shader at " + vertFile.getPath());
}
} else {
compiledShader = loadShader(fragPath);
}
fragFile = dataFile(fragPath);
fragLastKnownModified = fragFile.lastModified();
lastChecked = currentTimeMillis();
if (!fragFile.isFile()) {
println("Could not find shader at " + fragFile.getPath());
}
this.fragPath = fragPath;
this.vertPath = vertPath;
}
@SuppressWarnings("ManualMinMaxCalculation")
long max(long a, long b) {
if (a > b) {
return a;
}
return b;
}
void update(boolean filter, PGraphics pg) {
long currentTimeMillis = currentTimeMillis();
long lastModified = fragFile.lastModified();
if (vertFile != null) {
lastModified = max(lastModified, vertFile.lastModified());
}
if (compiledAtLeastOnce && currentTimeMillis < lastChecked + shaderRefreshRateInMillis) {
// println("compiled at least once, not checking, standard apply");
applyShader(compiledShader, filter, pg);
return;
}
if (!compiledAtLeastOnce && lastModified > lastKnownUncompilable) {
// println("first try");
tryCompileNewVersion(filter, pg, lastModified);
return;
}
lastChecked = currentTimeMillis;
if (lastModified > fragLastKnownModified && lastModified > lastKnownUncompilable) {
// println("file changed, repeat try");
tryCompileNewVersion(filter, pg, lastModified);
} else if (compiledAtLeastOnce) {
// println("file didn't change, standard apply");
applyShader(compiledShader, filter, pg);
}
}
private void applyShader(PShader shader, boolean filter, PGraphics pg) {
if (filter) {
pg.filter(shader);
} else {
pg.shader(shader);
}
}
private void tryCompileNewVersion(boolean filter, PGraphics pg, long lastModified) {
try {
PShader candidate;
if (vertFile == null) {
candidate = loadShader(fragPath);
} else {
candidate = loadShader(fragPath, vertPath);
}
// we need to call filter() or shader() here in order to catch any compilation errors and not halt
// the sketch
applyShader(candidate, filter, pg);
compiledShader = candidate;
compiledAtLeastOnce = true;
fragLastKnownModified = lastModified;
}
catch (Exception ex) {
lastKnownUncompilable = lastModified;
println("\n" + fragFile.getName() + ": " + ex.getMessage());
}
}
}
private class Key {
boolean justPressed;
boolean repeatedAlready = false;
boolean coded;
int character;
int lastRegistered = -1;
Key(Integer character, boolean coded) {
this.character = character;
this.coded = coded;
this.justPressed = true;
}
boolean repeatCheck() {
boolean shouldApply = justPressed ||
(!repeatedAlready && millis() > lastRegistered + KEY_REPEAT_DELAY_FIRST) ||
(repeatedAlready && millis() > lastRegistered + KEY_REPEAT_DELAY);
if (shouldApply) {
lastRegistered = millis();
if (!justPressed) {
repeatedAlready = true;
}
}
justPressed = false;
return shouldApply;
}
}
private class Group {
String name;
int animationStarted = -GROUP_TOGGLE_ANIMATION_DURATION;
boolean expanded = true;
ArrayList<Element> elements = new ArrayList<Element>();
float elementAlpha = 1;
Group(String name) {
this.name = name;
}
public boolean update(float y) {
if (activated(name, 0, y - cell, trayWidth, cell)) {
expanded = !expanded;
animationStarted = frameCount;
return true;
}
return false;
}
public void displayInTray(float x, float y) {
fill((keyboardSelected(name) || isMouseOverScrollAware(0, y - cell, trayWidth, cell)) ?
GRAYSCALE_TEXT_SELECTED : GRAYSCALE_TEXT_DARK);
textAlign(LEFT, BOTTOM);
textSize(textSize);
float animation = easedAnimation(animationStarted, GROUP_TOGGLE_ANIMATION_DURATION,
GROUP_TOGGLE_ANIMATION_EASING);
if (!expanded) {
animation = 1-animation;
}
elementAlpha = animation;
pushMatrix();
translate(cell*.3f, y-textSize*.55f);
rotate(animation*HALF_PI);
float size = cell*0.08f;
line(-size, size, size, 0);
line(-size, -size, size, 0);
popMatrix();
text(name, x, y);
}
public String getState() {
return GROUP_PREFIX + SEPARATOR + name + SEPARATOR + expanded;
}
public void setState(String state) {
String[] split = state.split(SEPARATOR);
expanded = Boolean.parseBoolean(split[2]);
}
}
private abstract class Element {
public float lastSelected = -DESELECTION_FADEOUT_DURATION;
Group group;
String name;
Element(Group group, String name) {
this.group = group;
this.name = name;
}
void reset() {
}
abstract boolean canHaveOverlay();
String getState() {
return group.name + SEPARATOR + name + SEPARATOR;
}
void setState(String newState) {
}
void update() {
}
void updateOverlay() {
}
void onOverlayShown() {
}
void onActivationWithoutOverlay(int x, float y, float w, float h) {
}
void displayOnTray(float x, float y) {
displayOnTray(x, y, name);
}
void displayOnTray(float x, float y, String text) {
textAlign(LEFT, BOTTOM);
textSize(textSize);
if (overlayVisible && this.equals(overlayOwner)) {
underlineAnimation(underlineTrayAnimationStarted, UNDERLINE_TRAY_ANIMATION_DURATION, x, y, true);
}
text(text, x, y);
}
float trayTextWidth() {
return textWidth(name);
}
void underlineAnimation(float startFrame, float duration, float x, float y, boolean stayExtended) {
float fullWidth = textWidth(name);
float animation = easedAnimation(startFrame, duration, UNDERLINE_TRAY_ANIMATION_EASING);
if (!stayExtended && animation == 1) {
animation = 0;
}
float w = fullWidth * animation;
float centerX = x + fullWidth * .5f;
strokeWeight(2);
line(centerX - w * .5f, y, centerX + w * .5f, y);
}
void displayCheckMarkOnTray(float x, float y, float animation, boolean fadeIn, boolean displayBox) {
float w = previewTrayBoxWidth;
pushMatrix();
translate(x - previewTrayBoxMargin, previewTrayBoxOffsetY);
noFill();
if (displayBox) {
rectMode(CENTER);
pushStyle();
strokeWeight(1);
stroke(GRAYSCALE_TEXT_DARK);
rect(-w, y - textSize * .5f, w, w);
popStyle();
}
strokeWeight(2);
beginShape();
int detail = 30;
float checkMarkTopLeftX = -w * 1.25f;
float checkMarkTopLeftY = y - textSize * .6f;
float lowestCheckMarkPointX = -w;
float lowestCheckMarkPointY = y - textSize * .4f;
float checkMarkTopRightX = 0;
float checkMarkTopRightY = y - textSize * .9f;
for (int i = 0; i < detail; i++) {
float iNorm = norm(i, 0, detail - 1);
if ((fadeIn && iNorm > animation) || (!fadeIn && iNorm < animation)) {
continue;
}
if (iNorm < .333f) {
float downwardStroke = norm(iNorm, 0, .333f);
float downwardX = lerp(checkMarkTopLeftX, lowestCheckMarkPointX, downwardStroke);
float downwardY = lerp(checkMarkTopLeftY, lowestCheckMarkPointY, downwardStroke);
vertex(downwardX, downwardY);
continue;
}
float upwardStroke = norm(iNorm, .333f, 1);
float upwardX = lerp(lowestCheckMarkPointX, checkMarkTopRightX, upwardStroke);
float upwardY = lerp(lowestCheckMarkPointY, checkMarkTopRightY, upwardStroke);
vertex(upwardX, upwardY);
}
endShape();
popMatrix();
}
void handleKeyboardInput() {
}
}
private class Radio extends Element {
ArrayList<String> options = new ArrayList<String>();
int valueIndex = 0;
Radio(Group parent, String name, String[] options) {
super(parent, name);
this.options.add(name);
this.options.addAll(Arrays.asList(options));
}
String getState() {
return super.getState() + valueIndex;
}
void setState(String newState) {
valueIndex = Integer.parseInt(newState.split(SEPARATOR)[2]);
}
public void reset() {
valueIndex = 0;
}
boolean canHaveOverlay() {
return false;
}
String value() {
return options.get(valueIndex);
}
void displayOnTray(float x, float y) {
super.displayOnTray(x, y, value());
displayDotsOnTray(x, y);
}
private void displayDotsOnTray(float x, float y) {
for (int i = 0; i < options.size(); i++) {
float size = 4;
float rectX = x + cell * .15f + i * size * 2.5f;
if (i == valueIndex) {
size *= 1.8f;
pushStyle();
noFill();
}
rectMode(CENTER);
rect(rectX, y + cell * .1f, size, size);
if (i == valueIndex) {
popStyle();
}
}
}
void onActivationWithoutOverlay(int x, float y, float w, float h) {
pushCurrentStateToUndo();
valueIndex++;
if (valueIndex >= options.size()) {
valueIndex = 0;
}
}
float trayTextWidth() {
return textWidth(value());
}
}
private class Button extends Element {
boolean value;
private float activationStarted = -CHECK_ANIMATION_DURATION * 2;
Button(Group parent, String name) {
super(parent, name);
}
boolean canHaveOverlay() {
return false;
}
void onActivationWithoutOverlay(int x, float y, float w, float h) {
value = true;
activationStarted = frameCount;
}
void displayOnTray(float x, float y) {
float checkMarkAnimation = easedAnimation(activationStarted, CHECK_ANIMATION_DURATION * 2,
CHECK_ANIMATION_EASING);
if (checkMarkAnimation > 0 && checkMarkAnimation < 1) {
if (checkMarkAnimation < .5) {
displayCheckMarkOnTray(x, y, checkMarkAnimation * 2, true, false);
} else {
displayCheckMarkOnTray(x, y, (checkMarkAnimation - .5f) * 2, false, false);
}
}
super.displayOnTray(x, y);
}
void update() {
value = false;
}
}
private class Toggle extends Element {
boolean checked, checkByDefault;
private float activationStarted = -UNDERLINE_TRAY_ANIMATION_DURATION;
Toggle(Group parent, String name, boolean initialState) {
super(parent, name);
this.checkByDefault = initialState;
this.checked = initialState;
}
String getState() {
return super.getState() + checked;
}
void setState(String newState) {
this.checked = Boolean.parseBoolean(newState.split(SEPARATOR)[2]);
}
boolean canHaveOverlay() {
return false;
}
void displayOnTray(float x, float y) {
float checkMark = easedAnimation(activationStarted, CHECK_ANIMATION_DURATION, CHECK_ANIMATION_EASING);
displayCheckMarkOnTray(x, y, checkMark, checked, true);
super.displayOnTray(x, y);
}
void reset() {
checked = checkByDefault;
}
void update() {
if (overlayVisible && overlayOwner != null && overlayOwner.equals(this) && actions.contains(ACTION_RESET)) {
pushCurrentStateToUndo();
reset();
}
}
void onActivationWithoutOverlay(int x, float y, float w, float h) {
pushCurrentStateToUndo();
activationStarted = frameCount;
checked = !checked;
}
}
private abstract class Slider extends Element {
Slider(Group parent, String name) {
super(parent, name);
}
protected float updateFullHorizontalSlider(float x, float y, float w, float h, float value, float precision,
float horizontalRevealAnimationStarted, boolean alternative,
float minValue, float maxValue) {
float deltaX = updateInfiniteSlider(precision, width, true, true, alternative);
float horizontalAnimation = easedAnimation(horizontalRevealAnimationStarted - SLIDER_REVEAL_START_SKIP,
SLIDER_REVEAL_DURATION, SLIDER_REVEAL_EASING);
displayInfiniteSliderCenterMode(x + width * .5f, y, w, h,
precision, value, horizontalAnimation, true, true, false, minValue, maxValue);
return deltaX;
}
@SuppressWarnings("SuspiciousNameCombination")
protected float updateFullHeightVerticalSlider(float x, float y, float w, float h, float value, float precision,
float verticalRevealAnimationStarted, boolean alternative,
float minValue, float maxValue) {
float deltaY = updateInfiniteSlider(precision, height, false, true, alternative);
float verticalAnimation = easedAnimation(verticalRevealAnimationStarted - SLIDER_REVEAL_START_SKIP,
SLIDER_REVEAL_DURATION, SLIDER_REVEAL_EASING);
displayInfiniteSliderCenterMode(x + height * .5f, y, w, h,
precision, value, verticalAnimation, false, true, false, minValue, maxValue);
return deltaY;
}
protected float updateInfiniteSlider(float precision, float sliderWidth, boolean horizontal, boolean reversed,
boolean alternative) {
if (mousePressed && isMouseOutsideGui()) {
float screenSpaceDelta = horizontal ? (pmouseX - mouseX) : (pmouseY - mouseY);
if (reversed) {
screenSpaceDelta *= -1;
}
float valueSpaceDelta = screenDistanceToValueDistance(screenSpaceDelta, precision);
if (valueSpaceDelta != 0) {
return valueSpaceDelta;
}
}
if (previousActions.contains(ACTION_ALT) && previousActions.contains(ACTION_CONTROL)) {
return keyboardDelta(true, horizontal, precision);
}
if ((alternative && !previousActions.contains(ACTION_ALT)) ||
(!alternative && previousActions.contains(ACTION_ALT))) {
return 0;
}
return keyboardDelta(false, horizontal, precision);
}
private float keyboardDelta(boolean directionless, boolean horizontal, float precision) {
if (actions.contains(ACTION_LEFT) && (horizontal || directionless)) {
return screenDistanceToValueDistance(-3, precision);
}
if (actions.contains(ACTION_RIGHT) && (horizontal || directionless)) {
return screenDistanceToValueDistance(3, precision);
}
if (actions.contains(ACTION_UP) && (!horizontal || directionless)) {
return screenDistanceToValueDistance(-3, precision);
}
if (actions.contains(ACTION_DOWN) && (!horizontal || directionless)) {
return screenDistanceToValueDistance(3, precision);
}
return 0f;
}
float screenDistanceToValueDistance(float screenSpaceDelta, float precision) {
float valueToScreenRatio = precision / width;
return screenSpaceDelta * valueToScreenRatio;
}
void displayInfiniteSliderCenterMode(float x, float y, float w, float h, float precision, float value,
float revealAnimation, boolean horizontal, boolean cutout,
boolean floored, float minValue, float maxValue) {
float markerHeight = h * revealAnimation;
pushMatrix();
pushStyle();
if (!horizontal) {
translate(width * .5f, height * .5f);
rotate(-HALF_PI);
translate(-height * .5f, -width * .5f);
}
translate(x, y);
noStroke();
displaySliderBackground(w, h, cutout, horizontal);
float weight = 2;
strokeWeight(weight);
displayHorizontalLine(w, revealAnimation);
if (!horizontal) {
pushMatrix();
scale(-1, 1);
}
displayMarkerLines(precision * 0.5f, 0, markerHeight * .6f, weight * revealAnimation,
true, value, precision, w, h, !horizontal, revealAnimation, minValue, maxValue);
displayMarkerLines(precision * .05f, 10, markerHeight * .3f, weight * revealAnimation,
false, value, precision, w, h, !horizontal, revealAnimation, minValue, maxValue);
if (!horizontal) {
popMatrix();
}
displayValue(w, h, precision, value, revealAnimation, floored);
popMatrix();
popStyle();
}
void displaySliderBackground(float w, float h, boolean cutout, boolean horizontal) {
fill(0, BACKGROUND_ALPHA);
rectMode(CENTER);
float xOffset = 0;
if (cutout) {
if (horizontal && trayVisible) {
xOffset = trayWidth;
} else if (!horizontal) {
xOffset = h;
}
}
rect(xOffset, 0, w, h);
}
void displayHorizontalLine(float w, float revealAnimation) {
stroke(GRAYSCALE_TEXT_DARK);
beginShape();
w *= revealAnimation;
for (int i = 0; i < w; i++) {
float iNorm = norm(i, 0, w);
float screenX = lerp(-w, w, iNorm);
stroke(GRAYSCALE_TEXT_SELECTED, darkenEdges(screenX, w));
vertex(screenX, 0);
}
endShape();
}
void displayMarkerLines(float frequency, int skipEveryNth, float markerHeight, float horizontalLineHeight,
boolean shouldDisplayValue, float value, float precision, float w, float h,
boolean flipTextHorizontally, float revealAnimationEased, float minValue,
float maxValue) {
float markerValue = -precision - value - frequency;
int i = 0;
while (markerValue < precision - value) {
markerValue += frequency;
if (skipEveryNth != 0 && i++ % skipEveryNth == 0) {
continue;
}
float markerNorm = norm(markerValue, -precision - value, precision - value);
displayMarkerLine(markerValue, precision, w, h, markerHeight, horizontalLineHeight, value,
shouldDisplayValue, flipTextHorizontally,
revealAnimationEased, minValue, maxValue);
}
}
void displayMarkerLine(float markerValue, float precision, float w, float h, float markerHeight,
float horizontalLineHeight,
float value, boolean shouldDisplayValue, boolean flipTextHorizontally,
float revealAnimationEased, float minValue, float maxValue) {
float moduloValue = markerValue;
while (moduloValue > precision) {
moduloValue -= precision * 2;
}
while (moduloValue < -precision) {
moduloValue += precision * 2;
}
float screenX = map(moduloValue, -precision, precision, -w, w);
float displayValue = moduloValue + value;
boolean isEdgeValue =
(displayValue < minValue + precision * .1 && displayValue > minValue - precision * .1) ||
(displayValue > maxValue - precision * .1 && displayValue < maxValue + precision * .1);
if (!isEdgeValue && (displayValue > maxValue || displayValue < minValue)) {
return;
}
float grayscale = darkenEdges(screenX, w);
fill(GRAYSCALE_TEXT_SELECTED, grayscale * revealAnimationEased);
stroke(GRAYSCALE_TEXT_SELECTED, grayscale * revealAnimationEased);
line(screenX, -markerHeight * .5f, screenX, -horizontalLineHeight * .5f);
if (shouldDisplayValue) {
if (flipTextHorizontally) {
pushMatrix();
scale(-1, 1);
}
String displayText = nf(displayValue, 0, 0);
if (displayText.equals("-0")) {
displayText = "0";
}
pushMatrix();
textAlign(CENTER, CENTER);
textSize(textSize);
float textX = screenX + ((displayText.equals("0") || displayValue > 0) ? 0 : -textWidth("-") * .5f);
text(displayText, flipTextHorizontally ? -textX : textX, h * .25f);
if (flipTextHorizontally) {
popMatrix();
}
popMatrix();
}
}
void displayValue(float w, float sliderHeight, float precision, float value, float animationEased,
boolean floored) {
fill(GRAYSCALE_TEXT_DARK);
textAlign(CENTER, CENTER);
textSize(textSize * 1.2f);
float textY = -cell * 2.5f;
float textX = 0;
String text;
if (floored) {
text = String.valueOf(floor(value));
} else if (abs(value) < 1) {
if (abs(value) < precision * .001f) {
text = nf(value, 0, 0);
} else {
text = String.valueOf(value);
}
} else {
text = nf(value, 0, 2);
}
if (text.startsWith("-")) {
textX -= textWidth("-") * .5f;
}
noStroke();
fill(0, BACKGROUND_ALPHA);
rectMode(CENTER);
rect(textX, textY + textSize * .2f, textWidth(text) + 20, textSize * 1.2f + 20);
fill(GRAYSCALE_TEXT_SELECTED * animationEased);
text(text, textX, textY);
stroke(GRAYSCALE_TEXT_SELECTED);
line(0, -5, 0, 5);
}
float darkenEdges(float screenX, float w) {
float xNorm = norm(screenX, -w, w);
float distanceFromCenter = abs(.5f - xNorm) * 4;
return 1 - ease(distanceFromCenter, SLIDER_EDGE_DARKEN_EASING);
}
void recordStateForUndo() {
if (mouseJustPressedOutsideGui() || keyboardInteractionJustStarted()) {
pushCurrentStateToUndo();
}
}
boolean keyboardInteractionJustStarted() {
boolean wasKeyboardActive = previousActions.contains(ACTION_LEFT) || previousActions.contains(ACTION_RIGHT);
boolean isKeyboardActive = actions.contains(ACTION_LEFT) || actions.contains(ACTION_RIGHT);
return !wasKeyboardActive && isKeyboardActive;
}
}
private class SliderFloat extends Slider {
boolean constrained, floored;
float value, precision, defaultValue, defaultPrecision, minValue, maxValue, lastValueDelta;
float sliderRevealAnimationStarted = -SLIDER_REVEAL_DURATION;
SliderFloat(Group parent, String name, float defaultValue, float precision,
boolean constrained, float min, float max, boolean floored) {
super(parent, name);
this.value = defaultValue;
this.defaultValue = defaultValue;
this.precision = precision;
this.defaultPrecision = precision;
this.floored = floored;
if (constrained) {
this.constrained = true;
minValue = min;
maxValue = max;
} else {
autoDetectConstraints(name);
}
}
private void autoDetectConstraints(String name) {
if (name.equals("fill") || name.equals("stroke")) {
this.constrained = true;
minValue = 0;
maxValue = 255;
} else if (name.contains("count") || name.contains("size") || name.contains("step")) {
this.constrained = true;
minValue = 0;
maxValue = Float.MAX_VALUE;
if (value == 0) {
this.value = 1;
}
} else if (name.equals("drag")) {
this.constrained = true;
minValue = 0;
maxValue = 1;
}
}
void handleKeyboardInput() {
if (previousActions.contains(ACTION_PRECISION_ZOOM_OUT) &&
((!floored && precision < FLOAT_PRECISION_MAXIMUM) || (floored && precision < INT_PRECISION_MAXIMUM))) {
precision *= 10f;
pushCurrentStateToUndo();
}
if (previousActions.contains(ACTION_PRECISION_ZOOM_IN) &&
((!floored && precision > FLOAT_PRECISION_MINIMUM) || (floored && precision > INT_PRECISION_MINIMUM))) {
precision *= .1f;
pushCurrentStateToUndo();
}
if (overlayVisible && overlayOwner.equals(this) && actions.contains(ACTION_RESET)) {
pushCurrentStateToUndo();
reset();
}
}
void reset() {
precision = defaultPrecision;
value = defaultValue;
}
String getState() {
return super.getState() + value + SEPARATOR + precision;
}
void setState(String newState) {
String[] split = newState.split(SEPARATOR);
// println(name, "value", value);
value = Float.parseFloat(split[2]);
precision = Float.parseFloat(split[3]);
}
void onOverlayShown() {
if (!overlayVisible || !horizontalOverlayVisible) {
sliderRevealAnimationStarted = frameCount;
}
horizontalOverlayVisible = true;
verticalOverlayVisible = false;
pickerOverlayVisible = false;
zOverlayVisible = false;
}
boolean canHaveOverlay() {
return true;
}
void updateOverlay() {
float valueDelta = updateInfiniteSlider(precision, width, true, false, false);
recordStateForUndo();
value += valueDelta;
lastValueDelta = valueDelta;
if (floored && valueDelta == 0 && lastValueDelta == 0) {
value = lerp(value, round(value), .2f);
}
if (constrained) {
value = constrain(value, minValue, maxValue);
}
float revealAnimation = easedAnimation(sliderRevealAnimationStarted - SLIDER_REVEAL_START_SKIP,
SLIDER_REVEAL_DURATION,
SLIDER_REVEAL_EASING);
displayInfiniteSliderCenterMode(width * .5f, height - cell, width, sliderHeight, precision,
value, revealAnimation, true, true, floored,
constrained ? minValue : -Float.MAX_VALUE,
constrained ? maxValue : Float.MAX_VALUE);
}
}
private class SliderXY extends Slider {
float deltaX;
float deltaY;
PVector value = new PVector();
PVector defaultValue = new PVector();
float precision, defaultPrecision;
float horizontalRevealAnimationStarted = -SLIDER_REVEAL_DURATION;
float verticalRevealAnimationStarted = -SLIDER_REVEAL_DURATION;
float interactionBufferMultiplier = 2.5f;
SliderXY(Group currentGroup, String name, float defaultX, float defaultY, float precision) {
super(currentGroup, name);
this.precision = precision;
this.defaultPrecision = precision;
value.x = defaultX;
value.y = defaultY;
defaultValue.x = defaultX;
defaultValue.y = defaultY;
}
String getState() {
return super.getState() + precision + SEPARATOR + value.x + SEPARATOR + value.y;
}
void setState(String newState) {
String[] xyz = newState.split(SEPARATOR);
precision = Float.parseFloat(xyz[2]);
value.x = Float.parseFloat(xyz[3]);
value.y = Float.parseFloat(xyz[4]);
}
boolean canHaveOverlay() {
return true;
}
void onOverlayShown() {
if (!overlayVisible || !horizontalOverlayVisible) {
horizontalRevealAnimationStarted = frameCount;
}
if (!overlayVisible || !verticalOverlayVisible) {
verticalRevealAnimationStarted = frameCount;
}
horizontalOverlayVisible = true;
verticalOverlayVisible = true;
pickerOverlayVisible = false;
zOverlayVisible = false;
}
void updateOverlay() {
recordStateForUndo();
updateXYSliders();
lockOtherSlidersOnMouseOver();
value.x += deltaX;
value.y += deltaY;
}
protected void lockOtherSlidersOnMouseOver() {
if (keyboardActive) {
return;
}
if (isMouseOverXSlider()) {
deltaY = 0;
} else if (isMouseOverYSlider()) {
deltaX = 0;
}
}
protected boolean isMouseOverXSlider() {
return isMouseOver(0, height - cell * interactionBufferMultiplier, width, sliderHeight*2);
}
protected boolean isMouseOverYSlider() {
return isMouseOver(width - cell * interactionBufferMultiplier, 0, sliderHeight*2, height);
}
void updateXYSliders() {
deltaX = updateFullHorizontalSlider(0, height - cell, width, sliderHeight, value.x, precision,
horizontalRevealAnimationStarted, false, -Float.MAX_VALUE, Float.MAX_VALUE);
deltaY = updateFullHeightVerticalSlider(0, width - cell, height, sliderHeight, value.y, precision,
verticalRevealAnimationStarted, false, -Float.MAX_VALUE, Float.MAX_VALUE);
}
boolean keyboardInteractionJustStarted() {
boolean wasKeyboardActive = previousActions.contains(ACTION_LEFT) ||
previousActions.contains(ACTION_RIGHT) ||
previousActions.contains(ACTION_UP) ||
previousActions.contains(ACTION_DOWN);
boolean isKeyboardActive = actions.contains(ACTION_LEFT) ||
actions.contains(ACTION_RIGHT) ||
actions.contains(ACTION_UP) ||
actions.contains(ACTION_DOWN);
return !wasKeyboardActive && isKeyboardActive;
}
void handleKeyboardInput() {
if (previousActions.contains(ACTION_PRECISION_ZOOM_IN) && precision > FLOAT_PRECISION_MINIMUM) {
precision *= .1f;
pushCurrentStateToUndo();
}
if (previousActions.contains(ACTION_PRECISION_ZOOM_OUT) && precision < FLOAT_PRECISION_MAXIMUM) {
precision *= 10f;
pushCurrentStateToUndo();
}
if (overlayVisible && overlayOwner.equals(this) && actions.contains(ACTION_RESET)) {
pushCurrentStateToUndo();
reset();
}
}
void reset() {
precision = defaultPrecision;
value.x = defaultValue.x;
value.y = defaultValue.y;
value.z = defaultValue.z;
}
}
private class SliderXYZ extends SliderXY {
private float zRevealAnimationStarted = -SLIDER_REVEAL_DURATION;
private float deltaZ;
SliderXYZ(Group currentGroup, String name, float defaultX, float defaultY, float defaultZ, float precision) {
super(currentGroup, name, defaultX, defaultY, precision);
this.defaultValue.z = defaultZ;
this.value.z = defaultZ;
}
void update() {
super.update();
}
void handleKeyboardInput() {
super.handleKeyboardInput();
}
String getState() {
return super.getState() + SEPARATOR + value.z;
}
void setState(String newState) {
super.setState(newState);
value.z = Float.parseFloat(newState.split(SEPARATOR)[5]);
}
void updateOverlay() {
recordStateForUndo();
deltaZ = updateInfiniteSlider(precision, height * .5f, false, true, true);
lockOtherSlidersOnMouseOver();
value.x += deltaX;
value.y += deltaY;
value.z += deltaZ;
float zAnimation = easedAnimation(zRevealAnimationStarted, SLIDER_REVEAL_DURATION, SLIDER_REVEAL_EASING);
displayInfiniteSliderCenterMode(height - height * .2f, width - sliderHeight * 2, height / 3f,
sliderHeight * .8f, precision, value.z, zAnimation, false, false,
false, -Float.MAX_VALUE, Float.MAX_VALUE);
super.updateXYSliders();
}
private boolean isMouseOverZSlider() {
stroke(1);
return isMouseOver(width - sliderHeight * 4, 0, sliderHeight * 3, height * .4f);
}
protected void lockOtherSlidersOnMouseOver() {
if (isMouseOverXSlider()) {
deltaY = 0;
deltaZ = 0;
}
if (isMouseOverYSlider()) {
deltaX = 0;
deltaZ = 0;
}
if (isMouseOverZSlider()) {
deltaX = 0;
deltaY = 0;
} else if (!(actions.contains(ACTION_LEFT) ||
actions.contains(ACTION_UP) ||
actions.contains(ACTION_RIGHT) ||
actions.contains(ACTION_DOWN))) {
deltaZ = 0;
}
}
void onOverlayShown() {
if (!overlayVisible || !horizontalOverlayVisible) {
horizontalRevealAnimationStarted = frameCount;
}
if (!overlayVisible || !verticalOverlayVisible) {
verticalRevealAnimationStarted = frameCount;
}
if (!overlayVisible || !zOverlayVisible) {
zRevealAnimationStarted = frameCount;
}
horizontalOverlayVisible = true;
verticalOverlayVisible = true;
pickerOverlayVisible = false;
zOverlayVisible = true;
}
}
public class HSBA {
private float hue, sat, br, alpha;
public HSBA(float hue, float sat, float br, float alpha) {
this.hue = hue;
this.sat = sat;
this.br = br;
this.alpha = alpha;
}
public HSBA() {
this.alpha = 1;
}
public int clr() {
pushStyle();
enforceConstraints();
colorMode(HSB, 1, 1, 1, 1);
int result = color(hue, sat, br, alpha);
popStyle();
return result;
}
public float hue() {
enforceConstraints();
return hue;
}
public void addHue(float val) {
hue += val;
enforceConstraints();
}
public float sat() {
enforceConstraints();
return sat;
}
public void setSat(float val) {
sat = val;
enforceConstraints();
}
public float br() {
enforceConstraints();
return br;
}
public void setBr(float val) {
br = val;
enforceConstraints();
}
public float alpha() {
enforceConstraints();
return alpha;
}
private void enforceConstraints() {
hue = hueModulo(hue);
sat = constrain(sat, 0, 1);
br = constrain(br, 0, 1);
alpha = constrain(alpha, 0, 1);
}
public void setAlpha(float val) {
alpha = val;
enforceConstraints();
}
}
private class ColorPicker extends Slider {
HSBA hsba;
float defaultHue, defaultSat, defaultBr, defaultAlpha;
float pickerRevealStarted = -PICKER_REVEAL_DURATION;
float huePrecision = .5f;
float alphaPrecision = 1;
private boolean brightnessLocked, saturationLocked;
private boolean satChanged, brChanged;
ColorPicker(Group currentGroup, String name, float hue, float sat, float br, float alpha) {
super(currentGroup, name);
this.hsba = new HSBA(hue, sat, br, alpha);
this.defaultHue = hue;
this.defaultSat = sat;
this.defaultBr = br;
this.defaultAlpha = alpha;
}
void handleKeyboardInput() {
if (overlayVisible && overlayOwner != null && overlayOwner.equals(this) && actions.contains(ACTION_RESET)) {
pushCurrentStateToUndo();
reset();
}
if (alphaPrecision > ALPHA_PRECISION_MINIMUM && previousActions.contains(ACTION_PRECISION_ZOOM_IN)) {
alphaPrecision *= .1f;
pushCurrentStateToUndo();
}
if (alphaPrecision < ALPHA_PRECISION_MAXIMUM && previousActions.contains(ACTION_PRECISION_ZOOM_OUT)) {
alphaPrecision *= 10;
pushCurrentStateToUndo();
}
if (previousActions.contains(ACTION_CONTROL)) {
satChanged = true;
if (actions.contains(ACTION_UP)) {
hsba.sat -= .01f;
} else if (actions.contains(ACTION_DOWN)) {
hsba.sat += .01f;
}
}
if (previousActions.contains(ACTION_ALT)) {
brChanged = true;
if (actions.contains(ACTION_UP)) {
hsba.br -= .01f;
} else if (actions.contains(ACTION_DOWN)) {
hsba.br += .01f;
}
}
hsba.enforceConstraints();
}
void reset() {
hsba.hue = defaultHue;
hsba.sat = defaultSat;
hsba.br = defaultBr;
hsba.alpha = defaultAlpha;
}
String getState() {
return super.getState() + hsba.hue + SEPARATOR + hsba.sat + SEPARATOR + hsba.br + SEPARATOR + hsba.alpha + SEPARATOR + alphaPrecision;
}
void setState(String newState) {
String[] split = newState.split(SEPARATOR);
hsba.hue = Float.parseFloat(split[2]);
hsba.sat = Float.parseFloat(split[3]);
hsba.br = Float.parseFloat(split[4]);
hsba.alpha = Float.parseFloat(split[5]);
alphaPrecision = Float.parseFloat(split[6]);
}
void displayOnTray(float x, float y) {
pushStyle();
stroke(GRAYSCALE_TEXT_DARK);
strokeWeight(1);
fill(hsba.clr());
rectMode(CENTER);
rect(x - previewTrayBoxMargin - previewTrayBoxWidth,
y - textSize * .5f, previewTrayBoxWidth, previewTrayBoxWidth);
popStyle();
super.displayOnTray(x, y);
}
boolean canHaveOverlay() {
return true;
}
void onOverlayShown() {
if (!pickerOverlayVisible) {
pickerRevealStarted = frameCount;
}
pickerOverlayVisible = true;
horizontalOverlayVisible = false;
verticalOverlayVisible = false;
zOverlayVisible = false;
}
@SuppressWarnings("SuspiciousNameCombination")
void updateOverlay() {
if (mouseJustReleased()) {
brightnessLocked = false;
saturationLocked = false;
}
recordStateForUndo();
pushStyle();
colorMode(HSB, 1, 1, 1, 1);
float revealAnimation = easedAnimation(pickerRevealStarted - PICKER_REVEAL_START_SKIP,
PICKER_REVEAL_DURATION, PICKER_REVEAL_EASING);
int tinySliderCount = 2;
float tinySliderMarginCellFraction = .2f;
float tinySliderWidth = cell * 1.5f * (1 + tinySliderMarginCellFraction);
float x = width - tinySliderWidth * tinySliderCount * (1 + tinySliderMarginCellFraction)
+ tinySliderMarginCellFraction * tinySliderCount;
float h = cell * 4;
float tinySliderTopY =
height - sliderHeight * .5f - cell * tinySliderMarginCellFraction - h * revealAnimation;
float lastSat = hsba.sat;
hsba.sat = updateTinySlider(x, tinySliderTopY, tinySliderWidth, h, brightnessLocked, SATURATION);
if (hsba.sat != lastSat && !saturationLocked) {
brightnessLocked = true;
}
if (saturationLocked) {
hsba.sat = lastSat;
}
displayTinySlider(x, tinySliderTopY, tinySliderWidth, cell * 4, hsba.sat, SATURATION,
brightnessLocked);
x += tinySliderWidth * 1.2f;
float lastBr = hsba.br;
hsba.br = updateTinySlider(x, tinySliderTopY, tinySliderWidth, h, saturationLocked, BRIGHTNESS);
if (hsba.br != lastBr && !brightnessLocked) {
saturationLocked = true;
}
if (brightnessLocked) {
hsba.br = lastBr;
}
displayTinySlider(x, tinySliderTopY, tinySliderWidth, cell * 4, hsba.br, BRIGHTNESS, saturationLocked);
displayInfiniteSliderCenterMode(height - height / 4f, width - sliderHeight * .5f, height / 2f, sliderHeight,
alphaPrecision, hsba.alpha, revealAnimation, false, false, false, 0, 1);
fill(GRAYSCALE_TEXT_DARK);
textAlign(CENTER, CENTER);
textSize(textSize);
text("alpha", width - sliderHeight * .5f, 15);
float alphaDelta = updateInfiniteSlider(alphaPrecision, height, false, false, false);
boolean isMouseInTopHalf = isMouseOver(width*.5f, 0, width*.5f, height / 2f);
if (!satChanged && !brChanged && (keyboardActive || isMouseInTopHalf)) {
hsba.alpha += alphaDelta;
}
displayHueSlider(sliderHeight, revealAnimation);
float hueDelta = updateInfiniteSlider(huePrecision, width, true, false, false);
if (!satChanged && !brChanged && (keyboardActive || !isMouseInTopHalf)) {
hsba.hue += hueDelta;
}
satChanged = false;
brChanged = false;
hsba.enforceConstraints();
displayValueRectangle(sliderHeight);
popStyle();
}
private void displayHueSlider(float h, float revealAnimation) {
displayHueStripCornerMode(height + cell - h * revealAnimation, h * .5f, true, revealAnimation);
displayHueStripCornerMode(height + cell - h * .5f * revealAnimation, h * .5f, false, revealAnimation);
}
private void displayHueStripCornerMode(float y, float h, boolean top, float revealAnimation) {
beginShape(TRIANGLE_STRIP);
noStroke();
int detail = floor(width * .3f);
for (int i = 0; i < detail; i++) {
float iNorm = norm(i, 0, detail - 1);
float x = iNorm * width;
if (abs(.5f - iNorm) * 2 > revealAnimation) {
continue;
}
float iHue = hueModulo(hsba.hue - .5f + iNorm);
int iColor = getColorAt(iHue, HUE);
fill(iColor);
vertex(x, y);
vertex(x, y + h);
}
endShape();
}
private void displayValueRectangle(float hueSliderHeight) {
float x = width * .5f;
float y = height - hueSliderHeight - cell * 3f;
noStroke();
fill(hsba.clr());
rectMode(CENTER);
rect(x, y, cell * 3, cell * 3);
}
private float updateTinySlider(float x, float topY, float w, float h, boolean forceActive, String type) {
float interactionBuffer = cell;
if (forceActive || (mousePressed && isMouseOver(x, topY - interactionBuffer, w,
h + interactionBuffer * 1.2f))) {
float newValue = constrain(map(mouseY, topY, topY + h, 0, 1), 0, 1);
setTinySliderValue(newValue, type);
}
return getTinySliderValue(type);
}
private void displayTinySlider(float x, float topY, float w, float h, float value, String type,
boolean mouseOver) {
beginShape(TRIANGLE_STRIP);
noStroke();
int detail = floor(h * .1f);
for (int i = 0; i < detail; i++) {
float iNorm = norm(i, 0, detail - 1);
float y = topY + h * iNorm;
fill(getColorAt(iNorm, type));
vertex(x, y);
vertex(x + w, y);
}
endShape();
float valueY = topY + h * value;
strokeWeight(2);
stroke((type.equals(SATURATION) && satChanged) ||
(type.equals(BRIGHTNESS) && brChanged) || mouseOver ?
GRAYSCALE_TEXT_SELECTED : GRAYSCALE_TEXT_DARK);
line(x - 2, valueY, x + w + 2, valueY);
}
private int getColorAt(float value, String type) {
if (type.equals(HUE)) {
return color(value, hsba.sat, hsba.br, hsba.alpha);
}
if (type.equals(SATURATION)) {
return color(hsba.hue, value, hsba.br, hsba.alpha);
}
if (type.equals(BRIGHTNESS)) {
return color(hsba.hue, hsba.sat, value, hsba.alpha);
}
return 0;
}
private float getTinySliderValue(String type) {
if (type.equals(SATURATION)) {
return hsba.sat;
}
if (type.equals(BRIGHTNESS)) {
return hsba.br;
}
return 0;
}
private void setTinySliderValue(float newValue, String type) {
if (type.equals(SATURATION)) {
hsba.sat = newValue;
satChanged = true;
}
if (type.equals(BRIGHTNESS)) {
hsba.br = newValue;
brChanged = true;
}
}
HSBA getHSBA() {
return hsba;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment