package interpolation; import java.awt.*; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.geom.GeneralPath; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import javax.swing.*; import javax.swing.event.ChangeListener; import gui.TransferableUtils; import kussmaulUtils.ViewUtils; public class DoubleInterpolator extends GraphInterpolator { private static final long serialVersionUID = -6215153672610067743L; private JSpinner manualSpinner; private static boolean alwaysAutoResize = false; private static int scaleTextInset = 2; private static Color scaleTextColor = new Color(0, 0, 0, 127); private static Color lineColor = new Color(0, 0, 0, 200); private static Color keyframeIndicatorColor = new Color(255, 0, 0, 32); private static Color zeroLineColor = new Color(0, 0, 0, 32); private JSpinner perciseTime; private JSpinner perciseValue; private JButton perciseButton; private double minValue; private double maxValue; private double startingMax; private double startingMin; private double startingVal; private ArrayList<Keyframe<Double>> keyframes; private Keyframe<Double> dragFrame; private JSpinner maxSpinner; private JSpinner minSpinner; private Algebra function; private JTextField equationField; private JLabel infoLabel; public DoubleInterpolator(double startingVal, double minValue, double maxValue, double visMin, double visMax, GetSet gs, ChangeListener... listeners) { super(gs, listeners); this.startingVal = startingVal; this.startingMin = Math.max(visMin, minValue); this.startingMax = Math.min(visMax, maxValue); this.minValue = minValue; this.maxValue = maxValue; keyframes = new ArrayList<Keyframe<Double>>(); keyframes.add(new Keyframe<Double>(0f, startingVal)); keyframes.add(new Keyframe<Double>(1f, startingVal)); initializeComponents(); addActionListeners(); gbc.gridwidth = 2; add(infoLabel, gbc); gbc.gridy++; gbc.gridwidth=1; gbc.weightx = 0; add(new JLabel("f(x) ="), gbc); gbc.weightx=1; gbc.gridx++; add(equationField, gbc); gbc.gridwidth = 2; gbc.gridy++; gbc.gridx=0; JPanel addPanel = new JPanel(new GridLayout(1, 4)); addPanel.add(new JLabel("Add keyframe:")); addPanel.add(perciseTime); addPanel.add(perciseValue); addPanel.add(perciseButton); add(addPanel, gbc); gbc.gridy++; pack(); } private void initializeComponents() { manualSpinner = new JSpinner(new SpinnerNumberModel(startingVal, minValue, maxValue, .01)); perciseTime = new JSpinner(new SpinnerNumberModel(0d, 0d, 1d, .05d)); perciseValue = new JSpinner(new SpinnerNumberModel(0d, minValue, maxValue, .1d)); perciseButton = new JButton("Add"); maxSpinner = new JSpinner(new SpinnerNumberModel(startingMax, minValue+1, maxValue, 1)); ((JSpinner.DefaultEditor) maxSpinner.getEditor()).getTextField().setColumns(3); ((JSpinner.DefaultEditor) maxSpinner.getEditor()).getTextField().setBackground(new Color(255, 255, 255, 127)); minSpinner = new JSpinner(new SpinnerNumberModel(startingMin, minValue, maxValue-1, 1)); ((JSpinner.DefaultEditor) minSpinner.getEditor()).getTextField().setColumns(3); ((JSpinner.DefaultEditor) minSpinner.getEditor()).getTextField().setBackground(new Color(255, 255, 255, 127)); GridBagConstraints gb2 = ViewUtils.createGBC(); gb2.anchor = GridBagConstraints.NORTHWEST; gb2.weighty = 0; graphPanel.setLayout(new GridBagLayout()); graphPanel.add(maxSpinner, gb2); gb2.weighty=1; gb2.gridy++; graphPanel.add(ViewUtils.createDummyComponent(), gb2); gb2.gridy++; gb2.weighty=0; gb2.anchor = GridBagConstraints.SOUTHWEST; graphPanel.add(minSpinner, gb2); function = new Algebra(startingVal+""); equationField = new JTextField(function.getFunction()); infoLabel = new JLabel("Time: -- Value: -- Min: -- Max: --"); createMenuBar(); } @SuppressWarnings("unchecked") private void createMenuBar() { JMenuItem copy = new JMenuItem("Copy"); copy.addActionListener(ae -> TransferableUtils.copyObject(Keyframe.deepCopy(keyframes))); copy.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.META_MASK)); JMenuItem paste = new JMenuItem("Paste"); paste.addActionListener(ae -> { Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this); try { ArrayList<Keyframe<Serializable>> data = (ArrayList<Keyframe<Serializable>>) t.getTransferData(TransferableUtils.objectDataFlavor); if(data.isEmpty()) return; //Yes this is ugly. There is really no better way of doing it though. Forgive me if(data.get(0).getValue() instanceof Double) keyframes = (ArrayList<Keyframe<Double>>) (Object) data; if(data.get(0).getValue() instanceof Float) keyframes = Keyframe.floatToDouble((ArrayList<Keyframe<Float>>) (Object) data); if(data.get(0).getValue() instanceof Integer) keyframes = Keyframe.intToDouble((ArrayList<Keyframe<Integer>>) (Object) data); refresh(false); } catch (UnsupportedFlavorException | IOException e1) { System.err.println("Invalid data copied"); } }); paste.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.META_MASK)); JMenuBar bar = new JMenuBar(); JMenu edit = new JMenu("Edit"); edit.add(copy); edit.add(paste); bar.add(edit); setJMenuBar(bar); } private void addActionListeners() { manualSpinner.addChangeListener(ce -> { refresh(false); }); perciseButton.addActionListener(ae -> { Keyframe<Double> key = new Keyframe<Double>(((Double) perciseTime.getValue()).floatValue(), (Double) perciseValue.getValue()); for(int i = 0; i < keyframes.size(); i++) if(Math.abs(keyframes.get(i).getTime()-key.getTime()) < .05f) { keyframes.remove(i); i--; } keyframes.add(key); Collections.sort(keyframes); refresh(false); }); equationField.addActionListener(ae -> { if(function.setFunction(equationField.getText())) { clear(0, 1); equationField.setBackground(new Color(255, 255, 255, 255)); for(float f = 0; f <= 1; f+=.02f) keyframes.add(new Keyframe<Double>(f, Math.min(maxValue, function.evalF(f)))); keyframes.add(new Keyframe<Double>(1f, Math.min(maxValue, function.evalF(1f)))); for (int i = 1; i < keyframes.size()-1; i++) { if(keyframes.get(i-1).getValue().equals(keyframes.get(i).getValue()) && keyframes.get(i).getValue().equals(keyframes.get(i+1).getValue())) { keyframes.remove(i); i--; } } refresh(true); } else { equationField.setBackground(new Color(255, 127, 127)); } }); minSpinner.addChangeListener(ce -> { repaint(); }); maxSpinner.addChangeListener(ce -> { repaint(); }); } @Override protected boolean restrictRange() { return false; } @Override protected void graphMoved(float x, float y, boolean rightClick) { refreshInfo(x, y); } @Override protected void graphDragged(float x, float y, boolean rightClick) { if(dragFrame != null) clear(dragFrame.getTime(), x); dragFrame = new Keyframe<Double>(x, getValFromPos(y)); refresh(false); refreshInfo(x, y); fireChangeEvent(); } @Override protected void graphReleased(float x, float y, boolean rightClick) { if(dragFrame == null) dragFrame = new Keyframe<Double>(x, getValFromPos(y)); keyframes.add(dragFrame); Collections.sort(keyframes); if(keyframes.get(keyframes.size()-1).getTime() != 1f) keyframes.add(new Keyframe<Double>(1f, dragFrame.getValue())); if(keyframes.get(0).getTime() != 0f) keyframes.add(new Keyframe<Double>(0f, dragFrame.getValue())); dragFrame = null; refresh(true); refreshInfo(x, y); fireChangeEvent(); } @Override protected void graphPressed(float x, float y, boolean rightClick) { if(dragFrame != null) clear(dragFrame.getTime(), x); dragFrame = new Keyframe<Double>(x, getValFromPos(y)); refresh(true); refreshInfo(x, y); } public void refreshInfo(float x, float y) { double value = dragFrame == null ? (double) getAnimationValue(x) : getValFromPos(y); infoLabel.setText(String.format(java.util.Locale.US, "Time: %.2f Value: %.2f Min: %.2f Max: %.2f", x, value, getMin(), getMax())); } public void refresh(boolean resize) { double max = getMax(); if(resize && (alwaysAutoResize || max >= (Double) maxSpinner.getValue())) maxSpinner.setValue(Math.min(maxValue, max > 0 ? max * 1.25: max)); ((SpinnerNumberModel) maxSpinner.getModel()).setMinimum(max); double min = getMin(); if(resize && (alwaysAutoResize || min <= (Double) minSpinner.getValue())) minSpinner.setValue(Math.max(minValue, min)); ((SpinnerNumberModel) minSpinner.getModel()).setMaximum(min); repaint(); fireChangeEvent(); } @Override public boolean isKeyframable() { return true; } @Override protected Object getAnimationValue(float time) { if(keyframes.size() == 0) if(dragFrame != null) return dragFrame.getValue(); else return 0; if(time <= keyframes.get(0).getTime()) return keyframes.get(0).getValue(); if(time >= keyframes.get(keyframes.size()-1).getTime() || keyframes.size() == 1) return keyframes.get(keyframes.size()-1).getValue(); for(int i = 0; i < keyframes.size()-1; i++) { if(keyframes.get(i).getTime() <= time && keyframes.get(i + 1).getTime() > time) { float distTo0 = time - keyframes.get(i).getTime(); float distTo1 = keyframes.get(i + 1).getTime() - time; float sum = distTo0 + distTo1; return (distTo0/sum) * keyframes.get(i+1).getValue() + (distTo1/sum) * keyframes.get(i).getValue(); } } return 0d; } @Override protected Object getStaticValue() { return ((Double) manualSpinner.getValue()); } @Override public String getInstructions() { return "This variable is an decimal number. Simply draw a graph of how it should change through the animation, or enter a function to start. You can adust the display range using the input in the right corners of the graph."; } @Override public JComponent getManualController() { return manualSpinner; } private double getValFromPos(float y) { double max = (Double) maxSpinner.getValue(); double min = (Double) minSpinner.getValue(); double range = max - min; return Math.min(maxValue, Math.max(minValue, (y * range)+min)); } private double getMax() { double max = dragFrame == null ? Double.MIN_VALUE : dragFrame.getValue(); for(int i = 0; i < keyframes.size(); i++) if(keyframes.get(i).getValue() > max) max = keyframes.get(i).getValue(); return max; } private double getMin() { double min = dragFrame == null ? Double.MAX_VALUE : dragFrame.getValue(); for(int i = 0; i < keyframes.size(); i++) if(keyframes.get(i).getValue() < min) min = keyframes.get(i).getValue(); return min; } private void clear(float t0, float t1) { Collections.sort(keyframes); for(int i = 0; i < keyframes.size(); i++) { float t = keyframes.get(i).getTime(); if((t >= t0 && t <= t1) || (t >= t1 && t <= t0)) { keyframes.remove(i); i--; } } } @Override public Dimension getGraphSize() { return new Dimension(500, 300); } @Override public void paintGraph(Graphics2D g, int width, int height) { FontMetrics fm = g.getFontMetrics(); double min = (Double) minSpinner.getValue(); double max = (Double) maxSpinner.getValue(); double range = max - min; g.setColor(scaleTextColor); g.drawString(String.format("%.1f", range/2d+min), scaleTextInset, (height+fm.getHeight())/2 - fm.getDescent()); g.setColor(lineColor); g.setStroke(new BasicStroke(3f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND)); ArrayList<Keyframe<Double>> copy = new ArrayList<>(keyframes); if(dragFrame != null) copy.add(dragFrame); Collections.sort(copy); if(copy.get(0).getTime() != 0f) copy.add(0, new Keyframe<Double>(0f, copy.get(0).getValue())); if(copy.get(copy.size()-1).getTime() != 1f) copy.add(new Keyframe<Double>(1f, copy.get(copy.size()-1).getValue())); GeneralPath path = new GeneralPath(); path.moveTo(copy.get(0).getTime() * width, Math.round(((1f - (copy.get(0).getValue()-min)/range) * height))); for(int i = 1; i < copy.size(); i++) path.lineTo(copy.get(i).getTime() * width, Math.round((1f - (copy.get(i).getValue()-min)/range) * height)-.5f); g.draw(path); g.setStroke(new BasicStroke(3)); //Draw red bar at each keyframe g.setColor(keyframeIndicatorColor); for(int i = 1; i < copy.size()-1; i++) g.drawLine((int) (copy.get(i).getTime() * width), 0, (int) (copy.get(i).getTime() * width), height); if(min < 0 && max > 0) { g.setColor(zeroLineColor); g.drawLine(0, (int) ((1-(-min/range))*height), width, (int) ((1-(-min/range))*height)); } } @Override public void paintButton(Graphics2D g, int width, int height) { g.setColor(Color.black); g.setStroke(new BasicStroke(1)); double min = getMin(); double max = getMax(); double range = max - min; GeneralPath path = new GeneralPath(); path.moveTo(keyframes.get(0).getTime() * width, (1f - (keyframes.get(0).getValue()-min)/range) * height); for(int i = 1; i < keyframes.size(); i++) path.lineTo(keyframes.get(i).getTime() * width, ((1f - (keyframes.get(i).getValue()-min)/range) * height-.5f)); g.draw(path); } // // public static void main(String[] args) { // DoubleInterpolator di = new DoubleInterpolator(0, -20, 20, -25, 25, null); // di.setVisible(true); // di.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // } }