Bug 896050 - Implement animation of CSS filter property. r=dholbert
authorDirk Schulze <krit@webkit.org>
Wed, 11 Sep 2013 15:24:03 -0700
changeset 159618 4eee1c589bb0fa20cf99b1b01535c69cf4631b43
parent 159617 0be65c045f1321042ea043bfccda97632b019cf8
child 159619 c10263f35b2c8b85691a1611f35af322157ad07f
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdholbert
bugs896050
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 896050 - Implement animation of CSS filter property. r=dholbert
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
@@ -3369,17 +3369,17 @@ CSS_PROP_SVGRESET(
     filter,
     filter,
     Filter,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Custom)
 CSS_PROP_SVGRESET(
     flood-color,
     flood_color,
     FloodColor,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HC,
     nullptr,
--- a/layout/style/nsStyleAnimation.cpp
+++ b/layout/style/nsStyleAnimation.cpp
@@ -243,16 +243,44 @@ ToPrimitive(nsCSSValue::Array* aArray)
 
 inline void
 nscoordToCSSValue(nscoord aCoord, nsCSSValue& aCSSValue)
 {
   aCSSValue.SetFloatValue(nsPresContext::AppUnitsToFloatCSSPixels(aCoord),
                           eCSSUnit_Pixel);
 }
 
+static void
+AppendCSSShadowValue(const nsCSSShadowItem *aShadow,
+                     nsCSSValueList **&aResultTail)
+{
+  NS_ABORT_IF_FALSE(aShadow, "shadow expected");
+
+  // X, Y, Radius, Spread, Color, Inset
+  nsRefPtr<nsCSSValue::Array> arr = nsCSSValue::Array::Create(6);
+  nscoordToCSSValue(aShadow->mXOffset, arr->Item(0));
+  nscoordToCSSValue(aShadow->mYOffset, arr->Item(1));
+  nscoordToCSSValue(aShadow->mRadius, arr->Item(2));
+  // NOTE: This code sometimes stores mSpread: 0 even when
+  // the parser would be required to leave it null.
+  nscoordToCSSValue(aShadow->mSpread, arr->Item(3));
+  if (aShadow->mHasColor) {
+    arr->Item(4).SetColorValue(aShadow->mColor);
+  }
+  if (aShadow->mInset) {
+    arr->Item(5).SetIntValue(NS_STYLE_BOX_SHADOW_INSET,
+                             eCSSUnit_Enumerated);
+  }
+
+  nsCSSValueList *resultItem = new nsCSSValueList;
+  resultItem->mValue.SetArrayValue(arr, eCSSUnit_Array);
+  *aResultTail = resultItem;
+  aResultTail = &resultItem->mNext;
+}
+
 // Like nsStyleCoord::Calc, but with length in float pixels instead of nscoord.
 struct CalcValue {
   float mLength, mPercent;
   bool mHasPercent;
 };
 
 // Requires a canonical calc() value that we generated.
 static CalcValue
@@ -748,16 +776,18 @@ nsStyleAnimation::ComputeDistance(nsCSSP
 
         shadow1 = shadow1->mNext;
         shadow2 = shadow2->mNext;
         NS_ABORT_IF_FALSE(!shadow1 == !shadow2, "lists should be same length");
       }
       aDistance = sqrt(squareDistance);
       return true;
     }
+    case eUnit_Filter:
+      // FIXME: Support paced animations for filter function interpolation.
     case eUnit_Transform: {
       return false;
     }
     case eUnit_BackgroundPosition: {
       const nsCSSValueList *position1 = aStartValue.GetCSSValueListValue();
       const nsCSSValueList *position2 = aEndValue.GetCSSValueListValue();
 
       double squareDistance = 0.0;
@@ -1027,16 +1057,48 @@ AddCSSValuePixelPercentCalc(const uint32
       break;
     default:
       return false;
   }
 
   return true;
 }
 
