Google Tag Manager

Showing posts with label AffineTransform. Show all posts
Showing posts with label AffineTransform. Show all posts

2023/01/31

Create numbers for a 7-segment digital clock using Path2D

Code

class DigitalNumber {
  private final double isosceles;
  private final double dx;
  private final double dy;
  private final double width;
  private final double height;
  private final Rectangle rect = new Rectangle();
  public static final Color OFF = new Color(0xCC_CC_CC);
  public static final Color ON = Color.DARK_GRAY;
  public static final Color BGC = Color.LIGHT_GRAY;
  private final List<Set<Seg>> numbers = Arrays.asList(
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.E, Seg.F),
      EnumSet.of(Seg.B, Seg.C),
      EnumSet.of(Seg.A, Seg.B, Seg.D, Seg.E, Seg.G),
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.G),
      EnumSet.of(Seg.B, Seg.C, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.C, Seg.D, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.C, Seg.D, Seg.E, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.B, Seg.C),
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.E, Seg.F, Seg.G),
      EnumSet.of(Seg.A, Seg.B, Seg.C, Seg.D, Seg.F, Seg.G));
  private Set<Seg> led = EnumSet.noneOf(Seg.class);

  protected DigitalNumber(double dx, double dy, double isosceles) {
    this.isosceles = isosceles;
    this.dx = dx;
    this.dy = dy;
    this.width = 2d * isosceles;
    this.height = width + isosceles;
    rect.setLocation((int) dx, (int) dy);
    rect.setSize((int) (width + 3 * isosceles), (int) (height * 2));
  }

  public Rectangle getBounds() {
    return rect;
  }

  public void setNumber(int num) {
    led = numbers.get(num);
  }

  public void turnOffNumber() {
    led.clear();
  }

  public void drawNumber(Graphics2D g2) {
    EnumSet.allOf(Seg.class).forEach(s -> {
      g2.setColor(led.contains(s) ? ON : OFF);
      Shape seg = s.getShape(dx, dy, width, height, isosceles);
      g2.fill(seg);
      g2.setColor(BGC);
      g2.draw(seg);
    });
  }
}

enum Seg { A, B, C, D, E, F, G }

References

2022/11/30

Rotate JTableHeader column title string to display vertically

Code

class VerticalTableHeaderRenderer implements TableCellRenderer {
  private static final String ASCENDING = "Table.ascendingSortIcon";
  private static final String DESCENDING = "Table.descendingSortIcon";
  private final Icon ascendingIcon;
  private final Icon descendingIcon;
  private final EmptyIcon emptyIcon = new EmptyIcon();
  private final JPanel intermediate = new JPanel();
  private final JLabel label = new JLabel("", null, SwingConstants.LEADING);

  protected VerticalTableHeaderRenderer() {
    ascendingIcon = UIManager.getLookAndFeelDefaults().getIcon(ASCENDING);
    descendingIcon = UIManager.getLookAndFeelDefaults().getIcon(DESCENDING);
    emptyIcon.width = ascendingIcon.getIconWidth();
    emptyIcon.height = ascendingIcon.getIconHeight();
  }

  @Override public Component getTableCellRendererComponent(
      JTable table, Object value, boolean isSelected, boolean hasFocus,
      int row, int column) {
    UIManager.put(ASCENDING, emptyIcon);
    UIManager.put(DESCENDING, emptyIcon);
    TableCellRenderer r = table.getTableHeader().getDefaultRenderer();
    Component c = r.getTableCellRendererComponent(
      table, value, isSelected, hasFocus, row, column);
    UIManager.put(ASCENDING, ascendingIcon);
    UIManager.put(DESCENDING, descendingIcon);
    if (c instanceof JLabel) {
      JLabel l = (JLabel) c;
      l.setHorizontalAlignment(SwingConstants.CENTER);
      SortOrder sortOrder = getColumnSortOrder(table, column);
      Icon sortIcon;
      switch (sortOrder) {
        case ASCENDING:
          sortIcon = ascendingIcon;
          break;
        case DESCENDING:
          sortIcon = descendingIcon;
          break;
        default: // case UNSORTED:
          sortIcon = emptyIcon;
          break;
      }
      label.setText(l.getText());
      label.setIcon(new RotateIcon(sortIcon, 90));
      label.setHorizontalTextPosition(SwingConstants.LEFT);
      label.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
      // l.setIcon(new RotateIcon(new ComponentIcon(label), -90));
      l.setIcon(makeVerticalHeaderIcon(label));
      l.setText(null);
    }
    return c;
  }

