package edu.gatech.cs2340.trydent.animation;
import java.util.LinkedList;
import java.util.List;
import edu.gatech.cs2340.trydent.TrydentException;
import edu.gatech.cs2340.trydent.math.BaseVector;
import edu.gatech.cs2340.trydent.math.Orientation;
import edu.gatech.cs2340.trydent.math.Position;
import edu.gatech.cs2340.trydent.math.Scale;
import edu.gatech.cs2340.trydent.math.curve.IndexWrapMode;
import edu.gatech.cs2340.trydent.math.curve.Interpolation;
import edu.gatech.cs2340.trydent.math.curve.SplineCurve;
/**
* Builder class used to construct KeyframeAnimations. This class should not be
* instantiated directly; use KeyframeAnimation.create().
*/
public class KeyframeAnimationBuilder {
private List<Keyframe> keyframes;
private Keyframe buildingFrame;
private double enforcedTotal;
private boolean circular;
KeyframeAnimationBuilder() {
// LinkedList because we will be adding lots of keyframes.
keyframes = new LinkedList<>();
buildingFrame = new Keyframe();
enforcedTotal = 0;
}
/**
* Sets the position of the current keyframe.
*
* @param position
* 2D position
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder setPosition(Position position) {
buildingFrame.setPosition(position);
return this;
}
/**
* Shifts the current keyframe by the given translation.
*
* @param translation
* amount to add to the current keyframe's position
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder moveBy(Position translation) {
buildingFrame.setPosition(buildingFrame.getPosition().add(translation));
return this;
}
/**
* Sets the rotation of the current keyframe.
*
* @param rotation
* rotation in degrees.
* @return the builder for method chaining
*/
public KeyframeAnimationBuilder setRotation(double rotation) {
buildingFrame.setRotation(rotation);
return this;
}
/**
* Rotates the current keyframe.
*
* @param rotation
* amount to rotate in degrees.
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder rotateBy(double rotation) {
buildingFrame.setRotation(buildingFrame.getRotation() + rotation);
return this;
}
/**
* Sets the scale of the current keyframe, with (1,1) being no scale.
*
* @param scale
* the new scale
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder setScale(Scale scale) {
buildingFrame.setScale(scale);
return this;
}
/**
* Scales the current keyframe.
*
* @param scale
* the vector to multiply the current scale by
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder scaleBy(Scale scale) {
buildingFrame.setScale(buildingFrame.getScale().scale(scale));
return this;
}
/**
* Sets the position, rotation, and scale of the current keyframe.
*
* @param orientation
* the new orientation
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder setOrientation(Orientation orientation) {
buildingFrame.setPosition(orientation.getPosition());
buildingFrame.setRotation(orientation.getRotation());
buildingFrame.setScale(orientation.getScale());
return this;
}
/**
* Sets the interpolation strategy used to generate in-between frames. This
* defaults to Interpolation.SMOOTH and for most uses will not need to be
* changed. (Advanced functionality)
*
* @param interpolation
* interpolationg strategy.
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder setInterpolation(Interpolation<BaseVector<?>> interpolation) {
buildingFrame.interpolation = interpolation;
return this;
}
/**
* Adds a new keyframe at the current position, rotation, and scale.
*
* @param duration
* Time in seconds it should take for the animation to get from
* this keyframe to the one after it. For the final keyframe,
* this value is ignored, as the final keyframe has no duration.
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder addKeyframe(double duration) {
buildingFrame.duration = duration;
keyframes.add(buildingFrame);
Keyframe next = new Keyframe();
next.interpolation = buildingFrame.interpolation;
next.setPosition(buildingFrame.getPosition());
next.setRotation(buildingFrame.getRotation());
next.setScale(buildingFrame.getScale());
buildingFrame = next;
return this;
}
/**
* Adds a new keyframe of zero duration at the current position, rotation,
* and scale.
* <p>
* Unless this is the final keyframe in the animation, this call should be
* followed by a call to setTotalDuration(duration), which will
* automatically change the duration of this frame and other zero-duration
* frames to appropriately fill in the remaining time.
*
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder addKeyframe() {
return addKeyframe(0);
}
/**
* Sets the duration for the /TOTAL/ animation, changing the duration of all
* individual keyframes that have been set so far to accomplish this.
*
* If any keyframes have a negative or 0 duration, they will be used to fill
* in the gaps in time, and the other frames will be left alone.
*
* @param duration
* the desired duration for the total animation
* @return the builder, for method chaining
*/
public KeyframeAnimationBuilder setTotalDuration(double duration) {
if (duration <= 0) {
throw new TrydentException("The duration of the animation must be a positive number!");
}
enforcedTotal = duration;
return this;
}
/**
* If true, this animation will be constructed so that it loops back to the
* beginning when it ends.
*
* @param circular
* if true, the animation will loop.
* @return the builder, for method training
*/
public KeyframeAnimationBuilder setAnimationCircular(boolean circular) {
this.circular = circular;
return this;
}
/**
* Builds and returns a KeyframeAnimation with the given keyframes.
*
* @return the constructed KeyframeAnimation using the keyframes previously
* defined.
*/
public KeyframeAnimation build() {
if (keyframes.isEmpty())
throw new TrydentException("Cannot build a keyframe animation without any frames!"
+ " Use addKeyframe(duration) to add a keyframe.");
enforceTotal();
double duration = 0;
int index = 0;
for (Keyframe frame : keyframes) {
if (++index == keyframes.size())
break;
duration += frame.duration;
}
if (keyframes.size() > 1) {
Keyframe last = keyframes.get(keyframes.size() - 1);
if (circular) {
last.duration = 1.0 / duration;
} else {
last.duration = 0;
}
duration += last.duration;
}
if (duration <= 0) {
throw new TrydentException("Total duration of animation must be positive!");
}
KeyframeAnimation animation = new KeyframeAnimation();
animation.duration = duration;
// Piecewise interpolation.
Interpolation<Keyframe> interpolation = new KeyframeInterpolation();
IndexWrapMode bounds = circular ? IndexWrapMode.WRAP : IndexWrapMode.CLAMP;
Keyframe[] points = keyframes.toArray(new Keyframe[keyframes.size()]);
double[] durations = new double[keyframes.size()];
for (int i = 0; i < points.length; i++) {
points[i].duration /= animation.duration;
durations[i] = points[i].duration;
}
animation.frameInterpolator = new SplineCurve<>(interpolation, bounds, points, durations);
return animation;
}
private void enforceTotal() {
if (enforcedTotal <= 0) {
int index = 0;
for (Keyframe frame : keyframes) {
if (++index == keyframes.size())
break;
if (frame.duration < 0) {
throw new TrydentException("Animation contains frames of negative duration,"
+ " and never called setTotalDuration() with a positive number to fix it!");
}
}
return;
}
if (keyframes.size() < 2) {
keyframes.get(0).duration = enforcedTotal;
return;
}
double total = 0;
int timeless = 0;
int index = 0;
for (Keyframe frame : keyframes) {
if (++index == keyframes.size())
break;
if (frame.duration > 0) {
total += frame.duration;
} else {
timeless++;
}
}
if (timeless == 0) {
// Rescale all duration to match total time.
for (Keyframe frame : keyframes) {
frame.duration *= enforcedTotal / total;
}
} else {
if (enforcedTotal > total) {
// Fill in the gaps.
index = 0;
for (Keyframe frame : keyframes) {
if (++index == keyframes.size())
break;
if (frame.duration <= 0) {
frame.duration = (enforcedTotal - total) / timeless;
}
}
} else {
// Rescale and fill in gaps reasonably.
double gapAmount = enforcedTotal / (keyframes.size() - 1);
double frameFactor = (enforcedTotal - gapAmount * timeless) / total;
index = 0;
for (Keyframe frame : keyframes) {
if (++index == keyframes.size())
break;
if (frame.duration <= 0) {
frame.duration = gapAmount;
} else {
frame.duration *= frameFactor;
}
}
}
}
}
}