+static inline float
+GetNumberOrPercent(const nsCSSValue &aValue)
+{
+  nsCSSUnit unit = aValue.GetUnit();
+  NS_ABORT_IF_FALSE(unit == eCSSUnit_Number || unit == eCSSUnit_Percent,
+                    "unexpected unit");
+  return (unit == eCSSUnit_Number) ?
+    aValue.GetFloatValue() : aValue.GetPercentValue();
+}
+
+static inline void
+AddCSSValuePercentNumber(const uint32_t aValueRestrictions,
+                         double aCoeff1, const nsCSSValue &aValue1,
+                         double aCoeff2, const nsCSSValue &aValue2,
+                         nsCSSValue &aResult, float aInitialVal)
+{
+  float n1 = GetNumberOrPercent(aValue1);
+  float n2 = GetNumberOrPercent(aValue2);
+
+  // Rather than interpolating aValue1 and aValue2 directly, we
+  // interpolate their *distances from aInitialVal* (the initial value,
+  // which is either 1 or 0 for "filter" functions).  This matters in
+  // cases where aInitialVal is nonzero and the coefficients don't add
+  // up to 1.  For example, if initialVal is 1, aCoeff1 is 0.5, and
+  // aCoeff2 is 0, then we'll return the value halfway between 1 and
+  // aValue1, rather than the value halfway between 0 and aValue1.
+  // Note that we do something similar in AddTransformScale().
+  float result = (n1 - aInitialVal) * aCoeff1 + (n2 - aInitialVal) * aCoeff2;
+  aResult.SetFloatValue(RestrictValue(aValueRestrictions, result + aInitialVal),
+                        eCSSUnit_Number);
+}
+
 static bool
 AddShadowItems(double aCoeff1, const nsCSSValue &aValue1,
                double aCoeff2, const nsCSSValue &aValue2,
                nsCSSValueList **&aResultTail)
 {
   // X, Y, Radius, Spread, Color, Inset
   NS_ABORT_IF_FALSE(aValue1.GetUnit() == eCSSUnit_Array,
                     "wrong unit");
@@ -1542,16 +1604,131 @@ AddDifferentTransformLists(double aCoeff
 }
 
 static bool
 TransformFunctionsMatch(nsCSSKeyword func1, nsCSSKeyword func2)
 {
   return ToPrimitive(func1) == ToPrimitive(func2);
 }
 
+static bool
+AddFilterFunctionImpl(double aCoeff1, const nsCSSValueList* aList1,
+                      double aCoeff2, const nsCSSValueList* aList2,
+                      nsCSSValueList**& aResultTail)
+{
+  // AddFilterFunction should be our only caller, and it should ensure that both
+  // args are non-null.
+  NS_ABORT_IF_FALSE(aList1, "expected filter list");
+  NS_ABORT_IF_FALSE(aList2, "expected filter list");
+  NS_ABORT_IF_FALSE(aList1->mValue.GetUnit() == eCSSUnit_Function,
+                    "expected function");
+  NS_ABORT_IF_FALSE(aList2->mValue.GetUnit() == eCSSUnit_Function,
+                    "expected function");
+  nsRefPtr<nsCSSValue::Array> a1 = aList1->mValue.GetArrayValue(),
+                              a2 = aList2->mValue.GetArrayValue();
+  nsCSSKeyword filterFunction = a1->Item(0).GetKeywordValue();
+  if (filterFunction != a2->Item(0).GetKeywordValue())
+    return false; // Can't add two filters of different types.
+
+  nsAutoPtr<nsCSSValueList> resultListEntry(new nsCSSValueList);
+  nsCSSValue::Array* result =
+    resultListEntry->mValue.InitFunction(filterFunction, 1);
+
+  // "hue-rotate" is the only filter-function that accepts negative values, and
+  // we don't use this "restrictions" variable in its clause below.
+  const uint32_t restrictions = CSS_PROPERTY_VALUE_NONNEGATIVE;
+  const nsCSSValue& funcArg1 = a1->Item(1);
+  const nsCSSValue& funcArg2 = a2->Item(1);
+  nsCSSValue& resultArg = result->Item(1);
+  float initialVal = 1.0f;
+  switch (filterFunction) {
+    case eCSSKeyword_blur: {
+      nsCSSUnit unit;
+      if (funcArg1.GetUnit() == funcArg2.GetUnit()) {
+        unit = funcArg1.GetUnit();
+      } else {
+        // If units differ, we'll just combine them with calc().
+        unit = eCSSUnit_Calc;
+      }
+      if (!AddCSSValuePixelPercentCalc(restrictions,
+                                       unit,
+                                       aCoeff1, funcArg1,
+                                       aCoeff2, funcArg2,
+                                       resultArg)) {
+        return false;
+      }
+      break;
+    }
+    case eCSSKeyword_grayscale:
+    case eCSSKeyword_invert:
+    case eCSSKeyword_sepia:
+      initialVal = 0.0f;
+    case eCSSKeyword_brightness:
+    case eCSSKeyword_contrast:
+    case eCSSKeyword_opacity:
+    case eCSSKeyword_saturate:
+      AddCSSValuePercentNumber(restrictions,
+                               aCoeff1, funcArg1,
+                               aCoeff2, funcArg2,
+                               resultArg,
+                               initialVal);
+      break;
+    case eCSSKeyword_hue_rotate:
+      AddCSSValueAngle(aCoeff1, funcArg1,
+                       aCoeff2, funcArg2,
+                       resultArg);
+      break;
+    case eCSSKeyword_drop_shadow: {
+      nsCSSValueList* resultShadow = resultArg.SetListValue();
+      nsAutoPtr<nsCSSValueList> shadowValue;
+      nsCSSValueList **shadowTail = getter_Transfers(shadowValue);
+      NS_ABORT_IF_FALSE(!funcArg1.GetListValue()->mNext &&
+                        !funcArg2.GetListValue()->mNext,
+                        "drop-shadow filter func doesn't support lists");
+      if (!AddShadowItems(aCoeff1, funcArg1.GetListValue()->mValue,
+                          aCoeff2, funcArg2.GetListValue()->mValue,
+                          shadowTail)) {
+        return false;
+      }
+      *resultShadow = *shadowValue;
+      break;
+    }
+    default:
+      NS_ABORT_IF_FALSE(false, "unknown filter function");
+      return false;
+  }
+
+  *aResultTail = resultListEntry.forget();
+  aResultTail = &(*aResultTail)->mNext;
+
+  return true;
+}
+
+static bool
+AddFilterFunction(double aCoeff1, const nsCSSValueList* aList1,
+                  double aCoeff2, const nsCSSValueList* aList2,
+                  nsCSSValueList**& aResultTail)
+{
+  NS_ABORT_IF_FALSE(aList1 || aList2,
+                    "one function list item must not be null");
+  // Note that one of our arguments could be null, indicating that
+  // it's the initial value. Rather than adding special null-handling
+  // logic, we just check for null values and replace them with
+  // 0 * the other value. That way, AddFilterFunctionImpl can assume
+  // its args are non-null.
+  if (!aList1) {
+    return AddFilterFunctionImpl(aCoeff2, aList2, 0, aList2, aResultTail);
+  }
+  if (!aList2) {
+    return AddFilterFunctionImpl(aCoeff1, aList1, 0, aList1, aResultTail);
+  }
+
+  return AddFilterFunctionImpl(aCoeff1, aList1, aCoeff2, aList2, aResultTail);
+}
+
 static nsCSSValueList*
 AddTransformLists(double aCoeff1, const nsCSSValueList* aList1,
                   double aCoeff2, const nsCSSValueList* aList2)
 {
   nsAutoPtr<nsCSSValueList> result;
   nsCSSValueList **resultTail = getter_Transfers(result);
 
   do {
@@ -2055,16 +2232,54 @@ nsStyleAnimation::AddWeighted(nsCSSPrope
           }
 
           longShadow = longShadow->mNext;
         }
       }
       aResultValue.SetAndAdoptCSSValueListValue(result.forget(), eUnit_Shadow);
       return true;
     }
