Category Archives: Java 2D

Java 2D – Trò chơi xếp hình – Tetris

Trong phần này chúng ta sẽ viết một game rất nổi tiếng đó là game Xếp hình 😀 (Tetris)

Tetris

Game được thiết kế và phát triển lần đầu tiên vào năm 1985 bởi một lập trình viên người Nga tên là Alexey Pajitnov. Kể từ đó, game này được phát triển thành nhiều phiên bản trên nhiều hệ máy khác nhau.

Trong game có 7 khối gạch có dạng hình chữ S, hình chữ Z, hình chữ T, hình chữ L, hình đường thẳng, hình chữ L ngược và hình vuông. Mỗi khối gạch được tạo nên bởi 4 hình vuông. Các khối gạch sẽ rơi từ trên xuống. Người chơi phải di chuyển và xoay các khối gạch sao cho chúng vừa khít với nhau, nếu các khối gạch lấp đầy một đường ngang thì người chơi ghi điểm. Trò chơi kết thúc khi các khối gạch chồng lên nhau quá cao.

tetrominoes

 

Mô tả game

Kích thước của các đối tượng trong game này được tính theo đơn vị 1 hình vuông chứ không theo pixel nữa. Kích thước của cửa sổ giờ đây là 10×22 hình vuông. Kích thước thật của các hình vuông được tính động theo kích thước cửa sổ. Tức là nếu kích thước cửa sổ là 200×400 pixel thì kích thước thật của môi hình vuông là 200×400 / 10×22 là khoảng 20×18 pixel (nhưng chúng ta cũng không quan tâm đến con số này).

Mỗi lần chạy chương trình là game sẽ tự động chạy ngay, tuy nhiên chúng ta có thể cho tạm dừng game bằng cách bấm phím P. Nếu muốn khối gạch rơi nhanh xuống đáy ngay lập tức thì bấm phím Space. Bấm phím D sẽ làm cho khối gạch rơi xuống thêm 1 hàng, có thể dùng phím này để tăng tốc độ rơi thêm một chút. Tốc độ game không thay đổi. Điểm sẽ là số hàng đã được loại bỏ.

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;


public class Tetris extends JFrame {

    private JLabel statusbar;

    public Tetris() {
        
        initUI();
   }
    
   private void initUI() {

        statusbar = new JLabel(" 0");
        add(statusbar, BorderLayout.SOUTH);
        Board board = new Board(this);
        add(board);
        board.start();

        setSize(200, 400);
        setTitle("Tetris");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);       
   }

   public JLabel getStatusBar() {
       
       return statusbar;
   }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                
                Tetris game = new Tetris();
                game.setVisible(true);
            }
        });                
    } 
}

Toàn bộ game này chúng ta viết trong 3 file, đầu tiên là file Tetris.java, file này là file main, làm các công việc khởi tạo cửa sổ, khởi tạo kích thước, chạy luồng main…

board.start();

Chúng ta gọi phương thức start() để khi chương trình chạy thì game cũng chạy luôn.

import java.util.Random;

public class Shape {

    protected enum Tetrominoes { NoShape, ZShape, SShape, LineShape, 
               TShape, SquareShape, LShape, MirroredLShape };

    private Tetrominoes pieceShape;
    private int coords[][];
    private int[][][] coordsTable;


    public Shape() {

        coords = new int[4][2];
        setShape(Tetrominoes.NoShape);
    }

    public void setShape(Tetrominoes shape) {

         coordsTable = new int[][][] {
            { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
            { { 0,-1 },   { 0, 0 },   {-1, 0 },   {-1, 1 } },
            { { 0,-1 },   { 0, 0 },   { 1, 0 },   { 1, 1 } },
            { { 0,-1 },   { 0, 0 },   { 0, 1 },   { 0, 2 } },
            { {-1, 0 },   { 0, 0 },   { 1, 0 },   { 0, 1 } },
            { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
            { {-1,-1 },   { 0,-1 },   { 0, 0 },   { 0, 1 } },
            { { 1,-1 },   { 0,-1 },   { 0, 0 },   { 0, 1 } }
        };

        for (int i = 0; i < 4 ; i++) {
            
            for (int j = 0; j < 2; ++j) {
                
                coords[i][j] = coordsTable[shape.ordinal()][i][j];
            }
        }
        
        pieceShape = shape;
    }

    private void setX(int index, int x) { coords[index][0] = x; }
    private void setY(int index, int y) { coords[index][1] = y; }
    public int x(int index) { return coords[index][0]; }
    public int y(int index) { return coords[index][1]; }
    public Tetrominoes getShape()  { return pieceShape; }

    public void setRandomShape() {
        
        Random r = new Random();
        int x = Math.abs(r.nextInt()) % 7 + 1;
        Tetrominoes[] values = Tetrominoes.values(); 
        setShape(values[x]);
    }

    public int minX() {
        
      int m = coords[0][0];
      
      for (int i=0; i < 4; i++) {
          
          m = Math.min(m, coords[i][0]);
      }
      
      return m;
    }


    public int minY() {
        
      int m = coords[0][1];
      
      for (int i=0; i < 4; i++) {
          
          m = Math.min(m, coords[i][1]);
      }
      
      return m;
    }

    public Shape rotateLeft() {
        
        if (pieceShape == Tetrominoes.SquareShape)
            return this;

        Shape result = new Shape();
        result.pieceShape = pieceShape;

        for (int i = 0; i < 4; ++i) {
            
            result.setX(i, y(i));
            result.setY(i, -x(i));
        }
        
        return result;
    }

    public Shape rotateRight() {
        
        if (pieceShape == Tetrominoes.SquareShape)
            return this;

        Shape result = new Shape();
        result.pieceShape = pieceShape;

        for (int i = 0; i < 4; ++i) {

            result.setX(i, -y(i));
            result.setY(i, x(i));
        }
        
        return result;
    }
}

Tiếp theo là file Shape.java, file này lưu thông tin về các khối gạch.

protected enum Tetrominoes { NoShape, ZShape, SShape, LineShape, 
            TShape, SquareShape, LShape, MirroredLShape };

Chúng ta định nghĩa tên các khối gạch trong enum Tetrominoes. Ngoài 7 khối cơ bản chúng ta tạo thêm 1 mối đặc biệt nữa là NoShape, khối này không có hình thù.

public Shape() {

    coords = new int[4][2];
    setShape(Tetrominoes.NoShape);
}

Trong phương thức khởi tạo, chúng ta tạo mảng coords để lưu giữ tọa độ thật của khối gạch.

coordsTable = new int[][][] {
   { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
   { { 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 } },
   { { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } },
   { { 0, -1 },  { 0, 0 },   { 0, 1 },   { 0, 2 } },
   { { -1, 0 },  { 0, 0 },   { 1, 0 },   { 0, 1 } },
   { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
   { { -1, -1 }, { 0, -1 },  { 0, 0 },   { 0, 1 } },
   { { 1, -1 },  { 0, -1 },  { 0, 0 },   { 0, 1 } }
};

Mảng coordsTable lưu tọa độ của các hình vuông tạo nên từng loại khối gạch.

for (int i = 0; i < 4 ; i++) {
    
    for (int j = 0; j < 2; ++j) {
        
        coords[i][j] = coordsTable[shape.ordinal()][i][j];
    }
}

Phương thức setShape() sẽ quy định cho đối tượng khối gạch đó là loại nào, tham số của phương thức này là một giá trị của enum Tetrominoes, từ tham số đó chúng ta lấy số thứ tự của thuộc tính đó bằng phương thức ordinal(), rồi dùng số thứ tự đó để lấy tọa độ trong mảng coordsTable và chép vào mảng coords.

Lý do tại sao tọa độ ở đây chỉ là 0, 1 và -1 thì bạn xem hình dưới đây để hình dung. Ví dụ { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, -1 } , sẽ đại diện cho khối gạch hình chữ S.

Coordinates
public Shape rotateLeft() {
    
    if (pieceShape == Tetrominoes.SquareShape)
        return this;

    Shape result = new Shape();
    result.pieceShape = pieceShape;

    for (int i = 0; i < 4; ++i) {
        
        result.setX(i, y(i));
        result.setY(i, -x(i));
    }
    
    return result;
}

Phương thức rotateLeft() làm nhiệm vụ xoay khối gạch qua bên trái. Ở đây mình sẽ không giải thích vì sao chúng ta lại có đoạn code xoay như trên vì rất khó giải thích, bạn cứ nhìn hình ở trên rồi tự suy ra sẽ thấy, nếu thắc mắc chỗ nào thì hãy comment ở cuối bài này và mình sẽ giải thích. Phương thức rotateRight() cũng tương tự là xoay khối gạch sang bên phải.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

import Shape.Tetrominoes;

public class Board extends JPanel 
        implements ActionListener {

    private final int BoardWidth = 10;
    private final int BoardHeight = 22;

    private Timer timer;
    private boolean isFallingFinished = false;
    private boolean isStarted = false;
    private boolean isPaused = false;
    private int numLinesRemoved = 0;
    private int curX = 0;
    private int curY = 0;
    private JLabel statusbar;
    private Shape curPiece;
    private Tetrominoes[] board;

    public Board(Tetris parent) {

        initBoard(parent);
    }
    
    private void initBoard(Tetris parent) {
        
       setFocusable(true);
       curPiece = new Shape();
       timer = new Timer(400, this);
       timer.start(); 

       statusbar =  parent.getStatusBar();
       board = new Tetrominoes[BoardWidth * BoardHeight];
       addKeyListener(new TAdapter());
       clearBoard();          
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        
        if (isFallingFinished) {
            
            isFallingFinished = false;
            newPiece();
        } else {
            
            oneLineDown();
        }
    }

    private int squareWidth() { return (int) getSize().getWidth() / BoardWidth; }
    private int squareHeight() { return (int) getSize().getHeight() / BoardHeight; }
    private Tetrominoes shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }


    public void start()  {
        
        if (isPaused)
            return;

        isStarted = true;
        isFallingFinished = false;
        numLinesRemoved = 0;
        clearBoard();

        newPiece();
        timer.start();
    }

    private void pause()  {
        
        if (!isStarted)
            return;

        isPaused = !isPaused;
        
        if (isPaused) {
            
            timer.stop();
            statusbar.setText("paused");
        } else {
            
            timer.start();
            statusbar.setText(String.valueOf(numLinesRemoved));
        }
        
        repaint();
    }
    
    private void doDrawing(Graphics g) {
        
        Dimension size = getSize();
        int boardTop = (int) size.getHeight() - BoardHeight * squareHeight();

        for (int i = 0; i < BoardHeight; ++i) {
            
            for (int j = 0; j < BoardWidth; ++j) {
                
                Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
                
                if (shape != Tetrominoes.NoShape)
                    drawSquare(g, 0 + j * squareWidth(),
                               boardTop + i * squareHeight(), shape);
            }
        }

        if (curPiece.getShape() != Tetrominoes.NoShape) {
            
            for (int i = 0; i < 4; ++i) { 
                int x = curX + curPiece.x(i); 
                int y = curY - curPiece.y(i); 
                drawSquare(g, 0 + x * squareWidth(), boardTop + (BoardHeight - y - 1) * squareHeight(), curPiece.getShape()); 
            } 
        } 
    } 
    
    @Override 
    public void paintComponent(Graphics g) { 
        super.paintComponent(g); 
        doDrawing(g); 
    } 

    private void dropDown() { 
        int newY = curY; 
        while (newY > 0) {
            
            if (!tryMove(curPiece, curX, newY - 1))
                break;
            --newY;
        }
        
        pieceDropped();
    }

    private void oneLineDown()  {
        
        if (!tryMove(curPiece, curX, curY - 1))
            pieceDropped();
    }


    private void clearBoard() {
        
        for (int i = 0; i < BoardHeight * BoardWidth; ++i)
            board[i] = Tetrominoes.NoShape;
    }

    private void pieceDropped() {
        
        for (int i = 0; i < 4; ++i) {
            
            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            board[(y * BoardWidth) + x] = curPiece.getShape();
        }

        removeFullLines();

        if (!isFallingFinished)
            newPiece();
    }

    private void newPiece()  {
        
        curPiece.setRandomShape();
        curX = BoardWidth / 2 + 1;
        curY = BoardHeight - 1 + curPiece.minY();

        if (!tryMove(curPiece, curX, curY)) {
            
            curPiece.setShape(Tetrominoes.NoShape);
            timer.stop();
            isStarted = false;
            statusbar.setText("game over");
        }
    }

    private boolean tryMove(Shape newPiece, int newX, int newY) {
        
        for (int i = 0; i < 4; ++i) {
            
            int x = newX + newPiece.x(i);
            int y = newY - newPiece.y(i);
            
            if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
                return false;
            
            if (shapeAt(x, y) != Tetrominoes.NoShape)
                return false;
        }

        curPiece = newPiece;
        curX = newX;
        curY = newY;

        repaint();

        return true;
    }

    private void removeFullLines() {
        
        int numFullLines = 0;

        for (int i = BoardHeight - 1; i >= 0; --i) {
            boolean lineIsFull = true;

            for (int j = 0; j < BoardWidth; ++j) {
                if (shapeAt(j, i) == Tetrominoes.NoShape) {
                    lineIsFull = false;
                    break;
                }
            }

            if (lineIsFull) {
                ++numFullLines;
                for (int k = i; k < BoardHeight - 1; ++k) {
                    for (int j = 0; j < BoardWidth; ++j) 
                        board[(k * BoardWidth) + j] = shapeAt(j, k + 1); 
                } 
            } 
        } 
        if (numFullLines > 0) {
            
            numLinesRemoved += numFullLines;
            statusbar.setText(String.valueOf(numLinesRemoved));
            isFallingFinished = true;
            curPiece.setShape(Tetrominoes.NoShape);
            repaint();
        }
     }

    private void drawSquare(Graphics g, int x, int y, Tetrominoes shape)  {
        
        Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102), 
            new Color(102, 204, 102), new Color(102, 102, 204), 
            new Color(204, 204, 102), new Color(204, 102, 204), 
            new Color(102, 204, 204), new Color(218, 170, 0)
        };

        Color color = colors[shape.ordinal()];

        g.setColor(color);
        g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);

        g.setColor(color.brighter());
        g.drawLine(x, y + squareHeight() - 1, x, y);
        g.drawLine(x, y, x + squareWidth() - 1, y);

        g.setColor(color.darker());
        g.drawLine(x + 1, y + squareHeight() - 1,
                         x + squareWidth() - 1, y + squareHeight() - 1);
        g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
                         x + squareWidth() - 1, y + 1);

    }

    class TAdapter extends KeyAdapter {
        
         @Override
         public void keyPressed(KeyEvent e) {

             if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape) {  
                 return;
             }

             int keycode = e.getKeyCode();

             if (keycode == 'p' || keycode == 'P') {
                 pause();
                 return;
             }

             if (isPaused)
                 return;

             switch (keycode) {
                 
             case KeyEvent.VK_LEFT:
                 tryMove(curPiece, curX - 1, curY);
                 break;
                 
             case KeyEvent.VK_RIGHT:
                 tryMove(curPiece, curX + 1, curY);
                 break;
                 
             case KeyEvent.VK_DOWN:
                 tryMove(curPiece.rotateRight(), curX, curY);
                 break;
                 
             case KeyEvent.VK_UP:
                 tryMove(curPiece.rotateLeft(), curX, curY);
                 break;
                 
             case KeyEvent.VK_SPACE:
                 dropDown();
                 break;
                 
             case 'd':
                 oneLineDown();
                 break;
                 
             case 'D':
                 oneLineDown();
                 break;
             }
         }
     }
}

