| /* |
| |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| */ |
| package org.apache.batik.anim; |
| |
| import java.awt.geom.Point2D; |
| |
| import org.apache.batik.anim.dom.AnimatableElement; |
| import org.apache.batik.anim.timing.TimedElement; |
| import org.apache.batik.anim.values.AnimatableAngleValue; |
| import org.apache.batik.anim.values.AnimatableMotionPointValue; |
| import org.apache.batik.anim.values.AnimatableValue; |
| import org.apache.batik.ext.awt.geom.Cubic; |
| import org.apache.batik.ext.awt.geom.ExtendedGeneralPath; |
| import org.apache.batik.ext.awt.geom.ExtendedPathIterator; |
| import org.apache.batik.ext.awt.geom.PathLength; |
| import org.apache.batik.util.SMILConstants; |
| |
| /** |
| * An animation class for 'animateMotion' animations. |
| * |
| * @author <a href="mailto:cam%40mcc%2eid%2eau">Cameron McCormack</a> |
| * @version $Id$ |
| */ |
| public class MotionAnimation extends InterpolatingAnimation { |
| |
| /** |
| * The path that describes the motion. |
| */ |
| protected ExtendedGeneralPath path; |
| |
| /** |
| * The path length calculation object. |
| */ |
| protected PathLength pathLength; |
| |
| /** |
| * The points defining the distance along the path that the |
| * keyTimes apply. |
| */ |
| protected float[] keyPoints; |
| |
| /** |
| * Whether automatic rotation should be performed. |
| */ |
| protected boolean rotateAuto; |
| |
| /** |
| * Whether the automatic rotation should be reversed. |
| */ |
| protected boolean rotateAutoReverse; |
| |
| /** |
| * The angle of rotation (in radians) to use when automatic rotation is |
| * not being used. |
| */ |
| protected float rotateAngle; |
| |
| /** |
| * Creates a new MotionAnimation. |
| */ |
| public MotionAnimation(TimedElement timedElement, |
| AnimatableElement animatableElement, |
| int calcMode, |
| float[] keyTimes, |
| float[] keySplines, |
| boolean additive, |
| boolean cumulative, |
| AnimatableValue[] values, |
| AnimatableValue from, |
| AnimatableValue to, |
| AnimatableValue by, |
| ExtendedGeneralPath path, |
| float[] keyPoints, |
| boolean rotateAuto, |
| boolean rotateAutoReverse, |
| float rotateAngle, |
| short rotateAngleUnit) { |
| super(timedElement, animatableElement, calcMode, keyTimes, keySplines, |
| additive, cumulative); |
| this.rotateAuto = rotateAuto; |
| this.rotateAutoReverse = rotateAutoReverse; |
| this.rotateAngle = AnimatableAngleValue.rad(rotateAngle, rotateAngleUnit); |
| |
| if (path == null) { |
| path = new ExtendedGeneralPath(); |
| if (values == null || values.length == 0) { |
| if (from != null) { |
| AnimatableMotionPointValue fromPt = (AnimatableMotionPointValue) from; |
| float x = fromPt.getX(); |
| float y = fromPt.getY(); |
| path.moveTo(x, y); |
| if (to != null) { |
| AnimatableMotionPointValue toPt = (AnimatableMotionPointValue) to; |
| path.lineTo(toPt.getX(), toPt.getY()); |
| } else if (by != null) { |
| AnimatableMotionPointValue byPt = (AnimatableMotionPointValue) by; |
| path.lineTo(x + byPt.getX(), y + byPt.getY()); |
| } else { |
| throw timedElement.createException |
| ("values.to.by.path.missing", |
| new Object[] { null }); |
| } |
| } else { |
| if (to != null) { |
| AnimatableMotionPointValue unPt = (AnimatableMotionPointValue) |
| animatableElement.getUnderlyingValue(); |
| AnimatableMotionPointValue toPt = (AnimatableMotionPointValue) to; |
| path.moveTo(unPt.getX(), unPt.getY()); |
| path.lineTo(toPt.getX(), toPt.getY()); |
| this.cumulative = false; |
| } else if (by != null) { |
| AnimatableMotionPointValue byPt = (AnimatableMotionPointValue) by; |
| path.moveTo(0, 0); |
| path.lineTo(byPt.getX(), byPt.getY()); |
| this.additive = true; |
| } else { |
| throw timedElement.createException |
| ("values.to.by.path.missing", |
| new Object[] { null }); |
| } |
| } |
| } else { |
| AnimatableMotionPointValue pt = (AnimatableMotionPointValue) values[0]; |
| path.moveTo(pt.getX(), pt.getY()); |
| for (int i = 1; i < values.length; i++) { |
| pt = (AnimatableMotionPointValue) values[i]; |
| path.lineTo(pt.getX(), pt.getY()); |
| } |
| } |
| } |
| this.path = path; |
| pathLength = new PathLength(path); |
| int segments = 0; |
| ExtendedPathIterator epi = path.getExtendedPathIterator(); |
| while (!epi.isDone()) { |
| int type = epi.currentSegment(); |
| if (type != ExtendedPathIterator.SEG_MOVETO) { |
| segments++; |
| } |
| epi.next(); |
| } |
| |
| int count = keyPoints == null ? segments + 1 : keyPoints.length; |
| float totalLength = pathLength.lengthOfPath(); |
| if (this.keyTimes != null && calcMode != CALC_MODE_PACED) { |
| if (this.keyTimes.length != count) { |
| throw timedElement.createException |
| ("attribute.malformed", |
| new Object[] { null, |
| SMILConstants.SMIL_KEY_TIMES_ATTRIBUTE }); |
| } |
| } else { |
| if (calcMode == CALC_MODE_LINEAR || calcMode == CALC_MODE_SPLINE) { |
| this.keyTimes = new float[count]; |
| for (int i = 0; i < count; i++) { |
| this.keyTimes[i] = (float) i / (count - 1); |
| } |
| } else if (calcMode == CALC_MODE_DISCRETE) { |
| this.keyTimes = new float[count]; |
| for (int i = 0; i < count; i++) { |
| this.keyTimes[i] = (float) i / count; |
| } |
| } else { // CALC_MODE_PACED |
| // This corrects the keyTimes to be paced, so from now on |
| // it can be considered the same as CALC_MODE_LINEAR. |
| epi = path.getExtendedPathIterator(); |
| this.keyTimes = new float[count]; |
| int j = 0; |
| for (int i = 0; i < count - 1; i++) { |
| while (epi.currentSegment() == |
| ExtendedPathIterator.SEG_MOVETO) { |
| j++; |
| epi.next(); |
| } |
| this.keyTimes[i] = |
| pathLength.getLengthAtSegment(j) / totalLength; |
| j++; |
| epi.next(); |
| } |
| this.keyTimes[count - 1] = 1f; |
| } |
| } |
| |
| if (keyPoints != null) { |
| if (keyPoints.length != this.keyTimes.length) { |
| throw timedElement.createException |
| ("attribute.malformed", |
| new Object[] { null, |
| SMILConstants.SMIL_KEY_POINTS_ATTRIBUTE }); |
| } |
| } else { |
| epi = path.getExtendedPathIterator(); |
| keyPoints = new float[count]; |
| int j = 0; |
| for (int i = 0; i < count - 1; i++) { |
| while (epi.currentSegment() == |
| ExtendedPathIterator.SEG_MOVETO) { |
| j++; |
| epi.next(); |
| } |
| keyPoints[i] = pathLength.getLengthAtSegment(j) / totalLength; |
| j++; |
| epi.next(); |
| } |
| keyPoints[count - 1] = 1f; |
| } |
| this.keyPoints = keyPoints; |
| } |
| |
| /** |
| * Called when the element is sampled at the given unit time. This updates |
| * the {@link #value} of the animation if active. |
| */ |
| protected void sampledAtUnitTime(float unitTime, int repeatIteration) { |
| AnimatableValue value, accumulation; |
| float interpolation = 0; |
| if (unitTime != 1) { |
| int keyTimeIndex = 0; |
| while (keyTimeIndex < keyTimes.length - 1 |
| && unitTime >= keyTimes[keyTimeIndex + 1]) { |
| keyTimeIndex++; |
| } |
| if (keyTimeIndex == keyTimes.length - 1 && calcMode == CALC_MODE_DISCRETE) { |
| keyTimeIndex = keyTimes.length - 2; |
| interpolation = 1; |
| } else { |
| if (calcMode == CALC_MODE_LINEAR || calcMode == CALC_MODE_PACED |
| || calcMode == CALC_MODE_SPLINE) { |
| if (unitTime == 0) { |
| interpolation = 0; |
| } else { |
| interpolation = (unitTime - keyTimes[keyTimeIndex]) |
| / (keyTimes[keyTimeIndex + 1] - keyTimes[keyTimeIndex]); |
| } |
| if (calcMode == CALC_MODE_SPLINE && unitTime != 0) { |
| // XXX This could be done better, e.g. with |
| // Newton-Raphson. |
| Cubic c = keySplineCubics[keyTimeIndex]; |
| float tolerance = 0.001f; |
| float min = 0; |
| float max = 1; |
| Point2D.Double p; |
| for (;;) { |
| float t = (min + max) / 2; |
| p = c.eval(t); |
| double x = p.getX(); |
| if (Math.abs(x - interpolation) < tolerance) { |
| break; |
| } |
| if (x < interpolation) { |
| min = t; |
| } else { |
| max = t; |
| } |
| } |
| interpolation = (float) p.getY(); |
| } |
| } |
| } |
| float point = keyPoints[keyTimeIndex]; |
| if (interpolation != 0) { |
| point += interpolation * |
| (keyPoints[keyTimeIndex + 1] - keyPoints[keyTimeIndex]); |
| } |
| point *= pathLength.lengthOfPath(); |
| Point2D p = pathLength.pointAtLength(point); |
| float ang; |
| if (rotateAuto) { |
| ang = pathLength.angleAtLength(point); |
| if (rotateAutoReverse) { |
| ang += Math.PI; |
| } |
| } else { |
| ang = rotateAngle; |
| } |
| value = new AnimatableMotionPointValue(null, (float) p.getX(), |
| (float) p.getY(), ang); |
| } else { |
| Point2D p = pathLength.pointAtLength(pathLength.lengthOfPath()); |
| float ang; |
| if (rotateAuto) { |
| ang = pathLength.angleAtLength(pathLength.lengthOfPath()); |
| if (rotateAutoReverse) { |
| ang += Math.PI; |
| } |
| } else { |
| ang = rotateAngle; |
| } |
| value = new AnimatableMotionPointValue(null, (float) p.getX(), |
| (float) p.getY(), ang); |
| } |
| if (cumulative) { |
| Point2D p = pathLength.pointAtLength(pathLength.lengthOfPath()); |
| float ang; |
| if (rotateAuto) { |
| ang = pathLength.angleAtLength(pathLength.lengthOfPath()); |
| if (rotateAutoReverse) { |
| ang += Math.PI; |
| } |
| } else { |
| ang = rotateAngle; |
| } |
| accumulation = new AnimatableMotionPointValue(null, (float) p.getX(), |
| (float) p.getY(), ang); |
| } else { |
| accumulation = null; |
| } |
| |
| this.value = value.interpolate(this.value, null, interpolation, |
| accumulation, repeatIteration); |
| if (this.value.hasChanged()) { |
| markDirty(); |
| } |
| } |
| } |