summaryrefslogtreecommitdiff
path: root/src/ch/epfl/maze/graphics/GraphicComponent.java
blob: e63b145732c5a2111c13a0702f23d6f1169b3afa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package ch.epfl.maze.graphics;

import ch.epfl.maze.util.Action;
import ch.epfl.maze.util.Direction;
import ch.epfl.maze.util.Vector2D;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;

/**
 * Graphic component of an animal that will be drawn by an {@link Animation}.
 *
 * @author EPFL
 */
public final class GraphicComponent {

    /* constants */
    public static final int MAXIMUM_FRAMES = 4;
    public static final int SQUARE_SIZE = Display.SQUARE_SIZE;

    /* drawing variables */
    private final Vector2D mPosition;
    private final BufferedImage mImage;
    private Action mAction;
    private boolean mRotate;

    /**
     * Constructs a graphic component with the image of the animal, the position
     * at which the component needs to be drawn, and the corresponding action
     * that it needs to perform.
     *
     * @param image    Image of animal
     * @param position Position at which the image will be drawn
     * @param action   Action that the component needs to perform
     */
    public GraphicComponent(BufferedImage image, Vector2D position, Action action) {
        // sanity checks
        if (image == null) {
            throw new IllegalArgumentException("BufferedImage cannot be null.");
        }
        if (position == null) {
            throw new IllegalArgumentException("Position cannot be null.");
        }
        if (action == null) {
            action = new Action(Direction.NONE, false);
        }

        // default values
        mImage = image;
        mPosition = position;
        mRotate = true;
        mAction = action;
    }

    /**
     * Notifies the component that it will die between two squares.
     */
    public void willDieMoving() {
        mAction = new Action(mAction.getDirection(), mAction.isSuccessful(), true);
    }

    /**
     * Asks the graphic component to paint itself on graphic environment, by
     * performing a ratio of its action.
     *
     * @param ratio        Ratio of the action to be performed
     * @param g            Graphic environment
     * @param targetWindow Window on which the graphic is being drawn
     */
    public void paint(float ratio, Graphics2D g, ImageObserver targetWindow) {
        if (mAction.getDirection() == Direction.NONE) {
            renderStuck(g, targetWindow);
        } else {
            if (ratio > 0.5) {
                if (!mAction.diesBetweenSquares() && !mAction.isSuccessful()) {
                    renderMove(1 - ratio, g, targetWindow, true);
                } else if (!mAction.diesBetweenSquares()) {
                    renderMove(ratio, g, targetWindow, false);
                }
            } else {
                renderMove(ratio, g, targetWindow, false);
            }
        }
    }

    /**
     * Draws the moving component on graphics environment and target window.
     * <p>
     * The function draws the animal at {@code (position + ratio*heading)}.
     *
     * @param ratio        Ratio of action performed by animation
     * @param g            Graphic environment
     * @param targetWindow Frame display
     * @param buzz         Buzzes the animal, used when he has just hit a wall
     */
    private void renderMove(float ratio, Graphics2D g, ImageObserver targetWindow, boolean buzz) {
        // transforms direction into vector
        Vector2D heading = mAction.getDirection().toVector().mul(SQUARE_SIZE);
        Vector2D normalized = heading.normalize();

        // loads the correct frame
        BufferedImage img = cropImage(ratio, mAction.getDirection());

        AffineTransform reset = new AffineTransform();

        // applies translation
        double newX = (mPosition.getX() + ratio * heading.getX());
        double newY = (mPosition.getY() + ratio * heading.getY());
        reset.translate(newX, newY);

        // applies rotation
        double rotation = 0;
        if (buzz) {
            rotation = -(Math.PI / 6.0) * Math.sin((60 * ratio) / Math.PI);
        }
        if (mRotate) {
            rotation += Math.atan2(normalized.getY(), normalized.getX()) - Math.PI / 2;
        }
        reset.rotate(rotation, SQUARE_SIZE / 2, SQUARE_SIZE / 2);

        // transforms and draws image
        g.setTransform(reset);
        g.drawImage(img, 0, 0, targetWindow);

        // inverts transformations
        reset.rotate(-rotation, SQUARE_SIZE / 2, SQUARE_SIZE / 2);
        reset.translate(-newX, -newY);
        g.setTransform(reset);
    }

    /**
     * Draws the still component on graphics environment and target window.
     * <p>
     * Draws a question mark if {@code mAction} is not successful
     *
     * @param g            Graphic environment
     * @param targetWindow Frame display
     */
    private void renderStuck(Graphics2D g, ImageObserver targetWindow) {
        // loads default frame of image with default direction
        BufferedImage img = cropImage(-1, Direction.NONE);

        AffineTransform reset = new AffineTransform();

        // applies translation
        double newX = mPosition.getX();
        double newY = mPosition.getY();
        reset.translate(newX, newY);

        // transforms and draws image
        g.setTransform(reset);
        g.drawImage(img, 0, 0, targetWindow);

        // draws interrogation mark
        if (!mAction.isSuccessful()) {
            ImageIcon icon = new ImageIcon("img/unknown.png");
            g.drawImage(icon.getImage(), SQUARE_SIZE - icon.getIconWidth() - 2, 2, targetWindow);
        }

        // inverts translation
        reset.translate(-newX, -newY);
        g.setTransform(reset);
    }

    /**
     * Transforms the image according to frame and movement.
     *
     * @param ratio The ratio of the action to perform
     * @param dir   The direction towards which the component faces
     * @return The correct frame that faces towards the direction specified
     */
    private BufferedImage cropImage(float ratio, Direction dir) {
        int width = mImage.getWidth();
        int height = mImage.getHeight();
        int frames = width / SQUARE_SIZE;
        int moves = height / SQUARE_SIZE;

        // sanity checks
        if (width % SQUARE_SIZE != 0 || height % SQUARE_SIZE != 0) {
            throw new UnsupportedOperationException(
                    "Image size is not a multiple of " + SQUARE_SIZE + " pixels, but " + width + "x" + height);
        }
        if (moves > Direction.values().length) {
            throw new UnsupportedOperationException(
                    "Image height has more than " + Direction.values().length + " moves (" + height + ")");
        }
        if (frames > MAXIMUM_FRAMES) {
            throw new UnsupportedOperationException(
                    "Image width has more than " + MAXIMUM_FRAMES + " frames (" + frames + ")");
        }

        // selects frame
        int frame = ((int) (ratio * 2 * frames)) % frames;
        if (frame >= frames) {
            frame = 0;
        } else if (ratio < 0 || ratio > 1) { // handles bad ratio
            frame = (int) (frames / 2);
        }

        // selects move
        int move = dir.intValue();
        mRotate = false;
        if (move >= moves) {
            mRotate = true;
            move = 0;
        }

        // selects subimage according to frame and move
        BufferedImage img = mImage.getSubimage(frame * SQUARE_SIZE, move * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE);

        return img;
    }

}