Skip to content

Instantly share code, notes, and snippets.

@RustyKnight
Created July 19, 2022 08:12
Show Gist options
  • Save RustyKnight/40a4581830b9899c6e18f5ddee0707a9 to your computer and use it in GitHub Desktop.
Save RustyKnight/40a4581830b9899c6e18f5ddee0707a9 to your computer and use it in GitHub Desktop.
Animate sorting
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new SortPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class SortPane extends JPanel {
final int SCREEN_WIDTH = 1280 / 3;
final int SCREEN_HEIGHT = 720 / 3;
final int BAR_HEIGHT = SCREEN_HEIGHT * 4 / 5;
final int BAR_WIDTH = 5;
final int NUM_BARS = SCREEN_WIDTH / BAR_WIDTH;
private JButton bubbleSort = new JButton();
private JButton insertSort = new JButton();
private int[] values = new int[NUM_BARS];
private int current = 0;
private int traverse = 0;
private Timer timer;
private int eventIndex;
private int min = 0;
private int max = 0;
private ColorBand colorBand = new ColorBand(
new float[]{0f, 1f},
new Color[]{Color.BLUE, Color.MAGENTA}
);
public SortPane() {
for (int i = 0; i < NUM_BARS; i++) {
values[i] = i * BAR_HEIGHT / NUM_BARS;
}
initSpan();
//InsertionSort Button
insertSort.setText("Insertion Sort");
this.add(insertSort);
insertSort.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
insertSort.setEnabled(false);
bubbleSort.setEnabled(false);
SortWorker worker = new SortWorker(values, new InsertionSort(), new SortWorker.Observer() {
@Override
public void stateDidChange(SortWorker worker, SwingWorker.StateValue state) {
System.out.println("State did change " + state);
}
@Override
public void didComplete(SortWorker worker, SortModel model) {
animate(model);
}
@Override
public void didFail(SortWorker worker, Exception exp) {
exp.printStackTrace();
JOptionPane.showMessageDialog(SortPane.this, "Pants", "Pants", JOptionPane.ERROR_MESSAGE);
animationDidComplete();
}
});
worker.execute();
}
});
//BubbleSort Button
bubbleSort.setText("Bubble Sort");
this.add(bubbleSort);
bubbleSort.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
}
});
}
protected void initSpan() {
min = Integer.MAX_VALUE;
max = Integer.MIN_VALUE;
for (int value : values) {
min = Math.min(value, min);
max = Math.max(value, max);
}
}
protected void animationDidComplete() {
insertSort.setEnabled(true);
bubbleSort.setEnabled(true);
}
protected void animate(SortModel model) {
if (timer != null) {
timer.stop();
}
eventIndex = -1;
values = model.getUnsortedValues();
initSpan();
timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
eventIndex++;
List<SortEvent> events = model.getEvents();
if (eventIndex >= events.size()) {
timer.stop();
animationDidComplete();
return;
}
SortEvent event = events.get(eventIndex);
if (event instanceof SortCurrentEvent) {
SortCurrentEvent evt = (SortCurrentEvent) event;
current = evt.getCurrent();
} else if (event instanceof SortTraverseEvent) {
SortTraverseEvent evt = (SortTraverseEvent) event;
traverse = evt.getTraverse();
} else if (event instanceof SortSwapEvent) {
SortSwapEvent evt = (SortSwapEvent) event;
int temp = values[evt.from()];
values[evt.from()] = values[evt.to()];
values[evt.to()] = temp;
}
repaint();
}
});
timer.start();
repaint();
}
@Override
public Dimension getPreferredSize() {
return new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
for (int i = 0; i < values.length; i++) {
int value = values[i];
float fraction = (float) (value - min) / (float) (max - min);
g2d.setColor(colorBand.colorAt(fraction));
g2d.fillRect(i * BAR_WIDTH, SCREEN_HEIGHT - values[i], BAR_WIDTH, values[i]);
}
g2d.setColor(Color.green);
g2d.fillRect(current * BAR_WIDTH, SCREEN_HEIGHT - values[current], BAR_WIDTH, values[current]);
g2d.setColor(Color.red);
g2d.fillRect(traverse * BAR_WIDTH, SCREEN_HEIGHT - values[traverse], BAR_WIDTH, values[traverse]);
g2d.dispose();
}
}
public interface SortEvent {
}
public interface SortSwapEvent extends SortEvent {
public int from();
public int to();
}
public interface SortCurrentEvent extends SortEvent {
public int getCurrent();
}
public interface SortTraverseEvent extends SortEvent {
public int getTraverse();
}
public interface SortModel {
public int[] getUnsortedValues();
public int[] getSortedValues();
public List<SortEvent> getEvents();
}
public class DefaultSortCurrentEvent implements SortCurrentEvent {
private int current;
public DefaultSortCurrentEvent(int current) {
this.current = current;
}
@Override
public int getCurrent() {
return current;
}
}
public class DefaultSortTraverseEvent implements SortTraverseEvent {
private int traverse;
public DefaultSortTraverseEvent(int traverse) {
this.traverse = traverse;
}
@Override
public int getTraverse() {
return traverse;
}
}
public class DefaultSortSwapEvent implements SortSwapEvent {
private int from;
private int to;
public DefaultSortSwapEvent(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public int from() {
return from;
}
@Override
public int to() {
return to;
}
}
public class DefaultSorterModel implements SortModel {
private int[] unsortedValues;
private int[] sortedValues;
private List<SortEvent> events;
public DefaultSorterModel(int[] unsortedValues, int[] sortedValues, List<SortEvent> events) {
this.unsortedValues = unsortedValues;
this.sortedValues = sortedValues;
this.events = events;
}
@Override
public int[] getUnsortedValues() {
return unsortedValues;
}
@Override
public int[] getSortedValues() {
return sortedValues;
}
@Override
public List<SortEvent> getEvents() {
return events;
}
}
public interface Sorter {
public SortModel sort(int[] values);
}
public abstract class AbstractSorter implements Sorter {
protected void swap(int[] values, int indexOne, int indexTwo) {
int temp = values[indexOne];
values[indexOne] = values[indexTwo];
values[indexTwo] = temp;
}
}
public class SortWorker extends SwingWorker<SortModel, Void> {
public interface Observer {
public void stateDidChange(SortWorker worker, SwingWorker.StateValue state);
public void didComplete(SortWorker worker, SortModel model);
public void didFail(SortWorker worker, Exception exp);
}
private int[] values;
private Sorter sorter;
private Observer observer;
public SortWorker(int[] values, Sorter sorter, Observer observer) {
this.values = values;
this.sorter = sorter;
this.observer = observer;
addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
observer.stateDidChange(SortWorker.this, getState());
}
});
}
protected int[] getValues() {
return values;
}
@Override
protected SortModel doInBackground() throws Exception {
int[] values = getValues();
int[] initalValues = Arrays.copyOf(values, values.length);
// Shuffle the values first...
ShuffleSorter shuffleSorter = new ShuffleSorter();
SortModel shuffled = shuffleSorter.sort(getValues());
// Get the events from the shuffle, we'll need those later
List<SortEvent> events = shuffled.getEvents();
// Get the "shuffled" values as a new starting point
int[] shuffledValues = shuffled.getSortedValues();
// Sorted the shuffled values...
SortModel sorted = sorter.sort(shuffledValues);
// Append all the sort events...
events.addAll(sorted.getEvents());
// Build a new model...
DefaultSorterModel model = new DefaultSorterModel(initalValues, sorted.getSortedValues(), events);
return model;
}
@Override
protected void done() {
super.done();
try {
SortModel model = get();
observer.didComplete(this, model);
} catch (Exception ex) {
observer.didFail(this, ex);
}
}
}
public class ShuffleSorter extends AbstractSorter {
@Override
public SortModel sort(int[] values) {
Random random = new Random();
int[] sortedValues = Arrays.copyOf(values, values.length);
int[] unsortedValues = Arrays.copyOf(values, values.length);
List<SortEvent> events = new ArrayList<>(values.length);
for (int i = 0; i < sortedValues.length; i++) {
int swap = random.nextInt(sortedValues.length - 1);
events.add(new DefaultSortCurrentEvent(i));
events.add(new DefaultSortTraverseEvent(swap));
events.add(new DefaultSortSwapEvent(i, swap));
swap(sortedValues, i, swap);
}
events.add(new DefaultSortCurrentEvent(values.length - 1));
events.add(new DefaultSortTraverseEvent(values.length - 1));
DefaultSorterModel model = new DefaultSorterModel(unsortedValues, sortedValues, events);
return model;
}
}
public class InsertionSort extends AbstractSorter {
@Override
public SortModel sort(int[] values) {
int[] sortedValues = Arrays.copyOf(values, values.length);
int[] unsortedValues = Arrays.copyOf(values, values.length);
List<SortEvent> events = new ArrayList<>(values.length);
for (int current = 1; current < unsortedValues.length; current++) {
int traverse = current;
events.add(new DefaultSortCurrentEvent(current));
while (traverse > 0 && sortedValues[traverse] < sortedValues[traverse - 1]) {
events.add(new DefaultSortSwapEvent(traverse, traverse - 1));
swap(sortedValues, traverse, traverse - 1);
traverse--;
events.add(new DefaultSortTraverseEvent(traverse));
}
}
events.add(new DefaultSortCurrentEvent(values.length - 1));
events.add(new DefaultSortTraverseEvent(values.length - 1));
DefaultSorterModel model = new DefaultSorterModel(unsortedValues, sortedValues, events);
return model;
}
}
}
@RustyKnight
Copy link
Author

This basically generates a "replayable" event based model, which can be animated - this prevents all the issues with trying to sync threads and trying to keep painting clean and all the other issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment