/** * PatchBayApplet - a Socket and Patch Cable approach to wiring up items. * * Jon Meyer, www.cybergrain.com, 2004. * * @author jonmeyer */ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionAdapter; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.QuadCurve2D; import java.util.ArrayList; import java.util.Date; import javax.swing.JApplet; import javax.swing.JLabel; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.Timer; class PatchBayComponent extends JComponent { // A list of all items in the patch panel (either patches or sockets) ArrayList items = new ArrayList(); // A list of all patches (wires connecting sockets) ArrayList patches = new ArrayList(); // A list of all source sockets ArrayList srcSockets = new ArrayList(); // A list of all destination sockets ArrayList dstSockets = new ArrayList(); // Labels for sources ArrayList srcLabels = new ArrayList(); // Labels for destinations ArrayList dstLabels = new ArrayList(); // Used to track the current time double startTime = System.currentTimeMillis() / 1000.0; double time; // Mouse state Item currentItem; Patch currentPatch; int lastX, lastY; String dragMode; String mouseMode; // Image used for each socket Image socketImage; // "Source" panel Image sourcePanelImage; int sourcePanelX = 24; int sourcePanelY = 10; static String sources[] = { "VHS Video", "Cable", "DVD Player", "Satellite", "CD Player", "Radio", }; // "Destination" panel Image destinationPanelImage; int destinationPanelX = 280; int destinationPanelY = 10; static String destinations[] = { "Kitchen", "Livingroom", "Bedroom", "Guest Room", }; // A Item is either a Patch or a Socket // class Item { // True if the mouse is over the item boolean mouseOver; // Called every frame public void animate() { } // Draws the item public void paint(Graphics2D g) { } // Used when the mouse is being dragged public void drag(String dragMode, int x, int y) { } // Used to test if the mouse is over the item public boolean pick(int x, int y) { return false; } } // A Patch is a connection between two Sockets // class Patch extends Item { Socket in, out; QuadCurve2D curve = new QuadCurve2D.Double(); double x1, y1, x2, y2; double ctrlx, ctrly; double wobbleTime; double wobbleFactor; double depth = 1; double alpha = 1; boolean fade; /** * Specifies the start and end point of the line */ public void setLine(double x1, double y1, double x2, double y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; // When the line moves, add a wobble wobbleFactor = .5; wobbleTime = lerp(.5, wobbleTime, time); } /** * Specifies the start point of the line */ public void setLineStart(double x1, double y1) { setLine(x1, y1, x2, y2); } /** * Specifies the end point of the line */ public void setLineEnd(double x2, double y2) { setLine(x1, y1, x2, y2); } /** * Specifies the socket that the patch has as its input */ public void connectIn(Socket in) { if (this.in != null) { this.in.connections.remove(this); } this.in = in; if (in != null) { in.connections.add(this); setLineStart(in.x, in.y); } } /** * Specifies the socket that the patch has as its output */ public void connectOut(Socket out) { if (this.out != null) { this.out.connections.remove(this); } this.out = out; if (out != null) { out.connections.add(this); setLineEnd(out.x, out.y); } } /** * Drags the patch around */ public void drag(String dragMode, int x, int y) { if (dragMode == "start") { // Moving the start point of the patch x1 = x; y1 = y; } else if (dragMode == "end") { // Moving the end point of the patch x2 = x; y2 = y; } // Calculate how much to wobble and how long to wobble // as the patch moves. The faster you move the mouse, the // morge energy it has, so the more the thing wobbles double energy = dist(lastX, lastY, x, y); energy /= 500; if (energy > 1) energy = 1; lastX = x; lastY = y; wobbleFactor = lerp(energy, wobbleFactor, 1); wobbleTime = lerp(.5, wobbleTime, time); } public void paint(Graphics2D g) { try { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } catch (Exception ex) { } // Compute length of line double dist = dist(x1,y1,x2,y2); // Compute position of control point - the // x position is centered on the midpoint. The // y position is offset a little below the midpoint. // ctrlx = (x1+x2) / 2; ctrly = (y1+y2) / 2 + .3 * dist; // Wobble the control point around if wobbleFactor is nonzero // if (wobbleFactor != 0) { double t = time - wobbleTime; ctrly += .07 * dist * wobbleFactor * Math.cos(t*8); ctrlx += .07 * dist * wobbleFactor * Math.sin(t*8) * (x1 < x2 ? 1 : -1); // Reduce the wobbling over time if (wobbleFactor < 0.05) wobbleFactor = 0; else wobbleFactor *= .9; } curve.setCurve(x1, y1, ctrlx, ctrly, x2, y2); // When the patch is deleted, it fades out if (fade) { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float)alpha)); alpha = alpha * .7; if (alpha < 0.05) { items.remove(this); patches.remove(this); } } // Draw a red and black oval at the start of the patch is connected to a source if (in != null && !(mouseMode == "in" && mouseOver)) { g.setColor(Color.red); g.fillOval((int)x1 - 6, (int)y1 - 6, 12, 12); g.setColor(Color.black); g.fillOval((int)x1 - 4, (int)y1 - 4, 8, 8); } // Draw a red and black oval at the start of the patch is connected to a destination if (out != null && !(mouseMode == "out" && mouseOver)) { g.setColor(Color.red); g.fillOval((int)x2 - 6, (int)y2 - 6, 12, 12); g.setColor(Color.black); g.fillOval((int)x2 - 4, (int)y2 - 4, 8, 8); } // Draw the cable itself - we draw it three times with different strokes and // colors to get a 3D effect Color red = new Color((int)(200 *depth), (int)(100 * depth), (int)(100 * depth)); g.setStroke(new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER)); g.setColor(red); g.draw(curve); // Hilite g.setStroke(new BasicStroke(1)); g.setColor(red.brighter()); g.translate(0, -1); g.draw(curve); // Lolite g.setColor(red.darker()); g.translate(0, 2); g.draw(curve); g.translate(0, -1); // Show it selected if (mouseOver) { g.setColor(Color.yellow); g.draw(curve); } if (fade) { g.setComposite(AlphaComposite.SrcOver); } } /** * @see Item#pick(int, int) */ public boolean pick(int x, int y) { return curve.intersects(x - 2, y - 2, 4, 4); } } // A Socket is a little hole that a patch can come out of or go into // class Socket extends Item { // If isSource is true, it represents a source, otherwise its a destination boolean isSource; // Position of the socket int x, y; // List of patches that are currently connected to the socket ArrayList connections = new ArrayList(); /** * Constructor for Socket. */ public Socket(boolean isSource, int x, int y) { this.isSource = isSource; this.x = x; this.y = y; } public void paint(Graphics2D g) { // Draw an image for the socket if (socketImage == null) socketImage = getIcon("socket.gif"); g.drawImage(socketImage, x - 10, y - 9, null); if (mouseOver) { // Show a hilight when the mouse is over the socket g.setColor(Color.yellow); g.drawOval(x - 8, y - 8, 16, 16); } } // Change the location of the socket public void setLocation(int x, int y) { this.x = x; this.y = y; for (int i = 0; i < connections.size(); i++) { // Move all the patches connected to the socket Patch patch = (Patch)connections.get(i); if (isSource) patch.setLineStart(x, y); else patch.setLineEnd(x, y); } } public boolean pick(int x, int y) { return Math.abs(x - this.x) < 20 && Math.abs(y - this.y) < 20; } } // Gets an image based from the current environment Image getIcon(String name) { return getToolkit().getImage(getClass().getResource(name)); } // This method is called whenever a patch is added or removed, // to calculate the "depths" of the patches. // // The depth is used to calculate how brightly to draw the patch, so patches closer // to the front are painted more brightly. // void resetDepths() { for (int i = 0; i < patches.size(); i++) { Patch s = (Patch)patches.get(i); double n = ((double)(i+1) / patches.size()); s.depth = lerp(n, .5, 1); } } /** * Constructor for PatchBayComponent */ PatchBayComponent() { // // Construct components // // Make the source labels and sockets for (int i = 0; i < sources.length; i++) { JLabel b = new JLabel(sources[i]); add(b); b.setSize(b.getPreferredSize()); b.setLocation(130 - b.getPreferredSize().width, i * 40 + 60); srcSockets.add(new Socket(true, 145, i * 40 + 74)); srcLabels.add(b); } // Make the destination labels and sockets for (int i = 0; i < destinations.length; i++) { JLabel b = new JLabel(destinations[i]); add(b); b.setSize(b.getPreferredSize()); b.setLocation(320, i * 40 + 60); dstSockets.add(new Socket(false, 300, i * 40 + 74)); dstLabels.add(b); } items.addAll(srcSockets); items.addAll(dstSockets); // // Event Handlers // addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { lastX = e.getX(); lastY = e.getY(); currentItem = null; // Find which item the mouse is over for (int i = items.size() - 1; i >= 0; i--) { Item s = (Item)items.get(i); if (s.pick(lastX, lastY)) { currentItem = s; break; } } if (currentItem != null && currentItem instanceof Socket) { // The mouse is over a socket - make a new patch for the socket Socket sock = (Socket)currentItem; currentPatch = new Patch(); patches.add(currentPatch); items.add(currentPatch); sock.connections.add(currentPatch); lastX = sock.x; lastY = sock.y; resetDepths(); currentPatch.setLine(lastX, lastY, lastX, lastY); // If its a source socket, we are dragging the end if the line // (which connects to a destination). If its a destination socket, // we are dragging the start of the line (which connects to a source). // if (sock.isSource) { dragMode = "end"; currentPatch.connectIn(sock); } else { dragMode = "start"; currentPatch.connectOut(sock); } } // If the mouse was pressed over no socket or patch... if (currentItem == null) { // See if we are dragging the source or destination panel by its titlebar // if (lastX - sourcePanelX < 140 && lastX - sourcePanelX > 0 && lastY - sourcePanelY < 35 && lastY - sourcePanelY > 0) dragMode = "sources"; if (lastX - destinationPanelX < 160 && lastX - destinationPanelX > 0 && lastY - destinationPanelY < 35 && lastY - destinationPanelY > 0) dragMode = "destinations"; } // If the mouse was pressed over a patch cable... if (currentItem != null && currentItem instanceof Patch) { Patch patch = (Patch)currentItem; if (dist(patch.x1, patch.y1, lastX, lastY) < 30) { // dragging the start of the patch cable dragMode = "start"; currentPatch = patch; patch.connectIn(null); patches.remove(patch); patches.add(patch); } else if (dist(patch.x2, patch.y2, lastX, lastY) < 30) { // dragging the end of the patch cable dragMode = "end"; currentPatch = patch; patch.connectOut(null); patches.remove(patch); patches.add(patch); resetDepths(); } else { // clicking on the middle of the patch cable currentItem = null; patch.fade = true; patch.alpha = 1; patch.connectIn(null); patch.connectOut(null); resetDepths(); } } } public void mouseReleased(MouseEvent e) { if (currentPatch != null) { // We are moving a patch around - see if the patch is now // over a socket... currentItem = null; for (int i = 0; i < items.size(); i++) { Item s = (Item)items.get(i); if (s.pick(e.getX(), e.getY())) { currentItem = s; break; } } if (currentItem instanceof Socket && ((dragMode == "start") == ((Socket)currentItem).isSource)) { // Making a new valid patch: // We have dragged the start of a patch to a source, // or the end of a patch to a destination, so now // make the connection Socket sock = (Socket)currentItem; if (sock.isSource) { currentPatch.connectIn(sock); } else { currentPatch.connectOut(sock); } } else { // Patch dragged off any socket - Disconnect the patch currentPatch.fade = true; currentPatch.connectIn(null); currentPatch.connectOut(null); } } currentPatch = null; dragMode = null; } }); addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent e) { // Hilite the appropriate item checkMouseOver(e.getX(), e.getY()); } public void mouseDragged(MouseEvent e) { // Hilite the appropriate item checkMouseOver(e.getX(), e.getY()); int dx = e.getX() - lastX; int dy = e.getY() - lastY; lastX = e.getX(); lastY = e.getY(); // Check if we are dragging the "sources" or "destinations" panel if (dragMode == "sources") { // Apply constraints to stop source panel moving to right of destination if (sourcePanelX + 140 + dx > destinationPanelX) return; if (sourcePanelX + dx < 0) return; if (sourcePanelY + dy < 0) return; if (sourcePanelY + dy + 300 > getSize().height) return; // Move all the labels for (int i = 0; i < srcLabels.size(); i++) { JComponent c = (JComponent)srcLabels.get(i); Point p = c.getLocation(); p.x += dx; p.y += dy; c.setLocation(p); } // Move all the sockets for (int i = 0; i < srcSockets.size(); i++) { Socket c = (Socket)srcSockets.get(i); c.setLocation(c.x + dx, c.y + dy); } sourcePanelX += dx; sourcePanelY += dy; PatchBayComponent.this.repaint(); } else if (dragMode == "destinations") { // Apply constraints to stop source panel moving to right of destination if (destinationPanelX + dx < sourcePanelX + 140) return; if (destinationPanelX + 160 + dx > getSize().width) return; if (destinationPanelY + dy < 0) return; if (destinationPanelY + dy + 230 > getSize().height) return; // Move all the labels for (int i = 0; i < dstLabels.size(); i++) { JComponent c = (JComponent)dstLabels.get(i); Point p = c.getLocation(); p.x += dx; p.y += dy; c.setLocation(p); } // Move all the sockets for (int i = 0; i < dstSockets.size(); i++) { Socket c = (Socket)dstSockets.get(i); c.setLocation(c.x + dx, c.y + dy); } destinationPanelX += dx; destinationPanelY += dy; PatchBayComponent.this.repaint(); } else if (currentPatch != null) // Drag a patch around currentPatch.drag(dragMode, e.getX(), e.getY()); } }); // Start a timer to repaint regularly Timer t = new Timer(100, new ActionListener() { public void actionPerformed(ActionEvent e) { repaint(); } }); t.start(); setLayout(null); } // Called when the mouse has moved to figure out what is currently hilited // void checkMouseOver(int x, int y) { Item picked = null; // Determine what the mouse is over for (int i = items.size() - 1; i >= 0; i--) { Item s = (Item)items.get(i); // Reset all item's mouseOver field to false s.mouseOver = false; if (dragMode != null) { // within a drag // during drags ignore patches if (s instanceof Patch) continue; // during drags, don't hilite a source socket if the user is // dragging the start of a patch, and don't hilite a destination // socket when the user is dragging the end of the patch. if (s instanceof Socket && ((dragMode == "start") != ((Socket)s).isSource)) { continue; } } if (picked == null) picked = (s.pick(x, y) ? s : null); } if (picked != null) { // The mouse is over something that needs hiliting picked.mouseOver = true; // Figure out if the mouse is over a source or destination socket that needs // to be hilited mouseMode = null; if (picked instanceof Patch) { Patch patch = (Patch)picked; if (patch.in != null && dist(patch.x1, patch.y1, x, y) < 30) { mouseMode = "in"; } else if (patch.out != null && dist(patch.x2, patch.y2, x, y) < 30) { mouseMode = "out"; } } } } // Main paint method public void paint(Graphics g) { // Draw the source panel if (sourcePanelImage == null) sourcePanelImage = getIcon("sources.jpg"); g.drawImage(sourcePanelImage, sourcePanelX, sourcePanelY, null); // Draw the destination panel if (destinationPanelImage == null) destinationPanelImage = getIcon("destinations.jpg"); g.drawImage(destinationPanelImage, destinationPanelX, destinationPanelY, null); // This draws all the labels within the panel super.paint(g); // Update the clock (used to calculate wobbles) time = (System.currentTimeMillis() / 1000.0) - startTime; // Draw all the items Graphics2D g2 = (Graphics2D)g; for (int i = 0; i < items.size(); i++) { Item s = (Item)items.get(i); s.paint(g2); } } // ------------------------------------------------------------------------ // -- Utility Methods // ------------------------------------------------------------------------ static double lerp(double t, double a, double b) { return a + t * (b-a); } static double dist(double x1, double y1, double x2, double y2) { return Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)); } } public class PatchBayApplet extends JApplet { public PatchBayApplet() { JComponent patchBay = new PatchBayComponent(); getContentPane().add(patchBay); getContentPane().setBackground(Color.black); } public static void main(String args[]) { JFrame f = new JFrame(); PatchBayComponent patchBay = new PatchBayComponent(); f.getContentPane().add(patchBay); f.getContentPane().setBackground(Color.black); f.setSize(600, 600); f.setVisible(true); f.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(1); } }); } }