  public static SortOrder getColumnSortOrder(JTable table, int column) {
    SortOrder rv = SortOrder.UNSORTED;
    if (table != null && table.getRowSorter() != null) {
      List<? extends RowSorter.SortKey> sortKeys =
        table.getRowSorter().getSortKeys();
      int mi = table.convertColumnIndexToModel(column);
      if (!sortKeys.isEmpty() && sortKeys.get(0).getColumn() == mi) {
        rv = sortKeys.get(0).getSortOrder();
      }
    }
    return rv;
  }

  private Icon makeVerticalHeaderIcon(Component label) {
    Dimension d = label.getPreferredSize();
    int w = d.height;
    int h = d.width;
    BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = (Graphics2D) bi.getGraphics();
    AffineTransform at = AffineTransform.getTranslateInstance(0, h);
    at.quadrantRotate(-1);
    g2.setTransform(at);
    SwingUtilities.paintComponent(g2, label, intermediate, 0, 0, h, w);
    g2.dispose();
    return new ImageIcon(bi);
  }
}

References

2022/08/31

Use AffineTransform to place Roman numerals on an analog clock face

Code

private final String[] arabicNumerals = {
  "12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
};
private final String[] romanNumerals = {
  "XII", "I", "II", "III", "IIII", "V", "VI", "VII", "VIII", "IX", "X", "XI"
};

private void paintClockNumbers(
    Graphics2D g2, double radius, double hourMarkerLen) {
  AffineTransform at = AffineTransform.getRotateInstance(0d);
  g2.setColor(Color.WHITE);
  Font font = g2.getFont();
  FontRenderContext frc = g2.getFontRenderContext();
  if (isRomanNumerals) {
    AffineTransform si = AffineTransform.getScaleInstance(1d, 2d);
    for (String txt : romanNumerals) {
      Shape s = getTextLayout(txt, font, frc).getOutline(si);
      Rectangle2D r = s.getBounds2D();
      double tx = r.getCenterX();
      double ty = radius - hourMarkerLen - r.getHeight() + r.getCenterY() * .5;
      Shape t = AffineTransform.getTranslateInstance(-tx, -ty)
                               .createTransformedShape(s);
      g2.fill(at.createTransformedShape(t));
      at.rotate(Math.PI / 6d);
    }
  } else {
    Point2D ptSrc = new Point2D.Double();
    for (String txt : arabicNumerals) {
      Shape s = getTextLayout(txt, font, frc).getOutline(null);
      Rectangle2D r = s.getBounds2D();
      double ty = radius - hourMarkerLen - r.getHeight() - r.getCenterY() * .5;
      ptSrc.setLocation(0d, -ty);
      Point2D pt = at.transform(ptSrc, null);
      double dx = pt.getX() - r.getCenterX();
      double dy = pt.getY() - r.getCenterY();
      // g2.drawString(txt, (float) dx, (float) dy);
      g2.fill(AffineTransform.getTranslateInstance(dx, dy)
        .createTransformedShape(s));
      at.rotate(Math.PI / 6d);
    }
  }
}

private static TextLayout getTextLayout(
    String txt, Font font, FontRenderContext frc) {
  return new TextLayout(txt, font, frc);
}

References

2021/05/31

Drawing Analog Clock Hands in JPanel using Timer

Code

class AnalogClock extends JPanel {
  protected LocalTime time = LocalTime.now(ZoneId.systemDefault());
  protected Timer timer = new Timer(200, e -> {
    time = LocalTime.now(ZoneId.systemDefault());
    repaint();
  });
  private transient HierarchyListener listener;

  @Override public void updateUI() {
    removeHierarchyListener(listener);
    super.updateUI();
    listener = e -> {
      if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
        if (e.getComponent().isShowing()) {
          // System.out.println("start");
          timer.start();
        } else {
          // System.out.println("stop");
          timer.stop();
        }
      }
    };
    addHierarchyListener(listener);
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    Rectangle rect = SwingUtilities.calculateInnerArea(this, null);
    g2.setColor(Color.BLACK);
    g2.fill(rect);
    float radius = Math.min(rect.width, rect.height) / 2f - 10f;
    g2.translate(rect.getCenterX(), rect.getCenterY());

    // Drawing the hour and minute markers
    float hourMarkerLen = radius / 6f - 10f;
    Shape hourMarker = new Line2D.Float(0f, hourMarkerLen - radius, 0f, -radius);
    Shape minuteMarker = new Line2D.Float(0f, hourMarkerLen / 2f - radius, 0f, -radius);
    AffineTransform at = AffineTransform.getRotateInstance(0d);
    g2.setStroke(new BasicStroke(2f));
    g2.setColor(Color.WHITE);
    for (int i = 0; i < 60; i++) {
      if (i % 5 == 0) {
        g2.draw(at.createTransformedShape(hourMarker));
      } else {
        g2.draw(at.createTransformedShape(minuteMarker));
      }
      at.rotate(Math.PI / 30d);
    }

