I'm new to JUNG
I have a FRLayout that represents a network topology with key nodes or vertices that are color as red and the other vertices blue
The edges from the starting node to the end node are blue
I want to be to demonstrate an animation of the path to the end node.
How can animate drawing the edges from a start_node to end_node with a specified time interval? Can you provide or reference an example?
You could attach a keyframe to your Edge data. Then, every time you draw (using a transformer) you can use the keyframe to adjust the gradient of the edge:
RenderContext<V, E> context = vv.getRenderContext();
context.setEdgeDrawPaintTransformer(new KeyframeGradientTransformer());
public class KeyframeGradientTransformer() implements Transformer<E, Paint> {
#Override
public Paint transform(Edge edge) {
// TODO: Here you would determine the gradient information
// based on the edge.getKeyframe().
Paint gradient = new GradientPaint(...);
return gradient;
}
}
EDIT:
I wrote up a quick example:
This animates from one vertex to another (along one edge). If you want to animate going through multiple vertices, that will require more logic. However, this looks pretty cool and should give you a start. If you (or anyone else) need more comments, just let me know and I can try and make it more clear.
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.LinearGradientPaint;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.apache.commons.collections15.Factory;
import org.apache.commons.collections15.Transformer;
import edu.uci.ics.jung.algorithms.generators.random.EppsteinPowerLawGenerator;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.algorithms.layout.SpringLayout;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.SparseMultigraph;
import edu.uci.ics.jung.graph.util.Pair;
import edu.uci.ics.jung.visualization.VisualizationViewer;
public class Test {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.setPreferredSize(new Dimension(1024, 768));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel content = new JPanel();
// Set up the graph and the display.
int numV = 70;
int numE = 50;
EppsteinPowerLawGenerator<String, String> gen = new EppsteinPowerLawGenerator<String, String>(
new GraphFactory(), new CountFactory(),
new CountFactory(), numV, numE, 10);
Graph<String, String> graph = gen.create();
Layout<String, String> layout = new SpringLayout<String, String>(
graph);
VisualizationViewer<String, String> vv = new VisualizationViewer<String, String>(
layout);
vv.getRenderContext().setEdgeStrokeTransformer(
new Transformer<String, Stroke>() {
#Override
public Stroke transform(String edge) {
return new BasicStroke(1.5f);
}
});
content.add(vv);
frame.setContentPane(content);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// Animate the edges!
AnimationTimerTask at = new AnimationTimerTask(vv);
Timer timer = new Timer();
timer.scheduleAtFixedRate(at, 10, 30);
}
});
}
static class AnimationTimerTask extends TimerTask {
private final double width = 0.1; // Size of the colored line.
private final double stepsize = 0.01;
private double keyframe = 0 + width; // Between 0.0 and 1.0
private VisualizationViewer<String, String> vv = null;
public AnimationTimerTask(VisualizationViewer<String, String> vv) {
this.vv = vv;
}
#Override
public void run() {
vv.getRenderContext().setEdgeDrawPaintTransformer(
new Transformer<String, Paint>() {
#Override
public Paint transform(String edge) {
// Find both points of the edge.
Pair<String> vs = vv.getGraphLayout().getGraph()
.getEndpoints(edge);
Point2D p1 = vv.getGraphLayout().transform(
vs.getFirst());
Point2D p2 = vv.getGraphLayout().transform(
vs.getSecond());
// This code won't handle self-edges.
if (p1.equals(p2)) {
return Color.red;
}
Color[] colors = { Color.gray, Color.red,
Color.gray };
float start = (float) Math.max(0.0, keyframe
- width);
float end = (float) Math.min(1.0, keyframe + width);
float[] fractions = { start, (float) keyframe, end };
return new LinearGradientPaint(p1, p2, fractions,
colors);
}
});
vv.repaint();
keyframe += stepsize;
keyframe %= 1.0;
}
}
static class GraphFactory implements Factory<Graph<String, String>> {
#Override
public Graph<String, String> create() {
return new SparseMultigraph<String, String>();
}
}
static class CountFactory implements Factory<String> {
private int count = 0;
#Override
public String create() {
return String.valueOf(count++);
}
}
}
Also, I've taken a bit of heat for this before so: this requires the JUNG library. If you don't have it, you can't run the SSCCEE.
Related
I have a code that has a button. When a button is pressed, circles appear at random positions with random colors. There can only be 10 circles.
Now that I added random colors functionality, the problem is that after each circle is drawn, its color starts changing infinetely.
How can I make it so the colors don't change?
class Panel extends JPanel {
private JButton button;
private Ellipse2D.Double[] circles;
Integer count;
public Panel() {
setup();
}
private void setup() {
count=new Integer(0);
circles=new Ellipse2D.Double[10];
button=new JButton(count.toString());
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
Random r=new Random();
//position circles with diameter 100 in a way
//that it would fit in a window's size
int highX=getWidth()-100;
int highY=getHeight()-100;
circles[count]=new
Ellipse2D.Double(r.nextInt(highX),
r.nextInt(highY), 100, 100);
count++;
button.setText(count.toString());
}
});
add(button);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
paintStuff(g);
repaint();
}
private void paintStuff(Graphics g) {
Graphics2D g2=(Graphics2D) g;
g2.setPaint(Color.RED);
if (count!=0) {
for (int i=0; i<count; i++) {
g2.draw(circles[i]);
Random r=new Random();
int red=r.nextInt(256);
int green=r.nextInt(256);
int blue=r.nextInt(256);
g2.setPaint(new Color(red, green, blue));
g2.fill(circles[i]);
}
}
}
}
public class Frame extends JFrame {
private Panel panel;
public Frame() {
panel=new Panel();
add(panel);
}
public static void main(String[] args) {
Frame frame=new Frame();
}
}
Never call repaint within a painting method as that causes a "poor-man's" animation to occur. Instead call it in your JButton's ActionListener. Also, don't randomize within the painting method, but rather do this within the ActionListener. The painting method is not under your control, and you don't want to use it to change your object's state, but rather only to display it.
Other suggestions:
Your code still needs to set the JFrame's setDefaultCloseOperation
and still needs to set the JFrame visible
You never suggest sizing in the code. Myself, I recommend overriding public Dimension getPreferredSize() of your JPanel and call pack() on the JFrame after adding the JPanel but before displaying it.
I'd rename your classes so that the names don't clash with core Java classes and cause confusion to your instructors, us, or your future self.
Don't keep re-creating a new Random object within the for loop. Rather why not simply give the class a Random field, create it once, but reuse the object repeatedly.
You will want to associate a color with your shape/Ellipse2D. For a one-to-one correspondence, consider using a Map such as a HashMap<Shape, Color>.
For example:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import javax.swing.*;
#SuppressWarnings("serial")
public class Panel2 extends JPanel {
// preferred size constants
private static final int PREF_W = 600;
private static final int PREF_H = PREF_W;
// map to hold circles and colors
private Map<Shape, Color> shapeColorMap = new LinkedHashMap<>();
public Panel2() {
add(new JButton(new RandomColorAction()));
}
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
return new Dimension(PREF_W, PREF_H);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
// create *smooth* drawings
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
paintStuff(g2);
}
private void paintStuff(Graphics2D g2) {
// iterate through our map extracting all circles and colors
// and drawing them
for (Entry<Shape, Color> entry : shapeColorMap.entrySet()) {
Shape shape = entry.getKey();
Color color = entry.getValue();
g2.setColor(color);
g2.fill(shape);
}
}
// listener for our button
private class RandomColorAction extends AbstractAction {
private static final int CIRC_WIDTH = 100;
private Random random = new Random();
private int count = 0;
public RandomColorAction() {
super("Random Circle: 0");
putValue(MNEMONIC_KEY, KeyEvent.VK_R);
}
#Override
public void actionPerformed(ActionEvent e) {
// create our random ellipses
int x = random.nextInt(getWidth() - CIRC_WIDTH);
int y = random.nextInt(getHeight() - CIRC_WIDTH);
Shape shape = new Ellipse2D.Double(x, y, CIRC_WIDTH, CIRC_WIDTH);
// create our random color using HSB for brighter colors
float hue = random.nextFloat();
float saturation = (float) (0.8 + random.nextFloat() * 0.2);
float brightness = (float) (0.8 + random.nextFloat() * 0.2);
Color color = Color.getHSBColor(hue, saturation, brightness);
shapeColorMap.put(shape, color);
// increment count, place items into map, repaint
count++;
putValue(NAME, "Random Circle: " + count);
repaint();
}
}
private static void createAndShowGui() {
Panel2 mainPanel = new Panel2();
JFrame frame = new JFrame("Panel2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}
In comments, Camickr astutely points out:
A painting method should paint the current state of the component. By using the HashMap you are introducing the possibility of randomness. The order of iteration through the map can't be guaranteed. Therefore as new entries are added to the map the order each Shape is painted could change. Generally not a problem, but if two random shapes ever overlap, the result good be flip flopping which shape is painted on top of one another.
And of course, he is absolutely correct, since there is no guaranteed order for a HashMap. Fortunately the variable itself was declared to be of Map type, and so to preserve order all one needs to do is to change the actual object type from HashMap to that of LinkedHashMap, a class which per its API:
This implementation spares its clients from the unspecified, generally chaotic ordering provided by HashMap (and Hashtable), without incurring the increased cost associated with TreeMap.
So for TLDR, change this:
private Map<Shape, Color> shapeColorMap = new HashMap<>();
to this:
private Map<Shape, Color> shapeColorMap = new LinkedHashMap<>();
Edited to fix the color calculation.
A just for the fun of it version that introduces Path2D and AffineTransform with a MouseListener/MouseMotionListener to allow for dragging the circles:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import javax.swing.*;
#SuppressWarnings("serial")
public class Panel2 extends JPanel {
// preferred size constants
private static final int PREF_W = 600;
private static final int PREF_H = PREF_W;
// map to hold circles and colors
private Map<Shape, Color> shapeColorMap = new LinkedHashMap<>();
public Panel2() {
add(new JButton(new RandomColorAction()));
MyMouse myMouse = new MyMouse();
addMouseListener(myMouse);
addMouseMotionListener(myMouse);
}
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
return new Dimension(PREF_W, PREF_H);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
// create *smooth* drawings
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
paintStuff(g2);
}
private void paintStuff(Graphics2D g2) {
// iterate through our map extracting all circles and colors
// and drawing them
for (Entry<Shape, Color> entry : shapeColorMap.entrySet()) {
Shape shape = entry.getKey();
Color color = entry.getValue();
g2.setColor(color);
g2.fill(shape);
}
}
private class MyMouse extends MouseAdapter {
private Entry<Shape, Color> selected = null;
private Path2D path;
private Point p = null;
#Override
public void mousePressed(MouseEvent e) {
Set<Entry<Shape, Color>> entrySet = shapeColorMap.entrySet();
// get Shape pressed
for (Entry<Shape, Color> entry : entrySet) {
if (entry.getKey().contains(e.getPoint())) {
selected = entry;
}
}
if (selected != null) {
path = new Path2D.Double(selected.getKey());
// move it to the top
entrySet.remove(selected);
shapeColorMap.put(path, selected.getValue());
p = e.getPoint();
repaint();
}
}
#Override
public void mouseReleased(MouseEvent e) {
if (selected != null) {
moveSelected(e);
}
selected = null;
}
#Override
public void mouseDragged(MouseEvent e) {
if (selected != null) {
moveSelected(e);
}
}
private void moveSelected(MouseEvent e) {
int x = e.getX() - p.x;
int y = e.getY() - p.y;
p = e.getPoint();
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
path.transform(at);
repaint();
}
}
// listener for our button
private class RandomColorAction extends AbstractAction {
private static final int CIRC_WIDTH = 100;
private Random random = new Random();
private int count = 0;
public RandomColorAction() {
super("Random Circle: 0");
putValue(MNEMONIC_KEY, KeyEvent.VK_R);
}
#Override
public void actionPerformed(ActionEvent e) {
// create our random ellipses
int x = random.nextInt(getWidth() - CIRC_WIDTH);
int y = random.nextInt(getHeight() - CIRC_WIDTH);
Shape shape = new Ellipse2D.Double(x, y, CIRC_WIDTH, CIRC_WIDTH);
// create our random color using HSB for brighter colors
float hue = random.nextFloat();
float saturation = (float) (0.8 + random.nextFloat() * 0.2);
float brightness = (float) (0.8 + random.nextFloat() * 0.2);
Color color = Color.getHSBColor(hue, saturation, brightness);
shapeColorMap.put(shape, color);
// increment count, place items into map, repaint
count++;
putValue(NAME, "Random Circle: " + count);
repaint();
}
}
private static void createAndShowGui() {
Panel2 mainPanel = new Panel2();
JFrame frame = new JFrame("Panel2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
}
paintStuff(Graphics g);
Is called many times, and each time it refreshes the circle color. That's the wrong place to set the color, you need to set it when you add the circle.
Create a java.awt.Color array as a global variable
private Color[] circlesColors;
Then just fill this array in the actionPerformed(...) method. This is the setupmethod with the changes
private void setup() {
count=new Integer(0);
circles=new Ellipse2D.Double[10];
circlesColors = new Color[10]; //Init the colors array to the same size of circles array
button=new JButton(count.toString());
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
Random r=new Random();
int highX=getWidth()-100;
int highY=getHeight()-100;
circles[count]=new Ellipse2D.Double(r.nextInt(highX), r.nextInt(highY), 100, 100);
circlesColors[count] = new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256)); //Assign random color
count++;
button.setText(count.toString());
}
});
add(button);
}
Then in your paint(...) method
private void paintStuff(Graphics g) {
Graphics2D g2=(Graphics2D) g;
g2.setPaint(Color.RED);
if (count!=0) {
for (int i=0; i<count; i++) {
g2.draw(circles[i]);
g2.setPaint(circlesColors[i]); //Get and set the color associated to the circle
g2.fill(circles[i]);
}
}
}
Currently as part of a project I'm working on, I am implementing a component which can be used to visualize a permutation of bits (as part of a cryptographic algorithm). I am doing so by creating two rows of "pins" and connecting them by drawing lines between the tips, creating a sort of web between them.
An important part of this is that I am using this visualization both on its own as well as a part of other visualizations (for example, I may want to include S-Boxes) and therefore I need to be able to turn the pins on and off. My solution to this was using JPanels to put the rows of pins into a header and footer panel which can be made invisible.
I am laying them out vertically in a BoxLayout, but I end up with space between them, even if I add glue above the header and below the footer.
My example looks like this when initialized:
And when I resize it, they come together a bit, but still only touch on one side:
I'm guessing this is some sort of silly mistake converting my user space into device space in terms of component size and layout, but for the life of me I cannot find it. This is my code, although I apologize for the mess:
import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Ellipse2D;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class PermutationWeb extends JPanel
{
private static enum EndPanelType
{
HEADER, FOOTER
}
private final JPanel header;
private final JPanel mainPanel;
private final JPanel footer;
private double widthFactor;
private double heightFactor;
private int widthMax;
private int heightMax;
private int[] indexMappings;
private Point2D.Double[] endpoints;
private Line2D.Double[] drawingLines;
public PermutationWeb(int indices, boolean endPanelsOn)
{
super();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
widthMax = (indices + 1)*2;
heightMax = (int)Math.round(widthMax*(3.0/17.0));
widthFactor = 1;
heightFactor = 1;
endpoints = new Point2D.Double[indices * 2];
drawingLines = new Line2D.Double[indices];
for(int i=0; i<indices; i++)
{
endpoints[i] = new Point2D.Double(i*2+2, 0);
endpoints[i+indices] = new Point2D.Double(i*2+2, heightMax);
drawingLines[i] = new Line2D.Double();
}
header = new WebEndPanel(EndPanelType.HEADER);
mainPanel = new WebMainPanel();
footer = new WebEndPanel(EndPanelType.FOOTER);
add(Box.createVerticalGlue());
add(header);
add(mainPanel);
add(footer);
add(Box.createVerticalGlue());
setEndPanelsOn(endPanelsOn);
}
public Point2D getEndpoint(int index)
{
return endpoints[index];
}
public void updateMappings(int[] mappings)
{
this.indexMappings = mappings;
for(int i=0; i<indexMappings.length; i++)
{
drawingLines[i].setLine(endpoints[i], endpoints[indexMappings.length + indexMappings[i]]);
}
//paint();
}
public void setEndPanelsOn(boolean endPanelsOn)
{
header.setVisible(endPanelsOn);
footer.setVisible(endPanelsOn);
}
#Override
public Dimension getMaximumSize()
{
int height = mainPanel.getHeight();
if(header.isVisible())
{
height += (header.getHeight() * 2);
}
int width = mainPanel.getWidth();
return new Dimension(width, height);
}
#Override
public Dimension getPreferredSize()
{
return getMaximumSize();
}
public static void main(String[] args)
{
JFrame jf = new JFrame();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setSize(800, 600);
int[] mappings = {0,4,8,12,1,5,9,13,2,6,10,14,3,7,11,15};
PermutationWeb webTest = new PermutationWeb(16, true);
jf.add(webTest);
jf.setVisible(true);
webTest.setVisible(true);
webTest.updateMappings(mappings);
System.out.printf("Header: [%s]\nMainPanel: [%s]\nFooter: [%s]\n",
webTest.header.getSize().toString(),
webTest.mainPanel.getSize().toString(),
webTest.footer.getSize().toString());
}
private class WebMainPanel extends WebSubPanel
{
private static final double HEIGHT_RATIO = 0.25;
#Override
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
super.paintComponent(g2);
scaleTo(getSize());
g2.scale(widthFactor, widthFactor);
g2.setStroke(new BasicStroke((float)(2.0/widthFactor)));
for(Line2D line: drawingLines)
{
g2.draw(line);
}
}
#Override
public Dimension getMaximumSize()
{
return new Dimension(MAX_WIDTH_PX, (int)(MAX_WIDTH_PX*HEIGHT_RATIO));
}
}
private class WebEndPanel extends WebSubPanel
{
private static final double HEIGHT_RATIO = 0.125;
private static final double PIN_RADIUS = 0.5;
private final EndPanelType endType;
private Line2D.Double[] edgeLines;
private Ellipse2D.Double[] pinHeads;
public WebEndPanel(EndPanelType endType)
{
super();
this.endType = endType;
this.edgeLines = new Line2D.Double[endpoints.length/2];
this.pinHeads = new Ellipse2D.Double[endpoints.length/2];
for(int i=0; i<edgeLines.length; i++)
{
Point2D pointA;
Point2D pointB;
if(EndPanelType.HEADER.equals(this.endType))
{
pointA = new Point2D.Double(i*2+2, 4);
pointB = new Point2D.Double(i*2+2, 2);
pinHeads[i] = new Ellipse2D.Double(
pointB.getX()-PIN_RADIUS,
pointB.getY()-PIN_RADIUS*2,
PIN_RADIUS*2,
PIN_RADIUS*2);
}
else // FOOTER
{
pointA = new Point2D.Double(i*2+2, 0);
pointB = new Point2D.Double(i*2+2, 2);
pinHeads[i] = new Ellipse2D.Double(
pointB.getX()-PIN_RADIUS,
3-PIN_RADIUS*2,
PIN_RADIUS*2,
PIN_RADIUS*2);
}
edgeLines[i] = new Line2D.Double(pointA, pointB);
}
}
#Override
public Dimension getMaximumSize()
{
return new Dimension(MAX_WIDTH_PX, (int)(MAX_WIDTH_PX*HEIGHT_RATIO));
}
#Override
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
super.paintComponent(g2);
scaleTo(getSize());
g2.scale(widthFactor, widthFactor);
g2.setStroke(new BasicStroke((float)(2.0/widthFactor)));
for(Line2D line: edgeLines)
{
g2.draw(line);
}
for(Ellipse2D pin: pinHeads)
{
g2.draw(pin);
}
}
}
private abstract class WebSubPanel extends JPanel
{
protected static final int MAX_WIDTH_PX = 800;
public WebSubPanel()
{
super();
setBorder(null);
setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
}
public void scaleTo(Dimension d)
{
widthFactor = d.getWidth() / (double)widthMax;
heightFactor = d.getHeight() / (double)heightMax;
}
#Override
public Dimension getPreferredSize()
{
return getMaximumSize();
}
}
}
The ultimate goal here is a resizable web where the header and footer WebEndPanels can be invisible, but have 0 space between them and the WebMainPanel when shown (as though they were a single entity).
A BoxLayout will resize a component up to its maximum size if space is available.
So you first need to implement the getPreferredSize() method of the component so it can be packed as displayed at its normal size.
Then if it has the ability to grow (and your custom painting supports this) you override the getMaximumSize() method to return the size.
So the painting needs to be based on the actual size of the panel if you want it to be contiguous with other panels.
I am trying to design a GUI with triangle shaped buttons. I have create the triangle button class correctly in so far as creating a JButton with my class constructor results in a triangle button on the page, but I fall short when it comes to placement of the button.
Could any direct me or have an example for creating a hexagonal shape from triangle buttons?
Here is my TriangleButton class for reference:
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
class TriangleButton extends JButton {
final static double side_len = 52; //Change for variable triangle size
final static double y_offset = (Math.sqrt(3) * side_len / 2);
private Shape triangle;
public TriangleButton(int spot){
triangle = createTriangle(spot);
}
public void paintBorder( Graphics g ) {
((Graphics2D)g).draw(triangle);
}
public void paintComponent( Graphics g ) {
((Graphics2D)g).fill(triangle);
}
public Dimension getPreferredSize() {
return new Dimension((int)side_len, (int)y_offset);
}
public boolean contains(int x, int y) {
return triangle.contains(x, y);
}
private Shape createTriangle(int spot) {
Polygon p = new Polygon();
p.addPoint( 0 , 0 );
p.addPoint( (int)side_len , 0 );
p.addPoint( (int)side_len/2, (int)(y_offset) );
return p;
}
}
The look I had in mind would be something like..
With space between the buttons.. basically just up-pointing and down-pointing triangles lined up.
But anything to put me in the right direction would be appreciated!
As an alternative, due to the complexities of generating a suitable layout to allow components to overlap, you could simply create a single button which housed all the triangles and which provided centralised control, for example
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractButton;
import javax.swing.DefaultButtonModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
HexagonButton btn = new HexagonButton();
btn.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println(Arrays.toString(btn.getSelectedObjects()));
System.out.println(e.getActionCommand());
}
});
add(btn);
}
}
public class HexagonButton extends AbstractButton {
public static final String TOP_RIGHT_QUAD = "Top.right";
public static final String TOP_QUAD = "Top";
public static final String TOP_LEFT_QUAD = "Top.left";
public static final String BOTTOM_LEFT_QUAD = "Bottom.left";
public static final String BOTTOM_QUAD = "Bottom";
public static final String BOTTOM_RIGHT_QUAD = "Bottom.right";
private Shape top;
private Shape topRight;
private Shape topLeft;
private Shape bottomLeft;
private Shape bottomRight;
private Shape bottom;
private Map<String, Shape> paths;
private String selectedQuad;
public HexagonButton() {
setModel(new DefaultButtonModel());
createPaths();
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
String previousQuad = selectedQuad;
selectedQuad = null;
for (String quad : paths.keySet()) {
Shape shape = paths.get(quad);
if (shape.contains(e.getPoint())) {
getModel().setPressed(true);
getModel().setArmed(true);
selectedQuad = quad;
if (!selectedQuad.equals(previousQuad)) {
fireActionPerformed(new ActionEvent(HexagonButton.this, ActionEvent.ACTION_PERFORMED, selectedQuad));
}
break;
}
}
repaint();
}
#Override
public void mouseReleased(MouseEvent e) {
getModel().setArmed(false);
getModel().setPressed(false);
}
});
}
#Override
public Object[] getSelectedObjects() {
return new Object[]{selectedQuad};
}
#Override
public void invalidate() {
super.invalidate();
createPaths();
}
protected void createPaths() {
topRight = create(0d, -60d);
top = create(-60d, -120d);
topLeft = create(-120d, -180d);
bottomLeft = create(-180d, -240d);
bottom = create(-240d, -300d);
bottomRight = create(-300d, -360d);
paths = new HashMap<>(6);
paths.put(TOP_RIGHT_QUAD, topRight);
paths.put(TOP_QUAD, top);
paths.put(TOP_LEFT_QUAD, topLeft);
paths.put(BOTTOM_LEFT_QUAD, bottomLeft);
paths.put(BOTTOM_QUAD, bottom);
paths.put(BOTTOM_RIGHT_QUAD, bottomRight);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(104, 104);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g); //To change body of generated methods, choose Tools | Templates.
Graphics2D g2d = (Graphics2D) g.create();
if (selectedQuad != null) {
Shape path = paths.get(selectedQuad);
g2d.setColor(UIManager.getColor("List.selectionBackground"));
g2d.fill(path);
}
g2d.setColor(getForeground());
g2d.draw(topRight);
g2d.draw(top);
g2d.draw(topLeft);
g2d.draw(bottomLeft);
g2d.draw(bottom);
g2d.draw(bottomRight);
g2d.dispose();
}
public Shape create(double startAngle, double endAngle) {
double width = getWidth();
double height = getHeight();
double radius = Math.min(width, height) / 2;
double xOffset = width - radius;
double yOffset = height - radius;
double startX = xOffset + radius * (Math.cos(Math.toRadians(startAngle)));
double startY = yOffset + radius * (Math.sin(Math.toRadians(startAngle)));
double endX = xOffset + radius * (Math.cos(Math.toRadians(endAngle)));
double endY = yOffset + radius * (Math.sin(Math.toRadians(endAngle)));
Path2D path = new Path2D.Double();
path.moveTo(xOffset, yOffset);
path.lineTo(startX, startY);
path.lineTo(endX, endY);
path.closePath();
return path;
}
}
public static class TriangleButton extends JButton {
final static double side_len = 52; //Change for variable triangle size
final static double y_offset = (Math.sqrt(3) * side_len / 2);
private Shape triangle;
public TriangleButton(int spot) {
triangle = createTriangle(spot);
}
#Override
public void paintBorder(Graphics g) {
super.paintBorder(g);
((Graphics2D) g).draw(triangle);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
((Graphics2D) g).fill(triangle);
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int) side_len, (int) y_offset);
}
#Override
public boolean contains(int x, int y) {
return triangle.contains(x, y);
}
private Shape createTriangle(int spot) {
Polygon p = new Polygon();
p.addPoint(0, 0);
p.addPoint((int) side_len, 0);
p.addPoint((int) side_len / 2, (int) (y_offset));
return p;
}
}
}
Using your class I made some changes and came up with the following:
import java.awt.*;
import java.awt.Shape;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.*;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class TriangleButton2 extends JButton {
final static double side_len = 52; //Change for variable triangle size
final static double y_offset = (Math.sqrt(3) * side_len / 2);
private Shape triangle;
public TriangleButton2(int degrees){
triangle = createTriangle(degrees);
setRolloverEnabled( false );
setContentAreaFilled( false );
setBorderPainted( false );
}
public void paintBorder( Graphics g ) {
((Graphics2D)g).draw(triangle);
}
public void paintComponent( Graphics g ) {
super.paintComponent(g);
((Graphics2D)g).fill(triangle);
}
public Dimension getPreferredSize() {
return new Dimension((int)side_len, (int)y_offset);
}
public boolean contains(int x, int y) {
return triangle.contains(x, y);
}
private Shape createTriangle(int degrees) {
Polygon p = new Polygon();
p.addPoint( 0 , 0 );
p.addPoint( (int)side_len , 0 );
p.addPoint( (int)side_len/2, (int)(y_offset) );
return ShapeUtils.rotate(p, degrees);
// return p;
}
private static void createAndShowGUI()
{
JPanel panelNorth = new JPanel( new FlowLayout(FlowLayout.CENTER, -22, 2) );
panelNorth.add( new TriangleButton2(180) );
panelNorth.add( new TriangleButton2(0) );
panelNorth.add( new TriangleButton2(180) );
JPanel panelSouth = new JPanel( new FlowLayout(FlowLayout.CENTER, -22, 1) );
panelSouth.add( new TriangleButton2(0) );
panelSouth.add( new TriangleButton2(180) );
panelSouth.add( new TriangleButton2(0) );
JFrame frame = new JFrame("SSCCE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panelNorth, BorderLayout.NORTH);
frame.add(panelSouth, BorderLayout.SOUTH);
frame.setLocationByPlatform( true );
frame.pack();
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
}
}
The above code uses the ShapeUtils class found in Playing With Shapes.
Not sure of the exact functionality you want from the button. Your current implantation doesn't have any visual effects when you click on the button or mouse over the button.
In this case you might want to consider just creating an Icon to represent your triangle. you can use the ShapeIcon class found in Playing With Shapes to create your triangle icons. Then you can use the ShapeComponent class also found in Playing With Shapes to create an actual component that you add to the panel.
The FlowLayout shows how you can overlap the buttons to get your desired layout effect.
Check out this page for some relatable information:
Creating custom JButton from images containing transparent pixels
It just might be easier creating JButtons from triangle images.
I'm working on a project where i need
to make circles and relate between them with cubicCurve
as shown in this image:
And i need to pass mouseclick events through the part of the cubicCurve colored with "ALICEBLUE", but still being able to pass mouseclick events to the Line colored with "BLACK".
cubicCurve Code:
public class transLine extends CubicCurve {
private Polygon polygon;
private Circle to;
private Circle from;
private double arrowHeadTransLength;
public Circle getTo() {
return to;
}
public void setTo(Circle to) {
this.to = to;
}
public Circle getFrom() {
return from;
}
public void setFrom(Circle from) {
this.from = from;
}
public transLine() {
setStroke(Color.BLACK);
setFill(Color.ALICEBLUE);
// doesn't work because i can't pass click event to the black line
setMouseTransparent(true);
// just arrow head part
polygon = new Polygon();
fixPolygon();
}
public void fixPolygon(){
Point2D A = new Point2D(getEndX(),getEndY());
Point2D Ap = new Point2D(getControlX2(),getControlY2());
Point2D Ms = getMSecond(A,Ap,arrowHeadTransLength);
Point2D P1 = getRotatePoint(Ms, A, Math.PI/7);
Point2D P2 = getRotatePoint(Ms, A, -Math.PI/7);
getPolygon().getPoints().setAll(new Double[]
{A.getX(),A.getY(),P1.getX(),P1.getY(),P2.getX(),P2.getY()});
}
public Point2D getMSecond(Point2D A, Point2D B, double r){
if( A.getX()==B.getX() && A.getY()==B.getY() ){
return B;
}
double a = (A.getY()-B.getY())/(A.getX()-B.getX());
double b = A.getY()-a*A.getX();
double xs = -r*(A.getX()-B.getX())/A.distance(B)+A.getX();
double ys = a*xs+b;
return new Point2D(xs,ys);
}
public Point2D getRotatePoint(Point2D P, Point2D O, double theta){
double rx = Math.cos(theta)*(P.getX()-O.getX()) - Math.sin(theta)*
(P.getY()-O.getY()) + O.getX();
double ry = Math.sin(theta)*(P.getX()-O.getX()) + Math.cos(theta)*
(P.getY()-O.getY()) + O.getY();
return new Point2D(rx,ry);
}
}
I can't see a particularly easy way to do this. I think that in order to have different mouse behaviors for the fill and the curve, you need those to be different nodes. So the idea is to create the curve and the fill as separate pieces, place them in a Group, and then use the Group in the display. Then make the piece representing the fill mouse-transparent.
Actually implementing that is a bit tricky; particularly getting a cubic curve that doesn't respond to mouse clicks in its "interior". The only way I could find to do this was to use a Path comprising of a MoveTo, a CubicCurveTo, and then another CubicCurveTo that retraced the reverse path (this ensures the interior is essentially empty). Then, as suggested, place that in a group along with a regular CubicCurve representing the filled portion.
SSCCE:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class ConnectingCubicCurve extends Application {
#Override
public void start(Stage primaryStage) {
Circle start = createDraggingCircle(100, 100, 10, Color.CORAL);
Circle end = createDraggingCircle(300, 300, 10, Color.CORAL);
Connection connection = new Connection();
connection.setFromCircle(start);
connection.setToCircle(end);
Pane pane = new Pane(connection.asNode(), start, end);
pane.setOnMouseClicked(e -> System.out.println("Click on pane"));
connection.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("Click on connection"));
start.setOnMouseClicked(e -> System.out.println("Click on start"));
end.setOnMouseClicked(e -> System.out.println("Click on end"));
Scene scene = new Scene(pane, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Circle createDraggingCircle(double centerX, double centerY, double radius, Paint fill) {
Circle circle = new Circle(centerX, centerY, radius, fill);
ObjectProperty<Point2D> mouseLoc = new SimpleObjectProperty<>();
circle.setOnDragDetected(e ->
mouseLoc.set(new Point2D(e.getSceneX(), e.getSceneY())));
circle.setOnMouseReleased(e -> mouseLoc.set(null));
circle.setOnMouseDragged(e -> {
if (mouseLoc.get() == null) return ;
double x = e.getSceneX() ;
double y = e.getSceneY() ;
double deltaX = x - mouseLoc.get().getX() ;
double deltaY = y - mouseLoc.get().getY() ;
circle.setCenterX(circle.getCenterX() + deltaX);
circle.setCenterY(circle.getCenterY() + deltaY);
mouseLoc.set(new Point2D(x, y));
});
return circle ;
}
public static class Connection {
private Path connectingLine ;
private CubicCurve fill ;
private Group group ;
private ObjectProperty<Circle> fromCircle = new SimpleObjectProperty<>();
private ObjectProperty<Circle> toCircle = new SimpleObjectProperty<>();
public Connection() {
connectingLine = new Path();
MoveTo start = new MoveTo();
CubicCurveTo curve = new CubicCurveTo();
CubicCurveTo reverseCurve = new CubicCurveTo();
reverseCurve.xProperty().bind(start.xProperty());
reverseCurve.yProperty().bind(start.yProperty());
reverseCurve.controlX1Property().bind(curve.controlX2Property());
reverseCurve.controlX2Property().bind(curve.controlX1Property());
reverseCurve.controlY1Property().bind(curve.controlY2Property());
reverseCurve.controlY2Property().bind(curve.controlY1Property());
connectingLine.getElements().addAll(start, curve, reverseCurve);
fill = new CubicCurve();
fill.setMouseTransparent(true);
group = new Group();
group.getChildren().addAll(fill, connectingLine);
connectingLine.setStroke(Color.BLACK);
connectingLine.setStrokeWidth(3);
fill.setStrokeWidth(0);
fill.setStroke(Color.TRANSPARENT);
fill.setFill(Color.ALICEBLUE);
fill.startXProperty().bind(start.xProperty());
fill.startYProperty().bind(start.yProperty());
fill.controlX1Property().bind(curve.controlX1Property());
fill.controlX2Property().bind(curve.controlX2Property());
fill.controlY1Property().bind(curve.controlY1Property());
fill.controlY2Property().bind(curve.controlY2Property());
fill.endXProperty().bind(curve.xProperty());
fill.endYProperty().bind(curve.yProperty());
fromCircle.addListener((obs, oldCircle, newCircle) -> {
if (oldCircle != null) {
start.xProperty().unbind();
start.yProperty().unbind();
}
if (newCircle != null) {
start.xProperty().bind(newCircle.centerXProperty());
start.yProperty().bind(newCircle.centerYProperty());
}
});
toCircle.addListener((obs, oldCircle, newCircle) -> {
if (oldCircle != null) {
curve.xProperty().unbind();
curve.yProperty().unbind();
}
if (newCircle != null) {
curve.xProperty().bind(newCircle.centerXProperty());
curve.yProperty().bind(newCircle.centerYProperty());
}
});
ChangeListener<Number> endpointListener = (obs, oldValue, newValue) -> {
Point2D startPoint = new Point2D(start.getX(), start.getY());
Point2D end = new Point2D(curve.getX(), curve.getY());
Point2D vector = end.subtract(startPoint);
Point2D perpVector = new Point2D(-vector.getY(), vector.getX());
Point2D control1 = startPoint.add(perpVector);
Point2D control2 = end.add(perpVector);
curve.setControlX1(control1.getX());
curve.setControlX2(control2.getX());
curve.setControlY1(control1.getY());
curve.setControlY2(control2.getY());
};
start.xProperty().addListener(endpointListener);
start.yProperty().addListener(endpointListener);
curve.xProperty().addListener(endpointListener);
curve.yProperty().addListener(endpointListener);
}
public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
connectingLine.addEventHandler(eventType, eventHandler);
}
public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) {
connectingLine.removeEventHandler(eventType, eventHandler);
}
public Node asNode() {
return group ;
}
public final ObjectProperty<Circle> fromCircleProperty() {
return this.fromCircle;
}
public final javafx.scene.shape.Circle getFromCircle() {
return this.fromCircleProperty().get();
}
public final void setFromCircle(final javafx.scene.shape.Circle fromCircle) {
this.fromCircleProperty().set(fromCircle);
}
public final ObjectProperty<Circle> toCircleProperty() {
return this.toCircle;
}
public final javafx.scene.shape.Circle getToCircle() {
return this.toCircleProperty().get();
}
public final void setToCircle(final javafx.scene.shape.Circle toCircle) {
this.toCircleProperty().set(toCircle);
}
}
public static void main(String[] args) {
launch(args);
}
}
My problem is that I have a rectangle presented with a small perspective, and I would like to stretch it back to be presented as a rectangle again.
To represent it visually, I currently have within my image something like the red shape, and I have 4 Points (each corner of this shape). As result I would like to have something like the blue shape, and I already have the Rectangle object for it.
I was wondering if there is a method to copy a polygon and draw it as another polygon stretched. I found something for Android (setPolyToPoly), but I couldn't find something like this for java.
Is there some reference or code sample that performs this operation, or maybe some idea how can I solve this problem?
I think I understand what you need: a so-called perspective transformation that can be applied to an image. Java has the built-in AffineTransform, but an affine transform always preserves the "parallelness" of lines, so you cannot use that.
Now if you search the web for "java perspective transformation", you will find lots of options like the JavaFX PerspectiveTransform, the JAI PerspectiveTransform. If you only need to stretch images, you can also use the JHLabs PerspectiveFilter and there are other options as well.
Here is some code that will turn stretch a polygon with four points to a rectangle.
public static Rectangle2D polyToRect(Polygon polygon) {
if (polygon.xpoints.length != 4 || polygon.ypoints.length != 4)
throw new IllegalArgumentException(
"More than four points, this cannot be fitted to a rectangle");
Rectangle2D rect = new Rectangle2D.Double();
for (int i = 0; i < 4; i++) {
Point2D point = new Point2D.Double(polygon.xpoints[i],
polygon.ypoints[i]);
rect.add(point);
}
return rect;
}
public static Polygon rectangleToPolygon(Rectangle2D rect) {
Polygon poly = new Polygon();
poly.addPoint((int) rect.getX(), (int) rect.getY());
poly.addPoint((int) (rect.getX() + rect.getWidth()), (int) rect.getY());
poly.addPoint((int) (rect.getX() + rect.getWidth()),
(int) (rect.getY() + rect.getHeight()));
poly.addPoint((int) rect.getX(), (int) (rect.getY() + rect.getHeight()));
return poly;
}
public static class drawPolyAndRect extends JPanel {
Polygon poly = new Polygon();
public drawPolyAndRect() {
poly.addPoint(0, 0);
poly.addPoint(400, 40);
poly.addPoint(400, 250);
poly.addPoint(0, 400);
}
#Override
#Transient
public Dimension getPreferredSize() {
return new Dimension(1000, 1000);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.green);
g2d.fill(poly);
Composite c = AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
0.5f);
g2d.setColor(Color.blue);
g2d.setComposite(c);
Rectangle2D polyToRect = polyToRect(poly);
g2d.fill(polyToRect);
// displace for drawing
polyToRect.setFrame(polyToRect.getX() + 100,
polyToRect.getY() + 100, polyToRect.getWidth(),
polyToRect.getHeight());
Polygon polyToRectToPoly = rectangleToPolygon(polyToRect);
g2d.fill(polyToRectToPoly);
g2d.dispose();
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Poly to rect");
frame.getContentPane().add(new drawPolyAndRect());
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
Essentially this uses how adding points to an empty rectangle2d works, it constructs the smallest possible rectangle containing all four points, stretching the polygon. Check the second static method if you want the returned rectangle as a polygon. Picture:
A JavaFX based solution using a PerspectiveTransform as suggested by #lbalazscs answer.
Toggle Perspective switches on and off the perspective effect on the content.
Morph Perspective smoothly animates a transition between the perspective transformed and non-perspective transformed content.
import javafx.animation.*;
import javafx.application.*;
import javafx.beans.value.*;
import javafx.geometry.Pos;
import javafx.scene.*;
import javafx.scene.control.ToggleButton;
import javafx.scene.effect.PerspectiveTransform;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PerspectiveMovement extends Application {
// perspective transformed group width and height.
private final int W = 280;
private final int H = 96;
// upper right and lower right co-ordinates of perspective transformed group.
private final int URY = 35;
private final int LRY = 65;
#Override public void start(Stage stage) {
final PerspectiveTransform perspectiveTransform = createPerspectiveTransform();
final Group group = new Group();
group.setCache(true);
setContent(group);
final ToggleButton perspectiveToggle = createToggle(
group,
perspectiveTransform
);
VBox layout = new VBox(10);
layout.setAlignment(Pos.CENTER);
layout.getChildren().setAll(
perspectiveToggle,
createMorph(perspectiveToggle, group, perspectiveTransform),
group
);
layout.setStyle("-fx-padding: 10px; -fx-background-color: rgb(17, 20, 25);");
stage.setScene(new Scene(layout));
stage.show();
}
private void setContent(Group group) {
Rectangle rect = new Rectangle(0, 5, W, 80);
rect.setFill(Color.web("0x3b596d"));
Text text = new Text();
text.setX(4.0);
text.setY(60.0);
text.setText("A long time ago");
text.setFill(Color.ALICEBLUE);
text.setFont(Font.font(null, FontWeight.BOLD, 36));
Image image = new Image(
"http://icons.iconarchive.com/icons/danrabbit/elementary/96/Star-icon.png"
);
ImageView imageView = new ImageView(image);
imageView.setX(50);
group.getChildren().addAll(rect, imageView, text);
}
private PerspectiveTransform createPerspectiveTransform() {
PerspectiveTransform perspectiveTransform = new PerspectiveTransform();
perspectiveTransform.setUlx(0.0);
perspectiveTransform.setUly(0.0);
perspectiveTransform.setUrx(W);
perspectiveTransform.setUry(URY);
perspectiveTransform.setLrx(W);
perspectiveTransform.setLry(LRY);
perspectiveTransform.setLlx(0.0);
perspectiveTransform.setLly(H);
return perspectiveTransform;
}
private ToggleButton createToggle(final Group group, final PerspectiveTransform perspectiveTransform) {
final ToggleButton toggle = new ToggleButton("Toggle Perspective");
toggle.selectedProperty().addListener(new ChangeListener<Boolean>() {
#Override public void changed(ObservableValue<? extends Boolean> observable, Boolean wasSelected, Boolean selected) {
if (selected) {
perspectiveTransform.setUry(URY);
perspectiveTransform.setLry(LRY);
group.setEffect(perspectiveTransform);
} else {
group.setEffect(null);
}
}
});
return toggle;
}
private ToggleButton createMorph(final ToggleButton perspectiveToggle, final Group group, final PerspectiveTransform perspectiveTransform) {
final Timeline distorter = new Timeline(
new KeyFrame(
Duration.seconds(0),
new KeyValue(perspectiveTransform.uryProperty(), 0, Interpolator.LINEAR),
new KeyValue(perspectiveTransform.lryProperty(), H, Interpolator.LINEAR)
),
new KeyFrame(
Duration.seconds(3),
new KeyValue(perspectiveTransform.uryProperty(), URY, Interpolator.LINEAR),
new KeyValue(perspectiveTransform.lryProperty(), LRY, Interpolator.LINEAR)
)
);
final ToggleButton morphToggle = new ToggleButton("Morph Perspective");
morphToggle.selectedProperty().addListener(new ChangeListener<Boolean>() {
#Override public void changed(ObservableValue<? extends Boolean> observable, Boolean wasSelected, Boolean selected) {
if (!perspectiveToggle.isSelected()) {
perspectiveToggle.fire();
}
if (selected) {
distorter.setRate(1);
distorter.play();
} else {
distorter.setRate(-1);
distorter.play();
}
}
});
return morphToggle;
}
}
I don't know if this could help you but let me give you what i made with what i understand:
import javax.swing.*;
import java.awt.*;
public class PolyToRectangle extends JPanel {
public static final int SPEED = 50; //less = more fast.
private int ax = 0, bx = 800, cx = 800, dx = 0,
ay = 0, by = 40, cy = 250, dy = 400;
private Polygon poly;
public PolyToRectangle() {
setPreferredSize(new Dimension(1200, 720));
poly = new Polygon(new int[]{ax, bx, cx, dx}, new int[]{ay, by, cy, dy}, 4);
}
#Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.draw(poly);
g2d.fill(poly);
}
public void polyToRectangle() throws InterruptedException {
int flag = 0;
for (int i = 0; i < 150; i++) {
flag++;
poly.addPoint(ax, ay);
poly.addPoint(bx, (by = flag % 3 == 0 ? --by : by));
poly.addPoint(cx, cy++);
poly.addPoint(dx, dy);
Thread.sleep(SPEED);
repaint();
}
}
protected void clear(Graphics g) {
super.paintComponent(g);
}
public static void main(String[] args) throws InterruptedException {
Frame frame = new JFrame();
PolyToRectangle se = new PolyToRectangle();
frame.add(se);
frame.pack();
frame.setVisible(true);
se.polyToRectangle();
}
}
Ok as you can see this code is more a "PolyToSquare" more than PolyToRectangle, but the main was to show the "effect" of repainting with a Thread.sleep in a for, maybe that's the "visual stretch" you are talking about, note that the numbers of iterations on the for depends on numbers of pixels from point 1 and 2 to "stretch" from polygon to rectangle, this is a "hand made" effect, maybe what #lbalazscs is suggesting is the best solution, hope to be helpful, regards.
EDIT: edited the code to be more clean and to follow in a more specific way your goal
(now is more a PolyToRectangle, fixed the bx and cx values).