package jrummikub.view.impl; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.GeneralPath; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.Map; import jrummikub.model.Position; import jrummikub.model.Stone; import jrummikub.model.StoneColor; /** * The StonePainter paints stones and converts between pixel and grid * coordinates */ class StonePainter { private static final float ASPECT_RATIO = 0.75f; private static final float DEFAULT_WIDTH = 40; private static final float TEXT_POS = 0.275f; private static final float FACE_WIDTH = 0.475f; private static final float CIRCLE_POS = 0.725f; private static final float CIRCLE_WIDTH = 0.45f; private static final Color BACKGROUND_COLOR = new Color(0.9f, 0.9f, 0.6f); private static final float BRIGHTER_SCALE = 1.15f; private static final float HOVER_RATIO = 0.7f; private Map> defaultStones; private Map> selectedStones; private Map> hoveredStones; private Map> hoveredSelectedStones; /** * The width of one pixel in the scale of 1.0 */ public static final float WIDTH_SCALE = 1 / DEFAULT_WIDTH; /** * The height of one pixel in the scale of 1.0 */ public static final float HEIGHT_SCALE = ASPECT_RATIO / DEFAULT_WIDTH; private float scale; private static int even(float f) { return 2 * (int) (f / 2); } private static Color brighter(Color color) { int r = (int) (color.getRed() * BRIGHTER_SCALE); int g = (int) (color.getGreen() * BRIGHTER_SCALE); int b = (int) (color.getBlue() * BRIGHTER_SCALE); return new Color(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255 : b); } private static Color hover(Color color) { int r = (int) (color.getRed() * HOVER_RATIO + 255 * (1 - HOVER_RATIO)); int g = (int) (color.getGreen() * HOVER_RATIO + 255 * (1 - HOVER_RATIO)); int b = (int) (color.getBlue() * HOVER_RATIO + 255 * (1 - HOVER_RATIO)); return new Color(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255 : b); } private static Color getColor(StoneColor color) { switch (color) { case BLACK: return new Color(0.15f, 0.15f, 0.15f); case BLUE: return new Color(0.0f, 0.0f, 1.0f); case ORANGE: return new Color(1.0f, 0.4f, 0.0f); case RED: return new Color(0.9f, 0.0f, 0.25f); case BROWN: return new Color(0.5f, 0.25f, 0.0f); case GREEN: return new Color(0.0f, 0.75f, 0.0f); case VIOLET: return new Color(0.75f, 0.325f, 0.75f); case WHITE: return new Color(1.0f, 1.0f, 1.0f); } return null; } /** * Sets the new grid scale * * @param scale * the new scale */ public void setScale(float scale) { this.scale = scale; if (this.scale == 0) { this.scale = 1; } resetPrepaint(); } /** * @param x * x position in screen coordinates * @param y * y position in screen coordinates * @return position in grid coordinates */ public Position calculatePosition(int x, int y) { float width = getStoneWidth(); float height = getStoneHeight(); return new Position(x / width, y / height); } /** * @return the width of a stone in the current scale in pixels */ public int getStoneWidth() { return even(DEFAULT_WIDTH * scale); } /** * @return the height of a stone in the current scale in pixels */ public int getStoneHeight() { return (int) (DEFAULT_WIDTH * scale / ASPECT_RATIO); } private BufferedImage prepaintStone(Color fg, Color bg, int value) { Rectangle r = new Rectangle(0, 0, getStoneWidth(), getStoneHeight()); BufferedImage img = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_RGB); Graphics2D g = img.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); paintStoneBackground(g, r, bg); if (value == 0) { paintJoker(g, r, fg); } else { paintStoneValue(g, r, fg, value); } paintCircle(g, r, bg); return img; } private BufferedImage getStoneImage(StoneColor color, int value, boolean selected, boolean hovered) { Map> stoneMap; if (selected) { if (hovered) { stoneMap = hoveredSelectedStones; } else { stoneMap = selectedStones; } } else { if (hovered) { stoneMap = hoveredStones; } else { stoneMap = defaultStones; } } BufferedImage image = stoneMap.get(color).get(value); if (image == null) { Color background = BACKGROUND_COLOR; Color foreground = getColor(color); if (selected) { background = background.darker(); foreground = foreground.darker(); } if (hovered) { background = hover(background); foreground = hover(foreground); } image = prepaintStone(foreground, background, value); stoneMap.get(color).put(value, image); } return image; } private void resetPrepaint() { defaultStones = new HashMap>(); selectedStones = new HashMap>(); hoveredStones = new HashMap>(); hoveredSelectedStones = new HashMap>(); for (StoneColor color : StoneColor.values()) { defaultStones.put(color, new HashMap()); selectedStones.put(color, new HashMap()); hoveredStones.put(color, new HashMap()); hoveredSelectedStones.put(color, new HashMap()); } } /** * @param scale * the scaling factor for the grid coordinates */ StonePainter(float scale) { setScale(scale); } private void paintStoneBackground(Graphics2D g, Rectangle r, Color background) { // Paint background g.setColor(background); g.fillRect(r.x, r.y, r.width, r.height); // Paint bevel border g.setColor(brighter(brighter(background))); g.fillRect(r.x, r.y, 1, r.height); g.setColor(brighter(background)); g.fillRect(r.x + 1, r.y + 1, 1, r.height - 2); g.setColor(brighter(brighter(background))); g.fillRect(r.x, r.y, r.width, 1); g.setColor(brighter(background)); g.fillRect(r.x + 1, r.y + 1, r.width - 2, 1); g.setColor(background.darker().darker()); g.fillRect(r.x + r.width - 1, r.y, 1, r.height); g.setColor(background.darker()); g.fillRect(r.x + r.width - 2, r.y + 1, 1, r.height - 2); g.setColor(background.darker().darker()); g.fillRect(r.x, r.y + r.height - 1, r.width, 1); g.setColor(background.darker()); g.fillRect(r.x + 1, r.y + r.height - 2, r.width - 2, 1); } private void paintJokerFace(Graphics2D g, Rectangle r) { Stroke oldStroke = g.getStroke(); g.setStroke(new BasicStroke(2)); g.drawOval(r.x, r.y, r.width, r.height); g.setStroke(new BasicStroke(1)); GeneralPath path = new GeneralPath(); // nose path.moveTo(r.x + 0.5f * r.width, r.y + 0.45f * r.height); path.lineTo(r.x + 0.53f * r.width, r.y + 0.6f * r.height); path.lineTo(r.x + 0.47f * r.width, r.y + 0.6f * r.height); path.closePath(); g.fill(path); path.reset(); // mouth, left path.moveTo(r.x + 0.23f * r.width, r.y + 0.75f * r.width); path.lineTo(r.x + 0.27f * r.width, r.y + 0.65f * r.width); // mouth, middle path.moveTo(r.x + 0.25f * r.width, r.y + 0.7f * r.width); path.lineTo(r.x + 0.5f * r.width, r.y + 0.8f * r.width); path.lineTo(r.x + 0.75f * r.width, r.y + 0.7f * r.width); // mouth, right path.moveTo(r.x + 0.77f * r.width, r.y + 0.75f * r.width); path.lineTo(r.x + 0.73f * r.width, r.y + 0.65f * r.width); g.draw(path); path.reset(); // left eye path.moveTo(r.x + 0.3f * r.width, r.y + 0.41f * r.height); path.lineTo(r.x + 0.375f * r.width, r.y + 0.375f * r.height); path.lineTo(r.x + 0.3f * r.width, r.y + 0.34f * r.height); path.lineTo(r.x + 0.225f * r.width, r.y + 0.375f * r.height); path.closePath(); g.draw(path); path.reset(); // right eye path.moveTo(r.x + 0.7f * r.width, r.y + 0.41f * r.height); path.lineTo(r.x + 0.625f * r.width, r.y + 0.375f * r.height); path.lineTo(r.x + 0.7f * r.width, r.y + 0.34f * r.height); path.lineTo(r.x + 0.775f * r.width, r.y + 0.375f * r.height); path.closePath(); g.draw(path); g.setStroke(oldStroke); } private void paintJoker(Graphics2D g, Rectangle r, Color color) { int faceSize = even(FACE_WIDTH * r.width); int pos = r.y + (int) (TEXT_POS * r.height); g.setColor(color); paintJokerFace(g, new Rectangle(r.x + r.width / 2 - faceSize / 2, pos - faceSize / 2, faceSize, faceSize)); } private void paintStoneValue(Graphics2D g, Rectangle r, Color color, int v) { int pos = r.y + (int) (TEXT_POS * r.height); g.setFont(new Font("SansSerif", Font.BOLD, r.height / 4)); FontMetrics fm = g.getFontMetrics(); String value = Integer.toString(v); Rectangle2D stringRect = fm.getStringBounds(value, g); if (scale > 1) { g.setColor(color.darker()); g.drawString(value, (int) (r.x + r.width / 2 - stringRect.getWidth() / 2) + 1, pos + (fm.getAscent() - fm.getDescent()) / 2 + 1); } g.setColor(color); g.drawString(value, (int) (r.x + r.width / 2 - stringRect.getWidth() / 2), pos + (fm.getAscent() - fm.getDescent()) / 2); } private void paintCircle(Graphics2D g, Rectangle r, Color background) { int size = even(r.width * CIRCLE_WIDTH); int pos = r.y + (int) (CIRCLE_POS * r.height); // Paint circle g.setColor(background.darker()); g.drawArc(r.x + r.width / 2 - size / 2, pos - size / 2, size, size, 50, 170); g.setColor(brighter(background)); g.drawArc((int) (r.x + r.width / 2 - size / 2), pos - size / 2, size, size, -130, 170); } /** * Paints a stone * * @param g * the graphics context to paint the stone on * @param stone * the stone to paint * @param p * the position of the stone * @param selected * if selected is true the stone will be painted darker * @param hovered * if hovered is true the stone will be painted brighter */ public void paintStone(Graphics2D g, Stone stone, Position p, boolean selected, boolean hovered) { int width = getStoneWidth(); int height = getStoneHeight(); int x = Math.round(p.getX() * width), y = Math.round(p.getY() * height); if (stone.isJoker()) { g.drawImage(getStoneImage(stone.getColor(), 0, selected, hovered), x, y, null); } else { g.drawImage(getStoneImage(stone.getColor(), stone.getValue(), selected, hovered), x, y, null); } } }