Add support for animation of stroke-dasharray to nsStyleAnimation. (Bug 523355) r=dholbert sr=bzbarsky
authorL. David Baron <dbaron@dbaron.org>
Wed, 21 Oct 2009 10:17:40 -0400
changeset 34064 8fbbf4665e897807e305ee0fd078653a124da264
parent 34063 573a37f0d13c045c4185f707ab8c67c5119f344c
child 34066 118c6162ccf680c9d7b8e91cbd98fc02a6e50e21
push idunknown
push userunknown
push dateunknown
reviewersdholbert, bzbarsky
bugs523355
milestone1.9.3a1pre
Add support for animation of stroke-dasharray to nsStyleAnimation. (Bug 523355) r=dholbert sr=bzbarsky
layout/style/nsCSSPropList.h
layout/style/nsStyleAnimation.cpp
layout/style/nsStyleAnimation.h
layout/style/test/test_transitions_per_property.html
--- a/layout/style/nsCSSPropList.h
+++ b/layout/style/nsCSSPropList.h
@@ -2937,18 +2937,18 @@ CSS_PROP_SVG(
     stroke-dasharray,
     stroke_dasharray,
     StrokeDasharray,
     CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     SVG,
     mStrokeDasharray,
     eCSSType_ValueList,
     nsnull,
-    CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    CSS_PROP_NO_OFFSET, /* property stored in 2 separate members */
+    eStyleAnimType_Custom)
 CSS_PROP_SVG(
     stroke-dashoffset,
     stroke_dashoffset,
     StrokeDashoffset,
     0,
     SVG,
     mStrokeDashoffset,
     eCSSType_Value,
--- a/layout/style/nsStyleAnimation.cpp
+++ b/layout/style/nsStyleAnimation.cpp
@@ -81,16 +81,43 @@ GetCommonUnit(nsStyleAnimation::Unit aFi
   if (aFirstUnit != aSecondUnit) {
     // NOTE: Some unit-pairings will need special handling,
     // e.g. percent vs coord (bug 520234)
     return nsStyleAnimation::eUnit_Null;
   }
   return aFirstUnit;
 }
 
+// Greatest Common Divisor
+static PRUint32
+gcd(PRUint32 a, PRUint32 b)
+{
+  // Euclid's algorithm; O(N) in the worst case.  (There are better
+  // ways, but we don't need them for stroke-dasharray animation.)
+  NS_ABORT_IF_FALSE(a > 0 && b > 0, "positive numbers expected");
+
+  while (a != b) {
+    if (a > b) {
+      a = a - b;
+    } else {
+      b = b - a;
+    }
+  }
+
+  return a;
+}
+
+// Least Common Multiple
+static PRUint32
+lcm(PRUint32 a, PRUint32 b)
+{
+  // Divide first to reduce overflow risk.
+  return (a / gcd(a, b)) * b;
+}
+
 // CLASS METHODS
 // -------------
 
 PRBool
 nsStyleAnimation::ComputeDistance(const Value& aStartValue,
                                   const Value& aEndValue,
                                   double& aDistance)
 {
@@ -148,46 +175,94 @@ nsStyleAnimation::ComputeDistance(const 
       double diffA = startA - endA;
       double diffR = startR - endR;
       double diffG = startG - endG;
       double diffB = startB - endB;
       aDistance = sqrt(diffA * diffA + diffR * diffR +
                        diffG * diffG + diffB * diffB);
       break;
     }
+    case eUnit_Dasharray: {
+      // NOTE: This produces results on substantially different scales
+      // for length values and percentage values, which might even be
+      // mixed in the same property value.  This means the result isn't
+      // particularly useful for paced animation.
+
+      // Call AddWeighted to make us lists of the same length.
+      Value normValue1, normValue2;
+      if (!AddWeighted(1.0, aStartValue, 0.0, aEndValue, normValue1) ||
+          !AddWeighted(0.0, aStartValue, 1.0, aEndValue, normValue2)) {
+        success = PR_FALSE;
+        break;
+      }
+
+      double distance = 0.0;
+      const nsCSSValueList *list1 = normValue1.GetCSSValueListValue();
+      const nsCSSValueList *list2 = normValue2.GetCSSValueListValue();
+
+      NS_ABORT_IF_FALSE(!list1 == !list2, "lists should be same length");
+      while (list1) {
+        const nsCSSValue &val1 = list1->mValue;
+        const nsCSSValue &val2 = list2->mValue;
+
+        NS_ABORT_IF_FALSE(val1.GetUnit() == val2.GetUnit(),
+                          "unit match should be assured by AddWeighted");
+        double diff;
+        switch (val1.GetUnit()) {
+          case eCSSUnit_Percent:
+            diff = val1.GetPercentValue() - val2.GetPercentValue();
+            break;
+          case eCSSUnit_Number:
+            diff = val1.GetFloatValue() - val2.GetFloatValue();
+            break;
+          default:
+            NS_ABORT_IF_FALSE(PR_FALSE, "unexpected unit");
+            return PR_FALSE;
+        }
+        distance += diff * diff;
+
+        list1 = list1->mNext;
+        list2 = list2->mNext;
+        NS_ABORT_IF_FALSE(!list1 == !list2, "lists should be same length");
+      }
+
+      aDistance = sqrt(distance);
+      break;
+    }
     case eUnit_Shadow: {
       // Call AddWeighted to make us lists of the same length.
       Value normValue1, normValue2;
       if (!AddWeighted(1.0, aStartValue, 0.0, aEndValue, normValue1) ||
           !AddWeighted(0.0, aStartValue, 1.0, aEndValue, normValue2)) {
         success = PR_FALSE;
         break;
       }
 
       const nsCSSValueList *shadow1 = normValue1.GetCSSValueListValue();
       const nsCSSValueList *shadow2 = normValue2.GetCSSValueListValue();
 
       double distance = 0.0f;
-      while (shadow1 && shadow2) {
+      NS_ABORT_IF_FALSE(!shadow1 == !shadow2, "lists should be same length");
+      while (shadow1) {
         nsCSSValue::Array *array1 = shadow1->mValue.GetArrayValue();
         nsCSSValue::Array *array2 = shadow2->mValue.GetArrayValue();
         for (PRUint32 i = 0; i < 4; ++i) {
           NS_ABORT_IF_FALSE(array1->Item(i).GetUnit() == eCSSUnit_Pixel,
                             "unexpected unit");
           NS_ABORT_IF_FALSE(array2->Item(i).GetUnit() == eCSSUnit_Pixel,
                             "unexpected unit");
           double diff = array1->Item(i).GetFloatValue() -
                         array2->Item(i).GetFloatValue();
           distance += diff * diff;
         }
 
-        const nsCSSValue& color1 = array1->Item(4);
-        const nsCSSValue& color2 = array2->Item(4);
-        const nsCSSValue& inset1 = array1->Item(5);
-        const nsCSSValue& inset2 = array2->Item(5);
+        const nsCSSValue &color1 = array1->Item(4);
+        const nsCSSValue &color2 = array2->Item(4);
+        const nsCSSValue &inset1 = array1->Item(5);
+        const nsCSSValue &inset2 = array2->Item(5);
         // There are only two possible states of the inset value:
         //  (1) GetUnit() == eCSSUnit_Null
         //  (2) GetUnit() == eCSSUnit_Enumerated &&
         //      GetIntValue() == NS_STYLE_BOX_SHADOW_INSET
         NS_ABORT_IF_FALSE(color1.GetUnit() == color2.GetUnit() &&
                           inset1 == inset2,
                           "AddWeighted should have failed");
 
@@ -204,18 +279,18 @@ nsStyleAnimation::ComputeDistance(const 
             nsStyleAnimation::ComputeDistance(color1Value, color2Value,
                                               colorDistance);
           NS_ABORT_IF_FALSE(ok, "should not fail");
           distance += colorDistance * colorDistance;
         }
 
         shadow1 = shadow1->mNext;
         shadow2 = shadow2->mNext;
+        NS_ABORT_IF_FALSE(!shadow1 == !shadow2, "lists should be same length");
       }
-      NS_ABORT_IF_FALSE(!shadow1 && !shadow2, "lists of different lengths");
       aDistance = sqrt(distance);
       break;
     }
     case eUnit_Null:
     case eUnit_None:
       success = PR_FALSE;
       break;
     default:
@@ -366,16 +441,74 @@ nsStyleAnimation::AddWeighted(double aCo
         PRUint8 Rres = ClampColor((R1 * aCoeff1 + R2 * aCoeff2) * factor);
         PRUint8 Gres = ClampColor((G1 * aCoeff1 + G2 * aCoeff2) * factor);
         PRUint8 Bres = ClampColor((B1 * aCoeff1 + B2 * aCoeff2) * factor);
         resultColor = NS_RGBA(Rres, Gres, Bres, Ares);
       }
       aResultValue.SetColorValue(resultColor);
       break;
     }
+    case eUnit_Dasharray: {
+      const nsCSSValueList *list1 = aValue1.GetCSSValueListValue();
+      const nsCSSValueList *list2 = aValue2.GetCSSValueListValue();
+
+      PRUint32 len1 = 0, len2 = 0;
+      for (const nsCSSValueList *v = list1; v; v = v->mNext) {
+        ++len1;
+      }
+      for (const nsCSSValueList *v = list2; v; v = v->mNext) {
+        ++len2;
+      }
+      NS_ABORT_IF_FALSE(len1 > 0 && len2 > 0, "unexpected length");
+
+      nsAutoPtr<nsCSSValueList> result;
+      nsCSSValueList **resultTail = getter_Transfers(result);
+      for (PRUint32 i = 0, i_end = lcm(len1, len2); i != i_end; ++i) {
+        const nsCSSValue &v1 = list1->mValue;
+        const nsCSSValue &v2 = list2->mValue;
+        NS_ABORT_IF_FALSE(v1.GetUnit() == eCSSUnit_Number ||
+                          v1.GetUnit() == eCSSUnit_Percent, "unexpected");
+        NS_ABORT_IF_FALSE(v2.GetUnit() == eCSSUnit_Number ||
+                          v2.GetUnit() == eCSSUnit_Percent, "unexpected");
+        if (v1.GetUnit() != v2.GetUnit()) {
+          // Can't animate between lengths and percentages (until calc()).
+          return PR_FALSE;
+        }
+
+        nsCSSValueList *item = new nsCSSValueList;
+        if (!item) {
+          return PR_FALSE;
+        }
+        *resultTail = item;
+        resultTail = &item->mNext;
+
+        if (v1.GetUnit() == eCSSUnit_Number) {
+          item->mValue.SetFloatValue(aCoeff1 * v1.GetFloatValue() +
+                                     aCoeff2 * v2.GetFloatValue(),
+                                     eCSSUnit_Number);
+        } else {
+          NS_ABORT_IF_FALSE(v1.GetUnit() == eCSSUnit_Percent, "unexpected");
+          item->mValue.SetPercentValue(aCoeff1 * v1.GetPercentValue() +
+                                       aCoeff2 * v2.GetPercentValue());
+        }
+
+        list1 = list1->mNext;
+        if (!list1) {
+          list1 = aValue1.GetCSSValueListValue();
+        }
+        list2 = list2->mNext;
+        if (!list2) {
+          list2 = aValue2.GetCSSValueListValue();
+        }
+      }
+
+      aResultValue.SetCSSValueListValue(result.forget(), eUnit_Dasharray,
+                                        PR_TRUE);
+      break;
+    }
     case eUnit_Shadow: {
       // This is implemented according to:
       // http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types-
       // and the third item in the summary of:
       // http://lists.w3.org/Archives/Public/www-style/2009Jul/0050.html
       const nsCSSValueList *shadow1 = aValue1.GetCSSValueListValue();
       const nsCSSValueList *shadow2 = aValue2.GetCSSValueListValue();
       nsAutoPtr<nsCSSValueList> result;
@@ -573,16 +706,17 @@ nsStyleAnimation::UncomputeValue(nsCSSPr
           SetBothValuesTo(val);
       } else {
         NS_ABORT_IF_FALSE(nsCSSProps::kTypeTable[aProperty] == eCSSType_Value,
                           "type mismatch");
         static_cast<nsCSSValue*>(aSpecifiedValue)->
           SetColorValue(aComputedValue.GetColorValue());
       }
       break;
+    case eUnit_Dasharray:
     case eUnit_Shadow:
       NS_ABORT_IF_FALSE(nsCSSProps::kTypeTable[aProperty] ==
                           eCSSType_ValueList, "type mismatch");
       *static_cast<nsCSSValueList**>(aSpecifiedValue) =
         aComputedValue.GetCSSValueListValue();
       break;
     default:
       return PR_FALSE;
@@ -749,16 +883,64 @@ nsStyleAnimation::ExtractComputedValue(n
         #else
           if (!styleOutline->GetOutlineColor(color))
             color = aStyleContext->GetStyleColor()->mColor;
         #endif
           aComputedValue.SetColorValue(color);
           break;
         }
 
+        case eCSSProperty_stroke_dasharray: {
+          const nsStyleSVG *svg = static_cast<const nsStyleSVG*>(styleStruct);
+          NS_ABORT_IF_FALSE((svg->mStrokeDasharray != nsnull) ==
+                            (svg->mStrokeDasharrayLength != 0),
+                            "pointer/length mismatch");
+          if (svg->mStrokeDasharray) {
+            nsAutoPtr<nsCSSValueList> result;
+            nsCSSValueList **resultTail = getter_Transfers(result);
+            for (PRUint32 i = 0, i_end = svg->mStrokeDasharrayLength;
+                 i != i_end; ++i) {
+              nsCSSValueList *item = new nsCSSValueList;
+              if (!item) {
+                return PR_FALSE;
+              }
+              *resultTail = item;
+              resultTail = &item->mNext;
+
+              const nsStyleCoord &coord = svg->mStrokeDasharray[i];
+              nsCSSValue &value = item->mValue;
+              switch (coord.GetUnit()) {
+                case eStyleUnit_Coord:
+                  // Number means the same thing as length; we want to
+                  // animate them the same way.  Normalize both to number
+                  // since it has more accuracy (float vs nscoord).
+                  value.SetFloatValue(nsPresContext::
+                    AppUnitsToFloatCSSPixels(coord.GetCoordValue()),
+                    eCSSUnit_Number);
+                  break;
+                case eStyleUnit_Factor:
+                  value.SetFloatValue(coord.GetFactorValue(),
+                                      eCSSUnit_Number);
+                  break;
+                case eStyleUnit_Percent:
+                  value.SetPercentValue(coord.GetPercentValue());
+                  break;
+                default:
+                  NS_ABORT_IF_FALSE(PR_FALSE, "unexpected unit");
+                  return PR_FALSE;
+              }
+            }
+            aComputedValue.SetCSSValueListValue(result.forget(),
+                                                eUnit_Dasharray, PR_TRUE);
+          } else {
+            aComputedValue.SetNoneValue();
+          }
+          break;
+        }
+
         default:
           NS_ABORT_IF_FALSE(PR_FALSE, "missing property implementation");
           return PR_FALSE;
       };
       return PR_TRUE;
     case eStyleAnimType_Coord:
       return StyleCoordToValue(*static_cast<const nsStyleCoord*>(
         StyleDataAtOffset(styleStruct, ssOffset)), aComputedValue);
@@ -907,16 +1089,17 @@ nsStyleAnimation::Value::operator=(const
       break;
     case eUnit_Percent:
     case eUnit_Float:
       mValue.mFloat = aOther.mValue.mFloat;
       break;
     case eUnit_Color:
       mValue.mColor = aOther.mValue.mColor;
       break;
+    case eUnit_Dasharray:
     case eUnit_Shadow:
       mValue.mCSSValueList = aOther.mValue.mCSSValueList
                                ? aOther.mValue.mCSSValueList->Clone() : nsnull;
   }
 
   return *this;
 }
 
@@ -1011,16 +1194,17 @@ nsStyleAnimation::Value::operator==(cons
       return PR_TRUE;
     case eUnit_Coord:
       return mValue.mCoord == aOther.mValue.mCoord;
     case eUnit_Percent:
     case eUnit_Float:
       return mValue.mFloat == aOther.mValue.mFloat;
     case eUnit_Color:
       return mValue.mColor == aOther.mValue.mColor;
+    case eUnit_Dasharray:
     case eUnit_Shadow:
       return nsCSSValueList::Equal(mValue.mCSSValueList,
                                    aOther.mValue.mCSSValueList);
   }
 
   NS_NOTREACHED("incomplete case");
   return PR_FALSE;
 }