Cuối cùng chúng ta có file Board.java. File này chịu trách nhiệm điều khiển các khối gạch cũng như xử lý các hoạt động diễn ra trong game.

...
private boolean isFallingFinished = false;
private boolean isStarted = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
...

Ý nghĩa của các thuộc tính trên như sau:

  • isFallingFinished: cho biết khối gạch hiện tại đã rơi xuống xong chưa, nếu rồi thì tạo một khối gạch mới
  • isStartedisPaused: dùng để kiểm tra trạng thái hiện tại của game là đang chơi hay tạm dừng hay đã kết thúc
  • numLinesRemoved: lưu số lượng các đường ngang đã bị xóa
  • curXcurY: lưu tọa độ hiện tại của khối gạch đang rơi
setFocusable(true);

Chúng ta phải gọi phương thức setFocusable() một cách tường minh để bàn phím có thể nhận sự kiện bấm phím.

timer = new Timer(400, this);
timer.start(); 

Đối tượng Timer sẽ giải phóng sự kiện sau mỗi 400 mili-giây và gọi đến phương thức actionPerformed(), có thể hiểu đây là vòng lặp của game.

@Override
public void actionPerformed(ActionEvent e) {
    
    if (isFallingFinished) {
        
        isFallingFinished = false;
        newPiece();
    } else {
        
        oneLineDown();
    }
}

Trong phương thức actionPerformed(), chúng ta kiểm tra xem khối gạch đang rơi đã rơi xong chưa, nếu chưa thì chúng ta gọi phương thức oneLineDown() để cho khối gạch này rơi thêm 1 hàng, nếu đã rơi xong rồi thì chúng ta gọi phương thức newPiece() để tạo một khối gạch mới.

Trong phương thức doDrawing() chúng ta vẽ các đối tượng lên màn hình.

for (int i = 0; i < BoardHeight; ++i) {
    
    for (int j = 0; j < BoardWidth; ++j) {
        
        Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
        
        if (shape != Tetrominoes.NoShape)
            drawSquare(g, 0 + j * squareWidth(),
                        boardTop + i * squareHeight(), shape);
    }
}

Đầu tiên chúng ta vẽ các khối gạch đã rơi xuống đáy màn hình. Các khối gạch đã rơi xong sẽ được lưu lại vị trí trong mảng board, chúng ta lấy lại thông tin đó thông qua phương thức shapeAt().

if (curPiece.getShape() != Tetrominoes.NoShape) {
    
    for (int i = 0; i < 4; ++i) {
        
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        drawSquare(g, 0 + x * squareWidth(),
                    boardTop + (BoardHeight - y - 1) * squareHeight(),
                    curPiece.getShape());
    }
}

Tiếp theo chúng ta vẽ khối gạch đang rơi.

private void dropDown() {
    
    int newY = curY;
    
    while (newY > 0) {
        
        if (!tryMove(curPiece, curX, newY - 1))
            break;
        --newY;
    }
    
    pieceDropped();
}

Nếu bấm phím space, khối gạch sẽ được rơi xuống đáy ngay lập tức bằng cách cho chạy một vòng lặp riêng.

private void clearBoard() {
    
    for (int i = 0; i < BoardHeight * BoardWidth; ++i)
        board[i] = Tetrominoes.NoShape;
}

Phương thức clearBoard() có nhiệm vụ lấp đầy cửa sổ bằng khối gạch NoShape, chúng ta làm thế để phục vụ việc kiểm tra va chạm sau này.

private void pieceDropped() {
    
    for (int i = 0; i < 4; ++i) {
        
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        board[(y * BoardWidth) + x] = curPiece.getShape();
    }

    removeFullLines();

    if (!isFallingFinished)
        newPiece();
}

Phương thức pieceDropped() được gọi mỗi lần có một khối gạch đã rơi xong (chạm đáy hoặc chạm một khối gạch khác đã nằm sẵn), phương thức này sẽ thêm tọa độ mới của các khối gạch trong mảng board sau đó tiến hành kiểm tra xem có hàng nào full hay không thông qua phương thức removeFullLines(), cuối cùng khởi tạo một khối gạch mới (chúng ta thêm câu lệnh if để kiểm tra xem khối gạch chắc chắn đã rơi xong chưa).

private void newPiece()  {
    
    curPiece.setRandomShape();
    curX = BoardWidth / 2 + 1;
    curY = BoardHeight - 1 + curPiece.minY();

    if (!tryMove(curPiece, curX, curY)) {
        
        curPiece.setShape(Tetrominoes.NoShape);
        timer.stop();
        isStarted = false;
        statusbar.setText("game over");
    }
}

Phương thức newPiece() khởi tạo một khối gạch mới và tính toán vị trí cho khối gạch. Nếu vị trí khởi tạo của khối gạch không đủ vì phần gạch bên dưới đã quá cao thì chúng ta tiến hành cho game kết thúc.

private boolean tryMove(Shape newPiece, int newX, int newY) {
    
    for (int i = 0; i < 4; ++i) {
        
        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);
        
        if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
            return false;
        
        if (shapeAt(x, y) != Tetrominoes.NoShape)
            return false;
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;

    repaint();

    return true;
}

Phương thức tryMove() sẽ di chuyển khối gạch, phương thức này trả về false nếu khối gạch không thể di chuyển được do đã chạm biên cửa sổ hoặc chạm các khối gạch khác.

int numFullLines = 0;

for (int i = BoardHeight - 1; i >= 0; --i) {
    boolean lineIsFull = true;

    for (int j = 0; j < BoardWidth; ++j) {
        if (shapeAt(j, i) == Tetrominoes.NoShape) {
            lineIsFull = false;
            break;
        }
    }

    if (lineIsFull) {
        ++numFullLines;
        for (int k = i; k < BoardHeight - 1; ++k) {
            for (int j = 0; j < BoardWidth; ++j)
                    board[(k * BoardWidth) + j] = shapeAt(j, k + 1);
        }
    }
}

Phương thức removeFullLines() kiểm tra xem có hàng nào đã full hay chưa, nếu có thì chúng ta tăng một biến đếm lên để dùng vào việc tính điểm, đồng thời chúng ta kéo các hàng phía trên xuống một hàng, để đảm bảo tất cả các hàng full đều bị xóa.

Các khối gạch của game có màu khác nhau. Các khối gạch được vẽ trong phương thức drawSquare().

g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);

Chúng ta vẽ các hình vuông có cạnh bên trái và cạnh trên màu sáng hơn với 2 cạnh còn lại để tạo hiệu ứng khối 3D.

Sự kiện bàn phím sẽ được nhận thông qua phương thức keyPressed() của lớp KeyAdapter.

case KeyEvent.VK_LEFT:
    tryMove(curPiece, curX - 1, curY);
    break;

Chẳng hạn như nếu bấm phím mũi tên trái, chúng ta thử di chuyển khối gạch sang bên trái.

Untitled

Java 2D – Xử lý sự kiện chuột

Trong phần này chúng ta sẽ làm một số thao tác với chuột.

Ví dụ 1

Bất cứ hình nào trong Java cũng đều kế thừa từ lớp Shape, lớp này có phương thức contains() dùng để kiểm tra xem một điểm có nằm trong phạm vi của hình đó hay không. Trong ví dụ này chúng ta sẽ sử dụng đến phương thức này,

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Rectangle2D rect;
    private Ellipse2D ellipse;
    private float alpha_rectangle;
    private float alpha_ellipse;

    public Surface() {
        
        initSurface();
    }
    
    private void initSurface() {
        
        addMouseListener(new HitTestAdapter());

        rect = new Rectangle2D.Float(20f, 20f, 80f, 50f);
        ellipse = new Ellipse2D.Float(120f, 30f, 60f, 60f);

        alpha_rectangle = 1f;
        alpha_ellipse = 1f;        
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setPaint(new Color(50, 50, 50));

        RenderingHints rh = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setRenderingHints(rh);

        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                alpha_rectangle));
        g2d.fill(rect);

        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                alpha_ellipse));
        g2d.fill(ellipse);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    class RectRunnable implements Runnable {

        private Thread runner;

        public RectRunnable() {
            
            initThread();
        }
        
        private void initThread() {
            
            runner = new Thread(this);
            runner.start();
        }

        @Override
        public void run() {

            while (alpha_rectangle >= 0) {
                
                repaint();
                alpha_rectangle += -0.01f;

                if (alpha_rectangle < 0) { 
                    alpha_rectangle = 0; 
                } 

                try { 
                    Thread.sleep(50); 
                } catch (InterruptedException ex) { 
                    Logger.getLogger(Surface.class.getName()).log(Level.SEVERE, 
                                                                  null, ex); 
                } 
            } 
         } 
    } 
    
    class HitTestAdapter extends MouseAdapter implements Runnable { 
        private RectRunnable rectAnimator; 
        private Thread ellipseAnimator; 

        @Override 
        public void mousePressed(MouseEvent e) { 
            int x = e.getX(); 
            int y = e.getY(); 
            if (rect.contains(x, y)) { 
                rectAnimator = new RectRunnable(); 
            } 
            if (ellipse.contains(x, y)) { 
                ellipseAnimator = new Thread(this); 
                ellipseAnimator.start(); 
            } 
        } 

        @Override 
        public void run() { 
            while (alpha_ellipse >= 0) { 
                repaint(); 
                alpha_ellipse += -0.01f; 
                if (alpha_ellipse < 0) { 
                    alpha_ellipse = 0; 
                } 
                try { 
                    Thread.sleep(50); 
                } catch (InterruptedException ex) { 
                    Logger.getLogger(Surface.class.getName()).log(Level.SEVERE, 
                                     null, ex); 
                } 
           } 
        } 
    } 
} 

public class HitTestingEx extends JFrame { 
    public HitTestingEx() { 
        add(new Surface()); 
        setTitle("Hit testing"); 
        setSize(250, 150); 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        setLocationRelativeTo(null); 
    } 

    public static void main(String[] args) { 
        EventQueue.invokeLater(new Runnable() { 
            @Override 
            public void run() { 
                HitTestingEx ex = new HitTestingEx(); 
                ex.setVisible(true); 
            } 
         }); 
    } 
} 

Chúng ta vẽ một hình chữ nhật và một hình tròn, nếu click chuột lên hình nào thì hình đó sẽ từ từ mờ dần rồi biến mất.

private float alpha_rectangle;
private float alpha_ellipse;

Hai biến alpha_rectanglealpha_allipse lưu trữ độ trong suốt của 2 hình. Hai biến này được kiểm soát thông qua các luồng độc lập (chi tiết về lập trình đa luồng sẽ được bàn tới trong một bài viết khác).

Chúng ta tạo lớp HitTestAdapter, lớp này xử lý sự kiện click chuột. Lớp này implements giao diện Runnable.

if (ellipse.contains(x, y)) {

    ellipseAnimator = new Thread(this);
    ellipseAnimator.start();
}

Khi click vào bên trong hình tròn thì chúng ta tạo một Thread mới rồi gọi phương thức start() để chạy luồng, ở đây là phương thức run() của chính lớp HitTestAdapter.

if (rect.contains(x, y)) {

    rectAnimator = new RectRunnable();
}

Đối với hình chữ nhật thì chúng ta chạy trong một luồng khác (từ lớp RectRunnable), chúng ta cho lớp này tự tạo luồng và tự chạy luôn trong phương thức khởi tạo.

public void run() {

    while (alpha_ellipse >= 0) {

        repaint();
        alpha_ellipse += -0.01f;
        ...
    }

Bên trong phương thức run() chúng ta code các vòng lặp để giảm dần độ trong suốt của các hình.

Untitled

Ví dụ 2

Trong ví dụ này chúng ta sẽ di chuyển và phóng to/thu nhỏ hình bằng chuột.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;


class Surface extends JPanel {

    private ZRectangle zrect;
    private ZEllipse zell;

    public Surface() {

        initUI();
    }
    
    private void initUI() {
        
        MovingAdapter ma = new MovingAdapter();

        addMouseMotionListener(ma);
        addMouseListener(ma);
        addMouseWheelListener(new ScaleHandler());

        zrect = new ZRectangle(50, 50, 50, 50);
        zell = new ZEllipse(150, 70, 80, 80);
    }
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g;
        
        Font font = new Font("Serif", Font.BOLD, 40);
        g2d.setFont(font);
        
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                        RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        g2d.setPaint(new Color(0, 0, 200));
        g2d.fill(zrect);
        g2d.setPaint(new Color(0, 200, 0));
        g2d.fill(zell);        
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        doDrawing(g);        
    }

    class ZEllipse extends Ellipse2D.Float {
        
        public ZEllipse(float x, float y, float width, float height) {
            
            setFrame(x, y, width, height);
        }

        public boolean isHit(float x, float y) {
            
            return getBounds2D().contains(x, y);
        }

        public void addX(float x) {
            
            this.x += x;
        }

        public void addY(float y) {
            
            this.y += y;
        }

        public void addWidth(float w) {
            
            this.width += w;
        }

        public void addHeight(float h) {
            
            this.height += h;
        }
    }

    class ZRectangle extends Rectangle2D.Float {

        public ZRectangle(float x, float y, float width, float height) {
            
            setRect(x, y, width, height);
        }

        public boolean isHit(float x, float y) {
            
            return getBounds2D().contains(x, y);
        }

        public void addX(float x) {
            
            this.x += x;
        }

        public void addY(float y) {
            
            this.y += y;
        }

        public void addWidth(float w) {
            
            this.width += w;
        }

        public void addHeight(float h) {
            
            this.height += h;
        }
    }

    class MovingAdapter extends MouseAdapter {

        private int x;
        private int y;

        @Override
        public void mousePressed(MouseEvent e) {
            
            x = e.getX();
            y = e.getY();
        }

        @Override
        public void mouseDragged(MouseEvent e) {

            doMove(e);
        }   
        
        private void doMove(MouseEvent e) {
            
            int dx = e.getX() - x;
            int dy = e.getY() - y;

            if (zrect.isHit(x, y)) {
                
                zrect.addX(dx);
                zrect.addY(dy);
                repaint();
            }

            if (zell.isHit(x, y)) {
                
                zell.addX(dx);
                zell.addY(dy);
                repaint();
            }

            x += dx;
            y += dy;            
        }
    }

    class ScaleHandler implements MouseWheelListener {
        
        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {

            doScale(e);
        }
        
        private void doScale(MouseWheelEvent e) {
            
            int x = e.getX();
            int y = e.getY();

            if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

                if (zrect.isHit(x, y)) {
                    
                    float amount =  e.getWheelRotation() * 5f;
                    zrect.addWidth(amount);
                    zrect.addHeight(amount);
                    repaint();
                }

                if (zell.isHit(x, y)) {
                    
                    float amount =  e.getWheelRotation() * 5f;
                    zell.addWidth(amount);
                    zell.addHeight(amount);
                    repaint();
                }
            }            
        }
    }
}

