blob: 3f48d92b91eabd77418000f1bba3feb192ba7018 [file] [log] [blame]
/*
* Copyright (C) 2004, 2005 Nikolas Zimmermann <zimmermann@kde.org>
* Copyright (C) 2004, 2005, 2006, 2007 Rob Buis <buis@kde.org>
* Copyright (C) 2007 Eric Seidel <eric@webkit.org>
* Copyright (C) 2008 Apple Inc. All rights reserved.
* Copyright (C) 2009 Cameron McCormack <cam@mcc.id.au>
* Copyright (C) Research In Motion Limited 2010. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "core/svg/SVGAnimationElement.h"
#include "bindings/core/v8/ExceptionState.h"
#include "core/SVGNames.h"
#include "core/frame/UseCounter.h"
#include "core/svg/SVGAnimateElement.h"
#include "core/svg/SVGParserUtilities.h"
#include "wtf/MathExtras.h"
namespace blink {
SVGAnimationElement::SVGAnimationElement(const QualifiedName& tagName,
Document& document)
: SVGSMILElement(tagName, document),
m_animationValid(false),
m_calcMode(CalcModeLinear),
m_animationMode(NoAnimation) {
ASSERT(RuntimeEnabledFeatures::smilEnabled());
UseCounter::count(document, UseCounter::SVGAnimationElement);
}
bool SVGAnimationElement::parseValues(const String& value,
Vector<String>& result) {
// Per the SMIL specification, leading and trailing white space, and white
// space before and after semicolon separators, is allowed and will be
// ignored.
// http://www.w3.org/TR/SVG11/animate.html#ValuesAttribute
result.clear();
Vector<String> parseList;
value.split(';', true, parseList);
unsigned last = parseList.size() - 1;
for (unsigned i = 0; i <= last; ++i) {
if (parseList[i].isEmpty()) {
// Tolerate trailing ';'
if (i < last)
goto fail;
} else {
parseList[i] = parseList[i].stripWhiteSpace();
result.push_back(parseList[i]);
}
}
return true;
fail:
result.clear();
return false;
}
static bool parseKeyTimes(const String& string,
Vector<float>& result,
bool verifyOrder) {
result.clear();
Vector<String> parseList;
string.split(';', true, parseList);
for (unsigned n = 0; n < parseList.size(); ++n) {
String timeString = parseList[n].stripWhiteSpace();
bool ok;
float time = timeString.toFloat(&ok);
if (!ok || time < 0 || time > 1)
goto fail;
if (verifyOrder) {
if (!n) {
if (time)
goto fail;
} else if (time < result.back()) {
goto fail;
}
}
result.push_back(time);
}
return true;
fail:
result.clear();
return false;
}
template <typename CharType>
static bool parseKeySplinesInternal(const String& string,
Vector<gfx::CubicBezier>& result) {
const CharType* ptr = string.getCharacters<CharType>();
const CharType* end = ptr + string.length();
skipOptionalSVGSpaces(ptr, end);
while (ptr < end) {
float posA = 0;
if (!parseNumber(ptr, end, posA))
return false;
float posB = 0;
if (!parseNumber(ptr, end, posB))
return false;
float posC = 0;
if (!parseNumber(ptr, end, posC))
return false;
float posD = 0;
if (!parseNumber(ptr, end, posD, DisallowWhitespace))
return false;
skipOptionalSVGSpaces(ptr, end);
if (ptr < end && *ptr == ';')
ptr++;
skipOptionalSVGSpaces(ptr, end);
result.push_back(gfx::CubicBezier(posA, posB, posC, posD));
}
return ptr == end;
}
static bool parseKeySplines(const String& string,
Vector<gfx::CubicBezier>& result) {
result.clear();
if (string.isEmpty())
return true;
bool parsed = true;
if (string.is8Bit())
parsed = parseKeySplinesInternal<LChar>(string, result);
else
parsed = parseKeySplinesInternal<UChar>(string, result);
if (!parsed) {
result.clear();
return false;
}
return true;
}
void SVGAnimationElement::parseAttribute(
const AttributeModificationParams& params) {
const QualifiedName& name = params.name;
if (name == SVGNames::valuesAttr) {
if (!parseValues(params.newValue, m_values)) {
reportAttributeParsingError(SVGParseStatus::ParsingFailed, name,
params.newValue);
return;
}
updateAnimationMode();
return;
}
if (name == SVGNames::keyTimesAttr) {
if (!parseKeyTimes(params.newValue, m_keyTimes, true)) {
reportAttributeParsingError(SVGParseStatus::ParsingFailed, name,
params.newValue);
}
return;
}
if (name == SVGNames::keyPointsAttr) {
if (isSVGAnimateMotionElement(*this)) {
// This is specified to be an animateMotion attribute only but it is
// simpler to put it here where the other timing calculatations are.
if (!parseKeyTimes(params.newValue, m_keyPoints, false)) {
reportAttributeParsingError(SVGParseStatus::ParsingFailed, name,
params.newValue);
}
}
return;
}
if (name == SVGNames::keySplinesAttr) {
if (!parseKeySplines(params.newValue, m_keySplines)) {
reportAttributeParsingError(SVGParseStatus::ParsingFailed, name,
params.newValue);
}
return;
}
if (name == SVGNames::calcModeAttr) {
setCalcMode(params.newValue);
return;
}
if (name == SVGNames::fromAttr || name == SVGNames::toAttr ||
name == SVGNames::byAttr) {
updateAnimationMode();
return;
}
SVGSMILElement::parseAttribute(params);
}
void SVGAnimationElement::svgAttributeChanged(const QualifiedName& attrName) {
if (attrName == SVGNames::valuesAttr || attrName == SVGNames::byAttr ||
attrName == SVGNames::fromAttr || attrName == SVGNames::toAttr ||
attrName == SVGNames::calcModeAttr ||
attrName == SVGNames::keySplinesAttr ||
attrName == SVGNames::keyPointsAttr ||
attrName == SVGNames::keyTimesAttr) {
animationAttributeChanged();
return;
}
SVGSMILElement::svgAttributeChanged(attrName);
}
void SVGAnimationElement::animationAttributeChanged() {
// Assumptions may not hold after an attribute change.
m_animationValid = false;
m_lastValuesAnimationFrom = String();
m_lastValuesAnimationTo = String();
setInactive();
}
float SVGAnimationElement::getStartTime(ExceptionState& exceptionState) const {
SMILTime startTime = intervalBegin();
if (!startTime.isFinite()) {
exceptionState.throwDOMException(InvalidStateError, "No current interval.");
return 0;
}
return clampTo<float>(startTime.value());
}
float SVGAnimationElement::getCurrentTime() const {
return clampTo<float>(elapsed().value());
}
float SVGAnimationElement::getSimpleDuration(
ExceptionState& exceptionState) const {
SMILTime duration = simpleDuration();
if (!duration.isFinite()) {
exceptionState.throwDOMException(NotSupportedError,
"No simple duration defined.");
return 0;
}
return clampTo<float>(duration.value());
}
void SVGAnimationElement::beginElementAt(float offset) {
DCHECK(std::isfinite(offset));
addInstanceTime(Begin, elapsed() + offset, SMILTimeWithOrigin::ScriptOrigin);
}
void SVGAnimationElement::endElementAt(float offset) {
DCHECK(std::isfinite(offset));
addInstanceTime(End, elapsed() + offset, SMILTimeWithOrigin::ScriptOrigin);
}
void SVGAnimationElement::updateAnimationMode() {
// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#AnimFuncValues
if (hasAttribute(SVGNames::valuesAttr))
setAnimationMode(ValuesAnimation);
else if (!toValue().isEmpty())
setAnimationMode(fromValue().isEmpty() ? ToAnimation : FromToAnimation);
else if (!byValue().isEmpty())
setAnimationMode(fromValue().isEmpty() ? ByAnimation : FromByAnimation);
else
setAnimationMode(NoAnimation);
}
void SVGAnimationElement::setCalcMode(const AtomicString& calcMode) {
DEFINE_STATIC_LOCAL(const AtomicString, discrete, ("discrete"));
DEFINE_STATIC_LOCAL(const AtomicString, linear, ("linear"));
DEFINE_STATIC_LOCAL(const AtomicString, paced, ("paced"));
DEFINE_STATIC_LOCAL(const AtomicString, spline, ("spline"));
if (calcMode == discrete) {
UseCounter::count(document(), UseCounter::SVGCalcModeDiscrete);
setCalcMode(CalcModeDiscrete);
} else if (calcMode == linear) {
if (isSVGAnimateMotionElement(*this))
UseCounter::count(document(), UseCounter::SVGCalcModeLinear);
// else linear is the default.
setCalcMode(CalcModeLinear);
} else if (calcMode == paced) {
if (!isSVGAnimateMotionElement(*this))
UseCounter::count(document(), UseCounter::SVGCalcModePaced);
// else paced is the default.
setCalcMode(CalcModePaced);
} else if (calcMode == spline) {
UseCounter::count(document(), UseCounter::SVGCalcModeSpline);
setCalcMode(CalcModeSpline);
} else
setCalcMode(isSVGAnimateMotionElement(*this) ? CalcModePaced
: CalcModeLinear);
}
String SVGAnimationElement::toValue() const {
return fastGetAttribute(SVGNames::toAttr);
}
String SVGAnimationElement::byValue() const {
return fastGetAttribute(SVGNames::byAttr);
}
String SVGAnimationElement::fromValue() const {
return fastGetAttribute(SVGNames::fromAttr);
}
bool SVGAnimationElement::isAdditive() {
DEFINE_STATIC_LOCAL(const AtomicString, sum, ("sum"));
const AtomicString& value = fastGetAttribute(SVGNames::additiveAttr);
return value == sum || getAnimationMode() == ByAnimation;
}
bool SVGAnimationElement::isAccumulated() const {
DEFINE_STATIC_LOCAL(const AtomicString, sum, ("sum"));
const AtomicString& value = fastGetAttribute(SVGNames::accumulateAttr);
return value == sum && getAnimationMode() != ToAnimation;
}
void SVGAnimationElement::calculateKeyTimesForCalcModePaced() {
ASSERT(getCalcMode() == CalcModePaced);
ASSERT(getAnimationMode() == ValuesAnimation);
unsigned valuesCount = m_values.size();
ASSERT(valuesCount >= 1);
if (valuesCount == 1)
return;
// FIXME, webkit.org/b/109010: m_keyTimes should not be modified in this
// function.
m_keyTimes.clear();
Vector<float> keyTimesForPaced;
float totalDistance = 0;
keyTimesForPaced.push_back(0);
for (unsigned n = 0; n < valuesCount - 1; ++n) {
// Distance in any units
float distance = calculateDistance(m_values[n], m_values[n + 1]);
if (distance < 0)
return;
totalDistance += distance;
keyTimesForPaced.push_back(distance);
}
if (!totalDistance)
return;
// Normalize.
for (unsigned n = 1; n < keyTimesForPaced.size() - 1; ++n)
keyTimesForPaced[n] =
keyTimesForPaced[n - 1] + keyTimesForPaced[n] / totalDistance;
keyTimesForPaced[keyTimesForPaced.size() - 1] = 1;
// Use key times calculated based on pacing instead of the user provided ones.
m_keyTimes = keyTimesForPaced;
}
static inline double solveEpsilon(double duration) {
return 1 / (200 * duration);
}
unsigned SVGAnimationElement::calculateKeyTimesIndex(float percent) const {
unsigned index;
unsigned keyTimesCount = m_keyTimes.size();
// For linear and spline animations, the last value must be '1'. In those
// cases we don't need to consider the last value, since |percent| is never
// greater than one.
if (keyTimesCount && getCalcMode() != CalcModeDiscrete)
keyTimesCount--;
for (index = 1; index < keyTimesCount; ++index) {
if (m_keyTimes[index] > percent)
break;
}
return --index;
}
float SVGAnimationElement::calculatePercentForSpline(
float percent,
unsigned splineIndex) const {
ASSERT(getCalcMode() == CalcModeSpline);
SECURITY_DCHECK(splineIndex < m_keySplines.size());
gfx::CubicBezier bezier = m_keySplines[splineIndex];
SMILTime duration = simpleDuration();
if (!duration.isFinite())
duration = 100.0;
return clampTo<float>(
bezier.SolveWithEpsilon(percent, solveEpsilon(duration.value())));
}
float SVGAnimationElement::calculatePercentFromKeyPoints(float percent) const {
ASSERT(!m_keyPoints.isEmpty());
ASSERT(getCalcMode() != CalcModePaced);
ASSERT(m_keyTimes.size() > 1);
ASSERT(m_keyPoints.size() == m_keyTimes.size());
if (percent == 1)
return m_keyPoints[m_keyPoints.size() - 1];
unsigned index = calculateKeyTimesIndex(percent);
float fromKeyPoint = m_keyPoints[index];
if (getCalcMode() == CalcModeDiscrete)
return fromKeyPoint;
ASSERT(index + 1 < m_keyTimes.size());
float fromPercent = m_keyTimes[index];
float toPercent = m_keyTimes[index + 1];
float toKeyPoint = m_keyPoints[index + 1];
float keyPointPercent = (percent - fromPercent) / (toPercent - fromPercent);
if (getCalcMode() == CalcModeSpline) {
ASSERT(m_keySplines.size() == m_keyPoints.size() - 1);
keyPointPercent = calculatePercentForSpline(keyPointPercent, index);
}
return (toKeyPoint - fromKeyPoint) * keyPointPercent + fromKeyPoint;
}
float SVGAnimationElement::calculatePercentForFromTo(float percent) const {
if (getCalcMode() == CalcModeDiscrete && m_keyTimes.size() == 2)
return percent > m_keyTimes[1] ? 1 : 0;
return percent;
}
void SVGAnimationElement::currentValuesFromKeyPoints(float percent,
float& effectivePercent,
String& from,
String& to) const {
ASSERT(!m_keyPoints.isEmpty());
ASSERT(m_keyPoints.size() == m_keyTimes.size());
ASSERT(getCalcMode() != CalcModePaced);
effectivePercent = calculatePercentFromKeyPoints(percent);
unsigned index =
effectivePercent == 1
? m_values.size() - 2
: static_cast<unsigned>(effectivePercent * (m_values.size() - 1));
from = m_values[index];
to = m_values[index + 1];
}
void SVGAnimationElement::currentValuesForValuesAnimation(
float percent,
float& effectivePercent,
String& from,
String& to) {
unsigned valuesCount = m_values.size();
ASSERT(m_animationValid);
ASSERT(valuesCount >= 1);
if (percent == 1 || valuesCount == 1) {
from = m_values[valuesCount - 1];
to = m_values[valuesCount - 1];
effectivePercent = 1;
return;
}
CalcMode calcMode = this->getCalcMode();
if (isSVGAnimateElement(*this)) {
SVGAnimateElement& animateElement = toSVGAnimateElement(*this);
if (!animateElement.animatedPropertyTypeSupportsAddition())
calcMode = CalcModeDiscrete;
}
if (!m_keyPoints.isEmpty() && calcMode != CalcModePaced)
return currentValuesFromKeyPoints(percent, effectivePercent, from, to);
unsigned keyTimesCount = m_keyTimes.size();
ASSERT(!keyTimesCount || valuesCount == keyTimesCount);
ASSERT(!keyTimesCount || (keyTimesCount > 1 && !m_keyTimes[0]));
unsigned index = calculateKeyTimesIndex(percent);
if (calcMode == CalcModeDiscrete) {
if (!keyTimesCount)
index = static_cast<unsigned>(percent * valuesCount);
from = m_values[index];
to = m_values[index];
effectivePercent = 0;
return;
}
float fromPercent;
float toPercent;
if (keyTimesCount) {
fromPercent = m_keyTimes[index];
toPercent = m_keyTimes[index + 1];
} else {
index = static_cast<unsigned>(floorf(percent * (valuesCount - 1)));
fromPercent = static_cast<float>(index) / (valuesCount - 1);
toPercent = static_cast<float>(index + 1) / (valuesCount - 1);
}
if (index == valuesCount - 1)
--index;
from = m_values[index];
to = m_values[index + 1];
ASSERT(toPercent > fromPercent);
effectivePercent = (percent - fromPercent) / (toPercent - fromPercent);
if (calcMode == CalcModeSpline) {
ASSERT(m_keySplines.size() == m_values.size() - 1);
effectivePercent = calculatePercentForSpline(effectivePercent, index);
}
}
void SVGAnimationElement::startedActiveInterval() {
m_animationValid = false;
if (!isValid() || !hasValidTarget())
return;
// These validations are appropriate for all animation modes.
if (fastHasAttribute(SVGNames::keyPointsAttr) &&
m_keyPoints.size() != m_keyTimes.size())
return;
AnimationMode animationMode = this->getAnimationMode();
CalcMode calcMode = this->getCalcMode();
if (calcMode == CalcModeSpline) {
unsigned splinesCount = m_keySplines.size();
if (!splinesCount || (fastHasAttribute(SVGNames::keyPointsAttr) &&
m_keyPoints.size() - 1 != splinesCount) ||
(animationMode == ValuesAnimation &&
m_values.size() - 1 != splinesCount) ||
(fastHasAttribute(SVGNames::keyTimesAttr) &&
m_keyTimes.size() - 1 != splinesCount))
return;
}
String from = fromValue();
String to = toValue();
String by = byValue();
if (animationMode == NoAnimation)
return;
if ((animationMode == FromToAnimation || animationMode == FromByAnimation ||
animationMode == ToAnimation || animationMode == ByAnimation) &&
(fastHasAttribute(SVGNames::keyPointsAttr) &&
fastHasAttribute(SVGNames::keyTimesAttr) &&
(m_keyTimes.size() < 2 || m_keyTimes.size() != m_keyPoints.size())))
return;
if (animationMode == FromToAnimation) {
m_animationValid = calculateFromAndToValues(from, to);
} else if (animationMode == ToAnimation) {
// For to-animations the from value is the current accumulated value from
// lower priority animations.
// The value is not static and is determined during the animation.
m_animationValid = calculateFromAndToValues(emptyString, to);
} else if (animationMode == FromByAnimation) {
m_animationValid = calculateFromAndByValues(from, by);
} else if (animationMode == ByAnimation) {
m_animationValid = calculateFromAndByValues(emptyString, by);
} else if (animationMode == ValuesAnimation) {
m_animationValid =
m_values.size() >= 1 && (calcMode == CalcModePaced ||
!fastHasAttribute(SVGNames::keyTimesAttr) ||
fastHasAttribute(SVGNames::keyPointsAttr) ||
(m_values.size() == m_keyTimes.size())) &&
(calcMode == CalcModeDiscrete || !m_keyTimes.size() ||
m_keyTimes.back() == 1) &&
(calcMode != CalcModeSpline ||
((m_keySplines.size() &&
(m_keySplines.size() == m_values.size() - 1)) ||
m_keySplines.size() == m_keyPoints.size() - 1)) &&
(!fastHasAttribute(SVGNames::keyPointsAttr) ||
(m_keyTimes.size() > 1 && m_keyTimes.size() == m_keyPoints.size()));
if (m_animationValid)
m_animationValid = calculateToAtEndOfDurationValue(m_values.back());
if (calcMode == CalcModePaced && m_animationValid)
calculateKeyTimesForCalcModePaced();
} else if (animationMode == PathAnimation) {
m_animationValid =
calcMode == CalcModePaced ||
!fastHasAttribute(SVGNames::keyPointsAttr) ||
(m_keyTimes.size() > 1 && m_keyTimes.size() == m_keyPoints.size());
}
if (m_animationValid && (isAdditive() || isAccumulated()))
UseCounter::count(&document(), UseCounter::SVGSMILAdditiveAnimation);
}
void SVGAnimationElement::updateAnimation(float percent,
unsigned repeatCount,
SVGSMILElement* resultElement) {
if (!m_animationValid || !targetElement())
return;
float effectivePercent;
CalcMode calcMode = this->getCalcMode();
AnimationMode animationMode = this->getAnimationMode();
if (animationMode == ValuesAnimation) {
String from;
String to;
currentValuesForValuesAnimation(percent, effectivePercent, from, to);
if (from != m_lastValuesAnimationFrom || to != m_lastValuesAnimationTo) {
m_animationValid = calculateFromAndToValues(from, to);
if (!m_animationValid)
return;
m_lastValuesAnimationFrom = from;
m_lastValuesAnimationTo = to;
}
} else if (!m_keyPoints.isEmpty() && calcMode != CalcModePaced)
effectivePercent = calculatePercentFromKeyPoints(percent);
else if (m_keyPoints.isEmpty() && calcMode == CalcModeSpline &&
m_keyTimes.size() > 1)
effectivePercent =
calculatePercentForSpline(percent, calculateKeyTimesIndex(percent));
else if (animationMode == FromToAnimation || animationMode == ToAnimation)
effectivePercent = calculatePercentForFromTo(percent);
else
effectivePercent = percent;
calculateAnimatedValue(effectivePercent, repeatCount, resultElement);
}
} // namespace blink