剪裁
最后修改于 2023 年 7 月 17 日
在 Java 2D 教程的这一部分,我们讨论裁剪。
剪裁
裁剪是将绘图限制在特定区域。这是出于效率原因,也是为了创建各种效果。在处理裁剪时,我们必须使用 `Graphics` 对象的副本,或者恢复原始的裁剪属性。更改裁剪不会影响现有的像素;它仅影响未来的渲染。
在下面的示例中,我们将图像裁剪成圆形。
package com.zetcode;
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);
}
});
}
}
一个圆在屏幕上移动,并显示底层图像的一部分。这就好像我们透过一个洞在看一样。
Graphics2D g2d = (Graphics2D) g.create();
我们创建了一个 `Graphics2D` 对象的副本。因此,更改裁剪不会影响 `Graphics2D` 对象被重用的其他 Swing 部分。
g2d.clip(new Ellipse2D.Double(pos_x, pos_y, RADIUS, RADIUS));
`clip` 方法将现有裁剪与参数中给定的形状结合起来。将结果的交集设置为裁剪。在我们的例子中,结果裁剪是圆形。
if (pos_x < 0) {
delta[0] = Math.random() % 4 + 5;
} else if (pos_x > w - RADIUS) {
delta[0] = -(Math.random() % 4 + 5);
}
如果圆圈碰到窗口的左侧或右侧,其移动方向会随机改变。顶部和底部边缘也是如此。
g2d.dispose();
完成绘制后,我们必须释放 `Graphics2D` 对象的副本。
裁剪形状
在下面的示例中,我们将裁剪到两个形状的交集:一个矩形和一个圆形。
package com.zetcode;
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);
}
});
}
}
在我们的例子中,我们有一个弹跳的圆和一个旋转的矩形。当这些形状重叠时,结果区域会填充颜色。
Shape oldClip = g2d.getClip();
由于我们没有创建 `Graphics2D` 对象的副本,我们存储了旧的裁剪以供以后使用。最后,我们必须将裁剪重置为原始裁剪。
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);
矩形正在旋转。它总是位于面板的中间。
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);
在这里,我们将绘图限制在两个形状的交集内。如果它们重叠,则会用颜色填充结果形状的内部。 `clip` 方法将初始裁剪(组件的客户端区域)与给定的两个形状结合起来。
g2d.setClip(oldClip);
使用 `setClip` 方法,我们在绘制形状之前将裁剪区域重置为旧的裁剪。与 `clip` 方法不同,`setClip` 不会组合裁剪区域。它将裁剪重置为新区域。因此,此方法应专门用于恢复旧裁剪。
在 Java 2D 教程的这一部分,我们讨论了裁剪。