public class MovingScalingEx extends JFrame {
    
    public MovingScalingEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());

        setTitle("Moving and scaling");
        setSize(300, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);        
    }

    public static void main(String[] args) {        
        
        EventQueue.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                MovingScalingEx ex = new MovingScalingEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta vẽ một hình vuông và một hình tròn, khi chuột đang nằm trong phạm vi của một trong hai hình, chúng ta có thể kéo hình đó đi bằng cách click giữ và kéo, hoặc phóng to/thu nhỏ hình bằng con lăn trên chuột.

private ZRectangle zrect;
private ZEllipse zell;

Ở đây chúng ta không dùng các lớp hình học có sẵn mà tự viết một lớp kế thừa.

addMouseMotionListener(ma);
addMouseListener(ma);
addMouseWheelListener(new ScaleHandler());

Ba dòng code trên thêm các đối tượng lắng nghe sự kiện click chuột, kéo chuột và lăn con lăn.

class ZEllipse extends Ellipse2D.Float {
    
    public ZEllipse(float x, float y, float width, float height) {
        
        setFrame(x, y, width, height);
    }

    public boolean isHit(float x, float y) {
        
        return getBounds2D().contains(x, y);
    }
...
}

Chúng ta kế thừa các lớp hình học và thêm vào một số phương thức để chủ động hơn trong việc thay đổi kích thước, vị trí… Ngoài ra chúng ta còn có phương thức isHit() để kiểm tra xem vị trí của chuột có nằm trong hình hay không.

Lớp MovingAdapter chịu trách nhiệm xử lý sự kiện click chuột và kéo thả chuột.

@Override
public void mousePressed(MouseEvent e) {
    
    x = e.getX();
    y = e.getY();
}

Trong phương thức mousePressed(), chúng ta lưu lại vị trí của chuột.

int dx = e.getX() - x;
int dy = e.getY() - y;

if (zrect.isHit(x, y)) { 
    zrect.addX(dx); 
    zrect.addY(dy); 
    repaint(); 
}

if(zell.isHit(x, y)) {
    zell.addX(dx);
    zell.addY(dy);
    repaint();
}

x += dx; 
y += dy;

Trong phương thức doMove(), nếu vị trí của chuột nằm trong vùng của hình thì chúng ta tính khoảng cách x, y hiện tại của chuột với vị trí khi click rồi di chuyển hình theo đúng vị trí đó.

Lớp ScaleHandler phụ trách việc phóng to/thu nhỏ hình.

if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

    if (zrect.isHit(x, y)) {
        
        float amount =  e.getWheelRotation() * 5f;
        zrect.addWidth(amount);
        zrect.addHeight(amount);
        repaint();
    }
...
}

Nếu chuột đang nằm trong hình và chúng ta lăn con lăn trên chuột thì kích thước của hình sẽ được phóng to hoặc thu nhỏ. Phương thức getWheelRotation() trả về số góc mà con lăn đã quay.

 Capture

Ví dụ 3

Trong ví dụ này, chúng ta vẽ một hình chữ nhật lớn, ở hai góc của hình chữ nhật này có 2 hình vuông nhỏ, chúng ta có thể kéo các hình vuông nhỏ này để thay đổi kích thước của hình chữ nhật lớn.

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Point2D[] points;
    private final int SIZE = 8;
    private int pos;

    public Surface() {

        initUI();
    }

    private void initUI() {

        addMouseListener(new ShapeTestAdapter());
        addMouseMotionListener(new ShapeTestAdapter());
        pos = -1;

        points = new Point2D[2];
        points[0] = new Point2D.Double(50, 50);
        points[1] = new Point2D.Double(150, 100);
    }
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2 = (Graphics2D) g;

        for (Point2D point : points) {
            double x = point.getX() - SIZE / 2;
            double y = point.getY() - SIZE / 2;
            g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
        }

        Rectangle2D r = new Rectangle2D.Double();
        r.setFrameFromDiagonal(points[0], points[1]);

        g2.draw(r);        
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        doDrawing(g);
    }

    private class ShapeTestAdapter extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent event) {

            Point p = event.getPoint();

            for (int i = 0; i < points.length; i++) {

                double x = points[i].getX() - SIZE / 2;
                double y = points[i].getY() - SIZE / 2;

                Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);

                if (r.contains(p)) {

                    pos = i;
                    return;
                }
            }
        }

        @Override
        public void mouseReleased(MouseEvent event) {

            pos = -1;
        }

        @Override
        public void mouseDragged(MouseEvent event) {

            if (pos == -1) {
                return;
            }

            points[pos] = event.getPoint();
            repaint();
        }
    }
}

public class ResizingRectangleEx extends JFrame {

    public ResizingRectangleEx()  {
        
        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());

        setTitle("Resize rectangle");
        setSize(300, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);                  
    }
    
    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                ResizingRectangleEx ex = new ResizingRectangleEx();
                ex.setVisible(true);
            }
        });
    }
}

Có hai cách để vẽ một hình chữ nhật, cách thứ nhất là cho tọa độ x, y của điểm trái-trên và số đo dài-rộng rồi vẽ, cách thứ hai là cho tọa độ x, y của 2 điểm trái-trên và phải-dưới rồi vẽ. Trong ví dụ này chúng ta dùng cả hai cách đó.

private Point2D[] points;

Đầu tiên là hai chình vuông nhỏ, chúng ta lưu trong 2 đối tượng Point2D.

private final int SIZE = 8;

Kích thước của hai hình vuông lưu trong hằng số SIZE.

points = new Point2D[2];
points[0] = new Point2D.Double(50, 50);
points[1] = new Point2D.Double(150, 100);

Khởi tạo vị trí ban đầu cho hai hình vuông.

for (int i = 0; i < points.length; i++) {

    double x = points[i].getX() - SIZE / 2;
    double y = points[i].getY() - SIZE / 2;
    g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
}

Trong phương thức doDrawing(), đầu tiên chúng ta vẽ hai hình vuông nhỏ trước.

Rectangle2D s = new Rectangle2D.Double();
s.setFrameFromDiagonal(points[0], points[1]);

g2.draw(s);

Tiếp theo chúng ta vẽ một hình chữ nhật dựa theo hai hình vuông nhỏ.

@Override
public void mousePressed(MouseEvent event) {

    Point p = event.getPoint();

    for (int i = 0; i < points.length; i++) {

        double x = points[i].getX() - SIZE / 2;
        double y = points[i].getY() - SIZE / 2;

        Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);

        if (r.contains(p)) {

            pos = i;
            return;
        }
    }
}

Trong phương thức mousePressed(), chúng ta kiểm tra xem chuột có nằm trong một trong hai hình vuông nhỏ hay không, nếu có thì lưu thứ tự của hình vuông đó lại trong biến pos.

@Override
public void mouseDragged(MouseEvent event) {

    if (pos == -1) {
        return;
    }

    points[pos] = event.getPoint();
    repaint();
}

Trong phương thức mouseDragged(), nếu chuột đang nằm trong một hình vuông thì chúng ta cập nhật lại tọa độ của hình vuông đó theo tọa độ của chuột.

Capture

Java 2D – Font chữ

Trong phần này chúng ta sẽ tìm hiểu về hệ thống font chữ.

Vẽ kí tự lên màn hình là một chủ đề phức tạp, có khi viết nguyên một quyển sách cũng chưa hết, nên trong bài này mình chỉ bàn về một số thao tác cơ bản.

Có hai loại font là font vật lý (Physical Font) và font logic (Logical Font). Font vật lý là font thật, được lưu trong hệ điều hành, font logic không phải font thật mà thực ra chỉ là 5 hệ font được định nghĩa bởi Java: Serif, SansSerif, Monospaced, Dialog, DialogInput. Khi được gọi thì font logic sẽ được Java map vào font vật lý.

Một Font là một tập hợp các ký tự, một kí tự được gọi là TypeFace, tập hợp các TypeFace giống nhau nhưng khác kiểu font (hoặc các tính chất khác như độ lớn, độ nghiêng…) được gọi là Font-Family.

Font trong hệ điều hành

Đoạn code dưới đây sẽ in ra toàn bộ font chữ hiện được hỗ trợ bởi hệ điều hành của bạn.
AllFontsEx.java
import java.awt.Font;
import java.awt.GraphicsEnvironment;

public class AllFontsEx {

    public static void main(String[] args) {

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        Font[] fonts = ge.getAllFonts();

        for (Font font : fonts) {
            System.out.print(font.getFontName() + " : ");
            System.out.println(font.getFamily());
        }
    }
}

Bản thân Java không chứa thông tin về các kiểu font trong hệ điều hành, Java chỉ lấy thông tin về font của hệ điều hành để sử dụng. Mỗi hệ điều hành có bộ font khác nhau. Để lấy thông tin về font chữ trong hệ điều hành thì chúng ta sử dụng các lớp GraphicsEnvironment, Font.

Font[] fonts = ge.getAllFonts();

Phương thức getAllFonts() trả về danh sách font hiện có trong GraphicsEnvironment.

System.out.print(fonts[i].getFontName() + " : ");
System.out.println(fonts[i].getFamily());

Chúng ta dùng phương thức getFontName()getFamimy() để lấy về tên font và tên family.

...
.VnArial : .VnArial
.VnArial Bold : .VnArial
.VnArial Bold Italic : .VnArial
.VnArial Italic : .VnArial
.VnArial Narrow : .VnArial Narrow
.VnArial Narrow Bold : .VnArial Narrow
.VnArial Narrow Italic : .VnArial Narrow
.VnArial NarrowH : .VnArial NarrowH
.VnArialH : .VnArialH
.VnArialH Bold Italic : .VnArialH
...

In text lên màn hình

Trong ví dụ dưới đây, chúng ta sẽ in một đoạn lyric lên panel.

import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JFrame;
import javax.swing.JPanel;


class Surface extends JPanel {
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g;

        RenderingHints rh =
            new RenderingHints(RenderingHints.KEY_ANTIALIASING, 
            RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
               RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setRenderingHints(rh);

        g2d.setFont(new Font("NewellsHand", Font.PLAIN, 18));

        g2d.drawString("Most relationships seem so transitory", 20, 30);
        g2d.drawString("They're all good but not the permanent one", 20, 60);
        g2d.drawString("Who doesn't long for someone to hold", 20, 90);
        g2d.drawString("Who knows how to love you without being told", 20, 120);
        g2d.drawString("Somebody tell me why I'm on my own", 20, 150);
        g2d.drawString("If there's a soulmate for everyone", 20, 180);        
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }
}

public class SoulmateEx extends JFrame {
    
    public SoulmateEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        setTitle("Soulmate");

        add(new Surface());

        setSize(420, 250);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);      
    }
 
    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                SoulmateEx ex = new SoulmateEx();
                ex.setVisible(true);
            }
        });
    }
}

Để thiết lập kiểu font mong muốn thì chúng ta dùng phương thức setFont().

g2d.setFont(new Font("NewellsHand", Font.PLAIN, 18));

Để in một đoạn text thì chúng ta dùng phương thức Graphics2D.drawString().

g2d.drawString("Most relationships seem so transitory", 20, 30);
Capture

Tạo hiệu dứng đổ bóng cho chữ

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;


public class ShadowedTextEx extends JFrame {

    private final int width = 300;
    private final int height = 130;

    private final String text = "War is hell";
    private TextLayout textLayout;

    public ShadowedTextEx() {

        initUI();
    }

    private void initUI() {
        
        setTitle("Shadowed Text");
        
        BufferedImage image = createImage();
        add(new JLabel(new ImageIcon(image)));
        
        setSize(300, 130);
        setLocationRelativeTo(null);       
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private void setRenderingHints(Graphics2D g) {
        
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                           RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                           RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    private BufferedImage createImage()  {

        int x = 10;
        int y = 100;

        Font font = new Font("Georgia", Font.ITALIC, 50);
        
        BufferedImage image = new BufferedImage(width, height, 
                BufferedImage.TYPE_INT_RGB);
        Graphics2D g1d = image.createGraphics();
        setRenderingHints(g1d);
        textLayout = new TextLayout(text, font, g1d.getFontRenderContext());
        g1d.setPaint(Color.WHITE);
        g1d.fillRect(0, 0, width, height);

        g1d.setPaint(new Color(150, 150, 150));
        textLayout.draw(g1d, x+3, y+3);
        g1d.dispose();

        float[] kernel = {
          1f / 9f, 1f / 9f, 1f / 9f, 
          1f / 9f, 1f / 9f, 1f / 9f, 
          1f / 9f, 1f / 9f, 1f / 9f 
        };

        ConvolveOp op =  new ConvolveOp(new Kernel(3, 3, kernel), 
                ConvolveOp.EDGE_NO_OP, null);
        BufferedImage image2 = op.filter(image, null);

        Graphics2D g2d = image2.createGraphics();
        setRenderingHints(g2d);
        g2d.setPaint(Color.BLACK);
        textLayout.draw(g2d, x, y);
        
        g2d.dispose();

        return image2;
    }        

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                ShadowedTextEx ex = new ShadowedTextEx();
                ex.setVisible(true);
            }
        });
    }
}

Để tạo bóng cho một đoạn text thì chúng ta vẽ hai đoạn text, một đoạn text chính và một đoạn text dùng làm bóng, trong đó đoạn text đổ bóng được làm mờ, có vị trí lệch một ít với đoạn text gốc.

textLayout = new TextLayout(text, font, g1d.getFontRenderContext());

Chúng ta dùng lớp TextLayout để quy định các tính chất cho đoạn text, lớp này cho phép chúng ta thao tác sâu hơn với font.

textLayout.draw(g1d, x+3, y+3);

Chúng ta vẽ đoạn text lên màn hình có vị trí lệch 3 pixel so với đoạn text gốc.

float[] kernel = {
    1f / 9f, 1f / 9f, 1f / 9f, 
    1f / 9f, 1f / 9f, 1f / 9f, 
    1f / 9f, 1f / 9f, 1f / 9f 
};

ConvolveOp op =  new ConvolveOp(new Kernel(3, 3, kernel), 
        ConvolveOp.EDGE_NO_OP, null);

Để tăng thêm hiệu ứng thì chúng ta áp dụng hiệu ứng mờ lên đoạn text.

BufferedImage image2 = op.filter(image, null);

Chúng ta áp dụng hiệu ứng mờ lên đoạn text gốc rồi gán vào đoạn text thứ hai dùng làm đổ bóng.

textLayout.draw(g2d, x, y);

Sau đó chúng ta vẽ đoạn text gốc để đảm bảo đoạn text gốc nằm đè lên đoạn text đổ bóng.

Capture

