Skip to content

Commit

Permalink
Merge pull request #211 from creme332/animation
Browse files Browse the repository at this point in the history
add translation and rotation animations
  • Loading branch information
creme332 authored Aug 10, 2024
2 parents 4768076 + cdd96bd commit ba135fa
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.github.creme332.model.AppState;
import com.github.creme332.model.Mode;
import com.github.creme332.model.ShapeWrapper;
import com.github.creme332.utils.RequestFocusListener;
import com.github.creme332.view.Canvas;

/**
Expand Down Expand Up @@ -60,11 +61,14 @@ private double[] requestReflectionLine() {
JTextField gradientField = new JTextField(5);
JTextField yInterceptField = new JTextField(5);
JPanel panel = new JPanel();
panel.add(new JLabel("Gradient (m):"));
panel.add(new JLabel("Gradient"));
panel.add(gradientField);
panel.add(new JLabel("Y-Intercept (b):"));
panel.add(new JLabel("Y-Intercept"));
panel.add(yInterceptField);

// Request focus on the textfield when dialog is displayed
gradientField.addHierarchyListener(new RequestFocusListener());

int result = JOptionPane.showConfirmDialog(canvas, panel, "Enter Line of Reflection",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.github.creme332.controller.canvas.transform;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import javax.swing.ButtonGroup;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.Timer;

import com.github.creme332.model.AppState;
import com.github.creme332.model.Mode;
import com.github.creme332.model.ShapeWrapper;
import com.github.creme332.utils.RequestFocusListener;
import com.github.creme332.view.Canvas;

public class Rotator extends AbstractTransformer {
Expand All @@ -27,15 +31,58 @@ public void handleShapeSelection(int shapeIndex) {
// Request rotation details from the user
RotationDetails rotationDetails = requestRotationDetails();

// Perform rotation
// Request focus again otherwise keyboard shortcuts will not work
canvas.getTopLevelAncestor().requestFocus();

// Calculate rotation angle in radians
double radAngle = Math.toRadians(rotationDetails.angle * (rotationDetails.isClockwise ? -1 : 1));
selectedWrapperCopy.rotate(radAngle, rotationDetails.pivot);

// Replace old shape with new one
canvasModel.getShapeManager().editShape(shapeIndex, selectedWrapperCopy);
startRotationAnimation(selectedWrapperCopy, shapeIndex, radAngle, rotationDetails.pivot);
}

/**
* Animates rotation of a given shape
*
* @param selectedWrapperCopy Shape to be rotated
* @param shapeIndex ID of shape in canvas
* @param radAngle Rotation angle
* @param pivot Rotation pivot
*/
public void startRotationAnimation(final ShapeWrapper selectedWrapperCopy, final int shapeIndex,
final double radAngle,
final Point2D pivot) {
if (radAngle == 0)
return;

final int animationDelay = 10; // Delay in milliseconds between updates
final double stepAngle = Math.toRadians(1.0) * Math.signum(radAngle); // Step size for each update (1 degree)
final double totalSteps = Math.abs(radAngle) / Math.abs(stepAngle); // Total number of steps

// Timer to handle the animation
Timer timer = new Timer(animationDelay, new ActionListener() {
private int stepCount = 0;
ShapeWrapper copyPreview;

@Override
public void actionPerformed(ActionEvent e) {
if (stepCount < totalSteps) {
copyPreview = new ShapeWrapper(selectedWrapperCopy);
copyPreview.rotate(stepAngle * stepCount, pivot); // Rotate by the step angle
canvasModel.getShapeManager().setShapePreview(copyPreview);
canvas.repaint();

stepCount++;
} else {
((Timer) e.getSource()).stop(); // Stop the timer when done
canvasModel.getShapeManager().setShapePreview(null);
// Replace old shape with new one so that transformation can be undo-ed
canvasModel.getShapeManager().editShape(shapeIndex, copyPreview);
canvas.repaint();
}
}
});

// Repaint canvas
canvas.repaint();
timer.start();
}

