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 { enum StoneState { NORMAL, SELECTED, INVALID } private static final double ASPECT_RATIO = 0.75f; private static final double DEFAULT_WIDTH = 40; private static final double TEXT_POS = 0.275f; private static final double FACE_WIDTH = 0.475f; private static final double CIRCLE_POS = 0.725f; private static final double CIRCLE_WIDTH = 0.45f; private static final Color BACKGROUND_COLOR = new Color(0.9f, 0.9f, 0.6f); private static final double BRIGHTER_SCALE = 1.15f; private static final double HOVER_RATIO = 0.7f; private static final double REDDEN_RATIO = 0.3f; private Map>> defaultStones; private Map>> hoveredStones; /** * The width of one pixel in the scale of 1.0 */ static final double WIDTH_SCALE = 1 / DEFAULT_WIDTH; /** * The height of one pixel in the scale of 1.0 */ static final double HEIGHT_SCALE = ASPECT_RATIO / DEFAULT_WIDTH; private double scale; private static int even(double 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 redden(Color color) { int r = (int) (color.getRed() * REDDEN_RATIO + 255 * (1 - REDDEN_RATIO)); int g = (int) (color.getGreen() * REDDEN_RATIO + 128 * (1 - REDDEN_RATIO)); int b = (int) (color.getBlue() * REDDEN_RATIO + 128 * (1 - REDDEN_RATIO)); return new Color(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255 : b); } static Color getColor(StoneColor color) { switch (color) { case BLACK: return new Color(0.0f, 0.0f, 0.0f); 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 AQUA: return new Color(0.0f, 0.85f, 0.75f); case GREEN: return new Color(0.0f, 0.65f, 0.0f); case VIOLET: return new Color(0.75f, 0.325f, 0.75f); case GRAY: return new Color(0.5f, 0.5f, 0.5f); } return null; } /** * Sets the new grid scale * * @param scale * the new scale */ void setScale(double 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 */ Position calculatePosition(int x, int y) { double width = getStoneWidth(); double height = getStoneHeight(); return new Position(x / width, y / height); } /** * @return the width of a stone in the current scale in pixels */ int getStoneWidth() { return Math.max(even(DEFAULT_WIDTH * scale), 1); } /** * @return the height of a stone in the current scale in pixels */ int getStoneHeight() { return Math.max((int) (DEFAULT_WIDTH * scale / ASPECT_RATIO), 1); } 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, StoneState state, boolean hovered) { Map> stoneMap; if (hovered) { stoneMap = hoveredStones.get(state); } else { stoneMap = defaultStones.get(state); } BufferedImage image = stoneMap.get(color).get(value); if (image == null) { Color background = BACKGROUND_COLOR; Color foreground = getColor(color); if (state == StoneState.SELECTED) { background = background.darker(); foreground = foreground.darker(); } else if (state == StoneState.INVALID) { background = redden(background); foreground = redden(foreground); } 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>>(); hoveredStones = new HashMap>>(); for (StoneState state : StoneState.values()) { Map> defaultStateStones = new HashMap>(); Map> hoveredStateStones = new HashMap>(); defaultStones.put(state, defaultStateStones); hoveredStones.put(state, hoveredStateStones); for (StoneColor color : StoneColor.values()) { defaultStateStones.put(color, new HashMap()); hoveredStateStones.put(color, new HashMap()); } } } /** * @param scale * the scaling factor for the grid coordinates */ StonePainter(double 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 = (v > 0) ? Integer.toString(v) : Character .toString((char) (-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 state * if the stone is selected the stone will be painted darker, if * it is invalid it will be painted in red * @param hovered * if hovered is true the stone will be painted brighter */ void paintStone(Graphics2D g, Stone stone, Position p, StoneState state, boolean hovered) { int width = getStoneWidth(); int height = getStoneHeight(); int x = (int) Math.round(p.getX() * width), y = (int) Math.round(p .getY() * height); if (stone.isJoker()) { g.drawImage(getStoneImage(stone.getColor(), 0, state, hovered), x, y, null); } else { g.drawImage( getStoneImage(stone.getColor(), stone.getValue(), state, hovered), x, y, null); } } }