/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.util;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import com.t3.GeometryUtil;
import com.t3.client.AppStyle;
import com.t3.client.TabletopTool;
import com.t3.client.ui.zone.ZoneRenderer;
import com.t3.swing.ImageLabel;
/**
*/
public class GraphicsUtil {
public static final int BOX_PADDINGX = 10;
public static final int BOX_PADDINGY = 2;
// TODO: Make this configurable
public static final ImageLabel GREY_LABEL = new ImageLabel("com/t3/client/image/grayLabelbox.png", 4, 4);
public static final ImageLabel BLUE_LABEL = new ImageLabel("com/t3/client/image/blueLabelbox.png", 4, 4);
public static final ImageLabel DARK_GREY_LABEL = new ImageLabel("com/t3/client/image/darkGreyLabelbox.png", 4, 4);
/**
* A multiline text wrapping popup.
*
* @param string
* - the string to display in he popup
* @param maxWidth
* - the max width in pixels before wrapping the text
*/
public static Rectangle drawPopup(Graphics2D g, String string, int x, int y, int justification, int maxWidth) {
return drawPopup(g, string, x, y, justification, Color.black, Color.white, maxWidth, 0.5f);
}
public static Rectangle drawPopup(Graphics2D g, String string, int x, int y, int justification, Color background, Color foreground, int maxWidth, float alpha) {
if (string == null) {
string = "";
}
// TODO: expand to work for variable width fonts.
Font oldFont = g.getFont();
Font fixedWidthFont = new Font("Courier New", 0, 12);
g.setFont(fixedWidthFont);
FontMetrics fm = g.getFontMetrics();
StringBuilder sb = new StringBuilder();
while (SwingUtilities.computeStringWidth(fm, sb.toString()) < maxWidth) {
sb.append("0");
}
int maxChars = sb.length() - 1;
string = StringUtil.wrapText(string, Math.min(maxChars, string.length()));
String pattern = "\n";
String[] stringByLine = string.split(pattern);
int rows = stringByLine.length;
String longestRow = new String();
for (int i = 0; i < rows; i++) {
if (longestRow.length() < stringByLine[i].length()) {
longestRow = stringByLine[i];
}
}
int strPixelHeight = fm.getHeight();
int strPixelWidth = SwingUtilities.computeStringWidth(fm, longestRow);
int width = strPixelWidth + BOX_PADDINGX * 2;
int height = strPixelHeight * rows + BOX_PADDINGY * 2;
ZoneRenderer renderer = TabletopTool.getFrame().getCurrentZoneRenderer();
y = Math.max(y - height, BOX_PADDINGY);
switch (justification) {
case SwingUtilities.CENTER:
x = x - strPixelWidth / 2 - BOX_PADDINGX;
break;
case SwingUtilities.RIGHT:
x = x - strPixelWidth - BOX_PADDINGX;
break;
case SwingUtilities.LEFT:
break;
}
x = Math.max(x, BOX_PADDINGX);
x = Math.min(x, renderer.getWidth() - width - BOX_PADDINGX);
// Box
Composite oldComposite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
Rectangle boxBounds = new Rectangle(x, y, width, height);
g.setColor(background);
g.fillRect(boxBounds.x, boxBounds.y, boxBounds.width, boxBounds.height);
AppStyle.border.paintWithin(g, boxBounds);
g.setComposite(oldComposite);
// Renderer message
g.setColor(foreground);
for (int i = 0; i < stringByLine.length; i++) {
int textX = x + BOX_PADDINGX;
int textY = y + BOX_PADDINGY + fm.getAscent() + strPixelHeight * i;
g.drawString(stringByLine[i], textX, textY);
}
g.setFont(oldFont);
return boxBounds;
}
public static Rectangle drawBoxedString(Graphics2D g, String string, int centerX, int centerY) {
return drawBoxedString(g, string, centerX, centerY, SwingUtilities.CENTER);
}
public static Rectangle drawBoxedString(Graphics2D g, String string, int x, int y, int justification) {
return drawBoxedString(g, string, x, y, justification, GREY_LABEL, Color.black);
}
public static Rectangle drawBoxedString(Graphics2D g, String string, int x, int y, int justification, ImageLabel background, Color foreground) {
if (string == null) {
string = "";
}
FontMetrics fm = g.getFontMetrics();
int strWidth = SwingUtilities.computeStringWidth(fm, string);
int width = strWidth + BOX_PADDINGX * 2;
int height = fm.getHeight() + BOX_PADDINGY * 2;
y = y - fm.getHeight() / 2 - BOX_PADDINGY;
switch (justification) {
case SwingUtilities.CENTER:
x = x - strWidth / 2 - BOX_PADDINGX;
break;
case SwingUtilities.RIGHT:
x = x - strWidth - BOX_PADDINGX;
break;
case SwingUtilities.LEFT:
break;
}
// Box
Rectangle boxBounds = new Rectangle(x, y, width, height);
background.renderLabel(g, x, y, width, height);
// Renderer message
g.setColor(foreground);
int textX = x + BOX_PADDINGX;
int textY = y + BOX_PADDINGY + fm.getAscent();
g.drawString(string, textX, textY);
return boxBounds;
}
public static Point2D getProjectedPoint(Point2D origin, Point2D target, int distance) {
double x1 = origin.getX();
double x2 = target.getX();
double y1 = origin.getY();
double y2 = target.getY();
double angle = Math.atan2(y2 - y1, x2 - x1);
double newX = x1 + distance * Math.cos(angle);
double newY = y1 + distance * Math.sin(angle);
return new Point2D.Double(newX, newY);
}
/**
* @return a lighter color, as opposed to a brighter color as in
* Color.brighter(). This prevents light colors from getting
* bleached out.
*/
public static Color lighter(Color c) {
if (c == null)
return null;
else {
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();
r += 64 * (255 - r) / 255;
g += 64 * (255 - g) / 255;
b += 64 * (255 - b) / 255;
return new Color(r, g, b);
}
}
public static Area createAreaBetween(Point a, Point b, int width) {
// Find the angle that is perpendicular to the slope of the points
double rise = b.y - a.y;
double run = b.x - a.x;
double theta1 = Math.atan2(rise, run) - Math.PI / 2;
double theta2 = Math.atan2(rise, run) + Math.PI / 2;
double ax1 = a.x + width * Math.cos(theta1);
double ay1 = a.y + width * Math.sin(theta1);
double ax2 = a.x + width * Math.cos(theta2);
double ay2 = a.y + width * Math.sin(theta2);
double bx1 = b.x + width * Math.cos(theta1);
double by1 = b.y + width * Math.sin(theta1);
double bx2 = b.x + width * Math.cos(theta2);
double by2 = b.y + width * Math.sin(theta2);
GeneralPath path = new GeneralPath();
path.moveTo((float) ax1, (float) ay1);
path.lineTo((float) ax2, (float) ay2);
path.lineTo((float) bx2, (float) by2);
path.lineTo((float) bx1, (float) by1);
path.closePath();
return new Area(path);
}
public static boolean intersects(Area lhs, Area rhs) {
if (lhs == null || lhs.isEmpty() || rhs == null || rhs.isEmpty()) {
return false;
}
if (!lhs.getBounds().intersects(rhs.getBounds())) {
return false;
}
Area newArea = new Area(lhs);
newArea.intersect(rhs);
return !newArea.isEmpty();
}
/**
* True if the lhs area totally contains the rhs area
*/
public static boolean contains(Area lhs, Area rhs) {
if (lhs == null || lhs.isEmpty() || rhs == null || rhs.isEmpty()) {
return false;
}
if (!lhs.getBounds().intersects(rhs.getBounds())) {
return false;
}
Area newArea = new Area(rhs);
newArea.subtract(lhs);
return newArea.isEmpty();
}
public static Area createLineSegmentEllipse(int x1, int y1, int x2, int y2, int steps) {
double x = Math.min(x1, x2);
double y = Math.min(y1, y2);
int w = Math.abs(x1 - x2);
int h = Math.abs(y1 - y2);
// Operate from the center of the ellipse
x += w / 2;
y += h / 2;
// The Ellipse class uses curves, which doesn't work with the topology, so we have to create a geometric ellipse
// out of line segments
GeneralPath path = new GeneralPath();
int a = w / 2;
int b = h / 2;
boolean firstMove = true;
for (double t = -Math.PI; t <= Math.PI; t += (2 * Math.PI / steps)) {
int px = (int) Math.round(x + a * Math.cos(t));
int py = (int) Math.round(y + b * Math.sin(t));
if (firstMove) {
path.moveTo(px, py);
firstMove = false;
} else {
path.lineTo(px, py);
}
}
path.closePath();
return new Area(path);
}
public static void renderSoftClipping(Graphics2D g, Shape shape, int width, double initialAlpha) {
// Our method actually uses double the width, let's update internally
width *= 2;
// Make a copy so that we don't have to revert our changes
Graphics2D g2 = (Graphics2D) g.create();
Area newClip = new Area(g.getClip());
newClip.intersect(new Area(shape));
g2.setClip(newClip);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); // Faster without antialiasing, and looks just as good
// float alpha = (float)initialAlpha / width / 6;
float alpha = .04f;
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
for (int i = 1; i < width; i += 2) {
// if (alpha * i < .2) {
// // Too faded to see anyway, don't waste cycles on it
// continue;
// }
g2.setStroke(new BasicStroke(i));
g2.draw(shape);
}
g2.dispose();
}
public static Area createLine(int width, Point2D... points) {
if (points.length < 2) {
throw new IllegalArgumentException("Must supply at least two points");
}
List<Point2D> bottomList = new ArrayList<Point2D>(points.length);
List<Point2D> topList = new ArrayList<Point2D>(points.length);
for (int i = 0; i < points.length; i++) {
double angle = i < points.length - 1 ? GeometryUtil.getAngle(points[i], points[i + 1]) : GeometryUtil.getAngle(points[i - 1], points[i]);
double lastAngle = i > 0 ? GeometryUtil.getAngle(points[i], points[i - 1]) : GeometryUtil.getAngle(points[i], points[i + 1]);
double delta = i > 0 && i < points.length - 1 ? Math.abs(GeometryUtil.getAngleDelta(angle, lastAngle)) : 180; // creates a 90-deg angle
double bottomAngle = (angle + delta / 2) % 360;
double topAngle = bottomAngle + 180;
// System.out.println(angle + " - " + delta + " - " + bottomAngle + " - " + topAngle);
bottomList.add(getPoint(points[i], bottomAngle, width));
topList.add(getPoint(points[i], topAngle, width));
}
// System.out.println(bottomList);
// System.out.println(topList);
Collections.reverse(topList);
GeneralPath path = new GeneralPath();
Point2D initialPoint = bottomList.remove(0);
path.moveTo((float) initialPoint.getX(), (float) initialPoint.getY());
for (Point2D point : bottomList) {
path.lineTo((float) point.getX(), (float) point.getY());
}
for (Point2D point : topList) {
path.lineTo((float) point.getX(), (float) point.getY());
}
path.closePath();
return new Area(path);
}
private static Point2D getPoint(Point2D point, double angle, double length) {
double x = point.getX() + length * Math.cos(Math.toRadians(angle));
double y = point.getY() - length * Math.sin(Math.toRadians(angle));
// System.out.println(point + " - " + angle + " - " + x + "x" + y + " - " + Math.cos(Math.toRadians(angle)) + " - " + Math.sin(Math.toRadians(angle)) + " - " + Math.toRadians(angle));
return new Point2D.Double(x, y);
}
public static void main(String[] args) {
final Point2D[] points = new Point2D[] { new Point(20, 20), new Point(50, 50), new Point(80, 20), new Point(100, 100) };
// final Point2D[] points = new Point2D[]{new Point(50, 50), new Point(20, 20), new Point(20, 100), new Point(50,75)};
final Area line = createLine(10, points);
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setBounds(10, 10, 200, 200);
JPanel p = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
Dimension size = getSize();
g.setColor(Color.white);
g.fillRect(0, 0, size.width, size.height);
g.setColor(Color.gray);
((Graphics2D) g).fill(line);
g.setColor(Color.red);
for (Point2D p : points) {
g.fillRect((int) (p.getX() - 1), (int) (p.getY() - 1), 2, 2);
}
}
};
f.add(p);
f.setVisible(true);
// System.out.println(area.equals(area2));
}
}