Author Archives: Phở Code

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

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

Trong phần này chúng ta sẽ vẽ các đối tượng hình học cơ bản.

Điểm

Đối tượng hình học cơ bản nhất là điểm, là một dấu chấm trên màn hình. Trong Java có sẵn lớp java.awt.Point để biểu diễn điểm nhưng không có phương thức nào để vẽ. Chúng ta sẽ vẽ điểm bằng phương thức drawLine(), phương thức này vẽ một đoạn thẳng từ điểm đầu tới điểm cuối, chúng ta dùng nó để vẽ điểm với hai điểm đầu và cuối trùng nhau.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel implements ActionListener {

    private final int DELAY = 150;
    private Timer timer;

    public Surface() {

        initTimer();
    }

    private void initTimer() {

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

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setPaint(Color.blue);

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

        Random r = new Random();

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

            int x = Math.abs(r.nextInt()) % w;
            int y = Math.abs(r.nextInt()) % h;
            g2d.drawLine(x, y, x, y);
        }
    }

    @Override
    public void paintComponent(Graphics g) {

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

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

public class PointsEx extends JFrame {

    public PointsEx() {

        initUI();
    }

    private void initUI() {

        final Surface surface = new Surface();
        add(surface);      

        setTitle("Points");
        setSize(350, 250);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

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

Đoạn code trên vẽ 2000 điểm ở vị trí ngẫu nhiên trên màn hình và thay đổi theo thời gian.

private void initTimer() {

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

Lớp javax.swing.Timer được dùng để tạo hiệu ứng chuyển động. Lớp này sẽ giải phóng sự kiện ActionEvents cứ sau một khoảng thời gian nhất định.

g2d.setPaint(Color.blue);

Điểm được vẽ sẽ có màu xanh lam.

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

Ở 2 dòng code trên, chúng ta lấy kích thước cửa sổ.

Random r = new Random();
int x = Math.abs(r.nextInt()) % w;
int y = Math.abs(r.nextInt()) % h;

Sau đó tính vị trí ngẫu nhiên dựa vào kích thước đó.

g2d.drawLine(x, y, x, y);

Chúng ta dùng phương thức drawLine() để vẽ điểm.

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

Trong phương thức xử lý sự kiện, chúng ta gọi phương thức repaint() để chương trình cập nhật lại vị trí mới của mỗi điểm.

Capture

Đoạn thẳng

Đoạn thẳng là một đối tượng hình học cơ bản. Trong lập trình thì đoạn thẳng là một đối tượng kết nối giữa hai điểm. Chúng ta dùng phương thức drawLine() để vẽ đoạn thẳng.

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;

        g2d.drawLine(30, 30, 200, 30);
        g2d.drawLine(200, 30, 30, 200);
        g2d.drawLine(30, 200, 200, 200);
        g2d.drawLine(200, 200, 30, 30);
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class LinesEx extends JFrame {

    public LinesEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Lines");
        setSize(350, 250);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

Trong đoạn code trên chúng ta vẽ bốn đoạn thẳng.

g2d.drawLine(30, 30, 200, 30);

Phương thức drawLine() nhận vào 4 tham số là tọa độ (x, y) của điểm đầu và điểm cuối.

Capture

Định dạng đoạn thẳng

Định dạng đoạn thẳng là cách mà đoạn thẳng đó được vẽ. Lớp BasicStroke là lớp định nghĩa một tập các cách vẽ khác nhau.

import java.awt.BasicStroke;
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();

        float[] dash1 = {2f, 0f, 2f};
        float[] dash2 = {1f, 1f, 1f};
        float[] dash3 = {4f, 0f, 2f};
        float[] dash4 = {4f, 4f, 1f};

        g2d.drawLine(20, 40, 250, 40);

        BasicStroke bs1 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash1, 2f);

        BasicStroke bs2 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash2, 2f);

        BasicStroke bs3 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash3, 2f);

        BasicStroke bs4 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash4, 2f);

        g2d.setStroke(bs1);
        g2d.drawLine(20, 80, 250, 80);

        g2d.setStroke(bs2);
        g2d.drawLine(20, 120, 250, 120);

        g2d.setStroke(bs3);
        g2d.drawLine(20, 160, 250, 160);

        g2d.setStroke(bs4);
        g2d.drawLine(20, 200, 250, 200);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class BasicStrokesEx extends JFrame {

    public BasicStrokesEx() {

        initUI();
    }
    
    private void initUI() {

        add(new Surface());

        setTitle("Basic strokes");
        setSize(280, 270);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

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

Trong đoạn code trên chúng ta vẽ một số kiểu đường thẳng khác nhau.

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

Ở đây chúng ta sẽ thay đổi thuộc tính stroke của đối tượng Graphics, do đó chúng ta nên tạo một bản sao của đối tượng này rồi làm việc với bản sao đó.

float[] dash1 = { 2f, 0f, 2f };
float[] dash2 = { 1f, 1f, 1f };
float[] dash3 = { 4f, 0f, 2f };
float[] dash4 = { 4f, 4f, 1f };

Thuộc tính dash định nghĩa kiểu vẽ giữa các phần hiện và các phần ẩn. Trong đoạn code trên thì dash1 có ý nghĩa là vẽ một đoạn dài 2f pixel, sau đó là khoảng trắng dài 0f pixel rồi lại vẽ đoạn thẳng dài 2f, cứ như thế.

BasicStroke bs1 = new BasicStroke(1, BasicStroke.CAP_BUTT, 
    BasicStroke.JOIN_ROUND, 1.0f, dash1, 2f );

Chúng ta tạo một đối tượng BasicStroke.

g2d.setStroke(bs1);

Phương thức setStroke() quy định kiểu vẽ cho đối tượng Graphics.

g2d.dispose();

Cuối cùng chúng ta gọi phương thức dispose() để hủy đối tượng graphics đã được copy.

Capture

Tham số CAP

Tham số CAP quy định phần viền ở góc đoạn thẳng. Tham số này có 3 giá trị là CAP_BUTT, CAP_ROUND, và CAP_SQUARE.

  • CAP_BUTT: capbutt
  • CAP_ROUND: capround
  • CAP_SQUARE: capsquare
import java.awt.BasicStroke;
import java.awt.EventQueue;
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.create();

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

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

        g2d.setRenderingHints(rh);

        BasicStroke bs1 = new BasicStroke(8, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs1);
        g2d.drawLine(20, 30, 250, 30);

        BasicStroke bs2 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs2);
        g2d.drawLine(20, 80, 250, 80);

        BasicStroke bs3 = new BasicStroke(8, BasicStroke.CAP_SQUARE,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs3);
        g2d.drawLine(20, 130, 250, 130);

        BasicStroke bs4 = new BasicStroke();
        g2d.setStroke(bs4);

        g2d.drawLine(20, 20, 20, 140);
        g2d.drawLine(250, 20, 250, 140);
        g2d.drawLine(254, 20, 254, 140);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class CapsEx extends JFrame {

    public CapsEx() {

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

        setTitle("Caps");
        setSize(280, 270);
        setLocationRelativeTo(null); 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

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

Đoạn code trên ví dụ về 3 tham số CAP khác nhau.

Capture

Tham số JOIN

Tham số JOIN quy định kiểu đường cong nối giữa hai đoạn thẳng cắt nhau. Tham số này có 3 giá trị là JOIN_BEVEL, JOIN_MITER, và JOIN_ROUND.

  • JOIN_BEVEL: joinbevel
  • JOIN_MITER: joinmiter
  • JOIN_ROUND: joinround
import java.awt.BasicStroke;
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();

        BasicStroke bs1 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs1);
        g2d.drawRect(15, 15, 80, 50);

        BasicStroke bs2 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_MITER);
        g2d.setStroke(bs2);
        g2d.drawRect(125, 15, 80, 50);

        BasicStroke bs3 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_ROUND);
        g2d.setStroke(bs3);
        g2d.drawRect(235, 15, 80, 50);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class JoinsEx extends JFrame {

    public JoinsEx() {

        initUI();
    }
    
    private void initUI() {

        add(new Surface());

        setTitle("Joins");
        setSize(340, 110);
        setLocationRelativeTo(null);  
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

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

Trong đoạn code trên chúng ta vẽ 3 hình chữ nhật với 3 kiểu JOIN khác nhau.

g2d.drawRect(15, 15, 80, 50);

Phương thức drawRect() vẽ hình chữ nhật với 4 tham số là tọa độ góc trái trên và góc phải dưới của hình chữ nhật.

Capture

Java 2D – Giới thiệu

Chào mừng bạn đến với series lập trình đồ họa 2D với Java. Series này chủ yếu nhắm đến các bạn mới bắt đầu bước vào lập trình đồ họa cơ bản.

Đồ họa Vector

Đồ họa trong máy tính có 2 loại là đồ họa vector và đồ họa raster. Trong đó đồ họa raster được dựng nên từ tập hợp các điểm ảnh (pixel), còn vector được dựng từ các đối tượng đồ họa cơ sở như điểm, đường thẳng, đường cong… các đối tượng đồ họa này được tính toán bằng các phương trình toán học. Cả hai loại đồ họa này đều có ưu và nhược điểm riêng. Đồ họa vector có các ưu điểm sau:

  • Kích thước nhỏ
  • Có thể zoom không giới hạn
  • Di chuyển, phóng to, xoay hình… không làm giảm chất lượng hình

Thư viện đồ họa 2D của Java hỗ trợ cả đồ họa vector và raster.

 

Cơ chế vẽ của Java

Bât cứ đoạn code nào bạn dùng để vẽ ra thứ gì cũng được đặt trong phương thức paintComponent(), phương thức này được override từ lớp JPanel. Phương thức này tự động được gọi bởi phương thức paint() (trong lớp java.awt.Component), ngoài phương thức paintComponent() còn có 2 phương thức khác được gọi cùng nữa là paintBorder() và paintChildren(). Nếu muốn bạn có thể override cả 2 phương thức đó cũng được nhưng thường thì cũng không cần thiết lắm. Trong series này thì chúng ta chỉ dùng đến paintComponent().

Đối tượng Graphics

Tham số của phương thức paintComponent() là một đối tượng lớp Graphics. Đối tượng này cho phép chúng ta vẽ các vật thể lên JPanel. Ngoài ra trong Java còn có lớp Graphics2D kế thừa từ lớp Graphics cung cấp các phương thức cao cấp hơn để hỗ trợ cho người lập trình vẽ một cách dễ dàng hơn.

Đối tượng này được khởi tạo bởi hệ thống và tự động được truyền vào phương thức paintComponent() khi phương thức này được gọi. Ngoài phương thức paintComponent() đối tượng Graphics còn được truyền vào hai phương thức đã nói trên là paintBorder()paintChildren(). Nếu một trong các phương thức này làm thay đổi trạng thái của đối tượng Graphics thì có thể dẫn đến một số vấn đề nên thường thì chúng ta sẽ tạo một đối tượng khác từ đối tượng Graphics gốc bằng phương thức create() và hủy đối tượng mới này với phương thức dispose(). Nếu chúng ta chỉ thay đổi các thuộc tính bình thường như màu, font chữ… thì không cần tạo mới, nhưng nếu chúng ta thay đổi các thuộc tính cao cấp bằng các phép biến đổi hình như cắt hình, xoay hình, zoom to nhỏ… thì chúng ta phải tạo một đối tượng Graphics khác.

Code ví dụ mẫu

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;
        g2d.drawString("Java 2D", 50, 50);
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class BasicEx extends JFrame {

    public BasicEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Simple Java 2D example");
        setSize(300, 200);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Đoạn code trên vẽ một đoạn text lên JPanel.

class Surface extends JPanel {
...
}

Chúng ta  tạo lớp Surface kế thừa từ lớp JPanel. Lớp này là lớp chính dùng để vẽ.

Graphics2D g2d = (Graphics2D) g;

Chúng ta tạo đối tượng Graphics2D để tận dụng một số tính năng cao cấp mà lớp Graphics không có.

g2d.drawString("Java 2D", 50, 50);

Phương thức drawString() “vẽ” một đoạn text tại vị trí (50, 50).

@Override
public void paintComponent(Graphics g) {

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

Tất cả mọi thứ đều được vẽ bên trong phương thức paintComponent(). Trong đó super.paintComponent() gọi lại phương thức paintComponent() của lớp cha là lớp JPanel, mà trong phương thức này ở lớp cha chỉ đơn giản là vẽ một cửa sổ trống không, tức là khi gọi phương thức này chúng ta làm công việc là xóa trắng toàn bộ cửa sổ, việc này rất có ích khi tạo hiệu ứng chuyển động. Ngoài ra ở đây mình không vẽ trực tiếp trong phương thức paintComponent() mà tạo phương thức doDrawing() rồi vẽ trong đó, mình làm thế là vì… thích 🙂

private void initUI() {
...
}

Phương thức initUI() làm công việc khởi tạo giao diện cho ứng dụng.

add(new Surface());

Chúng ta thêm Surface vào JFrame.

EventQueue.invokeLater(new Runnable() {

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

Phương thức invokeLater() sẽ chạy ứng dụng của chúng ta trong một luồng do EventQueue quản lý. Thực ra bạn cũng không cần đến EventQueue làm gì, chỉ cần tạo một đối tượng BasicEx rồi setVisible(true) là có thể chạy được nhưng trên tài liệu của Oracle thì lại khuyên chúng ta đặt bên trong EventQueue vì lý do là làm như vậy sẽ đảm bảo an toàn cho ứng dụng (mình cũng không hiểu tại sao) nên thôi thì mình cứ làm vậy 🙂

Capture