+
+    case eUnit_Filter: {
+      const nsCSSValueList *list1 = aValue1.GetCSSValueListValue();
+      const nsCSSValueList *list2 = aValue2.GetCSSValueListValue();
+
+      nsAutoPtr<nsCSSValueList> result;
+      nsCSSValueList **resultTail = getter_Transfers(result);
+      while (list1 || list2) {
+        NS_ABORT_IF_FALSE(!*resultTail,
+          "resultTail isn't pointing to the tail (may leak)");
+        if ((list1 && list1->mValue.GetUnit() != eCSSUnit_Function) ||
+            (list2 && list2->mValue.GetUnit() != eCSSUnit_Function)) {
+          // If we don't have filter-functions, we must have filter-URLs, which
+          // we can't add or interpolate.
+          return false;
+        }
+
+        if (!AddFilterFunction(aCoeff1, list1, aCoeff2, list2, resultTail)) {
+          // filter function mismatch
+          return false;
+        }
+
+        // move to next list items
+        if (list1) {
+          list1 = list1->mNext;
+        }
+        if (list2) {
+          list2 = list2->mNext;
+        }
+      };
+      NS_ABORT_IF_FALSE(!*resultTail,
+                        "resultTail isn't pointing to the tail (may leak)");
+
+      aResultValue.SetAndAdoptCSSValueListValue(result.forget(),
+                                                eUnit_Filter);
+      return true;
+    }
+
     case eUnit_Transform: {
       const nsCSSValueList *list1 = aValue1.GetCSSValueListValue();
       const nsCSSValueList *list2 = aValue2.GetCSSValueListValue();
 
       // We want to avoid the matrix decomposition when we can, since
       // avoiding it can produce better results both for compound
       // transforms and for skew and skewY (see below).  We can do this
       // in two cases:
@@ -2427,16 +2642,17 @@ nsStyleAnimation::UncomputeValue(nsCSSPr
       }
     } break;
     case eUnit_CSSRect: {
       nsCSSRect& rect = aSpecifiedValue.SetRectValue();
       rect = *aComputedValue.GetCSSRectValue();
     } break;
     case eUnit_Dasharray:
     case eUnit_Shadow:
+    case eUnit_Filter:
     case eUnit_Transform:
     case eUnit_BackgroundPosition:
       aSpecifiedValue.
         SetDependentListValue(aComputedValue.GetCSSValueListValue());
       break;
     case eUnit_CSSValuePairList:
       aSpecifiedValue.
         SetDependentPairListValue(aComputedValue.GetCSSValuePairListValue());
