/* Copyright (C) 2004, 2005 Nikolas Zimmermann 2004, 2005, 2006, 2007 Rob Buis Copyright (C) 2007 Eric Seidel This file is part of the KDE project 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 "config.h" #if ENABLE(SVG) #include "SVGAnimationElement.h" #include "CSSPropertyNames.h" #include "Document.h" #include "FloatConversion.h" #include "SVGParserUtilities.h" #include "SVGSVGElement.h" #include "SVGURIReference.h" #include "TimeScheduler.h" #include "XLinkNames.h" #include #include #include #include using namespace std; namespace WebCore { SVGAnimationElement::SVGAnimationElement(const QualifiedName& tagName, Document* doc) : SVGElement(tagName, doc) , SVGTests() , SVGExternalResourcesRequired() , m_targetElement(0) , m_connectedToTimer(false) , m_currentTime(0.0) , m_simpleDuration(0.0) , m_fill(FILL_REMOVE) , m_restart(RESTART_ALWAYS) , m_calcMode(CALCMODE_LINEAR) , m_additive(ADDITIVE_REPLACE) , m_accumulate(ACCUMULATE_NONE) , m_attributeType(ATTRIBUTETYPE_AUTO) , m_max(0.0) , m_min(0.0) , m_end(0.0) , m_begin(0.0) , m_repetitions(0) , m_repeatCount(0) { } SVGAnimationElement::~SVGAnimationElement() { } bool SVGAnimationElement::hasValidTarget() const { return targetElement(); } SVGElement* SVGAnimationElement::targetElement() const { if (!m_targetElement) { Node *target = 0; if (!m_href.isEmpty()) { target = document()->getElementById(SVGURIReference::getTarget(m_href)); } else if (parentNode()) { // TODO : do we really need to skip non element nodes? Can that happen at all? target = parentNode(); while (target) { if (target->nodeType() != ELEMENT_NODE) target = target->parentNode(); else break; } } if (target && target->isSVGElement()) m_targetElement = static_cast(target); } return m_targetElement; } float SVGAnimationElement::getEndTime() const { return narrowPrecisionToFloat(m_end); } float SVGAnimationElement::getStartTime() const { return narrowPrecisionToFloat(m_begin); } float SVGAnimationElement::getCurrentTime() const { return narrowPrecisionToFloat(m_currentTime); } float SVGAnimationElement::getSimpleDuration(ExceptionCode&) const { return narrowPrecisionToFloat(m_simpleDuration); } void SVGAnimationElement::parseKeyNumbers(Vector& keyNumbers, const String& value) { float number = 0.0f; const UChar* ptr = value.characters(); const UChar* end = ptr + value.length(); skipOptionalSpaces(ptr, end); while (ptr < end) { if (!parseNumber(ptr, end, number, false)) return; keyNumbers.append(number); if (!skipOptionalSpaces(ptr, end) || *ptr != ';') return; ptr++; skipOptionalSpaces(ptr, end); } } static void parseValues(Vector& values, const String& value) { const UChar* ptr = value.characters(); const UChar* end = ptr + value.length(); skipOptionalSpaces(ptr, end); while (ptr < end) { // SMIL 3.2.2 : Leading and trailing white space, and white space before and after semicolon separators, will be ignored. const UChar* valueStart = ptr; while (ptr < end && *ptr != ';') // careful not to ignore whitespace inside values ptr++; if (ptr == valueStart) break; // walk backwards from the ; to ignore any whitespace const UChar* valueEnd = ptr - 1; while (valueStart < valueEnd && isWhitespace(*valueEnd)) valueEnd--; values.append(String(valueStart, valueEnd - valueStart + 1)); skipOptionalSpacesOrDelimiter(ptr, end, ';'); } } static void parseKeySplines(Vector& keySplines, const String& value) { float number = 0.0f; SVGAnimationElement::KeySpline keySpline; const UChar* ptr = value.characters(); const UChar* end = ptr + value.length(); skipOptionalSpaces(ptr, end); while (ptr < end) { if (!(parseNumber(ptr, end, number, false) && skipOptionalSpaces(ptr, end))) return; keySpline.control1.setX(number); if (!(parseNumber(ptr, end, number, false) && skipOptionalSpaces(ptr, end))) return; keySpline.control1.setY(number); if (!(parseNumber(ptr, end, number, false) && skipOptionalSpaces(ptr, end))) return; keySpline.control2.setX(number); if (!parseNumber(ptr, end, number, false)) return; keySpline.control2.setY(number); keySplines.append(keySpline); if (!skipOptionalSpaces(ptr, end) || *ptr != ';') return; ptr++; skipOptionalSpaces(ptr, end); } } void SVGAnimationElement::parseBeginOrEndValue(double& number, const String& value) { RefPtr valueList = new SVGStringList(); valueList->parse(value, ';'); ExceptionCode ec = 0; for (unsigned int i = 0; i < valueList->numberOfItems(); i++) { String current = valueList->getItem(i, ec); if (current.startsWith("accessKey")) { // Register keyDownEventListener for the character String character = current.substring(current.length() - 2, 1); // FIXME: Not implemented! Supposed to register accessKey character } else if (current.startsWith("wallclock")) { int firstBrace = current.find('('); int secondBrace = current.find(')'); String wallclockValue = current.substring(firstBrace + 1, secondBrace - firstBrace - 2); // FIXME: Not implemented! Supposed to use wallClock value } else if (current.contains('.')) { int dotPosition = current.find('.'); String element = current.substring(0, dotPosition); String clockValue; if (current.contains("begin")) clockValue = current.substring(dotPosition + 6); else if (current.contains("end")) clockValue = current.substring(dotPosition + 4); else if (current.contains("repeat")) clockValue = current.substring(dotPosition + 7); else { // DOM2 Event Reference int plusMinusPosition = -1; if (current.contains('+')) plusMinusPosition = current.find('+'); else if (current.contains('-')) plusMinusPosition = current.find('-'); String event = current.substring(dotPosition + 1, plusMinusPosition - dotPosition - 1); clockValue = current.substring(dotPosition + event.length() + 1); // FIXME: supposed to use DOM Event } } else { number = parseClockValue(current); if (!isIndefinite(number)) number *= 1000.0; // FIXME: supposed to set begin/end time } } } void SVGAnimationElement::parseMappedAttribute(MappedAttribute* attr) { if (attr->name().matches(XLinkNames::hrefAttr)) m_href = attr->value(); else if (attr->name() == SVGNames::attributeNameAttr) m_attributeName = attr->value(); else if (attr->name() == SVGNames::attributeTypeAttr) { if (attr->value() == "CSS") m_attributeType = ATTRIBUTETYPE_CSS; else if (attr->value() == "XML") m_attributeType = ATTRIBUTETYPE_XML; else if (attr->value() == "auto") m_attributeType = ATTRIBUTETYPE_AUTO; } else if (attr->name() == SVGNames::beginAttr) parseBeginOrEndValue(m_begin, attr->value()); else if (attr->name() == SVGNames::endAttr) parseBeginOrEndValue(m_end, attr->value()); else if (attr->name() == SVGNames::durAttr) { m_simpleDuration = parseClockValue(attr->value()); if (!isIndefinite(m_simpleDuration)) m_simpleDuration *= 1000.0; } else if (attr->name() == SVGNames::minAttr) { m_min = parseClockValue(attr->value()); if (!isIndefinite(m_min)) m_min *= 1000.0; } else if (attr->name() == SVGNames::maxAttr) { m_max = parseClockValue(attr->value()); if (!isIndefinite(m_max)) m_max *= 1000.0; } else if (attr->name() == SVGNames::restartAttr) { if (attr->value() == "whenNotActive") m_restart = RESTART_WHENNOTACTIVE; else if (attr->value() == "never") m_restart = RESTART_NEVER; else if (attr->value() == "always") m_restart = RESTART_ALWAYS; } else if (attr->name() == SVGNames::repeatCountAttr) { if (attr->value() == "indefinite") m_repeatCount = DBL_MAX; else m_repeatCount = attr->value().toDouble(); } else if (attr->name() == SVGNames::repeatDurAttr) m_repeatDur = attr->value(); else if (attr->name() == SVGNames::fillAttr) { if (attr->value() == "freeze") m_fill = FILL_FREEZE; else if (attr->value() == "remove") m_fill = FILL_REMOVE; } else if (attr->name() == SVGNames::calcModeAttr) { if (attr->value() == "discrete") m_calcMode = CALCMODE_DISCRETE; else if (attr->value() == "linear") m_calcMode = CALCMODE_LINEAR; else if (attr->value() == "spline") m_calcMode = CALCMODE_SPLINE; else if (attr->value() == "paced") m_calcMode = CALCMODE_PACED; } else if (attr->name() == SVGNames::valuesAttr) { m_values.clear(); parseValues(m_values, attr->value()); } else if (attr->name() == SVGNames::keyTimesAttr) { m_keyTimes.clear(); parseKeyNumbers(m_keyTimes, attr->value()); } else if (attr->name() == SVGNames::keySplinesAttr) { m_keySplines.clear(); parseKeySplines(m_keySplines, attr->value()); } else if (attr->name() == SVGNames::fromAttr) m_from = attr->value(); else if (attr->name() == SVGNames::toAttr) m_to = attr->value(); else if (attr->name() == SVGNames::byAttr) m_by = attr->value(); else if (attr->name() == SVGNames::additiveAttr) { if (attr->value() == "sum") m_additive = ADDITIVE_SUM; else if (attr->value() == "replace") m_additive = ADDITIVE_REPLACE; } else if (attr->name() == SVGNames::accumulateAttr) { if (attr->value() == "sum") m_accumulate = ACCUMULATE_SUM; else if (attr->value() == "none") m_accumulate = ACCUMULATE_NONE; } else { if (SVGTests::parseMappedAttribute(attr)) return; if (SVGExternalResourcesRequired::parseMappedAttribute(attr)) return; SVGElement::parseMappedAttribute(attr); } } double SVGAnimationElement::parseClockValue(const String& data) { DeprecatedString parse = data.deprecatedString().stripWhiteSpace(); if (parse == "indefinite") // Saves some time... return DBL_MAX; double result; int doublePointOne = parse.find(':'); int doublePointTwo = parse.find(':', doublePointOne + 1); if (doublePointOne != -1 && doublePointTwo != -1) { // Spec: "Full clock values" unsigned int hours = parse.mid(0, 2).toUInt(); unsigned int minutes = parse.mid(3, 2).toUInt(); unsigned int seconds = parse.mid(6, 2).toUInt(); unsigned int milliseconds = 0; result = (3600 * hours) + (60 * minutes) + seconds; if (parse.find('.') != -1) { DeprecatedString temp = parse.mid(9, 2); milliseconds = temp.toUInt(); result += (milliseconds * (1 / pow(10.0, int(temp.length())))); } } else if (doublePointOne != -1 && doublePointTwo == -1) { // Spec: "Partial clock values" unsigned int minutes = parse.mid(0, 2).toUInt(); unsigned int seconds = parse.mid(3, 2).toUInt(); unsigned int milliseconds = 0; result = (60 * minutes) + seconds; if (parse.find('.') != -1) { DeprecatedString temp = parse.mid(6, 2); milliseconds = temp.toUInt(); result += (milliseconds * (1 / pow(10.0, int(temp.length())))); } } else { // Spec: "Timecount values" int dotPosition = parse.find('.'); if (parse.endsWith("h")) { if (dotPosition == -1) result = parse.mid(0, parse.length() - 1).toUInt() * 3600; else { result = parse.mid(0, dotPosition).toUInt() * 3600; DeprecatedString temp = parse.mid(dotPosition + 1, parse.length() - dotPosition - 2); result += (3600.0 * temp.toUInt()) * (1 / pow(10.0, int(temp.length()))); } } else if (parse.endsWith("min")) { if (dotPosition == -1) result = parse.mid(0, parse.length() - 3).toUInt() * 60; else { result = parse.mid(0, dotPosition).toUInt() * 60; DeprecatedString temp = parse.mid(dotPosition + 1, parse.length() - dotPosition - 4); result += (60.0 * temp.toUInt()) * (1 / pow(10.0, int(temp.length()))); } } else if (parse.endsWith("ms")) { if (dotPosition == -1) result = parse.mid(0, parse.length() - 2).toUInt() / 1000.0; else { result = parse.mid(0, dotPosition).toUInt() / 1000.0; DeprecatedString temp = parse.mid(dotPosition + 1, parse.length() - dotPosition - 3); result += (temp.toUInt() / 1000.0) * (1 / pow(10.0, int(temp.length()))); } } else if (parse.endsWith("s")) { if (dotPosition == -1) result = parse.mid(0, parse.length() - 1).toUInt(); else { result = parse.mid(0, dotPosition).toUInt(); DeprecatedString temp = parse.mid(dotPosition + 1, parse.length() - dotPosition - 2); result += temp.toUInt() * (1 / pow(10.0, int(temp.length()))); } } else result = parse.toDouble(); } return result; } void SVGAnimationElement::finishedParsing() { ownerSVGElement()->timeScheduler()->addTimer(this, lround(getStartTime())); SVGElement::finishedParsing(); } String SVGAnimationElement::targetAttributeAnimatedValue() const { // FIXME: This method is not entirely correct // It does not properly grab the true "animVal" instead grabs the baseVal (or something very close) if (!targetElement()) return String(); SVGElement* target = targetElement(); SVGStyledElement* styled = 0; if (target && target->isStyled()) styled = static_cast(target); String ret; EAttributeType attributeType = static_cast(m_attributeType); if (attributeType == ATTRIBUTETYPE_AUTO) { attributeType = ATTRIBUTETYPE_XML; // Spec: The implementation should match the attributeName to an attribute // for the target element. The implementation must first search through the // list of CSS properties for a matching property name, and if none is found, // search the default XML namespace for the element. if (styled && styled->style()) { if (styled->style()->getPropertyCSSValue(m_attributeName)) attributeType = ATTRIBUTETYPE_CSS; } } if (attributeType == ATTRIBUTETYPE_CSS) { if (styled && styled->style()) ret = styled->style()->getPropertyValue(m_attributeName); } if (attributeType == ATTRIBUTETYPE_XML || ret.isEmpty()) ret = targetElement()->getAttribute(m_attributeName); return ret; } void SVGAnimationElement::setTargetAttributeAnimatedValue(const String& value) { // FIXME: This method is not entirely correct // It does not properly set the "animVal", rather it sets the "baseVal" SVGAnimationElement::setTargetAttribute(targetElement(), m_attributeName, value, static_cast(m_attributeType)); } void SVGAnimationElement::setTargetAttribute(SVGElement* target, const String& name, const String& value, EAttributeType type) { if (!target || name.isNull() || value.isNull()) return; SVGStyledElement* styled = (target && target->isStyled()) ? static_cast(target) : 0; EAttributeType attributeType = type; if (type == ATTRIBUTETYPE_AUTO) { // Spec: The implementation should match the attributeName to an attribute // for the target element. The implementation must first search through the // list of CSS properties for a matching property name, and if none is found, // search the default XML namespace for the element. if (styled && styled->style() && styled->style()->getPropertyCSSValue(name)) attributeType = ATTRIBUTETYPE_CSS; else attributeType = ATTRIBUTETYPE_XML; } ExceptionCode ec = 0; if (attributeType == ATTRIBUTETYPE_CSS && styled && styled->style()) styled->style()->setProperty(name, value, "", ec); else if (attributeType == ATTRIBUTETYPE_XML) target->setAttribute(name, value, ec); } String SVGAnimationElement::attributeName() const { return m_attributeName; } bool SVGAnimationElement::connectedToTimer() const { return m_connectedToTimer; } bool SVGAnimationElement::isFrozen() const { return (m_fill == FILL_FREEZE); } bool SVGAnimationElement::isAdditive() const { return (m_additive == ADDITIVE_SUM) || (detectAnimationMode() == BY_ANIMATION); } bool SVGAnimationElement::isAccumulated() const { return (m_accumulate == ACCUMULATE_SUM) && (detectAnimationMode() != TO_ANIMATION); } EAnimationMode SVGAnimationElement::detectAnimationMode() const { if (hasAttribute(SVGNames::valuesAttr)) return VALUES_ANIMATION; else if ((!m_from.isEmpty() && !m_to.isEmpty()) || (!m_to.isEmpty())) { // to/from-to animation if (!m_from.isEmpty()) // from-to animation return FROM_TO_ANIMATION; else return TO_ANIMATION; } else if ((m_from.isEmpty() && m_to.isEmpty() && !m_by.isEmpty()) || (!m_from.isEmpty() && !m_by.isEmpty())) { // by/from-by animation if (!m_from.isEmpty()) // from-by animation return FROM_BY_ANIMATION; else return BY_ANIMATION; } return NO_ANIMATION; } double SVGAnimationElement::repeations() const { return m_repetitions; } bool SVGAnimationElement::isIndefinite(double value) { return (value == DBL_MAX); } void SVGAnimationElement::connectTimer() { ASSERT(!m_connectedToTimer); ownerSVGElement()->timeScheduler()->connectIntervalTimer(this); m_connectedToTimer = true; } void SVGAnimationElement::disconnectTimer() { ASSERT(m_connectedToTimer); ownerSVGElement()->timeScheduler()->disconnectIntervalTimer(this); m_connectedToTimer = false; } static double calculateTimePercentage(double elapsedSeconds, double start, double end, double duration, double repetitions) { double percentage = 0.0; double useElapsed = elapsedSeconds - (duration * repetitions); if (duration > 0.0 && end == 0.0) percentage = 1.0 - (((start + duration) - useElapsed) / duration); else if (duration > 0.0 && end != 0.0) { if (duration > end) percentage = 1.0 - (((start + end) - useElapsed) / end); else percentage = 1.0 - (((start + duration) - useElapsed) / duration); } else if (duration == 0.0 && end != 0.0) percentage = 1.0 - (((start + end) - useElapsed) / end); return percentage; } static inline void adjustPercentagePastForKeySplines(const Vector& keySplines, unsigned valueIndex, float& percentagePast) { if (percentagePast == 0.0f) // values at key times need no spline adjustment return; const SVGAnimationElement::KeySpline& keySpline = keySplines[valueIndex]; Path path; path.moveTo(FloatPoint()); path.addBezierCurveTo(keySpline.control1, keySpline.control2, FloatPoint(1.0f, 1.0f)); // FIXME: This needs to use a y-at-x function on path, to compute the y value then multiply percentagePast by that value } void SVGAnimationElement::valueIndexAndPercentagePastForDistance(float distancePercentage, unsigned& valueIndex, float& percentagePast) { // Unspecified: animation elements which do not support CALCMODE_PACED, we just always show the first value valueIndex = 0; percentagePast = 0; } float SVGAnimationElement::calculateTotalDistance() { return 0; } static inline void caculateValueIndexForKeyTimes(float timePercentage, const Vector& keyTimes, unsigned& valueIndex, float& lastKeyTime, float& nextKeyTime) { unsigned keyTimesCountMinusOne = keyTimes.size() - 1; valueIndex = 0; ASSERT(timePercentage >= keyTimes.first()); while ((valueIndex < keyTimesCountMinusOne) && (timePercentage >= keyTimes[valueIndex + 1])) valueIndex++; lastKeyTime = keyTimes[valueIndex]; if (valueIndex < keyTimesCountMinusOne) nextKeyTime = keyTimes[valueIndex + 1]; else nextKeyTime = lastKeyTime; } bool SVGAnimationElement::isValidAnimation() const { EAnimationMode animationMode = detectAnimationMode(); if (!hasValidTarget() || (animationMode == NO_ANIMATION)) return false; if (animationMode == VALUES_ANIMATION) { if (!m_values.size()) return false; if (m_keyTimes.size()) { if ((m_values.size() != m_keyTimes.size()) || (m_keyTimes.first() != 0)) return false; if (((m_calcMode == CALCMODE_SPLINE) || (m_calcMode == CALCMODE_LINEAR)) && (m_keyTimes.last() != 1)) return false; float lastKeyTime = 0; for (unsigned x = 0; x < m_keyTimes.size(); x++) { if (m_keyTimes[x] < lastKeyTime || m_keyTimes[x] > 1) return false; } } if (m_keySplines.size()) { if ((m_values.size() - 1) != m_keySplines.size()) return false; for (unsigned x = 0; x < m_keyTimes.size(); x++) if (m_keyTimes[x] < 0 || m_keyTimes[x] > 1) return false; } } return true; } void SVGAnimationElement::calculateValueIndexAndPercentagePast(float timePercentage, unsigned& valueIndex, float& percentagePast) { ASSERT(timePercentage <= 1.0f); ASSERT(isValidAnimation()); EAnimationMode animationMode = detectAnimationMode(); // to-animations have their own special handling if (animationMode == TO_ANIMATION) return; // paced is special, caculates values based on distance instead of time if (m_calcMode == CALCMODE_PACED) { float totalDistance = calculateTotalDistance(); float distancePercentage = totalDistance * timePercentage; valueIndexAndPercentagePastForDistance(distancePercentage, valueIndex, percentagePast); return; } // Figure out what our current index is based on on time // all further calculations are based on time percentages, to allow unifying keyTimes handling & normal animation float lastKeyTimePercentage = 0; float nextKeyTimePercentage = 0; if (m_keyTimes.size() && (m_keyTimes.size() == m_values.size())) caculateValueIndexForKeyTimes(timePercentage, m_keyTimes, valueIndex, lastKeyTimePercentage, nextKeyTimePercentage); else { unsigned lastPossibleIndex = (m_values.size() ? m_values.size() - 1: 1); unsigned flooredValueIndex = static_cast(timePercentage * lastPossibleIndex); valueIndex = flooredValueIndex; lastKeyTimePercentage = flooredValueIndex / (float)lastPossibleIndex; nextKeyTimePercentage = (flooredValueIndex + 1) / (float)lastPossibleIndex; } // No further caculation is needed if we're exactly on an index. if (timePercentage == lastKeyTimePercentage || lastKeyTimePercentage == nextKeyTimePercentage) { percentagePast = 0.0f; return; } // otherwise we decide what percent after that index if ((m_calcMode == CALCMODE_SPLINE) && (m_keySplines.size() == (m_values.size() - 1))) adjustPercentagePastForKeySplines(m_keySplines, valueIndex, percentagePast); else if (m_calcMode == CALCMODE_DISCRETE) percentagePast = 0.0f; else { // default (and fallback) mode: linear float keyTimeSpan = nextKeyTimePercentage - lastKeyTimePercentage; float timeSinceLastKeyTime = timePercentage - lastKeyTimePercentage; percentagePast = (timeSinceLastKeyTime / keyTimeSpan); } } bool SVGAnimationElement::updateAnimationBaseValueFromElement() { m_baseValue = targetAttributeAnimatedValue(); return true; } void SVGAnimationElement::applyAnimatedValueToElement() { setTargetAttributeAnimatedValue(m_animatedValue); } void SVGAnimationElement::handleTimerEvent(double elapsedSeconds, double timePercentage) { timePercentage = min(timePercentage, 1.0); if (!connectedToTimer()) { connectTimer(); return; } // FIXME: accumulate="sum" will not work w/o code similar to this: // if (isAccumulated() && repeations() != 0.0) // accumulateForRepetitions(m_repetitions); EAnimationMode animationMode = detectAnimationMode(); unsigned valueIndex = 0; float percentagePast = 0; calculateValueIndexAndPercentagePast(narrowPrecisionToFloat(timePercentage), valueIndex, percentagePast); calculateFromAndToValues(animationMode, valueIndex); updateAnimatedValue(animationMode, narrowPrecisionToFloat(timePercentage), valueIndex, percentagePast); if (timePercentage == 1.0) { if ((m_repeatCount > 0 && m_repetitions < m_repeatCount - 1) || isIndefinite(m_repeatCount)) { m_repetitions++; return; } else disconnectTimer(); } } bool SVGAnimationElement::updateAnimatedValueForElapsedSeconds(double elapsedSeconds) { // FIXME: fill="freeze" will not work without saving off the m_stoppedTime in a stop() method and having code similar to this: // if (isStopped()) { // if (m_fill == FILL_FREEZE) // elapsedSeconds = m_stoppedTime; // else // return false; // } // Validate animation timing settings: // #1 (duration > 0) -> fine // #2 (duration <= 0.0 && end > 0) -> fine if ((m_simpleDuration <= 0.0 && m_end <= 0.0) || (isIndefinite(m_simpleDuration) && m_end <= 0.0)) return false; // Ignore dur="0" or dur="-neg" double percentage = calculateTimePercentage(elapsedSeconds, m_begin, m_end, m_simpleDuration, m_repetitions); if (percentage <= 1.0 || connectedToTimer()) handleTimerEvent(elapsedSeconds, percentage); return true; // value was updated, need to apply } } // vim:ts=4:noet #endif // ENABLE(SVG)