    // Drawing the hour hand
    float hourHandLen = radius / 2f;
    Shape hourHand = new Line2D.Float(0f, 0f, 0f, -hourHandLen);
    double minuteRot = time.getMinute() * Math.PI / 30d;
    double hourRot = time.getHour() * Math.PI / 6d + minuteRot / 12d;
    g2.setStroke(new BasicStroke(8f));
    g2.setPaint(Color.LIGHT_GRAY);
    g2.draw(AffineTransform.getRotateInstance(hourRot).createTransformedShape(hourHand));

    // Drawing the minute hand
    float minuteHandLen = 5f * radius / 6f;
    Shape minuteHand = new Line2D.Float(0f, 0f, 0f, -minuteHandLen);
    g2.setStroke(new BasicStroke(4f));
    g2.setPaint(Color.WHITE);
    g2.draw(AffineTransform.getRotateInstance(minuteRot).createTransformedShape(minuteHand));

    // Drawing the second hand
    float r = radius / 6f;
    float secondHandLen = radius - r;
    Shape secondHand = new Line2D.Float(0f, r, 0f, -secondHandLen);
    double secondRot = time.getSecond() * Math.PI / 30d;
    g2.setPaint(Color.RED);
    g2.setStroke(new BasicStroke(1f));
    g2.draw(AffineTransform.getRotateInstance(secondRot).createTransformedShape(secondHand));
    g2.fill(new Ellipse2D.Float(-r / 4f, -r / 4f, r / 2f, r / 2f));

    g2.dispose();
  }
}

References

2020/08/31

Overlap the JLabel with a ribbon and slanted string in the corner

Code

class BadgeLabel extends JLabel {
  private final Color ribbonColor = new Color(0xAA_FF_64_00, true);
  private final String ribbonText;

  protected BadgeLabel(Icon image) {
    super(image);
    this.ribbonText = null;
  }

  protected BadgeLabel(Icon image, String ribbonText) {
    super(image);
    this.ribbonText = ribbonText;
  }

  @Override public void updateUI() {
    super.updateUI();
    setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
    setVerticalAlignment(SwingConstants.CENTER);
    setVerticalTextPosition(SwingConstants.BOTTOM);
    setHorizontalAlignment(SwingConstants.CENTER);
    setHorizontalTextPosition(SwingConstants.CENTER);
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setPaint(Color.WHITE);
    g2.fill(getShape());
    super.paintComponent(g);

    if (ribbonText != null) {
      Dimension d = getSize();
      float fontSize = 10f;
      int cx = (d.width - (int) fontSize) / 2;
      double theta = Math.toRadians(45d);

      Font font = g2.getFont().deriveFont(fontSize);
      g2.setFont(font);
      FontRenderContext frc = new FontRenderContext(null, true, true);

      Shape ribbon = new Rectangle2D.Double(cx, -fontSize, d.width, fontSize);
      AffineTransform at = AffineTransform.getRotateInstance(theta, cx, 0);
      g2.setPaint(ribbonColor);
      g2.fill(at.createTransformedShape(ribbon));

      TextLayout tl = new TextLayout(ribbonText, font, frc);
      g2.setPaint(Color.WHITE);
      Rectangle2D r = tl.getOutline(null).getBounds2D();
      double dx = cx + (d.width - cx) / Math.sqrt(2d) - r.getWidth() / 2d;
      double dy = fontSize / 2d + r.getY();
      AffineTransform tx = AffineTransform.getTranslateInstance(dx, dy);
      Shape s = tl.getOutline(tx);
      g2.fill(at.createTransformedShape(s));
    }
    g2.dispose();
  }

  @Override public boolean isOpaque() {
    return false;
  }

  protected Shape getShape() {
    Dimension d = getSize();
    double r = d.width / 2d;
    return new RoundRectangle2D.Double(
        0d, 0d, d.width - 1d, d.height - 1d, r, r);
  }
}

Explanation

JLabel
  • Override JLabel#isOpaque() to make it transparent
  • Overrides JLabel#paintComponent(...) to draw a round rectangle for the background, then the original icon for `JLabel`, then the ribbon in the upper right corner, then the ribbon string
Ribbon
  • Create a Rectangle for the ribbon so that the lower left of the ribbon is positioned near the middle of the x axis of the parent JLabel
  • Rotate the lower left corner of the ribbon rectangle 45 degrees(Math.toRadians(45d)) about the origin
Ribbon string
  • Convert the Ribbon string to Shape using the TextLayout.getOutline(...) method
  • Rotate this Shape 45 degrees from its bottom left

References

2020/03/31

Rotate the tab title of JTabbedPane vertically by 90 degrees

