ZetCode-图形教程-一-

ZetCode 图形教程(一)

原文:ZetCode

协议:CC BY-NC-SA 4.0

{% raw %}

Java 俄罗斯方块

原文: https://zetcode.com/tutorials/javagamestutorial/tetris/

在本章中,我们将在 Java Swing 中创建一个俄罗斯方块游戏克隆。 源代码和图像可以在作者的 Github Java-Tetris-Game 存储库中找到。

俄罗斯方块

俄罗斯方块游戏是有史以来最受欢迎的计算机游戏之一。 原始游戏是由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程的。此后,几乎所有版本的几乎所有计算机平台上都可以使用俄罗斯方块。 甚至我的手机都有俄罗斯方块游戏的修改版。

俄罗斯方块被称为下降块益智游戏。 在这个游戏中,我们有七个不同的形状,称为tetrominoes。 S 形,Z 形,T 形,L 形,线形,镜像 L 形和正方形。 这些形状中的每一个都形成有四个正方形。 形状从板上掉下来。 俄罗斯方块游戏的目的是移动和旋转形状,以便它们尽可能地适合。 如果我们设法形成一行,则该行将被破坏并得分。 我们玩俄罗斯方块游戏,直到达到顶峰。

Tetrominoes

图:Tetrominoes

开发

使用 Swing 绘画 API 绘制四方块。 我们使用java.util.Timer创建游戏周期。 形状以正方形为单位移动(而不是逐个像素移动)。 从数学上讲,游戏中的棋盘是一个简单的数字列表。

游戏启动后立即开始。 我们可以通过按p键暂停游戏。 空格键将把俄罗斯方块放在底部。 d键将片段向下一行。 (它可以用来加快下降速度。)游戏以恒定速度运行,没有实现加速。 分数是我们已删除的行数。

com/zetcode/Shape.java

package com.zetcode;

import java.util.Random;

public class Shape {

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

    private Tetrominoe pieceShape;
    private int[][] coords;

    public Shape() {

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

    void setShape(Tetrominoe shape) {

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

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

            System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
        }

        pieceShape = shape;
    }

    private void setX(int index, int x) {

        coords[index][0] = x;
    }

    private void setY(int index, int y) {

        coords[index][1] = y;
    }

    int x(int index) {

        return coords[index][0];
    }

    int y(int index) {

        return coords[index][1];
    }

    Tetrominoe getShape() {

        return pieceShape;
    }

    void setRandomShape() {

        var r = new Random();
        int x = Math.abs(r.nextInt()) % 7 + 1;

        Tetrominoe[] values = Tetrominoe.values();
        setShape(values[x]);
    }

    public int minX() {

        int m = coords[0][0];

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

            m = Math.min(m, coords[i][0]);
        }

        return m;
    }

    int minY() {

        int m = coords[0][1];

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

            m = Math.min(m, coords[i][1]);
        }

        return m;
    }

    Shape rotateLeft() {

        if (pieceShape == Tetrominoe.SquareShape) {

            return this;
        }

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

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

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

        return result;
    }

    Shape rotateRight() {

        if (pieceShape == Tetrominoe.SquareShape) {

            return this;
        }

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

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

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

        return result;
    }
}

Shape类提供有关俄罗斯方块的信息。

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

Tetrominoe枚举包含七个俄罗斯方块形状名称和一个称为NoShape的空形状。

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

coords数组保存俄罗斯方块的实际坐标。

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

coordsTable数组保存俄罗斯方块的所有可能的坐标值。 这是一个模板,所有作品都从该模板中获得其坐标值。

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

    System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
}

我们将来自coordsTable的一行坐标值放入俄罗斯方块的coords数组中。 注意ordinal()方法的用法。 在 C++ 中,枚举类型本质上是整数。 与 C++ 不同,Java 枚举是完整类,ordinal()方法返回枚举类型在枚举对象中的当前位置。

下图将帮助您更多地了解坐标值。 coords数组保存俄罗斯方块的坐标。 例如,数字(-1、1),(-1、0),(0、0)和(0,-1)表示旋转的 S 形。 下图说明了形状。

Coordinates

图:坐标

Shape rotateLeft() {

    if (pieceShape == Tetrominoe.SquareShape) {

        return this;
    }

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

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

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

    return result;
}

此代码将一块向左旋转。 正方形不必旋转。 这就是为什么我们只是将引用返回到当前对象。 查看上一张图像将有助于理解旋转。

com/zetcode/Board.java

package com.zetcode;

import com.zetcode.Shape.Tetrominoe;

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

public class Board extends JPanel {

    private final int BOARD_WIDTH = 10;
    private final int BOARD_HEIGHT = 22;
    private final int PERIOD_INTERVAL = 300;

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

    public Board(Tetris parent) {

        initBoard(parent);
    }

    private void initBoard(Tetris parent) {

        setFocusable(true);
        statusbar = parent.getStatusBar();
        addKeyListener(new TAdapter());
    }

    private int squareWidth() {

        return (int) getSize().getWidth() / BOARD_WIDTH;
    }

    private int squareHeight() {

        return (int) getSize().getHeight() / BOARD_HEIGHT;
    }

    private Tetrominoe shapeAt(int x, int y) {

        return board[(y * BOARD_WIDTH) + x];
    }

    void start() {

        curPiece = new Shape();
        board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];

        clearBoard();
        newPiece();

        timer = new Timer(PERIOD_INTERVAL, new GameCycle());
        timer.start();
    }

    private void pause() {

        isPaused = !isPaused;

        if (isPaused) {

            statusbar.setText("paused");
        } else {

            statusbar.setText(String.valueOf(numLinesRemoved));
        }

        repaint();
    }

    @Override
    public void paintComponent(Graphics g) {

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

    private void doDrawing(Graphics g) {

        var size = getSize();
        int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight();

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

            for (int j = 0; j < BOARD_WIDTH; j++) {

                Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);

                if (shape != Tetrominoe.NoShape) {

                    drawSquare(g, j * squareWidth(),
                            boardTop + i * squareHeight(), shape);
                }
            }
        }

        if (curPiece.getShape() != Tetrominoe.NoShape) {

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

                int x = curX + curPiece.x(i);
                int y = curY - curPiece.y(i);

                drawSquare(g, x * squareWidth(),
                        boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
                        curPiece.getShape());
            }
        }
    }

    private void dropDown() {

        int newY = curY;

        while (newY > 0) {

            if (!tryMove(curPiece, curX, newY - 1)) {

                break;
            }

            newY--;
        }

        pieceDropped();
    }

    private void oneLineDown() {

        if (!tryMove(curPiece, curX, curY - 1)) {

            pieceDropped();
        }
    }

    private void clearBoard() {

        for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {

            board[i] = Tetrominoe.NoShape;
        }
    }

    private void pieceDropped() {

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

            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            board[(y * BOARD_WIDTH) + x] = curPiece.getShape();
        }

        removeFullLines();

        if (!isFallingFinished) {

            newPiece();
        }
    }

    private void newPiece() {

        curPiece.setRandomShape();
        curX = BOARD_WIDTH / 2 + 1;
        curY = BOARD_HEIGHT - 1 + curPiece.minY();

        if (!tryMove(curPiece, curX, curY)) {

            curPiece.setShape(Tetrominoe.NoShape);
            timer.stop();

            var msg = String.format("Game over. Score: %d", numLinesRemoved);
            statusbar.setText(msg);
        }
    }

    private boolean tryMove(Shape newPiece, int newX, int newY) {

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

            int x = newX + newPiece.x(i);
            int y = newY - newPiece.y(i);

            if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {

                return false;
            }

            if (shapeAt(x, y) != Tetrominoe.NoShape) {

                return false;
            }
        }

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

        repaint();

        return true;
    }

    private void removeFullLines() {

        int numFullLines = 0;

        for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {

            boolean lineIsFull = true;

            for (int j = 0; j < BOARD_WIDTH; j++) {

                if (shapeAt(j, i) == Tetrominoe.NoShape) {

                    lineIsFull = false;
                    break;
                }
            }

            if (lineIsFull) {

                numFullLines++;

                for (int k = i; k < BOARD_HEIGHT - 1; k++) {
                    for (int j = 0; j < BOARD_WIDTH; j++) {
                        board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
                    }
                }
            }
        }

        if (numFullLines > 0) {

            numLinesRemoved += numFullLines;

            statusbar.setText(String.valueOf(numLinesRemoved));
            isFallingFinished = true;
            curPiece.setShape(Tetrominoe.NoShape);
        }
    }

    private void drawSquare(Graphics g, int x, int y, Tetrominoe shape) {

        Color colors[] = {new Color(0, 0, 0), new Color(204, 102, 102),
                new Color(102, 204, 102), new Color(102, 102, 204),
                new Color(204, 204, 102), new Color(204, 102, 204),
                new Color(102, 204, 204), new Color(218, 170, 0)
        };

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

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

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

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

    private class GameCycle implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {

            doGameCycle();
        }
    }

    private void doGameCycle() {

        update();
        repaint();
    }

    private void update() {

        if (isPaused) {

            return;
        }

        if (isFallingFinished) {

            isFallingFinished = false;
            newPiece();
        } else {

            oneLineDown();
        }
    }

    class TAdapter extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            if (curPiece.getShape() == Tetrominoe.NoShape) {

                return;
            }

            int keycode = e.getKeyCode();

            // Java 12 switch expressions
            switch (keycode) {

                case KeyEvent.VK_P -> pause();
                case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
                case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
                case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
                case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
                case KeyEvent.VK_SPACE -> dropDown();
                case KeyEvent.VK_D -> oneLineDown();
            }
        }
    }
}

最后,我们有Board.java文件。 这是游戏逻辑所在的位置。

private final int BOARD_WIDTH = 10;
private final int BOARD_HEIGHT = 22;
private final int PERIOD_INTERVAL = 300;

我们有四个常数。 BOARD_WIDTHBOARD_HEIGHT定义了电路板的大小。 PERIOD_INTERVAL常数定义游戏的速度。

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

一些重要的变量被初始化。 isFallingFinished确定俄罗斯方块形状是否已经完成下降,然后我们需要创建一个新形状。 isStarted用于检查游戏是否已经开始。 同样,isPaused用于检查游戏是否暂停。 numLinesRemoved计算到目前为止我们已删除的行数。 curXcurY确定下降的俄罗斯方块形状的实际位置。

private int squareWidth() {

    return (int) getSize().getWidth() / BOARD_WIDTH;
}

private int squareHeight() {

    return (int) getSize().getHeight() / BOARD_HEIGHT;
}

这些线确定单个 Tetrominoe 正方形的宽度和高度。

private Tetrominoe shapeAt(int x, int y) {

    return board[(y * BOARD_WIDTH) + x];
}

我们确定给定坐标处的形状。 形状存储在board数组中。

void start() {

    curPiece = new Shape();
    board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];
...

我们创建一个新的当前形状和一个新的板。

clearBoard();
newPiece();

清理板并初始化新的下降片。

timer = new Timer(PERIOD_INTERVAL, new GameCycle());
timer.start();

我们创建一个计时器。 计时器以PERIOD_INTERVAL间隔执行,从而创建游戏周期。

private void pause() {

    isPaused = !isPaused;

    if (isPaused) {

        statusbar.setText("paused");
    } else {

        statusbar.setText(String.valueOf(numLinesRemoved));
    }

    repaint();
}

pause()方法暂停或继续游戏。 游戏暂停后,我们会在状态栏中显示paused消息。

doDrawing()方法内部,我们在板上绘制了所有对象。 这幅画有两个步骤。

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

    for (int j = 0; j < BOARD_WIDTH; j++) {

        Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);

        if (shape != Tetrominoe.NoShape) {

            drawSquare(g, j * squareWidth(),
                    boardTop + i * squareHeight(), shape);
        }
    }
}

在第一步中,我们绘制掉落到板底部的所有形状或形状的其余部分。 所有正方形都记在板数组中。 我们使用shapeAt()方法访问它。

if (curPiece.getShape() != Tetrominoe.NoShape) {

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

        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);

        drawSquare(g, x * squareWidth(),
                boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
                curPiece.getShape());
    }
}

在第二步中,我们绘制实际的下降部分。

private void dropDown() {

    int newY = curY;

    while (newY > 0) {

        if (!tryMove(curPiece, curX, newY - 1)) {

            break;
        }

        newY--;
    }

    pieceDropped();
}

如果按Space键,则该片段将落到底部。 我们只是简单地尝试将一块下降到另一条俄罗斯方块下降的底部或顶部。 当俄罗斯方块结束下降时,将调用pieceDropped()

private void oneLineDown() {

    if (!tryMove(curPiece, curX, curY - 1)) {

        pieceDropped();
    }
}

oneLineDown()方法中,我们尝试将下降片向下移动一行,直到完全下降。

private void clearBoard() {

    for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {

        board[i] = Tetrominoe.NoShape;
    }
}

clearBoard()方法用空的Tetrominoe.NoShape填充电路板。 稍后将其用于碰撞检测。

private void pieceDropped() {

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

        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        board[(y * BOARD_WIDTH) + x] = curPiece.getShape();
    }

    removeFullLines();

    if (!isFallingFinished) {

        newPiece();
    }
}

pieceDropped()方法将下降的片段放入board数组。 棋盘再次保持了所有碎片的正方形和已经落下的碎片的剩余部分。 当一块完成落下时,就该检查我们是否可以从板上去除一些线了。 这是removeFullLines()方法的工作。 然后,我们创建一个新作品,或更准确地说,我们尝试创建一个新作品。

private void newPiece() {

    curPiece.setRandomShape();
    curX = BOARD_WIDTH / 2 + 1;
    curY = BOARD_HEIGHT - 1 + curPiece.minY();

    if (!tryMove(curPiece, curX, curY)) {

        curPiece.setShape(Tetrominoe.NoShape);
        timer.stop();

        var msg = String.format("Game over. Score: %d", numLinesRemoved);
        statusbar.setText(msg);
    }
}

newPiece()方法创建一个新的俄罗斯方块。 作品获得了新的随机形状。 然后,我们计算初始curXcurY值。 如果我们不能移动到初始位置,则游戏结束了,我们结束了。 计时器停止运行,我们在状态栏上显示Game over包含分数的字符串。

private boolean tryMove(Shape newPiece, int newX, int newY) {

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

        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);

        if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {

            return false;
        }

        if (shapeAt(x, y) != Tetrominoe.NoShape) {

            return false;
        }
    }

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

    repaint();

    return true;
}

tryMove()方法尝试移动俄罗斯方块。 如果该方法已到达板边界或与已经跌落的俄罗斯方块碎片相邻,则返回false

private void removeFullLines() {

    int numFullLines = 0;

    for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {

        boolean lineIsFull = true;

        for (int j = 0; j < BOARD_WIDTH; j++) {

            if (shapeAt(j, i) == Tetrominoe.NoShape) {

                lineIsFull = false;
                break;
            }
        }

        if (lineIsFull) {

            numFullLines++;

            for (int k = i; k < BOARD_HEIGHT - 1; k++) {
                for (int j = 0; j < BOARD_WIDTH; j++) {
                    board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
                }
            }
        }
    }

    if (numFullLines > 0) {

        numLinesRemoved += numFullLines;

        statusbar.setText(String.valueOf(numLinesRemoved));
        isFallingFinished = true;
        curPiece.setShape(Tetrominoe.NoShape);
    }
}

removeFullLines()方法内部,我们检查板上的所有行中是否有完整的行。 如果至少有一条实线,则将其删除。 找到整条线后,我们增加计数器。 我们将整行上方的所有行向下移动一行。 这样我们就破坏了整个生产线。 注意,在我们的俄罗斯方块游戏中,我们使用了所谓的天真重力。 这意味着正方形可能会漂浮在空白间隙上方。

private void drawSquare(Graphics g, int x, int y, Tetrominoe shape) {

    Color colors[] = {new Color(0, 0, 0), new Color(204, 102, 102),
            new Color(102, 204, 102), new Color(102, 102, 204),
            new Color(204, 204, 102), new Color(204, 102, 204),
            new Color(102, 204, 204), new Color(218, 170, 0)
    };

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

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

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

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

每个俄罗斯方块都有四个正方形。 每个正方形都使用drawSquare()方法绘制。 俄罗斯方块有不同的颜色。 正方形的左侧和顶部以较亮的颜色绘制。 类似地,底部和右侧用较深的颜色绘制。 这是为了模拟 3D 边缘。

private class GameCycle implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {

        doGameCycle();
    }
}

GameCycle中,我们调用doGameCycle()方法,以创建游戏周期。

private void doGameCycle() {

    update();
    repaint();
}

游戏分为游戏周期。 每个循环都会抽水游戏并重画棋盘。

private void update() {

    if (isPaused) {

        return;
    }

    if (isFallingFinished) {

        isFallingFinished = false;
        newPiece();
    } else {

        oneLineDown();
    }
}

update()代表游戏的第一步。 下降的零件向下一行,或者如果前一个零件已完成下降,则创建新的零件。

private class TAdapter extends KeyAdapter {

    @Override
    public void keyPressed(KeyEvent e) {
        ...

游戏由光标键控制。 我们在KeyAdapter中检查关键事件。

int keycode = e.getKeyCode();

我们使用getKeyCode()方法获得按键代码。

// Java 12 switch expressions
switch (keycode) {

    case KeyEvent.VK_P -> pause();
    case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
    case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
    case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
    case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
    case KeyEvent.VK_SPACE -> dropDown();
    case KeyEvent.VK_D -> oneLineDown();
}

使用 Java 12 switch表达式,我们将键事件绑定到方法。 例如,使用 Space 键,我们将下降的俄罗斯方块放下。

com/zetcode/Tetris.java

package com.zetcode;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;

/*
Java Tetris game clone

Author: Jan Bodnar
Website: http://zetcode.com
 */
public class Tetris extends JFrame {

    private JLabel statusbar;

    public Tetris() {

        initUI();
    }

    private void initUI() {

        statusbar = new JLabel(" 0");
        add(statusbar, BorderLayout.SOUTH);

        var board = new Board(this);
        add(board);
        board.start();

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

    JLabel getStatusBar() {

        return statusbar;
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var game = new Tetris();
            game.setVisible(true);
        });
    }
}

Tetris.java文件中,我们设置了游戏。 我们创建一个玩游戏的棋盘。 我们创建一个状态栏。

statusbar = new JLabel(" 0");
add(statusbar, BorderLayout.SOUTH);

分数显示在位于板底部的标签中。

var board = new Board(this);
add(board);
board.start();

棋盘被创建并添加到容器中。 start()方法启动俄罗斯方块游戏。

Tetris

图:俄罗斯方块

这是俄罗斯方块游戏。

{% endraw %}

Java 吃豆人

原文: https://zetcode.com/tutorials/javagamestutorial/pacman/

在 Java 2D 游戏教程的这一部分中,我们将创建一个简单的 Pacman 游戏克隆。

Pacman 是一款街机游戏,最初由日本公司 Namco 在 1980 年开发。Pacman 成为有史以来最受欢迎的街机游戏之一。

开发

以下代码示例是 Brian Postma 对 Pacman 游戏的翻版,可从 http://www.brianpostma.com 获得。 修改并简化了代码,以便于理解。

游戏的目标是收集迷宫中的所有点并避开鬼魂。 吃豆人的动画制作有两种方式:在迷宫中的位置和身体。 我们根据方向为他的身体制作四幅图像。 该动画用于创建吃豆人张开和闭合嘴巴的幻觉。 迷宫由15x15正方形组成。 迷宫的结构基于简单的整数数组。 吃豆人有三命。 我们还计算分数。

游戏包含两个文件:Board.javaPacman.java

Board.java

package com.zetcode;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Board extends JPanel implements ActionListener {

    private Dimension d;
    private final Font smallFont = new Font("Helvetica", Font.BOLD, 14);

    private Image ii;
    private final Color dotColor = new Color(192, 192, 0);
    private Color mazeColor;

    private boolean inGame = false;
    private boolean dying = false;

    private final int BLOCK_SIZE = 24;
    private final int N_BLOCKS = 15;
    private final int SCREEN_SIZE = N_BLOCKS * BLOCK_SIZE;
    private final int PAC_ANIM_DELAY = 2;
    private final int PACMAN_ANIM_COUNT = 4;
    private final int MAX_GHOSTS = 12;
    private final int PACMAN_SPEED = 6;

    private int pacAnimCount = PAC_ANIM_DELAY;
    private int pacAnimDir = 1;
    private int pacmanAnimPos = 0;
    private int N_GHOSTS = 6;
    private int pacsLeft, score;
    private int[] dx, dy;
    private int[] ghost_x, ghost_y, ghost_dx, ghost_dy, ghostSpeed;

    private Image ghost;
    private Image pacman1, pacman2up, pacman2left, pacman2right, pacman2down;
    private Image pacman3up, pacman3down, pacman3left, pacman3right;
    private Image pacman4up, pacman4down, pacman4left, pacman4right;

    private int pacman_x, pacman_y, pacmand_x, pacmand_y;
    private int req_dx, req_dy, view_dx, view_dy;

    private final short levelData[] = {
        19, 26, 26, 26, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 22,
        21, 0, 0, 0, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20,
        21, 0, 0, 0, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20,
        21, 0, 0, 0, 17, 16, 16, 24, 16, 16, 16, 16, 16, 16, 20,
        17, 18, 18, 18, 16, 16, 20, 0, 17, 16, 16, 16, 16, 16, 20,
        17, 16, 16, 16, 16, 16, 20, 0, 17, 16, 16, 16, 16, 24, 20,
        25, 16, 16, 16, 24, 24, 28, 0, 25, 24, 24, 16, 20, 0, 21,
        1, 17, 16, 20, 0, 0, 0, 0, 0, 0, 0, 17, 20, 0, 21,
        1, 17, 16, 16, 18, 18, 22, 0, 19, 18, 18, 16, 20, 0, 21,
        1, 17, 16, 16, 16, 16, 20, 0, 17, 16, 16, 16, 20, 0, 21,
        1, 17, 16, 16, 16, 16, 20, 0, 17, 16, 16, 16, 20, 0, 21,
        1, 17, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 20, 0, 21,
        1, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 0, 21,
        1, 25, 24, 24, 24, 24, 24, 24, 24, 24, 16, 16, 16, 18, 20,
        9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 25, 24, 24, 24, 28
    };

    private final int validSpeeds[] = {1, 2, 3, 4, 6, 8};
    private final int maxSpeed = 6;

    private int currentSpeed = 3;
    private short[] screenData;
    private Timer timer;

    public Board() {

        loadImages();
        initVariables();
        initBoard();
    }

    private void initBoard() {

        addKeyListener(new TAdapter());

        setFocusable(true);

        setBackground(Color.black);
    }

    private void initVariables() {

        screenData = new short[N_BLOCKS * N_BLOCKS];
        mazeColor = new Color(5, 100, 5);
        d = new Dimension(400, 400);
        ghost_x = new int[MAX_GHOSTS];
        ghost_dx = new int[MAX_GHOSTS];
        ghost_y = new int[MAX_GHOSTS];
        ghost_dy = new int[MAX_GHOSTS];
        ghostSpeed = new int[MAX_GHOSTS];
        dx = new int[4];
        dy = new int[4];

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

    @Override
    public void addNotify() {
        super.addNotify();

        initGame();
    }

    private void doAnim() {

        pacAnimCount--;

        if (pacAnimCount <= 0) {
            pacAnimCount = PAC_ANIM_DELAY;
            pacmanAnimPos = pacmanAnimPos + pacAnimDir;

            if (pacmanAnimPos == (PACMAN_ANIM_COUNT - 1) || pacmanAnimPos == 0) {
                pacAnimDir = -pacAnimDir;
            }
        }
    }

    private void playGame(Graphics2D g2d) {

        if (dying) {

            death();

        } else {

            movePacman();
            drawPacman(g2d);
            moveGhosts(g2d);
            checkMaze();
        }
    }

    private void showIntroScreen(Graphics2D g2d) {

        g2d.setColor(new Color(0, 32, 48));
        g2d.fillRect(50, SCREEN_SIZE / 2 - 30, SCREEN_SIZE - 100, 50);
        g2d.setColor(Color.white);
        g2d.drawRect(50, SCREEN_SIZE / 2 - 30, SCREEN_SIZE - 100, 50);

        String s = "Press s to start.";
        Font small = new Font("Helvetica", Font.BOLD, 14);
        FontMetrics metr = this.getFontMetrics(small);

        g2d.setColor(Color.white);
        g2d.setFont(small);
        g2d.drawString(s, (SCREEN_SIZE - metr.stringWidth(s)) / 2, SCREEN_SIZE / 2);
    }

    private void drawScore(Graphics2D g) {

        int i;
        String s;

        g.setFont(smallFont);
        g.setColor(new Color(96, 128, 255));
        s = "Score: " + score;
        g.drawString(s, SCREEN_SIZE / 2 + 96, SCREEN_SIZE + 16);

        for (i = 0; i < pacsLeft; i++) {
            g.drawImage(pacman3left, i * 28 + 8, SCREEN_SIZE + 1, this);
        }
    }

    private void checkMaze() {

        short i = 0;
        boolean finished = true;

        while (i < N_BLOCKS * N_BLOCKS && finished) {

            if ((screenData[i] & 48) != 0) {
                finished = false;
            }

            i++;
        }

        if (finished) {

            score += 50;

            if (N_GHOSTS < MAX_GHOSTS) {
                N_GHOSTS++;
            }

            if (currentSpeed < maxSpeed) {
                currentSpeed++;
            }

            initLevel();
        }
    }

    private void death() {

        pacsLeft--;

        if (pacsLeft == 0) {
            inGame = false;
        }

        continueLevel();
    }

    private void moveGhosts(Graphics2D g2d) {

        short i;
        int pos;
        int count;

        for (i = 0; i < N_GHOSTS; i++) {
            if (ghost_x[i] % BLOCK_SIZE == 0 && ghost_y[i] % BLOCK_SIZE == 0) {
                pos = ghost_x[i] / BLOCK_SIZE + N_BLOCKS * (int) (ghost_y[i] / BLOCK_SIZE);

                count = 0;

                if ((screenData[pos] & 1) == 0 && ghost_dx[i] != 1) {
                    dx[count] = -1;
                    dy[count] = 0;
                    count++;
                }

                if ((screenData[pos] & 2) == 0 && ghost_dy[i] != 1) {
                    dx[count] = 0;
                    dy[count] = -1;
                    count++;
                }

                if ((screenData[pos] & 4) == 0 && ghost_dx[i] != -1) {
                    dx[count] = 1;
                    dy[count] = 0;
                    count++;
                }

                if ((screenData[pos] & 8) == 0 && ghost_dy[i] != -1) {
                    dx[count] = 0;
                    dy[count] = 1;
                    count++;
                }

                if (count == 0) {

                    if ((screenData[pos] & 15) == 15) {
                        ghost_dx[i] = 0;
                        ghost_dy[i] = 0;
                    } else {
                        ghost_dx[i] = -ghost_dx[i];
                        ghost_dy[i] = -ghost_dy[i];
                    }

                } else {

                    count = (int) (Math.random() * count);

                    if (count > 3) {
                        count = 3;
                    }

                    ghost_dx[i] = dx[count];
                    ghost_dy[i] = dy[count];
                }

            }

            ghost_x[i] = ghost_x[i] + (ghost_dx[i] * ghostSpeed[i]);
            ghost_y[i] = ghost_y[i] + (ghost_dy[i] * ghostSpeed[i]);
            drawGhost(g2d, ghost_x[i] + 1, ghost_y[i] + 1);

            if (pacman_x > (ghost_x[i] - 12) && pacman_x < (ghost_x[i] + 12)
                    && pacman_y > (ghost_y[i] - 12) && pacman_y < (ghost_y[i] + 12)
                    && inGame) {

                dying = true;
            }
        }
    }

    private void drawGhost(Graphics2D g2d, int x, int y) {

        g2d.drawImage(ghost, x, y, this);
    }

    private void movePacman() {

        int pos;
        short ch;

        if (req_dx == -pacmand_x && req_dy == -pacmand_y) {
            pacmand_x = req_dx;
            pacmand_y = req_dy;
            view_dx = pacmand_x;
            view_dy = pacmand_y;
        }

        if (pacman_x % BLOCK_SIZE == 0 && pacman_y % BLOCK_SIZE == 0) {
            pos = pacman_x / BLOCK_SIZE + N_BLOCKS * (int) (pacman_y / BLOCK_SIZE);
            ch = screenData[pos];

            if ((ch & 16) != 0) {
                screenData[pos] = (short) (ch & 15);
                score++;
            }

            if (req_dx != 0 || req_dy != 0) {
                if (!((req_dx == -1 && req_dy == 0 && (ch & 1) != 0)
                        || (req_dx == 1 && req_dy == 0 && (ch & 4) != 0)
                        || (req_dx == 0 && req_dy == -1 && (ch & 2) != 0)
                        || (req_dx == 0 && req_dy == 1 && (ch & 8) != 0))) {
                    pacmand_x = req_dx;
                    pacmand_y = req_dy;
                    view_dx = pacmand_x;
                    view_dy = pacmand_y;
                }
            }

            // Check for standstill
            if ((pacmand_x == -1 && pacmand_y == 0 && (ch & 1) != 0)
                    || (pacmand_x == 1 && pacmand_y == 0 && (ch & 4) != 0)
                    || (pacmand_x == 0 && pacmand_y == -1 && (ch & 2) != 0)
                    || (pacmand_x == 0 && pacmand_y == 1 && (ch & 8) != 0)) {
                pacmand_x = 0;
                pacmand_y = 0;
            }
        }
        pacman_x = pacman_x + PACMAN_SPEED * pacmand_x;
        pacman_y = pacman_y + PACMAN_SPEED * pacmand_y;
    }

    private void drawPacman(Graphics2D g2d) {

        if (view_dx == -1) {
            drawPacnanLeft(g2d);
        } else if (view_dx == 1) {
            drawPacmanRight(g2d);
        } else if (view_dy == -1) {
            drawPacmanUp(g2d);
        } else {
            drawPacmanDown(g2d);
        }
    }

    private void drawPacmanUp(Graphics2D g2d) {

        switch (pacmanAnimPos) {
            case 1:
                g2d.drawImage(pacman2up, pacman_x + 1, pacman_y + 1, this);
                break;
            case 2:
                g2d.drawImage(pacman3up, pacman_x + 1, pacman_y + 1, this);
                break;
            case 3:
                g2d.drawImage(pacman4up, pacman_x + 1, pacman_y + 1, this);
                break;
            default:
                g2d.drawImage(pacman1, pacman_x + 1, pacman_y + 1, this);
                break;
        }
    }

    private void drawPacmanDown(Graphics2D g2d) {

        switch (pacmanAnimPos) {
            case 1:
                g2d.drawImage(pacman2down, pacman_x + 1, pacman_y + 1, this);
                break;
            case 2:
                g2d.drawImage(pacman3down, pacman_x + 1, pacman_y + 1, this);
                break;
            case 3:
                g2d.drawImage(pacman4down, pacman_x + 1, pacman_y + 1, this);
                break;
            default:
                g2d.drawImage(pacman1, pacman_x + 1, pacman_y + 1, this);
                break;
        }
    }

    private void drawPacnanLeft(Graphics2D g2d) {

        switch (pacmanAnimPos) {
            case 1:
                g2d.drawImage(pacman2left, pacman_x + 1, pacman_y + 1, this);
                break;
            case 2:
                g2d.drawImage(pacman3left, pacman_x + 1, pacman_y + 1, this);
                break;
            case 3:
                g2d.drawImage(pacman4left, pacman_x + 1, pacman_y + 1, this);
                break;
            default:
                g2d.drawImage(pacman1, pacman_x + 1, pacman_y + 1, this);
                break;
        }
    }

    private void drawPacmanRight(Graphics2D g2d) {

        switch (pacmanAnimPos) {
            case 1:
                g2d.drawImage(pacman2right, pacman_x + 1, pacman_y + 1, this);
                break;
            case 2:
                g2d.drawImage(pacman3right, pacman_x + 1, pacman_y + 1, this);
                break;
            case 3:
                g2d.drawImage(pacman4right, pacman_x + 1, pacman_y + 1, this);
                break;
            default:
                g2d.drawImage(pacman1, pacman_x + 1, pacman_y + 1, this);
                break;
        }
    }

    private void drawMaze(Graphics2D g2d) {

        short i = 0;
        int x, y;

        for (y = 0; y < SCREEN_SIZE; y += BLOCK_SIZE) {
            for (x = 0; x < SCREEN_SIZE; x += BLOCK_SIZE) {

                g2d.setColor(mazeColor);
                g2d.setStroke(new BasicStroke(2));

                if ((screenData[i] & 1) != 0) { 
                    g2d.drawLine(x, y, x, y + BLOCK_SIZE - 1);
                }

                if ((screenData[i] & 2) != 0) { 
                    g2d.drawLine(x, y, x + BLOCK_SIZE - 1, y);
                }

                if ((screenData[i] & 4) != 0) { 
                    g2d.drawLine(x + BLOCK_SIZE - 1, y, x + BLOCK_SIZE - 1,
                            y + BLOCK_SIZE - 1);
                }

                if ((screenData[i] & 8) != 0) { 
                    g2d.drawLine(x, y + BLOCK_SIZE - 1, x + BLOCK_SIZE - 1,
                            y + BLOCK_SIZE - 1);
                }

                if ((screenData[i] & 16) != 0) { 
                    g2d.setColor(dotColor);
                    g2d.fillRect(x + 11, y + 11, 2, 2);
                }

                i++;
            }
        }
    }

    private void initGame() {

        pacsLeft = 3;
        score = 0;
        initLevel();
        N_GHOSTS = 6;
        currentSpeed = 3;
    }

    private void initLevel() {

        int i;
        for (i = 0; i < N_BLOCKS * N_BLOCKS; i++) {
            screenData[i] = levelData[i];
        }

        continueLevel();
    }

    private void continueLevel() {

        short i;
        int dx = 1;
        int random;

        for (i = 0; i < N_GHOSTS; i++) {

            ghost_y[i] = 4 * BLOCK_SIZE;
            ghost_x[i] = 4 * BLOCK_SIZE;
            ghost_dy[i] = 0;
            ghost_dx[i] = dx;
            dx = -dx;
            random = (int) (Math.random() * (currentSpeed + 1));

            if (random > currentSpeed) {
                random = currentSpeed;
            }

            ghostSpeed[i] = validSpeeds[random];
        }

        pacman_x = 7 * BLOCK_SIZE;
        pacman_y = 11 * BLOCK_SIZE;
        pacmand_x = 0;
        pacmand_y = 0;
        req_dx = 0;
        req_dy = 0;
        view_dx = -1;
        view_dy = 0;
        dying = false;
    }

    private void loadImages() {

        ghost = new ImageIcon("images/ghost.png").getImage();
        pacman1 = new ImageIcon("images/pacman.png").getImage();
        pacman2up = new ImageIcon("images/up1.png").getImage();
        pacman3up = new ImageIcon("images/up2.png").getImage();
        pacman4up = new ImageIcon("images/up3.png").getImage();
        pacman2down = new ImageIcon("images/down1.png").getImage();
        pacman3down = new ImageIcon("images/down2.png").getImage();
        pacman4down = new ImageIcon("images/down3.png").getImage();
        pacman2left = new ImageIcon("images/left1.png").getImage();
        pacman3left = new ImageIcon("images/left2.png").getImage();
        pacman4left = new ImageIcon("images/left3.png").getImage();
        pacman2right = new ImageIcon("images/right1.png").getImage();
        pacman3right = new ImageIcon("images/right2.png").getImage();
        pacman4right = new ImageIcon("images/right3.png").getImage();

    }

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

        doDrawing(g);
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setColor(Color.black);
        g2d.fillRect(0, 0, d.width, d.height);

        drawMaze(g2d);
        drawScore(g2d);
        doAnim();

        if (inGame) {
            playGame(g2d);
        } else {
            showIntroScreen(g2d);
        }

        g2d.drawImage(ii, 5, 5, this);
        Toolkit.getDefaultToolkit().sync();
        g2d.dispose();
    }

    class TAdapter extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            int key = e.getKeyCode();

            if (inGame) {
                if (key == KeyEvent.VK_LEFT) {
                    req_dx = -1;
                    req_dy = 0;
                } else if (key == KeyEvent.VK_RIGHT) {
                    req_dx = 1;
                    req_dy = 0;
                } else if (key == KeyEvent.VK_UP) {
                    req_dx = 0;
                    req_dy = -1;
                } else if (key == KeyEvent.VK_DOWN) {
                    req_dx = 0;
                    req_dy = 1;
                } else if (key == KeyEvent.VK_ESCAPE && timer.isRunning()) {
                    inGame = false;
                } else if (key == KeyEvent.VK_PAUSE) {
                    if (timer.isRunning()) {
                        timer.stop();
                    } else {
                        timer.start();
                    }
                }
            } else {
                if (key == 's' || key == 'S') {
                    inGame = true;
                    initGame();
                }
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {

            int key = e.getKeyCode();

            if (key == Event.LEFT || key == Event.RIGHT
                    || key == Event.UP || key == Event.DOWN) {
                req_dx = 0;
                req_dy = 0;
            }
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        repaint();
    }
}

用光标键控制吃豆人。 Esc 键完成游戏,暂停键暂停游戏。

private int pacman_x, pacman_y, pacmand_x, pacmand_y;

前两个变量存储吃豆子精灵的 x 和 y 坐标。 最后两个变量是水平和垂直方向的增量变化。

private final short levelData[] = {
    19, 26, 26, 26, 18, 18, 18, 18, ...
};

这些数字组成了迷宫。 它们提供了信息,我们可以据此创建角点。 1 号是左上角。 数字 2、4 和 8 分别代表顶角,右角和底角。 16 号是重点。 可以添加这些数字,例如左上角的数字 19 表示正方形将具有顶部和左侧边框以及一个点(16 + 2 + 1)。

private void doAnim() {

    pacAnimCount--;

    if (pacAnimCount <= 0) {
        pacAnimCount = PAC_ANIM_DELAY;
        pacmanAnimPos = pacmanAnimPos + pacAnimDir;

        if (pacmanAnimPos == (PACMAN_ANIM_COUNT - 1) || pacmanAnimPos == 0) {
            pacAnimDir = -pacAnimDir;
        }
    }
}

doAnim()pacmanAnimPos变量进行计数,该变量确定绘制哪种吃豆子图像。 有四个吃豆人图像。 还有一个PAC_ANIM_DELAY常数,它会使动画变慢。 否则,吃豆人会太快张开嘴。

boolean finished = true;

while (i < N_BLOCKS * N_BLOCKS && finished) {

    if ((screenData[i] & 48) != 0) {
        finished = false;
    }

    i++;
}

此代码是checkMaze()方法的一部分。 它检查吃豆人是否还有剩余的食物要吃。 数字 16 代表一个点。 如果所有积分都被消耗掉了,我们将进入下一个层次。 (在我们的例子中,我们只是重新启动游戏。)

接下来,我们将研究moveGhosts()方法。 鬼魂移动一个正方形,然后决定是否改变方向。

if (ghost_x[i] % BLOCK_SIZE == 0 && ghost_y[i] % BLOCK_SIZE == 0) {

我们仅在完成移动一个正方形后才继续。

pos = pacman_x / BLOCK_SIZE + N_BLOCKS * (int) (pacman_y / BLOCK_SIZE);

这条线确定了幻影的位置; 在哪个位置/正方形。 有 225 个理论职位。 (鬼不能在墙上移动。)

if ((screenData[pos] & 1) == 0 && ghost_dx[i] != 1) {
    dx[count] = -1;
    dy[count] = 0;
    count++;
}

如果左侧没有障碍物并且幻影尚未向右移动,则幻影将向左移动。 该代码的真正含义是什么? 如果幽灵进入隧道,他将朝着同一方向继续前进,直到他离开隧道。 鬼影的移动部分是随机的。 我们不会在长隧道中应用这种随机性,因为幽灵可能会卡在那里。

if (pacman_x > (ghost_x[i] - 12) && pacman_x < (ghost_x[i] + 12)
        && pacman_y > (ghost_y[i] - 12) && pacman_y < (ghost_y[i] + 12)
        && inGame) {

    dying = true;
}

如果幽灵和吃豆人之间发生碰撞,吃豆人会死。

接下来,我们将研究movePacman()方法。 req_dxreq_dy变量在TAdapter内部类中确定。 这些变量由光标键控制。

if ((ch & 16) != 0) {
    screenData[pos] = (short) (ch & 15);
    score++;
}

如果吃豆人移动到带点的位置,我们将其从迷宫中移出并增加得分值。

if ((pacmand_x == -1 && pacmand_y == 0 && (ch & 1) != 0)
        || (pacmand_x == 1 && pacmand_y == 0 && (ch & 4) != 0)
        || (pacmand_x == 0 && pacmand_y == -1 && (ch & 2) != 0)
        || (pacmand_x == 0 && pacmand_y == 1 && (ch & 8) != 0)) {
    pacmand_x = 0;
    pacmand_y = 0;
}

如果吃豆子无法按当前方向继续前进,则会停下来。

private void drawPacman(Graphics2D g2d) {

    if (view_dx == -1) {
        drawPacnanLeft(g2d);
    } else if (view_dx == 1) {
        drawPacmanRight(g2d);
    } else if (view_dy == -1) {
        drawPacmanUp(g2d);
    } else {
        drawPacmanDown(g2d);
    }
}

吃豆人有四个可能的方向。 所有方向都有四个图像。 这些图像用于为吃豆人张嘴和闭嘴动画。

drawMaze()方法从screenData数组中的数字中提取迷宫。 数字 1 是左侧边框,2 是顶部边框,4 是右侧边框,8 是底部边框,16 是点。 我们只需在迷宫中浏览所有 225 平方。 例如,在screenData数组中有 9 个。 我们设置了第一位(1)和第四位(8)。 因此,我们在此特定正方形上绘制了底部和左侧边框。

if ((screenData[i] & 1) != 0) { 
    g2d.drawLine(x, y, x, y + BLOCK_SIZE - 1);
}

如果设置了数字的第一位,我们将绘制左边框。

Pacman.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class Pacman extends JFrame {

    public Pacman() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setTitle("Pacman");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(380, 420);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            Pacman ex = new Pacman();
            ex.setVisible(true);
        });
    }
}

这是带有main方法的 Pacman 文件。

Pacman

图:吃豆人

这是吃豆子游戏。

Java 太空侵略者

原文: https://zetcode.com/tutorials/javagamestutorial/spaceinvaders/

在 Java 2D 游戏教程的这一部分中,我们将使用 Java 创建一个简单的太空侵略者游戏克隆。 源代码和图像可在作者的 Github Java-Space-Invaders 存储库中找到。

太空侵略者是由 Nishikado Tomohiro Nishikado 设计的街机游戏。 它于 1978 年首次发布。

在“太空侵略者”游戏中,玩家控制一门大炮。 他即将拯救地球免遭邪恶太空入侵者的入侵。

用 Java 开发太空侵略者

在我们的 Java 克隆中,我们有 24 个入侵者。 这些外星人重重炮击地面。 当玩家射击导弹时,只有当导弹击中外星人或棋盘顶部时,他才能射击另一枚导弹。 玩家使用空格键射击。 外星人随机发射炸弹。 每个外星人只有在前一个击中底部后才会发射炸弹。

com/zetcode/SpaceInvaders.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class SpaceInvaders extends JFrame  {

    public SpaceInvaders() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setTitle("Space Invaders");
        setSize(Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT);

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setResizable(false);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new SpaceInvaders();
            ex.setVisible(true);
        });
    }
}

这是主要的类。 它设置了应用。

com/zetcode/Commons.java

package com.zetcode;

public interface Commons {

    int BOARD_WIDTH = 358;
    int BOARD_HEIGHT = 350;
    int BORDER_RIGHT = 30;
    int BORDER_LEFT = 5;

    int GROUND = 290;
    int BOMB_HEIGHT = 5;

    int ALIEN_HEIGHT = 12;
    int ALIEN_WIDTH = 12;
    int ALIEN_INIT_X = 150;
    int ALIEN_INIT_Y = 5;

    int GO_DOWN = 15;
    int NUMBER_OF_ALIENS_TO_DESTROY = 24;
    int CHANCE = 5;
    int DELAY = 17;
    int PLAYER_WIDTH = 15;
    int PLAYER_HEIGHT = 10;
}

Commons.java文件具有一些公共常数。 他们是不言自明的。

com/zetcode/sprite/Alien.java

package com.zetcode.sprite;

import javax.swing.ImageIcon;

public class Alien extends Sprite {

    private Bomb bomb;

    public Alien(int x, int y) {

        initAlien(x, y);
    }

    private void initAlien(int x, int y) {

        this.x = x;
        this.y = y;

        bomb = new Bomb(x, y);

        var alienImg = "simg/alien.png";
        var ii = new ImageIcon(alienImg);

        setImage(ii.getImage());
    }

    public void act(int direction) {

        this.x += direction;
    }

    public Bomb getBomb() {

        return bomb;
    }

    public class Bomb extends Sprite {

        private boolean destroyed;

        public Bomb(int x, int y) {

            initBomb(x, y);
        }

        private void initBomb(int x, int y) {

            setDestroyed(true);

            this.x = x;
            this.y = y;

            var bombImg = "simg/bomb.png";
            var ii = new ImageIcon(bombImg);
            setImage(ii.getImage());
        }

        public void setDestroyed(boolean destroyed) {

            this.destroyed = destroyed;
        }

        public boolean isDestroyed() {

            return destroyed;
        }
    }
}

这是Alien子画面。 每个外星人都有一个内部的Bomb类。

public void act(int direction) {

    this.x += direction;
}

Board类中调用act()方法。 用于在水平方向上定位外星人。

public Bomb getBomb() {

    return bomb;
}

当外星人将要投下炸弹时,将调用getBomb()方法。

com/zetcode/sprite/Player.java

package com.zetcode.sprite;

import com.zetcode.Commons;

import javax.swing.ImageIcon;
import java.awt.event.KeyEvent;

public class Player extends Sprite {

    private int width;

    public Player() {

        initPlayer();
    }

    private void initPlayer() {

        var playerImg = "simg/player.png";
        var ii = new ImageIcon(playerImg);

        width = ii.getImage().getWidth(null);
        setImage(ii.getImage());

        int START_X = 270;
        setX(START_X);

        int START_Y = 280;
        setY(START_Y);
    }

    public void act() {

        x += dx;

        if (x <= 2) {

            x = 2;
        }

        if (x >= Commons.BOARD_WIDTH - 2 * width) {

            x = Commons.BOARD_WIDTH - 2 * width;
        }
    }

    public void keyPressed(KeyEvent e) {

        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {

            dx = -2;
        }

        if (key == KeyEvent.VK_RIGHT) {

            dx = 2;
        }
    }

    public void keyReleased(KeyEvent e) {

        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {

            dx = 0;
        }

        if (key == KeyEvent.VK_RIGHT) {

            dx = 0;
        }
    }
}

这是Player子画面。 我们用光标键控制大炮。

int START_X = 270;
setX(START_X);

int START_Y = 280;
setY(START_Y);

这些是播放器精灵的初始坐标。

public void keyPressed(KeyEvent e) {

    int key = e.getKeyCode();

    if (key == KeyEvent.VK_LEFT) {

        dx = -2;
    }
...

如果按左光标键,则dx变量将设置为 -2。 下次调用act()方法时,播放器向左移动。

public void keyReleased(KeyEvent e) {

    int key = e.getKeyCode();

    if (key == KeyEvent.VK_LEFT) {

        dx = 0;
    }

    if (key == KeyEvent.VK_RIGHT) {

        dx = 0;
    }
}

如果释放左或右光标,则dx变量将设置为零。 播放器精灵停止移动。

com/zetcode/sprite/Shot.java

package com.zetcode.sprite;

import javax.swing.ImageIcon;

public class Shot extends Sprite {

    public Shot() {
    }

    public Shot(int x, int y) {

        initShot(x, y);
    }

    private void initShot(int x, int y) {

        var shotImg = "simg/shot.png";
        var ii = new ImageIcon(shotImg);
        setImage(ii.getImage());

        int H_SPACE = 6;
        setX(x + H_SPACE);

        int V_SPACE = 1;
        setY(y - V_SPACE);
    }
}

这是Shot子画面。 用 Space 键触发射击。 H_SPACEV_SPACE常数用于适当地定位导弹。

com/zetcode/sprite/Sprite.java

package com.zetcode.sprite;

import java.awt.Image;

public class Sprite {

    private boolean visible;
    private Image image;
    private boolean dying;

    int x;
    int y;
    int dx;

    public Sprite() {

        visible = true;
    }

    public void die() {

        visible = false;
    }

    public boolean isVisible() {

        return visible;
    }

    protected void setVisible(boolean visible) {

        this.visible = visible;
    }

    public void setImage(Image image) {

        this.image = image;
    }

    public Image getImage() {

        return image;
    }

    public void setX(int x) {

        this.x = x;
    }

    public void setY(int y) {

        this.y = y;
    }

    public int getY() {

        return y;
    }

    public int getX() {

        return x;
    }

    public void setDying(boolean dying) {

        this.dying = dying;
    }

    public boolean isDying() {

        return this.dying;
    }
}

这是基本的Sprite类。 其他精灵也继承自它。 它具有一些常用功能。

com/zetcode/Board.java

package com.zetcode;

import com.zetcode.sprite.Alien;
import com.zetcode.sprite.Player;
import com.zetcode.sprite.Shot;

import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

public class Board extends JPanel {

    private Dimension d;
    private List<Alien> aliens;
    private Player player;
    private Shot shot;

    private int direction = -1;
    private int deaths = 0;

    private boolean inGame = true;
    private String explImg = "simg/explosion.png";
    private String message = "Game Over";

    private Timer timer;

    public Board() {

        initBoard();
        gameInit();
    }

    private void initBoard() {

        addKeyListener(new TAdapter());
        setFocusable(true);
        d = new Dimension(Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT);
        setBackground(Color.black);

        timer = new Timer(Commons.DELAY, new GameCycle());
        timer.start();

        gameInit();
    }

    private void gameInit() {

        aliens = new ArrayList<>();

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

                var alien = new Alien(Commons.ALIEN_INIT_X + 18 * j,
                        Commons.ALIEN_INIT_Y + 18 * i);
                aliens.add(alien);
            }
        }

        player = new Player();
        shot = new Shot();
    }

    private void drawAliens(Graphics g) {

        for (Alien alien : aliens) {

            if (alien.isVisible()) {

                g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
            }

            if (alien.isDying()) {

                alien.die();
            }
        }
    }

    private void drawPlayer(Graphics g) {

        if (player.isVisible()) {

            g.drawImage(player.getImage(), player.getX(), player.getY(), this);
        }

        if (player.isDying()) {

            player.die();
            inGame = false;
        }
    }

    private void drawShot(Graphics g) {

        if (shot.isVisible()) {

            g.drawImage(shot.getImage(), shot.getX(), shot.getY(), this);
        }
    }

    private void drawBombing(Graphics g) {

        for (Alien a : aliens) {

            Alien.Bomb b = a.getBomb();

            if (!b.isDestroyed()) {

                g.drawImage(b.getImage(), b.getX(), b.getY(), this);
            }
        }
    }

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

        doDrawing(g);
    }

    private void doDrawing(Graphics g) {

        g.setColor(Color.black);
        g.fillRect(0, 0, d.width, d.height);
        g.setColor(Color.green);

        if (inGame) {

            g.drawLine(0, Commons.GROUND,
                    Commons.BOARD_WIDTH, Commons.GROUND);

            drawAliens(g);
            drawPlayer(g);
            drawShot(g);
            drawBombing(g);

        } else {

            if (timer.isRunning()) {
                timer.stop();
            }

            gameOver(g);
        }

        Toolkit.getDefaultToolkit().sync();
    }

    private void gameOver(Graphics g) {

        g.setColor(Color.black);
        g.fillRect(0, 0, Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT);

        g.setColor(new Color(0, 32, 48));
        g.fillRect(50, Commons.BOARD_WIDTH / 2 - 30, Commons.BOARD_WIDTH - 100, 50);
        g.setColor(Color.white);
        g.drawRect(50, Commons.BOARD_WIDTH / 2 - 30, Commons.BOARD_WIDTH - 100, 50);

        var small = new Font("Helvetica", Font.BOLD, 14);
        var fontMetrics = this.getFontMetrics(small);

        g.setColor(Color.white);
        g.setFont(small);
        g.drawString(message, (Commons.BOARD_WIDTH - fontMetrics.stringWidth(message)) / 2,
                Commons.BOARD_WIDTH / 2);
    }

    private void update() {

        if (deaths == Commons.NUMBER_OF_ALIENS_TO_DESTROY) {

            inGame = false;
            timer.stop();
            message = "Game won!";
        }

        // player
        player.act();

        // shot
        if (shot.isVisible()) {

            int shotX = shot.getX();
            int shotY = shot.getY();

            for (Alien alien : aliens) {

                int alienX = alien.getX();
                int alienY = alien.getY();

                if (alien.isVisible() && shot.isVisible()) {
                    if (shotX >= (alienX)
                            && shotX <= (alienX + Commons.ALIEN_WIDTH)
                            && shotY >= (alienY)
                            && shotY <= (alienY + Commons.ALIEN_HEIGHT)) {

                        var ii = new ImageIcon(explImg);
                        alien.setImage(ii.getImage());
                        alien.setDying(true);
                        deaths++;
                        shot.die();
                    }
                }
            }

            int y = shot.getY();
            y -= 4;

            if (y < 0) {
                shot.die();
            } else {
                shot.setY(y);
            }
        }

        // aliens

        for (Alien alien : aliens) {

            int x = alien.getX();

            if (x >= Commons.BOARD_WIDTH - Commons.BORDER_RIGHT && direction != -1) {

                direction = -1;

                Iterator<Alien> i1 = aliens.iterator();

                while (i1.hasNext()) {

                    Alien a2 = i1.next();
                    a2.setY(a2.getY() + Commons.GO_DOWN);
                }
            }

            if (x <= Commons.BORDER_LEFT && direction != 1) {

                direction = 1;

                Iterator<Alien> i2 = aliens.iterator();

                while (i2.hasNext()) {

                    Alien a = i2.next();
                    a.setY(a.getY() + Commons.GO_DOWN);
                }
            }
        }

        Iterator<Alien> it = aliens.iterator();

        while (it.hasNext()) {

            Alien alien = it.next();

            if (alien.isVisible()) {

                int y = alien.getY();

                if (y > Commons.GROUND - Commons.ALIEN_HEIGHT) {
                    inGame = false;
                    message = "Invasion!";
                }

                alien.act(direction);
            }
        }

        // bombs
        var generator = new Random();

        for (Alien alien : aliens) {

            int shot = generator.nextInt(15);
            Alien.Bomb bomb = alien.getBomb();

            if (shot == Commons.CHANCE && alien.isVisible() && bomb.isDestroyed()) {

                bomb.setDestroyed(false);
                bomb.setX(alien.getX());
                bomb.setY(alien.getY());
            }

            int bombX = bomb.getX();
            int bombY = bomb.getY();
            int playerX = player.getX();
            int playerY = player.getY();

            if (player.isVisible() && !bomb.isDestroyed()) {

                if (bombX >= (playerX)
                        && bombX <= (playerX + Commons.PLAYER_WIDTH)
                        && bombY >= (playerY)
                        && bombY <= (playerY + Commons.PLAYER_HEIGHT)) {

                    var ii = new ImageIcon(explImg);
                    player.setImage(ii.getImage());
                    player.setDying(true);
                    bomb.setDestroyed(true);
                }
            }

            if (!bomb.isDestroyed()) {

                bomb.setY(bomb.getY() + 1);

                if (bomb.getY() >= Commons.GROUND - Commons.BOMB_HEIGHT) {

                    bomb.setDestroyed(true);
                }
            }
        }
    }

    private void doGameCycle() {

        update();
        repaint();
    }

    private class GameCycle implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {

            doGameCycle();
        }
    }

    private class TAdapter extends KeyAdapter {

        @Override
        public void keyReleased(KeyEvent e) {

            player.keyReleased(e);
        }

        @Override
        public void keyPressed(KeyEvent e) {

            player.keyPressed(e);

            int x = player.getX();
            int y = player.getY();

            int key = e.getKeyCode();

            if (key == KeyEvent.VK_SPACE) {

                if (inGame) {

                    if (!shot.isVisible()) {

                        shot = new Shot(x, y);
                    }
                }
            }
        }
    }
}

游戏的主要逻辑位于Board类中。

private void gameInit() {

    aliens = new ArrayList<>();

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

            var alien = new Alien(Commons.ALIEN_INIT_X + 18 * j,
                    Commons.ALIEN_INIT_Y + 18 * i);
            aliens.add(alien);
        }
    }

    player = new Player();
    shot = new Shot();
}

gameInit()方法中,我们创建了 24 个外星人。 外星人图像大小为12x12px。 我们在外星人中间放了 6px 的空间。 我们还创建了播放器和射击对象。

private void drawBombing(Graphics g) {

    for (Alien a : aliens) {

        Alien.Bomb b = a.getBomb();

        if (!b.isDestroyed()) {

            g.drawImage(b.getImage(), b.getX(), b.getY(), this);
        }
    }
}

drawBombing()方法绘制由外星人发射的炸弹。

if (inGame) {

    g.drawLine(0, Commons.GROUND,
            Commons.BOARD_WIDTH, Commons.GROUND);

    drawAliens(g);
    drawPlayer(g);
    drawShot(g);
    drawBombing(g);

} ...

doDrawing()方法中,我们绘制地面,外星人,玩家,射击和炸弹。

private void update() {

    if (deaths == Commons.NUMBER_OF_ALIENS_TO_DESTROY) {

        inGame = false;
        timer.stop();
        message = "Game won!";
    }
...

update()方法内,我们检查损坏的行数。 如果我们消灭所有外星人,我们将赢得比赛。

if (alien.isVisible() && shot.isVisible()) {
    if (shotX >= (alienX)
            && shotX <= (alienX + Commons.ALIEN_WIDTH)
            && shotY >= (alienY)
            && shotY <= (alienY + Commons.ALIEN_HEIGHT)) {

        var ii = new ImageIcon(explImg);
        alien.setImage(ii.getImage());
        alien.setDying(true);
        deaths++;
        shot.die();
    }
}

如果玩家触发的射击与外星人发生碰撞,则该外星人的船只将被摧毁。 更确切地说,将设置垂死标记。 我们用它来显示爆炸。 死亡变量增加,射击精灵被破坏。

if (x >= Commons.BOARD_WIDTH - Commons.BORDER_RIGHT && direction != -1) {

    direction = -1;

    Iterator<Alien> i1 = aliens.iterator();

    while (i1.hasNext()) {

        Alien a2 = i1.next();
        a2.setY(a2.getY() + Commons.GO_DOWN);
    }
}

如果外星人到达Board的右端,他们将向下移动并将其方向更改为左侧。

Iterator<Alien> it = aliens.iterator();

    while (it.hasNext()) {

        Alien alien = it.next();

        if (alien.isVisible()) {

            int y = alien.getY();

            if (y > Commons.GROUND - Commons.ALIEN_HEIGHT) {
                inGame = false;
                message = "Invasion!";
            }

            alien.act(direction);
        }
    }

此代码可移动外星人。 如果他们到达最低点,入侵就开始了。

int shot = generator.nextInt(15);
Alien.Bomb bomb = alien.getBomb();

if (shot == Commons.CHANCE && alien.isVisible() && bomb.isDestroyed()) {

    bomb.setDestroyed(false);
    bomb.setX(alien.getX());
    bomb.setY(alien.getY());
}

这是确定外星人是否会投下炸弹的代码。 不得破坏外星人; 即他必须是可见的。 必须设置炸弹的销毁标志。 换句话说,这是外星人的第一枚炸弹掉落,或者是先前投下的炸弹已经落地。 如果满足这两个条件,轰炸就留给了机会。

if (!bomb.isDestroyed()) {

    bomb.setY(bomb.getY() + 1);

    if (bomb.getY() >= Commons.GROUND - Commons.BOMB_HEIGHT) {

        bomb.setDestroyed(true);
    }
}

如果炸弹没有被销毁,它将离地面 1px。 如果到达底部,则设置销毁标志。 外星人现在准备投下另一枚炸弹。

public void keyReleased(KeyEvent e) {

    player.keyReleased(e);
}

此特定KeyEvent的实际处理被委派给播放器精灵。

Space Invaders

图:太空侵略者

在 Java 游戏教程的这一部分中,我们创建了太空侵略者。

Java 扫雷

原文: https://zetcode.com/tutorials/javagamestutorial/minesweeper/

在 Java 2D 游戏教程的这一部分中,我们创建一个扫雷游戏克隆。 源代码和图像可以在作者的 Github Java-Minesweeper-Game 存储库中找到。

扫雷

《扫雷》是一款流行的棋盘游戏,默认情况下附带许多操作系统。 游戏的目标是扫雷场中的所有地雷。 如果玩家单击包含地雷的单元格,则地雷会引爆,游戏结束。

单元格可以包含数字,也可以为空白。 该数字表示与该特定单元相邻的地雷数量。 我们通过右键单击在单元格上设置标记。 通过这种方式,我们表明我们相信,有一个地雷。

Java Minesweeper 游戏的开发

游戏包含两个类别:BoardMinesweepersrc/resources目录中有 13 张图片。

com/zetcode/Board.java

package com.zetcode;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Board extends JPanel {

    private final int NUM_IMAGES = 13;
    private final int CELL_SIZE = 15;

    private final int COVER_FOR_CELL = 10;
    private final int MARK_FOR_CELL = 10;
    private final int EMPTY_CELL = 0;
    private final int MINE_CELL = 9;
    private final int COVERED_MINE_CELL = MINE_CELL + COVER_FOR_CELL;
    private final int MARKED_MINE_CELL = COVERED_MINE_CELL + MARK_FOR_CELL;

    private final int DRAW_MINE = 9;
    private final int DRAW_COVER = 10;
    private final int DRAW_MARK = 11;
    private final int DRAW_WRONG_MARK = 12;

    private final int N_MINES = 40;
    private final int N_ROWS = 16;
    private final int N_COLS = 16;

    private final int BOARD_WIDTH = N_COLS * CELL_SIZE + 1;
    private final int BOARD_HEIGHT = N_ROWS * CELL_SIZE + 1;

    private int[] field;
    private boolean inGame;
    private int minesLeft;
    private Image[] img;

    private int allCells;
    private final JLabel statusbar;

    public Board(JLabel statusbar) {

        this.statusbar = statusbar;
        initBoard();
    }

    private void initBoard() {

        setPreferredSize(new Dimension(BOARD_WIDTH, BOARD_HEIGHT));

        img = new Image[NUM_IMAGES];

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

            var path = "src/resources/" + i + ".png";
            img[i] = (new ImageIcon(path)).getImage();
        }

        addMouseListener(new MinesAdapter());
        newGame();
    }

    private void newGame() {

        int cell;

        var random = new Random();
        inGame = true;
        minesLeft = N_MINES;

        allCells = N_ROWS * N_COLS;
        field = new int[allCells];

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

            field[i] = COVER_FOR_CELL;
        }

        statusbar.setText(Integer.toString(minesLeft));

        int i = 0;

        while (i < N_MINES) {

            int position = (int) (allCells * random.nextDouble());

            if ((position < allCells)
                    && (field[position] != COVERED_MINE_CELL)) {

                int current_col = position % N_COLS;
                field[position] = COVERED_MINE_CELL;
                i++;

                if (current_col > 0) {
                    cell = position - 1 - N_COLS;
                    if (cell >= 0) {
                        if (field[cell] != COVERED_MINE_CELL) {
                            field[cell] += 1;
                        }
                    }
                    cell = position - 1;
                    if (cell >= 0) {
                        if (field[cell] != COVERED_MINE_CELL) {
                            field[cell] += 1;
                        }
                    }

                    cell = position + N_COLS - 1;
                    if (cell < allCells) {
                        if (field[cell] != COVERED_MINE_CELL) {
                            field[cell] += 1;
                        }
                    }
                }

                cell = position - N_COLS;
                if (cell >= 0) {
                    if (field[cell] != COVERED_MINE_CELL) {
                        field[cell] += 1;
                    }
                }

                cell = position + N_COLS;
                if (cell < allCells) {
                    if (field[cell] != COVERED_MINE_CELL) {
                        field[cell] += 1;
                    }
                }

                if (current_col < (N_COLS - 1)) {
                    cell = position - N_COLS + 1;
                    if (cell >= 0) {
                        if (field[cell] != COVERED_MINE_CELL) {
                            field[cell] += 1;
                        }
                    }
                    cell = position + N_COLS + 1;
                    if (cell < allCells) {
                        if (field[cell] != COVERED_MINE_CELL) {
                            field[cell] += 1;
                        }
                    }
                    cell = position + 1;
                    if (cell < allCells) {
                        if (field[cell] != COVERED_MINE_CELL) {
                            field[cell] += 1;
                        }
                    }
                }
            }
        }
    }

    private void find_empty_cells(int j) {

        int current_col = j % N_COLS;
        int cell;

        if (current_col > 0) {
            cell = j - N_COLS - 1;
            if (cell >= 0) {
                if (field[cell] > MINE_CELL) {
                    field[cell] -= COVER_FOR_CELL;
                    if (field[cell] == EMPTY_CELL) {
                        find_empty_cells(cell);
                    }
                }
            }

            cell = j - 1;
            if (cell >= 0) {
                if (field[cell] > MINE_CELL) {
                    field[cell] -= COVER_FOR_CELL;
                    if (field[cell] == EMPTY_CELL) {
                        find_empty_cells(cell);
                    }
                }
            }

            cell = j + N_COLS - 1;
            if (cell < allCells) {
                if (field[cell] > MINE_CELL) {
                    field[cell] -= COVER_FOR_CELL;
                    if (field[cell] == EMPTY_CELL) {
                        find_empty_cells(cell);
                    }
                }
            }
        }

        cell = j - N_COLS;
        if (cell >= 0) {
            if (field[cell] > MINE_CELL) {
                field[cell] -= COVER_FOR_CELL;
                if (field[cell] == EMPTY_CELL) {
                    find_empty_cells(cell);
                }
            }
        }

        cell = j + N_COLS;
        if (cell < allCells) {
            if (field[cell] > MINE_CELL) {
                field[cell] -= COVER_FOR_CELL;
                if (field[cell] == EMPTY_CELL) {
                    find_empty_cells(cell);
                }
            }
        }

        if (current_col < (N_COLS - 1)) {
            cell = j - N_COLS + 1;
            if (cell >= 0) {
                if (field[cell] > MINE_CELL) {
                    field[cell] -= COVER_FOR_CELL;
                    if (field[cell] == EMPTY_CELL) {
                        find_empty_cells(cell);
                    }
                }
            }

            cell = j + N_COLS + 1;
            if (cell < allCells) {
                if (field[cell] > MINE_CELL) {
                    field[cell] -= COVER_FOR_CELL;
                    if (field[cell] == EMPTY_CELL) {
                        find_empty_cells(cell);
                    }
                }
            }

            cell = j + 1;
            if (cell < allCells) {
                if (field[cell] > MINE_CELL) {
                    field[cell] -= COVER_FOR_CELL;
                    if (field[cell] == EMPTY_CELL) {
                        find_empty_cells(cell);
                    }
                }
            }
        }

    }

    @Override
    public void paintComponent(Graphics g) {

        int uncover = 0;

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

            for (int j = 0; j < N_COLS; j++) {

                int cell = field[(i * N_COLS) + j];

                if (inGame && cell == MINE_CELL) {

                    inGame = false;
                }

                if (!inGame) {

                    if (cell == COVERED_MINE_CELL) {
                        cell = DRAW_MINE;
                    } else if (cell == MARKED_MINE_CELL) {
                        cell = DRAW_MARK;
                    } else if (cell > COVERED_MINE_CELL) {
                        cell = DRAW_WRONG_MARK;
                    } else if (cell > MINE_CELL) {
                        cell = DRAW_COVER;
                    }

                } else {

                    if (cell > COVERED_MINE_CELL) {
                        cell = DRAW_MARK;
                    } else if (cell > MINE_CELL) {
                        cell = DRAW_COVER;
                        uncover++;
                    }
                }

                g.drawImage(img[cell], (j * CELL_SIZE),
                        (i * CELL_SIZE), this);
            }
        }

        if (uncover == 0 && inGame) {

            inGame = false;
            statusbar.setText("Game won");

        } else if (!inGame) {

            statusbar.setText("Game lost");
        }
    }

    private class MinesAdapter extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {

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

            int cCol = x / CELL_SIZE;
            int cRow = y / CELL_SIZE;

            boolean doRepaint = false;

            if (!inGame) {

                newGame();
                repaint();
            }

            if ((x < N_COLS * CELL_SIZE) && (y < N_ROWS * CELL_SIZE)) {

                if (e.getButton() == MouseEvent.BUTTON3) {

                    if (field[(cRow * N_COLS) + cCol] > MINE_CELL) {

                        doRepaint = true;

                        if (field[(cRow * N_COLS) + cCol] <= COVERED_MINE_CELL) {

                            if (minesLeft > 0) {
                                field[(cRow * N_COLS) + cCol] += MARK_FOR_CELL;
                                minesLeft--;
                                String msg = Integer.toString(minesLeft);
                                statusbar.setText(msg);
                            } else {
                                statusbar.setText("No marks left");
                            }
                        } else {

                            field[(cRow * N_COLS) + cCol] -= MARK_FOR_CELL;
                            minesLeft++;
                            String msg = Integer.toString(minesLeft);
                            statusbar.setText(msg);
                        }
                    }

                } else {

                    if (field[(cRow * N_COLS) + cCol] > COVERED_MINE_CELL) {

                        return;
                    }

                    if ((field[(cRow * N_COLS) + cCol] > MINE_CELL)
                            && (field[(cRow * N_COLS) + cCol] < MARKED_MINE_CELL)) {

                        field[(cRow * N_COLS) + cCol] -= COVER_FOR_CELL;
                        doRepaint = true;

                        if (field[(cRow * N_COLS) + cCol] == MINE_CELL) {
                            inGame = false;
                        }

                        if (field[(cRow * N_COLS) + cCol] == EMPTY_CELL) {
                            find_empty_cells((cRow * N_COLS) + cCol);
                        }
                    }
                }

                if (doRepaint) {
                    repaint();
                }
            }
        }
    }
}

首先,我们定义游戏中使用的常量。

private final int NUM_IMAGES = 13;
private final int CELL_SIZE = 15;

此游戏中使用了十三张图像。 一个牢房最多可被八个地雷包围,因此我们需要一号到八号。 我们需要一个空单元格,一个地雷,一个被遮盖的单元格,一个标记的单元格,最后需要一个错误标记的单元格的图像。 每个图像的大小为15x15像素。

private final int COVER_FOR_CELL = 10;
private final int MARK_FOR_CELL = 10;
private final int EMPTY_CELL = 0;
...

地雷区是数字数组。 例如,0 表示一个空单元格。 数字 10 用于电池盖和标记。 使用常量可以提高代码的可读性。

private final int MINE_CELL = 9;

MINE_CELL表示包含地雷的单元。

private final int COVERED_MINE_CELL = MINE_CELL + COVER_FOR_CELL;
private final int MARKED_MINE_CELL = COVERED_MINE_CELL + MARK_FOR_CELL;

COVERED_MINE_CELL用于覆盖并包含地雷的区域。 MARKED_MINE_CELL代码是由用户标记的隐蔽地雷单元。

private final int DRAW_MINE = 9;
private final int DRAW_COVER = 10;
private final int DRAW_MARK = 11;
private final int DRAW_WRONG_MARK = 12;

这些竞争因素决定是否绘制地雷,地雷覆盖物,标记和标记错误的单元。

private final int N_MINES = 40;
private final int N_ROWS = 16;
private final int N_COLS = 16;

我们游戏中的雷区有 40 个隐藏的地雷。 该字段中有 16 行和 16 列。 因此,雷场中共有 262 个牢房。

private int[] field;

该字段是数字数组。 字段中的每个单元格都有一个特定的编号。 例如,一个矿井的编号为 9。一个矿井的编号为 2 意味着它与两个矿井相邻。 数字已添加。 例如,一个被覆盖的地雷的编号为 19,地雷的编号为 9,电池盖的编号为 10,依此类推。

private boolean inGame;

inGame变量确定我们是在游戏中还是游戏结束。

private int minesLeft;

minesLeft变量要标记为左侧的地雷数量。

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

    var path = "src/resources/" + i + ".png";
    img[i] = (new ImageIcon(path)).getImage();
}

我们将图像加载到图像数组中。 这些图像分别命名为0.png1.png ... 12.png

newGame()启动扫雷游戏。

allCells = N_ROWS * N_COLS;
field = new int[allCells];

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

    field[i] = COVER_FOR_CELL;
}

这些线设置了雷区。 默认情况下覆盖每个单元格。

int i = 0;

while (i < N_MINES) {

    int position = (int) (allCells * random.nextDouble());

    if ((position < allCells)
            && (field[position] != COVERED_MINE_CELL)) {

        int current_col = position % N_COLS;
        field[position] = COVERED_MINE_CELL;
        i++;
...    

在白色周期中,我们将所有地雷随机放置在野外。

cell = position - N_COLS;

if (cell >= 0) {
    if (field[cell] != COVERED_MINE_CELL) {
        field[cell] += 1;
    }
}

每个单元最多可以包围八个单元。 (这不适用于边界单元。)我们为每个随机放置的地雷增加相邻单元的数量。 在我们的示例中,我们向相关单元格的顶部邻居添加 1。

find_empty_cells()方法中,我们找到了空单元格。 如果玩家单击雷区,则游戏结束。 如果他单击与地雷相邻的单元,则会发现一个数字,该数字指示该单元与地雷相邻的数量。 单击一个空单元格会导致发现许多其他空单元格以及带有数字的单元格,这些数字在空边界的空间周围形成边界。 我们使用递归算法来查找空单元格。

cell = j - 1;
if (cell <= 0) {
    if (field[cell] > MINE_CELL) {
        field[cell] -= COVER_FOR_CELL;
        if (field[cell] == EMPTY_CELL) {
            find_empty_cells(cell);
        }
    }
}

在此代码中,我们检查位于相关空单元格左侧的单元格。 如果不为空,则将其覆盖。 如果为空,则通过递归调用find_empty_cells()方法来重复整个过程。

paintComponent()方法将数字转换为图像。

if (!inGame) {
    if (cell == COVERED_MINE_CELL) {
        cell = DRAW_MINE;
    } else if (cell == MARKED_MINE_CELL) {
        cell = DRAW_MARK;
    } else if (cell > COVERED_MINE_CELL) {
        cell = DRAW_WRONG_MARK;
    } else if (cell > MINE_CELL) {
        cell = DRAW_COVER;
    }
} ... 

如果游戏结束并且我们输了,我们将显示所有未发现的地雷(如果有的话),并显示所有错误标记的单元格(如果有)。

g.drawImage(img[cell], (j * CELL_SIZE),
    (i * CELL_SIZE), this);

此代码行绘制了窗口上的每个单元格。

if (uncover == 0 && inGame) {

    inGame = false;
    statusbar.setText("Game won");
} else if (!inGame) {

    statusbar.setText("Game lost");
}

如果没有什么可以发现的,我们就赢了。 如果inGame变量设置为false,我们将丢失。

mousePressed()方法中,我们对鼠标单击做出反应。 扫雷游戏完全由鼠标控制。 我们对鼠标左键和右键单击做出反应。

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

我们确定鼠标指针的 x 和 y 坐标。

int cCol = x / CELL_SIZE;
int cRow = y / CELL_SIZE;

我们计算雷区的相应列和行。

if ((x < N_COLS * CELL_SIZE) && (y < N_ROWS * CELL_SIZE)) {

我们检查我们是否位于雷区。

if (e.getButton() == MouseEvent.BUTTON3) {

地雷的发现是通过鼠标右键完成的。

field[(cRow * N_COLS) + cCol] += MARK_FOR_CELL;
minesLeft--;

如果右键单击未标记的单元格,则将MARK_FOR_CELL添加到表示该单元格的数字中。 这导致在paintComponent()方法中绘制带有标记的覆盖单元。

field[(cRow * N_COLS) + cCol] -= MARK_FOR_CELL;
minesLeft++;

var msg = Integer.toString(minesLeft);
statusbar.setText(msg);

如果我们在已经标记的单元格上单击鼠标左键,我们将删除标记并增加要标记的单元格的数量。

if (field[(cRow * N_COLS) + cCol] > COVERED_MINE_CELL) {
    return;
}

如果单击覆盖并标记的单元格,则不会发生任何事情。 必须首先通过另一次右键单击来发现它,然后才可以在其上单击鼠标左键。

field[(cRow * N_COLS) + cCol] -= COVER_FOR_CELL;

左键单击可从单元中移除盖子。

if (field[(cRow * N_COLS) + cCol] == MINE_CELL) {
    inGame = false;
}

if (field[(cRow * N_COLS) + cCol] == EMPTY_CELL) {
    find_empty_cells((cRow * N_COLS) + cCol);
}                          

万一我们左键单击地雷,游戏就结束了。 如果我们在空白单元格上单击鼠标左键,我们将调用find_empty_cells()方法,该方法递归地找到所有相邻的空白单元格。

if (doRepaint) {
    repaint();
}

如果需要重新粉刷电路板(例如,设置或移除了标记),我们将调用repaint()方法。

com/zetcode/Minesweeper.java

package com.zetcode;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;

/**
 * Java Minesweeper Game
 *
 * Author: Jan Bodnar
 * Website: http://zetcode.com
 */

public class Minesweeper extends JFrame {

    private JLabel statusbar;

    public Minesweeper() {

        initUI();
    }

    private void initUI() {

        statusbar = new JLabel("");
        add(statusbar, BorderLayout.SOUTH);

        add(new Board(statusbar));

        setResizable(false);
        pack();

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new Minesweeper();
            ex.setVisible(true);
        });
    }
}

这是主要的类。

setResizable(false);

窗口大小固定。 为此,我们使用setResizable()方法。

Minesweeper

图:扫雷

在 Java 2D 游戏教程的这一部分中,我们创建了扫雷游戏的 Java 复制版本。

Java 推箱子

原文: https://zetcode.com/tutorials/javagamestutorial/sokoban/

在 Java 2D 游戏教程的这一部分中,我们将创建 Java 推箱子游戏克隆。 源代码和图像可以在作者的 Github Java-Sokoban-Game 存储库中找到。

推箱子

推箱子是另一个经典的电脑游戏。 它由 Imabayashi Hiroyuki 于 1980 年创建。 推箱子是日语的仓库管理员。 玩家在迷宫周围推箱子。 目的是将所有盒子放置在指定的位置。

Java 推箱子游戏的开发

我们使用光标键控制推箱子对象。 我们也可以按 R 键重新启动电平。 将所有行李放在目的地区域后,游戏结束。 我们在窗口的左上角绘制"Completed"字符串。

Board.java

package com.zetcode;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import javax.swing.JPanel;

public class Board extends JPanel {

    private final int OFFSET = 30;
    private final int SPACE = 20;
    private final int LEFT_COLLISION = 1;
    private final int RIGHT_COLLISION = 2;
    private final int TOP_COLLISION = 3;
    private final int BOTTOM_COLLISION = 4;

    private ArrayList<Wall> walls;
    private ArrayList<Baggage> baggs;
    private ArrayList<Area> areas;

    private Player soko;
    private int w = 0;
    private int h = 0;

    private boolean isCompleted = false;

    private String level
            = "    ######\n"
            + "    ##   #\n"
            + "    ##$  #\n"
            + "  ####  $##\n"
            + "  ##  $ $ #\n"
            + "#### # ## #   ######\n"
            + "##   # ## #####  ..#\n"
            + "## $  $          ..#\n"
            + "###### ### #@##  ..#\n"
            + "    ##     #########\n"
            + "    ########\n";

    public Board() {

        initBoard();
    }

    private void initBoard() {

        addKeyListener(new TAdapter());
        setFocusable(true);
        initWorld();
    }

    public int getBoardWidth() {
        return this.w;
    }

    public int getBoardHeight() {
        return this.h;
    }

    private void initWorld() {

        walls = new ArrayList<>();
        baggs = new ArrayList<>();
        areas = new ArrayList<>();

        int x = OFFSET;
        int y = OFFSET;

        Wall wall;
        Baggage b;
        Area a;

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

            char item = level.charAt(i);

            switch (item) {

                case '\n':
                    y += SPACE;

                    if (this.w < x) {
                        this.w = x;
                    }

                    x = OFFSET;
                    break;

                case '#':
                    wall = new Wall(x, y);
                    walls.add(wall);
                    x += SPACE;
                    break;

                case '$':
                    b = new Baggage(x, y);
                    baggs.add(b);
                    x += SPACE;
                    break;

                case '.':
                    a = new Area(x, y);
                    areas.add(a);
                    x += SPACE;
                    break;

                case '@':
                    soko = new Player(x, y);
                    x += SPACE;
                    break;

                case ' ':
                    x += SPACE;
                    break;

                default:
                    break;
            }

            h = y;
        }
    }

    private void buildWorld(Graphics g) {

        g.setColor(new Color(250, 240, 170));
        g.fillRect(0, 0, this.getWidth(), this.getHeight());

        ArrayList<Actor> world = new ArrayList<>();

        world.addAll(walls);
        world.addAll(areas);
        world.addAll(baggs);
        world.add(soko);

        for (int i = 0; i < world.size(); i++) {

            Actor item = world.get(i);

            if (item instanceof Player || item instanceof Baggage) {

                g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
            } else {

                g.drawImage(item.getImage(), item.x(), item.y(), this);
            }

            if (isCompleted) {

                g.setColor(new Color(0, 0, 0));
                g.drawString("Completed", 25, 20);
            }

        }
    }

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

        buildWorld(g);
    }

    private class TAdapter extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            if (isCompleted) {
                return;
            }

            int key = e.getKeyCode();

            switch (key) {

                case KeyEvent.VK_LEFT:

                    if (checkWallCollision(soko,
                            LEFT_COLLISION)) {
                        return;
                    }

                    if (checkBagCollision(LEFT_COLLISION)) {
                        return;
                    }

                    soko.move(-SPACE, 0);

                    break;

                case KeyEvent.VK_RIGHT:

                    if (checkWallCollision(soko, RIGHT_COLLISION)) {
                        return;
                    }

                    if (checkBagCollision(RIGHT_COLLISION)) {
                        return;
                    }

                    soko.move(SPACE, 0);

                    break;

                case KeyEvent.VK_UP:

                    if (checkWallCollision(soko, TOP_COLLISION)) {
                        return;
                    }

                    if (checkBagCollision(TOP_COLLISION)) {
                        return;
                    }

                    soko.move(0, -SPACE);

                    break;

                case KeyEvent.VK_DOWN:

                    if (checkWallCollision(soko, BOTTOM_COLLISION)) {
                        return;
                    }

                    if (checkBagCollision(BOTTOM_COLLISION)) {
                        return;
                    }

                    soko.move(0, SPACE);

                    break;

                case KeyEvent.VK_R:

                    restartLevel();

                    break;

                default:
                    break;
            }

            repaint();
        }
    }

    private boolean checkWallCollision(Actor actor, int type) {

        switch (type) {

            case LEFT_COLLISION:

                for (int i = 0; i < walls.size(); i++) {

                    Wall wall = walls.get(i);

                    if (actor.isLeftCollision(wall)) {

                        return true;
                    }
                }

                return false;

            case RIGHT_COLLISION:

                for (int i = 0; i < walls.size(); i++) {

                    Wall wall = walls.get(i);

                    if (actor.isRightCollision(wall)) {
                        return true;
                    }
                }

                return false;

            case TOP_COLLISION:

                for (int i = 0; i < walls.size(); i++) {

                    Wall wall = walls.get(i);

                    if (actor.isTopCollision(wall)) {

                        return true;
                    }
                }

                return false;

            case BOTTOM_COLLISION:

                for (int i = 0; i < walls.size(); i++) {

                    Wall wall = walls.get(i);

                    if (actor.isBottomCollision(wall)) {

                        return true;
                    }
                }

                return false;

            default:
                break;
        }

        return false;
    }

    private boolean checkBagCollision(int type) {

        switch (type) {

            case LEFT_COLLISION:

                for (int i = 0; i < baggs.size(); i++) {

                    Baggage bag = baggs.get(i);

                    if (soko.isLeftCollision(bag)) {

                        for (int j = 0; j < baggs.size(); j++) {

                            Baggage item = baggs.get(j);

                            if (!bag.equals(item)) {

                                if (bag.isLeftCollision(item)) {
                                    return true;
                                }
                            }

                            if (checkWallCollision(bag, LEFT_COLLISION)) {
                                return true;
                            }
                        }

                        bag.move(-SPACE, 0);
                        isCompleted();
                    }
                }

                return false;

            case RIGHT_COLLISION:

                for (int i = 0; i < baggs.size(); i++) {

                    Baggage bag = baggs.get(i);

                    if (soko.isRightCollision(bag)) {

                        for (int j = 0; j < baggs.size(); j++) {

                            Baggage item = baggs.get(j);

                            if (!bag.equals(item)) {

                                if (bag.isRightCollision(item)) {
                                    return true;
                                }
                            }

                            if (checkWallCollision(bag, RIGHT_COLLISION)) {
                                return true;
                            }
                        }

                        bag.move(SPACE, 0);
                        isCompleted();
                    }
                }
                return false;

            case TOP_COLLISION:

                for (int i = 0; i < baggs.size(); i++) {

                    Baggage bag = baggs.get(i);

                    if (soko.isTopCollision(bag)) {

                        for (int j = 0; j < baggs.size(); j++) {

                            Baggage item = baggs.get(j);

                            if (!bag.equals(item)) {

                                if (bag.isTopCollision(item)) {
                                    return true;
                                }
                            }

                            if (checkWallCollision(bag, TOP_COLLISION)) {
                                return true;
                            }
                        }

                        bag.move(0, -SPACE);
                        isCompleted();
                    }
                }

                return false;

            case BOTTOM_COLLISION:

                for (int i = 0; i < baggs.size(); i++) {

                    Baggage bag = baggs.get(i);

                    if (soko.isBottomCollision(bag)) {

                        for (int j = 0; j < baggs.size(); j++) {

                            Baggage item = baggs.get(j);

                            if (!bag.equals(item)) {

                                if (bag.isBottomCollision(item)) {
                                    return true;
                                }
                            }

                            if (checkWallCollision(bag,BOTTOM_COLLISION)) {

                                return true;
                            }
                        }

                        bag.move(0, SPACE);
                        isCompleted();
                    }
                }

                break;

            default:
                break;
        }

        return false;
    }

    public void isCompleted() {

        int nOfBags = baggs.size();
        int finishedBags = 0;

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

            Baggage bag = baggs.get(i);

            for (int j = 0; j < nOfBags; j++) {

                Area area =  areas.get(j);

                if (bag.x() == area.x() && bag.y() == area.y()) {

                    finishedBags += 1;
                }
            }
        }

        if (finishedBags == nOfBags) {

            isCompleted = true;
            repaint();
        }
    }

    public void restartLevel() {

        areas.clear();
        baggs.clear();
        walls.clear();

        initWorld();

        if (isCompleted) {
            isCompleted = false;
        }
    }
}

游戏简化了。 它仅提供非常基本的功能。 该代码比容易理解。 游戏只有一个关卡。

private final int OFFSET = 30;
private final int SPACE = 20;
private final int LEFT_COLLISION = 1;
private final int RIGHT_COLLISION = 2;
private final int TOP_COLLISION = 3;
private final int BOTTOM_COLLISION = 4;

墙图片大小为20x20像素。 这反映了SPACE常数。 OFFSET是窗口边界和游戏世界之间的距离。 有四种类型的碰撞。 每个数字都由一个数字常数表示。

private ArrayList<Wall> walls;
private ArrayList<Baggage> baggs;
private ArrayList<Area> areas;

墙壁,行李和区域是特殊的容器,可容纳游戏的所有墙壁,行李和区域。

private String level =
          "    ######\n"
        + "    ##   #\n"
        + "    ##$  #\n"
        + "  ####  $##\n"
        + "  ##  $ $ #\n"
        + "#### # ## #   ######\n"
        + "##   # ## #####  ..#\n"
        + "## $  $          ..#\n"
        + "###### ### #@##  ..#\n"
        + "    ##     #########\n"
        + "    ########\n";

这是游戏的水平。 除空格外,还有五个字符。 井号(#)代表墙。 美元($)表示要移动的框。 点(.)字符表示我们必须移动框的位置。 at 字符(@)是推箱子。 最后,换行符(\n)开始了世界的新行。

private void initWorld() {

    walls = new ArrayList<>();
    baggs = new ArrayList<>();
    areas = new ArrayList<>();

    int x = OFFSET;
    int y = OFFSET;
...

initWorld()方法启动游戏世界。 它遍历级别字符串并填充上述列表。

case '$':
    b = new Baggage(x, y);
    baggs.add(b);
    x += SPACE;
    break;

对于美元字符,我们创建一个Baggage对象。 该对象将附加到行李列表。 x 变量相应增加。

private void buildWorld(Graphics g) {
...

buildWorld()方法在窗口上绘制游戏世界。

ArrayList<Actor> world = new ArrayList<>();

world.addAll(walls);
world.addAll(areas);
world.addAll(baggs);
world.add(soko);

我们创建一个包含游戏所有对象的世界列表。

for (int i = 0; i < world.size(); i++) {

    Actor item = world.get(i);

    if (item instanceof Player || item instanceof Baggage) {

        g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
    } else {

        g.drawImage(item.getImage(), item.x(), item.y(), this);
    }
...
}

我们遍历世界容器并绘制对象。 播放器和行李图像稍小。 我们在其坐标上添加 2px 以使其居中。

if (isCompleted) {

    g.setColor(new Color(0, 0, 0));
    g.drawString("Completed", 25, 20);
}

如果完成该级别,则在窗口的左上角绘制"Completed"

case KeyEvent.VK_LEFT:

    if (checkWallCollision(soko,
            LEFT_COLLISION)) {
        return;
    }

    if (checkBagCollision(LEFT_COLLISION)) {
        return;
    }

    soko.move(-SPACE, 0);

    break;

keyPressed()方法内部,我们检查了按下了哪些键。 我们用光标键控制推箱子对象。 如果按左光标键,我们将检查推箱子是否与墙壁或行李相撞。 如果没有,我们将推箱子向左移动。

case KeyEvent.VK_R:

    restartLevel();

    break;

如果按R键,我们将重新启动该级别。

case LEFT_COLLISION:

    for (int i = 0; i < walls.size(); i++) {

        Wall wall = walls.get(i);

        if (actor.isLeftCollision(wall)) {

            return true;
        }
    }

    return false;

创建checkWallCollision()方法以确保推箱子或行李不会通过墙壁。 有四种类型的碰撞。 上面几行检查是否有左碰撞。

private boolean checkBagCollision(int type) {
...
}

checkBagCollision()涉及更多。 行李可能会与墙壁,推箱子或其他行李发生碰撞。 仅当行李与推箱子碰撞且不与其他行李或墙壁碰撞时,才可以移动行李。 搬运行李时,该通过调用isCompleted()方法检查水平是否已完成。

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

    Baggage bag = baggs.get(i);

    for (int j = 0; j < nOfBags; j++) {

        Area area =  areas.get(j);

        if (bag.x() == area.x() && bag.y() == area.y()) {

            finishedBags += 1;
        }
    }
}

isCompleted()方法检查级别是否完成。 我们得到行李数。 我们比较所有行李和目的地区域的 x 和 y 坐标。

if (finishedBags == nOfBags) {

    isCompleted = true;
    repaint();
}

finishedBags变量等于游戏中的行李数时,游戏结束。

private void restartLevel() {

    areas.clear();
    baggs.clear();
    walls.clear();

    initWorld();

    if (isCompleted) {
        isCompleted = false;
    }
}

如果我们做了一些不好的动作,我们可以重新启动关卡。 我们从列表中删除所有对象,然后再次启动世界。 isCompleted变量设置为false

Actor.java

package com.zetcode;

import java.awt.Image;

public class Actor {

    private final int SPACE = 20;

    private int x;
    private int y;
    private Image image;

    public Actor(int x, int y) {

        this.x = x;
        this.y = y;
    }

    public Image getImage() {
        return image;
    }

    public void setImage(Image img) {
        image = img;
    }

    public int x() {

        return x;
    }

    public int y() {

        return y;
    }

    public void setX(int x) {

        this.x = x;
    }

    public void setY(int y) {

        this.y = y;
    }

    public boolean isLeftCollision(Actor actor) {

        return x() - SPACE == actor.x() && y() == actor.y();
    }

    public boolean isRightCollision(Actor actor) {

        return x() + SPACE == actor.x() && y() == actor.y();
    }

    public boolean isTopCollision(Actor actor) {

        return y() - SPACE == actor.y() && x() == actor.x();
    }

    public boolean isBottomCollision(Actor actor) {

        return y() + SPACE == actor.y() && x() == actor.x();
    }
}

这是Actor类。 该类是游戏中其他演员的基础类。 它封装了推箱子游戏中对象的基本功能。

public boolean isLeftCollision(Actor actor) {

    return x() - SPACE == actor.x() && y() == actor.y();
}

此方法检查演员是否与左侧的另一个演员(墙壁,行李,推箱子)相撞。

Wall.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Wall extends Actor {

    private Image image;

    public Wall(int x, int y) {
        super(x, y);

        initWall();
    }

    private void initWall() {

        ImageIcon iicon = new ImageIcon("src/resources/wall.png");
        image = iicon.getImage();
        setImage(image);
    }
}

这是Wall类。 它继承自Actor类。 构建后,它将从资源中加载墙图像。

Player.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Player extends Actor {

    public Player(int x, int y) {
        super(x, y);

        initPlayer();
    }

    private void initPlayer() {

        ImageIcon iicon = new ImageIcon("src/resources/sokoban.png");
        Image image = iicon.getImage();
        setImage(image);
    }

    public void move(int x, int y) {

        int dx = x() + x;
        int dy = y() + y;

        setX(dx);
        setY(dy);
    }
}

这是Player类。

public void move(int x, int y) {

    int dx = x() + x;
    int dy = y() + y;

    setX(dx);
    setY(dy);
}

move()方法将对象移动到世界内部。

Baggage.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Baggage extends Actor {

    public Baggage(int x, int y) {
        super(x, y);

        initBaggage();
    }

    private void initBaggage() {

        ImageIcon iicon = new ImageIcon("src/resources/baggage.png");
        Image image = iicon.getImage();
        setImage(image);
    }

    public void move(int x, int y) {

        int dx = x() + x;
        int dy = y() + y;

        setX(dx);
        setY(dy);
    }
}

这是Baggage对象的类。 该对象是可移动的,因此也具有move()方法。

Area.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Area extends Actor {

    public Area(int x, int y) {
        super(x, y);

        initArea();
    }

    private void initArea() {

        ImageIcon iicon = new ImageIcon("src/resources/area.png");
        Image image = iicon.getImage();
        setImage(image);
    }
}

这是Area类。 这是我们尝试放置行李的对象。

Sokoban.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class Sokoban extends JFrame {

    private final int OFFSET = 30;

    public Sokoban() {

        initUI();
    }

    private void initUI() {

        Board board = new Board();
        add(board);

        setTitle("Sokoban");

        setSize(board.getBoardWidth() + OFFSET,
                board.getBoardHeight() + 2 * OFFSET);

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            Sokoban game = new Sokoban();
            game.setVisible(true);
        });
    }
}

这是主要的类。

Sokoban

图:推箱子

这是推箱子游戏。

Java 2D 教程

原文: https://zetcode.com/gfx/java2d/

这是 Java 2D 教程。 在本教程中,我们将学习 Java 2D 编程的基础知识。 Java 2D 教程适合初学者和中级 Java 程序员。

目录

Java 2D

Java 2D 是使用 Java 编程语言绘制二维图形的 API。 Java 2D 是一项强大的技术。 它可以用来创建丰富的用户界面,游戏,动画,多媒体应用或各种特殊效果。

相关教程

要在 Swing 中创建 GUI 应用,您可以查看 Java Swing 教程JavaFX 教程涵盖了现代 JavaFX RIA 平台。 JFreeChart 教程显示了如何在 Java 中创建图表。 要创建 Java 2D 游戏,您可以查看 Java 2D 游戏教程。 要学习 Java 语言的基础知识,可以查看 Java 教程

介绍

原文: https://zetcode.com/gfx/java2d/introduction/

在 Java 2D 教程的这一部分中,我们将介绍 Java 2D 技术。

关于

这是 Java 2D 教程。 它是针对初学者的。 本教程将教您 Java 2D 编程的基础知识。 可以在此处下载本教程中使用的图像。

矢量图形

有两种不同的计算机图形:矢量图形和栅格图形。 栅格(位图)图形将图像表示为像素的集合。 矢量图形是使用诸如点,线,曲线或多边形之类的几何图元来表示图像。 这些基元是使用数学方程式创建的。 两种类型的计算机图形都有优点和缺点。 矢量图形的优点是:

  • 较小的大小
  • 无限放大的能力
  • 移动,缩放,填充或旋转不会降低图像质量

Java 2D API 提供了可用于矢量和栅格图形的工具。

Java 2D API

Java 2D 是用于使用 Java 编程语言绘制二维图形的 API。

Java 2D API 提供以下功能:

  • 用于显示设备和打印机的统一渲染模型
  • 多种几何图元
  • 形状,文字和图像的命中检测
  • 合成模型
  • 增强的色彩支持
  • 打印文件
  • 控制渲染质量

Java 2D API 增强了 Abstract Windowing Toolkit(AWT)的图形,文本和图像处理功能。 AWT 是用于在 Java 中创建用户界面和图形的原始工具包。 出于兼容性目的,Java 2D 从技术上讲是 AWT 工具箱的超集。

Java 2D 是一项强大的技术。 它可以用来创建丰富的用户界面,游戏,动画,多媒体应用或各种特殊效果。

绘画机制

自定义绘画代码应放在paintComponent()方法中。 该在绘制时调用此方法。 绘画子系统首先调用paint()方法。 此方法调用以下三个方法:

  • paintComponent()
  • paintBorder()
  • paintChildren()

在特定情况下,我们可能想覆盖paintBorder()paintChildren()方法。 在大多数情况下,我们将覆盖paintComponent()方法。

图形对象

唯一的paintComponent's参数是Graphics对象。 它公开了许多绘制 2D 形状和获取有关应用图形环境的信息的方法。 Graphics2D类扩展了Graphics类,以提供对几何,坐标转换,颜色管理和文本布局的更复杂的控制。

在将Graphics对象传递给paintComponent()方法之前,先对其进行初始化,然后将其转换为paintBorder()paintChildren()方法。 这种重用可以提高性能,但是如果绘画代码永久更改Graphics状态,则可能会导致问题。 因此,我们必须恢复原始设置或使用Graphics对象的副本。 该副本是使用Graphicscreate()方法创建的; 必须稍后使用dispose()方法将其释放。

实际上,如果我们设置以下属性:字体,颜色和呈现提示,则无需创建Graphics对象的副本。 对于所有其他属性(尤其是剪裁,复合操作和转换),我们必须创建Graphics对象的副本并在以后处置它。

简单的 Java 2D 示例

我们将创建一个 Java 2D 应用的简单示例。

SimpleEx.java

package com.zetcode;

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);
            }
        });
    }
}

我们在JPanel组件上绘制文本。 Java 2D 教程中的许多代码都重复了。

class Surface extends JPanel {
...
}

我们创建一个Surface类。 此类将是我们的绘图面板。 它继承自JPanel组件。

Graphics2D g2d = (Graphics2D) g;

Graphics2D类是用于在 Java 2D 中渲染图形的基本类。 它以通用方式表示设备数量。 它扩展了旧的Graphics对象。 要进行所有高级操作,必须进行此转换。

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

在这里,我们使用drawString()方法在面板上绘制一个字符串。

@Override
public void paintComponent(Graphics g) {

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

自定义绘画是在paintComponent()方法内部执行的,我们将其覆盖。 super.paintComponent()方法调用父类的方法。 准备用于绘图的组件会做一些必要的工作。 我们将图形委托给doDrawing()方法。

private void initUI() {
...
}

initUI()方法启动应用的用户界面。

add(new Surface());

该表面已添加到JFrame容器中。

EventQueue.invokeLater(new Runnable() {

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

我们创建代码示例的实例,并使它在屏幕上可见。 invokeLater()方法将应用放置在 Swing 事件队列中。 它用于确保所有 UI 更新都是并发安全的。

Simple Java 2D example

图:简单 Java 2D example

参考

以下资源用于创建本教程:

Java 2D 教程的这一部分是对 Java 2D 库的介绍。

基本绘图

原文: https://zetcode.com/gfx/java2d/basicdrawing/

在 Java 2D 教程的这一部分中,我们进行一些基本绘制。

最简单的图形原语就是点。 它是窗口上的一个点。 有一个Point类用于表示坐标空间中的一个点,但是没有绘制点的方法。 为了绘制一个点,我们使用了drawLine()方法,其中为该方法的两个参数都提供了一个点。

PointsEx.java

package com.zetcode;

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();
    }

    public Timer getTimer() {

        return timer;
    }

    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);

        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                Timer timer = surface.getTimer();
                timer.stop();
            }
        });

        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);
            }
        });
    }
}

该示例在窗口上随机绘制 2000 个点。 计时器用于绘制循环中的点。

private void initTimer() {

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

javax.swing.Timer用于创建动画。 它以指定的时间间隔触发ActionEvents

g2d.setPaint(Color.blue);

这些点被涂成蓝色。

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

我们得到组件的宽度和高度。

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

我们得到一个上面计算出的区域大小范围内的随机数。

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

在这里,我们指出了这一点。 如前所述,我们使用drawLine()方法。 我们两次指定相同的点。

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

每个动作事件,我们都调用repaint()方法。 这将导致整个客户区被重绘。

addWindowListener(new WindowAdapter() {
    @Override
    public void windowClosing(WindowEvent e) {
        Timer timer = surface.getTimer();
        timer.stop();
    }
});

当窗口即将关闭时,我们检索计时器并使用其stop()方法将其关闭。 未明确取消的计时器可能无限期地占用资源。 EXIT_ON_CLOSE默认关闭操作将关闭 JVM 及其所有线程,因此对于我们的示例而言,这不是必需的。 但是,尽管如此,作为一种好的编程习惯,我们仍然会这样做。

Points

图:点

直线

线是简单的图形基元。 线是连接两个点的对象。 使用drawLine()方法绘制线。

LinesEx.java

package com.zetcode;

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);
            }
        });
    }
}

我们用四个线画一个简单的对象。

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

画一条直线。 该方法的参数是两点的 x,y 坐标。

Lines

图:直线

基本轮廓

BasicStroke类为图形基元的轮廓定义了一组基本的渲染属性。 这些渲染属性包括宽度,端盖,线连接,斜接限制和笔划线。

BasicStrokesEx.java

package com.zetcode;

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);
            }
        });
    }
}

在此示例中,我们显示了各种笔划线。 笔划线属性是一种模式,通过混合不透明部分和透明部分来创建。

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

我们将更改Graphics对象的stroke属性; 因此,我们使用Graphics对象的副本。 (请记住,如果我们更改字体,颜色或渲染提示以外的属性,则必须创建一个副本。)

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

在这里,我们定义了四种不同的笔划线模式。

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

该行构造一个BasicStroke对象。

g2d.setStroke(bs1);

我们使用setStroke()方法将BasicStroke应用于当前图形上下文。

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

drawLine()方法画一条线。

g2d.dispose();

最后,我们放置Graphics对象的副本。

Basic strokes

图:基本描边

端帽

上限是应用于未封闭子路径和破折线段末端的装饰。 Java 2D 中有三种不同的端盖:CAP_BUTTCAP_ROUNDCAP_SQUARE

  • CAP_BUTT - 结束未封闭的子路径和虚线段,不添加任何修饰。
  • CAP_ROUND - 用圆形装饰结束未封闭的子路径和虚线段,该圆形装饰的半径等于笔的宽度的一半。
  • CAP_SQUARE - 以方形投影结束未封闭的子路径和虚线段,该方形投影超出段的末端并延伸到等于线宽一半的距离。

CapsEx.java

package com.zetcode;

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);
            }
        });
    }
}

在我们的示例中,我们显示了所有三种类型的端盖。

BasicStroke bs1 = new BasicStroke(8, BasicStroke.CAP_BUTT,
        BasicStroke.JOIN_BEVEL);
g2d.setStroke(bs1);

创建并应用带有对接盖的基本描边。 CAP_BUTT不添加装饰。

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

我们画了三根垂直线来解释端盖之间的差异。 具有CAP_ROUNDCAP_SQUARE的线比具有CAP_BUTT的线大。 究竟多大取决于行的大小。 在我们的情况下,线的宽度为 8 像素。 线长 8 像素-左侧 4 像素,右侧 4 像素。 从图片中应该清楚。

Caps

图:端帽

连接

线连接是应用于两个路径段的交点以及子路径端点的交点的修饰。 一共有三种装饰:JOIN_BEVELJOIN_MITERJOIN_ROUND

  • JOIN_BEVEL - 通过将宽轮廓的外角与直线段相连来连接路径段。
  • JOIN_MITER - 通过扩展路径段的外部边缘直到它们交汇来连接路径段。
  • JOIN_ROUND - 通过以线宽一半的半径四舍五入拐角来连接路径段。

JoinsEx.java

package com.zetcode;

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);
            }
        });
    }
}

此代码示例显示了三个不同的线联接在起作用。

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

在这里,我们创建一个带有JOIN_BEVEL联接的矩形。

Joins

图:Joins

在 Java 2D 教程的这一部分中,我们做了一些基本的绘制。

形状和填充

原文: https://zetcode.com/gfx/java2d/shapesandfills/

在 Java 2D 教程的这一部分中,我们创建一些基本的和更高级的形状。 我们用纯色,渐变和纹理填充形状。

基本形状

首先,我们绘制一些基本的 Java 2D 形状。

BasicShapes.java

package com.zetcode;

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);
            }
        });
    }
}

在此示例中,我们在面板上绘制了六个基本形状:正方形,矩形,圆角矩形,椭圆形,弧形和圆形。

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

形状将以灰色背景绘制。

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

fillRect()方法用于绘制矩形和正方形。 前两个参数是要绘制的形状的 x,y 坐标。 最后两个参数是形状的宽度和高度。

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

在这里,我们创建一个圆角矩形。 最后两个参数是四个角处圆弧的水平和垂直直径。

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

fill()方法绘制给定形状的内部-椭圆。

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

fillArc()方法填充覆盖指定矩形的圆弧或椭圆弧。 圆弧是圆的圆周的一部分。

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

使用fillOval()方法绘制一个圆。

Basic shapes

图:基本形状

一般路径

可以使用GeneralPath构造更复杂的形状。 它代表由直线,二次贝塞尔曲线和三次贝塞尔曲线构成的几何路径。

下面的示例使用此类创建星形。

StarEx.java

package com.zetcode;

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);
            }
        });
    }    
}

我们从一系列角度创造一颗星星。

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 } 
};

这些是星星的坐标。

GeneralPath star = new GeneralPath();

在这里,我们实例化了GeneralPath类。

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

我们移到GeneralPath的初始坐标。

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

在这里,我们连接星的所有坐标。

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

我们封闭路径并填充星星内部。

Star

图:星星

区域

创建复杂形状的另一种方法是组成区域。 Area存储和操纵二维空间的封闭区域的与分辨率无关的描述。 可以通过加,减,交,异或运算来操纵它。

AreasEx.java

package com.zetcode;

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);
            }
        });
    }
}

该示例通过操纵区域来创建三种不同的形状。

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 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); 

这些线通过在椭圆上添加矩形来构造形状。

Areas

图:区域

色彩

Color类用于处理 Java 2D 中的颜色。 要使用当前颜色填充矩形,我们使用fillRect()方法。

ColoursEx.java

package com.zetcode;

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);
            }
        });
    }
}

在示例中,我们绘制了九个彩色矩形。

Graphics2D g2d = (Graphics2D) g;

更改图形上下文的color属性时,无需创建Graphics2D类的副本或重置该值。

g2d.setColor(new Color(125, 167, 116));

使用Color类创建新的颜色。 构造器的参数是新颜色的红色,绿色和蓝色部分。 setColor()方法将图形上下文的当前颜色设置为指定的颜色值。 所有后续图形操作均使用此颜色值。

g2d.fillRect(10, 15, 90, 60);

为了用颜色填充矩形,我们使用fillRect()方法。

Colours

图:颜色

渐变

在计算机图形学中,渐变是从浅到深或从一种颜色到另一种颜色的阴影的平滑混合。 在 2D 绘图程序和绘画程序中,渐变用于创建彩色背景和特殊效果以及模拟灯光和阴影。 (answers.com)

GradientsEx.java

package com.zetcode;

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);
            }
        });
    }    
}

我们的代码示例展示了五个带有渐变的矩形。

GradientPaint gp4 = new GradientPaint(25, 25, 
    Color.blue, 15, 25, Color.black, true);

要使用渐变,我们使用GradientPaint类。 通过操纵颜色值以及起点和终点,我们可以获得不同的结果。

g2d.setPaint(gp4);

通过调用setPaint()方法激活渐变。

Gradients

图:渐变

纹理

纹理是应用于形状的位图图像。 要在 Java 2D 中使用纹理,我们使用TexturePaint类。 通过setPaint()方法应用纹理。

TexturesEx.java

package com.zetcode;

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);
            }
        });
    }
}

在代码示例中,我们用三个不同的纹理填充三个矩形。

slate = ImageIO.read(new File("slate.png"));

使用ImageIO类,我们将图像读取到缓冲图像中。

slatetp = new TexturePaint(slate, new Rectangle(0, 0, 90, 60));

我们从缓冲图像中创建一个TexturePaint类。

g2d.setPaint(slatetp);
g2d.fillRect(10, 15, 90, 60);

我们用纹理填充矩形。

Textures

图:纹理

在 Java 2D 教程的这一部分中,我们介绍了 Java 2D 库的一些基本形状和更高级的形状。

透明度

原文: https://zetcode.com/gfx/java2d/transparency/

在 Java 2D 的这一部分中,我们讨论透明性。 我们提供一些基本定义和一些有趣的透明效果。

透明度说明

透明性是指能够透视材料的质量。 了解透明度的最简单方法是想象一块玻璃或水。 从技术上讲,光线可以穿过玻璃,这样我们就可以看到玻璃后面的物体。

在计算机图形学中,我们可以使用 alpha 合成实现透明效果。 Alpha 合成是将图像与背景组合以创建部分透明外观的过程。 合成过程使用 alpha 通道。 Alpha 通道是图形文件格式的 8 位层,用于表达半透明性(透明度)。 每个像素的额外八位用作掩码,表示 256 级半透明。
(answers.com,wikipedia.org)

AlphaComposite类用于 Java 2D 中的透明性。 它实现了基本的 alpha 合成规则,用于合并源像素和目标像素,以实现图形和图像的融合和透明效果。 要创建AlphaComposite,我们提供两个值:规则指示符和 alpha 值。 该规则指定了我们如何组合源像素和目标像素。 最常见的是AlphaComposite.SRC_OVER。 alpha 值的范围可以从0.0f(完全透明)到1.0f(完全不透明)。

透明矩形

第一个示例绘制了十个具有不同透明度级别的矩形。

TransparentRectanglesEx.java

package com.zetcode;

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);
            }
        });
    }
}

在我们的示例中,我们绘制了 10 个具有不同透明度级别的蓝色矩形。

float alpha = i * 0.1f;

alpha 值在for循环中动态变化。

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

AlphaComposite.getInstance()方法使用指定的规则和常数 alpha 来创建AlphaComposite对象,以与源的 alpha 相乘。

g2d.setComposite(alcom);

setComposite()方法设置Graphics2D对象的复合属性。

Transparent rectangles

图:透明矩形

淡出演示

在下一个示例中,我们将淡出图像。 图像将逐渐变得更加透明,直到完全不可见为止。

FadeOutEx.java

package com.zetcode;

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);
            }
        });
    }
}

使用AlphaComposite,我们逐渐淡出面板上的图像。

private void setSurfaceSize() {

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

setSurfaceSize()方法找出图像的大小并为面板设置首选大小。 首选大小与pack()方法的组合将显示恰好足以显示整个图像的窗口。

private void initTimer() {

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

initTimer()方法启动一个计时器。 计时器在指定的初始延迟后触发操作事件。 在事件之间延迟之后会生成连续的动作事件。 为了响应动作事件,我们将更改 Alpha 值并重新绘制面板。

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

此代码在面板上绘制具有增加的透明度的图像。

private void step() {

    alpha += -0.01f;

    if (alpha <= 0) {

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

step()方法代表淡出周期。 alpha逐渐降低。 请注意,alpha 值不得为负。 当达到零时,计时器停止。

repaint();

repaint()方法重新绘制组件。 它调用面板组件的paint()方法,然后又调用paintComponent()方法。

等待演示

在此示例中,我们使用透明效果创建一个等待演示。 我们绘制了 8 条逐渐消失的线,从而产生了一条线在移动的错觉。 此类效果通常用于通知用户幕后正在进行繁重的任务。 例如,通过互联网流式传输视频时。

WaitingEx.java

package com.zetcode;

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);
            }
        });
    }
}

我们用八个不同的 alpha 值绘制八条线。

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

这是此演示中使用的透明度值的二维数组。 有 8 行,每行一种状态。 8 行中的每行将连续使用这些值。

g2d.setStroke(new BasicStroke(STROKE_WIDTH, BasicStroke.CAP_ROUND,
        BasicStroke.JOIN_ROUND));

我们使线条更粗一些,以便更好地显示它们。 我们用圆帽画线。

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

这段代码绘制了八行。 rotate()方法用于沿直线旋转线。

Waiting

图:等待

在 Java 2D 教程的这一部分中,我们讨论了透明性。

Java 2D 游戏教程

原文: https://zetcode.com/tutorials/javagamestutorial/

这是 Java 2D 游戏教程。 在本教程中,您将学习 Java 2D 游戏编程的基础知识。 Java 2D 游戏教程适合初学者和中级程序员。

目录

本教程使用 Java Swing 来创建游戏。

电子书

ZetCode 上提供了独特的电子书 Java 2D 游戏编程。 该电子书为 PDF 格式,共有 115 页。

相关教程

您可能还想看看 Java 教程在 Java 教程Java Swing 教程JavaFX 教程中显示图像, 或 Java 2D 教程

合成

原文: https://zetcode.com/gfx/java2d/composition/

在 Java 2D 编程教程的这一部分中,我们定义了合成操作。

合成是将来自不同来源的视觉元素组合成单个图像。 它们被用来创建一种幻觉,即所有这些元素都是同一场景的一部分。 合成在电影行业中被广泛使用来创造人群,否则将是昂贵或不可能创造的整个新世界。 (wikipedia.org)

工作方式

有几种合成操作。 我们在下一个代码示例中展示其中的一些。 AlphaComposite类实现基本的 alpha 合成规则,用于组合源色和目标色,以实现图形和图像的混合和透明效果。

假设我们要在面板上绘制两个对象。 绘制的第一个对象称为目标,第二个称为源。 AlphaComposite类确定如何将这两个对象混合在一起。 如果我们有AlphaComposite.SRC_OVER规则,则将在两个对象重叠的位置绘制源对象的像素。

CompositionEx.java

package com.zetcode;

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 < 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);
            }
        });
    }
}

我们绘制两个矩形,并将它们与六个不同的合成操作合并。

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

在这里,我们有六个不同的合成规则。

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

在这里,我们得到AlphaComposite类。

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

我们使用缓冲图像执行合成操作。

Graphics2D gbi = buffImg.createGraphics();

使用createGraphics()方法从缓冲的图像创建Graphics2D对象。

gbi.setComposite(ac);

setComposite()方法为Graphics2D上下文设置组合。

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

使用drawImage()方法在面板上绘制缓冲图像。

gbi.dispose();

必须处理创建的图形对象。

Composition

图:组合

太阳和云

在下一个示例中,我们显示太阳来自云层后面。 我们将在此动画中使用合成技术。

SunAndCloudEx.java

package com.zetcode;

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();
        cloud = new ImageIcon("cloud.png").getImage();
    }

    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 <= 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);
            }
        });
    }
}

太阳来自云层背后。 云终于消失了。

private void loadImages() {

    sun = new ImageIcon("sun.png").getImage();
    cloud = new ImageIcon("cloud.png").getImage();
}

我们从磁盘加载两个映像。

private void initTimer() {

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

initTimer()方法内部,计时器被激活。

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

我们使用AlphaComposite.SRC_OVER规则-源与目标混合并覆盖空白像素。

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

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

图像被渲染到BufferedImage中,随后被复制到屏幕上。 setComposite()指定在渲染过程中如何将新像素与图形设备上的现有像素合并。

Sun & cloud

图:太阳和云

聚光灯

聚光灯是只照亮很小区域的强光束,特别用于将注意力集中在舞台表演者身上。

SpotlightEx.java

package com.zetcode;

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);
            }
        });
    }
}

使用构图规则和 Alpha 透明度值创建聚光灯效果。 还必须注意,我们的图像具有透明背景。

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

创建了BufferedImage。 它的大小等于面板的大小。 我们的 PNG 文件具有透明背景; 因此,我们使用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);
}

如果鼠标在面板区域中,则AlphaComposite.SrcAtop规则用于在鼠标指针周围绘制一个完全不透明的圆圈。

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

这两行描绘了图像的其余部分。 AlphaComposite.SrcOver规则用于创建高度透明的图像,并将其与背景混合。

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

在最后一步中,缓冲的图像将在面板的整个区域上绘制。

Spotlight

图:聚光灯

在 Java 2D 教程的这一部分中,我们讨论了图像合成。

剪裁

原文: https://zetcode.com/gfx/java2d/clipping/

在 Java 2D 教程的这一部分中,我们将讨论裁剪。

剪裁

剪裁将图形限制在某个区域。 这样做是出于效率原因并产生各种效果。 使用剪裁时,我们必须使用Graphics对象的副本,或者恢复原始的剪裁属性。 更改剪裁不会影响现有像素; 它仅影响将来的渲染。

在以下示例中,我们将图像裁剪为圆形。

ClippingEx.java

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对象的副本。

剪裁形状

在下面的示例中,我们将剪切到两个形状的交点:矩形和圆形。

ClippingShapesEx.java

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()不合并剪切区域。 它将剪裁重置到新区域。 因此,此方法应专门用于还原旧剪裁。

Clipping shapes

图:剪裁形状

在 Java 2D 教程的这一部分中,我们讨论了剪裁。

变换

原文: https://zetcode.com/gfx/java2d/transformations/

在 Java 2D 编程教程的这一部分中,我们将讨论变换。

仿射变换由零个或多个线性变换(旋转,缩放或剪切)和平移(移位)组成。 几个线性变换可以组合成一个矩阵。 旋转是使刚体绕固定点移动的变换。 缩放是一种放大或缩小对象的变换。 比例因子在所有方向上都是相同的。 平移是使每个点在指定方向上移动恒定距离的变换。 剪切是一种使对象垂直于给定轴移动的变换,该值在轴的一侧比另一侧更大。 数据来源:(wikipedia.org,freedictionary.com)

AffineTransform是 Java 2D 中用于执行仿射变换的类。

平移

以下示例描述了一个简单的平移。

TranslationEx.java

package com.zetcode;

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);
            }
        });                    
    }
}

该示例绘制一个矩形。 然后,我们进行平移并再次绘制相同的矩形。

g2d.translate(150, 50);

这条线将Graphics2D上下文的原点移到新点。

Translation

图:平移

旋转

下一个示例演示了旋转。

RotationEx.java

package com.zetcode;

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.PI/4);
        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);
            }
        });                    
    }
}

该示例绘制一个矩形,执行平移和旋转,然后再次绘制相同的矩形。

g2d.rotate(Math.PI/4);

rotate()方法执行旋转。 请注意,旋转参数以弧度为单位。

Rotation

图:旋转

缩放

下一个示例演示对象的缩放。 缩放是通过scale()方法完成的。 在此方法中,我们提供了两个参数。 它们是 x 比例因子和 y 比例因子,通过它们分别沿 x 或 y 轴缩放坐标。

ScalingEx.java

package com.zetcode;

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);
            }
        });
    }
}

我们有一个矩形。 首先,我们将其按比例缩小,然后再按比例放大。

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

另一种缩放将添加到第一个。 因此,我们需要创建并应用新的仿射变换。

Scaling

图:缩放

剪切

在以下示例中,我们执行剪切。 我们使用share()方法。

ShearingEx.java

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.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);
            }
        });
    }
}

在此示例中,我们以三种不同的颜色绘制了三个矩形。 它们形成一个结构。 他们两个被剪掉了。

tx2.shear(0, 1);

这两个参数是乘数,坐标在 x 和 y 轴的方向上移动。

Shearing

图:抖动

甜甜圈

在下面的示例中,我们通过旋转椭圆来创建复杂的形状。

DonutEx.java

package com.zetcode;

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.geom.AffineTransform;
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.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();
        double w = size.getWidth();
        double h = size.getHeight();

        Ellipse2D e = new Ellipse2D.Double(0, 0, 80, 130);
        g2d.setStroke(new BasicStroke(1));
        g2d.setPaint(Color.gray);

        for (double deg = 0; deg < 360; deg += 5) {
            AffineTransform at =
                    AffineTransform.getTranslateInstance(w / 2, h / 2);
            at.rotate(Math.toRadians(deg));
            g2d.draw(at.createTransformedShape(e));
        }
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class DonutEx extends JFrame {

    public DonutEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Donut");
        setSize(370, 320);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

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

在此示例中,我们创建一个甜甜圈形状。

Ellipse2D e = new Ellipse2D.Double(0, 0, 80, 130);
g2d.setStroke(new BasicStroke(1));
g2d.setPaint(Color.gray);

刚开始时有一个椭圆。

for (double deg = 0; deg < 360; deg += 5) {
    AffineTransform at =
            AffineTransform.getTranslateInstance(w / 2, h / 2);
    at.rotate(Math.toRadians(deg));
    g2d.draw(at.createTransformedShape(e));
}

旋转几圈后,有一个甜甜圈。

在 Java 2D 教程的这一部分中,我们讨论了变换。

特效

原文: https://zetcode.com/gfx/java2d/effects/

在 Java 2D 编程教程的这一部分中,我们将展示一些效果。

泡泡

在第一个示例中,我们将看到不断增长的彩色气泡,它们在屏幕上随机出现和消失。 该示例来自 Java 2D 演示。

BubblesEx.java

package com.zetcode;

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);
            }
        });
    }
}

这是泡泡的示例。

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 void initSurface() {

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

initSurface()方法为面板设置黑色背景。 我们创建三个数组。 椭圆数组(椭圆是椭圆的一种特殊情况),每个椭圆大小的数组以及椭圆描边的数组。 动画期间,气泡的大小和笔触都会增加。

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);
    }
}    

ellipses数组填充有椭圆对象。 posRandEllipses()方法将椭圆对象随机放置在窗口上。 椭圆的初始大小也是随机选择的。

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);
}

posRandEllipses()方法将椭圆随机放置在窗口上。 esizeestroke数组填充有值。 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]);
        }
    }
}

动画包括步骤。 在每个步骤中,我们增加每个椭圆的笔触和大小值。 气泡达到最大大小后,将其重置为最小大小,并在面板上随机重新放置。 否则,将显示增加的值。

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]);
    }
}

drawEllipses()方法从面板上的数组绘制所有椭圆。

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

doDrawing()方法中,我们计算面板的大小。 如果调整窗口大小,气泡将随机分布在窗口的整个区域。

@Override
public void actionPerformed(ActionEvent e) {

    repaint();
}

计时器对象以指定的时间间隔触发动作事件。 repaint()方法重新绘制面板组件。

Bubbles

图:泡泡

星星

下一个示例显示了一个旋转和缩放的星星。

StarDemoEx.java

package com.zetcode;

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);
            }
        });
    }
}

在此演示中,我们有一颗星星。 星星旋转生长然后收缩。

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 double angle = 0;
private double scale = 1;
private double delta = 0.01;

当我们旋转星星时使用anglescale因子确定星星的大小。 最后,delta因子是刻度的变化量。

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

使用translate()方法将坐标系移到窗口的中间。

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]);
}

GeneralPath用于创建星形。 通过moveTo()方法将第一点添加到路径。 通过lineTo()方法添加星星的后续点。

g2d.rotate(angle);
g2d.scale(scale, scale);

我们执行旋转和缩放操作。

g2d.fill(star);        

fill()方法填充星形的内部。

if (scale < 0.01) {

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

    delta = -delta;
}

该代码控制星的收缩和增长量。

泡芙

接下来,我们显示粉扑效果。 这种效果在 Flash 动画或电影介绍中很常见。 文本在屏幕上逐渐增长,一段时间后它逐渐消失。

PuffEx.java

package com.zetcode;

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 = "ZetCode";
        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);
            }
        });      
    }
}

该示例在窗口上绘制了一个不断增长的文本,从某个角度看,该文本变得越来越透明,直到看不见为止。

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

这是我们用于文本的字体。

FontMetrics fm = g2d.getFontMetrics();

getFontMetrics()返回FontMetrics类。 该类存储有关在特定屏幕上呈现特定字体的信息。

int stringWidth = fm.stringWidth(s);

我们使用FontMetrics对象的stringWidth()方法来获取字符串的宽度。

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

在这里,我们设置所绘制文本的透明度。

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

此代码行在窗口的(水平)中间绘制字符串。

if (x > 40)
    alpha -= 0.01;

琴弦高于 40 点后,琴弦开始褪色。

在 Java 2D 教程的这一部分中,我们做了一些视觉效果。

图像

原文: https://zetcode.com/gfx/java2d/java2dimages/

在 Java 2D 教程的这一部分中,我们将处理图像。

BufferedImage是使用 Java 2D 处理图像的基础类。 它是存储在内存中的像素矩形。

显示图像

在第一个示例中,我们在面板上显示图像。

DisplayImageEx.java

package com.zetcode;

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);
            }
        });
    }
}

在示例中,我们在面板上显示图像。 调整窗口大小以适合图像的大小。

private void loadImage() {

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

我们使用ImageIcon类加载图像。 该图像位于当前工作目录中。

private void setSurfaceSize() {

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

我们确定加载图像的大小。 使用setPreferredSize()方法,我们设置Surface面板的首选大小。 JFrame容器的pack()方法将使框架适合其子代的大小。 在我们的例子中是Surface面板。 结果,窗口将被调整大小以精确显示加载的图像。

private void doDrawing(Graphics g) {

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

使用drawImage()方法在面板上绘制图像。 最后一个参数是ImageObserver类。 有时用于异步加载。 当我们不需要异步加载图像时,可以将null放在此处。

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

pack()方法调整容器的大小以适合子面板的大小。

灰度图像

在计算中,灰度数字图像是其中每个像素的值是单个样本的图像,也就是说,它携带有关其强度的完整(且唯一)信息。 这种图像仅由中性灰色阴影组成,从最弱的黑色到最强的白色不等。 (维基百科)

在下一个示例中,我们使用 Java 2D 创建灰度图像。

GrayScaleImage.java

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.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);
            }
        });
    }
}

有几种创建灰度图像的方法。 我们通过将图像数据写入BufferedImage.TYPE_BYTE_GRAY类型的缓冲图像中来实现。

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

我们创建一个BufferedImage.TYPE_BYTE_GRAY类型的BufferedImage类。

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

在这里,我们将蘑菇图像绘制到缓冲图像中。

g2d.dispose();      

使用createGraphics()方法创建的图形对象应手动释放。 当这些对象返回时,作为对象的paint()update()方法的参数提供的图形对象将由系统自动释放。

private void doDrawing(Graphics g) {

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

缓冲的图像通过drawImage()方法绘制在面板上。

图像翻转

以下示例翻转图像。 我们将要过滤图像。 有一种filter()方法正在转换图像。

FlippedImageEx.java

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.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);
            }
        });
    }
}

在我们的代码示例中,我们水平翻转图像。

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

翻转图像意味着对其进行缩放和平移。 因此,我们进行了AffineTransform操作。

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

这是可用的过滤操作之一。 这也可以通过像素操纵来完成。 但是 Java 2D 提供了高级类,使操作图像更加容易。 在我们的情况下,AffineTransformOp类对图像像素执行缩放和平移。

private void doDrawing(Graphics g) {

    Graphics2D g2d = (Graphics2D) g;

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

这两个图像都画在面板上。

private void setSurfaceSize() {

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

    Dimension d = new Dimension(3*SPACE+2*w, h+2*SPACE);
    setPreferredSize(d);
}

我们设置面板的首选大小。 我们计算大小,以便可以在面板上放置两个图像,并在它们之间以及图像和窗口边框之间留一些空间。

图像模糊

下一个代码示例使图像模糊。 模糊意味着没有聚焦的图像。 为了模糊图像,我们使用了卷积运算。 这是一种数学运算,也用于边缘检测或噪声消除。 模糊操作可用于各种图形效果。 例如,创建速度错觉或显示人的注意力不集中。

模糊滤镜操作用像素及其邻居的平均值替换图像中的每个像素。 卷积是每个像素的运算。 对图像中的每个像素重复相同的算法。 内核可以看作是一个二维的数字网格,它按顺序遍历图像的每个像素,并一路执行计算。 由于图像也可以被视为数字的二维网格,因此将内核应用于图像可以可视化为在较大的网格(图像)上移动的小网格(内核)。 (developer.apple.com)

BlurredImageEx.java

package com.zetcode;

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);
            }
        });
    }
}

在代码示例中,我们从磁盘加载图像,对图像执行模糊操作,然后在窗口上显示结果。

private void loadImage() {

    try {

        mshi = ImageIO.read(new File("mushrooms.jpg"));
    } catch (IOException ex) {

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

ImageIO类的read()方法从磁盘读取图像并返回BufferedImage

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 doDrawing(Graphics g) {

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

模糊的图像绘制在窗口上。

反射

在下一个示例中,我们显示反射图像。 这种效果使人产生幻觉,好像图像在水中被反射一样。 以下代码示例受到 jhlabs.com 中代码的启发。

ReflectionEx.java

package com.zetcode;

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);
            }
        });
    }
}

在示例中,我们创建了反射图像的错觉。

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);

这是代码中最重要的部分。 我们使第二个图像透明。 但是透明度不是恒定不变的。 图像逐渐淡出。 这是通过GradientPaint类实现的。

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 * imageHeight + gap);
g2d.scale(1, -1);

此代码翻转图像并将其转换为原始图像下方。 平移操作是必需的,因为缩放操作会使图像上下颠倒并向上平移图像。 要了解发生了什么,只需拍摄照片并将其放在桌子上并翻转即可。

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

绘制反射的图像。

@Override
public Dimension getPreferredSize() {

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

设置组件的首选大小的另一种方法是重写getPreferredSize()方法。

Reflection

图:反射

在 Java2D 教程的这一部分中,我们处理了图像。

文字和字体

原文: https://zetcode.com/gfx/java2d/textfonts/

在 Java 2D 教程的这一部分中,我们将使用文本和字体。

文字和字体

渲染文本是另一个复杂的话题。 它很容易填满一本专门的书。 在这里,我们仅提供一些基本示例。

字符是表示诸如字母,数字或标点符号之类的项目的符号。 字形是用于呈现字符或字符序列的形状。 在拉丁字母中,字形通常代表一个字符。 在其他书写系统中,一个字符可能由几个字形组成,例如ť,ž,ú,ô。 这些是带有重音符号的拉丁字符。

字体有两种:物理字体和逻辑字体。 实际的字体是实际的字体库。 逻辑字体是 Java 平台定义的五个字体家族:Serif,SansSerif,Monospaced,Dialog 和 DialogInput。 逻辑字体不是实际的字体库。 Java 运行时环境将逻辑字体名称映射到物理字体。

可以使用各种字体在窗口上绘制文本。 字体是一组具有特定字体设计和大小的字体字符。 各种字体包括 Helvetica,Georgia,Times 或 Verdana。 具有特定样式的字形的集合形成字体面。 字体的集合构成字体家族。 (docs.oracle.com,answers.com)

系统字体

此控制台示例将在您的平台上打印所有可用字体。

AllFontsEx.java

package com.zetcode;

import java.awt.Font;
import java.awt.GraphicsEnvironment;

public class AllFontsEx {

    public static void main(String[] args) {

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

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

我们打印每种已安装字体的名称和系列。

GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();

有些对象是特定平台的典型对象,其中包括字体。 Unix,OS X 和 Windows 平台上的字体集合有所不同。 GraphicsEnvironment类描述特定平台上可用的GraphicsDevice对象和Font对象的集合。

Font[] fonts = ge.getAllFonts();

getAllFonts()返回GraphicsEnvironment中可用的所有字体。

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

字体名称和字体系列会打印到终端上。

...
URW Bookman L Demi Bold : URW Bookman L
URW Bookman L Demi Bold Italic : URW Bookman L
URW Bookman L Light : URW Bookman L
URW Bookman L Light Italic : URW Bookman L
URW Chancery L Medium Italic : URW Chancery L
URW Gothic L Book : URW Gothic L
URW Gothic L Book Oblique : URW Gothic L
URW Gothic L Demi : URW Gothic L
URW Gothic L Demi Oblique : URW Gothic L
URW Palladio L Bold : URW Palladio L
URW Palladio L Bold Italic : URW Palladio L
URW Palladio L Italic : URW Palladio L
URW Palladio L Roman : URW Palladio L
Ubuntu : Ubuntu
Ubuntu Bold : Ubuntu
Ubuntu Bold Italic : Ubuntu
Ubuntu Condensed : Ubuntu Condensed
Ubuntu Italic : Ubuntu
...

这是 Ubuntu Linux 上所有字体的摘录。

灵魂伴侣

在下一个示例中,我们将在面板上显示一些歌词。

SoulmateEx.java

package com.zetcode;

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

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

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

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

        g2d.setRenderingHints(rh);

        g2d.setFont(new Font("Purisa", Font.PLAIN, 13));

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

    @Override
    public void paintComponent(Graphics g) {

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

public class SoulmateEx extends JFrame {

    public SoulmateEx() {

        initUI();
    }

    private void initUI() {

        setTitle("Soulmate");

        add(new Surface());

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

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

在此示例中,我们在面板上绘制文本。 我们选择一种特定的字体类型。

g2d.setFont(new Font("Purisa", Font.PLAIN, 13));

在这里,我们设置 Purisa 字体类型。

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

drawString()方法使用Graphics2D上下文中的当前文本属性状态来呈现文本。

Soulmate

图:灵魂伴侣

Unicode

下一个示例演示如何显示 Unicode 文本。 请注意,在实际应用中,文本将放置在代码之外的单独资源中。

$ cat fyodor
Фёдор Михайлович Достоевский родился 30 октября (11 ноября) 1821 года в Москве. 
Был вторым из 7 детей. Отец, Михаил Андреевич, работал в госпитале для бедных. 
...

我们有一个名为fyodor的文件,其中的文本在西里尔字母中。

$ native2ascii fyodor unicode

我们使用名为native2ascii的工具,可以在 jdk 的bin目录中找到该工具。 它将带有本地编码字符的文件转换为带有 Unicode 编码字符的文件。 第一个参数是输入文件,第二个参数是输出文件。

$ cat unicode
\u0424\u0451\u0434\u043e\u0440 \u041c\u0438\u0445\u0430\u0439\u043b\u043e\u0432\u0438\u0447 
...

unicode 编码中的相同文本。

UnicodeEx.java

package com.zetcode;

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

class Surface extends JPanel {

    String sent1 = "\u0424\u0451\u0434\u043e\u0440 \u041c\u0438\u0445" +
"\u0430\u0439\u043b\u043e\u0432\u0438\u0447 \u0414\u043e\u0441\u0442" +
"\u043e\u0435\u0432\u0441\u043a\u0438\u0439 \u0440\u043e\u0434\u0438" +
"\u043b\u0441\u044f 30 \u043e\u043a\u0442\u044f\u0431\u0440\u044f " +
"(11 \u043d\u043e\u044f\u0431\u0440\u044f) 1821 \u0433\u043e\u0434" +
"\u0430 \u0432 \u041c\u043e\u0441\u043a\u0432\u0435\. ";

    String sent2 = "\u0411\u044b\u043b \u0432\u0442\u043e\u0440\u044b\u043c " +
"\u0438\u0437 7 \u0434\u0435\u0442\u0435\u0439\. \u041e\u0442\u0435\u0446, " +
"\u041c\u0438\u0445\u0430\u0438\u043b \u0410\u043d\u0434\u0440\u0435\u0435" +
"\u0432\u0438\u0447, \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0432 " +
"\u0433\u043e\u0441\u043f\u0438\u0442\u0430\u043b\u0435 \u0434\u043b\u044f " +
"\u0431\u0435\u0434\u043d\u044b\u0445."; 

    String sent3 = "\u041c\u0430\u0442\u044c, \u041c\u0430\u0440\u0438\u044f " +
"\u0424\u0451\u0434\u043e\u0440\u043e\u0432\u043d\u0430 " +
"(\u0432 \u0434\u0435\u0432\u0438\u0447\u0435\u0441\u0442\u0432\u0435 " +
"\u041d\u0435\u0447\u0430\u0435\u0432\u0430), \u043f\u0440\u043e\u0438\u0441" +
"\u0445\u043e\u0434\u0438\u043b\u0430 \u0438\u0437 \u043a\u0443\u043f\u0435" +
"\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0440\u043e\u0434\u0430.";

    String sent4 = "\u041a\u043e\u0433\u0434\u0430 \u0414\u043e\u0441\u0442" +
"\u043e\u0435\u0432\u0441\u043a\u043e\u043c\u0443 \u0431\u044b\u043b\u043e 15 " +
"\u043b\u0435\u0442, \u0435\u0433\u043e \u043c\u0430\u0442\u044c " +
"\u0443\u043c\u0435\u0440\u043b\u0430 \u043e\u0442 \u0447\u0430\u0445\u043e" +
"\u0442\u043a\u0438, \u0438 \u043e\u0442\u0435\u0446 \u043e\u0442\u043f\u0440" +
"\u0430\u0432\u0438\u043b";

    String sent5 = "\u0441\u0442\u0430\u0440\u0448\u0438\u0445 \u0441\u044b" +
"\u043d\u043e\u0432\u0435\u0439, \u0424\u0451\u0434\u043e\u0440\u0430 \u0438 " +
"\u041c\u0438\u0445\u0430\u0438\u043b\u0430 (\u0432\u043f\u043e\u0441\u043b" +
"\u0435\u0434\u0441\u0442\u0432\u0438\u0438 \u0442\u0430\u043a\u0436\u0435 " +
"\u0441\u0442\u0430\u0432\u0448\u0435\u0433\u043e \u043f\u0438\u0441\u0430" +
"\u0442\u0435\u043b\u0435\u043c),"; 

    String sent6 = "\u0432 \u043f\u0430\u043d\u0441\u0438\u043e\u043d \u041a. " +
"\u0424\. \u041a\u043e\u0441\u0442\u043e\u043c\u0430\u0440\u043e\u0432\u0430 " +
"\u0432 \u041f\u0435\u0442\u0435\u0440\u0431\u0443\u0440\u0433\u0435.";

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

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

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

        g2d.setRenderingHints(rh);

        g2d.setFont(new Font("Franklin Gothic Medium", Font.PLAIN, 11));

        g2d.drawString(sent1, 20, 30);
        g2d.drawString(sent2, 20, 55);
        g2d.drawString(sent3, 20, 80);
        g2d.drawString(sent4, 20, 120);
        g2d.drawString(sent5, 20, 145);
        g2d.drawString(sent6, 20, 170);        
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class UnicodeEx extends JFrame {

    public UnicodeEx() {

        initUI();
    }

    private void initUI() {

        setTitle("Unicode");

        add(new Surface());

        setSize(550, 230);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);        
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

请注意,文本将超出实际程序中的源代码范围。 为了简化起见,此处将文本保留在源代码中。

String sent1 = "\u0424\u0451\u0434\u043e\u0440 \u041c\u0438\u0445" +
...

这是第一个 Unicode 句子。

g2d.drawString(sent1, 20, 30);

drawString()方法绘制句子。

Unicode

图:Unicode

阴影文字

在下一个示例中,我们创建阴影文本。 通过两次绘制相同的文本来创建效果。 一个文本用作主体文本,另一个作为阴影。 阴影的文本会稍微移动一点,以浅灰色显示并模糊。

ShadowedTextEx.java

package com.zetcode;

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

public class ShadowedTextEx extends JFrame {

    private final int width = 490;
    private final int height = 150;

    private final String text = "Disciplin ist macht";
    private TextLayout textLayout;

    public ShadowedTextEx() {

        initUI();
    }

    private void initUI() {

        setTitle("Shadowed Text");

        BufferedImage image = createImage();
        add(new JLabel(new ImageIcon(image)));

        setSize(490, 150);
        setLocationRelativeTo(null);       
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private void setRenderingHints(Graphics2D g) {

        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                           RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                           RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    private BufferedImage createImage()  {

        int x = 10;
        int y = 100;

        Font font = new Font("Georgia", Font.ITALIC, 50);

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

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

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

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

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

        g2d.dispose();

        return image2;
    }        

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

这次我们不使用paintComponent()方法。 相反,我们绘制成缓冲的图像。

Font font = new Font("Georgia", Font.ITALIC, 50);

我们选择斜体格鲁吉亚,大小为 50 点。

BufferedImage image = new BufferedImage(width, height, 
        BufferedImage.TYPE_INT_RGB);

我们创建第一个缓冲图像。

Graphics2D g1d = image.createGraphics();

从缓冲的图像创建一个Graphics2D对象。 它将用于绘制缓冲图像。

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

我们创建一个TextLayout类。 TextLayout是样式字符数据的不可变图形表示。 它用于对文本和字体进行高级操作。

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

draw()方法在指定的Graphics2D上下文中的指定位置呈现此TextLayout。 第二和第三个参数指定TextLayout的原点坐标。

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

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

此操作将产生模糊效果。 该效果用于阴影文本。

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

我们对第一张图像应用模糊效果,然后将结果复制到第二张缓冲的图像。

textLayout.draw(g2d, x, y);

此时,TextLayout对象中同时包含原始文本和模糊文本。

Shadow Text

图:阴影文字

文字属性

绘制文本时,我们可以控制其各种属性。 我们可以使用FontTextAttributesAttributeString类修改文本呈现。 Font类表示用于呈现文本的字体。 TextAttribute类定义用于文本呈现的属性键和属性值。 最后,AttributedString类保存文本和相关的属性信息。

TextAttributesEx.java

package com.zetcode;

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

class Surface extends JPanel {

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

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

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

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

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

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

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

        AttributedString as2 = new AttributedString(java);

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

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

    @Override
    public void paintComponent(Graphics g) {

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

public class TextAttributesEx extends JFrame {

    public TextAttributesEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setSize(620, 190);
        setTitle("Text attributes");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

在我们的示例中,我们演示了各种文本呈现方式。

AttributedString as1 = new AttributedString(words);

我们从words字符串中创建一个AttributeString

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

在这里,我们向AttributeString类添加了一个新属性。 此属性指定前七个字符将显示为红色。

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

第一个文本绘制在面板上。 因为此刻我们使用AttributeString类而不是直接使用字符串,所以我们使用重载的drawString()方法,该方法将AttributedCharacterIterator实例作为其第一个参数。

Text Attributes

图:文本属性

旋转文字

在最后一个示例中,我们在面板上显示了旋转的文本。 要旋转文本,我们执行旋转和平移操作。 如前所述,glyph是用于呈现字符的形状。 因此,在我们的代码示例中,我们需要获取文本的所有字形,获取它们的度量值并对其进行逐一处理。

我们将与几个重要的类一起工作。 FontRenderContext类是正确测量文本所需信息的容器。 GlyphVector对象是一个字形的集合,其中包含用于将每个字形放置在变换后的坐标空间中的几何信息。

RotatedTextEx.java

package com.zetcode;

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

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

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

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

        String s = "ZetCode, tutorials for programmers";

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

        g2d.translate(20, 20);

        FontRenderContext frc = g2d.getFontRenderContext();

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

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

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

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

        g2d.dispose();
    }    

    @Override
    public void paintComponent(Graphics g) {

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

public class RotatedTextEx extends JFrame {

    public RotatedTextEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Rotated text");
        setSize(450, 300);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

这是一个旋转的文本示例。

String s = "ZetCode, tutorials for programmers";

我们旋转此文本。 因为文本采用 Latin1 编码,所以字形以一对一方式对应于字符。

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

在这里,我们创建一个GlyphVector对象。 GlyphVector是字形及其位置的集合。

int length = gv.getNumGlyphs();

在这里,我们获得文本的字形数量。 如果将数字打印到控制台,则得到 34。因此,在本例中,每个字符都是一个字形。

Point2D p = gv.getGlyphPosition(i);

遍历字形向量时,我们使用getGlyphPosition()方法计算字形的位置。

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

我们计算字形旋转的程度。

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

我们进行仿射旋转变换。

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

getGlyphOutline()方法返回指定字形的ShapecreateTransformedShape()方法返回通过仿射变换操作修改的新Shape对象。

g2d.fill(transformedGlyph);

最后,我们绘制字形。

Rotated text

图:旋转文本

在 Java 2D 教程的这一部分中,我们介绍了文本和字体。

命中测试,移动物体

原文: https://zetcode.com/gfx/java2d/hitmove/

在 Java 2D 编程教程的这一部分中,我们首先讨论命中测试。 我们展示了如何确定是否在面板上的某个形状内单击。 在第二个示例中,我们创建两个形状,可以用鼠标在面板上移动它们,并用鼠标滚轮调整它们的大小。 在最后一个示例中,我们将调整具有两个控制点的矩形的大小。

命中测试

命中测试确定我们是否已经用鼠标指针单击了Shape内部。 每个Shape都有contains()方法。 该方法测试指定的Point2D是否在Shape的边界内。

HitTestingEx.java

package com.zetcode;

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

class Surface extends JPanel {

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

    public Surface() {

        initSurface();
    }

    private void initSurface() {

        addMouseListener(new HitTestAdapter());

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

        alpha_rectangle = 1f;
        alpha_ellipse = 1f;        
    }

    private void doDrawing(Graphics g) {

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

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

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

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

        g2d.setRenderingHints(rh);

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

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

        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

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

    class RectRunnable implements Runnable {

        private Thread runner;

        public RectRunnable() {

            initThread();
        }

        private void initThread() {

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

        @Override
        public void run() {

            while (alpha_rectangle >= 0) {

                repaint();
                alpha_rectangle += -0.01f;

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

                try {

                    Thread.sleep(50);
                } catch (InterruptedException ex) {

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

    class HitTestAdapter extends MouseAdapter
            implements Runnable {

        private RectRunnable rectAnimator;
        private Thread ellipseAnimator;

        @Override
        public void mousePressed(MouseEvent e) {

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

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

                rectAnimator = new RectRunnable();
            }

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

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

        @Override
        public void run() {

            while (alpha_ellipse >= 0) {

                repaint();
                alpha_ellipse += -0.01f;

                if (alpha_ellipse < 0) {

                    alpha_ellipse = 0;
                }

                try {

                    Thread.sleep(50);
                } catch (InterruptedException ex) {

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

public class HitTestingEx extends JFrame {

    public HitTestingEx() {

        add(new Surface());

        setTitle("Hit testing");
        setSize(250, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);           
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

在我们的示例中,我们有两个Shapes:一个矩形和一个圆形。 通过单击它们,它们逐渐开始消失。 在此示例中,我们使用线程。

private Rectangle2D rect;
private Ellipse2D ellipse;

我们使用矩形和椭圆形。

private float alpha_rectangle;
private float alpha_ellipse;

这两个变量控制两个几何对象的透明度。

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

doDrawing()方法内部,我们设置矩形的透明度。 alpha_rectangle在专用的Thread内部进行计算。

HitTestAdapter类负责处理鼠标事件。 它确实实现了Runnable接口,这意味着它还创建了第一个线程。

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

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

如果我们在椭圆内按下,将创建一个新的Thread。 该线程调用run()方法。 在我们的例子中,它是类本身的run()方法(HitTestAdapter)。

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

    rectAnimator = new RectRunnable();
}

对于矩形,我们有一个单独的内部类-RectRunnable类。 此类在构造器中创建自己的线程。

public void run() {

    while (alpha_ellipse >= 0) {

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

请注意,run()方法仅被调用一次。 要实际执行某项操作,我们必须实现一个while循环。 while循环重新绘制面板并减小alpha_ellipse变量。

Hit testing

图:点击测试

移动和缩放

在下一部分中,我们将学习如何使用面板上的鼠标移动和缩放图形对象。 它可用于在我们的应用中移动和缩放图表,图表或其他各种对象。

MovingScalingEx.java

package com.zetcode;

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

class Surface extends JPanel {

    private ZRectangle zrect;
    private ZEllipse zell;

    public Surface() {

        initUI();
    }

    private void initUI() {

        MovingAdapter ma = new MovingAdapter();

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

        zrect = new ZRectangle(50, 50, 50, 50);
        zell = new ZEllipse(150, 70, 80, 80);
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        Font font = new Font("Serif", Font.BOLD, 40);
        g2d.setFont(font);

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

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

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

        doDrawing(g);        
    }

    class ZEllipse extends Ellipse2D.Float {

        public ZEllipse(float x, float y, float width, float height) {

            setFrame(x, y, width, height);
        }

        public boolean isHit(float x, float y) {

            return getBounds2D().contains(x, y);
        }

        public void addX(float x) {

            this.x += x;
        }

        public void addY(float y) {

            this.y += y;
        }

        public void addWidth(float w) {

            this.width += w;
        }

        public void addHeight(float h) {

            this.height += h;
        }
    }

    class ZRectangle extends Rectangle2D.Float {

        public ZRectangle(float x, float y, float width, float height) {

            setRect(x, y, width, height);
        }

        public boolean isHit(float x, float y) {

            return getBounds2D().contains(x, y);
        }

        public void addX(float x) {

            this.x += x;
        }

        public void addY(float y) {

            this.y += y;
        }

        public void addWidth(float w) {

            this.width += w;
        }

        public void addHeight(float h) {

            this.height += h;
        }
    }

    class MovingAdapter extends MouseAdapter {

        private int x;
        private int y;

        @Override
        public void mousePressed(MouseEvent e) {

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

        @Override
        public void mouseDragged(MouseEvent e) {

            doMove(e);
        }   

        private void doMove(MouseEvent e) {

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

            if (zrect.isHit(x, y)) {

                zrect.addX(dx);
                zrect.addY(dy);
                repaint();
            }

            if (zell.isHit(x, y)) {

                zell.addX(dx);
                zell.addY(dy);
                repaint();
            }

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

    class ScaleHandler implements MouseWheelListener {

        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {

            doScale(e);
        }

        private void doScale(MouseWheelEvent e) {

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

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

                if (zrect.isHit(x, y)) {

                    float amount =  e.getWheelRotation() * 5f;
                    zrect.addWidth(amount);
                    zrect.addHeight(amount);
                    repaint();
                }

                if (zell.isHit(x, y)) {

                    float amount =  e.getWheelRotation() * 5f;
                    zell.addWidth(amount);
                    zell.addHeight(amount);
                    repaint();
                }
            }            
        }
    }
}

public class MovingScalingEx extends JFrame {

    public MovingScalingEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

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

    public static void main(String[] args) {        

        EventQueue.invokeLater(new Runnable() {

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

在我们的代码示例中,我们有两个图形对象:一个矩形和一个圆形。 我们可以通过单击它们并拖动它们来移动它们。 我们还可以通过将鼠标光标放在对象上并移动鼠标滚轮来放大或缩小它们。

private ZRectangle zrect;
private ZEllipse zell;

正如我们已经提到的,面板上有一个矩形和一个椭圆。 这两个类都扩展了 Java AWT 包中内置类的功能。

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

我们注册了三个监听器。 这些监听器捕获鼠标按下,鼠标拖动和鼠标滚轮事件。

class ZEllipse extends Ellipse2D.Float {

    public ZEllipse(float x, float y, float width, float height) {

        setFrame(x, y, width, height);
    }

    public boolean isHit(float x, float y) {

        return getBounds2D().contains(x, y);
    }
...
}

这段代码摘录显示了ZEllipse类。 它扩展了内置的Ellipse2D.Float类。 它增加了缩放和移动椭圆的功能。 例如,isHit()方法确定鼠标指针是否在椭圆区域内。

MovingAdapter类处理鼠标按下和鼠标拖动事件。

@Override
public void mousePressed(MouseEvent e) {

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

mousePressed()方法中,我们存储对象的初始 x 和 y 坐标。

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

doMove()方法内部,我们计算拖动对象的距离。

if (zrect.isHit(x, y)) {

    zrect.addX(dx);
    zrect.addY(dy);
    repaint();
}

如果在矩形区域内,则更新矩形的 x 和 y 坐标并重新绘制面板。

x += dx;
y += dy;

初始坐标将更新。

ScaleHandler类处理对象的缩放。

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

    if (zrect.isHit(x, y)) {

        float amount =  e.getWheelRotation() * 5f;
        zrect.addWidth(amount);
        zrect.addHeight(amount);
        repaint();
    }
...
}

如果移动鼠标滚轮,并且光标位于矩形区域内,则将调整矩形大小并重新绘制面板。 通过getWheelRotation()方法计算缩放比例,该方法返回车轮旋转量。

调整矩形大小

在下一个示例中,我们显示如何调整形状的大小。 我们的形状是一个矩形。 在矩形上,我们绘制了两个小的黑色矩形。 通过单击这些小矩形并拖动它们,我们可以调整主矩形的大小。

ResizingRectangleEx.java

package com.zetcode;

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

class Surface extends JPanel {

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

    public Surface() {

        initUI();
    }

    private void initUI() {

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

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

    private void doDrawing(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

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

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

        g2.draw(r);        
    }

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

        doDrawing(g);
    }

    private class ShapeTestAdapter extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent event) {

            Point p = event.getPoint();

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

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

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

                if (r.contains(p)) {

                    pos = i;
                    return;
                }
            }
        }

        @Override
        public void mouseReleased(MouseEvent event) {

            pos = -1;
        }

        @Override
        public void mouseDragged(MouseEvent event) {

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

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

public class ResizingRectangleEx extends JFrame {

    public ResizingRectangleEx()  {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Resize rectangle");
        setSize(300, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);                  
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

有两种创建矩形的方法。 一种方法是提供左上角点的 x 和 y 坐标以及矩形的宽度和高度。 另一种方法是提供左上角和右下角点。 在我们的代码示例中,我们将同时使用这两种方法。

private Point2D[] points;

在此数组中,我们存储构成矩形的点。

private final int SIZE = 8;

这是黑色小矩形的大小。

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

这些是矩形的初始坐标。

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

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

此代码绘制了两个小的控制矩形。

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

g2.draw(s);

在这里,我们从这些点绘制一个矩形。

@Override
public void mousePressed(MouseEvent event) {

    Point p = event.getPoint();

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

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

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

        if (r.contains(p)) {

            pos = i;
            return;
        }
    }
}

mousePressed()方法中,我们确定是否单击了两个控制点之一。 如果我们点击其中一个,则pos变量将存储其中的哪个。

@Override
public void mouseDragged(MouseEvent event) {

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

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

在这里,矩形是动态调整大小的。 在mouseDragged()事件期间,我们获取当前点,更新点数组并重新绘制面板。

Resize Rectangle

图:缩放矩形

在 Java 2D 教程的这一部分中,我们介绍了命中测试和移动对象。

俄罗斯方块

原文: https://zetcode.com/gfx/java2d/tetris/

在本章中,我们将在 Java Swing 中创建一个俄罗斯方块游戏克隆。

俄罗斯方块

俄罗斯方块游戏是有史以来最受欢迎的计算机游戏之一。 原始游戏是由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程的。此后,几乎所有版本的几乎所有计算机平台上都可以使用俄罗斯方块。 甚至我的手机都有俄罗斯方块游戏的修改版。

俄罗斯方块被称为下降块益智游戏。 在这个游戏中,我们有七个不同的形状,称为 tetrominoes 。 S 形,Z 形,T 形,L 形,线形,镜像 L 形和正方形。 这些形状中的每一个都形成有四个正方形。 形状从板上掉下来。 俄罗斯方块游戏的目的是移动和旋转形状,以便它们尽可能地适合。 如果我们设法形成一行,则该行将被破坏并得分。 我们玩俄罗斯方块游戏,直到达到顶峰。

Tetrominoes

图:Tetrominoes

开发

我们的俄罗斯方块游戏没有图像,我们使用 Swing 绘图 API 绘制四面体。 每个计算机游戏的背后都有一个数学模型。 俄罗斯方块也是如此。

游戏背后的一些想法。

  • 我们使用计时器类创建游戏周期
  • 绘制四方块
  • 形状以正方形为单位移动(不是逐个像素移动)
  • 从数学上讲,棋盘是简单的数字列表

我对游戏做了一些简化,以便于理解。 游戏启动后立即开始。 我们可以通过按p键暂停游戏。 空格键将把俄罗斯方块放在底部。 d键将片段向下一行。 (它可以用来加快下降速度。)游戏以恒定速度运行,没有实现加速。 分数是我们已删除的行数。

Tetris.java

package com.zetcode;

import java.awt.BorderLayout;

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

public class Tetris extends JFrame {

    private JLabel statusbar;

    public Tetris() {

        initUI();
   }

   private void initUI() {

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

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

   public JLabel getStatusBar() {

       return statusbar;
   }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {

                Tetris game = new Tetris();
                game.setVisible(true);
            }
        });                
    } 
}

Tetris.java文件中,我们设置了游戏。 我们创建一个玩游戏的棋盘。 我们创建一个状态栏。

board.start();

start()方法启动俄罗斯方块游戏。 窗口出现在屏幕上之后。

Shape.java

package com.zetcode;

import java.util.Random;

public class Shape {

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

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

    public Shape() {

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

    public void setShape(Tetrominoes shape) {

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

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

            for (int j = 0; j < 2; ++j) {

                coords[i][j] = coordsTable[shape.ordinal()][i][j];
            }
        }

        pieceShape = shape;
    }

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

    public void setRandomShape() {

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

    public int minX() {

      int m = coords[0][0];

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

          m = Math.min(m, coords[i][0]);
      }

      return m;
    }

    public int minY() {

      int m = coords[0][1];

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

          m = Math.min(m, coords[i][1]);
      }

      return m;
    }

    public Shape rotateLeft() {

        if (pieceShape == Tetrominoes.SquareShape)
            return this;

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

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

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

        return result;
    }

    public Shape rotateRight() {

        if (pieceShape == Tetrominoes.SquareShape)
            return this;

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

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

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

        return result;
    }
}

Shape类提供有关俄罗斯方块的信息。

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

Tetrominoes枚举拥有所有七个俄罗斯方块形状。 加上此处称为NoShape的空形状。

public Shape() {

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

这是Shape类的构造器。 coords数组保存俄罗斯方块的实际坐标。

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

coordsTable数组保存我们的俄罗斯方块的所有可能的坐标值。 这是一个模板,所有零件均从该模板获取其坐标值。

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

    for (int j = 0; j < 2; ++j) {

        coords[i][j] = coordsTable[shape.ordinal()][i][j];
    }
}

在这里,我们将一行坐标值从coordsTable放置到俄罗斯方块的coords数组中。 请注意ordinal()方法的使用。 在 C++ 中,枚举类型本质上是一个整数。 与 C++ 不同,Java 枚举是完整类。 并且ordinal()方法返回枚举类型在枚举对象中的当前位置。

下图将帮助您更多地了解坐标值。 coords数组保存俄罗斯方块的坐标。 例如,数字{0, -1}{0, 0}{-1, 0}{-1, -1}表示旋转的 S 形。 下图说明了形状。

Coordinates

图:坐标

public Shape rotateLeft() {

    if (pieceShape == Tetrominoes.SquareShape)
        return this;

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

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

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

    return result;
}

此代码将棋子向左旋转。 正方形不必旋转。 这就是为什么我们只是将引用返回到当前对象。 查看上一张图像将有助于理解旋转。

Board.java

package com.zetcode;

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

import com.zetcode.Shape.Tetrominoes;

public class Board extends JPanel 
        implements ActionListener {

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

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

    public Board(Tetris parent) {

        initBoard(parent);
    }

    private void initBoard(Tetris parent) {

       setFocusable(true);
       curPiece = new Shape();
       timer = new Timer(400, this);
       timer.start(); 

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

    @Override
    public void actionPerformed(ActionEvent e) {

        if (isFallingFinished) {

            isFallingFinished = false;
            newPiece();
        } else {

            oneLineDown();
        }
    }

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

    public void start()  {

        if (isPaused)
            return;

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

        newPiece();
        timer.start();
    }

    private void pause()  {

        if (!isStarted)
            return;

        isPaused = !isPaused;

        if (isPaused) {

            timer.stop();
            statusbar.setText("paused");
        } else {

            timer.start();
            statusbar.setText(String.valueOf(numLinesRemoved));
        }

        repaint();
    }

    private void doDrawing(Graphics g) {

        Dimension size = getSize();
        int boardTop = (int) size.getHeight() - BoardHeight * squareHeight();

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

            for (int j = 0; j < BoardWidth; ++j) {

                Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);

                if (shape != Tetrominoes.NoShape)
                    drawSquare(g, 0 + j * squareWidth(),
                               boardTop + i * squareHeight(), shape);
            }
        }

        if (curPiece.getShape() != Tetrominoes.NoShape) {

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

                int x = curX + curPiece.x(i);
                int y = curY - curPiece.y(i);
                drawSquare(g, 0 + x * squareWidth(),
                           boardTop + (BoardHeight - y - 1) * squareHeight(),
                           curPiece.getShape());
            }
        }        
    }

    @Override
    public void paintComponent(Graphics g) { 

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

    private void dropDown() {

        int newY = curY;

        while (newY > 0) {

            if (!tryMove(curPiece, curX, newY - 1))
                break;
            --newY;
        }

        pieceDropped();
    }

    private void oneLineDown()  {

        if (!tryMove(curPiece, curX, curY - 1))
            pieceDropped();
    }

    private void clearBoard() {

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

    private void pieceDropped() {

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

            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            board[(y * BoardWidth) + x] = curPiece.getShape();
        }

        removeFullLines();

        if (!isFallingFinished)
            newPiece();
    }

    private void newPiece()  {

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

        if (!tryMove(curPiece, curX, curY)) {

            curPiece.setShape(Tetrominoes.NoShape);
            timer.stop();
            isStarted = false;
            statusbar.setText("game over");
        }
    }

    private boolean tryMove(Shape newPiece, int newX, int newY) {

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

            int x = newX + newPiece.x(i);
            int y = newY - newPiece.y(i);

            if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
                return false;

            if (shapeAt(x, y) != Tetrominoes.NoShape)
                return false;
        }

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

        repaint();

        return true;
    }

    private void removeFullLines() {

        int numFullLines = 0;

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

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

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

        if (numFullLines > 0) {

            numLinesRemoved += numFullLines;
            statusbar.setText(String.valueOf(numLinesRemoved));
            isFallingFinished = true;
            curPiece.setShape(Tetrominoes.NoShape);
            repaint();
        }
     }

    private void drawSquare(Graphics g, int x, int y, Tetrominoes shape)  {

        Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102), 
            new Color(102, 204, 102), new Color(102, 102, 204), 
            new Color(204, 204, 102), new Color(204, 102, 204), 
            new Color(102, 204, 204), new Color(218, 170, 0)
        };

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

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

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

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

    }

    class TAdapter extends KeyAdapter {

         @Override
         public void keyPressed(KeyEvent e) {

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

             int keycode = e.getKeyCode();

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

             if (isPaused)
                 return;

             switch (keycode) {

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

             case KeyEvent.VK_RIGHT:
                 tryMove(curPiece, curX + 1, curY);
                 break;

             case KeyEvent.VK_DOWN:
                 tryMove(curPiece.rotateRight(), curX, curY);
                 break;

             case KeyEvent.VK_UP:
                 tryMove(curPiece.rotateLeft(), curX, curY);
                 break;

             case KeyEvent.VK_SPACE:
                 dropDown();
                 break;

             case 'd':
                 oneLineDown();
                 break;

             case 'D':
                 oneLineDown();
                 break;
             }
         }
     }
}

最后,我们有Board.java文件。 这是游戏逻辑所在的位置。

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

我们初始化一些重要的变量。 isFallingFinished变量确定俄罗斯方块形状是否已完成下降,然后我们需要创建一个新形状。 numLinesRemoved计算行数,到目前为止我们已经删除了行数。 curXcurY变量确定下降的俄罗斯方块形状的实际位置。

setFocusable(true);

我们必须显式调用setFocusable()方法。 从现在开始,开发板具有键盘输入。

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

Timer对象在指定的延迟后触发一个或多个操作事件。 在我们的例子中,计时器每 400ms 调用一次actionPerformed()方法。

@Override
public void actionPerformed(ActionEvent e) {

    if (isFallingFinished) {

        isFallingFinished = false;
        newPiece();
    } else {

        oneLineDown();
    }
}

actionPerformed()方法检查下降是否已完成。 如果是这样,将创建一个新作品。 如果不是,下降的俄罗斯方块下降了一行。

doDrawing()方法内部,我们在板上绘制了所有对象。 这幅画有两个步骤。

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

    for (int j = 0; j < BoardWidth; ++j) {

        Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);

        if (shape != Tetrominoes.NoShape)
            drawSquare(g, 0 + j * squareWidth(),
                        boardTop + i * squareHeight(), shape);
    }
}

在第一步中,我们绘制所有形状或已掉落到板底部的形状的其余部分。 所有正方形都记在板数组中。 我们使用shapeAt()方法访问它。

if (curPiece.getShape() != Tetrominoes.NoShape) {

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

        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        drawSquare(g, 0 + x * squareWidth(),
                    boardTop + (BoardHeight - y - 1) * squareHeight(),
                    curPiece.getShape());
    }
}   

在第二步中,我们绘制实际的下降部分。

private void dropDown() {

    int newY = curY;

    while (newY > 0) {

        if (!tryMove(curPiece, curX, newY - 1))
            break;
        --newY;
    }

    pieceDropped();
}

如果按空格键,则该片段将落到底部。 我们只是简单地尝试将一块下降到另一条俄罗斯方块下降的底部或顶部。

private void clearBoard() {

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

clearBoard()方法用空的NoShapes填充电路板。 稍后将其用于碰撞检测。

private void pieceDropped() {

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

        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        board[(y * BoardWidth) + x] = curPiece.getShape();
    }

    removeFullLines();

    if (!isFallingFinished)
        newPiece();
}

pieceDropped()方法将下降的碎片放入板数组中。 棋盘再次保持了所有碎片的正方形和已经落下的碎片的剩余部分。 当一块完成落下时,就该检查是否可以去除板上的一些线了。 这是removeFullLines()方法的工作。 然后,我们创建一个新作品。 更准确地说,我们尝试创建一个新作品。

private void newPiece()  {

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

    if (!tryMove(curPiece, curX, curY)) {

        curPiece.setShape(Tetrominoes.NoShape);
        timer.stop();
        isStarted = false;
        statusbar.setText("game over");
    }
}

newPiece()方法创建一个新的俄罗斯方块。 作品获得了新的随机形状。 然后,我们计算初始curXcurY值。 如果我们不能移动到初始位置,则游戏结束。 我们加油。 计时器停止。 我们将游戏放在状态栏上的字符串上方。

private boolean tryMove(Shape newPiece, int newX, int newY) {

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

        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);

        if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
            return false;

        if (shapeAt(x, y) != Tetrominoes.NoShape)
            return false;
    }

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

    repaint();

    return true;
}

tryMove()方法尝试移动俄罗斯方块。 如果该方法已达到板边界或与已经跌落的俄罗斯方块相邻,则返回false

int numFullLines = 0;

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

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

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

removeFullLines()方法内部,我们检查板中所有行中是否有完整行。 如果至少有一条实线,则将其删除。 找到整条线后,我们增加计数器。 我们将整行上方的所有行向下移动一行。 这样我们就破坏了整个生产线。 注意,在我们的俄罗斯方块游戏中,我们使用了所谓的天真重力。 这意味着,正方形可能会漂浮在空白间隙上方。

每个俄罗斯方块都有四个正方形。 每个正方形都使用drawSquare()方法绘制。 俄罗斯方块有不同的颜色。

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

正方形的左侧和顶部以较亮的颜色绘制。 类似地,底部和右侧用较深的颜色绘制。 这是为了模拟 3D 边缘。

我们使用键盘控制游戏。 控制机制通过KeyAdapter实现。 这是一个覆盖keyPressed()方法的内部类。

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

如果按向左箭头键,则尝试将下降片向左移动一个正方形。

Tetris

图:俄罗斯方块

这是俄罗斯方块游戏。

Cario 图形教程

原文: https://zetcode.com/gfx/cairo/

这是 Cario 图形教程。 本教程将教您使用 C 编程语言在 Cario 进行图形编程的基础知识。 本教程适用于初学者和中级开发者。

目录

Cario

Cario 是用于创建 2D 矢量图形的库。 它是用 C 编程语言编写的。 还有其他计算机语言的绑定。 Python,Perl,C++ ,C# 或 Java。 Cario 是一个多平台库。 它适用于 Linux,BSD,OSX。

相关教程

PyCairo 教程用 Python 语言介绍了 Cairo 库。 您还可以查看 GTK+ 教程

Cario 图形库

原文: https://zetcode.com/gfx/cairo/cairolib/

欢迎来到 Cario 图形教程。 本教程将教您 Cairo 2D 矢量绘图库的基础知识和一些高级主题。 在大多数示例中,我们将使用 GTK+ 编程库。 本教程使用 C 编程语言完成。

2D 矢量图形

有两种不同的计算机图形。 矢量和光栅图形。 栅格图形将图像表示为像素的集合。 矢量图形是使用诸如点,线,曲线或多边形之类的几何图元来表示图像。 这些基元是使用数学方程式创建的。

两种类型的计算机图形都有优点和缺点。 矢量图形优于栅格的优点是:

  • 较小的大小
  • 无限放大的能力
  • 移动,缩放,填充或旋转不会降低图像质量

Cario

Cario 是用于创建 2D 矢量图形的库。 它是用 C 编程语言编写的。 存在其他计算机语言的绑定。 Python,Perl,C++ ,C# ,Java。 Cairo 是一个多平台库,可在 Linux,BSD 和 Mac OS 上运行。

Cario 支持各种后端。

  • X Window 系统
  • Win32 GDI
  • Mac OS X
  • PNG
  • PDF 格式
  • PostScript
  • SVG

这意味着我们可以使用该库在 Linux/BSD,Windows,Mac OS 上的 Windows 上绘图,并且可以使用该库创建 PNG 图像,PDF 文件,PostScript 文件和 SVG 文件。

我们可以将 cairo 库与 Windows OS 上的 GDI+ 库和 Mac OS 上的 Quartz 2D 比较。 Cario 是一个开源软件库。 从 2.8 版开始,cairo 库是 GTK+ 系统的一部分。

编译示例

这些示例使用 C 编程语言创建。 我们使用 GNU C 编译器来编译它们。

gcc example.c -o example `pkg-config --cflags --libs gtk+-3.0` 

请注意,编译选项的顺序很重要。

Java 游戏基础

原文: https://zetcode.com/tutorials/javagamestutorial/basics/

在 Java 2D 游戏教程的这一部分中,我们将介绍创建游戏所需的一些基础知识。 我们创建一个基本应用,绘制一个甜甜圈,并显示图片。

关于

这是 Java 2D 游戏教程。 它是针对初学者的。 本教程将教您使用 Java 编程语言和 Swing GUI 工具箱进行 2D 游戏编程的基础。 可以在此处下载本教程中使用的图像。

应用

在本教程中,我们将显示每个游戏的框架。

Board.java

package com.zetcode;

import javax.swing.JPanel;

public class Board extends JPanel {

    public Board() {}
}

Board是进行游戏的面板。

Application.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class Application extends JFrame {

    public Application() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setSize(250, 200);

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            Application ex = new Application();
            ex.setVisible(true);
        });
    }
}

这是游戏的切入点。 这里我们有main方法。

add(new Board());

在这里,我们将Board放置在JFrame容器的中心。

setSize(250, 200);

此行设置窗口的大小。

setDefaultCloseOperation(EXIT_ON_CLOSE);

当我们单击关闭按钮时,这将关闭应用。 这不是默认行为。

setLocationRelativeTo(null);

null传递给setLocationRelativeTo()方法时,窗口将在屏幕上居中。

public static void main(String[] args) {

    EventQueue.invokeLater(() -> {
        Application ex = new Application();
        ex.setVisible(true);
    });
}

我们创建代码示例的实例,并使它在屏幕上可见。

甜甜圈

板上的对象可以是图像,也可以使用 Java 2D API 提供的绘画工具绘制。 在下一个示例中,我们绘制一个甜甜圈形状。

Board.java

package com.zetcode;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import javax.swing.JPanel;

public class Board extends JPanel {

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

        drawDonut(g);
    }

    private void drawDonut(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);

        Dimension size = getSize();
        double w = size.getWidth();
        double h = size.getHeight();

        Ellipse2D e = new Ellipse2D.Double(0, 0, 80, 130);
        g2d.setStroke(new BasicStroke(1));
        g2d.setColor(Color.gray);

        for (double deg = 0; deg < 360; deg += 5) {
            AffineTransform at
                    = AffineTransform.getTranslateInstance(w/2, h/2);
            at.rotate(Math.toRadians(deg));
            g2d.draw(at.createTransformedShape(e));
        }
    }
}

绘画是在paintComponent()方法内完成的。

private void drawDonut(Graphics g) {
...
}

将实际绘画委派给特定方法是一种很好的编程习惯。

Graphics2D g2d = (Graphics2D) g;

Graphics2D类扩展了Graphics类。 它提供了对几何图形,坐标转换,颜色管理和文本布局的更复杂的控制。

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();
double w = size.getWidth();
double h = size.getHeight();

我们得到窗口的高度和宽度。 我们需要它们将甜甜圈形状在窗口上居中。

Ellipse2D e = new Ellipse2D.Double(0, 0, 80, 130);
g2d.setStroke(new BasicStroke(1));
g2d.setColor(Color.gray);

在这里,我们创建椭圆。

for (double deg = 0; deg < 360; deg += 5) {
    AffineTransform at
            = AffineTransform.getTranslateInstance(w/2, h/2);
    at.rotate(Math.toRadians(deg));
    g2d.draw(at.createTransformedShape(e));
}

在此,椭圆旋转 72 次以创建一个甜甜圈形状。

Donut.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class DonutExample extends JFrame {

    public DonutExample() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setSize(330, 330);

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            DonutExample ex = new DonutExample();
            ex.setVisible(true);
        });
    }
}

这是主要的类。

绘制图像

当我们创建计算机游戏时,我们经常使用图像。 在下一个示例中,我们加载图像并将其绘制在板上。 如果找不到图像文件,请查看 Java 教程中的显示图像

Board.java

package com.zetcode;

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

public class Board extends JPanel {

    private Image bardejov;

    public Board() {

        initBoard();
    }

    private void initBoard() {

        loadImage();

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

    private void loadImage() {

        ImageIcon ii = new ImageIcon("src/resources/bardejov.png");
        bardejov = ii.getImage();        
    }

    @Override
    public void paintComponent(Graphics g) {

        g.drawImage(bardejov, 0, 0, null);
    }
}

我们为董事会制作了一个城镇形象。 图像绘制在paintComponent()方法内部。

ImageIcon ii = new ImageIcon("src/resources/bardejov.png");

我们创建一个ImageIcon

bardejov = ii.getImage();

我们从ImageIcon中得到一个Image

g.drawImage(bardejov, 0, 0, null);

我们在窗口上绘制图像。

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

我们确定图像的宽度和高度。 面板的首选大小设置为图像大小。 与JFramepack()方法配合使用时,窗口正好足以显示图像。

ImageExample.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class ImageExample extends JFrame {

    public ImageExample() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        pack();

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            ImageExample ex = new ImageExample();
            ex.setVisible(true);
        });
    }
}

这是示例的主要类。

Image

图:图像

在本章中,我们介绍了 Java 游戏编程的一些基础知识。

Cario 定义

原文: https://zetcode.com/gfx/cairo/cairodefinitions/

在 Cairo 图形教程的这一部分中,我们将为 Cairo 图形库提供一些有用的定义。 这将帮助我们更好地了解 Cairo 绘图模型。

上下文

Cairo 中的绘制是通过上下文完成的。 Cairo 上下文包含所有描述绘制方式的图形状态参数。 这包括信息,例如线宽,颜色,要绘制的表面以及许多其他内容。 这允许实际的绘图函数采用较少的参数来简化界面。

使用 Cairo 进行的所有绘制始终都是对cairo_t对象完成的。 Cairo 上下文与特定表面相关。 PDF,SVG,PNG,GtkWindow 等。

路径

路径由一条或多条线组成。 这些线由两个或多个锚点连接。 路径可以由直线和曲线组成。 路径有两种。 打开和关闭路径。 在封闭的路径中,起点和终点相遇。 在开放路径中,起点和终点不相交。

在 Cairo,我们从一条空路开始。 首先,我们定义一条路径,然后通过抚摸和填充它们使它们可见。 重要说明。 在每个cairo_stroke()cairo_fill()函数调用之后,该路径将被清空。 我们必须定义一条新路径。

路径由子路径组成。

资源

来源是我们在绘图中使用的油漆。 我们可以将源与笔或墨水进行比较,以用于绘制轮廓和填充形状。 有四种基本来源:颜色,渐变,图案和图像。

表面

表面是我们要绘制的目标。 我们可以使用 PDF 或 PostScript 表面渲染文档,并通过 Xlib 和 Win32 表面直接绘制到平台上。

该文档提到以下方面:

typedef enum _cairo_surface_type {
  CAIRO_SURFACE_TYPE_IMAGE,
  CAIRO_SURFACE_TYPE_PDF,
  CAIRO_SURFACE_TYPE_PS,
  CAIRO_SURFACE_TYPE_XLIB,
  CAIRO_SURFACE_TYPE_XCB,
  CAIRO_SURFACE_TYPE_GLITZ,
  CAIRO_SURFACE_TYPE_QUARTZ,
  CAIRO_SURFACE_TYPE_WIN32,
  CAIRO_SURFACE_TYPE_BEOS,
  CAIRO_SURFACE_TYPE_DIRECTFB,
  CAIRO_SURFACE_TYPE_SVG,
  CAIRO_SURFACE_TYPE_OS2
} cairo_surface_type_t;

遮罩

在将源应用于表面之前,先对其进行过滤。 遮罩用作过滤器。 遮罩确定在哪里应用源,在哪里不应用。 遮罩的不透明部分允许复制源。 透明零件不允许将源复制到表面。

图案

Cairo 图案代表在表面上绘制时的来源。 在 Cairo 中,您可以从中读取图案,该图案用作绘制操作的来源或蒙版。 Cairo 中的图案可以是实心,基于表面或渐变的图案。

在 Cairo 教程的这一章中,我们给出了一些基本定义。

Cairo 后端

原文: https://zetcode.com/gfx/cairo/cairobackends/

Cairo 库支持各种后端。 在 Cairo 图形教程的这一部分中,我们将使用 Cairo 创建 PNG 图像,PDF 文件,SVG 文件,并在 GTK 窗口上绘制。

PNG 图像

在第一个示例中,我们将创建一个 PNG 图像。

#include <cairo.h>

int main(void)
{
  cairo_surface_t *surface;
  cairo_t *cr;

  surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 390, 60);
  cr = cairo_create(surface);

  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, 40.0);

  cairo_move_to(cr, 10.0, 50.0);
  cairo_show_text(cr, "Disziplin ist Macht.");

  cairo_surface_write_to_png(surface, "image.png");

  cairo_destroy(cr);
  cairo_surface_destroy(surface);

  return 0;
}

本示例是一个小型控制台应用,它将创建一个 PNG 图像。

#include <cairo.h>

在此头文件中,我们将找到函数和常量的声明。

cairo_surface_t *surface;
cairo_t *cr;

在这里,我们声明一个表面和一个 Cairo 上下文。

surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 390, 60);
cr = cairo_create(surface);

我们创建一个表面和一个 Cairo 上下文。 表面是 390x60 像素的图像。

cairo_set_source_rgb(cr, 0, 0, 0);

我们将用黑色墨水绘制。

cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
    CAIRO_FONT_WEIGHT_NORMAL);
cairo_set_font_size(cr, 40.0);

我们选择一种字体类型并设置其大小。

cairo_move_to(cr, 10.0, 50.0);
cairo_show_text(cr, "Disziplin ist Macht.");

我们移到图像内的(10.0,50.0)位置并绘制文本。

cairo_surface_write_to_png(surface, "image.png");

此函数调用将创建 PNG 图像。

cairo_destroy(cr);
cairo_surface_destroy(surface);

最后,我们清理资源。

PDF 文件

在第二个示例中,我们将使用 Cairo 库创建一个简单的 PDF 文件。

#include <cairo.h>
#include <cairo-pdf.h>

int main(void) 
{
  cairo_surface_t *surface;
  cairo_t *cr;

  surface = cairo_pdf_surface_create("pdffile.pdf", 504, 648);
  cr = cairo_create(surface);

  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size (cr, 40.0);

  cairo_move_to(cr, 10.0, 50.0);
  cairo_show_text(cr, "Disziplin ist Macht.");

  cairo_show_page(cr);

  cairo_surface_destroy(surface);
  cairo_destroy(cr);

  return 0;
}

我们必须在 PDF 查看器中打开 PDF 文件。 Linux 用户可以使用 KPDF 或 Evince 查看器。

surface = cairo_pdf_surface_create("pdffile.pdf", 504, 648);

要渲染 PDF 文件,我们必须使用cairo_pdf_surface_create()函数调用来创建 PDF 曲面。 PDF 文件的大小以磅为单位指定,这是排版的标准。

cairo_show_page(cr);

cairo_show_page()完成 PDF 文件的渲染。

PDF file in Evince

图:Evince 中的 PDF 文件

SVG 文件

下一个示例创建一个简单的 SVG(可缩放矢量图形)文件。 SVG 是当今最热门的技术之一。

#include <cairo.h>
#include <cairo-svg.h> 

int main(void) 
{
  cairo_surface_t *surface;
  cairo_t *cr;

  surface = cairo_svg_surface_create("svgfile.svg", 390, 60);
  cr = cairo_create(surface);

  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, 40.0);

  cairo_move_to(cr, 10.0, 50.0);
  cairo_show_text(cr, "Disziplin ist Macht.");

  cairo_surface_destroy(surface);
  cairo_destroy(cr);

  return 0;
}

我们可以使用 Firefox,Opera 或 Inkscape 程序打开svgfile.svg文件。

surface = cairo_svg_surface_create("svgfile.svg", 390, 60);

要在 Cairo 创建 SVG 文件,我们必须使用cairo_svg_surface_create()函数调用来创建 svg 曲面。

cr = cairo_create(surface);

Cairo 上下文是从 SVG 曲面创建的。

其余代码与前面的示例相同。

SVG file in Chrome

SVG file in Chrome

GTK 窗口

在最后一个示例中,我们将在 GTK 窗口上绘制。 该后端将在本教程的其余部分中使用。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, 40.0);

  cairo_move_to(cr, 10.0, 50.0);
  cairo_show_text(cr, "Disziplin ist Macht.");    
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 400, 90); 
  gtk_window_set_title(GTK_WINDOW(window), "GTK window");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

该示例弹出一个居中的 GTK 窗口,在该窗口上绘制"Disziplin ist Macht"文本。

#include <cairo.h>
#include <gtk/gtk.h>

我们包括必要的 Cairo 和 GTK 标头。

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);

  return FALSE;
}

我们将实际图形委托给do_drawing()函数。 发送的参数是 Cairo 上下文。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, 40.0);

  cairo_move_to(cr, 10.0, 50.0);
  cairo_show_text(cr, "Disziplin ist Macht.");    
}

Cairo 功能执行绘图。

darea = gtk_drawing_area_new();
gtk_container_add(GTK_CONTAINER(window), darea);

我们创建一个GtkDrawingArea小部件,并将其添加到容器窗口。 用于自定义绘图。

g_signal_connect(G_OBJECT(darea), "draw", 
    G_CALLBACK(on_draw_event), NULL); 

当需要重新绘制GtkDrawingArea小部件时,它将发出draw信号。 我们将该信号连接到on_draw_event()回调。

GTK window

图:GTK 窗口

在本章中,我们介绍了受支持的 Cairo 后端。

Cairo 基本图形

原文: https://zetcode.com/gfx/cairo/basicdrawing/

在 Cairo 图形教程的这一部分中,我们将绘制一些基本图元。 我们将绘制简单的线条,使用填充和描边操作,我们将讨论笔划线,线帽和线连接。

直线

线是非常基本的矢量对象。 要画一条线,我们使用两个函数调用。 通过cairo_move_to()调用指定起点。 线的终点通过cairo_line_to()调用指定。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

struct {
  int count;
  double coordx[100];
  double coordy[100];
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{
  do_drawing(cr);

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_set_line_width(cr, 0.5);

  int i, j;
  for (i = 0; i <= glob.count - 1; i++ ) {
      for (j = 0; j <= glob.count - 1; j++ ) {
          cairo_move_to(cr, glob.coordx[i], glob.coordy[i]);
          cairo_line_to(cr, glob.coordx[j], glob.coordy[j]);
      }
  }

  glob.count = 0;
  cairo_stroke(cr);    
}

static gboolean clicked(GtkWidget *widget, GdkEventButton *event,
    gpointer user_data)
{
    if (event->button == 1) {
        glob.coordx[glob.count] = event->x;
        glob.coordy[glob.count++] = event->y;
    }

    if (event->button == 3) {
        gtk_widget_queue_draw(widget);
    }

    return TRUE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  glob.count = 0;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);

  gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);  

  g_signal_connect(window, "button-press-event", 
      G_CALLBACK(clicked), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); 
  gtk_window_set_title(GTK_WINDOW(window), "Lines");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

在我们的示例中,我们用鼠标左键随机单击一个窗口。 每次点击都存储在一个数组中。 当我们右键单击窗口时,所有点都与数组中的每个点相连。 这样,我们可以创建一些有趣的对象。 右键单击绘制的对象将清除窗口,我们可以单击另一个对象。

cairo_set_source_rgb(cr, 0, 0, 0);
cairo_set_line_width (cr, 0.5);

线条将用黑色墨水绘制,宽度为 0.5 点。

int i, j;
for (i = 0; i <= glob.count - 1; i++ ) {
    for (j = 0; j <= glob.count - 1; j++ ) {
        cairo_move_to(cr, glob.coordx[i], glob.coordy[i]);
        cairo_line_to(cr, glob.coordx[j], glob.coordy[j]);
    }
}

我们将数组中的每个点连接到其他每个点。

cairo_stroke(cr);

cairo_stroke()调用画线。

g_signal_connect(window, "button-press-event", 
    G_CALLBACK(clicked), NULL);

我们将button-press-event连接到单击的回调。

if (event->button == 1) {
    glob.coordx[glob.count] = event->x;
    glob.coordy[glob.count++] = event->y;
}

在单击的回调中,我们确定是单击鼠标左键还是单击鼠标右键。 如果单击鼠标左键,则将 x,y 坐标存储到数组中。

if (event->button == 3) {
    gtk_widget_queue_draw(widget);
}

通过右键单击,我们重新绘制窗口。

Lines

图:直线

填充和描边

描边操作绘制形状的轮廓,填充操作填充形状的内部。

#include <cairo.h>
#include <gtk/gtk.h>
#include <math.h>

static void do_drawing(cairo_t *, GtkWidget *);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{  
  do_drawing(cr, widget);  

  return FALSE;
}

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  GtkWidget *win = gtk_widget_get_toplevel(widget);

  int width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);

  cairo_set_line_width(cr, 9);  
  cairo_set_source_rgb(cr, 0.69, 0.19, 0);

  cairo_translate(cr, width/2, height/2);
  cairo_arc(cr, 0, 0, 50, 0, 2 * M_PI);
  cairo_stroke_preserve(cr);

  cairo_set_source_rgb(cr, 0.3, 0.4, 0.6); 
  cairo_fill(cr);      
}

int main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); 
  gtk_window_set_title(GTK_WINDOW(window), "Fill & stroke");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

在我们的示例中,我们将绘制一个圆并用纯色填充它。

#include <math.h>

M_PI常量需要此头文件。

GtkWidget *win = gtk_widget_get_toplevel(widget);

int width, height;
gtk_window_get_size(GTK_WINDOW(win), &width, &height);

在这里,我们获得了窗口的宽度和高度。 绘制圆时,将需要这些值。 当我们调整窗口大小时,圆圈将被调整大小。

cairo_set_line_width(cr, 9);  
cairo_set_source_rgb(cr, 0.69, 0.19, 0);

我们使用set_line_width()方法设置线宽。 我们使用set_source_rgb()方法将光源设置为深红色。

cairo_translate(cr, width/2, height/2);
cairo_arc(cr, 0, 0, 50, 0, 2 * M_PI);
cairo_stroke_preserve(cr);

使用cairo_translate()方法,我们将图形原点移动到窗口的中心。 我们希望我们的圈子居中。 arc()方法向 Cairo 绘图上下文添加了新的圆形路径。 最后,stroke_preserve()方法绘制圆的轮廓。 与stroke()方法不同,它还保留了形状以供以后绘制。

cairo_set_source_rgb(cr, 0.3, 0.4, 0.6); 
cairo_fill(cr);

在这里,我们用蓝色填充线圈。

Fill and stroke

图:填充和描边

笔划线

每条线可以用不同的笔划线绘制。 它定义了线条的样式。 笔划线由cairo_stroke()函数调用使用。 笔划线由cairo_set_dash()函数指定。 该模式由虚线数组设置,该数组是一个正浮点值的数组。 他们设置笔划线图案的开和关部分。 我们还指定了数组的长度和偏移值。 如果长度为 0,则禁用虚线。 如果为 1,则假定对称图案,并交替显示由虚线表示的单个值指定的大小的打开和关闭部分。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgba(cr, 0, 0, 0, 1);

  static const double dashed1[] = {4.0, 21.0, 2.0};
  static int len1  = sizeof(dashed1) / sizeof(dashed1[0]);

  static const double dashed2[] = {14.0, 6.0};
  static int len2  = sizeof(dashed2) / sizeof(dashed2[0]);

  static const double dashed3[] = {1.0};

  cairo_set_line_width(cr, 1.5);

  cairo_set_dash(cr, dashed1, len1, 0);

  cairo_move_to(cr, 40, 30);  
  cairo_line_to(cr, 200, 30);
  cairo_stroke(cr);

  cairo_set_dash(cr, dashed2, len2, 1);

  cairo_move_to(cr, 40, 50);  
  cairo_line_to(cr, 200, 50);
  cairo_stroke(cr);

  cairo_set_dash(cr, dashed3, 1, 0);

  cairo_move_to(cr, 40, 70);  
  cairo_line_to(cr, 200, 70);
  cairo_stroke(cr);  
}

在此示例中,我们将绘制三条具有不同笔划线图案的线。

static const double dashed1[] = {4.0, 21.0, 2.0};

我们有三个数字的模式。 我们得出 4 分,未得出 21 分,得出 2 分。 然后,未绘制 4 点,未绘制 21 点和未绘制 2 点。 该模式轮流直到行尾。

static int len1  = sizeof(dashed1) / sizeof(dashed1[0]);

我们得到数组的大小。

cairo_set_dash(cr, dashed1, len1, 0);

我们设置笔划线。

static const double dashed3[] = {1.0};
...
cairo_set_dash(cr, dashed3, 1, 0);

cairo_move_to(cr, 40, 70);  
cairo_line_to(cr, 200, 70);
cairo_stroke(cr);  

这些线创建了一条带有描边的对称划线交替交替的单个点和断开点的线。

Dashes

图:虚线

线帽

线帽是线的端点。

  • CAIRO_LINE_CAP_SQUARE
  • CAIRO_LINE_CAP_ROUND
  • CAIRO_LINE_CAP_BUTT

Cairo 有三种不同的线帽样式。

Line caps

图:正方形,圆和端帽

带有CAIRO_LINE_CAP_SQUARE上限的行的大小将不同于带有CAIRO_LINE_CAP_BUTT上限的行。 如果一条线的宽度为 px 宽,则带有CAIRO_LINE_CAP_SQUARE上限的线的宽度将恰好为 px 宽度。 开头为width / 2像素,结尾为width / 2像素。

static void do_drawing(cairo_t *cr)
{
  cairo_set_line_width(cr, 10);

  cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT); 
  cairo_move_to(cr, 30, 50); 
  cairo_line_to(cr, 150, 50);
  cairo_stroke(cr);

  cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); 
  cairo_move_to(cr, 30, 90); 
  cairo_line_to(cr, 150, 90);
  cairo_stroke(cr);

  cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE); 
  cairo_move_to(cr, 30, 130); 
  cairo_line_to(cr, 150, 130);
  cairo_stroke(cr);

  cairo_set_line_width(cr, 1.5);

  cairo_move_to(cr, 30, 40);  
  cairo_line_to(cr, 30, 140);
  cairo_stroke(cr);

  cairo_move_to(cr, 150, 40);  
  cairo_line_to(cr, 150, 140);
  cairo_stroke(cr);

  cairo_move_to(cr, 155, 40);  
  cairo_line_to(cr, 155, 140);
  cairo_stroke(cr);    
}

该示例绘制了具有三个不同上限的三条线。 它还将以图形方式显示行大小的差异。

cairo_set_line_width(cr, 10);

我们的线将是 10 像素宽。

cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); 
cairo_move_to(cr, 30, 90); 
cairo_line_to(cr, 150, 90);
cairo_stroke(cr);

在这里,我们用CAIRO_LINE_CAP_ROUND帽画一条水平线。

cairo_set_line_width(cr, 1.5);

cairo_move_to(cr, 30, 40);  
cairo_line_to(cr, 30, 140);
cairo_stroke(cr);

这是用来说明大小差异的三条垂直线之一。

Line caps

图:线帽

线连接

可以使用三种不同的连接样式来连接线:

  • CAIRO_LINE_JOIN_BEVEL
  • CAIRO_LINE_JOIN_ROUND
  • CAIRO_LINE_JOIN_MITER

Bevel, Round, Miter line joins

图:斜角,圆角,斜接线连接

CAIRO_LINE_JOIN_BEVEL使用切除连接,其中切除距离接合点的线宽一半。 CAIRO_LINE_JOIN_ROUND使用圆形连接,其中圆心是连接点。 CAIRO_LINE_JOIN_MITER使用了一个尖角。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0.1, 0, 0);

  cairo_rectangle(cr, 30, 30, 100, 100);
  cairo_set_line_width(cr, 14);
  cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER); 
  cairo_stroke(cr);

  cairo_rectangle(cr, 160, 30, 100, 100);
  cairo_set_line_width(cr, 14);
  cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL); 
  cairo_stroke(cr);

  cairo_rectangle(cr, 100, 160, 100, 100);
  cairo_set_line_width(cr, 14);
  cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); 
  cairo_stroke(cr);    
}

在此示例中,我们绘制了三个具有各种线连接的粗矩形。

cairo_rectangle(cr, 30, 30, 100, 100);
cairo_set_line_width(cr, 14);
cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER); 
cairo_stroke(cr);

在此代码示例中,我们绘制具有CAIRO_LINE_JOIN_MITER连接样式的矩形。 线宽为 14px。

Line joins

图:直线连接

在本章中,我们做了一些基本绘图。

形状和填充

原文: https://zetcode.com/gfx/cairo/shapesfills/

在 Cairo 教程的这一部分中,我们将创建一些基本的和更高级的形状。 我们将用纯色,图案和渐变填充它们。 渐变将在单独的章节中介绍。

基本形状

Cairo API 具有一些用于创建简单形状的基本函数。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
  cairo_set_line_width(cr, 1);

  cairo_rectangle(cr, 20, 20, 120, 80);
  cairo_rectangle(cr, 180, 20, 80, 80);
  cairo_stroke_preserve(cr);
  cairo_fill(cr);

  cairo_arc(cr, 330, 60, 40, 0, 2*M_PI);
  cairo_stroke_preserve(cr);
  cairo_fill(cr);

  cairo_arc(cr, 90, 160, 40, M_PI/4, M_PI);
  cairo_close_path(cr);
  cairo_stroke_preserve(cr);
  cairo_fill(cr);

  cairo_translate(cr, 220, 180);
  cairo_scale(cr, 1, 0.7);
  cairo_arc(cr, 0, 0, 50, 0, 2*M_PI);
  cairo_stroke_preserve(cr);
  cairo_fill(cr);
}

在此示例中,我们将创建一个矩形,正方形,圆形,弧形和椭圆形。

cairo_rectangle(cr, 20, 20, 120, 80);
cairo_rectangle(cr, 180, 20, 80, 80);

cairo_rectangle()用于创建正方形和矩形。 正方形只是矩形的一种特定类型。

cairo_arc(cr, 330, 60, 40, 0, 2*M_PI);

这条线创建一个圆。

cairo_scale(cr, 1, 0.7);
cairo_arc(cr, 0, 0, 50, 0, 2*M_PI);

我们使用cairo_scale()函数调用来创建一个椭圆。

Basic shapes

图:基本形状

可以使用基本图元的组合来创建其他形状。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

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

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{
  do_drawing(cr);

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{ 
  cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
  cairo_set_line_width(cr, 1);

  gint i;
  for (i = 0; i < 10; i++) {
      cairo_line_to(cr, points[i][0], points[i][1]);
  }

  cairo_close_path(cr);
  cairo_stroke_preserve(cr);
  cairo_fill(cr);

  cairo_move_to(cr, 240, 40);
  cairo_line_to(cr, 240, 160);
  cairo_line_to(cr, 350, 160);
  cairo_close_path(cr);

  cairo_stroke_preserve(cr);
  cairo_fill(cr);

  cairo_move_to(cr, 380, 40);
  cairo_line_to(cr, 380, 160);
  cairo_line_to(cr, 450, 160);
  cairo_curve_to(cr, 440, 155, 380, 145, 380, 40);

  cairo_stroke_preserve(cr);
  cairo_fill(cr);  
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 460, 240); 
  gtk_window_set_title(GTK_WINDOW(window), "Other shapes");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

在此示例中,我们将星形对象创建为三角形和修改后的三角形。 这些对象是使用直线和一条曲线创建的。

gint i;
for (i = 0; i < 10; i++ ) {
    cairo_line_to(cr, points[i][0], points[i][1]);
}

cairo_close_path(cr);

通过连接点数组中的所有点来绘制星形。 通过调用cairo_close_path()函数将星星结束,该函数将星星的最后两个点连接在一起。

cairo_move_to(cr, 380, 40);
cairo_line_to(cr, 380, 160);
cairo_line_to(cr, 450, 160);
cairo_curve_to(cr, 440, 155, 380, 145, 380, 40);

修改后的三角形是两条直线和一条曲线的简单组合。

Other shapes

图:其它形状

填充

填充填充形状的内部。 填充可以是纯色,图案或渐变。

纯色

颜色是代表红色,绿色和蓝色(RGB)强度值的组合的对象。 Cairo 有效 RGB 值在 0 到 1 的范围内。

static void do_drawing(cairo_t *cr)
{ 
  cairo_set_source_rgb(cr, 0.5, 0.5, 1);
  cairo_rectangle(cr, 20, 20, 100, 100);
  cairo_fill(cr);

  cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
  cairo_rectangle(cr, 150, 20, 100, 100);
  cairo_fill(cr);

  cairo_set_source_rgb(cr, 0, 0.3, 0);
  cairo_rectangle(cr, 20, 140, 100, 100);
  cairo_fill(cr);

  cairo_set_source_rgb(cr, 1, 0, 0.5);
  cairo_rectangle(cr, 150, 140, 100, 100);
  cairo_fill(cr);  
}

在示例中,我们绘制了四个彩色矩形。

cairo_set_source_rgb(cr, 0.5, 0.5, 1);
cairo_rectangle(cr, 20, 20, 100, 100);
cairo_fill(cr);

cairo_set_source_rgb()函数调用将源设置为不透明的颜色。 参数是红色,绿色和蓝色强度值。 通过调用cairo_fill()函数,源可用于填充矩形的内部。

Solid colours

图:纯色

图案

图案是可以填充形状的复杂图形对象。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

cairo_surface_t *surface1;
cairo_surface_t *surface2;
cairo_surface_t *surface3;
cairo_surface_t *surface4;

static void create_surfaces() {
  surface1 = cairo_image_surface_create_from_png("blueweb.png");
  surface2 = cairo_image_surface_create_from_png("maple.png");
  surface3 = cairo_image_surface_create_from_png("crack.png");
  surface4 = cairo_image_surface_create_from_png("chocolate.png");
}

static void destroy_surfaces() {
  cairo_surface_destroy(surface1);
  cairo_surface_destroy(surface2);
  cairo_surface_destroy(surface3);
  cairo_surface_destroy(surface4);
}

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{
  do_drawing(cr);

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_pattern_t *pattern1;
  cairo_pattern_t *pattern2;
  cairo_pattern_t *pattern3;
  cairo_pattern_t *pattern4;

  pattern1 = cairo_pattern_create_for_surface(surface1);
  pattern2 = cairo_pattern_create_for_surface(surface2);
  pattern3 = cairo_pattern_create_for_surface(surface3);
  pattern4 = cairo_pattern_create_for_surface(surface4);

  cairo_set_source(cr, pattern1);
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
  cairo_rectangle(cr, 20, 20, 100, 100);
  cairo_fill(cr);

  cairo_set_source(cr, pattern2); 
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT); 
  cairo_rectangle(cr, 150, 20, 100, 100);
  cairo_fill(cr);

  cairo_set_source(cr, pattern3);
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
  cairo_rectangle(cr, 20, 140, 100, 100);
  cairo_fill(cr);

  cairo_set_source(cr, pattern4);
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
  cairo_rectangle(cr, 150, 140, 100, 100);
  cairo_fill(cr);

  cairo_pattern_destroy(pattern1);
  cairo_pattern_destroy(pattern2);
  cairo_pattern_destroy(pattern3);
  cairo_pattern_destroy(pattern4);      
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);  

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  create_surfaces();

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 270, 260); 
  gtk_window_set_title(GTK_WINDOW(window), "Patterns");

  gtk_widget_show_all(window);

  gtk_main();

  destroy_surfaces();

  return 0;
}

在此示例中,我们再次绘制了四个矩形。 这次,我们用一些模式填充它们。 我们使用来自 Gimp 图像处理器的四个图案图像。 我们必须保留这些模式的原始大小,因为我们将对它们进行平铺。

我们在on_draw_event()函数之外创建图像表面。 每次需要重新绘制窗口时,每次从硬盘读取数据都不是很有效。

pattern1 = cairo_pattern_create_for_surface(surface1);

我们通过调用cairo_pattern_create_for_surface()函数从表面创建图案。

cairo_set_source(cr, pattern1);
cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
cairo_rectangle(cr, 20, 20, 100, 100);
cairo_fill(cr);

在这里,我们绘制第一个矩形。 cairo_set_source()告诉 Cairo 上下文使用图案作为绘图源。 图像图案可能不完全适合形状。 我们将模式设置为CAIRO_EXTEND_REPEAT,这将导致图案通过重复平铺。 cairo_rectangle()创建一个矩形路径。 最后,cairo_fill()用源填充路径。

本章介绍了 Cairo 的形状和填充。

渐变

原文: https://zetcode.com/gfx/cairo/gradients/

在 Cairo 图形教程的这一部分中,我们将介绍渐变。 我们将提到线性和径向渐变。

在计算机图形学中,渐变是从浅到深或从一种颜色到另一种颜色的阴影的平滑混合。 在 2D 绘图程序和绘画程序中,渐变用于创建彩色背景和特殊效果以及模拟灯光和阴影。 (answers.com)

线性渐变

线性渐变是沿着一条线的颜色混合或颜色阴影混合。 它们是使用cairo_pattern_create_linear()函数创建的。

#include <cairo.h>
#include <gtk/gtk.h>

void draw_gradient1(cairo_t *);
void draw_gradient2(cairo_t *);
void draw_gradient3(cairo_t *);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{         
  draw_gradient1(cr);
  draw_gradient2(cr);
  draw_gradient3(cr);  

  return FALSE;
}

void draw_gradient1(cairo_t *cr)
{
  cairo_pattern_t *pat1;  
  pat1 = cairo_pattern_create_linear(0.0, 0.0,  350.0, 350.0);

  gdouble j;
  gint count = 1;
  for ( j = 0.1; j < 1; j += 0.1 ) {
      if (( count % 2 ))  {
          cairo_pattern_add_color_stop_rgb(pat1, j, 0, 0, 0);
      } else { 
          cairo_pattern_add_color_stop_rgb(pat1, j, 1, 0, 0);
      }
   count++;
  }

  cairo_rectangle(cr, 20, 20, 300, 100);
  cairo_set_source(cr, pat1);
  cairo_fill(cr);  

  cairo_pattern_destroy(pat1);
}

void draw_gradient2(cairo_t *cr)
{
  cairo_pattern_t *pat2;
  pat2 = cairo_pattern_create_linear(0.0, 0.0,  350.0, 0.0);

  gdouble i;
  gint count = 1;
  for ( i = 0.05; i < 0.95; i += 0.025 ) {
      if (( count % 2 ))  {
          cairo_pattern_add_color_stop_rgb(pat2, i, 0, 0, 0);
      } else { 
          cairo_pattern_add_color_stop_rgb(pat2, i, 0, 0, 1);
      }
   count++;
  }

  cairo_rectangle(cr, 20, 140, 300, 100);
  cairo_set_source(cr, pat2);
  cairo_fill(cr);  

  cairo_pattern_destroy(pat2);
}

void draw_gradient3(cairo_t *cr)
{
  cairo_pattern_t *pat3;
  pat3 = cairo_pattern_create_linear(20.0, 260.0, 20.0, 360.0);

  cairo_pattern_add_color_stop_rgb(pat3, 0.1, 0, 0, 0);
  cairo_pattern_add_color_stop_rgb(pat3, 0.5, 1, 1, 0);
  cairo_pattern_add_color_stop_rgb(pat3, 0.9, 0, 0, 0);

  cairo_rectangle(cr, 20, 260, 300, 100);
  cairo_set_source(cr, pat3);
  cairo_fill(cr);  

  cairo_pattern_destroy(pat3);
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 340, 390); 
  gtk_window_set_title(GTK_WINDOW(window), "Linear gradients");

  gtk_widget_set_app_paintable(window, TRUE);
  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

该示例绘制了三个填充有线性渐变的矩形。

pat3 = cairo_pattern_create_linear(20.0, 260.0, 20.0, 360.0);

在这里,我们创建一个线性渐变图案。 参数指定直线,沿着该直线绘制渐变。 在我们的情况下,这是一条垂直线。

cairo_pattern_add_color_stop_rgb(pat3, 0.1, 0, 0, 0);
cairo_pattern_add_color_stop_rgb(pat3, 0.5, 1, 1, 0);
cairo_pattern_add_color_stop_rgb(pat3, 0.9, 0, 0, 0);

我们定义色标以产生渐变图案。 在这种情况下,渐变是黑色和黄色的混合。 通过添加两个黑色和一个黄色色标,我们创建了一个水平渐变图案。 这些停止实际上是什么意思? 在我们的情况下,我们从黑色开始,该颜色将以大小的 1/10 停止。 然后,我们开始逐渐涂成黄色,最终达到形状的中心。 黄色停在大小的 9/10,我们再次开始用黑色绘画,直到结束。

Linear gradients

图:线性渐变

径向渐变

径向渐变是两个圆之间颜色或阴影的混合。 cairo_pattern_create_radial()函数用于在 Cairo 创建径向渐变。

#include <cairo.h>
#include <math.h>
#include <gtk/gtk.h>

void draw_gradient1(cairo_t *);
void draw_gradient2(cairo_t *);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{         
  draw_gradient1(cr);
  draw_gradient2(cr); 

  return FALSE;
}

void draw_gradient1(cairo_t *cr)
{
  cairo_pattern_t *r1; 

  cairo_set_source_rgba(cr, 0, 0, 0, 1);
  cairo_set_line_width(cr, 12);  
  cairo_translate(cr, 60, 60);

  r1 = cairo_pattern_create_radial(30, 30, 10, 30, 30, 90);
  cairo_pattern_add_color_stop_rgba(r1, 0, 1, 1, 1, 1);
  cairo_pattern_add_color_stop_rgba(r1, 1, 0.6, 0.6, 0.6, 1);
  cairo_set_source(cr, r1);
  cairo_arc(cr, 0, 0, 40, 0, M_PI * 2);
  cairo_fill(cr);

  cairo_pattern_destroy(r1);
}

void draw_gradient2(cairo_t *cr)
{
  cairo_pattern_t *r2; 

  cairo_translate(cr, 120, 0);

  r2 = cairo_pattern_create_radial(0, 0, 10, 0, 0, 40);  
  cairo_pattern_add_color_stop_rgb(r2, 0, 1, 1, 0);
  cairo_pattern_add_color_stop_rgb(r2, 0.8, 0, 0, 0);
  cairo_set_source(cr, r2);
  cairo_arc(cr, 0, 0, 40, 0, M_PI * 2);
  cairo_fill(cr);     
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); 
  gtk_window_set_title(GTK_WINDOW(window), "Radial gradients");

  gtk_widget_set_app_paintable(window, TRUE);
  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

在示例中,我们绘制了两个径向渐变。

r1 = cairo_pattern_create_radial(30, 30, 10, 30, 30, 90);
cairo_pattern_add_color_stop_rgba(r1, 0, 1, 1, 1, 1);
cairo_pattern_add_color_stop_rgba(r1, 1, 0.6, 0.6, 0.6, 1);
cairo_set_source(cr, r1);
cairo_arc(cr, 0, 0, 40, 0, M_PI * 2);
cairo_fill(cr);

我们画一个圆,并用径向渐变填充其内部。 径向梯度由两个圆定义。 cairo_pattern_add_color_stop_rgba()函数定义颜色。 我们可以试验圆的位置或半径的长度。 在第一个渐变示例中,我们创建了一个类似于 3D 形状的对象。

r2 = cairo_pattern_create_radial(0, 0, 10, 0, 0, 40);  
cairo_pattern_add_color_stop_rgb(r2, 0, 1, 1, 0);
cairo_pattern_add_color_stop_rgb(r2, 0.8, 0, 0, 0);
cairo_set_source(cr, r2);
cairo_arc(cr, 0, 0, 40, 0, M_PI * 2);
cairo_fill(cr); 

在此示例中,定义径向渐变的圆和自定义绘制的圆具有共同的中心点。

Radial gradients

图:径向渐变

在 Cairo 图形教程的这一章中,我们介绍了渐变。

透明度

原文: https://zetcode.com/gfx/cairo/transparency/

在 Cairo C API 教程的这一部分中,我们将讨论透明度。 我们将提供一些基本定义和两个有趣的透明效果。

透明性是指能够透视材料的质量。 了解透明度的最简单方法是想象一块玻璃或水。 从技术上讲,光线可以穿过玻璃,这样我们就可以看到玻璃后面的物体。

在计算机图形学中,我们可以使用 alpha 合成实现透明效果。 Alpha 合成是将图像与背景组合以创建部分透明外观的过程。 合成过程使用 alpha 通道。 Alpha 通道是图形文件格式的 8 位层,用于表达半透明性(透明度)。 每个像素的额外八位用作掩码,表示 256 级半透明。
(answers.com,wikipedia.org)

透明矩形

第一个示例将绘制十个透明度不同的矩形。

static void do_drawing(cairo_t *cr)
{
  gint i;
  for ( i = 1; i <= 10; i++) {
      cairo_set_source_rgba(cr, 0, 0, 1, i*0.1);
      cairo_rectangle(cr, 50*i, 20, 40, 40);
      cairo_fill(cr);  
  }      
}

cairo_set_source_rgba()具有可选的 alpha 参数以提供透明度。 此代码创建十个矩形,其 alpha 值从 0.1 到 1。

Transparency

图:透明度

泡芙效果

在以下示例中,我们创建一个粉扑效果。 该示例将显示一个不断增长的居中文本,该文本将从某个点逐渐淡出。 这是一个非常常见的效果,我们经常可以在 Flash 动画中看到它。 cairo_paint_with_alpha()方法对于产生效果至关重要。

#include <cairo.h>
#include <gtk/gtk.h>

void do_drawing(cairo_t *, GtkWidget *);

struct {
  gboolean timer; 
  gdouble alpha;
  gdouble size;
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr, widget);

  return FALSE;
}

void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  cairo_text_extents_t extents;

  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);  

  gint x = width/2;
  gint y = height/2;

  cairo_set_source_rgb(cr, 0.5, 0, 0); 
  cairo_paint(cr);   

  cairo_select_font_face(cr, "Courier",
      CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_BOLD);

  glob.size += 0.8;

  if (glob.size > 20) {
      glob.alpha -= 0.01;
  }

  cairo_set_font_size(cr, glob.size);
  cairo_set_source_rgb(cr, 1, 1, 1); 

  cairo_text_extents(cr, "ZetCode", &extents);
  cairo_move_to(cr, x - extents.width/2, y);
  cairo_text_path(cr, "ZetCode");
  cairo_clip(cr);

  cairo_paint_with_alpha(cr, glob.alpha);

  if (glob.alpha <= 0) {
      glob.timer = FALSE;
  }     
}

static gboolean time_handler(GtkWidget *widget)
{ 
  if (!glob.timer) return FALSE;
  gtk_widget_queue_draw(widget);

  return TRUE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  

  glob.timer = TRUE;
  glob.alpha = 1.0;
  glob.size = 1.0;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 350, 200); 
  gtk_window_set_title(GTK_WINDOW(window), "Puff");

  g_timeout_add(14, (GSourceFunc) time_handler, (gpointer) window);

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

该示例在窗口上创建一个逐渐增长和褪色的文本。

struct {
  gboolean timer; 
  gdouble alpha;
  gdouble size;
} glob; 

在这里,我们在结构内部定义了一些变量。 这用于避免使用全局变量。

draw_text(cr, widget); 

文本的实际绘制委托给draw_text()函数。

GtkWidget *win = gtk_widget_get_toplevel(widget);

gint width, height;
gtk_window_get_size(GTK_WINDOW(win), &width, &height);  

gint x = width/2;
gint y = height/2;

文本将在窗口上居中。 因此,我们需要找出父窗口小部件的大小。

cairo_set_source_rgb(cr, 0.5, 0, 0); 
cairo_paint(cr); 

窗口的背景充满了一些深红色。

cairo_select_font_face(cr, "Courier",
    CAIRO_FONT_SLANT_NORMAL,
    CAIRO_FONT_WEIGHT_BOLD);

文本将以 Courier 粗体显示。

glob.size += 0.8;

if (glob.size > 20) {
    glob.alpha -= 0.01;
}

文本大小增加了 0.8 个单位。 达到 20 个单位后,alpha 值开始减小。 文本逐渐消失。

cairo_text_extents(cr, "ZetCode", &extents);
cairo_move_to(cr, x - extents.width/2, y);

我们得到了文本指标。 我们将仅使用文本宽度。 我们移动到文本将在窗口上居中的位置。

cairo_text_path(cr, "ZetCode");
cairo_clip(cr);

cairo_paint_with_alpha(cr, glob.alpha);

我们使用cairo_text_path()方法获得文本的路径。 我们使用cairo_clip()方法将绘画限制为当前路径。 cairo_paint_with_alpha()方法使用 alpha 值的掩码在当前剪裁区域内的任何地方绘制当前源。

glob.timer = TRUE;
glob.alpha = 1.0;
glob.size = 1.0;

我们初始化三个变量。

static gboolean time_handler(GtkWidget *widget)
{
  if (!glob.timer) return FALSE;

  gtk_widget_queue_draw(widget);

  return TRUE;
}

time_handler调用的主要功能是定期重绘窗口。 当函数返回FALSE时,超时功能将停止工作。

g_timeout_add(14, (GSourceFunc) time_handler, (gpointer) window);

我们创建一个计时器函数。 该函数每 14 毫秒调用一次time_handler

Puff effect

图:粉扑效果

等待演示

在此示例中,我们使用透明效果创建一个等待演示。 我们将绘制 8 条线,这些线将逐渐消失,从而产生一种错觉,即一条线在移动。 此类效果通常用于通知用户幕后正在进行繁重的任务。 一个示例是通过互联网流式传输视频。

#include <cairo.h>
#include <gtk/gtk.h>
#include <math.h>

static void do_drawing(cairo_t *, GtkWidget *);

struct {
  gushort count;
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr, widget);

  return FALSE;
}

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{  
  static gdouble const trs[8][8] = {
      { 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, }
  };

  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);

  cairo_translate(cr, width/2, height/2);

  gint i = 0;
  for (i = 0; i < 8; i++) {
      cairo_set_line_width(cr, 3);
      cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
      cairo_set_source_rgba(cr, 0, 0, 0, trs[glob.count%8][i]);

      cairo_move_to(cr, 0.0, -10.0);
      cairo_line_to(cr, 0.0, -40.0);
      cairo_rotate(cr, M_PI/4);

      cairo_stroke(cr);
  }   
}

static gboolean time_handler(GtkWidget *widget)
{
  glob.count += 1;
  gtk_widget_queue_draw(widget);

  return TRUE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  

  glob.count = 0;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 250, 150); 
  gtk_window_set_title(GTK_WINDOW(window), "Waiting demo");

  g_timeout_add(100, (GSourceFunc) time_handler, (gpointer) window);
  gtk_widget_show_all(window);  

  gtk_main();

  return 0;
}

我们用八个不同的 alpha 值绘制八条线。

 static gdouble const trs[8][8] = {
     { 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, }
 };

这是此演示中使用的透明度值的二维数组。 有 8 行,每行一种状态。 8 行中的每行将连续使用这些值。

cairo_set_line_width(cr, 3);
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);

我们使线条更粗一些,以便更好地显示它们。 我们用带帽的线画线。

cairo_set_source_rgba(cr, 0, 0, 0, trs[glob.count%8][i]);

在这里,我们定义了一条线的透明度值。

cairo_move_to(cr, 0.0, -10.0);
cairo_line_to(cr, 0.0, -40.0);
cairo_rotate(cr, M_PI/4);

这些代码将绘制八行中的每一行。

g_timeout_add(100, (GSourceFunc) time_handler, (gpointer) window);

我们使用计时器函数来创建动画。

Waiting demo

图:等待 demo

在 Cairo 教程的这一部分中,我们介绍了透明度。

合成

原文: https://zetcode.com/gfx/cairo/compositing/

在 Cairo 图形编程教程的这一部分中,我们将定义合成操作。

合成是将来自不同来源的视觉元素组合成单个图像。 它们被用来创建一种幻觉,即所有这些元素都是同一场景的一部分。 合成在电影行业中被广泛使用来创造人群,否则将是昂贵或不可能创造的整个新世界。 (wikipedia.org)

工作方式

有几种合成操作。 Cairo 图形库具有 14 种不同的合成操作。

#include <cairo.h>
#include <gtk/gtk.h>

void do_drawing(cairo_t *cr, gint x, gint w,
    gint h, cairo_operator_t op)
{
  cairo_t *first_cr, *second_cr;
  cairo_surface_t *first, *second;

  first = cairo_surface_create_similar(cairo_get_target(cr),
      CAIRO_CONTENT_COLOR_ALPHA, w, h);

  second = cairo_surface_create_similar(cairo_get_target(cr),
      CAIRO_CONTENT_COLOR_ALPHA, w, h);

  first_cr = cairo_create(first);
  cairo_set_source_rgb(first_cr, 0, 0, 0.4);
  cairo_rectangle(first_cr, x, 20, 50, 50);
  cairo_fill(first_cr);

  second_cr = cairo_create(second);
  cairo_set_source_rgb(second_cr, 0.5, 0.5, 0);
  cairo_rectangle(second_cr, x+10, 40, 50, 50);
  cairo_fill(second_cr);

  cairo_set_operator(first_cr, op);
  cairo_set_source_surface(first_cr, second, 0, 0);
  cairo_paint(first_cr);

  cairo_set_source_surface(cr, first, 0, 0);
  cairo_paint(cr);

  cairo_surface_destroy(first);
  cairo_surface_destroy(second);

  cairo_destroy(first_cr);
  cairo_destroy(second_cr);

}

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  cairo_operator_t oper[] = {
    CAIRO_OPERATOR_DEST_OVER, 
    CAIRO_OPERATOR_DEST_IN, 
    CAIRO_OPERATOR_OUT,
    CAIRO_OPERATOR_ADD, 
    CAIRO_OPERATOR_ATOP,
    CAIRO_OPERATOR_DEST_ATOP,
  };

  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);

  gint i;
  gint x, y;
  for(x=20, y=20, i=0; i < 6; x+=80, i++) {
      do_drawing(cr, x, width, height, oper[i] );
  }

  return FALSE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 510, 120);
  gtk_window_set_title(GTK_WINDOW(window), "Compositing operations");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

在我们的示例中,我们将在两个正方形上显示 6 种不同的合成操作。

first = cairo_surface_create_similar(cairo_get_target(cr),
    CAIRO_CONTENT_COLOR_ALPHA, w, h);

second = cairo_surface_create_similar(cairo_get_target(cr),
    CAIRO_CONTENT_COLOR_ALPHA, w, h);

我们创建两个曲面。

first_cr = cairo_create(first);
cairo_set_source_rgb(first_cr, 0, 0, 0.4);
cairo_rectangle(first_cr, x, 20, 50, 50);
cairo_fill(first_cr);

我们在表面绘制一个矩形。

cairo_set_operator(first_cr, op);
cairo_set_source_surface(first_cr, second, 0, 0);
cairo_paint(first_cr);

我们在曲面上应用合成操作。

cairo_set_source_surface(cr, first, 0, 0);
cairo_paint(cr);

最后,我们将结果绘制到 GTK+ 窗口上。

cairo_operator_t oper[] = {
  CAIRO_OPERATOR_DEST_OVER, 
  CAIRO_OPERATOR_DEST_IN, 
  CAIRO_OPERATOR_OUT,
  CAIRO_OPERATOR_ADD, 
  CAIRO_OPERATOR_ATOP,
  CAIRO_OPERATOR_DEST_ATOP,
};

在我们的示例中,我们使用这六个合成操作。

Compositing operations

图:合成操作

本章介绍了 Cairo 合成。

剪裁和遮罩

原文: https://zetcode.com/gfx/cairo/clippingmasking/

在 Cairo 教程的这一部分中,我们将讨论剪切和遮罩。

剪裁

剪裁将图形限制在某个区域。 这样做是出于效率方面的考虑,并会产生有趣的效果。

在下面的示例中,我们将裁剪图像。

#include <cairo.h>
#include <gtk/gtk.h>
#include <math.h>

static void do_drawing(cairo_t *, GtkWidget *);

struct {
  cairo_surface_t *image;
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr, widget);

  return FALSE;
}

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  static gint pos_x = 128;
  static gint pos_y = 128;
  static gint radius = 40;  
  static gint delta[] = { 3, 3 };

  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);

  if (pos_x < 0 + radius) {
      delta[0] = rand() % 4 + 5;
  } else if (pos_x > width - radius) {
      delta[0] = -(rand() % 4 + 5);
  }

  if (pos_y < 0 + radius) {
      delta[1] = rand() % 4 + 5;
  } else if (pos_y > height - radius) {
      delta[1] = -(rand() % 4 + 5);
  }

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

  cairo_set_source_surface(cr, glob.image, 1, 1);
  cairo_arc(cr, pos_x, pos_y, radius, 0, 2*M_PI);
  cairo_clip(cr);
  cairo_paint(cr);      
}

static gboolean time_handler(GtkWidget *widget)
{  
  gtk_widget_queue_draw(widget);
  return TRUE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  
  gint width, height;  

  glob.image = cairo_image_surface_create_from_png("turnacastle.png");
  width = cairo_image_surface_get_width(glob.image);
  height = cairo_image_surface_get_height(glob.image); 

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), width+2, height+2); 
  gtk_window_set_title(GTK_WINDOW(window), "Clip image");

  gtk_widget_show_all(window);
  g_timeout_add(100, (GSourceFunc) time_handler, (gpointer) window);

  gtk_main();

  cairo_surface_destroy(glob.image);

  return 0;
}

在此示例中,我们将裁剪图像。 屏幕上正在移动一个圆圈,并显示了一部分基础图像。 这就像我们从孔中看一样。

if (pos_x < 0 + radius) {
    delta[0] = rand() % 4 + 5;
} else if (pos_x > width - radius) {
    delta[0] = -(rand() % 4 + 5);
}

如果圆碰到窗口的左侧或右侧,则圆的移动方向会随机变化。 顶部和底部也一样。

cairo_set_source_surface(cr, glob.image, 1, 1);
cairo_arc(cr, pos_x, pos_y, radius, 0, 2*M_PI);

在这里,我们绘制图像和一个圆。 请注意,我们目前不在窗口上绘制,而仅在内存中绘制。

cairo_clip(cr);

cairo_clip()设置剪切区域。 裁剪区域是当前使用的路径。 当前路径是通过cairo_arc()函数调用创建的。

cairo_paint(cr);

cairo_paint()在当前剪裁区域内的任何地方绘制当前源。

glob.image = cairo_image_surface_create_from_png("turnacastle.png");

使用cairo_image_surface_create_from_png()函数从 PNG 图像创建图像表面。

Clipping image

图:剪裁图像

遮罩

在将源应用于表面之前,先对其进行过滤。 遮罩用作过滤器。 遮罩确定在哪里应用源,在哪里不应用。 遮罩的不透明部分允许复制源。 透明零件不允许将源复制到表面。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

struct {
  cairo_surface_t *surface;
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);  

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0, 0, 0);  
  cairo_mask_surface(cr, glob.surface, 0, 0);
  cairo_fill(cr);      
}

static void create_surface()
{
  glob.surface = cairo_image_surface_create_from_png("omen.png");
}

static void destroy_surface()
{
  cairo_surface_destroy(glob.surface);
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;  

  gtk_init(&argc, &argv);

  create_surface();

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);  
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 305, 100); 
  gtk_window_set_title(GTK_WINDOW(window), "Mask");

  gtk_widget_show_all(window);

  gtk_main();

  destroy_surface();

  return 0;
}

这个小例子清楚地说明了遮罩背后的基本思想。 遮罩可确定在何处绘画和不在何处绘画。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0, 0, 0);  
  cairo_mask_surface(cr, glob.surface, 0, 0);
  cairo_fill(cr);      
}

do_drawing()函数中,我们使用图像作为遮罩。 因此,它显示在窗口上。

Applying a mask

图:应用遮罩

蒙蔽效果

在此代码示例中,我们将忽略图像。 这类似于我们使用卷帘所做的。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

struct {
  cairo_surface_t *image;
  cairo_surface_t *surface;
  gboolean timer;
  gint img_width;
  gint img_height;
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);

  return FALSE;
}

static gboolean time_handler(GtkWidget *widget)
{
  if (!glob.timer) return FALSE;

  gtk_widget_queue_draw(widget);
  return TRUE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_t *ic;    
  static gint h = 0;     

  ic = cairo_create(glob.surface);

  cairo_rectangle(ic, 0, 0, glob.img_width, h);
  cairo_fill(ic);

  h += 1;
  if ( h == glob.img_height) glob.timer = FALSE;

  cairo_set_source_surface(cr, glob.image, 10, 10);
  cairo_mask_surface(cr, glob.surface, 10, 10);

  cairo_destroy(ic);  
}

static void init_vars()
{  
  glob.timer = TRUE;
  glob.image = cairo_image_surface_create_from_png("beckov.png");  
  glob.img_width = cairo_image_surface_get_width(glob.image);
  glob.img_height = cairo_image_surface_get_height(glob.image);  
  glob.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 
                     glob.img_width, glob.img_height);
}

static void cleanup()
{
  cairo_surface_destroy(glob.image);
  cairo_surface_destroy(glob.surface);   
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;    

  gtk_init(&argc, &argv);

  init_vars();

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER(window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 325, 250); 
  gtk_window_set_title(GTK_WINDOW(window), "Blind down");

  g_timeout_add(15, (GSourceFunc) time_handler, (gpointer) window);

  gtk_widget_show_all(window);

  gtk_main();

  cleanup();

  return 0;
}

盲目效应背后的想法很简单。 图像高度为h像素。 我们绘制高度为 1px 的 0、1、2 ... 线。 每个周期,图像的一部分高 1px,直到整个图像可见为止。

struct {
  cairo_surface_t *image;
  cairo_surface_t *surface;
  gboolean timer;
  gint img_width;
  gint img_height;
} glob;

在全局结构中,我们将存储两个表面,一个计时器以及图像的宽度和高度变量。

static void init_vars()
{  
  glob.timer = TRUE;
  glob.image = cairo_image_surface_create_from_png("beckov.png");  
  glob.img_width = cairo_image_surface_get_width(glob.image);
  glob.img_height = cairo_image_surface_get_height(glob.image);  
  glob.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 
                     glob.img_width, glob.img_height);
}

init_vars()函数中,我们初始化先前声明的变量。 最后一行创建一个空的图像表面。 它将用我们之前创建的图像表面的像素线填充。

ic = cairo_create(glob.surface);

我们从空图像源创建一个 cairo 上下文。

cairo_rectangle(ic, 0, 0, glob.img_width, h);
cairo_fill(ic);

我们在最初为空的图像中绘制一个矩形。 矩形每个周期将高出 1 像素。 以这种方式创建的图像稍后将用作遮罩。

h += 1;

要显示的图像高度增加一个单位。

if ( h == glob.img_height) glob.timer = FALSE;

当我们在 GTK 窗口上绘制整个图像时,我们将停止计时器函数。

cairo_set_source_surface(cr, glob.image, 10, 10);
cairo_mask_surface(cr, glob.surface, 10, 10);

城堡的图像被设置为绘画的来源。 cairo_mask_surface()使用表面的 Alpha 通道作为遮罩来绘制电流源。

static void cleanup()
{
  cairo_surface_destroy(glob.image);
  cairo_surface_destroy(glob.surface);   
}

cleanup()函数中,我们销毁了创建的曲面。

本章涉及在 Cairo 的​​剪切和遮罩。

变换

原文: https://zetcode.com/gfx/cairo/transformations/

在 Cairo 图形编程教程的这一部分中,我们将讨论变换。

仿射变换由零个或多个线性变换(旋转,缩放或剪切)和平移(移位)组成。 几个线性变换可以组合成一个矩阵。 旋转是使刚体绕固定点移动的变换。 缩放比例是一种放大或缩小对象的变换。 比例因子在所有方向上都是相同的。 变换是一种变换,可以使每个点在指定方向上移动恒定的距离。 剪切是一种将对象垂直于给定轴移动的变换,该值在轴的一侧比另一侧更大。

数据来源:(wikipedia.org,freedictionary.com)

平移

以下示例描述了一个简单的平移。

static void do_drawing(cairo_t *cr)
{ 
  cairo_set_source_rgb(cr, 0.2, 0.3, 0.8);
  cairo_rectangle(cr, 10, 10, 30, 30);
  cairo_fill(cr);

  cairo_translate(cr, 20, 20);
  cairo_set_source_rgb(cr, 0.8, 0.3, 0.2);
  cairo_rectangle(cr, 0, 0, 30, 30);
  cairo_fill(cr);

  cairo_translate(cr, 30, 30);
  cairo_set_source_rgb(cr, 0.8, 0.8, 0.2);
  cairo_rectangle(cr, 0, 0, 30, 30);
  cairo_fill(cr);

  cairo_translate(cr, 40, 40);
  cairo_set_source_rgb(cr, 0.3, 0.8, 0.8);
  cairo_rectangle(cr, 0, 0, 30, 30);
  cairo_fill(cr);    
}

该示例画一个矩形。 然后,我们进行平移并再次绘制相同的矩形。

cairo_translate(cr, 20, 20);

cairo_translate()函数通过变换用户空间原点来修改当前变换矩阵。 在我们的例子中,我们在两个方向上将原点移动了 20 个单位。

Translation

图:平移

剪切

在以下示例中,我们执行剪切操作。 剪切是沿特定轴的对象变形。 此操作没有剪切功能。 我们需要创建自己的变换矩阵。 注意,可以通过创建变换矩阵来执行每个仿射变换。

static void do_drawing(cairo_t *cr)
{  
  cairo_matrix_t matrix;

  cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
  cairo_rectangle(cr, 20, 30, 80, 50);
  cairo_fill(cr);
  cairo_matrix_init(&matrix,
      1.0, 0.5,
      0.0, 1.0,
      0.0, 0.0);

  cairo_transform(cr, &matrix);
  cairo_rectangle(cr, 130, 30, 80, 50);
  cairo_fill(cr);
}

在此代码示例中,我们执行一个简单的剪切操作。

cairo_matrix_t matrix;

cairo_matrix_t是具有仿射变换的结构。

cairo_matrix_init(&matrix,
    1.0, 0.5,
    0.0, 1.0,
    0.0, 0.0);

此变换将 y 值剪切为 x 值的 0.5。

cairo_transform(cr, &matrix);

我们使用transform()方法执行变换。

Shearing

图:抖动

缩放

下一个示例演示了缩放操作。 缩放是一种变换操作,其中对象被放大或缩小。

static void do_drawing(cairo_t *cr)
{        
  cairo_set_source_rgb(cr, 0.2, 0.3, 0.8);
  cairo_rectangle(cr, 10, 10, 90, 90);    
  cairo_fill(cr);

  cairo_scale(cr, 0.6, 0.6);
  cairo_set_source_rgb(cr, 0.8, 0.3, 0.2);
  cairo_rectangle(cr, 30, 30, 90, 90);    
  cairo_fill(cr);  

  cairo_scale(cr, 0.8, 0.8);
  cairo_set_source_rgb(cr, 0.8, 0.8, 0.2);
  cairo_rectangle(cr, 50, 50, 90, 90);    
  cairo_fill(cr);      
}

我们绘制三个90x90px的矩形。 在其中两个上,我们执行缩放操作。

cairo_scale(cr, 0.6, 0.6);
cairo_set_source_rgb(cr, 0.8, 0.3, 0.2);
cairo_rectangle(cr, 30, 30, 90, 90);    
cairo_fill(cr);  

我们将矩形均匀缩放 0.6 倍。

cairo_scale(cr, 0.8, 0.8);
cairo_set_source_rgb(cr, 0.8, 0.8, 0.2);
cairo_rectangle(cr, 50, 50, 90, 90);    
cairo_fill(cr); 

在这里,我们以 0.8 的系数执行另一个缩放操作。 如果看图片,我们看到第三个黄色矩形是最小的矩形。 即使我们使用了较小的比例因子。 这是因为变换操作是累加的。 实际上,第三个矩形的缩放比例为 0.528(0.6x0.8)。

Scaling

图:缩放

隔离变换

变换操作是累加的。 为了将一个操作与另一个操作隔离开,我们可以使用cairo_save()cairo_restore()函数。 cairo_save()函数可复制图形上下文的当前状态,并将其保存在已保存状态的内部栈中。 cairo_restore()函数将把上下文重新建立为保存状态。

static void do_drawing(cairo_t *cr)
{     
  cairo_set_source_rgb(cr, 0.2, 0.3, 0.8);
  cairo_rectangle(cr, 10, 10, 90, 90);    
  cairo_fill(cr);

  cairo_save(cr);
  cairo_scale(cr, 0.6, 0.6);
  cairo_set_source_rgb(cr, 0.8, 0.3, 0.2);
  cairo_rectangle(cr, 30, 30, 90, 90);    
  cairo_fill(cr);
  cairo_restore(cr);

  cairo_save(cr);
  cairo_scale(cr, 0.8, 0.8);
  cairo_set_source_rgb(cr, 0.8, 0.8, 0.2);
  cairo_rectangle(cr, 50, 50, 90, 90);    
  cairo_fill(cr);        
  cairo_restore(cr);
}

在示例中,我们缩放了两个矩形。 这次我们将缩放操作相互隔离。

cairo_save(cr);
cairo_scale(cr, 0.6, 0.6);
cairo_set_source_rgb(cr, 0.8, 0.3, 0.2);
cairo_rectangle(cr, 30, 30, 90, 90);    
cairo_fill(cr);
cairo_restore(cr);

我们通过将cairo_save()cairo_restore()函数之间的cairo_scale()函数隔离缩放操作。

Isolating transformations

图:隔离转换

现在,第三个黄色矩形大于第二个红色矩形。

甜甜圈

在下面的示例中,我们通过旋转一堆椭圆来创建复杂的形状。

#include <cairo.h>
#include <gtk/gtk.h>
#include <math.h>

static void do_drawing(cairo_t *, GtkWidget *widget);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{        
  do_drawing(cr, widget);

  return FALSE;
}

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{         
  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);

  cairo_set_line_width(cr, 0.5);
  cairo_translate(cr, width/2, height/2);
  cairo_arc(cr, 0, 0, 120, 0, 2 * M_PI);
  cairo_stroke(cr);

  gint i;
  for (i = 0; i < 36; i++) {
      cairo_save(cr);
      cairo_rotate(cr, i*M_PI/36);
      cairo_scale(cr, 0.3, 1);
      cairo_arc(cr, 0, 0, 120, 0, 2 * M_PI);
      cairo_restore(cr);
      cairo_stroke(cr);      
  }    
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL);
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 350, 250); 
  gtk_window_set_title(GTK_WINDOW(window), "Donut");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

我们将进行旋转和缩放操作。 我们还将保存和恢复 Cairo 上下文。

cairo_translate(cr, width/2, height/2);
cairo_arc(cr, 0, 0, 120, 0, 2 * M_PI);
cairo_stroke(cr);

在 GTK+ 窗口的中间,我们创建了一个圆。 这将是我们椭圆的边界圆。

gint i;
for (i = 0; i < 36; i++) {
    cairo_save(cr);
    cairo_rotate(cr, i*M_PI/36);
    cairo_scale(cr, 0.3, 1);
    cairo_arc(cr, 0, 0, 120, 0, 2 * M_PI);
    cairo_restore(cr);
    cairo_stroke(cr);      
}   

我们沿着边界圆的路径创建了 36 个椭圆。 我们使用cairo_save()cairo_restore()方法将每个旋转和缩放操作相互隔离。

星星

下一个示例显示了一个旋转和缩放的星星。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *, GtkWidget *widget);

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

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr, widget);

  return FALSE;
}

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  static gdouble angle = 0;
  static gdouble scale = 1;
  static gdouble delta = 0.01;

  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint width, height;
  gtk_window_get_size(GTK_WINDOW(win), &width, &height);

  cairo_set_source_rgb(cr, 0, 0.44, 0.7);
  cairo_set_line_width(cr, 1);

  cairo_translate(cr, width/2, height/2 );
  cairo_rotate(cr, angle);
  cairo_scale(cr, scale, scale);

  gint i;

  for ( i = 0; i < 10; i++ ) {
      cairo_line_to(cr, points[i][0], points[i][1]);
  }

  cairo_close_path(cr);
  cairo_fill(cr);
  cairo_stroke(cr);

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

  scale += delta;
  angle += 0.01;    
}

static gboolean time_handler(GtkWidget *widget)
{
  gtk_widget_queue_draw(widget);

  return TRUE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);  

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); 
  gtk_window_set_title(GTK_WINDOW(window), "Star");

  g_timeout_add(10, (GSourceFunc) time_handler, (gpointer) window);  

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

在此示例中,我们创建一个星形对象。 我们将对其进行平移,旋转和缩放。

int points[11][2] = { 
    { 0, 85 }, 
    { 75, 75 }, 
    { 100, 10 }, 
...

从这些点将构造星形对象。

static gdouble angle = 0;
static gdouble scale = 1;
static gdouble delta = 0.01;

我们初始化三个重要变量。 角度用于旋转,比例用于缩放星形对象。 delta变量控制星星何时生长以及何时收缩。

cairo_translate(cr, width/2, height/2);
cairo_rotate(cr, angle);
cairo_scale(cr, scale, scale);

我们将星星移到窗口中间。 旋转并缩放比例。

gint i;
for ( i = 0; i < 10; i++ ) {
    cairo_line_to(cr, points[i][0], points[i][1]);
}

cairo_close_path(cr);
cairo_fill(cr);
cairo_stroke(cr);

在这里,我们绘制星形对象。

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

这些线控制星形对象的生长或收缩。

在 Cairo 图形教程的这一部分中,我们讨论了变换。

Cairo 文字

原文: https://zetcode.com/gfx/cairo/cairotext/

在 Cairo 图形教程的这一部分中,我们将处理文本。

灵魂伴侣

在第一个示例中,我们将在 GTK+ 窗口上显示一些歌词。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); 

  cairo_select_font_face(cr, "Purisa",
      CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_BOLD);

  cairo_set_font_size(cr, 13);

  cairo_move_to(cr, 20, 30);
  cairo_show_text(cr, "Most relationships seem so transitory");  
  cairo_move_to(cr, 20, 60);
  cairo_show_text(cr, "They're all good but not the permanent one");

  cairo_move_to(cr, 20, 120);
  cairo_show_text(cr, "Who doesn't long for someone to hold");

  cairo_move_to(cr, 20, 150);
  cairo_show_text(cr, "Who knows how to love you without being told");
  cairo_move_to(cr, 20, 180);
  cairo_show_text(cr, "Somebody tell me why I'm on my own");
  cairo_move_to(cr, 20, 210);
  cairo_show_text(cr, "If there's a soulmate for everyone");    
}

在此示例中,我们显示了 Natasha Bedingfield 的 Soulmate 歌曲的部分歌词。

cairo_select_font_face(cr, "Purisa",
   CAIRO_FONT_SLANT_NORMAL,
   CAIRO_FONT_WEIGHT_BOLD);

在这里,我们选择字体。 该函数采用三个参数,字体系列,字体倾斜度和字体粗细。

cairo_set_font_size(cr, 13);

在这里,我们指定字体大小。

cairo_move_to(cr, 20, 30);
cairo_show_text(cr, "Most relationships seem so transitory"); 

通过指定文本的位置并调用cairo_show_text()函数,可以在窗口上显示文本。

Soulmate

图:灵魂伴侣

居中文字

接下来,我们将展示如何在窗口上居中放置文本。

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  cairo_text_extents_t extents;

  GtkWidget *win = gtk_widget_get_toplevel(widget);

  gint w, h;
  gtk_window_get_size(GTK_WINDOW(win), &w, &h);    

  cairo_select_font_face(cr, "Courier",
      CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_BOLD);

  cairo_set_font_size(cr, 60);

  cairo_text_extents(cr, "ZetCode", &extents);

  cairo_move_to(cr, w/2 - extents.width/2, h/2);  
  cairo_show_text(cr, "ZetCode");    
}

该代码将使文本在窗口上居中。 即使我们调整窗口大小,它仍然居中。

GtkWidget *win = gtk_widget_get_toplevel(widget);

gint w, h;
gtk_window_get_size(GTK_WINDOW(win), &w, &h);    

为了使文本在窗口上居中,有必要获取父窗口的大小。

cairo_select_font_face(cr, "Courier",
    CAIRO_FONT_SLANT_NORMAL,
    CAIRO_FONT_WEIGHT_BOLD);

cairo_set_font_size(cr, 60);

我们选择要显示的字体及其大小。

cairo_text_extents(cr, "ZetCode", &extents);

我们得到了文本范围。 这些是描述文字的数字。 我们的示例需要文本的宽度。

cairo_move_to(cr, w/2 - extents.width/2, h/2);  
cairo_show_text(cr, "ZetCode");   

我们将文本放置在窗口的中间,并使用cairo_show_text()方法显示它。

Centered text

图:居中文本

带阴影的文字

现在,我们将在窗口上显示阴影文本。

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  cairo_select_font_face(cr, "Serif", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, 50);

  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_move_to(cr, 40, 60);  
  cairo_show_text(cr, "ZetCode");  

  cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
  cairo_move_to(cr, 43, 63);  
  cairo_show_text(cr, "ZetCode");    
}

要创建阴影,我们将文本绘制两次。 以不同的颜色。 第二个文本向右和向下移动一点。

cairo_set_source_rgb(cr, 0, 0, 0);
cairo_move_to(cr, 40, 60);  
cairo_show_text(cr, "ZetCode"); 

第一个文本用黑色墨水绘制。 它充当阴影。

cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
cairo_move_to(cr, 43, 63);  
cairo_show_text(cr, "ZetCode");  

第二个文本用灰色墨水绘制。 它向右和向下移动了 3px。

Shaded text

图:阴影文本

渐变填充文本

以下示例将产生很好的效果。 我们将使用一些线性渐变填充文本。

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{  
  cairo_pattern_t *pat; 

  cairo_set_source_rgb(cr, 0.2, 0.2, 0.2);
  cairo_paint(cr);

  gint h = 90;

  cairo_select_font_face(cr, "Serif", CAIRO_FONT_SLANT_ITALIC, 
      CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, h);

  pat = cairo_pattern_create_linear(0, 15, 0, h*0.8);
  cairo_pattern_set_extend(pat, CAIRO_EXTEND_REPEAT);
  cairo_pattern_add_color_stop_rgb(pat, 0.0, 1, 0.6, 0);
  cairo_pattern_add_color_stop_rgb(pat, 0.5, 1, 0.3, 0);

  cairo_move_to(cr, 15, 80);
  cairo_text_path(cr, "ZetCode");
  cairo_set_source(cr, pat);
  cairo_fill(cr);
}

我们在充满线性渐变的窗口上绘制文本。 颜色是一些橙色。

cairo_set_source_rgb(cr, 0.2, 0.2, 0.2);
cairo_paint(cr);

为了使其更具视觉吸引力,我们将背景涂成深灰色。

pat = cairo_pattern_create_linear(0, 15, 0, h*0.8);
cairo_pattern_set_extend(pat, CAIRO_EXTEND_REPEAT);
cairo_pattern_add_color_stop_rgb(pat, 0.0, 1, 0.6, 0);
cairo_pattern_add_color_stop_rgb(pat, 0.5, 1, 0.3, 0);

将创建线性渐变。

cairo_move_to(cr, 15, 80);
cairo_text_path(cr, "ZetCode");
cairo_set_source(cr, pat);
cairo_fill(cr);

文本显示在窗口上。 我们使用渐变作为绘画源。

Text filled with gradient

图:用渐变填充的文本

字形

cairo_show_text()方法仅适用于简单的文本呈现。 Cairo 开发者将其称为玩具方法。 使用字形可以完成更专业的文本渲染。 标志符号是图形符号,可提供字符形式。 字符提供含义。 它可以有多个字形。 角色没有内在的外观。 字形没有内在的含义。

请注意,Pango 库解决了许多常见的编程要求,包括文本。

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{    
  cairo_select_font_face(cr, "Serif", CAIRO_FONT_SLANT_NORMAL,
      CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, 13);

  const int n_glyphs = 20 * 35;
  cairo_glyph_t glyphs[n_glyphs];

  gint i = 0;  
  gint x, y;

  for (y=0; y<20; y++) {
      for (x=0; x<35; x++) {
          glyphs[i] = (cairo_glyph_t) {i, x*15 + 20, y*18 + 20};
          i++;
      }
  }

  cairo_show_glyphs(cr, glyphs, n_glyphs);
} 

该代码显示了所选字体的 700 个字形。

const int n_glyphs = 20 * 35;
cairo_glyph_t glyphs[n_glyphs];

字形数组将存储三个整数值。 第一个值是字形到所选字体类型的索引。 第二和第三值是字形的 x,y 位置。

cairo_show_glyphs(cr, glyphs, n_glyphs);

cairo_show_glyphs()方法在窗口上显示字形。

本章介绍了 Cairo 文本。

动画

原文: https://zetcode.com/tutorials/javagamestutorial/animation/

在 Java 2D 游戏教程的这一部分中,我们将使用动画。

动画

动画是图像序列的快速显示,会产生运动的错觉。 我们将为董事会上的星星设置动画。 我们将以三种基本方式实现这一运动。 我们将使用 Swing 计时器,标准工具计时器和线程。

动画是游戏编程中的一个复杂主题。 Java 游戏有望在具有不同硬件规格的多种操作系统上运行。 线程提供了最准确的计时解决方案。 但是,对于我们简单的 2D 游戏,其他两个选项也可以是一个选项。

Swing 计时器

在第一个示例中,我们将使用 Swing 计时器来创建动画。 这是在 Java 游戏中为对象设置动画的最简单但最无效的方法。

SwingTimerEx.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class SwingTimerEx extends JFrame {

    public SwingTimerEx() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setResizable(false);
        pack();

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            SwingTimerEx ex = new SwingTimerEx();
            ex.setVisible(true);
        });
    }
}

这是代码示例的主要类。

setResizable(false);
pack();

setResizable()设置是否可以调整帧大小。 pack()方法使此窗口的大小适合其子级的首选大小和布局。 请注意,这两种方法的调用顺序很重要。 (setResizable()在某些平台上更改了帧的插入;在pack()方法之后调用此方法可能会导致错误的结果-星号不会精确地进入窗口的右下边界。)

Board.java

package com.zetcode;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Board extends JPanel
        implements ActionListener {

    private final int B_WIDTH = 350;
    private final int B_HEIGHT = 350;
    private final int INITIAL_X = -40;
    private final int INITIAL_Y = -40;
    private final int DELAY = 25;

    private Image star;
    private Timer timer;
    private int x, y;

    public Board() {

        initBoard();
    }

    private void loadImage() {

        ImageIcon ii = new ImageIcon("src/resources/star.png");
        star = ii.getImage();
    }

    private void initBoard() {

        setBackground(Color.BLACK);
        setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));

        loadImage();

        x = INITIAL_X;
        y = INITIAL_Y;

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

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

        drawStar(g);
    }

    private void drawStar(Graphics g) {

        g.drawImage(star, x, y, this);
        Toolkit.getDefaultToolkit().sync();
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        x += 1;
        y += 1;

        if (y > B_HEIGHT) {

            y = INITIAL_Y;
            x = INITIAL_X;
        }

        repaint();
    }
}

Board类中,我们将星星从左上角移到右下角。

private final int B_WIDTH = 350;
private final int B_HEIGHT = 350;
private final int INITIAL_X = -40;
private final int INITIAL_Y = -40;
private final int DELAY = 25;

定义了五个常数。 前两个常数是板的宽度和高度。 第三和第四是星星的初始坐标。 最后一个确定动画的速度。

private void loadImage() {

    ImageIcon ii = new ImageIcon("src/resources/star.png");
    star = ii.getImage();
}

loadImage()方法中,我们创建ImageIcon类的实例。 该图像位于项目目录中。 getImage()方法将从此类返回Image对象。 该对象将绘制在板上。

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

在这里,我们创建一个 Swing Timer类,并调用其start()方法。 计时器每DELAY毫秒就会调用一次actionPerformed()方法。 为了使用actionPerformed()方法,我们必须实现ActionListener接口。

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

    drawStar(g);
}

自定义绘画是通过paintComponent()方法完成的。 请注意,我们还调用其父级的paintComponent()方法。 实际绘画将委托给drawStar()方法。

private void drawStar(Graphics g) {

    g.drawImage(star, x, y, this);
    Toolkit.getDefaultToolkit().sync();
}

drawStar()方法中,我们使用drawImage()方法在窗口上绘制图像。 Toolkit.getDefaultToolkit().sync()在缓冲图形事件的系统上同步绘画。 没有这条线,动画在 Linux 上可能会不流畅。

@Override
public void actionPerformed(ActionEvent e) {

    x += 1;
    y += 1;

    if (y > B_HEIGHT) {

        y = INITIAL_Y;
        x = INITIAL_X;
    }

    repaint();
}

计时器反复调用actionPerformed()方法。 在方法内部,我们增加星形对象的 x 和 y 值。 然后我们调用repaint()方法,这将导致paintComponent()被调用。 这样,我们可以定期重绘Board从而制作动画。

Star

图:星星

实用计时器

这与以前的方法非常相似。 我们使用java.util.Timer代替javax.Swing.Timer。 对于 Java Swing 游戏,这种方式更为准确。

UtilityTimerEx.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class UtilityTimerEx extends JFrame {

    public UtilityTimerEx() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setResizable(false);
        pack();

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            JFrame ex = new UtilityTimerEx();
            ex.setVisible(true);
        });
    }
}

这是主要的类。

Board.java

package com.zetcode;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.ImageIcon;
import javax.swing.JPanel;

public class Board extends JPanel  {

    private final int B_WIDTH = 350;
    private final int B_HEIGHT = 350;
    private final int INITIAL_X = -40;
    private final int INITIAL_Y = -40;    
    private final int INITIAL_DELAY = 100;
    private final int PERIOD_INTERVAL = 25;

    private Image star;
    private Timer timer;
    private int x, y;

    public Board() {

        initBoard();        
    }

    private void loadImage() {

        ImageIcon ii = new ImageIcon("src/resources/star.png");
        star = ii.getImage();        
    }

    private void initBoard() {

        setBackground(Color.BLACK);
        setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));

        loadImage();

        x = INITIAL_X;
        y = INITIAL_Y;

        timer = new Timer();
        timer.scheduleAtFixedRate(new ScheduleTask(), 
                INITIAL_DELAY, PERIOD_INTERVAL);        
    }

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

        drawStar(g);
    }

    private void drawStar(Graphics g) {

        g.drawImage(star, x, y, this);
        Toolkit.getDefaultToolkit().sync();
    }

    private class ScheduleTask extends TimerTask {

        @Override
        public void run() {

            x += 1;
            y += 1;

            if (y > B_HEIGHT) {
                y = INITIAL_Y;
                x = INITIAL_X;
            }

            repaint();
        }
    }
}

在此示例中,计时器将定期调用ScheduleTask类的run()方法。

timer = new Timer();
timer.scheduleAtFixedRate(new ScheduleTask(), 
        INITIAL_DELAY, PERIOD_INTERVAL); 

在这里,我们创建一个计时器并按特定的时间间隔安排任务。 有一个初始延迟。

@Override
public void run() {
    ...
}

计时器每 10 毫秒将调用此run()方法。

线程

使用线程对对象进行动画处理是最有效,最准确的动画处理方式。

ThreadAnimationEx.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class ThreadAnimationEx extends JFrame {

    public ThreadAnimationEx() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setResizable(false);
        pack();

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

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            JFrame ex = new ThreadAnimationEx();
            ex.setVisible(true);
        });
    }
}

This is the main class.

Board.java

package com.zetcode;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class Board extends JPanel
        implements Runnable {

    private final int B_WIDTH = 350;
    private final int B_HEIGHT = 350;
    private final int INITIAL_X = -40;
    private final int INITIAL_Y = -40;
    private final int DELAY = 25;

    private Image star;
    private Thread animator;
    private int x, y;

    public Board() {

        initBoard();
    }

    private void loadImage() {

        ImageIcon ii = new ImageIcon("src/resources/star.png");
        star = ii.getImage();
    }

    private void initBoard() {

        setBackground(Color.BLACK);
        setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));

        loadImage();

        x = INITIAL_X;
        y = INITIAL_Y;
    }

    @Override
    public void addNotify() {
        super.addNotify();

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

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

        drawStar(g);
    }

    private void drawStar(Graphics g) {

        g.drawImage(star, x, y, this);
        Toolkit.getDefaultToolkit().sync();
    }

    private void cycle() {

        x += 1;
        y += 1;

        if (y > B_HEIGHT) {

            y = INITIAL_Y;
            x = INITIAL_X;
        }
    }

    @Override
    public void run() {

        long beforeTime, timeDiff, sleep;

        beforeTime = System.currentTimeMillis();

        while (true) {

            cycle();
            repaint();

            timeDiff = System.currentTimeMillis() - beforeTime;
            sleep = DELAY - timeDiff;

            if (sleep < 0) {
                sleep = 2;
            }

            try {
                Thread.sleep(sleep);
            } catch (InterruptedException e) {

                String msg = String.format("Thread interrupted: %s", e.getMessage());

                JOptionPane.showMessageDialog(this, msg, "Error", 
                    JOptionPane.ERROR_MESSAGE);
            }

            beforeTime = System.currentTimeMillis();
        }
    }
}

在前面的示例中,我们以特定的间隔执行任务。 在此示例中,动画将在线程内进行。 run()方法仅被调用一次。 这就是为什么我们在方法中有一个while循环的原因。 从该方法中,我们称为cycle()repaint()方法。

@Override
public void addNotify() {
    super.addNotify();

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

在将我们的JPanel添加到JFrame组件后,将调用addNotify()方法。 此方法通常用于各种初始化任务。

我们希望我们的游戏以恒定的速度平稳运行。 因此,我们计算系统时间。

timeDiff = System.currentTimeMillis() - beforeTime;
sleep = DELAY - timeDiff;

cycle()repaint()方法可能在不同的while周期中花费不同的时间。 我们计算两种方法的运行时间,并将其从DELAY常数中减去。 这样,我们要确保每个while周期都在恒定时间运行。 在我们的情况下,每个周期为DELAY ms。

Java 2D 游戏教程的这一部分涵盖了动画。

Cairo 中的图像

原文: https://zetcode.com/gfx/cairo/cairoimages/

在 Cairo 图形教程的这一部分中,我们将讨论图像。 我们将展示如何在 GTK 窗口上显示图像。 我们还将用图像创建一些效果。

显示图像

在第一个示例中,我们将显示一个图像。

#include <cairo.h>
#include <gtk/gtk.h>

struct {
  cairo_surface_t *image;  
} glob;

static void do_drawing(cairo_t *);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_surface(cr, glob.image, 10, 10);
  cairo_paint(cr);    
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  glob.image = cairo_image_surface_create_from_png("stmichaelschurch.png");

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK (gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 300, 220); 
  gtk_window_set_title(GTK_WINDOW(window), "Image");

  gtk_widget_show_all(window);

  gtk_main();

  cairo_surface_destroy(glob.image);

  return 0;
}

该示例显示图像。

glob.image = cairo_image_surface_create_from_png("stmichaelschurch.png");

我们从 PNG 图像创建图像表面。 出于效率原因,该函数在主函数中调用。

cairo_set_source_surface(cr, glob.image, 10, 10);

我们从创建的图像表面创建一个绘画源。

cairo_paint(cr);   

我们在窗口上绘制源。

cairo_surface_destroy(glob.image);

最后,表面被破坏。

水印

在图像上绘制信息是很常见的。 写在图像上的文本称为水印。 水印用于识别图像。 它们可能是版权声明或图像创建时间。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *, GtkWidget *widget);

struct {
  cairo_surface_t *image;  
} glob;

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr, widget);  

  return FALSE;
}

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  cairo_set_source_surface(cr, glob.image, 10, 10);
  cairo_paint(cr);
}

static void load_image()
{        
  glob.image = cairo_image_surface_create_from_png("beckov.png"); 
}

static void draw_mark() 
{ 
  cairo_t *ic;
  ic = cairo_create(glob.image);
  cairo_set_font_size(ic, 11);

  cairo_set_source_rgb(ic, 0.9 , 0.9 , 0.9);
  cairo_move_to(ic, 20, 30);
  cairo_show_text(ic, " Beckov 2012 , (c) Jan Bodnar ");
  cairo_stroke(ic);   
}

int main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  load_image();
  draw_mark();

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 350, 250); 
  gtk_window_set_title(GTK_WINDOW(window), "Watermark");

  gtk_widget_show_all(window);

  gtk_main();

  cairo_surface_destroy(glob.image);

  return 0;
}

我们在图像上绘制版权信息。

static void load_image()
{        
  glob.image = cairo_image_surface_create_from_png("beckov.png"); 
}

load_image()方法中,我们从 PNG 图像创建图像表面。

static void draw_mark() 
{ 
  cairo_t *ic;
  ic = cairo_create(glob.image);
...

draw_mark()函数中,我们在图像上绘制版权信息。 首先,我们从图像表面创建一个绘图上下文。

cairo_set_font_size(ic, 11);

cairo_set_source_rgb(ic, 0.9 , 0.9 , 0.9);
cairo_move_to(ic, 20, 30);
cairo_show_text(ic, " Beckov 2012 , (c) Jan Bodnar ");
cairo_stroke(ic);   

然后,我们用白色绘制一个小的文本。

static void do_drawing(cairo_t *cr, GtkWidget *widget)
{
  cairo_set_source_surface(cr, glob.image, 10, 10);
  cairo_paint(cr);
}

图像表面绘制在窗口上。

频谱效应

我们称其为频谱效应,因为它类似于旧的 ZX 频谱计算机。 当您将图像加载到这台计算机时,它逐渐出现在屏幕上。 下一个例子是基于这种经验。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

struct {
  gboolean timer;
  cairo_surface_t *image;
  cairo_surface_t *surface;
  gint img_width;
  gint img_height;
} glob;

static void init_vars()
{
  glob.image = cairo_image_surface_create_from_png("beckov.png");

  glob.img_width = cairo_image_surface_get_width(glob.image);
  glob.img_height = cairo_image_surface_get_height(glob.image);  

  glob.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 
      glob.img_width, glob.img_height);    
  glob.timer = TRUE;   
}

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_t *ic;

  static gint count = 0;

  ic = cairo_create(glob.surface);

  gint i, j;
  for (i = 0; i <= glob.img_height; i+=7) {
      for (j = 0 ; j < count; j++) {
          cairo_move_to(ic, 0, i+j);
          cairo_line_to(ic, glob.img_width, i+j);
      }
  }

  count++;
  if (count == 8) glob.timer = FALSE;

  cairo_set_source_surface(cr, glob.image, 10, 10);
  cairo_mask_surface(cr, glob.surface, 10, 10);
  cairo_stroke(ic);

  cairo_destroy(ic);  
}

static gboolean time_handler(GtkWidget *widget)
{
  if (!glob.timer) return FALSE;

  gtk_widget_queue_draw(widget);
  return TRUE;
}

int main(int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  init_vars();

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(G_OBJECT(window), "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 325, 250); 
  gtk_window_set_title(GTK_WINDOW(window), "Spectrum");

  g_timeout_add(400, (GSourceFunc) time_handler, (gpointer) window);

  gtk_widget_show_all(window);

  gtk_main();

  cairo_surface_destroy(glob.image);
  cairo_surface_destroy(glob.surface);  

  return 0;
}

我们将图像分为由 8 行组成的 n 个部分。 每个周期,图像的每个部分都会变大一个像素。 创建的图像将用作显示城堡图像的遮罩。

struct {
  gboolean timer;
  cairo_surface_t *image;
  cairo_surface_t *surface;
  gint img_width;
  gint img_height;
} glob;

全局结构存储在更多函数中使用的变量。

static void init_vars()
{
  glob.image = cairo_image_surface_create_from_png("beckov.png");

  glob.img_width = cairo_image_surface_get_width(glob.image);
  glob.img_height = cairo_image_surface_get_height(glob.image);  

  glob.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 
      glob.img_width, glob.img_height);    
  glob.timer = TRUE;   
}

init_vars()函数中,我们启动上述变量。

gint i, j;
for (i = 0; i <= glob.img_height; i+=7) {
    for (j = 0 ; j < count; j++) {
        cairo_move_to(ic, 0, i+j);
        cairo_line_to(ic, glob.img_width, i+j);
    }
}

我们逐步将线绘制到 n 个部分中的每个部分。

count++;
if (count == 8) glob.timer = FALSE;

8 个步骤后,动画结束。

cairo_set_source_surface(cr, glob.image, 10, 10);
cairo_mask_surface(cr, glob.surface, 10, 10);
cairo_stroke(ic);

使用遮罩操作,我们在窗口上绘制图像的各个部分。

本章介绍了 Cairo 的图像。

根窗口

原文: https://zetcode.com/gfx/cairo/root/

在 Cairo 图形教程的这一部分中,我们将使用根窗口。 根窗口是我们通常具有图标快捷方式的桌面窗口。

可以使用根窗口进行操作。 从程序员的角度来看,它只是一种特殊的窗口。

透明窗

我们的第一个示例将创建一个透明窗口。 我们将看到窗口对象下方的内容。

#include <cairo.h>
#include <gtk/gtk.h>

static void do_drawing(cairo_t *);

static void tran_setup(GtkWidget *win)
{        
  GdkScreen *screen;
  GdkVisual *visual;

  gtk_widget_set_app_paintable(win, TRUE);
  screen = gdk_screen_get_default();
  visual = gdk_screen_get_rgba_visual(screen);

  if (visual != NULL && gdk_screen_is_composited(screen)) {
      gtk_widget_set_visual(win, visual);
  }
}

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);  

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.4);
  cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
  cairo_paint(cr);
}

int main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *darea;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  tran_setup(window);

  darea = gtk_drawing_area_new();
  gtk_container_add(GTK_CONTAINER (window), darea);

  g_signal_connect(G_OBJECT(darea), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 300, 250); 
  gtk_window_set_title(GTK_WINDOW(window), "Transparent window");

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

为了创建透明窗口,我们获得了屏幕对象的视觉效果并将其设置为我们的窗口。 在on_draw()方法中,我们绘制屏幕的可视对象。 这产生了部分透明的幻觉。

gtk_widget_set_app_paintable(win, TRUE);

我们必须设置要绘制的应用。

screen = gdk_screen_get_default();

gdk_screen_get_default()方法返回屏幕对象。

visual = gdk_screen_get_rgba_visual(screen);

从屏幕窗口中,我们可以看到它。 视觉内容包含低级显示信息。

if (visual != NULL && gdk_screen_is_composited(screen)) {
    gtk_widget_set_visual(win, visual);
}

并非所有的显示器都支持此操作。 因此,我们检查屏幕是否支持合成并且返回的视觉效果不是NULL。 我们将屏幕的视觉效果设置为窗口的视觉效果。

static void do_drawing(cairo_t *cr)
{
  cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.4);
  cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
  cairo_paint(cr);
}

我们使用部分透明的源来绘制屏幕窗口。 CAIRO_OPERATOR_SOURCE在我们绘制源代码的地方创建了合成操作。 这是屏幕窗口。 为了获得完全透明,我们将 alpha 值设置为 0 或使用CAIRO_OPERATOR_CLEAR运算符。

Transparent window

图:透明窗口

截屏

根窗口对于截图也是必不可少的。

#include <cairo.h>
#include <gdk/gdk.h>

int main (int argc, char *argv[])
{
  gdk_init(&argc, &argv);

  GdkWindow *root_win = gdk_get_default_root_window();
  gint width = gdk_window_get_width(root_win);
  gint height = gdk_window_get_height(root_win);

  cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
      width, height);

  GdkPixbuf *pb = gdk_pixbuf_get_from_window(root_win, 0, 0, width, height);

  cairo_t *cr = cairo_create(surface);        
  gdk_cairo_set_source_pixbuf(cr, pb, 0, 0);  
  cairo_paint(cr);  

  cairo_surface_write_to_png(surface, "image.png");

  cairo_destroy(cr);
  cairo_surface_destroy(surface);

  return 0;
}

该示例捕获整个屏幕的快照。 在此示例中,我们不使用完整的 GTK 窗口系统。 我们使用 Cairo 和 GDK 库来完成这项工作。

gdk_init(&argc, &argv);

gdk_init()初始化 GDK 库并连接到窗口系统。

GdkWindow *root_win = gdk_get_default_root_window();

我们通过gdk_get_default_root_window()函数调用获得了根窗口。

gint width = gdk_window_get_width(root_win);
gint height = gdk_window_get_height(root_win);

我们确定根窗口的宽度和高度。

cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
    width, height);

空的图像表面被创建。 它具有根窗口的大小。

GdkPixbuf *pb = gdk_pixbuf_get_from_window(root_win, 0, 0, width, height);

我们使用gdk_pixbuf_get_from_window()函数调用从根窗口中获得一个pixbufpixbuf是描述内存中图像的对象。

cairo_t *cr = cairo_create(surface);        
gdk_cairo_set_source_pixbuf(cr, pb, 0, 0);  
cairo_paint(cr);

在上述代码行中,我们在之前创建的图像表面上创建了 Cairo 绘图上下文。 我们将 pixbuf 放在绘图上下文上并将其绘制在表面上。

cairo_surface_write_to_png(surface, "image.png");

使用write_to_png()方法将图像表面写入 PNG 图像。

cairo_destroy(cr);
cairo_surface_destroy(surface);

我们清理资源。

显示信息

在第三个示例中,我们将在桌面窗口上显示一条消息。

#include <cairo.h>
#include <gtk/gtk.h>
#include <pango/pango.h>

static void do_drawing(cairo_t *);

static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, 
    gpointer user_data)
{      
  do_drawing(cr);  

  return FALSE;
}

static void do_drawing(cairo_t *cr)
{
   cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
   cairo_paint(cr);
   cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
}

static void setup(GtkWidget *win)
{        
  gtk_widget_set_app_paintable(win, TRUE);
  gtk_window_set_type_hint(GTK_WINDOW(win), GDK_WINDOW_TYPE_HINT_DOCK);
  gtk_window_set_keep_below(GTK_WINDOW(win), TRUE);

  GdkScreen *screen = gdk_screen_get_default();
  GdkVisual *visual = gdk_screen_get_rgba_visual(screen);

  if (visual != NULL && gdk_screen_is_composited(screen)) {
      gtk_widget_set_visual(win, visual);
  }  
}

int main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *lbl;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  setup(window);

  lbl = gtk_label_new("ZetCode, tutorials for programmers");

  PangoFontDescription *fd = pango_font_description_from_string("Serif 20");
  gtk_widget_modify_font(lbl, fd);  
  gtk_container_add(GTK_CONTAINER(window), lbl);  

  GdkColor color;
  gdk_color_parse("white", &color);
  gtk_widget_modify_fg(lbl, GTK_STATE_NORMAL, &color);

  g_signal_connect(G_OBJECT(window), "draw", 
      G_CALLBACK(on_draw_event), NULL); 
  g_signal_connect(window, "destroy",
      G_CALLBACK(gtk_main_quit), NULL);

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 350, 250); 

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

该代码在根窗口上显示消息标签。

static void do_drawing(cairo_t *cr)
{
   cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
   cairo_paint(cr);
   cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
}

我们使用CAIRO_OPERATOR_CLEAR运算符清除窗口背景。 然后我们设置CAIRO_OPERATOR_OVER以绘制标签窗口小部件。

gtk_widget_set_app_paintable(win, TRUE);

我们将操纵应用窗口,因此我们使其可绘制。

gtk_window_set_type_hint(GTK_WINDOW(win), GDK_WINDOW_TYPE_HINT_DOCK);

实现此窗口提示会删除窗口边框和装饰。

gtk_window_set_keep_below(GTK_WINDOW(win), TRUE);

我们始终将应用始终放在根窗口的底部。

GdkScreen *screen = gdk_screen_get_default();
GdkVisual *visual = gdk_screen_get_rgba_visual(screen);

if (visual != NULL && gdk_screen_is_composited(screen)) {
    gtk_widget_set_visual(win, visual);
  } 

我们将屏幕的外观设置为应用的外观。

lbl = gtk_label_new("ZetCode, tutorials for programmers");

我们创建一个消息标签。

PangoFontDescription *fd = pango_font_description_from_string("Serif 20");
gtk_widget_modify_font(lbl, fd);  

在 Pango 模块的帮助下,我们为文本选择特定的字体。

gtk_container_add(GTK_CONTAINER(window), lbl);   

标签贴在窗户上。

GdkColor color;
gdk_color_parse("white", &color);
gtk_widget_modify_fg(lbl, GTK_STATE_NORMAL, &color);

我们将文本修改为白色。

Message on the root window

图:根窗口上的消息

在本章中,我们使用了 Cairo 的桌面窗口。

PyCairo 教程

原文: https://zetcode.com/gfx/pycairo/

这是 PyCairo 教程。 在本教程中,我们将学习 Python 和 Cairo 库中的 2D 图形编程。

目录

PyCairo

PyCairo 是用于 Cairo 库的 Python 模块。 它是对 Cairo C 库的一组 Python 绑定。 它与 C API 紧密匹配,但需要更多 Python 方式的情况除外。

相关教程

Cairo 图形教程涵盖了使用 C 编程语言编写的 Cairo 库。

PyCairo 简介

原文: https://zetcode.com/gfx/pycairo/introduction/

这是 PyCairo 教程。 本教程将使用 Python 语言教您 Cairo 2D 库的基础知识和一些高级主题。 在大多数示例中,我们将使用 Python GTK 后端生成我们的输出。 可以在此处下载本教程中使用的图像。

计算机图像

有两种不同的计算机图形:矢量图形和栅格图形。 光栅图形将图像表示为像素集合。 矢量图形是使用几何图元(例如点,线,曲线或多边形)来表示图像。 这些基元是使用数学方程式创建的。

两种类型的计算机图形都有优点和缺点。 矢量图形优于栅格的优点是:

  • 较小的大小
  • 无限放大的能力
  • 移动,缩放,填充和旋转不会降低图像质量

Cairo

Cairo 是用于创建 2D 矢量图形的库。 它是用 C 编程语言编写的。 存在其他计算机语言的绑定,包括 Python,Perl,C++ ,C# 或 Java。 Cairo 是一个多平台库; 它适用于 Linux,BSD,Windows 和 OSX。

Cairo 支持各种后端。 后端是用于显示创建的图形的输出设备。

  • X Window 系统
  • Win32 GDI
  • Mac OS X
  • PNG
  • PDF 格式
  • PostScript
  • SVG

这意味着我们可以使用该库在 Windows,Linux,Windows,OSX 上进行绘制,并且可以使用该库创建 PNG 图像,PDF 文件,PostScript 文件和 SVG 文件。

我们可以将 Cairo 库与 Windows OS 上的 GDI+ 库和 Mac OS 上的 Quartz 2D 比较。 Cairo 是一个开源软件库。 从 2.8 版开始,Cairo 是 GTK 系统的一部分。

定义

在这里,我们提供了一些有用的定义。 要在 PyCairo 中进行绘制,我们首先必须创建一个绘制上下文。 绘图上下文包含所有描述绘图方式的图形状态参数。 这包括诸如线宽,颜色,要绘制的表面以及许多其他信息。 它允许实际的绘图函数采用较少的参数来简化界面。

路径是用于创建基本形状(如直线,圆弧和曲线)的点的集合。 路径有两种:开放路径和封闭路径。 在封闭的路径中,起点和终点相遇。 在开放路径中,起点和终点不相交。 在 PyCairo 中,我们从空路径开始。 首先,我们定义一条路径,然后通过抚摸和/或填充它们使它们可见。 在每个stroke()fill()方法调用之后,该路径将被清空。 我们必须定义一条新路径。 如果我们想保留现有路径以用于以后的绘图,则可以使用stroke_preserve()fill_preserve()方法。 路径由子路径组成。

源是我们在绘图中使用的油漆。 我们可以将源与用来绘制轮廓和填充形状的钢笔或墨水进行比较。 有四种基本来源:颜色,渐变,图案和图像。

曲面是我们要绘制的目标。 我们可以使用 PDF 或 PostScript 曲面渲染文档,并通过 Xlib 和 Win32 曲面直接绘制到平台上。

在将源应用于表面之前,先对其进行过滤。 掩码用作过滤器。 它确定在哪里应用源,不在哪里应用。 遮罩的不透明部分允许复制源。 透明零件不允许将源复制到表面。

模式代表在表面上绘制时的光源。 在 PyCairo 中,图案是您可以读取的东西,用作绘制操作的源或蒙版。 图案可以是实心的,基于表面的或渐变的。

数据来源

为了创建本教程,我们使用了以下材料。 Apple Cocoa 制图指南PyCairo 参考Cairo 文档

PyCairo 后端

原文: https://zetcode.com/gfx/pycairo/backends/

PyCairo 支持各种后端。 后端是可以显示 PyCairo 产生的图形的地方。 我们使用 PyCairo 创建 PNG 图像,PDF 文件,SVG 文件,然后在 GTK 窗口上绘制。

PNG 图像

在第一个示例中,我们创建一个 PNG 图像。

pngimage.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program uses PyCairo to 
produce a PNG image.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

import cairo

def main():

    ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, 390, 60)
    cr = cairo.Context(ims)

    cr.set_source_rgb(0, 0, 0)
    cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
        cairo.FONT_WEIGHT_NORMAL)
    cr.set_font_size(40)

    cr.move_to(10, 50)
    cr.show_text("Disziplin ist Macht.")

    ims.write_to_png("image.png")

if __name__ == "__main__":    
    main()

此示例是一个小型控制台应用,可创建 PNG 图像。

import cairo

我们导入 PyCairo 模块。

ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, 390, 60)
cr = cairo.Context(ims)

我们创建一个曲面,并从该曲面创建一个 Cairo 上下文。 表面是 390x60 像素的图像。

cr.set_source_rgb(0, 0, 0)

我们用黑色墨水绘制文本。 墨水通过set_source_rgb()方法指定。

cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
    cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(40)

我们通过select_font_face()方法选择一种字体类型,并通过set_font_size()方法设置字体大小。

cr.move_to(10, 50)
cr.show_text("Disziplin ist Macht.")

我们将图像内的位置移至x = 10.0y = 50.0并绘制文本。

ims.write_to_png("image.png")

write_to_png()方法将表面的内容写入 PNG 图像。

PNG image in Eye of Gnome

图:Gnome之眼中的 PNG 图像

PDF 文件

在第二个示例中,我们创建一个简单的 PDF 文件。

pdffile.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program uses PyCairo to 
produce a PDF image.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

import cairo

def main():

    ps = cairo.PDFSurface("pdffile.pdf", 504, 648)
    cr = cairo.Context(ps)

    cr.set_source_rgb(0, 0, 0)
    cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
        cairo.FONT_WEIGHT_NORMAL)
    cr.set_font_size(40)

    cr.move_to(10, 50)
    cr.show_text("Disziplin ist Macht.")
    cr.show_page()

if __name__ == "__main__":    
    main()

我们必须在 PDF 查看器中打开 PDF 文件。 Linux 用户可以使用 KPDF 或 Evince 查看器。

ps = cairo.PDFSurface("pdffile.pdf", 504, 648)

要渲染 PDF 文件,我们必须使用cairo.PDFSurface对象创建 PDF 曲面。 PDF 文件的大小以磅为单位指定,这是排版的标准。

cr.show_page()

show_page()完成 PDF 文件的渲染。

PDF file in Evince

图:Evince 中的 PDF 文件

SVG 文件

下一个示例创建一个简单的 SVG(可缩放矢量图形)文件。 SVG 文件是基于 XML 的文件格式。

svgfile.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program uses PyCairo to 
produce a SVG file.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

import cairo

def main():

    ps = cairo.SVGSurface("svgfile.svg", 390, 60)
    cr = cairo.Context(ps)

    cr.set_source_rgb(0, 0, 0)
    cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
        cairo.FONT_WEIGHT_NORMAL)
    cr.set_font_size(40)

    cr.move_to(10, 50)
    cr.show_text("Disziplin ist Macht.")
    cr.show_page()

if __name__ == "__main__":    
    main()

我们可以使用网络浏览器(例如 Google Chrome)或矢量绘图程序(例如 Inkscape)打开 SVG 文件。

ps = cairo.SVGSurface("svgfile.svg", 390, 60)

要在 PyCairo 中创建 SVG 文件,我们必须使用cairo.SVGSurface对象创建 SVG 表面。

cr.show_page()

show_page()方法调用完成了 SVG 文件的呈现。

SVG file in Chrome

SVG file in Chrome

GTK 窗口

在最后一个示例中,我们在 GTK 窗口上绘制。 该后端将在本教程的其余部分中使用。

gtkwindow.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program uses PyCairo to 
draw on a window in GTK.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

from gi.repository import Gtk
import cairo

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("GTK window")
        self.resize(420, 120)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_draw(self, wid, cr):

        cr.set_source_rgb(0, 0, 0)
        cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
            cairo.FONT_WEIGHT_NORMAL)
        cr.set_font_size(40)

        cr.move_to(10, 50)
        cr.show_text("Disziplin ist Macht.")

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

该示例弹出一个居中的 GTK 窗口,在该窗口上绘制"Disziplin ist Macht"文本。

from gi.repository import Gtk
import cairo

我们导入必要的 PyCairo 和 GTK 模块。

darea = Gtk.DrawingArea()

我们将使用Gtk.DrawingArea小部件。

darea.connect("draw", self.on_draw)

重新绘制窗口时,会发出draw信号。 我们将该信号连接到on_draw()回调。

def on_draw(self, wid, cr):
...

绘图是在on_draw()方法内部完成的。 第三个参数是 Cairo 上下文。 它是自动提供给我们的; Cairo 库内置在 GTK 系统中。

GTK window

图:GTK 窗口

在本章中,我们介绍了受支持的 PyCairo 后端。

PyCairo 中的基本绘图

原文: https://zetcode.com/gfx/pycairo/basicdrawing/

在 PyCairo 教程的这一部分中,我们绘制了一些基本图元。 我们使用填充和描边操作,笔划线,线帽和线连接。

直线

线是非常基本的矢量对象。 要画一条线,我们使用两个方法调用。 通过move_to()调用指定起点。 线的终点通过line_to()调用指定。

lines.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

In this program, we connect all mouse
clicks with a line.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

from gi.repository import Gtk, Gdk
import cairo

class MouseButtons:

    LEFT_BUTTON = 1
    RIGHT_BUTTON = 3

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        self.darea = Gtk.DrawingArea()
        self.darea.connect("draw", self.on_draw)
        self.darea.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)        
        self.add(self.darea)

        self.coords = []

        self.darea.connect("button-press-event", self.on_button_press)

        self.set_title("Lines")
        self.resize(300, 200)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_draw(self, wid, cr):

        cr.set_source_rgb(0, 0, 0)
        cr.set_line_width(0.5)

        for i in self.coords:
            for j in self.coords:

                cr.move_to(i[0], i[1])
                cr.line_to(j[0], j[1]) 
                cr.stroke()

        del self.coords[:]            

    def on_button_press(self, w, e):

        if e.type == Gdk.EventType.BUTTON_PRESS \
            and e.button == MouseButtons.LEFT_BUTTON:

            self.coords.append([e.x, e.y])

        if e.type == Gdk.EventType.BUTTON_PRESS \
            and e.button == MouseButtons.RIGHT_BUTTON:

            self.darea.queue_draw()           

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在我们的示例中,我们用鼠标左键随机单击窗口。 每次点击都存储在一个列表中。 当我们右键单击窗口时,所有点都与列表中的每个其他点相连。 右键单击将清除窗口。

class MouseButtons:

    LEFT_BUTTON = 1
    RIGHT_BUTTON = 3

GTK 文档仅声明鼠标左键的编号为 1,鼠标右键的编号为 3。我们创建了一个自定义类,其中包含一些鼠标键的标识符。

self.darea.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)   

默认情况下,某些事件未启用。 鼠标按下事件就在其中。 因此,我们需要使用set_event()方法启用鼠标按下事件。

self.darea.connect("button-press-event", self.on_button_press)

在此代码示例中,我们对鼠标按下事件做出反应。

cr.set_source_rgb(0, 0, 0)
cr.set_line_width(0.5)

线条以黑色墨水绘制,宽度为 0.5 点。

for i in self.coords:
    for j in self.coords:

        cr.move_to(i[0], i[1])
        cr.line_to(j[0], j[1]) 
        cr.stroke()

我们将列表中的每个点连接到其他每个点。 stroke()调用画线。

del self.coords[:]    

最后,将删除所有坐标。 现在,我们可以创建另一个对象。

def on_button_press(self, w, e):

    if e.type == Gdk.EventType.BUTTON_PRESS \
        and e.button == MouseButtons.LEFT_BUTTON:

        self.coords.append([e.x, e.y])
...

如果按下鼠标左键,我们会将其 x 和 y 坐标添加到self.coords列表中。

if e.type == Gdk.EventType.BUTTON_PRESS \
    and e.button == MouseButtons.RIGHT_BUTTON:

    self.darea.queue_draw()

在按下鼠标右键的情况下,我们调用queue_draw()方法来重绘绘图区域。 所有的点都用线连接。

Lines

图:直线

填充和描边

描边操作绘制形状的轮廓,填充操作填充形状的内部。

fillstroke.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This code example draws a circle
using the PyCairo library.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

from gi.repository import Gtk
import cairo
import math

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Fill & stroke")
        self.resize(230, 150)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_draw(self, wid, cr):

        cr.set_line_width(9)
        cr.set_source_rgb(0.7, 0.2, 0.0)

        w, h = self.get_size()      

        cr.translate(w/2, h/2)
        cr.arc(0, 0, 50, 0, 2*math.pi)
        cr.stroke_preserve()

        cr.set_source_rgb(0.3, 0.4, 0.6)
        cr.fill()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在示例中,我们绘制了一个圆圈,并用纯色填充。

import math

用于绘制圆的pi常数需要此模块。

cr.set_line_width(9)
cr.set_source_rgb(0.7, 0.2, 0.0)

我们使用set_line_width()方法设置线宽。 我们使用set_source_rgb()方法将光源设置为深红色。

w, h = self.get_size()     

在这里,我们获得了窗口的宽度和高度。 我们需要这些值使圆在窗口上居中。

cr.translate(w/2, h/2)
cr.arc(0, 0, 50, 0, 2*math.pi)
cr.stroke_preserve()

使用translate()方法,我们将图形原点移动到窗口的中心。 我们希望我们的圈子居中。 arc()方法向 Cairo 图形上下文添加了新的圆形路径。 最后,stroke_preserve()方法绘制圆的轮廓。 与stroke()方法不同,它还保留了形状以供以后绘制。

cr.set_source_rgb(0.3, 0.4, 0.6)
cr.fill()

我们使用fill()方法更改绘制颜色,并用新颜色填充圆。

Fill & stroke

图:填充和描边

笔划线

每条线可以用不同的笔划线绘制。 笔笔划线定义线条的样式。 笔划线由set_dash()方法指定。 该模式由笔划线列表设置,笔划线列表是浮点值的列表。 他们设置笔划线图案的开和关部分。 stroke()方法使用笔划线创建一条线。 如果笔划线为 0,则禁用笔划线。 如果笔划线的数量为 1,则假定使用对称模式,其中交替的开和关部分由笔划线中的单个值指定。

def on_draw(self, wid, cr):

    cr.set_source_rgba(0, 0, 0, 1)
    cr.set_line_width(2)

    cr.set_dash([4.0, 21.0, 2.0])

    cr.move_to(40, 30)  
    cr.line_to(250, 30)
    cr.stroke()

    cr.set_dash([14.0, 6.0])

    cr.move_to(40, 50)
    cr.line_to(250, 50)
    cr.stroke()

    cr.set_dash([1.0])

    cr.move_to(40, 70)
    cr.line_to(250, 70)
    cr.stroke()                

我们用三个不同的笔划线画了三条线。

cr.set_dash([4.0, 21.0, 2.0])

我们有三个数字的模式。 我们绘制了 4 个点,未绘制 21 个,绘制了 2 个点,然后绘制了 4 个点,绘制了 21 个点。 和 2 未绘制。 该模式轮流直到行尾。

cr.set_dash([14.0, 6.0])

在这种模式下,我们总是绘制 14 点,未绘制 6 点。

cr.set_dash([1.0])

在这里,我们创建了一个对称图案的笔划线,该图案交替出现单个接通和断开点。

Pen dashes

图:笔划线

线帽

线帽是线的端点。

  • Cairo.LINE_CAP_BUTT
  • Cairo.LINE_CAP_ROUND
  • Cairo.LINE_CAP_SQUARE

Cairo 有三种不同的线帽样式。

Line caps

图:正方形,圆和端帽

带有cairo.LINE_CAP_SQUARE帽的线的大小与带有cairo.LINE_CAP_BUTT帽的线的大小不同。 如果一行的宽度是 x 单位,则带cairo.LINE_CAP_SQUARE上限的行的大小将恰好是 x 单位; 开头x / 2个单位,结尾x / 2个单位。

def on_draw(self, wid, cr):

    cr.set_source_rgba(0, 0, 0, 1)
    cr.set_line_width(12)

    cr.set_line_cap(cairo.LINE_CAP_BUTT)
    cr.move_to(30, 50)
    cr.line_to(150, 50)
    cr.stroke()

    cr.set_line_cap(cairo.LINE_CAP_ROUND)
    cr.move_to(30, 90)
    cr.line_to(150, 90)
    cr.stroke()

    cr.set_line_cap(cairo.LINE_CAP_SQUARE)
    cr.move_to(30, 130)
    cr.line_to(150, 130)
    cr.stroke()

    cr.set_line_width(1.5)

    cr.move_to(30, 35)
    cr.line_to(30, 145)
    cr.stroke()

    cr.move_to(150, 35)
    cr.line_to(150, 145)
    cr.stroke()

    cr.move_to(155, 35)
    cr.line_to(155, 145)
    cr.stroke()

该示例绘制具有三个不同线帽的三条线。 通过绘制三条额外的细垂直线,它还将以图形方式显示线的大小差异。

cr.set_line_width(12)

我们的生产线将是 12 个单位宽。 默认线宽为 2。

cr.set_line_cap(cairo.LINE_CAP_ROUND)
cr.move_to(30, 90)
cr.line_to(150, 90)
cr.stroke()

在这里,我们用cairo.LINE_CAP_ROUND帽画一条水平线。

cr.set_line_width(1.5)

cr.move_to(30, 35)
cr.line_to(30, 145)
cr.stroke()

这是用来说明大小差异的三条垂直线之一。

Line caps

图:直线的端帽

线连接

可以使用三种不同的连接样式来连接线。

  • Cairo.LINE_JOIN_MITER
  • Cairo.LINE_JOIN_BEVEL
  • Cairo.LINE_JOIN_ROUND

Bevel, Round, Miter line joins

图:斜角,圆角,斜接线连接

def on_draw(self, wid, cr):

    cr.set_line_width(14)

    cr.rectangle(30, 30, 100, 100)        
    cr.set_line_join(cairo.LINE_JOIN_MITER)
    cr.stroke()

    cr.rectangle(160, 30, 100, 100)
    cr.set_line_join(cairo.LINE_JOIN_BEVEL)
    cr.stroke()

    cr.rectangle(100, 160, 100, 100)
    cr.set_line_join(cairo.LINE_JOIN_ROUND)
    cr.stroke()

在此示例中,我们绘制了三个具有各种线连接的粗矩形。

cr.set_line_width(14)

线宽为 14 个单位。

cr.rectangle(30, 30, 100, 100)        
cr.set_line_join(cairo.LINE_JOIN_MITER)
cr.stroke()

在这里,我们绘制一个具有cairo.LINE_JOIN_MITER连接样式的矩形。

Line joins

图:直线连接

贝塞尔曲线

贝塞尔曲线是由数学公式定义的曲线。 绘制曲线的数学方法由 PierreBézier 在 1960 年代后期创建,用于雷诺的汽车制造。

curve_to(x1, y1, x2, y2, x3, y3)

curve_to()方法将三次贝塞尔曲线样条添加到路径。 参数为第一控制点的 x 和 y 坐标,第二控制点的 x 和 y 坐标以及曲线末端的 x 和 y 坐标。

def on_draw(self, wid, cr):

    cr.move_to(20, 40)
    cr.curve_to(320, 200, 330, 110, 450, 40)        
    cr.stroke()

在此示例中,使用curve_to()方法绘制了贝塞尔曲线。

Bézier curve

图:贝塞尔曲线

在 PyCairo 教程的这一章中,我们做了一些基本的绘制。

PyCairo 形状和填充

原文: https://zetcode.com/gfx/pycairo/shapesfills/

在 PyCairo 教程的这一部分中,我们创建一些基本的和更高级的形状。 我们用纯色,图案和渐变填充这些形状。 渐变将在单独的章节中介绍。

基本形状

PyCairo 有一些创建简单形状的基本方法。

def on_draw(self, wid, cr):

    cr.set_source_rgb(0.6, 0.6, 0.6)

    cr.rectangle(20, 20, 120, 80)
    cr.rectangle(180, 20, 80, 80)
    cr.fill()

    cr.arc(330, 60, 40, 0, 2*math.pi)
    cr.fill()

    cr.arc(90, 160, 40, math.pi/4, math.pi)
    cr.fill()

    cr.translate(220, 180)
    cr.scale(1, 0.7)
    cr.arc(0, 0, 50, 0, 2*math.pi)
    cr.fill()

在此示例中,我们创建一个矩形,一个正方形,一个圆形,一个弧形和一个椭圆形。

cr.rectangle(20, 20, 120, 80)
cr.rectangle(180, 20, 80, 80)

rectangle()方法用于创建正方形和矩形。 正方形只是矩形的一种特定类型。 参数是窗口左上角的 x 和 y 坐标以及矩形的宽度和高度。

cr.arc(330, 60, 40, 0, 2*math.pi)

arc()方法创建一个圆。 参数是弧度中心的 x 和 y 坐标,半径以及弧度的开始和结束角度。

cr.arc(90, 160, 40, math.pi/4, math.pi)

在这里,我们画一条弧,是圆的一部分。

cr.scale(1, 0.7)
cr.arc(0, 0, 50, 0, 2*math.pi)

我们使用scale()arc()方法创建一个椭圆。

Basic Shapes

图:基本形状

可以使用基本图元的组合来创建其他形状。

complex_shapes.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This code example draws another
three shapes in PyCairo.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

from gi.repository import Gtk
import cairo

class cv(object):

    points = ( 
        ( 0, 85 ), 
        ( 75, 75 ), 
        ( 100, 10 ), 
        ( 125, 75 ), 
        ( 200, 85 ),
        ( 150, 125 ), 
        ( 160, 190 ),
        ( 100, 150 ), 
        ( 40, 190 ),
        ( 50, 125 ),
        ( 0, 85 )
    )

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Complex shapes")
        self.resize(460, 240)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_draw(self, wid, cr):

        cr.set_source_rgb(0.6, 0.6, 0.6)
        cr.set_line_width(1)

        for i in range(10):
            cr.line_to(cv.points[i][0], cv.points[i][1])

        cr.fill()

        cr.move_to(240, 40)
        cr.line_to(240, 160)
        cr.line_to(350, 160)
        cr.fill()

        cr.move_to(380, 40)
        cr.line_to(380, 160)
        cr.line_to(450, 160)
        cr.curve_to(440, 155, 380, 145, 380, 40)
        cr.fill()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在此示例中,我们创建一个星形对象,一个三角形和一个修改后的三角形。 这些对象是使用直线和一条曲线创建的。

for i in range(10):
    cr.line_to(cv.points[i][0], cv.points[i][1])

cr.fill()

通过连接点元组中的所有点来绘制星形。 fill()方法用当前颜色填充星形对象。

cr.move_to(240, 40)
cr.line_to(240, 160)
cr.line_to(350, 160)
cr.fill()

这些线创建一个三角形。 最后两点将自动合并。

cr.move_to(380, 40)
cr.line_to(380, 160)
cr.line_to(450, 160)
cr.curve_to(440, 155, 380, 145, 380, 40)
cr.fill()

修改后的三角形是两条直线和一条曲线的简单组合。

Comlex shapes

图:复杂形状

填充

填充填充形状的内部。 填充可以是纯色,图案或渐变。

纯色

颜色是代表红色,绿色和蓝色(RGB)强度值的组合的对象。 PyCairo 的有效 RGB 值在 0 到 1 的范围内。

def on_draw(self, wid, cr):

    cr.set_source_rgb(0.2, 0.23, 0.9)
    cr.rectangle(10, 15, 90, 60)
    cr.fill()

    cr.set_source_rgb(0.9, 0.1, 0.1)
    cr.rectangle(130, 15, 90, 60)
    cr.fill()

    cr.set_source_rgb(0.4, 0.9, 0.4)
    cr.rectangle(250, 15, 90, 60)
    cr.fill()           

在示例中,我们绘制了四个彩色矩形。

cr.set_source_rgb(0.2, 0.23, 0.9)
cr.rectangle(10, 15, 90, 60)
cr.fill()

set_source_rgb()方法将源设置为不透明的颜色。 参数是红色,绿色,蓝色强度值。 通过调用fill()方法,该源用于填充矩形的内部。

Solid colors

图:纯色

图案

图案是可以用于填充形状的复杂图形对象。

patterns.py

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program shows how to work
with patterns in PyCairo.

Author: Jan Bodnar
Website: zetcode.com 
Last edited: April 2016
'''

from gi.repository import Gtk
import cairo

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()
        self.create_surpat()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Patterns")
        self.resize(300, 290)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def create_surpat(self):

        sr1 = cairo.ImageSurface.create_from_png("blueweb.png")
        sr2 = cairo.ImageSurface.create_from_png("maple.png")
        sr3 = cairo.ImageSurface.create_from_png("crack.png")
        sr4 = cairo.ImageSurface.create_from_png("chocolate.png")

        self.pt1 = cairo.SurfacePattern(sr1)
        self.pt1.set_extend(cairo.EXTEND_REPEAT)
        self.pt2 = cairo.SurfacePattern(sr2)
        self.pt2.set_extend(cairo.EXTEND_REPEAT)
        self.pt3 = cairo.SurfacePattern(sr3)
        self.pt3.set_extend(cairo.EXTEND_REPEAT)
        self.pt4 = cairo.SurfacePattern(sr4)
        self.pt4.set_extend(cairo.EXTEND_REPEAT)        

    def on_draw(self, wid, cr):

        cr.set_source(self.pt1)
        cr.rectangle(20, 20, 100, 100)
        cr.fill()

        cr.set_source(self.pt2) 
        cr.rectangle(150, 20, 100, 100)
        cr.fill()

        cr.set_source(self.pt3)
        cr.rectangle(20, 140, 100, 100)
        cr.fill()

        cr.set_source(self.pt4)
        cr.rectangle(150, 140, 100, 100)
        cr.fill()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在此示例中,我们绘制了四个矩形。 这次我们用一些模式填充它们。 我们使用来自 Gimp 图像处理器的四个图案图像。 我们必须保留这些图案的原始大小,因为我们将它们平铺。

我们在draw()方法之外创建图像表面。 每次需要重新绘制窗口时,从硬盘读取数据都不会很有效。

sr1 = cairo.ImageSurface.create_from_png("blueweb.png")

从 PNG 图像创建图像表面。

self.pt1 = cairo.SurfacePattern(sr1)
self.pt1.set_extend(cairo.EXTEND_REPEAT)

从表面创建图案。 我们将模式设置为cairo.EXTEND_REPEAT,这将导致图案通过重复平铺。

cr.set_source(self.pt1)
cr.rectangle(20, 20, 100, 100)
cr.fill()

在这里,我们绘制第一个矩形。 set_source()方法告诉 Cairo 上下文使用图案作为绘图源。 图像图案可能不完全适合形状。 rectangle()创建一个矩形路径。 最后,fill()方法用源填充路径。

本章介绍了 PyCairo 的形状和填充。

PyCairo 渐变

原文: https://zetcode.com/gfx/pycairo/gradients/

在 PyCairo 教程的这一部分中,我们将介绍渐变。 我们将提到线性和径向渐变。

在计算机图形学中,渐变是从浅到深或从一种颜色到另一种颜色的阴影的平滑混合。 在 2D 绘图程序和绘画程序中,渐变用于创建彩色背景和特殊效果以及模拟灯光和阴影。 (answers.com)

线性渐变

线性渐变是沿着一条线的颜色混合或颜色阴影混合。 它们由 PyCairo 中的cairo.LinearGradient类表示。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program works with linear
gradients in PyCairo.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk
import cairo

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Linear gradients")
        self.resize(340, 390)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_draw(self, wid, cr):

        self.draw_gradient1(cr)
        self.draw_gradient2(cr)
        self.draw_gradient3(cr)

    def draw_gradient1(self, cr):

        lg1 = cairo.LinearGradient(0.0, 0.0, 350.0, 350.0)

        count = 1

        i = 0.1    
        while i < 1.0: 
            if count % 2:
                lg1.add_color_stop_rgba(i, 0, 0, 0, 1)
            else:
                lg1.add_color_stop_rgba(i, 1, 0, 0, 1)
            i = i + 0.1
            count = count + 1      

        cr.rectangle(20, 20, 300, 100)
        cr.set_source(lg1)
        cr.fill()

    def draw_gradient2(self, cr):        

        lg2 = cairo.LinearGradient(0.0, 0.0, 350.0, 0)

        count = 1

        i = 0.05    
        while i < 0.95: 
            if count % 2:
                lg2.add_color_stop_rgba(i, 0, 0, 0, 1)
            else:
                lg2.add_color_stop_rgba(i, 0, 0, 1, 1)
            i = i + 0.025
            count = count + 1        

        cr.rectangle(20, 140, 300, 100)
        cr.set_source(lg2)
        cr.fill()

    def draw_gradient3(self, cr):        

        lg3 = cairo.LinearGradient(20.0, 260.0,  20.0, 360.0)
        lg3.add_color_stop_rgba(0.1, 0, 0, 0, 1) 
        lg3.add_color_stop_rgba(0.5, 1, 1, 0, 1) 
        lg3.add_color_stop_rgba(0.9, 0, 0, 0, 1) 

        cr.rectangle(20, 260, 300, 100)
        cr.set_source(lg3)
        cr.fill()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

该示例绘制了三个填充有线性渐变的矩形。

lg3 = cairo.LinearGradient(20.0, 260.0,  20.0, 360.0)

在这里,我们创建一个线性渐变。 参数指定直线,沿着该直线绘制渐变。 这是一条水平线。

lg3.add_color_stop_rgba(0.1, 0, 0, 0, 1) 
lg3.add_color_stop_rgba(0.5, 1, 1, 0, 1) 
lg3.add_color_stop_rgba(0.9, 0, 0, 0, 1) 

我们定义色标以产生渐变图案。 在这种情况下,渐变是黑色和黄色的混合。 通过添加两个黑色和一个黄色色标,我们创建了一个水平渐变图案。 这些停止实际上是什么意思? 在我们的情况下,我们从黑色开始,该黑色将停止在大小的 1/10 处。 然后,我们开始逐渐涂成黄色,最终达到形状的中心。 黄色停在大小的 9/10,我们再次开始用黑色绘画,直到结束。

Linear gradients

图:线性渐变

径向渐变

径向渐变是两个圆之间颜色或阴影的混合。 cairo.RadialGradient类用于在 PyCairo 中创建径向渐变。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial

This program works with radial
gradients in PyCairo.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk
import cairo
import math

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Radial gradients")
        self.resize(300, 200)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_draw(self, wid, cr):

        self.draw_gradient1(cr)
        self.draw_gradient2(cr)

    def draw_gradient1(self, cr):

        cr.set_source_rgba(0, 0, 0, 1)
        cr.set_line_width(12)

        cr.translate(60, 60)

        r1 = cairo.RadialGradient(30, 30, 10, 30, 30, 90)
        r1.add_color_stop_rgba(0, 1, 1, 1, 1)
        r1.add_color_stop_rgba(1, 0.6, 0.6, 0.6, 1)
        cr.set_source(r1)
        cr.arc(0, 0, 40, 0, math.pi * 2)
        cr.fill()

        cr.translate(120, 0)

    def draw_gradient2(self, cr):        

        r2 = cairo.RadialGradient(0, 0, 10, 0, 0, 40)
        r2.add_color_stop_rgb(0, 1, 1, 0)
        r2.add_color_stop_rgb(0.8, 0, 0, 0)
        cr.set_source(r2)
        cr.arc(0, 0, 40, 0, math.pi * 2)
        cr.fill()   

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在示例中,我们绘制了两个径向渐变。

r1 = cairo.RadialGradient(30, 30, 10, 30, 30, 90)
r1.add_color_stop_rgba(0, 1, 1, 1, 1)
r1.add_color_stop_rgba(1, 0.6, 0.6, 0.6, 1)
cr.set_source(r1)
cr.arc(0, 0, 40, 0, math.pi * 2)
cr.fill()

我们画一个圆,并用径向渐变填充其内部。 径向梯度由两个圆定义。 add_color_stop_rgba()方法定义颜色。 我们可以试验圆的位置或半径的长度。 在第一个渐变示例中,我们创建了一个类似于 3D 形状的对象。

r2 = cairo.RadialGradient(0, 0, 10, 0, 0, 40)
r2.add_color_stop_rgb(0, 1, 1, 0)
r2.add_color_stop_rgb(0.8, 0, 0, 0)
cr.set_source(r2)
cr.arc(0, 0, 40, 0, math.pi * 2)
cr.fill()  

在此示例中,定义径向渐变的圆和自定义绘制的圆具有共同的中心点。

Radial gradients

图:径向渐变

在本章中,我们介绍了 PyCairo 渐变。

PyCairo 剪裁&遮罩

原文: https://zetcode.com/gfx/pycairo/clipmask/

在 PyCairo 教程的这一部分中,我们将讨论剪切和遮罩操作。

剪裁

剪裁是将绘图限制为特定区域。 这样做是出于效率方面的考虑,并会产生有趣的效果。 PyCairo 具有clip()方法来设置裁剪。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program shows how to perform
clipping in PyCairo.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk, GLib
import cairo
import math
import random

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()
        self.load_image()
        self.init_vars()

    def init_ui(self):    

        self.darea = Gtk.DrawingArea()
        self.darea.connect("draw", self.on_draw)
        self.add(self.darea)

        GLib.timeout_add(100, self.on_timer)

        self.set_title("Clipping")
        self.resize(300, 200)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def load_image(self):

        self.image = cairo.ImageSurface.create_from_png("beckov.png")

    def init_vars(self):    

        self.pos_x = 128
        self.pos_y = 128
        self.radius = 40

        self.delta = [3, 3]        

    def on_timer(self):

        self.pos_x += self.delta[0]
        self.pos_y += self.delta[1]        

        self.darea.queue_draw()
        return True           

    def on_draw(self, wid, cr):

        w, h = self.get_size()

        if (self.pos_x < 0 + self.radius):
            self.delta[0] = random.randint(5, 9)
        elif (self.pos_x > w - self.radius):
            self.delta[0] = -random.randint(5, 9)

        if (self.pos_y < 0 + self.radius): 
            self.delta[1] = random.randint(5, 9)
        elif (self.pos_y > h - self.radius):
            self.delta[1] = -random.randint(5, 9)        

        cr.set_source_surface(self.image, 1, 1)
        cr.arc(self.pos_x, self.pos_y, self.radius, 0, 2*math.pi)
        cr.clip()
        cr.paint()        

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在此示例中,我们将裁剪图像。 圆圈在窗口区域上移动并显示基础图像的一部分。 这就像我们从孔中看一样。

def load_image(self):

    self.image = cairo.ImageSurface.create_from_png("beckov.png")

这是基础图像。 每个计时器周期,我们都会看到此图像的一部分。

if (self.pos_x < 0 + self.radius):
    self.delta[0] = random.randint(5, 9)
elif (self.pos_x > w - self.radius):
    self.delta[0]= -random.randint(5, 9) 

如果圆碰到窗口的左侧或右侧,则圆的移动方向会随机变化。 顶部和底部也一样。

cr.arc(self.pos_x, self.pos_y, self.radius, 0, 2*math.pi)

此行为 Cairo 上下文添加了一条循环路径。

cr.clip()

clip()设置剪切区域。 裁剪区域是当前使用的路径。 当前路径是通过arc()方法调用创建的。

cr.paint()

paint()在当前剪裁区域内的任何地方绘制当前源。

Clipping

图:剪裁

遮罩

在将源应用于表面之前,先对其进行过滤。 遮罩用作过滤器。 遮罩确定在哪里应用源,在哪里不应用。 遮罩的不透明部分允许复制源。 透明零件不允许将源复制到表面。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program demonstrates masking.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk
import cairo

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()
        self.load_image()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Masking")
        self.resize(310, 100)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def load_image(self):    

        self.ims = cairo.ImageSurface.create_from_png("omen.png")

    def on_draw(self, wid, cr):

        cr.mask_surface(self.ims, 0, 0);
        cr.fill()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

在该示例中,遮罩确定在哪里绘画和在哪里不绘画。

cr.mask_surface(self.ims, 0, 0);
cr.fill()

我们使用图像作为遮罩,从而将其显示在窗口上。

Masking

图:遮罩

蒙蔽效果

在此代码示例中,我们将忽略图像。 这类似于我们使用卷帘所做的。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program creates a blind down
effect using masking operation.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk, GLib
import cairo
import math

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()
        self.load_image()
        self.init_vars()

    def init_ui(self):    

        self.darea = Gtk.DrawingArea()
        self.darea.connect("draw", self.on_draw)
        self.add(self.darea)

        GLib.timeout_add(35, self.on_timer)

        self.set_title("Blind down")
        self.resize(325, 250)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def load_image(self):

        self.image = cairo.ImageSurface.create_from_png("beckov.png")        

    def init_vars(self):        

        self.timer = True
        self.h = 0
        self.iw = self.image.get_width()
        self.ih = self.image.get_height()   

        self.ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, 
            self.iw, self.ih)           

    def on_timer(self):

        if (not self.timer):
            return False

        self.darea.queue_draw()
        return True           

    def on_draw(self, wid, cr):

        ic = cairo.Context(self.ims)

        ic.rectangle(0, 0, self.iw, self.h)
        ic.fill()

        self.h += 1

        if (self.h == self.ih): 
            self.timer = False

        cr.set_source_surface(self.image, 10, 10)
        cr.mask_surface(self.ims, 10, 10)        

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

盲目效应背后的想法很简单。 图像高度为 h 像素。 我们绘制高度为 1px 的 0、1、2 ... 线。 每个周期,图像的一部分高 1px,直到整个图像可见为止。

def load_image(self):

    self.image = cairo.ImageSurface.create_from_png("beckov.png")            

load_image()方法中,我们从 PNG 图像创建图像表面。

def init_vars(self):        

    self.timer = True
    self.h = 0
    self.iw = self.image.get_width()
    self.ih = self.image.get_height()   

    self.ims = cairo.ImageSurface(cairo.FORMAT_ARGB32, 
        self.iw, self.ih)               

init_vars()方法中,我们初始化一些变量。 我们启动self.timerself.h变量。 我们得到加载图像的宽度和高度。 然后我们创建一个空的图像表面。 它将用我们之前创建的图像表面的像素线填充。

ic = cairo.Context(self.ims)

我们从空图像源创建一个 cairo 上下文。

ic.rectangle(0, 0, self.iw, self.h)
ic.fill()

我们在最初为空的图像中绘制一个矩形。 矩形每个周期将高出 1 像素。 以这种方式创建的图像稍后将用作遮罩。

self.h += 1

要显示的图像高度增加一个单位。

if (self.h == self.ih): 
    self.timer = False

当我们在 GTK 窗口上绘制整个图像时,我们将停止timer方法。

cr.set_source_surface(self.image, 10, 10)
cr.mask_surface(self.ims, 10, 10)

城堡的图像被设置为绘画的来源。 mask_surface()使用表面的 Alpha 通道作为遮罩来绘制电流源。

本章介绍了 PyCairo 中的剪裁和遮罩。

PyCairo 的透明度

原文: https://zetcode.com/gfx/pycairo/transparency/

在 PyCairo 教程的这一部分中,我们将讨论透明度。 我们将提供一些基本定义和三个有趣的透明度示例。

透明性是指能够透视材料的质量。 了解透明度的最简单方法是想象一块玻璃或水。 从技术上讲,光线可以穿过玻璃,这样我们就可以看到玻璃后面的物体。

在计算机图形学中,我们可以使用 alpha 合成实现透明效果。 Alpha 合成是将图像与背景组合以创建部分透明外观的过程。 合成过程使用 alpha 通道。 Alpha 通道是图形文件格式的 8 位层,用于表达半透明性(透明度)。 每个像素的额外八位用作掩码,表示 256 级半透明。
(answers.com,wikipedia.org)

透明矩形

第一个示例将绘制十个透明度不同的矩形。

def on_draw(self, wid, cr):

    for i in range(1, 11):

        cr.set_source_rgba(0, 0, 1, i*0.1)
        cr.rectangle(50*i, 20, 40, 40)
        cr.fill()        

set_source_rgba()方法具有 alpha 参数以提供透明度。

for i in range(1, 11):

    cr.set_source_rgba(0, 0, 1, i*0.1)
    cr.rectangle(50*i, 20, 40, 40)
    cr.fill()

此代码创建十个矩形,其 alpha 值从 0.1 到 1。

Transparent rectangles

图:透明矩形

泡芙效果

在以下示例中,我们创建一个粉扑效果。 该示例将显示一个不断增长的居中文本,该文本将从某个点逐渐淡出。 这是一个非常常见的效果,我们经常可以在 Flash 动画中看到它。 paint_with_alpha()方法对于产生效果至关重要。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program creates a 'puff'
effect.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk, GLib
import cairo

class cv(object):

    SPEED = 14
    TEXT_SIZE_MAX = 20
    ALPHA_DECREASE = 0.01
    SIZE_INCREASE = 0.8

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        self.darea = Gtk.DrawingArea()
        self.darea.connect("draw", self.on_draw)
        self.add(self.darea)

        self.timer = True
        self.alpha = 1.0
        self.size = 1.0       

        GLib.timeout_add(cv.SPEED, self.on_timer)

        self.set_title("Puff")
        self.resize(350, 200)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_timer(self):

        if not self.timer: return False

        self.darea.queue_draw()
        return True        

    def on_draw(self, wid, cr):

        w, h = self.get_size()

        cr.set_source_rgb(0.5, 0, 0)
        cr.paint()

        cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL, 
            cairo.FONT_WEIGHT_BOLD)

        self.size = self.size + cv.SIZE_INCREASE

        if self.size > cv.TEXT_SIZE_MAX:
            self.alpha = self.alpha - cv.ALPHA_DECREASE

        cr.set_font_size(self.size)
        cr.set_source_rgb(1, 1, 1)

        (x, y, width, height, dx, dy) = cr.text_extents("ZetCode")

        cr.move_to(w/2 - width/2, h/2)
        cr.text_path("ZetCode")
        cr.clip()
        cr.paint_with_alpha(self.alpha)

        if self.alpha <= 0:
            self.timer = False

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

该示例在窗口上创建一个逐渐增长和褪色的文本。

class cv(object):

    SPEED = 14
    TEXT_SIZE_MAX = 20
    ALPHA_DECREASE = 0.01
    SIZE_INCREASE = 0.8

在这里,我们定义了示例中使用的一些常量。

self.alpha = 1.0
self.size = 1.0     

这两个变量存储当前的 alpha 值和文本大小。

GLib.timeout_add(cv.SPEED, self.on_timer)

每 14 毫秒调用一次on_timer()方法。

def on_timer(self):

    if not self.timer: return False

    self.darea.queue_draw()
    return True 

on_timer()方法中,我们使用queue_draw()方法重绘绘图区域小部件。

def on_draw(self, wid, cr):

    w, h = self.get_size()

    cr.set_source_rgb(0.5, 0, 0)
    cr.paint()

    cr.select_font_face("Courier", cairo.FONT_SLANT_NORMAL, 
        cairo.FONT_WEIGHT_BOLD)
...

on_draw()方法中,我们获得窗口工作区的宽度和高度。 这些值用于使文本居中。 我们用一些深红色填充窗口的背景。 我们为文本选择一种 Courier 字体。

(x, y, width, height, dx, dy) = cr.text_extents("ZetCode")

我们得到了文本指标。 我们将仅使用文本宽度。

cr.move_to(w/2 - width/2, h/2)

我们移动到文本将在窗口上居中的位置。

cr.text_path("ZetCode")
cr.clip()
cr.paint_with_alpha(self.alpha)

我们使用text_path()方法获得文本的路径。 我们使用clip()方法将绘画限制为当前路径。 paint_with_alpha()方法使用 alpha 值的掩码在当前剪裁区域内的任何地方绘制当前源。

Puff effect

图:粉扑效果

反射图像

在下一个示例中,我们显示反射图像。 这种效果使人产生幻觉,好像图像在水中被反射一样。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program creates an image reflection.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk
import cairo
import sys

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()
        self.load_image()
        self.init_vars()

    def init_ui(self):    

        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)

        self.set_title("Reflection")
        self.resize(300, 350)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def load_image(self):          

        try:
            self.s = cairo.ImageSurface.create_from_png("slanec.png")
        except Exception, e:
            print e.message
            sys.exit(1)              

    def init_vars(self):

        self.imageWidth = self.s.get_width()
        self.imageHeight = self.s.get_height()
        self.gap = 40
        self.border = 20                

    def on_draw(self, wid, cr):

        w, h = self.get_size()

        lg = cairo.LinearGradient(w/2, 0, w/2, h*3)
        lg.add_color_stop_rgba(0, 0, 0, 0, 1)
        lg.add_color_stop_rgba(h, 0.2, 0.2, 0.2, 1)

        cr.set_source(lg)
        cr.paint()

        cr.set_source_surface(self.s, self.border, self.border)
        cr.paint()

        alpha = 0.7
        step = 1.0 / self.imageHeight

        cr.translate(0, 2 * self.imageHeight + self.gap)
        cr.scale(1, -1)

        i = 0

        while(i < self.imageHeight):

            cr.rectangle(self.border, self.imageHeight-i, 
                self.imageWidth, 1)

            i = i + 1

            cr.save()
            cr.clip()
            cr.set_source_surface(self.s, self.border, 
                self.border)

            alpha = alpha - step

            cr.paint_with_alpha(alpha)
            cr.restore()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

窗户上显示了一座城堡的倒影。

def load_image(self):          

    try:
        self.s = cairo.ImageSurface.create_from_png("slanec.png")
    except Exception, e:
        print e.message
        sys.exit(1)    

load_image()方法中,从 PNG 图像创建图像表面。

def init_vars(self):

    self.imageWidth = self.s.get_width()
    self.imageHeight = self.s.get_height()
    self.gap = 40
    self.border = 20  

init_vars()方法内部,我们获得图像的宽度和高度。 我们还定义了两个变量。

lg = cairo.LinearGradient(w/2, 0, w/2, h*3)
lg.add_color_stop_rgba(0, 0, 0, 0, 1)
lg.add_color_stop_rgba(h, 0.2, 0.2, 0.2, 1)

cr.set_source(lg)
cr.paint()

窗口的背景填充有渐变颜料。 涂料是从黑色到深灰色的平滑混合。

cr.translate(0, 2 * self.imageHeight + self.gap)
cr.scale(1, -1)

此代码翻转图像并将其转换为原始图像下方。 平移操作是必需的,因为缩放操作会使图像上下颠倒并向上平移图像。 要了解发生了什么,只需拍摄一张照片并将其放在桌子上即可。 并翻转它。

i = 0

while(i < self.imageHeight):

    cr.rectangle(self.border, self.imageHeight-i, 
        self.imageWidth, 1)

    i = i + 1

    cr.save()
    cr.clip()
    cr.set_source_surface(self.s, self.border, 
        self.border)

    alpha = alpha - step

    cr.paint_with_alpha(alpha)
    cr.restore()

这是最后一部分。 我们使第二个图像透明。 但是透明度不是恒定的。 图像逐渐淡出。 反射的图像逐行绘制。 clip()方法将图形限制为高度为 1 的矩形。paint_with_alpha()在绘制图像表面的当前剪裁时会考虑透明度。

Reflected image

图:反射图像

等待演示

在此示例中,我们使用透明效果创建一个等待演示。 我们将绘制 8 条线,这些线将逐渐消失,从而产生一种错觉,即一条线在移动。 此类效果通常用于通知用户幕后正在进行繁重的任务。 一个示例是通过互联网流式传输视频。

#!/usr/bin/python

'''
ZetCode PyCairo tutorial 

This program creates a 'waiting' effect.

author: Jan Bodnar
website: zetcode.com 
last edited: August 2012
'''

from gi.repository import Gtk, GLib
import cairo
import math

class cv(object):

    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, )
    )

    SPEED = 100
    CLIMIT = 1000
    NLINES = 8  

class Example(Gtk.Window):

    def __init__(self):
        super(Example, self).__init__()

        self.init_ui()

    def init_ui(self):    

        self.darea = Gtk.DrawingArea()
        self.darea.connect("draw", self.on_draw)
        self.add(self.darea)

        self.count = 0

        GLib.timeout_add(cv.SPEED, self.on_timer)        

        self.set_title("Waiting")
        self.resize(250, 150)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_timer(self):    

        self.count = self.count + 1

        if self.count >= cv.CLIMIT:
            self.count = 0        

        self.darea.queue_draw()

        return True        

    def on_draw(self, wid, cr):

        cr.set_line_width(3)
        cr.set_line_cap(cairo.LINE_CAP_ROUND)

        w, h = self.get_size()

        cr.translate(w/2, h/2)

        for i in range(cv.NLINES):

            cr.set_source_rgba(0, 0, 0, cv.trs[self.count%8][i])
            cr.move_to(0.0, -10.0)
            cr.line_to(0.0, -40.0)
            cr.rotate(math.pi/4)
            cr.stroke()

def main():

    app = Example()
    Gtk.main()

if __name__ == "__main__":    
    main()

我们用八个不同的 alpha 值绘制八条线。

class cv(object):

    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, )
    )
...

这是此演示中使用的透明度值的二维元组。 有 8 行,每行一种状态。 8 行中的每行将连续使用这些值。

SPEED = 100
CLIMIT = 1000
NLINES = 8  

SPEED常数控制动画的速度。 CLIMITself.count变量的最大数量。 达到此限制后,变量将重置为 0。NLINES是示例中绘制的行数。

GLib.timeout_add(cv.SPEED, self.on_timer)

我们使用计时器函数来创建动画。 on_timer()方法的每个cv.SPEED ms 被调用。

def on_timer(self):    

    self.count = self.count + 1

    if self.count >= cv.CLIMIT:
        self.count = 0        

    self.darea.queue_draw()

    return True  

on_timer()方法中,我们增加self.count变量。 如果变量达到cv.CLIMIT常量,则将其设置为 0。我们防止溢出,并且不使用大数。

def on_draw(self, wid, cr):

    cr.set_line_width(3)
    cr.set_line_cap(cairo.LINE_CAP_ROUND)
...

我们使线条更粗一些,以便更好地显示它们。 我们用带帽的线画线。

w, h = self.get_size()

cr.translate(w/2, h/2)

我们将图形放置在窗口的中央。

for i in range(cv.NLINES):

    cr.set_source_rgba(0, 0, 0, cv.trs[self.count%8][i])
    cr.move_to(0.0, -10.0)
    cr.line_to(0.0, -40.0)
    cr.rotate(math.pi/4)
    cr.stroke()

for循环中,我们绘制了 8 条具有不同透明度值的旋转线。 线以 45 度角分开。

Waiting demo

图:等待 demo

在 PyCairo 教程的这一部分中,我们介绍了透明度。

移动精灵

原文: https://zetcode.com/tutorials/javagamestutorial/movingsprites/

在 Java 2D 游戏教程的这一部分中,我们将使用精灵。

术语精灵具有多种含义。 它用于表示场景中的图像或动画。

它也用于表示游戏中的任何可移动对象。 含义之一也是在游戏中封装角色的代码。 在我们的教程中,通过使用精灵,我们引用了一个可移动对象或其 Java 类。

移动精灵

在第一个示例中,我们有一个太空飞船。 我们可以使用光标键在板上移动宇宙飞船。

SpaceShip.java

package com.zetcode;

import java.awt.Image;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;

public class SpaceShip {

    private int dx;
    private int dy;
    private int x = 40;
    private int y = 60;
    private int w;
    private int h;
    private Image image;

    public SpaceShip() {

        loadImage();
    }

    private void loadImage() {

        ImageIcon ii = new ImageIcon("src/resources/spaceship.png");
        image = ii.getImage(); 

        w = image.getWidth(null);
        h = image.getHeight(null);
    }

    public void move() {

        x += dx;
        y += dy;
    }

    public int getX() {

        return x;
    }

    public int getY() {

        return y;
    }

    public int getWidth() {

        return w;
    }

    public int getHeight() {

        return h;
    }    

    public Image getImage() {

        return image;
    }

    public void keyPressed(KeyEvent e) {

        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {
            dx = -2;
        }

        if (key == KeyEvent.VK_RIGHT) {
            dx = 2;
        }

        if (key == KeyEvent.VK_UP) {
            dy = -2;
        }

        if (key == KeyEvent.VK_DOWN) {
            dy = 2;
        }
    }

    public void keyReleased(KeyEvent e) {

        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {
            dx = 0;
        }

        if (key == KeyEvent.VK_RIGHT) {
            dx = 0;
        }

        if (key == KeyEvent.VK_UP) {
            dy = 0;
        }

        if (key == KeyEvent.VK_DOWN) {
            dy = 0;
        }
    }
}

此类代表一艘太空飞船。 在此类中,我们保留子画面的图像和子画面的坐标。 keyPressed()keyReleased()方法控制精灵是否在移动。

public void move() {
    x += dx;
    y += dy;
}

move()方法更改子画面的坐标。 这些 x 和 y 值在paintComponent()方法中用于绘制子画面的图像。

if (key == KeyEvent.VK_LEFT) {
    dx = 0;
}

释放左光标键时,将dx变量设置为零。 航天器将停止移动。

Board.java

package com.zetcode;

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

public class Board extends JPanel implements ActionListener {

    private Timer timer;
    private SpaceShip spaceShip;
    private final int DELAY = 10;

    public Board() {

        initBoard();
    }

    private void initBoard() {

        addKeyListener(new TAdapter());
        setBackground(Color.black);
	setFocusable(true);

        spaceShip = new SpaceShip();

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

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

        doDrawing(g);

        Toolkit.getDefaultToolkit().sync();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.drawImage(spaceShip.getImage(), spaceShip.getX(), 
            spaceShip.getY(), this);
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        step();
    }

    private void step() {

        spaceShip.move();

        repaint(spaceShip.getX()-1, spaceShip.getY()-1, 
                spaceShip.getWidth()+2, spaceShip.getHeight()+2);     
    }    

    private class TAdapter extends KeyAdapter {

        @Override
        public void keyReleased(KeyEvent e) {
            spaceShip.keyReleased(e);
        }

        @Override
        public void keyPressed(KeyEvent e) {
            spaceShip.keyPressed(e);
        }
    }
}

这是Board类。

private void doDrawing(Graphics g) {

    Graphics2D g2d = (Graphics2D) g;

    g2d.drawImage(ship.getImage(), ship.getX(), ship.getY(), this);
}

doDrawing()方法中,我们使用drawImage()方法绘制宇宙飞船。 我们从精灵类中获得图像和坐标。

@Override
public void actionPerformed(ActionEvent e) {

    step();
}

actionPerformed()方法每DELAY ms 调用一次。 我们称为step()方法。

private void step() {

    ship.move();
    repaint(ship.getX()-1, ship.getY()-1, 
            ship.getWidth()+2, ship.getHeight()+2);     
}    

我们移动精灵并重新粉刷已更改的电路板部分。 我们使用一种小的优化技术,该技术仅重新绘制实际更改的窗口的小区域。

private class TAdapter extends KeyAdapter {

    @Override
    public void keyReleased(KeyEvent e) {
        craft.keyReleased(e);
    }

    @Override
    public void keyPressed(KeyEvent e) {
        craft.keyPressed(e);
    }
}

Board类中,我们监听关键事件。 KeyAdapter类的重写方法将处理委托给Craft类的方法。

MovingSpriteEx.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class MovingSpriteEx extends JFrame {

    public MovingSpriteEx() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setTitle("Moving sprite");
        setSize(400, 300);

        setLocationRelativeTo(null);
        setResizable(false);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            MovingSpriteEx ex = new MovingSpriteEx();
            ex.setVisible(true);
        });
    }
}

这是主要的类。

Moving sprite

图:移动精灵

射击导弹

在下一个示例中,我们在示例中添加了另一个精灵类型-导弹。 用 Space 键发射导弹。

Sprite.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Sprite {

    protected int x;
    protected int y;
    protected int width;
    protected int height;
    protected boolean visible;
    protected Image image;

    public Sprite(int x, int y) {

        this.x = x;
        this.y = y;
        visible = true;
    }

    protected void loadImage(String imageName) {

        ImageIcon ii = new ImageIcon(imageName);
        image = ii.getImage();
    }

    protected void getImageDimensions() {

        width = image.getWidth(null);
        height = image.getHeight(null);
    }    

    public Image getImage() {
        return image;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(Boolean visible) {
        this.visible = visible;
    }
}

Sprite类共享MissileSpaceShip类的通用代码。

public Sprite(int x, int y) {

    this.x = x;
    this.y = y;

    visible = true;
}

构造器初始化 x 和 y 坐标以及visible变量。

Missile.java

package com.zetcode;

public class Missile extends Sprite {

    private final int BOARD_WIDTH = 390;
    private final int MISSILE_SPEED = 2;

    public Missile(int x, int y) {
        super(x, y);

        initMissile();
    }

    private void initMissile() {

        loadImage("src/resources/missile.png");  
        getImageDimensions();
    }

    public void move() {

        x += MISSILE_SPEED;

        if (x > BOARD_WIDTH) {
            visible = false;
        }
    }
}

在这里,我们有一个名为Missile的新精灵。

public void move() {

    x += MISSILE_SPEED;

    if (x > BOARD_WIDTH) {
        vis = false;
    }
}

导弹以恒定速度运动。 当它碰到Board的右边界时,它变得不可见。 然后将其从导弹列表中删除。

SpaceShip.java

package com.zetcode;

import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;

public class SpaceShip extends Sprite {

    private int dx;
    private int dy;
    private List<Missile> missiles;

    public SpaceShip(int x, int y) {
        super(x, y);

        initSpaceShip();
    }

    private void initSpaceShip() {

        missiles = new ArrayList<>();

        loadImage("src/resources/spaceship.png"); 
        getImageDimensions();
    }

    public void move() {
        x += dx;
        y += dy;
    }

    public List<Missile> getMissiles() {
        return missiles;
    }

    public void keyPressed(KeyEvent e) {

        int key = e.getKeyCode();

        if (key == KeyEvent.VK_SPACE) {
            fire();
        }

        if (key == KeyEvent.VK_LEFT) {
            dx = -1;
        }

        if (key == KeyEvent.VK_RIGHT) {
            dx = 1;
        }

        if (key == KeyEvent.VK_UP) {
            dy = -1;
        }

        if (key == KeyEvent.VK_DOWN) {
            dy = 1;
        }
    }

    public void fire() {
        missiles.add(new Missile(x + width, y + height / 2));
    }

    public void keyReleased(KeyEvent e) {

        int key = e.getKeyCode();

        if (key == KeyEvent.VK_LEFT) {
            dx = 0;
        }

        if (key == KeyEvent.VK_RIGHT) {
            dx = 0;
        }

        if (key == KeyEvent.VK_UP) {
            dy = 0;
        }

        if (key == KeyEvent.VK_DOWN) {
            dy = 0;
        }
    }
}

这是SpaceShip类。

if (key == KeyEvent.VK_SPACE) {
    fire();
}

如果按空格键,则会触发。

public void fire() {
    missiles.add(new Missile(x + width, y + height / 2));
}

fire()方法创建一个新的Missile对象并将其添加到导弹列表中。

public List<Missile> getMissiles() {
    return missiles;
}

getMissiles()方法返回导弹列表。 从Board类调用它。

Board.java

package com.zetcode;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Board extends JPanel implements ActionListener {

    private final int ICRAFT_X = 40;
    private final int ICRAFT_Y = 60;
    private final int DELAY = 10;
    private Timer timer;
    private SpaceShip spaceShip;

    public Board() {

        initBoard();
    }

    private void initBoard() {

        addKeyListener(new TAdapter());
        setBackground(Color.BLACK);
        setFocusable(true);

        spaceShip = new SpaceShip(ICRAFT_X, ICRAFT_Y);

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

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

        doDrawing(g);

        Toolkit.getDefaultToolkit().sync();
    }

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.drawImage(spaceShip.getImage(), spaceShip.getX(),
                spaceShip.getY(), this);

        List<Missile> missiles = spaceShip.getMissiles();

        for (Missile missile : missiles) {

            g2d.drawImage(missile.getImage(), missile.getX(),
                    missile.getY(), this);
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        updateMissiles();
        updateSpaceShip();

        repaint();
    }

    private void updateMissiles() {

        List<Missile> missiles = spaceShip.getMissiles();

        for (int i = 0; i < missiles.size(); i++) {

            Missile missile = missiles.get(i);

            if (missile.isVisible()) {

                missile.move();
            } else {

                missiles.remove(i);
            }
        }
    }

    private void updateSpaceShip() {

        spaceShip.move();
    }

    private class TAdapter extends KeyAdapter {

        @Override
        public void keyReleased(KeyEvent e) {
            spaceShip.keyReleased(e);
        }

        @Override
        public void keyPressed(KeyEvent e) {
            spaceShip.keyPressed(e);
        }
    }
}

This is the Board class.

private void doDrawing(Graphics g) {

    Graphics2D g2d = (Graphics2D) g;

    g2d.drawImage(spaceShip.getImage(), spaceShip.getX(),
            spaceShip.getY(), this);

    List<Missile> missiles = spaceShip.getMissiles();

    for (Missile missile : missiles) {

        g2d.drawImage(missile.getImage(), missile.getX(),
                missile.getY(), this);
    }
}

doDrawing()方法中,我们绘制飞行器和所有可用的导弹。

private void updateMissiles() {

    List<Missile> missiles = spaceShip.getMissiles();

    for (int i = 0; i < missiles.size(); i++) {

        Missile missile = missiles.get(i);

        if (missile.isVisible()) {

            missile.move();
        } else {

            missiles.remove(i);
        }
    }
}

updateMissiles()方法中,我们解析missiles列表中的所有导弹。 根据isVisible()方法返回的内容,我们要么移动导弹,要么将其从容器中取出。

ShootingMissilesEx.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class ShootingMissilesEx extends JFrame {

    public ShootingMissilesEx() {

        initUI();
    }

    private void initUI() {

        add(new Board());

        setSize(400, 300);
        setResizable(false);

        setTitle("Shooting missiles");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {
            ShootingMissilesEx ex = new ShootingMissilesEx();
            ex.setVisible(true);
        });
    }
}

最后,这是主要类。

Shooting missiles

图:发射导弹

在本章中,我们介绍了精灵。

posted @ 2024-10-24 18:20  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报