@@ -2539,22 +2755,37 @@ StyleCoordToValue(const nsStyleCoord& aC
 
 static bool
 StyleCoordToCSSValue(const nsStyleCoord& aCoord, nsCSSValue& aCSSValue)
 {
   switch (aCoord.GetUnit()) {
     case eStyleUnit_Coord:
       nscoordToCSSValue(aCoord.GetCoordValue(), aCSSValue);
       break;
+    case eStyleUnit_Factor:
+      aCSSValue.SetFloatValue(aCoord.GetFactorValue(), eCSSUnit_Number);
+      break;
     case eStyleUnit_Percent:
       aCSSValue.SetPercentValue(aCoord.GetPercentValue());
       break;
     case eStyleUnit_Calc:
       SetCalcValue(aCoord.GetCalcValue(), aCSSValue);
       break;
+    case eStyleUnit_Degree:
+      aCSSValue.SetFloatValue(aCoord.GetAngleValue(), eCSSUnit_Degree);
+      break;
+    case eStyleUnit_Grad:
+      aCSSValue.SetFloatValue(aCoord.GetAngleValue(), eCSSUnit_Grad);
+      break;
+    case eStyleUnit_Radian:
+      aCSSValue.SetFloatValue(aCoord.GetAngleValue(), eCSSUnit_Radian);
+      break;
+    case eStyleUnit_Turn:
+      aCSSValue.SetFloatValue(aCoord.GetAngleValue(), eCSSUnit_Turn);
+      break;
     default:
       NS_ABORT_IF_FALSE(false, "unexpected unit");
       return false;
   }
   return true;
 }
 
 /*
@@ -3007,16 +3238,73 @@ nsStyleAnimation::ExtractComputedValue(n
                 break;
             }
           }
 
           aComputedValue.SetAndAdoptCSSValuePairListValue(result.forget());
           break;
         }
 
+        case eCSSProperty_filter: {
+          const nsStyleSVGReset *svgReset =
+            static_cast<const nsStyleSVGReset*>(styleStruct);
+          const nsTArray<nsStyleFilter>& filters = svgReset->mFilters;
+          nsAutoPtr<nsCSSValueList> result;
+          nsCSSValueList **resultTail = getter_Transfers(result);
+          for (uint32_t i = 0; i < filters.Length(); ++i) {
+            nsCSSValueList *item = new nsCSSValueList;
+            *resultTail = item;
+            resultTail = &item->mNext;
+            const nsStyleFilter& filter = filters[i];
+            int32_t type = filter.GetType();
+            if (type == NS_STYLE_FILTER_URL) {
+              nsIDocument* doc = aStyleContext->PresContext()->Document();
+              nsRefPtr<nsStringBuffer> uriAsStringBuffer =
+                GetURIAsUtf16StringBuffer(filter.GetURL());
+              nsRefPtr<mozilla::css::URLValue> url =
+                new mozilla::css::URLValue(filter.GetURL(),
+                                           uriAsStringBuffer,
+                                           doc->GetDocumentURI(),
+                                           doc->NodePrincipal());
+              item->mValue.SetURLValue(url);
+            } else {
+              nsCSSKeyword functionName =
+                nsCSSProps::ValueToKeywordEnum(type,
+                  nsCSSProps::kFilterFunctionKTable);
+              nsCSSValue::Array* filterArray =
+                item->mValue.InitFunction(functionName, 1);
+              if (type >= NS_STYLE_FILTER_BLUR && type <= NS_STYLE_FILTER_HUE_ROTATE) {
+                if (!StyleCoordToCSSValue(
+                      filter.GetFilterParameter(),
+                      filterArray->Item(1))) {
+                  return false;
+                }
+              } else if (type == NS_STYLE_FILTER_DROP_SHADOW) {
+                nsCSSValueList* shadowResult = filterArray->Item(1).SetListValue();
+                nsAutoPtr<nsCSSValueList> tmpShadowValue;
+                nsCSSValueList **tmpShadowResultTail = getter_Transfers(tmpShadowValue);
+                nsCSSShadowArray* shadowArray = filter.GetDropShadow();
+                NS_ABORT_IF_FALSE(shadowArray->Length() == 1,
+                                  "expected exactly one shadow");
+                AppendCSSShadowValue(shadowArray->ShadowAt(0), tmpShadowResultTail);
+                *shadowResult = *tmpShadowValue;
+              } else {
+                // We checked all possible nsStyleFilter types but
+                // NS_STYLE_FILTER_NULL before. We should never enter this path.
+                NS_NOTREACHED("no other filter functions defined");
+                return false;
+              }
+            }
+          }
+
+          aComputedValue.SetAndAdoptCSSValueListValue(result.forget(),
+                                                      eUnit_Filter);
+          break;
+        }
+
         case eCSSProperty_transform: {
           const nsStyleDisplay *display =
             static_cast<const nsStyleDisplay*>(styleStruct);
           nsAutoPtr<nsCSSValueList> result;
           if (display->mSpecifiedTransform) {
             // Clone, and convert all lengths (not percents) to pixels.
             nsCSSValueList **resultTail = getter_Transfers(result);
             for (const nsCSSValueList *l = display->mSpecifiedTransform;
@@ -3167,40 +3455,17 @@ nsStyleAnimation::ExtractComputedValue(n
           StyleDataAtOffset(styleStruct, ssOffset));
       if (!shadowArray) {
         aComputedValue.SetAndAdoptCSSValueListValue(nullptr, eUnit_Shadow);
         return true;
       }
       nsAutoPtr<nsCSSValueList> result;
       nsCSSValueList **resultTail = getter_Transfers(result);
       for (uint32_t i = 0, i_end = shadowArray->Length(); i < i_end; ++i) {
-        const nsCSSShadowItem *shadow = shadowArray->ShadowAt(i);
-        // X, Y, Radius, Spread, Color, Inset
-        nsRefPtr<nsCSSValue::Array> arr = nsCSSValue::Array::Create(6);
-        nscoordToCSSValue(shadow->mXOffset, arr->Item(0));
-        nscoordToCSSValue(shadow->mYOffset, arr->Item(1));
-        nscoordToCSSValue(shadow->mRadius, arr->Item(2));
-        // NOTE: This code sometimes stores mSpread: 0 even when
-        // the parser would be required to leave it null.
-        nscoordToCSSValue(shadow->mSpread, arr->Item(3));
-        if (shadow->mHasColor) {
-          arr->Item(4).SetColorValue(shadow->mColor);
-        }
-        if (shadow->mInset) {
-          arr->Item(5).SetIntValue(NS_STYLE_BOX_SHADOW_INSET,
-                                   eCSSUnit_Enumerated);
-        }
-
-        nsCSSValueList *resultItem = new nsCSSValueList;
-        if (!resultItem) {
-          return false;
-        }
-        resultItem->mValue.SetArrayValue(arr, eCSSUnit_Array);
-        *resultTail = resultItem;
-        resultTail = &resultItem->mNext;
+        AppendCSSShadowValue(shadowArray->ShadowAt(i), resultTail);
       }
       aComputedValue.SetAndAdoptCSSValueListValue(result.forget(),
                                                   eUnit_Shadow);
       return true;
     }
     case eStyleAnimType_None:
       NS_NOTREACHED("shouldn't use on non-animatable properties");
   }
@@ -3294,22 +3559,24 @@ nsStyleAnimation::Value::operator=(const
       break;
     case eUnit_CSSRect:
       NS_ABORT_IF_FALSE(aOther.mValue.mCSSRect, "rects may not be null");
       mValue.mCSSRect = new nsCSSRect(*aOther.mValue.mCSSRect);
       if (!mValue.mCSSRect) {
         mUnit = eUnit_Null;
       }
       break;
+    case eUnit_Filter:
     case eUnit_Dasharray:
     case eUnit_Shadow:
     case eUnit_Transform:
     case eUnit_BackgroundPosition:
-      NS_ABORT_IF_FALSE(mUnit == eUnit_Shadow || aOther.mValue.mCSSValueList,
-                        "value lists other than shadows may not be null");
+      NS_ABORT_IF_FALSE(mUnit == eUnit_Shadow || mUnit == eUnit_Filter ||
+                        aOther.mValue.mCSSValueList,
+                        "value lists other than shadows and filters may not be null");
       if (aOther.mValue.mCSSValueList) {
         mValue.mCSSValueList = aOther.mValue.mCSSValueList->Clone();
         if (!mValue.mCSSValueList) {
           mUnit = eUnit_Null;
         }
       } else {
         mValue.mCSSValueList = nullptr;
       }
@@ -3448,18 +3715,19 @@ nsStyleAnimation::Value::SetAndAdoptCSSR
 }
 
 void
 nsStyleAnimation::Value::SetAndAdoptCSSValueListValue(
                            nsCSSValueList *aValueList, Unit aUnit)
 {
   FreeValue();
   NS_ABORT_IF_FALSE(IsCSSValueListUnit(aUnit), "bad unit");
-  NS_ABORT_IF_FALSE(aUnit != eUnit_Dasharray || aValueList != nullptr,
-                    "dasharrays may not be null");
+  NS_ABORT_IF_FALSE(aUnit != eUnit_Dasharray || aUnit != eUnit_Filter ||
+                    aValueList != nullptr,
+                    "dasharrays and filters may not be null");
   mUnit = aUnit;
   mValue.mCSSValueList = aValueList; // take ownership
 }
 
 void
 nsStyleAnimation::Value::SetAndAdoptCSSValuePairListValue(
                            nsCSSValuePairList *aValuePairList)
 {
@@ -3518,16 +3786,17 @@ nsStyleAnimation::Value::operator==(cons
       return *mValue.mCSSValue == *aOther.mValue.mCSSValue;
     case eUnit_CSSValuePair:
       return *mValue.mCSSValuePair == *aOther.mValue.mCSSValuePair;
     case eUnit_CSSValueTriplet:
       return *mValue.mCSSValueTriplet == *aOther.mValue.mCSSValueTriplet;
     case eUnit_CSSRect:
       return *mValue.mCSSRect == *aOther.mValue.mCSSRect;
     case eUnit_Dasharray:
+    case eUnit_Filter:
     case eUnit_Shadow:
     case eUnit_Transform:
     case eUnit_BackgroundPosition:
       return *mValue.mCSSValueList == *aOther.mValue.mCSSValueList;
     case eUnit_CSSValuePairList:
       return *mValue.mCSSValuePairList == *aOther.mValue.mCSSValuePairList;
     case eUnit_UnparsedString:
       return (NS_strcmp(GetStringBufferValue(),
--- a/layout/style/nsStyleAnimation.h
+++ b/layout/style/nsStyleAnimation.h
@@ -216,16 +216,17 @@ public:
     eUnit_Float,
     eUnit_Color,
     eUnit_Calc, // nsCSSValue* (never null), always with a single
                 // calc() expression that's either length or length+percent
     eUnit_CSSValuePair, // nsCSSValuePair* (never null)
     eUnit_CSSValueTriplet, // nsCSSValueTriplet* (never null)
     eUnit_CSSRect, // nsCSSRect* (never null)
     eUnit_Dasharray, // nsCSSValueList* (never null)
+    eUnit_Filter, // nsCSSValueList* (may be null)
     eUnit_Shadow, // nsCSSValueList* (may be null)
     eUnit_Transform, // nsCSSValueList* (never null)
     eUnit_BackgroundPosition, // nsCSSValueList* (never null)
     eUnit_CSSValuePairList, // nsCSSValuePairList* (never null)
     eUnit_UnparsedString // nsStringBuffer* (never null)
   };
 
   class Value {
@@ -375,18 +376,19 @@ public:
     }
     static bool IsCSSValueTripletUnit(Unit aUnit) {
       return aUnit == eUnit_CSSValueTriplet;
     }
     static bool IsCSSRectUnit(Unit aUnit) {
       return aUnit == eUnit_CSSRect;
     }
     static bool IsCSSValueListUnit(Unit aUnit) {
-      return aUnit == eUnit_Dasharray || aUnit == eUnit_Shadow ||
-             aUnit == eUnit_Transform || aUnit == eUnit_BackgroundPosition;
+      return aUnit == eUnit_Dasharray || aUnit == eUnit_Filter ||
+             aUnit == eUnit_Shadow || aUnit == eUnit_Transform ||
+             aUnit == eUnit_BackgroundPosition;
     }
     static bool IsCSSValuePairListUnit(Unit aUnit) {
       return aUnit == eUnit_CSSValuePairList;
     }
     static bool IsStringUnit(Unit aUnit) {
       return aUnit == eUnit_UnparsedString;
     }
   };
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -117,16 +117,17 @@ var supported_properties = {
                 test_length_unclamped, test_percent_unclamped ],
     "clip": [ test_rect_transition ],
     "color": [ test_color_transition ],
     "fill": [ test_color_transition ],
     "fill-opacity" : [ test_float_zeroToOne_transition,
                        // opacity is clamped in computed style
                        // (not parsing/interpolation)
                        test_float_zeroToOne_clamped ],
+    "filter" : [ test_filter_transition ],
     "flood-color": [ test_color_transition ],
     "flood-opacity" : [ test_float_zeroToOne_transition,
                         // opacity is clamped in computed style
                         // (not parsing/interpolation)
                         test_float_zeroToOne_clamped ],
     "font-size": [ test_length_transition, test_percent_transition,
                    test_length_percent_calc_transition,
                    test_length_clamped, test_percent_clamped ],
@@ -571,16 +572,166 @@ var transformTests = [
     end: 'skewX(-45deg) rotate(90deg)',
     expected_uncomputed: 'skewX(22.5deg) rotate(90deg)' },
   { start: 'skewX(-60deg) rotate(90deg) translate(0)',
     end: 'skewX(60deg) rotate(90deg)',
     expected: computeMatrix('rotate(120deg) skewX(' + Math.atan(Math.tan(Math.PI * 60/180) / 2) + 'rad) scale(2, 0.5)'),
     round_error_ok: true },
 ];
 
+var filterTests = [
+  { start: "none", end: "none",
+    expected: ["none"] },
+  // function from none (number/length)
+  { start: "none", end: "brightness(0.5)",
+    expected: ["brightness", 0.875] },
+  { start: "none", end: "contrast(0.5)",
+    expected: ["contrast", 0.875] },
+  { start: "none", end: "grayscale(0.5)",
+    expected: ["grayscale", 0.125] },
+  { start: "none", end: "invert(0.5)",
+    expected: ["invert", 0.125] },
+  { start: "none", end: "opacity(0.5)",
+    expected: ["opacity", 0.875] },
+  { start: "none", end: "saturate(0.5)",
+    expected: ["saturate", 0.875] },
+  { start: "none", end: "sepia(0.5)",
+    expected: ["sepia", 0.125] },
+  { start: "none", end: "blur(50px)",
+    expected: ["blur", 12.5] },
+  // function to none (number/length)
+  { start: "brightness(0.5)", end: "none",
+    expected: ["brightness", 0.625] },
+  { start: "contrast(0.5)", end: "none",
+    expected: ["contrast", 0.625] },
+  { start: "grayscale(0.5)", end: "none",
+    expected: ["grayscale", 0.375] },
+  { start: "invert(0.5)", end: "none",
+    expected: ["invert", 0.375] },
+  { start: "opacity(0.5)", end: "none",
+    expected: ["opacity", 0.625] },
+  { start: "saturate(0.5)", end: "none",
+    expected: ["saturate", 0.625] },
+  { start: "sepia(0.5)", end: "none",
+    expected: ["sepia", 0.375] },
+  { start: "blur(50px)", end: "none",
+    expected: ["blur", 37.5] },
+  // function to same function (number/length)
+  { start: "brightness(0.25)", end: "brightness(0.75)",
+    expected: ["brightness", 0.375] },
+  { start: "contrast(0.25)", end: "contrast(0.75)",
+    expected: ["contrast", 0.375] },
+  { start: "grayscale(0.25)", end: "grayscale(0.75)",
+    expected: ["grayscale", 0.375] },
+  { start: "invert(0.25)", end: "invert(0.75)",
+    expected: ["invert", 0.375] },
+  { start: "opacity(0.25)", end: "opacity(0.75)",
+    expected: ["opacity", 0.375] },
+  { start: "saturate(0.25)", end: "saturate(0.75)",
+    expected: ["saturate", 0.375] },
+  { start: "sepia(0.25)", end: "sepia(0.75)",
+    expected: ["sepia", 0.375] },
+  { start: "blur(25px)", end: "blur(75px)",
+    expected: ["blur", 37.5] },
+  // function to same function (percent)
+  { start: "brightness(25%)", end: "brightness(75%)",
+    expected: ["brightness", 0.375] },
+  { start: "contrast(25%)", end: "contrast(75%)",
+    expected: ["contrast", 0.375] },
+  { start: "grayscale(25%)", end: "grayscale(75%)",
+    expected: ["grayscale", 0.375] },
+  { start: "invert(25%)", end: "invert(75%)",
+    expected: ["invert", 0.375] },
+  { start: "opacity(25%)", end: "opacity(75%)",
+    expected: ["opacity", 0.375] },
+  { start: "saturate(25%)", end: "saturate(75%)",
+    expected: ["saturate", 0.375] },
+  { start: "sepia(25%)", end: "sepia(75%)",
+    expected: ["sepia", 0.375] },
+  // function to same function (percent, number/length)
+  { start: "brightness(0.25)", end: "brightness(75%)",
+    expected: ["brightness", 0.375] },
+  { start: "contrast(25%)", end: "contrast(0.75)",
+    expected: ["contrast", 0.375] },
+  // hue-rotate with different angle values
+  { start: "hue-rotate(0deg)", end: "hue-rotate(720deg)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0rad)", end: "hue-rotate("+4*Math.PI+"rad)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0grad)", end: "hue-rotate(800grad)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0turn)", end: "hue-rotate(2turn)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0deg)", end: "hue-rotate("+4*Math.PI+"rad)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0turn)", end: "hue-rotate(800grad)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0grad)", end: "hue-rotate("+4*Math.PI+"rad)",
+    expected: ["hue-rotate", Math.PI.toFixed(5)] },
+  { start: "hue-rotate(0grad)", end: "hue-rotate(0turn)",
+    expected: ["hue-rotate", 0] },
+  // multiple matching functions, same length
+  { start: "contrast(25%) brightness(0.25) blur(25px) sepia(75%)",
+    end: "contrast(75%) brightness(0.75) blur(75px) sepia(25%)",
+    expected: ["contrast", 0.375, "brightness", 0.375, "blur", 37.5, "sepia", 0.625] },
+  { start: "invert(25%) brightness(0.25) blur(25px) invert(50%) brightness(0.5) blur(50px)",
+    end: "invert(75%) brightness(0.75) blur(75px)",
+    expected: ["invert", 0.375, "brightness", 0.375, "blur", 37.5, "invert", 0.375, "brightness", 0.625, "blur", 37.5] },
+  // multiple matching functions, different length
+  { start: "contrast(25%) brightness(0.5) blur(50px)",
+    end: "contrast(75%)",
+    expected: ["contrast", 0.375, "brightness", 0.625, "blur", 37.5] },
+  // mismatching filter functions
+  { start: "contrast(0%)", end: "blur(10px)",
+    expected: ["blur", 10] },
+  // not supported interpolations
+  { start: "none", end: "url('#b')",
+    expected: ["url", "\""+document.URL+"#b\""] },
+  { start: "url('#a')", end: "none",
+    expected: ["none"] },
+  { start: "url('#a')", end: "url('#b')",
+    expected: ["url", "\""+document.URL+"#b\""] },
+  { start: "url('#a')", end: "blur(10px)",
+    expected: ["blur", 10] },
+  { start: "blur(10px)", end: "url('#a')",
+    expected: ["url", "\""+document.URL+"#a\""] },
+  { start: "blur(0px) url('#a')", end: "blur(20px)",
+    expected: ["blur", 20] },
+  { start: "blur(0px)", end: "blur(20px) url('#a')",
+    expected: ["blur", 20, "url", "\""+document.URL+"#a\""] },
+  { start: "contrast(0.25) brightness(0.25) blur(25px)",
+    end: "contrast(0.75) url('#a')",
+    expected: ["contrast", 0.75, "url", "\""+document.URL+"#a\""] },
+  { start: "contrast(0.25) brightness(0.25) blur(75px)",
+    end: "brightness(0.75) contrast(0.75) blur(25px)",
+    expected: ["brightness", 0.75, "contrast", 0.75, "blur", 25] },
+  { start: "contrast(0.25) brightness(0.25) blur(25px)",
+    end: "contrast(0.75) brightness(0.75) contrast(0.75)",
+    expected: ["contrast", 0.75, "brightness", 0.75, "contrast", 0.75] },
+  // drop-shadow animation
+  { start: "none",
+    end: "drop-shadow(rgb(0, 0, 0) 4px 4px 0px)",
+    expected: ["drop-shadow", "rgba(0, 0, 0, 0.25) 1px 1px 0px"] },
+  { start: "drop-shadow(rgb(0, 0, 0) 0px 0px 0px)",
+    end: "drop-shadow(rgb(0, 0, 0) 4px 4px 0px)",
+    expected: ["drop-shadow", "rgb(0, 0, 0) 1px 1px 0px"] },
+  { start: "drop-shadow(#038000 4px 4px)",
+    end: "drop-shadow(8px 8px 8px red)",
+    expected: ["drop-shadow", "rgb(66, 96, 0) 5px 5px 2px"] },
+  { start: "blur(25px) drop-shadow(8px 8px)",
+    end: "blur(75px)",
+    expected: ["blur", 37.5, "drop-shadow", "rgb(0, 0, 0) 6px 6px 0px"] },
+  { start: "blur(75px)",
+    end: "blur(25px) drop-shadow(8px 8px)",
+    expected: ["blur", 62.5, "drop-shadow", "rgb(0, 0, 0) 2px 2px 0px"] },
+  { start: "drop-shadow(2px 2px blue)",
+    end: "none",
+    expected: ["drop-shadow", "rgba(0, 0, 255, 0.75) 1.5px 1.5px 0px"] },
+];
+
 var prop;
 for (prop in supported_properties) {
   // Test that prop is in the property database.
   ok(prop in gCSSProperties, "property " + prop + " in gCSSProperties");
 
   // Test that the entry has at least one test function.
   ok(supported_properties[prop].length > 0,
      "property " + prop + " must have at least one test function");
@@ -990,16 +1141,95 @@ function test_border_color_transition(pr
   is(cs.getPropertyValue(prop), "rgb(96, 48, 32)",
      "color-valued property " + prop + ": interpolation of initial value");
 
   check_distance(prop, "rgb(128, 64, 0)", "rgb(96, 48, 32)", "initial");
 
   div.style.removeProperty("color");
 }
 
+function filter_function_list_equals(string, expectedList)
+{
+  // Check simple case "none"
+  if (string == "none" && string == expectedList[0]) {
+    return true;
+  }
+  // The regular expression does not filter out the last parenthesis.
+  // Remove last character for now.
+  is(string.substring(string.length - 1, string.length), ')', "Check if last character is close-paren.")
+  string = string.substring(0, string.length - 1);
+  var reg = /\)*\s*(blur|brightness|contrast|grayscale|hue\-rotate|invert|opacity|saturate|sepia|drop\-shadow|url)\(/
+  var matches = string.split(reg);
+  // First item must be empty. All other items are of functionName, functionValue.
+  if (!matches || matches.shift() != "") {
+    ok(false, "computed style of 'filter' isn't in the format we expect");
+    return false;
+  }
+
+  // Odd items are the function name, even items the function value.
+  if (!matches.length || matches.length % 2 ||
+      expectedList.length != matches.length) {
+    ok(false, "computed style of 'filter' isn't in the format we expect");
+    return false;
+  }
+  for (var i = 0; i < matches.length; i += 2) {
+    var functionName = matches[i];
+    var functionValue = matches[i+1];
+    var expected = expectedList[i+1]
+    var tolerance = 0;
+    // Check if we have the expected function.
+    if (functionName != expectedList[i]) {
+      return false;
+    }
+    if (functionName == "blur") {
+      // Last two characters must be "px".
+      if (functionValue.search("px") != functionValue.length - 2) {
+        return false;
+      }
+      functionValue = functionValue.substring(0, functionValue.length - 2);
+    } else if (functionName == "hue-rotate") {
+      // Last two characters must be "rad".
+      if (functionValue.search("rad") != functionValue.length - 3) {
+        return false;
+      }
+      tolerance = 0.001;
+      functionValue = functionValue.substring(0, functionValue.length - 3);
+    } else if (functionName == "drop-shadow" || functionName == "url") {
+      if (functionValue != expected) {
+        return false;
+      }
+      continue;
+    }
+    // Check if string is not a number or difference is not in tolerance level.
+    if (isNaN(functionValue) ||
+      Math.abs(parseFloat(functionValue) - expected) > tolerance) {
+      return false;
+    }
+  }
+  return true;
+}
+
+function test_filter_transition(prop) {
+  if (!SpecialPowers.getBoolPref("layout.css.filters.enabled")) {
+    return;
+  }
+  for (var i in filterTests) {
+    var test = filterTests[i];
+    div.style.setProperty("transition-property", "none", "");
+    div.style.setProperty(prop, test.start, "");
+    cs.getPropertyValue(prop);
+    div.style.setProperty("transition-property", prop, "");
+    div.style.setProperty(prop, test.end, "");
+    var actual = cs.getPropertyValue(prop);
+    ok(filter_function_list_equals(actual, test.expected),
+                                   "Filter property is " + actual + " expected values of " +
+                                   test.expected);
+  }
+}
+
 function test_shadow_transition(prop) {
   var spreadStr = (prop == "box-shadow") ? " 0px" : "";
 
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "none", "");
   is(cs.getPropertyValue(prop), "none",
      "shadow-valued property " + prop + ": computed value before transition");
   div.style.setProperty("transition-property", prop, "");