/*
* PanoramaPanel.java
* Eisenkraut
*
* Copyright (c) 2004-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*/
package de.sciss.eisenkraut.gui;
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("serial")
public class PanoramaPanel extends JComponent {
private final AffineTransform at = new AffineTransform();
protected static final Insets insets = new Insets(1, 1, 1, 1);
private static final Shape shpCtrlIn = new Ellipse2D.Double(-2, -2, 5, 5);
private static final Area shpCtrlOut;
private static final Paint pntCtrlInLight = new Color(0x00, 0x00, 0x00, 0x7F);
private static final Paint pntCtrlOutLight = new Color(0x00, 0x00, 0x00, 0x3F);
private static final Paint pntCtrlOutSLight = new Color(0x00, 0x00, 0xFF, 0x7F);
private static final Paint pntCtrlInDark = new Color(0xFF, 0xFF, 0xFF, 0x7F);
private static final Paint pntCtrlOutDark = new Color(0xFF, 0xFF, 0xFF, 0x3F);
private static final Paint pntCtrlOutSDark = new Color(0x7F, 0x7F, 0xFF, 0x7F);
private final Paint pntCtrlIn, pntCtrlOut, pntCtrlOutS;
private final List<Shape> outlines = new ArrayList<Shape>();
private final List<Shape> tOutlines = new ArrayList<Shape>();
private final List<Paint> outlinePaints = new ArrayList<Paint>();
private final List<Shape> areas = new ArrayList<Shape>();
private final List<Shape> tAreas = new ArrayList<Shape>();
private final List<Paint> areaPaints = new ArrayList<Paint>();
private final Point2D ctrlPt = new Point2D.Double( 0.0, 0.75 );
protected double recentRadius = -1;
private boolean recalculate = true;
protected boolean isDragging = false;
protected final double startAngle, deltaAngle;
private final int numSpots;
protected double azi, spread;
private static final List<ActionListener> collListeners = new ArrayList<ActionListener>();
private final boolean isDark;
static {
shpCtrlOut = new Area(new Ellipse2D.Double(-7, -7, 15, 15));
shpCtrlOut.subtract(new Area(new Ellipse2D.Double(-4, -4, 9, 9)));
}
public PanoramaPanel(final int numSpots, double startAng) {
super();
isDark = GraphicsUtil.isDarkSkin();
pntCtrlIn = isDark ? pntCtrlInDark : pntCtrlInLight;
pntCtrlOut = isDark ? pntCtrlOutDark : pntCtrlOutLight;
pntCtrlOutS = isDark ? pntCtrlOutSDark : pntCtrlOutSLight;
if (startAng < 0) {
startAng = 360 - ((-startAng) % 360);
}
startAngle = startAng % 360;
deltaAngle = 360.0 / numSpots;
this.numSpots = numSpots;
outlines.add(new Ellipse2D.Double(-1.0, -1.0, 2.0, 2.0));
outlinePaints.add(isDark ? Color.white : Color.black);
outlines.add(new Ellipse2D.Double(-0.75, -0.75, 1.5, 1.5));
outlinePaints.add(pntCtrlOut);
double angDeg, angRad, dx, dy;
double speakerWidth = Math.max( 4, Math.min( 10, deltaAngle / 2 ));
Arc2D arc1 = new Arc2D.Double( -1.15, -1.15, 2.3, 2.3, 0, speakerWidth, Arc2D.PIE );
Arc2D arc2 = new Arc2D.Double( -1.05, -1.05, 2.1, 2.1, 0, speakerWidth, Arc2D.PIE );
Area area;
for (int i = 0; i < numSpots; i++) {
angDeg = startAngle + i * deltaAngle;
angRad = (-angDeg + 90) * Math.PI / 180;
dx = Math.cos(angRad);
dy = Math.sin(angRad);
outlines.add(new Line2D.Double(0.0, 0.0, dx, dy));
outlinePaints.add(pntCtrlOut);
arc1.setAngleStart(angDeg - 90 - speakerWidth / 2);
arc2.setAngleStart(angDeg - 90 - speakerWidth / 2);
area = new Area(arc1);
area.subtract(new Area(arc2));
areas.add(area);
areaPaints.add(pntCtrlIn);
}
setMinimumSize (new Dimension( 64, 64));
setPreferredSize(new Dimension(128, 128));
setBorder(BorderFactory.createEmptyBorder(insets.top, insets.left, insets.bottom, insets.right));
MouseInputAdapter mia = new MouseInputAdapter() {
public void mousePressed(MouseEvent e) {
final Point2D mousePt = getVirtualMousePos(e);
isDragging = true;
processDrag(mousePt, !e.isControlDown());
}
public void mouseReleased(MouseEvent e) {
isDragging = false;
repaint();
}
// x ^= radius, y ^= angleRad
private Point2D getVirtualMousePos(MouseEvent e) {
final double x = ((e.getX() - insets.left) / recentRadius) - 1.15;
final double y = 1.15 - ((e.getY() - insets.top) / recentRadius);
return new Point2D.Double(
Math.min(1.0, Math.sqrt(x * x + y * y)),
Math.atan2(y, x));
}
public void mouseDragged(MouseEvent e) {
if (isDragging) processDrag(getVirtualMousePos(e), !e.isControlDown());
}
public void mouseMoved(MouseEvent e) {
mouseDragged(e);
}
private void processDrag(Point2D mousePt, boolean snap) {
double dragAngDeg, temp, temp2; // , spreadDegH;
if( snap ) {
if( mousePt.getX() < 0.1 ) {
mousePt.setLocation( 0.0, mousePt.getY() );
} else if( Math.abs( mousePt.getX() - 0.75 ) < 0.1 ) {
mousePt.setLocation( 0.75, mousePt.getY() );
} else if( 1.0 - mousePt.getX() < 0.1 ) {
mousePt.setLocation( 1.0, mousePt.getY() );
}
dragAngDeg = -(mousePt.getY() * 180 / Math.PI) + 90;
if( dragAngDeg < 0 ) dragAngDeg += 360;
for( int i = 0; i < numSpots * 2; i++ ) {
temp = (startAngle + (deltaAngle * i) / 2) % 360;
temp2 = Math.abs( temp - dragAngDeg );
if( temp2 > 180 ) temp2 = 360 - temp2;
if( temp2 < 5 ) {
mousePt.setLocation( mousePt.getX(), (-temp + 90) * Math.PI / 180 );
break;
}
}
}
// azi + spread
if( mousePt.getX() <= 0.75 ) {
spread = 1.0 - (mousePt.getX() / 0.75);
} else {
spread = (0.75 - mousePt.getX()) / 0.25;
}
dragAngDeg = -(mousePt.getY() * 180 / Math.PI) + 90;
setAzimuthAndSpread( dragAngDeg, spread );
dispatchAction();
}
};
addMouseListener(mia);
addMouseMotionListener(mia);
}
public void beginDragging()
{
isDragging = true;
}
protected void dispatchAction() {
final ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null);
synchronized (collListeners) {
for (ActionListener collListener : collListeners) {
collListener.actionPerformed(e);
}
}
}
public void addActionListener(ActionListener l) {
synchronized (collListeners) {
collListeners.add(l);
}
}
public void removeActionListener(ActionListener l) {
synchronized (collListeners) {
collListeners.remove(l);
}
}
private void recalculateColours() {
double temp, spreadDegH;
if( spread >= 0.0 ) {
spreadDegH = spread * (1.0 - 2.0 / numSpots) + 2.0 / numSpots;
} else {
spreadDegH = (2.0 + spread) / numSpots;
}
spreadDegH *= 180;
for (int i = 0; i < numSpots; i++) {
temp = (startAngle + deltaAngle * i) % 360;
temp = Math.abs(temp - azi);
if (temp > 180) temp = 360 - temp;
temp = Math.max(0.0, 1.0 - temp / spreadDegH);
final double tempI = 1.0 - temp;
final int red = (int) ((temp * 0xC0) + (isDark ? tempI * 0xFF : 0));
final int green = isDark ? (int) (tempI * 0xFF) : 0;
final int blue = green;
final int alpha = (int) ((temp + 1.0) * 0x7F);
areaPaints.set(i, new Color(red, green, blue, alpha));
}
}
public void setAzimuthAndSpread(double azi, double spread) {
this.spread = Math.max(-1.0, Math.min(1.0, spread));
this.azi = azi < 0 ? 360 - ((-azi) % 360) : azi % 360;
ctrlPt.setLocation(PanoramaPanel.aziAndSpreadToCtrlPoint(this.azi, this.spread));
recalculateColours();
if (isVisible()) repaint();
}
public static Point2D aziAndSpreadToCtrlPoint(double azi, double spread) {
final double r = spread >= 0.0 ? (1.0 - spread) * 0.75 : 0.75 - spread * 0.25;
final double angRad = (-azi + 90) * Math.PI / 180;
return new Point2D.Double(r * Math.cos(angRad), r * Math.sin(angRad));
}
public double getAzimuth()
{
return azi;
}
public double getSpread()
{
return spread;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
final Graphics2D g2 = (Graphics2D) g;
final int currentWidth = getWidth() - insets.left - insets.right;
final int currentHeight = getHeight() - insets.top - insets.bottom;
final AffineTransform oldAT = g2.getTransform();
final double radius = Math.min( currentWidth, currentHeight ) / 2.3;
double trnsX, trnsY;
if ((radius != recentRadius) || recalculate) {
recentRadius = radius;
at.setToScale(radius, -radius);
at.translate(1.15, -1.15);
recalculateTransforms();
}
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.translate(insets.left, insets.top);
for (int i = 0; i < tOutlines.size(); i++) {
g2.setPaint(outlinePaints.get(i));
g2.draw(tOutlines.get(i));
}
for (int i = 0; i < tAreas.size(); i++) {
g2.setPaint(areaPaints.get(i));
g2.fill(tAreas.get(i));
}
trnsX = (ctrlPt.getX() + 1.15) * radius;
trnsY = (1.15 - ctrlPt.getY()) * radius;
g2.translate(trnsX, trnsY);
g2.setPaint(pntCtrlIn);
g2.fill(shpCtrlIn);
if (isDragging) {
g2.setPaint(pntCtrlOutS);
} else {
g2.setPaint(pntCtrlOut);
}
g2.fill(shpCtrlOut);
g2.translate(-trnsX, -trnsY);
g2.setTransform(oldAT);
}
private void recalculateTransforms() {
tOutlines.clear();
for (Shape outline : outlines) {
tOutlines.add(at.createTransformedShape(outline));
}
tAreas.clear();
for (Shape area : areas) {
tAreas.add(at.createTransformedShape(area));
}
recalculate = false;
}
}