Daily Archives: 17/02/2016

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