/* * Copyright 2008-2014, David Karnok * The file is part of the Open Imperium Galactica project. * * The code should be distributed under the LGPL license. * See http://www.gnu.org/licenses/lgpl.html for details. */ package hu.openig.render; import hu.openig.core.Pair; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.TexturePaint; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Random; /** * Utility class for common rendering tasks and algorithms. * @author akarnokd */ public final class RenderTools { /** Utility class. */ private RenderTools() { } /** The top status bar height constant. */ public static final int STATUS_BAR_TOP = 20; /** The bottom status bar height constant. */ public static final int STATUS_BAR_BOTTOM = 18; /** * Set the rectangle (X, Y) so that the rectangle is centered on the screen * denoted by the screen width and height. * The centering may consider the effect of the top and bottom status bar * @param current the current rectangle * @param screenWidth the screen width * @param screenHeight the screen height * @param considerStatusbars consider, that the status bars take 20px top and 18 pixels bottom? * @return the updated current rectangle */ public static Rectangle centerScreen(Rectangle current, int screenWidth, int screenHeight, boolean considerStatusbars) { if (current == null) { throw new IllegalArgumentException("current is null"); } if (current.width == 0) { throw new IllegalArgumentException("current.width is zero"); } if (current.height == 0) { throw new IllegalArgumentException("current.height is zero"); } current.x = (screenWidth - current.width) / 2; if (considerStatusbars) { current.y = STATUS_BAR_TOP + (screenHeight - STATUS_BAR_TOP - STATUS_BAR_BOTTOM - current.height) / 2; } else { current.y = (screenHeight - current.height) / 2; } return current; } /** * Paint a semi-transparent area around the supplied panel. * @param panel the panel to paint around * @param screenWidth the target screen width * @param screenHeight the target screen height * @param g2 the target graphics object * @param alpha the transparency level * @param considerStatusbars consider, that the status bars take 20px top and 18 pixels bottom? */ public static void darkenAround( Rectangle panel, int screenWidth, int screenHeight, Graphics2D g2, float alpha, boolean considerStatusbars) { Composite c = null; Paint p = null; if (fast) { p = g2.getPaint(); BufferedImage fimg = holes((float)Math.min(1.0, alpha * 2)); g2.setPaint(new TexturePaint(fimg, new Rectangle(panel.x, panel.y, fimg.getWidth(), fimg.getHeight()))); } else { c = g2.getComposite(); g2.setComposite(AlphaComposite.SrcOver.derive(alpha)); g2.setColor(Color.BLACK); } if (considerStatusbars) { fillRectAbsolute(0, STATUS_BAR_TOP, screenWidth - 1, panel.y - 1, g2); fillRectAbsolute(0, panel.y + panel.height, screenWidth - 1, screenHeight - 1 - STATUS_BAR_BOTTOM, g2); } else { fillRectAbsolute(0, 0, screenWidth - 1, panel.y - 1, g2); fillRectAbsolute(0, panel.y + panel.height, screenWidth - 1, screenHeight - 1, g2); } fillRectAbsolute(0, panel.y, panel.x - 1, panel.y + panel.height - 1, g2); fillRectAbsolute(panel.x + panel.width, panel.y, screenWidth - 1, panel.y + panel.height - 1, g2); if (fast) { g2.setPaint(p); } else { g2.setComposite(c); } } /** Use a faster drawing trick for alpha composition? */ static boolean fast = false; /** The cache for the last alpha blending. */ static float lastAlpha; /** The image to use for the alpha blending. */ static BufferedImage lastHoles; /** * Create a transparent image which has holes. * @param alpha the effective transparency level * @return the image */ static BufferedImage holes(float alpha) { if (alpha != lastAlpha) { int r = (int)(255 * (1 - alpha)); int size = 16; // The pattern size int[] pixels = new int[size * size]; for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if ((i % 2) == (j % 2)) { pixels[i * size + j] = argb(255, r, r, r); } } } BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); img.setRGB(0, 0, size, size, pixels, 0, size); lastHoles = img; lastAlpha = alpha; return img; } return lastHoles; } /** * Create an ARGB color from its components. * @param a The alpha channel value * @param r the RED component * @param g the GREEN component * @param b the BLUE component * @return the composite color */ public static int argb(int a, int r, int g, int b) { return ((a << 24) & 0xFF000000) | ((r << 16) & 0x00FF0000) | ((g << 8) & 0x0000FF00) | ((b) & 0x000000FF) ; } /** * Fill the rectangle given by absolute coordinates. * @param x the left start of the filling inclusive * @param y the top start of the filling inclusive * @param x2 the right end of the filling inclusive * @param y2 the bottom end of the filling inclusive * @param g2 the graphics context */ public static void fillRectAbsolute(int x, int y, int x2, int y2, Graphics2D g2) { if (x >= x2 || y >= y2) { return; } g2.fillRect(x, y, x2 - x + 1, y2 - y + 1); } /** * Paint a 5x5 ABC grid on the given rectangle by equally distributing its cells. * The caller should remember to apply a clipping region if necessary. * @param g2 the graphics object * @param rect the rectangle to fill in * @param gridColor the color of the grid * @param text the text renderer to print the labels */ public static void paintGrid(Graphics2D g2, Rectangle rect, Color gridColor, TextRenderer text) { g2.setColor(gridColor); Stroke st = g2.getStroke(); //FIXME the dotted line rendering is somehow very slow // g2.setStroke(gfx.gridStroke); float fw = rect.width; float fh = rect.height; float dx = fw / 5; float dy = fh / 5; float y0 = dy; float x0 = dx; for (int i = 1; i < 5; i++) { g2.drawLine((int)(rect.x + x0), rect.y, (int)(rect.x + x0), (int)(rect.y + fh)); g2.drawLine(rect.x, (int)(rect.y + y0), (int)(rect.x + fw), (int)(rect.y + y0)); x0 += dx; y0 += dy; } int i = 0; y0 = dy - 6; x0 = 2; for (char c = 'A'; c < 'Z'; c++) { text.paintTo(g2, (int)(rect.x + x0), (int)(rect.y + y0), 5, TextRenderer.GRAY, String.valueOf(c)); x0 += dx; i++; if (i % 5 == 0) { x0 = 2; y0 += dy; } } g2.setStroke(st); } /** Star rendering starting color. */ private static int startStars = 0x685CA4; /** Star rendering end color. */ private static int endStars = 0xFCFCFC; /** Number of stars per layer. */ private static final int STAR_COUNT = 512; /** Number of layers. */ private static final int STAR_LAYER_COUNT = 4; /** A star object. */ private static class Star { /** The star proportional position. */ public double x; /** The star proportional position. */ public double y; /** The star color. */ public Color color; } /** The list of stars. */ private static List<Star> stars = new ArrayList<>(); /** * Paint the multiple layer of stars. * @param g2 the graphics object * @param rect the target rectangle * @param view the viewport rectangle * @param starmapClip the clipping region to avoid even calling a graphics operation outside there for performance reasons * @param zoomIndex the current zoom index * @param zoomLevelCount the maximum zoom level */ public static void paintStars(Graphics2D g2, Rectangle rect, Rectangle view, Rectangle starmapClip, int zoomIndex, int zoomLevelCount) { int starsize = zoomIndex < zoomLevelCount / 2.5 ? 1 : 2; double xf = (view.x - rect.x) * 1.0 / rect.width; double yf = (view.y - rect.y) * 1.0 / rect.height; Color last = null; for (int i = 0; i < stars.size(); i++) { Star s = stars.get(i); int layerIdx = i / STAR_COUNT; double layer = 0.9 - layerIdx * 0.10; double w = rect.width * layer; double h = rect.height * layer; double lx = rect.x; double ly = rect.y; int x = (int)(lx + xf * (rect.width - w) + s.x * rect.width); int y = (int)(ly + yf * (rect.height - h) + s.y * rect.height); if (starmapClip.contains(x, y)) { if (last != s.color) { g2.setColor(s.color); last = s.color; } g2.fillRect(x, y, starsize, starsize); } } } static { precalculateStars(); } /** * Precalculates the star background locations and colors. */ private static void precalculateStars() { Random random = new Random(0); Color[] colors = new Color[8]; for (int i = 0; i < colors.length; i++) { colors[i] = new Color(mixColors(startStars, endStars, random.nextFloat())); } for (int i = 0; i < STAR_COUNT * STAR_LAYER_COUNT; i++) { Star s = new Star(); s.x = random.nextDouble(); s.y = random.nextDouble(); s.color = colors[random.nextInt(colors.length)]; stars.add(s); } Collections.sort(stars, new Comparator<Star>() { @Override public int compare(Star o1, Star o2) { int c1 = o1.color.getRGB() & 0xFFFFFF; int c2 = o2.color.getRGB() & 0xFFFFFF; return c1 - c2; } }); } /** * Mix two colors with a factor. * @param c1 the first color * @param c2 the second color * @param rate the mixing factor * @return the mixed color */ public static int mixColors(int c1, int c2, float rate) { return ((int)((c1 & 0xFF0000) * rate + (c2 & 0xFF0000) * (1 - rate)) & 0xFF0000) | ((int)((c1 & 0xFF00) * rate + (c2 & 0xFF00) * (1 - rate)) & 0xFF00) | ((int)((c1 & 0xFF) * rate + (c2 & 0xFF) * (1 - rate)) & 0xFF); } /** * Flood-fill the area of the given rectangle with the given image. * @param g2 the graphics context * @param rect the target rectangle * @param image the image to use */ public static void fill(Graphics2D g2, Rectangle rect, BufferedImage image) { fill(g2, rect.x, rect.y, rect.width, rect.height, image); } /** * Flood-fill the area of the given rectangle with the given image. * @param g2 the graphics context * @param x the left coordinate * @param y the top coordinate * @param width the area width * @param height the area height * @param image the image to use */ public static void fill(Graphics2D g2, int x, int y, int width, int height, BufferedImage image) { Paint save = g2.getPaint(); TexturePaint tp = new TexturePaint(image, new Rectangle(x, y, image.getWidth(), image.getHeight())); g2.setPaint(tp); g2.fillRect(x, y, width, height); g2.setPaint(save); } /** * Render the target image centered relative to the given rectangle. * @param g2 the graphics object * @param rect the target rectangle * @param image the image to render */ public static void drawCentered(Graphics2D g2, Rectangle rect, BufferedImage image) { int dw = (rect.width - image.getWidth()) / 2; int dh = (rect.height - image.getHeight()) / 2; g2.drawImage(image, rect.x + dw, rect.y + dh, null); } /** * Enable and disable the bilinear interpolation mode for the graphics. * @param g2 the target graphics object. * @param active activate? */ public static void setInterpolation(Graphics2D g2, boolean active) { if (active) { g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); } else { g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); } } /** * Enable and disable the bilinear interpolation mode for the graphics. * @param g2 the target graphics object. * @param active activate? */ public static void setAntiailas(Graphics2D g2, boolean active) { if (active) { g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } else { g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); } } /** * Check if a specified point is inside a specified rectangle. * @param x0 the upper left coordinate * @param y0 the upper left coordinate * @param x1 the lower right coordinate (exclusive) * @param y1 the lower right coordinate (exclusive) * @param x the point to check * @param y the point to check * @return True if the point is inside the rectangle, * false otherwise. */ public static boolean isPointInsideRectangle(double x0, double y0, double x1, double y1, double x, double y) { return x >= x0 && x < x1 && y >= y0 && y < y1; } /** * Return true if c is between a and b. * @param a range value (inclusive) * @param b range value (inclusive) * @param c the value to test * @return true if between */ private static boolean isBetween(double a, double b, double c) { return b > a ? c >= a && c <= b : c >= b && c <= a; } /** * Check if two points are on the same side of a given line. * Algorithm from Sedgewick page 350. * * @param x0 the line start X * @param y0 the line start Y * @param x1 the line end X * @param y1 the line end Y * @param px0 the first point X * @param py0 the first point Y * @param px1 the second point X * @param py1 the second point Y * @return <0 if points on opposite sides. * =0 if one of the points is exactly on the line * >0 if points on same side. */ private static int sameSide(double x0, double y0, double x1, double y1, double px0, double py0, double px1, double py1) { int sameSide = 0; double dx = x1 - x0; double dy = y1 - y0; double dx1 = px0 - x0; double dy1 = py0 - y0; double dx2 = px1 - x1; double dy2 = py1 - y1; // Cross product of the vector from the endpoint of the line to the point double c1 = dx * dy1 - dy * dx1; double c2 = dx * dy2 - dy * dx2; if (c1 != 0 && c2 != 0) { sameSide = c1 < 0 != c2 < 0 ? -1 : 1; } else if (dx == 0 && dx1 == 0 && dx2 == 0) { sameSide = !isBetween(y0, y1, py0) && !isBetween(y0, y1, py1) ? 1 : 0; } else if (dy == 0 && dy1 == 0 && dy2 == 0) { sameSide = !isBetween(x0, x1, px0) && !isBetween(x0, x1, px1) ? 1 : 0; } return sameSide; } /** * Check if two line segments intersects. * * @param x0 the first line start X * @param y0 the first line start Y * @param x1 the first line end X * @param y1 the first line end Y * @param x2 the second line start X * @param y2 the second line start Y * @param x3 the second line end X * @param y3 the second line end Y * @return True if the two lines intersects. */ public static boolean isLineIntersectingLine(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) { int s1 = sameSide(x0, y0, x1, y1, x2, y2, x3, y3); int s2 = sameSide(x2, y2, x3, y3, x0, y0, x1, y1); return s1 <= 0 && s2 <= 0; } /** * Check if a specified line intersects a specified rectangle. * @param lx0 the line start X * @param ly0 the line start Y * @param lx1 the line end X * @param ly1 the line end Y * @param x0 the rectangle left * @param y0 the rectangle top * @param x1 the rectangle right (excluded) * @param y1 the rectangle bottom (excluded) * @return True if the line intersects the rectangle, * false otherwise. */ public static boolean isLineIntersectingRectangle(double lx0, double ly0, double lx1, double ly1, double x0, double y0, double x1, double y1) { // Is one of the line endpoints inside the rectangle if (isPointInsideRectangle(x0, y0, x1, y1, lx0, ly0) || isPointInsideRectangle(x0, y0, x1, y1, lx1, ly1)) { return true; } // If it intersects it goes through. Need to check three sides only. // Check against top rectangle line if (isLineIntersectingLine(lx0, ly0, lx1, ly1, x0, y0, x1, y0)) { return true; } // Check against left rectangle line if (isLineIntersectingLine(lx0, ly0, lx1, ly1, x0, y0, x0, y1)) { return true; } // Check against bottom rectangle line return isLineIntersectingLine(lx0, ly0, lx1, ly1, x0, y1, x1, y1); } /** * Compute the scaling factor and the top-left point where the rendering should scale from * in order to keep the content fit the enclosing window. * @param windowWidth the window width * @param windowHeight the window height * @param contentWidth the content width * @param contentHeight the content height * @return the origin and scale */ public static Pair<Point, Double> fitWindow(int windowWidth, int windowHeight, int contentWidth, int contentHeight) { double sx = windowWidth * 1.0 / contentWidth; double sy = windowHeight * 1.0 / contentHeight; double scalex = Math.min(sx, sy); double scaley = scalex; return Pair.of(new Point( (int)(windowWidth - contentWidth * scalex) / 2, (int)(windowHeight - contentHeight * scaley) / 2 ), scalex); } }