Một số tính chất khác

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextAttribute;
import java.text.AttributedString;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private final String words = "Valour fate kinship darkness";
    private final String java = "Java TM";

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        Font font = new Font("Serif", Font.PLAIN, 40);

        AttributedString as1 = new AttributedString(words);
        as1.addAttribute(TextAttribute.FONT, font);

        as1.addAttribute(TextAttribute.FOREGROUND, Color.red, 0, 6);
        as1.addAttribute(TextAttribute.UNDERLINE, 
                TextAttribute.UNDERLINE_ON, 7, 11);
        as1.addAttribute(TextAttribute.BACKGROUND, Color.LIGHT_GRAY, 12, 19);
        as1.addAttribute(TextAttribute.STRIKETHROUGH,
                TextAttribute.STRIKETHROUGH_ON, 20, 28);

        g2d.drawString(as1.getIterator(), 15, 60);

        AttributedString as2 = new AttributedString(java);

        as2.addAttribute(TextAttribute.SIZE, 40);
        as2.addAttribute(TextAttribute.SUPERSCRIPT,
                TextAttribute.SUPERSCRIPT_SUPER, 5, 7);

        g2d.drawString(as2.getIterator(), 130, 125);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class TextAttributesEx extends JFrame {

    public TextAttributesEx() {

        initUI();
    }

    private void initUI() {
        
        add(new Surface());
        
        setSize(620, 190);
        setTitle("Text attributes");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                TextAttributesEx ex = new TextAttributesEx();
                ex.setVisible(true);
            }
        });
    }
}

Để thay đổi các tính chất của text thì chúng ta dùng các lớp Font, TextAttribute AttributedString. Lớp Font quy định kiểu font, lớp TextAttribute quy định một số tính chất như màu chữ, màu nền… còn lớp AttributedString lưu thông tin về đoạn text và các tính chất của nó.

AttributedString as1 = new AttributedString(words);

Đầu tiên chúng ta tạo một đối tượng AttributeString.

as1.addAttribute(TextAttribute.FOREGROUND, Color.red, 0, 6);

Chúng ta có thể thêm một số tính chất khác vào đoạn text thông qua phương thức addAttribute(), dòng trên có ý nghĩa là thiết lập màu chữ của các kí tự từ vị trí 0 đến vị trí 6 có màu đỏ.

g2d.drawString(as1.getIterator(), 15, 60);

Sau khi đã định nghĩa các tính chất mong muốn, chúng ta vẽ đoạn text lên nhưng chúng ta sẽ lấy đoạn text đó từ phương thức AttributedString.getIterator() chứ không dùng đoạn string gốc.

Capture

Xoay chữ

Trong bài xử lý ảnh chúng ta đã học cách xoay các đối tượng hình học. Bản thân các ký tự được vẽ trong Java cũng là các đối tượng hình học, chúng ta sẽ lấy các đối tượng đó ra mà xoay rồi vẽ như vẽ một đối tượng hình học bình thường.

import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        String s = "Welcome to PhoCode";

        Font font = new Font("Courier", Font.PLAIN, 13);

        g2d.translate(20, 20);

        FontRenderContext frc = g2d.getFontRenderContext();

        GlyphVector gv = font.createGlyphVector(frc, s);
        int length = gv.getNumGlyphs();

        for (int i = 0; i < length; i++) {
            
            Point2D p = gv.getGlyphPosition(i);
            double theta = (double) i / (double) (length - 1) * Math.PI / 3;
            AffineTransform at = AffineTransform.getTranslateInstance(p.getX(),
                    p.getY());
            at.rotate(theta);

            Shape glyph = gv.getGlyphOutline(i);
            Shape transformedGlyph = at.createTransformedShape(glyph);
            g2d.fill(transformedGlyph);
        }        
        
        g2d.dispose();
    }    
    
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }       
}

public class RotatedTextEx extends JFrame {
    
    public RotatedTextEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());
        
        setTitle("Rotated text");
        setSize(280, 210);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                RotatedTextEx ex = new RotatedTextEx();
                ex.setVisible(true);
            }
        });       
    }
}

Chúng ta sẽ dùng lớp FontRenderContext và lớp GlyphVector, lớp FontRenderContext chứa các thông tin cần thiết để lấy kích thước của kí tự, lớp GlyphVector lưu thông tin về các đặc điểm hình học của kí tự.

GlyphVector gv = font.createGlyphVector(frc, s);

Đầu tiên chúng ta tạo một đối tượng GlyphVector, trong đó lưu những thông tin về hình ảnh, vị trí…

int length = gv.getNumGlyphs();

Mỗi kí tự trong đoạn text sẽ được lưu trong một đối tượng hình học riêng, nên chúng ta lấy về số lượng các kí tự (cũng như các đối tượng hình học).

Point2D p = gv.getGlyphPosition(i);

Sau đó chúng ta lặp qua từng kí tự, tại mỗi lần lặp chúng ta lấy ra tọa độ của đối tượng vẽ ra kí tự đó bằng phương thức getGlyphPosition().

double theta = (double) i / (double) (length - 1) * Math.PI / 3;

Tiếp theo chúng ta tính góc xoay cho đối tượng đó dựa vào thứ tự của nó trong chuỗi.

AffineTransform at = AffineTransform.getTranslateInstance(p.getX(),
    p.getY());
at.rotate(theta);

Tiếp theo chúng ta xoay bằng cách sử dụng lớp AffineTransform.

Shape glyph = gv.getGlyphOutline(i);
Shape transformedGlyph = at.createTransformedShape(glyph);

Cuối cùng chúng ta dùng phương thức getGlyphOutline() để lấy về đối tượng hình học của kí tự hiện tại rồi dùng phương thức createTransformedShape() để tạo ra đối tượng đó ở trạng thái đã xoay.

g2d.fill(transformedGlyph);

Cuối cùng chúng ta vẽ đối tượng đó lên Panel.

Capture

Java 2D – Xử lý ảnh

Trong phần này chúng ta sẽ làm việc với ảnh.

Xử lý ảnh là một lĩnh vực khó (ít nhất là đối với mình), mặc dù Java cung cấp rất nhiều các hàm API cấp cao để đơn giản hóa việc xử lý nhưng trong phạm vi bài này mình chỉ đề cập đến một số thao tác xử lý ảnh đơn giản.

Lớp BufferedImage là lớp chuyên để làm việc với ảnh, lớp này lưu một mảng 2 chiều chứa thông tin của từng pixel trong ảnh.