@Override
Expand All @@ -45,8 +92,11 @@ public boolean shouldDraw() {

private RotationDetails requestRotationDetails() {
JTextField angleField = new JTextField(5);
JTextField pivotXField = new JTextField(5);
JTextField pivotYField = new JTextField(5);
JTextField pivotXField = new JTextField("0", 5);
JTextField pivotYField = new JTextField("0", 5);

// Request focus on the textfield when dialog is displayed
angleField.addHierarchyListener(new RequestFocusListener());

JRadioButton clockwiseButton = new JRadioButton("clockwise");
JRadioButton counterClockwiseButton = new JRadioButton("counterclockwise");
Expand All @@ -69,9 +119,6 @@ private RotationDetails requestRotationDetails() {
int result = JOptionPane.showConfirmDialog(canvas, panel, "Rotate About Point",
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);

// Request focus again otherwise keyboard shortcuts will not work
canvas.getTopLevelAncestor().requestFocus();

if (result == JOptionPane.OK_OPTION) {
try {
double angle = Double.parseDouble(angleField.getText());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.github.creme332.model.AppState;
import com.github.creme332.model.Mode;
import com.github.creme332.model.ShapeWrapper;
import com.github.creme332.utils.RequestFocusListener;
import com.github.creme332.view.Canvas;

/**
Expand Down Expand Up @@ -59,20 +60,23 @@ public boolean shouldDraw() {
* cancelled.
*/
private double[] requestScaleData() {
JTextField xField = new JTextField(5);
JTextField yField = new JTextField(5);
JTextField xField = new JTextField("0", 5);
JTextField yField = new JTextField("0", 5);
JTextField sxField = new JTextField(5);
JTextField syField = new JTextField(5);

JPanel panel = new JPanel();
panel.add(new JLabel("X:"));
panel.add(xField);
panel.add(new JLabel("Y:"));
panel.add(yField);
panel.add(new JLabel("Scale X:"));
panel.add(new JLabel("Scale X"));
panel.add(sxField);
panel.add(new JLabel("Scale Y:"));
panel.add(new JLabel("Scale Y"));
panel.add(syField);
panel.add(new JLabel("X"));
panel.add(xField);
panel.add(new JLabel("Y"));
panel.add(yField);

// Request focus on the textfield when dialog is displayed
sxField.addHierarchyListener(new RequestFocusListener());

int result = JOptionPane.showConfirmDialog(canvas, panel, "Enter scaling data",
JOptionPane.OK_CANCEL_OPTION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.github.creme332.model.AppState;
import com.github.creme332.model.Mode;
import com.github.creme332.model.ShapeWrapper;
import com.github.creme332.utils.RequestFocusListener;
import com.github.creme332.view.Canvas;

/**
Expand Down Expand Up @@ -54,11 +55,14 @@ private double[] requestShearFactors() {
JTextField sxField = new JTextField(5);
JTextField syField = new JTextField(5);
JPanel panel = new JPanel();
panel.add(new JLabel("Shear X:"));
panel.add(new JLabel("Shear X"));
panel.add(sxField);
panel.add(new JLabel("Shear Y:"));
panel.add(new JLabel("Shear Y"));
panel.add(syField);

// Request focus on the textfield when dialog is displayed
sxField.addHierarchyListener(new RequestFocusListener());

int result = JOptionPane.showConfirmDialog(canvas, panel, "Enter shear factors",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.github.creme332.controller.canvas.transform;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

import com.github.creme332.model.AppState;
import com.github.creme332.model.Mode;
import com.github.creme332.model.ShapeWrapper;
import com.github.creme332.utils.RequestFocusListener;
import com.github.creme332.view.Canvas;

/**
Expand All @@ -31,14 +35,45 @@ public void handleShapeSelection(int shapeIndex) {
// request user for translation vector
final Point2D translationVector = requestTranslationVector();

// translate wrapper
selectedWrapperCopy.translate(translationVector);
startTranslationAnimation(selectedWrapperCopy, shapeIndex, translationVector);
}

// replace old shape with new one
canvasModel.getShapeManager().editShape(shapeIndex, selectedWrapperCopy);
/**
* Animates the translation of a given shape using linear interpolation.
*/
public void startTranslationAnimation(final ShapeWrapper selectedWrapperCopy, final int shapeIndex,
Point2D translationVector) {
final int totalSteps = 60; // 60 frames
final int animationDuration = 1000; // 1 second
final int animationDelay = animationDuration / totalSteps; // Delay in milliseconds between updates

// Timer to handle the animation
Timer timer = new Timer(animationDelay, new ActionListener() {
private int stepCount = 0;
ShapeWrapper copyPreview;

@Override
public void actionPerformed(ActionEvent e) {
if (stepCount <= totalSteps) {
copyPreview = new ShapeWrapper(selectedWrapperCopy);
Point2D newTranslationVector = new Point2D.Double(translationVector.getX() * stepCount / totalSteps,
translationVector.getY() * stepCount / totalSteps);
copyPreview.translate(newTranslationVector);
canvasModel.getShapeManager().setShapePreview(copyPreview);
canvas.repaint();

stepCount++;
} else {
((Timer) e.getSource()).stop(); // Stop the timer when done
canvasModel.getShapeManager().setShapePreview(null);
// Replace old shape with new one so that transformation can be undo-ed
canvasModel.getShapeManager().editShape(shapeIndex, copyPreview);
canvas.repaint();
}
}
});

// repaint canvas
canvas.repaint();
timer.start();
}

@Override
Expand All @@ -58,11 +93,14 @@ private Point2D requestTranslationVector() {
JTextField rxField = new JTextField(5);
JTextField ryField = new JTextField(5);
JPanel panel = new JPanel();
panel.add(new JLabel("X:"));
panel.add(new JLabel("X"));
panel.add(rxField);
panel.add(new JLabel("Y:"));
panel.add(new JLabel("Y"));
panel.add(ryField);

// Request focus on the textfield when dialog is displayed
rxField.addHierarchyListener(new RequestFocusListener());

int result = JOptionPane.showConfirmDialog(null, panel, "Enter translation vector",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/github/creme332/model/ShapeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ public void deleteShape(final int shapeIndex) {
}

public void editShape(final int oldShapeIndex, final ShapeWrapper newShape) {
if (newShape == null) {
throw new NullPointerException("Edit shape failed: Cannot replace a shape with null.");
}

final ShapeWrapper oldShape = shapes.get(oldShapeIndex);
if (oldShapeIndex != -1) {
shapes.set(oldShapeIndex, newShape);
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/com/github/creme332/model/ShapeWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,11 @@ public void translate(final Point2D translationVector) {
// translate plotted points
for (int i = 0; i < plottedPoints.size(); i++) {
Point2D oldPoint = plottedPoints.get(i);
int roundedX = (int) (oldPoint.getX() + translationVector.getX());
int roundedY = (int) (oldPoint.getY() + translationVector.getY());

plottedPoints.set(i,
new Point2D.Double(oldPoint.getX() + translationVector.getX(),
oldPoint.getY() + translationVector.getY()));
new Point2D.Double(roundedX, roundedY));
}
}

Expand All @@ -207,6 +209,9 @@ public void translate(final Point2D translationVector) {
* @param pivot the x-y coordinates of the rotation point
*/
public void rotate(double radAngle, Point2D pivot) {
if (radAngle == 0)
return;

AffineTransform transform = new AffineTransform();

// Step 1: Translate the shape to the origin (negative of the rotation point)
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/github/creme332/utils/RequestFocusListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.github.creme332.utils;

import java.awt.Component;
import java.awt.Window;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.SwingUtilities;

/**
* Focuses a component in a JOptionPane. By default, it is not possible to
* request focus to a component inside a JOptionPane.
* Reference:
* https://bugs.openjdk.org/browse/JDK-5018574?focusedId=12217314&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-12217314
*
*/
public class RequestFocusListener implements HierarchyListener {

@Override
public void hierarchyChanged(HierarchyEvent e) {
final Component c = e.getComponent();
if (c.isShowing() && (e.getChangeFlags() &
HierarchyEvent.SHOWING_CHANGED) != 0) {
Window toplevel = SwingUtilities.getWindowAncestor(c);
toplevel.addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
c.requestFocus();
}
});

}
}
}

0 comments on commit ba135fa

Please sign in to comment.