Daily Archives: 19/02/2016

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