Load ảnh

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Image mshi;

    public Surface() {
        
        loadImage();
        setSurfaceSize();
    }

    private void loadImage() {

        mshi = new ImageIcon("mushrooms.jpg").getImage();
    }
    
    private void setSurfaceSize() {
        
        Dimension d = new Dimension();
        d.width = mshi.getWidth(null);
        d.height = mshi.getHeight(null);
        setPreferredSize(d);        
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;
        g2d.drawImage(mshi, 0, 0, null);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class DisplayImageEx extends JFrame {

    public DisplayImageEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        pack();
        
        setTitle("Mushrooms");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                DisplayImageEx ex = new DisplayImageEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta load ảnh và chỉnh kích thước của cửa sổ bằng với kích thước của ảnh.

private void loadImage() {

    mshi = new ImageIcon("mushrooms.jpg").getImage();
}

Để load ảnh thì chúng ta dùng lớp ImageIcon rồi dùng phương thức getImage() để lấy về đối tượng Image.

private void setSurfaceSize() {
    
    Dimension d = new Dimension();
    d.width = mshi.getWidth(null);
    d.height = mshi.getHeight(null);
    setPreferredSize(d);        
}

Trong phương thức setSurfaceSize(), chúng ta lấy kích thước của ảnh rồi dùng phương thức setPreferredSize() để thiết lập kích thước cửa sổ.

private void doDrawing(Graphics g) {

    Graphics2D g2d = (Graphics2D) g;
    g2d.drawImage(mshi, 0, 0, null);
}

Để vẽ ảnh lên JPanel thì chúng ta dùng phương thức drawImage(). Tham số thứ 4 trong phương thức là một một đối tượng ImageObserver, đối tượng này thực hiện các thao tác đồng bộ các thay đổi của ảnh trước khi được vẽ lên màn hình, tuy nhiên ở đây chúng ta chưa cần đến nên để null.

private void initUI() {
    ...
    pack();
    ...
}

Phương thức pack() thay đổi kích thước của cửa sổ để phù hợp với kích thước của JPanel.

Tạo ảnh đen trắng

Trong đồ họa máy tính thì ảnh đen trắng được biểu diễn bởi một kênh màu (khác với ảnh màu có 3 kênh là Red Green Blue) cùng với giá trị alpha để biểu diễn mức độ trong suốt của điểm ảnh.

Trong ví dụ dưới đây, chúng ta sẽ tạo ảnh đen trắng từ một ảnh có sẵn.

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Image mshi;
    private BufferedImage bufimg;
    private Dimension d;

    public Surface() {

        loadImage();
        determineAndSetSize();
        createGrayImage();
    }

    private void determineAndSetSize() {

        d = new Dimension();
        d.width = mshi.getWidth(null);
        d.height = mshi.getHeight(null);
        setPreferredSize(d);
    }
    
    private void createGrayImage() {
        
        bufimg = new BufferedImage(d.width, d.height, 
                BufferedImage.TYPE_BYTE_GRAY);

        Graphics2D g2d = bufimg.createGraphics();
        g2d.drawImage(mshi, 0, 0, null);
        g2d.dispose();        
    }

    private void loadImage() {

        mshi = new ImageIcon("mushrooms.jpg").getImage();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;
        g2d.drawImage(bufimg, null, 0, 0);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class GrayScaleImageEx extends JFrame {

    public GrayScaleImageEx() {
        
        initUI();
    }

    private void initUI() {

        Surface dpnl = new Surface();
        add(dpnl);

        pack();
        
        setTitle("GrayScale image");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                GrayScaleImageEx ex = new GrayScaleImageEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong Java có rất nhiều cách để tạo một ảnh đen trắng. Trong ví dụ này thì chúng ta dùng thuộc tính BufferedImage.TYPE_BYTE_GRAY.

bufimg = new BufferedImage(d.width, d.height, 
        BufferedImage.TYPE_BYTE_GRAY);

Đầu tiên chúng ta tạo một đối tượng BufferedImage với thuộc tính BufferedImage.TYPE_BYTE_GRAY.

Graphics2D g2d = bufimg.createGraphics();
g2d.drawImage(mshi, 0, 0, null);

Tiếp theo chúng ta tạo một đối tượng Graphics2D rồi gọi phương thức drawImage() để vẽ ảnh vào BufferedImage.

private void doDrawing(Graphics g) {

    Graphics2D g2d = (Graphics2D) g;
    g2d.drawImage(bufimg, null, 0, 0);
}

Bên trong phương thức doDrawing() chúng ta vẽ lại BufferedImage lên JPanel.

Lật ảnh

Trong ví dụ dưới đây, chúng ta sẽ thực hiện thao tác lật ảnh.

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Image mshi;
    private BufferedImage bufimg;
    private final int SPACE = 10;

    public Surface() {

        loadImage();
        createFlippedImage();
        setSurfaceSize();
    }

    private void loadImage() {

        mshi = new ImageIcon("mushrooms.jpg").getImage();
    }

    private void createFlippedImage() {
        
        bufimg = new BufferedImage(mshi.getWidth(null),
                mshi.getHeight(null), BufferedImage.TYPE_INT_RGB);
        
        Graphics gb = bufimg.getGraphics();
        gb.drawImage(mshi, 0, 0, null);
        gb.dispose();

        AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
        tx.translate(-mshi.getWidth(null), 0);
        AffineTransformOp op = new AffineTransformOp(tx,
                AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
        bufimg = op.filter(bufimg, null);        
    }
    
    private void setSurfaceSize() {
                
        int w = bufimg.getWidth();
        int h = bufimg.getHeight();
        
        Dimension d = new Dimension(3*SPACE+2*w, h+2*SPACE);
        setPreferredSize(d);
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.drawImage(mshi, SPACE, SPACE, null);
        g2d.drawImage(bufimg, null, 2*SPACE + bufimg.getWidth(), SPACE);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class FlippedImageEx extends JFrame {

    public FlippedImageEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());
        pack();

        setTitle("Flipped image");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                FlippedImageEx ex = new FlippedImageEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta sẽ lật ảnh theo chiều ngang.

AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
tx.translate(-castle.getWidth(null), 0);

Thao tác lật ảnh gồm có 2 bước, đầu tiên chúng ta scale nó theo chiều ngược lại tức là tỉ lệ -1 sau đó tịnh tiến tâm vẽ lùi về bên trái với khoảng cách bằng với kích thước của ảnh.

AffineTransformOp op = new AffineTransformOp(tx, 
                        AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
bufferedImage = op.filter(bufferedImage, null)

Sau khi đã định nghĩa thao tác lật ảnh trong đối tượng AffineTransform, chúng ta copy từng pixel trong ảnh vào một BufferedImage, trong Java có sẵn lớp AffineTransformOp cho phép chúng ta làm điều này dễ dàng với phương thức filter().

private void doDrawing(Graphics g) {

    Graphics2D g2d = (Graphics2D) g;

    g2d.drawImage(mshi, SPACE, SPACE, null);
    g2d.drawImage(bufimg, null, 2*SPACE + bufimg.getWidth(), SPACE);
}

Chúng ta vẽ cả ảnh gốc và ảnh đã được lật vào panel.

private void setSurfaceSize() {
            
    int w = bufimg.getWidth();
    int h = bufimg.getHeight();
    
    Dimension d = new Dimension(3*SPACE+2*w, h+2*SPACE);
    setPreferredSize(d);
}

Chúng ta thiết lập kích thước panel dựa theo kích thước ảnh, kích thước của panel sẽ có chiều ngang bằng 2 lần kích thước ảnh vì chúng ta vẽ cả ảnh gốc và ảnh đã được lật, đồng thời chiều ngang sẽ rộng thêm vài pixel vì chúng ta muốn có một chút khoảng trống giữa 2 ảnh và viền panel.

Làm mờ ảnh

Trong ví dụ dưới đây chúng ta sẽ làm mờ một ảnh, ảnh mờ thường được thấy khi bạn cầm máy ảnh mà bị rung tay, hay trong các hiệu ứng tốc độ…

Trong đồ họa máy tính thì việc làm mờ một ảnh được thực hiện bằng cách thay thế từng pixel trên ảnh bằng trung bình cộng của các pixel xung quanh nó (từ 3 đến 8 điểm). Có rất nhiều kiểu làm mờ một ảnh, ở đây mình chỉ demo một kiểu đơn giản, các kiểu khác sẽ được đề cập trong một tương lai không ai biết 🙂

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private BufferedImage mshi;
    private BufferedImage bluri;

    public Surface() {

        loadImage();
        createBlurredImage();
        setSurfaceSize();
    }

    private void loadImage() {
        
        try {
            
            mshi = ImageIO.read(new File("mushrooms.jpg"));
        } catch (IOException ex) {
            
            Logger.getLogger(Surface.class.getName()).log(
                    Level.WARNING, null, ex);
        }
    }
    
    private void createBlurredImage() {

        float[] blurKernel = {
            1 / 9f, 1 / 9f, 1 / 9f,
            1 / 9f, 1 / 9f, 1 / 9f,
            1 / 9f, 1 / 9f, 1 / 9f
        };

        BufferedImageOp blur = new ConvolveOp(new Kernel(3, 3, blurKernel));
        bluri = blur.filter(mshi, new BufferedImage(mshi.getWidth(),
                mshi.getHeight(), mshi.getType()));
    }
    
    private void setSurfaceSize() {
        
        Dimension d = new Dimension();
        d.width = mshi.getWidth(null);
        d.height = mshi.getHeight(null);
        setPreferredSize(d);        
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;
        g2d.drawImage(bluri, null, 0, 0);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class BlurredImageEx extends JFrame {

    public BlurredImageEx() {

        setTitle("Blurred image");
        add(new Surface());

        pack();
        
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                
                BlurredImageEx ex = new BlurredImageEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta load ảnh từ đĩa cứng sau đó làm mờ nó rồi hiển thị lên màn hình.

float[] blurKernel = {
    1 / 9f, 1 / 9f, 1 / 9f,
    1 / 9f, 1 / 9f, 1 / 9f,
    1 / 9f, 1 / 9f, 1 / 9f
};

Mảng blurKernel được dùng cho việc tính toán các pixel.

BufferedImageOp blur = new ConvolveOp(new Kernel(3, 3, blurKernel));
bluri = blur.filter(mshi, new BufferedImage(mshi.getWidth(),
        mshi.getHeight(), mshi.getType()));

Ảnh sẽ được làm mờ bằng lớp ConvolveOp.

Tạo ảnh phản chiếu

Trong ví dụ dưới đây chúng ta sẽ tạo hiệu ứng phản chiếu cho ảnh giống như hình ảnh phản chiếu của hồ nước.

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private BufferedImage image;
    private BufferedImage refImage;
    private int img_w;
    private int img_h;
    private final int SPACE = 30;

    public Surface() {

        loadImage();
        getImageSize();
        createReflectedImage();
    }

    private void loadImage() {

        try {

            image = ImageIO.read(new File("rotunda.jpg"));
        } catch (Exception ex) {

            Logger.getLogger(Surface.class.getName()).log(
                    Level.WARNING, null, ex);
        }
    }

    private void getImageSize() {

        img_w = image.getWidth();
        img_h = image.getHeight();
    }

    private void createReflectedImage() {
        
        float opacity = 0.4f;
        float fadeHeight = 0.3f;
        
        refImage = new BufferedImage(img_w, img_h,
                BufferedImage.TYPE_INT_ARGB);        
        Graphics2D rg = refImage.createGraphics();
        rg.drawImage(image, 0, 0, null);
        rg.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN));
        rg.setPaint(new GradientPaint(0, img_h * fadeHeight,
                new Color(0.0f, 0.0f, 0.0f, 0.0f), 0, img_h,
                new Color(0.0f, 0.0f, 0.0f, opacity)));

        rg.fillRect(0, 0, img_w, img_h);
        rg.dispose();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        int win_w = getWidth();
        int win_h = getHeight();

        int gap = 20;

        g2d.setPaint(new GradientPaint(0, 0, Color.black, 0, 
                win_h, Color.darkGray));
        g2d.fillRect(0, 0, win_w, win_h);
        g2d.translate((win_w - img_w) / 2, win_h / 2 - img_h);
        g2d.drawImage(image, 0, 0, null);
        g2d.translate(0, 2 * img_h + gap);
        g2d.scale(1, -1);

        g2d.drawImage(refImage, 0, 0, null);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    @Override
    public Dimension getPreferredSize() {

        return new Dimension(img_w + 2 * SPACE, 2 * img_h + 3 * SPACE);
    }
}

public class ReflectionEx extends JFrame {

    public ReflectionEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());
        pack();
        
        setTitle("Reflection");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                
                ReflectionEx ex = new ReflectionEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta load ảnh rồi tạo một ảnh khác làm phản chiếu từ ảnh gốc.

refImage = new BufferedImage(img_w, img_h,
        BufferedImage.TYPE_INT_ARGB);        
Graphics2D rg = refImage.createGraphics();
rg.drawImage(image, 0, 0, null);

Đầu tiên chúng ta tạo đối tượng BufferedImage dùng làm ảnh phản chiếu.

rg.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN));
rg.setPaint(new GradientPaint(0, img_h * fadeHeight,
        new Color(0.0f, 0.0f, 0.0f, 0.0f), 0, img_h,
        new Color(0.0f, 0.0f, 0.0f, opacity)));

rg.fillRect(0, 0, img_w, img_h);

Tiếp theo chúng ta tạo phần mờ bằng lớp GradientPaint, lớp này được dùng để vẽ các dải màu nhưng ở đây chúng ta vẽ ảnh nên phần màu không được hiển thị, tham số opacity biểu diễn giá trị alpha sẽ làm cho ảnh mờ dần dần.

g2d.setPaint(new GradientPaint(0, 0, Color.black, 0, 
        win_h, Color.darkGray));
g2d.fillRect(0, 0, win_w, win_h);

Phần màu nền của cửa sổ cũng được vẽ bằng gradient từ màu đen dần chuyển sang màu xám từ trên xuống.

g2d.translate(0, 2 * imageHeight + gap);
g2d.scale(1, -1);

Phần ảnh được làm mờ sẽ được lật lại theo chiều dọc và được vẽ bên dưới ảnh gốc.

@Override
public Dimension getPreferredSize() {

    return new Dimension(img_w + 2 * SPACE, 2 * img_h + 3 * SPACE);
}

Khác với các ví dụ trên, ở đây chúng ta chỉnh kích thước cửa sổ bằng cách override lại phương thức getPreferredSize().

Capture

Java 2D – Mô phỏng một số hiệu ứng đơn giản

Trong phần này chúng ta sẽ tập tành làm một số hiệu ứng đơn giản.

Ví dụ 1

Trong ví dụ đầu tiên, chúng ta sẽ mô phỏng hiệu ứng bong bóng.

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel
        implements ActionListener {

    private final Color colors[] = {
        Color.blue, Color.cyan, Color.green,
        Color.magenta, Color.orange, Color.pink,
        Color.red, Color.yellow, Color.lightGray, Color.white
    };

    private Ellipse2D.Float[] ellipses;
    private double esize[];
    private float estroke[];
    private double maxSize = 0;
    private final int NUMBER_OF_ELLIPSES = 25;
    private final int DELAY = 30;
    private final int INITIAL_DELAY = 150;    
    private Timer timer;

    public Surface() {

        initSurface();
        initEllipses();
        initTimer();
    }

    private void initSurface() {

        setBackground(Color.black);
        ellipses = new Ellipse2D.Float[NUMBER_OF_ELLIPSES];
        esize = new double[ellipses.length];
        estroke = new float[ellipses.length];
    }

    private void initEllipses() {

        int w = 350;
        int h = 250;

        maxSize = w / 10;

        for (int i = 0; i < ellipses.length; i++) {

            ellipses[i] = new Ellipse2D.Float();
            posRandEllipses(i, maxSize * Math.random(), w, h);
        }
    }

    private void initTimer() {

        timer = new Timer(DELAY, this);
        timer.setInitialDelay(INITIAL_DELAY);
        timer.start();
    }

    private void posRandEllipses(int i, double size, int w, int h) {

        esize[i] = size;
        estroke[i] = 1.0f;
        double x = Math.random() * (w - (maxSize / 2));
        double y = Math.random() * (h - (maxSize / 2));
        ellipses[i].setFrame(x, y, size, size);
    }

    private void doStep(int w, int h) {

        for (int i = 0; i < ellipses.length; i++) { 
            estroke[i] += 0.025f; 
            esize[i]++; 
            if (esize[i] > maxSize) {
                posRandEllipses(i, 1, w, h);
            } else {
                ellipses[i].setFrame(ellipses[i].getX(), 
                                     ellipses[i].getY(),
                                     esize[i], 
                                     esize[i]);
            }
        }
    }

    private void drawEllipses(Graphics2D g2d) {

        for (int i = 0; i < ellipses.length; i++) {

            g2d.setColor(colors[i % colors.length]);
            g2d.setStroke(new BasicStroke(estroke[i]));
            g2d.draw(ellipses[i]);
        }
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        RenderingHints rh
                = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setRenderingHints(rh);

        Dimension size = getSize();
        doStep(size.width, size.height);
        drawEllipses(g2d);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        repaint();
    }
}

public class BubblesEx extends JFrame {

    public BubblesEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());
        
        setTitle("Bubbles");
        setSize(350, 250);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                BubblesEx ex = new BubblesEx();
                ex.setVisible(true);
            }
        });
    }
}

Các bong bóng sẽ xuất hiện tại các vị trí ngẫu nhiên trên màn hình với kích thước và màu sắc khác nhau, kích thước của chúng sẽ tăng dần đến một mức độ nào đó thì biến mất.

private final Color colors[] = {
    Color.blue, Color.cyan, Color.green,
    Color.magenta, Color.orange, Color.pink,
    Color.red, Color.yellow, Color.lightGray, Color.white
};

Màu sắc sẽ được lưu vào một mảng để gọi đến cho tiện.

private void initSurface() {

    setBackground(Color.black);
    ellipses = new Ellipse2D.Float[NUMBER_OF_ELLIPSES];
    esize = new double[ellipses.length];
    estroke = new float[ellipses.length];
}

Phương thức initSurface() thực hiện công việc khởi tạo ban đầu, chúng ta thiết lập màu nền màn hình là màu đen. Sau đó khởi tạo 3 mảng, mảng ellipses lưu các đối tượng elip, mảng esize lưu kích thước của các elip, mảng etstroke lưu độ dày của nét vẽ.

private void initEllipses() {

    int w = 350;
    int h = 250;
            
    maxSize = w / 10;
    
    for (int i = 0; i < ellipses.length; i++) {
        
        ellipses[i] = new Ellipse2D.Float();
        posRandEllipses(i, maxSize * Math.random(), w, h);
    }
}

Phương thức initEllipses() thực hiện khởi tạo từng đối tượng Ellipse2D rồi gọi phương thức posRandEllipse() để tiếp tục khởi tạo các thông tin cần thiết.

private void posRandEllipses(int i, double size, int w, int h) {

    esize[i] = size;
    estroke[i] = 1.0f;
    double x = Math.random() * (w - (maxSize / 2));
    double y = Math.random() * (h - (maxSize / 2));
    ellipses[i].setFrame(x, y, size, size);
}

Phương thức posRandEllipses() lưu kích thước và độ dày của nét vẽ vào hai mảng esizeestroke, sau đó khởi tạo ngẫu nhiên vị trí của từng hình elip rồi thiết lập thông tin đó bằng phương thức setFrame().

private void doStep(int w, int h) {

    for (int i = 0; i < ellipses.length; i++) { 
        estroke[i] += 0.025f; 
        esize[i]++; 
        if (esize[i] > maxSize) {            
            posRandEllipses(i, 1, w, h);
        } else {            
            ellipses[i].setFrame(ellipses[i].getX(), 
                                 ellipses[i].getY(),
                                 esize[i], 
                                 esize[i]);
        }
    }
}

Phương thức doStep() thực hiện phần animation (tạo hiệu ứng). Cứ mỗi lần lặp, chúng ta tăng kích thước và độ dày của từng hình elip lên, nếu hình elip nào có kích thước vượt quá ngưỡng maxSize thì chúng ta khởi tạo lại kích thước mới với vị trí mới cho hình elip đó.

private void drawEllipses(Graphics2D g2d) {

    for (int i = 0; i < ellipses.length; i++) {

        g2d.setColor(colors[i % colors.length]);
        g2d.setStroke(new BasicStroke(estroke[i]));
        g2d.draw(ellipses[i]);
    }
}

Phương thức drawEllipses() sẽ thực hiện việc vẽ toàn bộ các hình elip lên màn hình với màu được chọn ngẫu nhiên.

Dimension size = getSize();
doStep(size.width, size.height);

Ngoài ra trong phương thức doDrawing() chúng ta cũng phải lấy lại kích thước cửa sổ để đề phòng kích thước cửa sổ thay đổi thì vị trí các hình elip cũng phải thay đổi theo cho phù hợp.

Capture

Ví dụ 2

Trong ví dụ này chúng ta tạo một ngôi sao chuyển động.

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.GeneralPath;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel
        implements ActionListener {

    private final int points[][] = {
        {0, 85}, {75, 75}, {100, 10}, {125, 75},
        {200, 85}, {150, 125}, {160, 190}, {100, 150},
        {40, 190}, {50, 125}, {0, 85}
    };
    
    private Timer timer;
    private double angle = 0;
    private double scale = 1;
    private double delta = 0.01;
    
    private final int DELAY = 10;

    public Surface() {

        initTimer();
    }
    
    private void initTimer() {
        
        timer = new Timer(DELAY, this);
        timer.start();        
    }

    private void doDrawing(Graphics g) {
        
        int h = getHeight();
        int w = getWidth();

        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);

        g2d.translate(w / 2, h / 2);
        GeneralPath star = new GeneralPath();
        star.moveTo(points[0][0], points[0][1]);

        for (int k = 1; k < points.length; k++) {
            
            star.lineTo(points[k][0], points[k][1]);
        }

        g2d.rotate(angle);
        g2d.scale(scale, scale);
        g2d.fill(star);        
        
        g2d.dispose();
    }
    
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
    
    private void step() {
        
        if (scale < 0.01) { 
            delta = -delta; 
        } else if (scale > 0.99) {            
            delta = -delta;
        }

        scale += delta;
        angle += 0.01;        
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        step();
        repaint();
    }
}

public class StarDemoEx extends JFrame {

    public StarDemoEx() {

        initUI();
    }

    private void initUI() {
        
        add(new Surface());

        setTitle("Star");
        setSize(420, 250);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                StarDemoEx ex = new StarDemoEx();
                ex.setVisible(true);
            }
        });
    }
}

Ngôi sao sẽ tự xoay quanh mình và tự phóng to/thu nhỏ trong một phạm vi cố định, cho cảm giác giống như ngôi sao đang nhảy lên nhảy xuống.

private final int points[][] = {
    {0, 85}, {75, 75}, {100, 10}, {125, 75},
    {200, 85}, {150, 125}, {160, 190}, {100, 150},
    {40, 190}, {50, 125}, {0, 85}
};

Chúng ta vẽ ngôi sao bằng cách vẽ một đa giác, các điểm của đa giác được lưu trong mảng points.

private double angle = 0;
private double scale = 1;
private double delta = 0.01;

Biến angle lưu hướng xoay của ngôi sao, biến scale lưu tỉ lệ phóng to/thu nhỏ của ngôi sao, biến delta lưu tốc độ phóng to/thu nhỏ của ngôi sao.

g2d.translate(w / 2, h / 2);

Chúng ta tịnh tiến vào giữa màn hình.

if (scale < 0.01) { 
    delta = -delta; 
} else if (scale > 0.99) {    
    delta = -delta;
}

Ngôi sao chỉ được phóng to và thu nhỏ trong một phạm vi cố định.

Untitled

Ví dụ 3

Trong ví dụ này chúng ta mô phỏng hiệu ứng Puff rất được hay dùng trong phim ảnh.

import java.awt.AlphaComposite;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;