Code

private Icon makeVerticalTabIcon(String title, Icon icon, boolean clockwise) {
  JLabel label = new JLabel(title, icon, SwingConstants.LEADING);
  label.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
  Dimension d = label.getPreferredSize();
  int w = d.height;
  int h = d.width;
  BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
  Graphics2D g2 = (Graphics2D) bi.getGraphics();
  AffineTransform at = clockwise
      ? AffineTransform.getTranslateInstance(w, 0)
      : AffineTransform.getTranslateInstance(0, h);
  at.quadrantRotate(clockwise ? 1 : -1);
  g2.setTransform(at);
  SwingUtilities.paintComponent(g2, label, this, 0, 0, d.width, d.height);
  g2.dispose();
  return new ImageIcon(bi);
}

References

2017/03/30

Make a condensed Font and use it with JTextArea

Code

Font font = new Font(Font.MONOSPACED, Font.PLAIN, 18).deriveFont(
   AffineTransform.getScaleInstance(.9, 1d));
textArea.setFont(font);

References

2015/06/30

An Image inside a JScrollPane zooming by mouse wheel and panning by mouse click to drag

Code

class ZoomAndPanePanel extends JPanel {
  private final AffineTransform zoomTransform = new AffineTransform();
  private final transient Image img;
  private final Rectangle imgrect;
  private transient ZoomHandler handler;
  private transient DragScrollListener listener;

  protected ZoomAndPanePanel(Image img) {
    super();
    this.img = img;
    this.imgrect = new Rectangle(img.getWidth(this), img.getHeight(this));
  }

  @Override protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setPaint(new Color(0x55_FF_00_00, true));
    Rectangle r = new Rectangle(500, 140, 150, 150);

    // use: AffineTransform#concatenate(...) and Graphics2D#setTransform(...)
    // AffineTransform at = g2.getTransform();
    // at.concatenate(zoomTransform);
    // g2.setTransform(at);
    // g2.drawImage(img, 0, 0, this);
    // g2.fill(r);

    // or use: Graphics2D#drawImage(Image, AffineTransform, ImageObserver)
    g2.drawImage(img, zoomTransform, this);
    // or: g2.drawRenderedImage((RenderedImage) img, zoomTransform);
    g2.fill(zoomTransform.createTransformedShape(r));

    // BAD EXAMPLE
    // g2.setTransform(zoomTransform);
    // g2.drawImage(img, 0, 0, this);

    g2.dispose();
  }

  @Override public Dimension getPreferredSize() {
    Rectangle r = zoomTransform.createTransformedShape(imgrect).getBounds();
    return new Dimension(r.width, r.height);
  }

  @Override public void updateUI() {
    removeMouseListener(listener);
    removeMouseMotionListener(listener);
    removeMouseWheelListener(handler);
    super.updateUI();
    listener = new DragScrollListener();
    addMouseListener(listener);
    addMouseMotionListener(listener);
    handler = new ZoomHandler();
    addMouseWheelListener(handler);
  }

  protected class ZoomHandler extends MouseAdapter {
    private static final double ZOOM_MULTIPLICATION_FACTOR = 1.2;
    private static final int MIN_ZOOM = -10;
    private static final int MAX_ZOOM = 10;
    private static final int EXTENT = 1;
    private final BoundedRangeModel zoomRange = new DefaultBoundedRangeModel(
        0, EXTENT, MIN_ZOOM, MAX_ZOOM + EXTENT);
    @Override public void mouseWheelMoved(MouseWheelEvent e) {
      int dir = e.getWheelRotation();
      int z = zoomRange.getValue();
      zoomRange.setValue(z + EXTENT * (dir > 0 ? -1 : 1));
      if (z != zoomRange.getValue()) {
        Component c = e.getComponent();
        Container p = SwingUtilities.getAncestorOfClass(JViewport.class, c);
        if (p instanceof JViewport) {
          JViewport vport = (JViewport) p;
          Rectangle ovr = vport.getViewRect();
          double s = dir > 0 ? 1d / ZOOM_MULTIPLICATION_FACTOR
                             : ZOOM_MULTIPLICATION_FACTOR;
          zoomTransform.scale(s, s);
          // double s = 1d + zoomRange.getValue() * .1;
          // zoomTransform.setToScale(s, s);
          Rectangle nvr = AffineTransform.getScaleInstance(s, s)
                                         .createTransformedShape(ovr)
                                         .getBounds();
          Point vp = nvr.getLocation();
          vp.translate(
              (nvr.width - ovr.width) / 2,
              (nvr.height - ovr.height) / 2);
          vport.setViewPosition(vp);
          c.revalidate();
          c.repaint();
        }
      }
    }
  }
}

References