--- a/layout/style/nsStyleAnimation.h
+++ b/layout/style/nsStyleAnimation.h
@@ -78,16 +78,21 @@ public:
   static PRBool Add(Value& aDest, const Value& aValueToAdd,
                     PRUint32 aCount) {
     return AddWeighted(1.0, aDest, aCount, aValueToAdd, aDest);
   }
 
   /**
    * Calculates a measure of 'distance' between two values.
    *
+   * This measure of Distance is guaranteed to be proportional to
+   * portions passed to Interpolate, Add, or AddWeighted.  However, for
+   * some types of Value it may not produce sensible results for paced
+   * animation.
+   *
    * If this method succeeds, the returned distance value is guaranteed to be
    * non-negative.
    *
    * @param aStartValue The start of the interval for which the distance
    *                    should be calculated.
    * @param aEndValue   The end of the interval for which the distance
    *                    should be calculated.
    * @param aDistance   The result of the calculation.
@@ -215,16 +220,17 @@ public:
     eUnit_Null, // not initialized
     eUnit_Normal,
     eUnit_Auto,
     eUnit_None,
     eUnit_Coord,
     eUnit_Percent,
     eUnit_Float,
     eUnit_Color,
+    eUnit_Dasharray, // nsCSSValueList* (never null)
     eUnit_Shadow  // nsCSSValueList* (may be null)
   };
 
   class Value {
   private:
     Unit mUnit;
     union {
       nscoord mCoord;
@@ -297,14 +303,14 @@ public:
     PRBool operator==(const Value& aOther) const;
     PRBool operator!=(const Value& aOther) const
       { return !(*this == aOther); }
 
   private:
     void FreeValue();
 
     static PRBool IsCSSValueListUnit(Unit aUnit) {
-      return aUnit == eUnit_Shadow;
+      return aUnit == eUnit_Dasharray || aUnit == eUnit_Shadow;
     }
   };
 };
 
 #endif
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -90,16 +90,17 @@ var supported_properties = {
     "padding-bottom": [ test_length_transition, test_percent_transition ],
     "padding-left": [ test_length_transition, test_percent_transition ],
     "padding-right": [ test_length_transition, test_percent_transition ],
     "padding-top": [ test_length_transition, test_percent_transition ],
     "right": [ test_length_transition, test_percent_transition ],
     "stop-color": [ test_color_transition ],
     "stop-opacity" : [ test_float_zeroToOne_transition ],
     "stroke": [ test_color_transition ],
+    "stroke-dasharray": [ test_dasharray_transition ],
     "stroke-dashoffset": [ test_length_transition, test_percent_transition ],
     "stroke-miterlimit": [ test_float_aboveOne_transition ],
     "stroke-opacity" : [ test_float_zeroToOne_transition ],
     "stroke-width": [ test_length_transition, test_percent_transition ],
     "text-indent": [ test_length_transition, test_percent_transition ],
     "text-shadow": [ test_shadow_transition ],
     "top": [ test_length_transition, test_percent_transition ],
     "vertical-align": [ test_length_transition, test_percent_transition ],
@@ -315,12 +316,50 @@ function test_shadow_transition(prop) {
   div.style.setProperty(prop, "2px 2px 2px", "");
   is(cs.getPropertyValue(prop), defaultColor + "2px 2px 2px" + spreadStr,
      "shadow-valued property " + prop + ": non-interpolable cases");
   div.style.setProperty(prop, "4px 8px 6px", "");
   is(cs.getPropertyValue(prop), defaultColor + "3px 5px 4px" + spreadStr,
      "shadow-valued property " + prop + ": interpolation without color");
 }
 
+function test_dasharray_transition(prop) {
+  div.style.setProperty("-moz-transition-property", "none", "");
+  div.style.setProperty(prop, "3", "");
+  is(cs.getPropertyValue(prop), "3",
+     "dasharray-valued property " + prop +
+     ": computed value before transition");
+  div.style.setProperty("-moz-transition-property", prop, "");
+  div.style.setProperty(prop, "9px", "");
+  is(cs.getPropertyValue(prop), "6",
+     "dasharray-valued property " + prop + ": interpolation of dasharray");
+  div.style.setProperty(prop, "none", "");
+  is(cs.getPropertyValue(prop), "none",
+     "dasharray-valued property " + prop + ": non-interpolability of none");
+  div.style.setProperty(prop, "6,8px,4,4", "");
+  is(cs.getPropertyValue(prop), "6, 8px, 4, 4",
+     "dasharray-valued property " + prop +
+     ": computed value before transition");
+  div.style.setProperty(prop, "10, 10,10,10px", "");
+  is(cs.getPropertyValue(prop), "8, 9, 7, 7",
+     "dasharray-valued property " + prop + ": interpolation of dasharray");
+  div.style.setProperty(prop, "none", "");
+  is(cs.getPropertyValue(prop), "none",
+     "dasharray-valued property " + prop + ": non-interpolability of none");
+  div.style.setProperty(prop, "4,8,2", "");
+  is(cs.getPropertyValue(prop), "4, 8, 2",
+     "dasharray-valued property " + prop +
+     ": computed value before transition");
+  div.style.setProperty(prop, "2,4,6,8", "");
+  is(cs.getPropertyValue(prop), "3, 6, 4, 6, 5, 3, 5, 8, 2, 4, 7, 5",
+     "dasharray-valued property " + prop + ": interpolation of dasharray");
+  div.style.setProperty(prop, "2,50%,6,8", "");
+  is(cs.getPropertyValue(prop), "2, 50%, 6, 8",
+     "dasharray-valued property " + prop + ": non-interpolability of mixed units");
+  div.style.setProperty(prop, "4,20%,2,2", "");
+  is(cs.getPropertyValue(prop), "3, 35%, 4, 5",
+     "dasharray-valued property " + prop + ": interpolation of dasharray");
+}
+
 </script>
 </pre>
 </body>
 </html>