class Surface extends JPanel 
        implements ActionListener {

    private Timer timer;
    private int x = 1;
    private float alpha = 1;
    private final int DELAY = 15;
    private final int INITIAL_DELAY = 200;

    public Surface() {
        
        initTimer();
    }
    
    private void initTimer() {
        
        timer = new Timer(DELAY, this);
        timer.setInitialDelay(INITIAL_DELAY);
        timer.start();               
    }
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();
        
        RenderingHints rh =
            new RenderingHints(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
               RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setRenderingHints(rh);

        Font font = new Font("Dialog", Font.PLAIN, x);
        g2d.setFont(font);

        FontMetrics fm = g2d.getFontMetrics();
        String s = "PhoCode";
        Dimension size = getSize();

        int w = (int) size.getWidth();
        int h = (int) size.getHeight();

        int stringWidth = fm.stringWidth(s);
        AlphaComposite ac = AlphaComposite.getInstance(
                AlphaComposite.SRC_OVER, alpha);
        g2d.setComposite(ac);

        g2d.drawString(s, (w - stringWidth) / 2, h / 2);        
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);        
        doDrawing(g);
    }   
    
    private void step() {
        
        x += 1;

        if (x > 40)
            alpha -= 0.01;

        if (alpha <= 0.01)
            timer.stop();        
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        
        step();
        repaint();
    }        
}

public class PuffEx extends JFrame {    
    
    public PuffEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        setTitle("Puff");

        add(new Surface());

        setSize(400, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);        
    }

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                PuffEx ex = new PuffEx();
                ex.setVisible(true);
            }
        });      
    }
}

Trong hiệu ứng Puff, một đoạn text hiện ra rồi từ từ to dần và biến mất.

FontMetrics fm = g2d.getFontMetrics();

Lớp FontMetrics lưu thông tin về kiểu Font tương ứng với từng loại màn hình, để lấy thông tin đó thì chúng ta dùng phương thức getFontMetrics().

int stringWidth = fm.stringWidth(s);

Chúng ta lấy kích thước của đoạn text bằng phương thức FontMetrics.stringWidth().

AlphaComposite ac = AlphaComposite.getInstance(
        AlphaComposite.SRC_OVER, alpha);
g2d.setComposite(ac);

Chúng ta thiết lập độ trong suốt cho đoạn text.

g2d.drawString(s, (w - stringWidth) / 2, h / 2);

Dòng trên vẽ đoạn text ra giữa màn hình.

if (x > 40)
    alpha -= 0.01;

Khi kích thước của đoạn text lớn hơn 40 thì bắt đầu cho mờ dần.

Untitled

Java 2D – Biến đổi hình

Trong phần này chúng ta sẽ tìm hiểu về các phép biến đổi hình.

Một phép biến đối hình có thể là kết quả của nhiều phép biến đổi tuyến tính bao gồm phép xoay hình, co dãn hình, biến dạng hình và phép tịnh tiến. Trong đó xoay hình là công việc xoay một đối tượng xung quanh một điểm cố định. Phép co dãn thực hiện phóng to hoặc thu nhỏ đối tượng. Phép tịnh tiến di chuyển một đối tượng theo một hướng nào đó. Phép biến dạng là thay đổi hình dạng của đối tượng theo một trục nào đó.

Trong Java chúng ta sử dụng lớp AffineTransform để thực hiện các phép biến đổi nói trên.

Phép tịnh tiến – Translation

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setPaint(new Color(150, 150, 150));
        g2d.fillRect(20, 20, 80, 50);
        g2d.translate(150, 50);
        g2d.fillRect(20, 20, 80, 50);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }
}

public class TranslationEx extends JFrame {
    
    public TranslationEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());

        setTitle("Translation");
        setSize(300, 200);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                TranslationEx ex = new TranslationEx();
                ex.setVisible(true);
            }
        });                    
    }
}

Trong ví dụ trên, chúng ta vẽ một hình chữ nhật, sau đó thực hiện phép tịnh tiến rồi vẽ thêm một hình chữ nhật nữa.

g2d.translate(150, 50);

Phương thức translate() di chuyển tâm của đối tượng Graphics2D đến tọa độ mới.

Capture

Lưu ý là phương thức translate() di chuyển tâm vẽ của đối tượng Graphics2D chứ không di chuyển hình chữ nhật nào cả:

Capture

Phép xoay hình – Rotation

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setPaint(new Color(150, 150, 150));
        g2d.fillRect(20, 20, 80, 50);
        g2d.translate(180, -50);
        g2d.rotate(Math.toRadians(45));
        g2d.fillRect(80, 80, 80, 50);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }
}

public class RotationEx extends JFrame {
    
    public RotationEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        setTitle("Rotation");

        add(new Surface());

        setSize(300, 200);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                RotationEx ex = new RotationEx();
                ex.setVisible(true);
            }
        });                    
    }
}

Trong ví dụ trên, chúng ta vẽ một hình chữ nhật, sau đó thực hiện phép xoay hình rồi vẽ lại hình chữ nhật đó.

g2d.rotate(Math.toRadians(45));

Để xoay hình thì chúng ta dùng phương thức rotate(). Đơn vị góc của phương thức này là radian nên nếu bạn dùng đơn vị độ thì nên chuyển sang radian bằng phương thức Math.toRadians() trước.

Capture

Cũng giống như với phép tịnh tiến, phép xoay chỉ xoay cửa sổ từ tâm vẽ chứ không xoay một hình nào cả:

Capture

Hình minh họa vẽ bằng paint nên hơi xấu 🙂

Phép co dãn hình – Scaling

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setColor(new Color(150, 150, 150));
        g2d.fillRect(20, 20, 80, 50);

        AffineTransform tx1 = new AffineTransform();
        tx1.translate(110, 22);
        tx1.scale(0.5, 0.5);

        g2d.setTransform(tx1);
        g2d.fillRect(0, 0, 80, 50);

        AffineTransform tx2 = new AffineTransform();
        tx2.translate(170, 20);
        tx2.scale(1.5, 1.5);

        g2d.setTransform(tx2);
        g2d.fillRect(0, 0, 80, 50);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class ScalingEx extends JFrame {

    public ScalingEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Scaling");
        setSize(330, 160);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                ScalingEx ex = new ScalingEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ trên chúng ta vẽ một hình chữ nhật rồi phóng to và thu nhỏ hình chữ nhật đó.

AffineTransform tx2 = new AffineTransform();
tx2.translate(170, 20);
tx2.scale(1.5, 1.5);

Ở đây chúng ta thực hiện các phép co dãn sử dụng phương thức scale() của lớp AffineTransform.

Capture

Phép biến dạng – Shearing

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        AffineTransform tx1 = new AffineTransform();
        tx1.translate(50, 90);

        g2d.setTransform(tx1);
        g2d.setPaint(Color.green);
        g2d.drawRect(0, 0, 160, 50);

        AffineTransform tx2 = new AffineTransform();
        tx2.translate(50, 90);
        tx2.shear(0, 1);

        g2d.setTransform(tx2);
        g2d.setPaint(Color.blue);

        g2d.draw(new Rectangle(0, 0, 80, 50));

        AffineTransform tx3 = new AffineTransform();
        tx3.translate(130, 10);
        tx3.shear(0, 1);

        g2d.setTransform(tx3);
        g2d.setPaint(Color.red);
        g2d.drawRect(0, 0, 80, 50);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class ShearingEx extends JFrame {

    public ShearingEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Shearing");
        setSize(330, 270);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                ShearingEx ex = new ShearingEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ trên, chúng ta vẽ 3 hình chữ nhật và áp dụng phép biến dạng cho 2 hình.

tx2.shear(0, 1);

Để thực hiện phép biến dạng thì chúng ta dùng phương thức shear(), tham số đầu tiên làm biến dạng theo trục x, tham số thứ 2 biến dạng theo trục y.

Capture

Java 2D – Cắt hình

Trong phần này chúng ta sẽ học cách cắt hình.

Cắt hình trong Java là loại bỏ một phần của cửa sổ, chỉ vẽ một khu vực nào đó để tạo một số hiệu ứng. Khi cắt hình thì chúng ta nên tạo một đối tượng Graphics mới hoặc phục hồi đối tượng cũ về trạng thái ban đầu.

Ví dụ 1

Trong ví dụ này, chúng ta sẽ cắt hình thành một hình tròn.

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;


class Surface extends JPanel 
        implements ActionListener {

    private int pos_x = 8;
    private int pos_y = 8;
    private final int RADIUS = 90;
    private final int DELAY = 35;

    private Timer timer;
    private Image image;

    private final double delta[] = { 3, 3 };

    public Surface() {
        
        loadImage();
        determineAndSetImageSize();
        initTimer();
    }
    
    private void loadImage() {
        
        image = new ImageIcon("mushrooms.jpg").getImage();
    }
    
    private void determineAndSetImageSize() {
        
        int h = image.getHeight(this);
        int w = image.getWidth(this);
        setPreferredSize(new Dimension(w, h));        
    }    

    private void initTimer() {   

        timer = new Timer(DELAY, this);
        timer.start();
    }
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.clip(new Ellipse2D.Double(pos_x, pos_y, RADIUS, RADIUS));
        g2d.drawImage(image, 0, 0, null); 
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
        
        moveCircle();
        repaint();
    }
    
    private void moveCircle() {

        int w = getWidth();
        int h = getHeight();

        if (pos_x < 0) { 
            delta[0] = Math.random() % 4 + 5; 
        } else if (pos_x > w - RADIUS) {
            
            delta[0] = -(Math.random() % 4 + 5);
        }

        if (pos_y < 0 ) { 
            delta[1] = Math.random() % 4 + 5; 
        } else if (pos_y > h - RADIUS) {
            
            delta[1] = -(Math.random() % 4 + 5);
        }

        pos_x += delta[0];
        pos_y += delta[1];
    }       
}

public class ClippingEx extends JFrame {
    
    public ClippingEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        setTitle("Clipping");

        add(new Surface());

        pack();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);        
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
        
            @Override
            public void run() {
                ClippingEx cl = new ClippingEx();
                cl.setVisible(true);
            }
        });        
    }
}

Đoạn code trên tạo một hình tròn di chuyển trên màn hình, hình tròn di chuyển đến đâu thì ảnh hiện ra ở đó.

Graphics2D g2d = (Graphics2D) g.create();

Cắt hình làm thay đổi một số trạng thái của đối tượng Graphics2D do đó chúng ta nên tạo một đối tượng Graphics2D mới để không làm ảnh hưởng đến việc vẽ các đối tượng khác mặc dù ở đây chúng ta chỉ vẽ một đối tượng.

g2d.clip(new Ellipse2D.Double(pos_x, pos_y, RADIUS, RADIUS));

Phương thức clip() sẽ thiết lập vùng được vẽ là một hình tròn (tạo từ lớp Elippse2D). Java sẽ không vẽ bất cứ thứ gì bên ngoài hình elip này.

if (pos_x < 0) { 
    delta[0] = Math.random() % 4 + 5; 
} else if (pos_x > w - RADIUS) {
    
    delta[0] = -(Math.random() % 4 + 5);
}

Ngoài ra chúng ta di chuyển hình tròn quanh cửa sổ cho thêm sinh động. Đoạn code trên kiểm tra tọa độ của hình tròn để không cho hình tròn di chuyển ra bên ngoài cửa sổ.

Ví dụ 2

Trong ví dụ dưới đây, chúng ta sẽ thực hiện việc giao vùng vẽ của một hình chữ nhật và một hình tròn.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;


class Surface extends JPanel
        implements ActionListener {

    private Timer timer;
    private double rotate = 1;
    private int pos_x = 8;
    private int pos_y = 8;
    private final double delta[] = {1, 1};
    
    private final int RADIUS = 60;
    

    public Surface() {

        initTimer();
    }

    private void initTimer() {

        timer = new Timer(10, this);
        timer.start();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);
        
        Shape oldClip = g2d.getClip();

        int w = getWidth();
        int h = getHeight();

        Rectangle rect = new Rectangle(0, 0, 200, 80);

        AffineTransform tx = new AffineTransform();
        tx.rotate(Math.toRadians(rotate), w / 2, h / 2);
        tx.translate(w / 2 - 100, h / 2 - 40);

        Ellipse2D circle = new Ellipse2D.Double(pos_x, pos_y,
                RADIUS, RADIUS);

        GeneralPath path = new GeneralPath();
        path.append(tx.createTransformedShape(rect), false);

        g2d.clip(circle);
        g2d.clip(path);
        
        g2d.setPaint(new Color(110, 110, 110));
        g2d.fill(circle);

        g2d.setClip(oldClip);

        g2d.draw(circle);
        g2d.draw(path);
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        doDrawing(g);
    }

    public void step() {

        int w = getWidth();
        int h = getHeight();
        
        rotate += 1;

        if (pos_x < 0) { 
            delta[0] = 1; 
        } else if (pos_x > w - RADIUS) {

            delta[0] = -1;
        }

        if (pos_y < 0) { 
            delta[1] = 1; 
        } else if (pos_y > h - RADIUS) {

            delta[1] = -1;
        }

        pos_x += delta[0];
        pos_y += delta[1];
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        step();
        repaint();
    }
}

public class ClippingShapesEx extends JFrame {

    public ClippingShapesEx() {

        initUI();
    }

    private void initUI() {

        setTitle("Clipping shapes");

        add(new Surface());

        setSize(350, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                ClippingShapesEx ex = new ClippingShapesEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta vẽ một hình tròn di chuyển xung quanh màn hình, một hình chữ nhật tự xoay tròn ở giữa màn hình. Chúng ta tô màu xám đen cho vùng được giao giữa hình chữ nhật và hình tròn.

Shape oldClip = g2d.getClip();

Khác với các ví dụ trước là chúng ta tạo một bản copy của đối tượng Graphics2D, ở đây chúng ta lưu lại trạng thái thuộc tính clip của đối tượng Graphics2D bằng phương thức getClip() rồi lưu vào đối tượng oldClip.

Rectangle rect = new Rectangle(0, 0, 200, 80);

AffineTransform tx = new AffineTransform();
tx.rotate(Math.toRadians(rotate), w / 2, h / 2);
tx.translate(w / 2 - 100, h / 2 - 40);

Bốn dòng code trên xoay hình chữ nhật xung quanh tâm của nó sử dụng lớp AffineTransform (chúng ta sẽ tìm hiểu về lớp này trong bài sau), tâm của hình chữ nhật luôn nằm giữa màn hình.

GeneralPath path = new GeneralPath();
path.append(tx.createTransformedShape(rect), false);

Phương thức createTransformedShape() tạo một hình chữ nhật từ lớp AffineTransform sau khi lớp này thực hiện một số phép biến đổi.

g2d.clip(circle);
g2d.clip(path);

g2d.setPaint(new Color(110, 110, 110));
g2d.fill(circle);

Chúng ta gọi 2 phương thức clip với 2 đối tượng hình học để chỉ cho java biết rằng chỉ được vẽ ở những vùng có cả hình chữ nhật và hình tròn rồi gọi phương thức setPaint()fill() để tô màu cho vùng đó.

g2d.setClip(oldClip);
g2d.draw(circle);
g2d.draw(path);

Sau khi chúng ta vẽ ra phần tô màu xám đen được giao giữa hình chữ nhật và hình tròn thì chúng ta gọi phương thức setClip(oldClip) để lấy lại trạng thái của vùng được vẽ lúc chưa được cắt hình (ở đây toàn màn hình) rồi gọi 2 phương thức draw() để vẽ 2 hình chữ nhật và hình tròn.

Capture

Java 2D – Kết hợp các đối tượng hình học

Trong phần này chúng ta sẽ kết hợp các đối tượng hình học với nhau.

Lớp AlphaComposite

Trong bài trước chúng ta đã dùng lớp AlphaComposite với giá trị rule để tạo hiệu ứng trong suốt, trong phần này chúng ta tiếp tục tìm hiểu về giá trị này để trộn các đối tượng với nhau.

Giả sử bạn vẽ 2 hình chữ nhật lên JPanel, trong Java đối tượng đầu tiên được vẽ được gọi là đối tượng đích (Destination – DST), đối tượng thứ hai được gọi là đối tượng nguồn (Source – SRC), giả sử bạn dùng rule là AlphaComposite.SRC_OVER, thì khi vẽ ra đối tượng nguồn sẽ nằm đè lên đối tượng đích.

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private final int rules[] = {
        AlphaComposite.DST,
        AlphaComposite.DST_ATOP,
        AlphaComposite.DST_OUT,
        AlphaComposite.SRC,
        AlphaComposite.SRC_ATOP,
        AlphaComposite.SRC_OUT
    };    
    
    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        for (int x = 20, y = 20, i = 0; i &lt; rules.length; x += 60, i++) {

            AlphaComposite ac = AlphaComposite.getInstance(rules[i], 0.8f);

            BufferedImage buffImg = new BufferedImage(60, 60, 
                                    BufferedImage.TYPE_INT_ARGB);
            Graphics2D gbi = buffImg.createGraphics();

            gbi.setPaint(Color.blue);
            gbi.fillRect(0, 0, 40, 40);
            gbi.setComposite(ac);

            gbi.setPaint(Color.green);
            gbi.fillRect(5, 5, 40, 40);

            g2d.drawImage(buffImg, x, y, null);
            gbi.dispose();
        }
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class CompositionEx extends JFrame {

    public CompositionEx() {

        add(new Surface());

        setTitle("Composition");
        setSize(400, 120);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                CompositionEx ex = new CompositionEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong đoạn code trên chúng ta vẽ hai hình vuông cắt nhau với 6 rule khác nhau.

private final int rules[] = {
    AlphaComposite.DST,
    AlphaComposite.DST_ATOP,
    AlphaComposite.DST_OUT,
    AlphaComposite.SRC,
    AlphaComposite.SRC_ATOP,
    AlphaComposite.SRC_OUT
}; 

Chúng ta lưu sẵn các rule trong mảng rules.

AlphaComposite ac = AlphaComposite.getInstance(rules[i], 0.8f);

Tiếp theo là tạo đối tượng AlphaComposite.

BufferedImage buffImg = new BufferedImage(60, 60,
        BufferedImage.TYPE_INT_ARGB);

Ở đây chúng ta không dùng đối tượng g2d để vẽ trực tiếp mà vẽ trong một BufferedImage rồi mới đưa buffer này vào trong g2d. Bạn có thể hiểu BufferedImage là một tờ giấy, chúng ta vẽ lên tờ giấy đó rồi dán lên tường là JPanel.

Graphics2D gbi = buffImg.createGraphics();

Để vẽ lên BufferedImage thì chúng ta dùng đối tượng Graphics2D tạo từ buffer với phương thức createGraphics() chứ không dùng g2d.

g2d.drawImage(buffImg, x, y, null);

Sau khi đã vẽ lên buffer thì chúng ta dùng g2d để vẽ lên JPanel với phương thức drawImage().

gbi.dispose();

Nhớ phải loại bỏ tất cả các đối tượng Graphics2D sau khi dùng xong.

Capture

Ví dụ 1

Chúng ta luyện tập tiếp với AlphaComposite thông qua ví dụ dưới đây.
import java.awt.AlphaComposite;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel implements ActionListener {

    private Image sun;
    private Image cloud;
    private Timer timer;
    private float alpha = 1f;
    
    private final int DELAY = 600;

    public Surface() {

        loadImages();
        initTimer();
    }

    private void loadImages() {

        sun = new ImageIcon("sun.png").getImage().getScaledInstance(100, 100, 
                                                     Image.SCALE_DEFAULT);
        cloud = new ImageIcon("cloud.png").getImage().getScaledInstance(200, 200,                                                           Image.SCALE_DEFAULT);
    }

    private void initTimer() {

        timer = new Timer(DELAY, this);
        timer.start();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        BufferedImage buffImg = new BufferedImage(220, 140,
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D gbi = buffImg.createGraphics();

        AlphaComposite ac = AlphaComposite.getInstance(
                AlphaComposite.SRC_OVER, alpha);

        gbi.drawImage(sun, 40, 30, null);
        gbi.setComposite(ac);
        gbi.drawImage(cloud, 0, 0, null);

        g2d.drawImage(buffImg, 20, 20, null);

        gbi.dispose();
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    private void step() {
        
        alpha -= 0.1;

        if (alpha &lt;= 0) {

            alpha = 0;
            timer.stop();
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        step();
        repaint();
    }
}

public class SunAndCloudEx extends JFrame {

    public SunAndCloudEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Sun and cloud");
        setSize(300, 210);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                SunAndCloudEx ex = new SunAndCloudEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ này, chúng ta vẽ mặt trời và đám mây, đám mây sẽ từ từ biến mất nhường chỗ cho mặt trời.

private void loadImages() {
    
    sun = new ImageIcon("sun.png").getImage().getScaledInstance(100, 100,                                                                   Image.SCALE_DEFAULT);
    cloud = new ImageIcon("cloud.png").getImage().getScaledInstance(200, 200,                                                               Image.SCALE_DEFAULT);
}

Chúng ta dùng lớp ImageIcon để load ảnh và thay đổi lại kích thước cho phù hợp nếu cần.

AlphaComposite ac = AlphaComposite.getInstance(
        AlphaComposite.SRC_OVER, alpha);

Ở đây chúng ta dùng rule là SRC_OVER để trộn màu đám mây với màu nền.

Untitled

Ví dụ 2

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;


class Surface extends JPanel {

    private final int RADIUS = 50;
    private Image image;
    private int iw;
    private int ih;
    private int x;
    private int y;
    private boolean mouseIn;

    public Surface() {

        initUI();
    }

    private void initUI() {

        loadImage();

        iw = image.getWidth(null);
        ih = image.getHeight(null);

        addMouseMotionListener(new MyMouseAdapter());
        addMouseListener(new MyMouseAdapter());
    }

    private void loadImage() {

        image = new ImageIcon("penguin.png").getImage();
    }

    @Override
    protected void paintComponent(Graphics g) {
    
        super.paintComponent(g);
        doDrawing(g);
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        int midX = (getWidth() - iw) / 2;
        int midY = (getHeight() - ih) / 2;

        BufferedImage bi = new BufferedImage(getWidth(),
                getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D bigr = bi.createGraphics();

        if (mouseIn) {
            bigr.setPaint(Color.white);
            bigr.fillOval(x - RADIUS, y - RADIUS, RADIUS * 2,
                    RADIUS * 2);
            bigr.setComposite(AlphaComposite.SrcAtop);
            bigr.drawImage(image, midX, midY, iw, ih, this);
        }

        bigr.setComposite(AlphaComposite.SrcOver.derive(0.1f));
        bigr.drawImage(image, midX, midY, iw, ih, this);
        bigr.dispose();

        g2d.drawImage(bi, 0, 0, getWidth(), getHeight(), this);

        g2d.dispose();
    }

    private class MyMouseAdapter extends MouseAdapter {

        @Override
        public void mouseExited(MouseEvent e) {
            mouseIn = false;
            repaint();
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            mouseIn = true;
        }

        @Override
        public void mouseMoved(MouseEvent e) {

            x = e.getX();
            y = e.getY();

            repaint();
        }
    }
}

public class SpotlightEx extends JFrame {

    public SpotlightEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setSize(350, 300);
        setTitle("Spotlight");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                SpotlightEx ex = new SpotlightEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ này, chúng ta tạo hiệu ứng đèn pin.

BufferedImage bi = new BufferedImage(getWidth(),
        getHeight(), BufferedImage.TYPE_INT_ARGB);

Ảnh nền của chúng ta có phần nền trong suốt (chỉ có ảnh PNG mới có) nên ở đây chúng ta dùng kiểu BufferedImage.TYPE_INT_ARGB.

if (mouseIn) {
    bigr.fillOval(x - RADIUS, y - RADIUS, RADIUS * 2,
            RADIUS * 2);
    bigr.setComposite(AlphaComposite.SrcAtop);
    bigr.drawImage(image, midX, midY, iw, ih, this);
}

Chúng ta vẽ một hình tròn tại vị trí của chuột. Giá trị AlphaComposite.SrcAtop vẽ hình rõ hoàn toàn, không mờ, tức là mặc định rule này dùng alpha là 1.0

bigr.setComposite(AlphaComposite.SrcOver.derive(0.1f));
bigr.drawImage(image, midX, midY, iw, ih, this);

Hai dòng trên vẽ toàn bộ hình còn lại. Giá trị AlphaComposite.SrcOver.derive(0.1f) vẽ hình gần như mờ hoàn toàn.

g2d.drawImage(bi, 0, 0, getWidth(), getHeight(), this);

Tất cả những thứ trên chúng ta vẽ trong buffer vì thế cuối cùng chúng ta vẽ lại toàn bộ buffer và JPanel.

Untitled

Java 2D – Độ trong suốt

Trong bài này chúng ta sẽ tìm hiểu về độ trong suốt và làm một số hiệu ứng cơ bản.

Độ trong suốt (Transparency)

Trong đồ họa máy tính, độ trong suốt của một điểm ảnh được tính toán bằng cách pha trộn điểm ảnh cần làm trong suốt với điểm ảnh nền, các điểm ảnh nền được gọi chung là kênh alpha (alpha channel), các điểm ảnh nền là một số 8 bit để biểu diễn 256 cấp độ trong suốt.

Java cung cấp lớp AlphaComposite dùng để biểu diễn độ trong suốt, lớp này thực hiện các phép tính toán để pha trộn điểm ảnh nguồn và điểm ảnh đích để tạo ra hiệu ứng trong suốt. Lớp này làm việc với 2 giá trị chính là rule (thường chúng ta dùng hằng số AlphaComposite.SRC_OVER) và value (từ 0.0f là vô hình đến 1.0f là hiển thị rõ hoàn toàn),

Ví dụ

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {    
    
    private void doDrawing(Graphics g) {        
        
        Graphics2D g2d = (Graphics2D) g.create();
        
        g2d.setPaint(Color.blue);

        for (int i = 1; i <= 10; i++) {
            
            float alpha = i * 0.1f;
            AlphaComposite alcom = AlphaComposite.getInstance(
                    AlphaComposite.SRC_OVER, alpha);
            g2d.setComposite(alcom);
            g2d.fillRect(50 * i, 20, 40, 40);
        }        
        
        g2d.dispose();
    }
        
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class TransparentRectanglesEx extends JFrame {
    
    public TransparentRectanglesEx() {
        
        initUI();
    }
    
    private void initUI() {
                
        add(new Surface());
        
        setTitle("Transparent rectangles");
        setSize(590, 120);
        setLocationRelativeTo(null);            
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    
    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                TransparentRectanglesEx ex = new TransparentRectanglesEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta vẽ 10 hình chữ nhật với 10 cấp độ trong suốt khác nhau.

float alpha = i * 0.1f;

Giá trị alpha tăng dần qua vòng lặp.

AlphaComposite alcom = AlphaComposite.getInstance(
        AlphaComposite.SRC_OVER, alpha);

Phương thức AlphaComposite.getInstance() tạo một đối tượng AlphaComposite.

g2d.setComposite(alcom);

Tiếp theo chúng ta gọi phương thức setComposite() để thiết lập thuộc tính trong suốt.

Capture

Hiệu ứng làm mờ

import java.awt.AlphaComposite;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel
        implements ActionListener {

    private Image img;
    private Timer timer;
    private float alpha = 1f;
    
    private final int DELAY = 40;
    private final int INITIAL_DELAY = 500;

    public Surface() {

        loadImage();
        setSurfaceSize();
        initTimer();
    }

    private void loadImage() {

        img = new ImageIcon("mushrooms.jpg").getImage();
    }

    private void setSurfaceSize() {

        int h = img.getHeight(this);
        int w = img.getWidth(this);
        setPreferredSize(new Dimension(w, h));
    }

    private void initTimer() {

        timer = new Timer(DELAY, this);
        timer.setInitialDelay(INITIAL_DELAY);
        timer.start();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        AlphaComposite acomp = AlphaComposite.getInstance(
                AlphaComposite.SRC_OVER, alpha);
        g2d.setComposite(acomp);
        g2d.drawImage(img, 0, 0, null);

        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    private void step() {
        
        alpha += -0.01f;

        if (alpha <= 0) {

            alpha = 0;
            timer.stop();
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        step();
        repaint();
    }
}

public class FadeOutEx extends JFrame {

    public FadeOutEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        pack();

        setTitle("Fade out");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                FadeOutEx ex = new FadeOutEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ trên, chúng ta hiển thị một ảnh và dần dần làm mờ nó qua thời gian.

private void setSurfaceSize() {
    
    int h = img.getHeight(this);
    int w = img.getWidth(this);
    setPreferredSize(new Dimension(w, h));        
}

Phương thức setSurfaceSize() cùng với phương thức pack() thiết lập kích thước cửa sổ bằng với kích thước của ảnh.

private void initTimer() {

    timer = new Timer(DELAY, this);
    timer.setInitialDelay(INITIAL_DELAY);
    timer.start();
}

Phương thức initTimer() chạy đồng hồ. Cứ sau một khoảng thời gian thì đồng hồ sẽ giải phóng sự kiện và gọi tới phương thức actionPerformed(), chúng ta cho giảm dần dần giá trị alpha và gọi phương thức repaint().

private void step() {
    
    alpha += -0.01f;

    if (alpha <= 0) {

        alpha = 0;
        timer.stop();
    }
}

Phương thức step() biểu diễn quy trình làm mờ. Thuộc tính alpha giảm dần giá trị qua thời gian và không được là số âm.

repaint();

Phương thức repaint() chỉ đơn giản là gọi đến phương thức paintComponent().

Đồng hồ chờ

Trong ví dụ dưới đây, chúng ta sẽ làm một đồng hồ chờ giống như khi bạn xem Youtube mà bị nghẽn mạng :).

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel
        implements ActionListener {

    private Timer timer;
    private int count;
    private final int INITIAL_DELAY = 200;
    private final int DELAY = 80;
    private final int NUMBER_OF_LINES = 8;
    private final int STROKE_WIDTH = 3;
    
    private final double[][] trs = {
        {0.0, 0.15, 0.30, 0.5, 0.65, 0.80, 0.9, 1.0},
        {1.0, 0.0, 0.15, 0.30, 0.5, 0.65, 0.8, 0.9},
        {0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65, 0.8},
        {0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65},
        {0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5},
        {0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3},
        {0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15},
        {0.15, 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0}
    };

    public Surface() {
        
        initTimer();
    }
    
    private void initTimer() {
        
        timer = new Timer(DELAY, this);
        timer.setInitialDelay(INITIAL_DELAY);
        timer.start();        
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);
        
        int width = getWidth();
        int height = getHeight();

        g2d.setStroke(new BasicStroke(STROKE_WIDTH, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_ROUND));
        g2d.translate(width / 2, height / 2);

        for (int i = 0; i < NUMBER_OF_LINES; i++) {
            
            float alpha = (float) trs[count % NUMBER_OF_LINES][i];
            AlphaComposite acomp = AlphaComposite.getInstance(
                    AlphaComposite.SRC_OVER, alpha);
            g2d.setComposite(acomp);

            g2d.rotate(Math.PI / 4f);
            g2d.drawLine(0, -10, 0, -40);
        }
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        repaint();
        count++;
    }
}

public class WaitingEx extends JFrame {

    public WaitingEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Waiting");
        setSize(300, 200);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                WaitingEx ex = new WaitingEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta mô phỏng hiệu ứng này bằng cách vẽ 8 đoạn thẳng với 8 giá trị alpha khác nhau.

private final double[][] trs = { 
...
};

Mảng hai chiều trs lưu trữ giá trị alpha cho 8 đoạn thẳng. Mảng này có 8 hàng, các đoạn thẳng sẽ thay phiên nhau sử dụng những giá trị này.

g2d.rotate(Math.PI/4f);
g2d.drawLine(0, -10, 0, -40);

Chúng ta dùng phương thức rotate() để xoay các đoạn thẳng xung quanh một hình tròn. Chúng ta sẽ tìm hiểu thêm về cách xoay hình trong các bài sau.

Capture

Java 2D – Các đối tượng hình học – phần 2

Trong phần này chúng ta tìm hiểu về một số đối tượng hình học cao cấp hơn và cách sử dụng màu sắc, ảnh texture…

Đa giác

Đầu tiên chúng ta vẽ một số đa giác cơ bản.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;


class Surface extends JPanel {
    
    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setPaint(new Color(150, 150, 150));

        RenderingHints rh = new RenderingHints(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
               RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setRenderingHints(rh);

        g2d.fillRect(30, 20, 50, 50);
        g2d.fillRect(120, 20, 90, 60);
        g2d.fillRoundRect(250, 20, 70, 60, 25, 25);

        g2d.fill(new Ellipse2D.Double(10, 100, 80, 100));
        g2d.fillArc(120, 130, 110, 100, 5, 150);
        g2d.fillOval(270, 130, 50, 50);
   } 

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }    
}

public class BasicShapesEx extends JFrame {

    public BasicShapesEx() {

        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());
        
        setTitle("Basic shapes");
        setSize(350, 250);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
        
            @Override
            public void run() {
                BasicShapesEx ex = new BasicShapesEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ trên chúng ta vẽ 6 đa giác là hình vuông, hình chữ nhật, hình chữ nhật có góc tròn, hình elip, hình quạt và hình tròn.

g2d.setPaint(new Color(150, 150, 150));

Các đa giác sẽ được vẽ màu xám.

g2d.fillRect(20, 20, 50, 50);
g2d.fillRect(120, 20, 90, 60);

Chúng ta dùng phương thức fillRect() để vẽ hình chữ nhật và hình vuông. Hai tham số đầu tiên là tọa độ góc trái-trên của hình chữ nhật, 2 tham số cuối cùng là chiều dài và chiều rộng của hình chữ nhật.

g2d.fillRoundRect(250, 20, 70, 60, 25, 25);

Phương thức fillRoundRect() dùng để vẽ hình chữ nhật có góc tròn, tham số cũng tương tự như fillRect() ngoại trừ 2 tham số cuối cùng là số đo độ cong của 2 đường ngang và 2 đường dọc. Bạn có thể thay đổi giá trị 2 tham số này để thấy sự khác biệt.

g2d.fill(new Ellipse2D.Double(10, 100, 80, 100));

Ở dòng code trên chúng ta dùng phương thức fill() để vẽ một đa giác cho trước là một elip.

g2d.fillArc(120, 130, 110, 100, 5, 150);

Phương thức fillArc() vẽ một hình quạt, 4 tham số đầu tiên là tọa độ góc trái-trên và góc phải-dưới của hình quạt. Tham số thứ 5 là hướng mà quạt quay tới, tham số thứ 6 là độ lớn của quạt.

g2d.fillOval(270, 130, 50, 50);

Phương thức fillOval() vẽ một hình tròn.

Capture

Đa giác lồi

Để vẽ các đa giác phức tạp hơn thì chúng ta dùng lớp GeneralPath. Lớp này vẽ đa giác bằng cách nối đoạn thẳng với tập các điểm cho trước.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.GeneralPath;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private final double points[][] = { 
        { 0, 85 }, { 75, 75 }, { 100, 10 }, { 125, 75 }, 
        { 200, 85 }, { 150, 125 }, { 160, 190 }, { 100, 150 }, 
        { 40, 190 }, { 50, 125 }, { 0, 85 } 
    };
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                             RenderingHints.VALUE_ANTIALIAS_ON);

        g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                             RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setPaint(Color.gray);
        g2d.translate(25, 5);

        GeneralPath star = new GeneralPath();

        star.moveTo(points[0][0], points[0][1]);

        for (int k = 1; k < points.length; k++)
            star.lineTo(points[k][0], points[k][1]);

        star.closePath();
        g2d.fill(star);        
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        doDrawing(g);
    }
}

public class StarEx extends JFrame {
    
    public StarEx() {

        initUI();
    }    
    
    private void initUI() {
        
        add(new Surface());
        
        setTitle("Star");
        setSize(350, 250);
        setLocationRelativeTo(null);           
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    
    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                StarEx ex = new StarEx();
                ex.setVisible(true);
            }
        });
    }    
}

Ở ví dụ trên chúng ta vẽ hình một ngôi sao từ một tập hợp điểm.

GeneralPath star = new GeneralPath();

Khởi tạo đối tượng GeneralPath.

star.moveTo(points[0][0], points[0][1]);

Đầu tiên chúng ta chuyển vị trí bắt đầu vẽ đến điểm đầu tiên.

for (int k = 1; k < points.length; k++)
    star.lineTo(points[k][0], points[k][1]);

Tiếp theo chúng ta duyệt qua tập điểm và dùng phương thức lineTo() để nối các điểm này lại.

star.closePath();
g2d.fill(star);

Sau khi đã nối xong chúng ta gọi phương thức closePath() để báo rằng việc nối đã hoàn tất và gọi phương thức fill() để tô màu ngôi sao.

Capture

Lớp Area

Lớp Area cho phép tạo các hình phức tạp hơn bằng cách trộn lẫn các hình có sẵn lại với nhau.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {
        
    public void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;
        
        RenderingHints rh = new RenderingHints(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
               RenderingHints.VALUE_RENDER_QUALITY);    
        g2d.setRenderingHints(rh);

        g2d.setPaint(Color.gray);
        
        Area a1 = new Area(new Rectangle2D.Double(20, 20, 100, 100));
        Area a2 = new Area(new Ellipse2D.Double(50, 50, 100, 100));
        
        a1.subtract(a2);
        g2d.fill(a1);
        
        Area a3 = new Area(new Rectangle2D.Double(150, 20, 100, 100));
        Area a4 = new Area(new Ellipse2D.Double(150, 20, 100, 100));        
        
        a3.subtract(a4);
        g2d.fill(a3);
        
        Area a5 = new Area(new Rectangle2D.Double(280, 20, 100, 100));
        Area a6 = new Area(new Ellipse2D.Double(320, 40, 100, 100));        
        
        a5.add(a6);
        g2d.fill(a5);        
    }

    @Override
    public void paintComponent(Graphics g) {
                
        super.paintComponent(g);
        doDrawing(g);
    }           
}

public class AreasEx extends JFrame {

    public AreasEx() {

        initUI();
    }

    private void initUI() {
        
        add(new Surface());
        
        setTitle("Areas");
        setSize(450, 200);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                AreasEx ex = new AreasEx();
                ex.setVisible(true);
            }
        });
    }
}

Đoạn code trên tạo 3 hình khác nhau.

Area a1 = new Area(new Rectangle2D.Double(20, 20, 100, 100));
Area a2 = new Area(new Ellipse2D.Double(50, 50, 100, 100));

a1.subtract(a2);
g2d.fill(a1);

Hai dòng code trên tạo 1 hình chữ nhật và một hình elip rồi cắt một vùng trên hình chữ nhật bằng với hình elip bằng phương thức substract().

Area a5 = new Area(new Rectangle2D.Double(280, 20, 100, 100));
Area a6 = new Area(new Ellipse2D.Double(320, 40, 100, 100));        

a5.add(a6);
g2d.fill(a5); 

Đối với 2 hình a5a6 thì chúng ta cho chúng nằm đè lên nhau bằng phương thức add().

Capture

Tô màu

Lớp Color là lớp chuyên làm việc với màu. Chúng ta sẽ tìm hiểu về lớp này qua ví dụ sau.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {
        
    public void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setColor(new Color(125, 167, 116));
        g2d.fillRect(10, 10, 90, 60);

        g2d.setColor(new Color(42, 179, 231));
        g2d.fillRect(130, 10, 90, 60);

        g2d.setColor(new Color(70, 67, 123));
        g2d.fillRect(250, 10, 90, 60);

        g2d.setColor(new Color(130, 100, 84));
        g2d.fillRect(10, 100, 90, 60);

        g2d.setColor(new Color(252, 211, 61));
        g2d.fillRect(130, 100, 90, 60);

        g2d.setColor(new Color(241, 98, 69));
        g2d.fillRect(250, 100, 90, 60);

        g2d.setColor(new Color(217, 146, 54));
        g2d.fillRect(10, 190, 90, 60);

        g2d.setColor(new Color(63, 121, 186));
        g2d.fillRect(130, 190, 90, 60);

        g2d.setColor(new Color(31, 21, 1));
        g2d.fillRect(250, 190, 90, 60);
    }

    @Override
    public void paintComponent(Graphics g) {
                
        super.paintComponent(g);
        doDrawing(g);
    }           
}

public class ColoursEx extends JFrame {

    public ColoursEx() {

        initUI();
    }

    private void initUI() {
        
        add(new Surface());
        
        setTitle("Colours");
        setSize(360, 300);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                ColoursEx ex = new ColoursEx();
                ex.setVisible(true);
            }
        });
    }
}

Để tạo màu thì chúng ta dùng phương thức setColor() và đưa vào một đối tượng Color. Đối tượng này khi được tạo ra nhận 3 tham số là giá trị đỏ (Red), xanh lá (Green) và xanh lam (Blue).

Capture

Gradient

Gradient là dải màu từ màu này đến màu khác. Có thể ứng dụng để làm mô phỏng bóng tối và ánh sáng.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        GradientPaint gp1 = new GradientPaint(5, 5, 
            Color.red, 20, 20, Color.black, true);

        g2d.setPaint(gp1);
        g2d.fillRect(20, 20, 300, 40);

        GradientPaint gp2 = new GradientPaint(5, 25, 
            Color.yellow, 20, 2, Color.black, true);

        g2d.setPaint(gp2);
        g2d.fillRect(20, 80, 300, 40);

        GradientPaint gp3 = new GradientPaint(5, 25, 
            Color.green, 2, 2, Color.black, true);

        g2d.setPaint(gp3);
        g2d.fillRect(20, 140, 300, 40);

        GradientPaint gp4 = new GradientPaint(25, 25, 
            Color.blue, 15, 25, Color.black, true);

        g2d.setPaint(gp4);
        g2d.fillRect(20, 200, 300, 40);

        GradientPaint gp5 = new GradientPaint(0, 0, 
             Color.orange, 0, 20, Color.black, true);

        g2d.setPaint(gp5);
        g2d.fillRect(20, 260, 300, 40);   
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }
}

public class GradientsEx extends JFrame {
    
    public GradientsEx() {

        initUI();
    }    
    
    private void initUI() {
        
        add(new Surface());
        
        setTitle("Gradients");
        setSize(350, 350);
        setLocationRelativeTo(null);            
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    
    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
        
            @Override
            public void run() {
                GradientsEx ex = new GradientsEx();
                ex.setVisible(true);
            }
        });
    }    
}

Trong ví dụ này chúng ta tạo 5 hình chữ nhật với 5 gradient khác nhau.

GradientPaint gp4 = new GradientPaint(25, 25, 
    Color.blue, 15, 25, Color.black, true);

Để hiện dải màu thì chúng ta dùng lớp GradientPaint(x1, y1, Color1, x2, y2, Color2, cyclic), trong đó các tham số có ý nghĩa là tô màu từ điểm (x1, y1) với màu Color1 dần dần chuyển sang màu Color2 tại vị trí (x2, y2). Tham số cyclic cho biết các dải màu có được phép lặp lại hay không.

g2d.setPaint(gp4);

Sau khi đã thiết lập màu gradient, chúng ta gọi phương thức setPaint() để kích hoạt gradient.

Capture

Ảnh Texture

Ảnh texture là ảnh dùng để hiện lên trên đa giác. Để sử dụng texture thì chúng ta dùng lớp TexturePaint.

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.TexturePaint;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private BufferedImage slate;
    private BufferedImage java;
    private BufferedImage pane;
    private TexturePaint slatetp;
    private TexturePaint javatp;
    private TexturePaint panetp;

    public Surface() {

        loadImages();
    }

    private void loadImages() {

        try {

            slate = ImageIO.read(new File("slate.png"));
            java = ImageIO.read(new File("java.png"));
            pane = ImageIO.read(new File("pane.png"));

        } catch (IOException ex) {

            Logger.getLogger(Surface.class.getName()).log(
                    Level.SEVERE, null, ex);
        }
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        slatetp = new TexturePaint(slate, new Rectangle(0, 0, 90, 60));
        javatp = new TexturePaint(java, new Rectangle(0, 0, 90, 60));
        panetp = new TexturePaint(pane, new Rectangle(0, 0, 90, 60));

        g2d.setPaint(slatetp);
        g2d.fillRect(10, 15, 90, 60);

        g2d.setPaint(javatp);
        g2d.fillRect(130, 15, 90, 60);

        g2d.setPaint(panetp);
        g2d.fillRect(250, 15, 90, 60);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class TexturesEx extends JFrame {

    public TexturesEx() {

        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());

        setTitle("Textures");
        setSize(360, 120);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
        
            @Override
            public void run() {
                TexturesEx ex = new TexturesEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong đoạn code trên chúng ta vẽ 3 hình chữ nhật với 3 texture khác nhau.

slate = ImageIO.read(new File("slate.png"));

Chúng ta dùng lớp ImageIO để load ảnh vào bộ nhớ.

slatetp = new TexturePaint(slate, new Rectangle(0, 0, 90, 60));

Tiếp theo chúng ta tạo ảnh texture từ lớp TexturePaint.

g2d.setPaint(slatetp);
g2d.fillRect(10, 15, 90, 60);

Cuối cùng chúng ta vẽ hình chữ nhật.

Capture