From 10ac32e77a36f60df3f616eca1ac3be17b2dba36 Mon Sep 17 00:00:00 2001 From: Dirk Schulze Date: Wed, 11 Sep 2013 15:24:03 -0700 Subject: [PATCH] Bug 896050 - Implement animation of CSS filter property. r=dholbert --- layout/style/nsCSSPropList.h | 2 +- layout/style/nsStyleAnimation.cpp | 325 ++++++++++++++++-- layout/style/nsStyleAnimation.h | 6 +- .../test/test_transitions_per_property.html | 230 +++++++++++++ 4 files changed, 532 insertions(+), 31 deletions(-) diff --git a/layout/style/nsCSSPropList.h b/layout/style/nsCSSPropList.h index 1fd3ffcc0017..e2a217225557 100644 --- a/layout/style/nsCSSPropList.h +++ b/layout/style/nsCSSPropList.h @@ -3374,7 +3374,7 @@ CSS_PROP_SVGRESET( 0, nullptr, CSS_PROP_NO_OFFSET, - eStyleAnimType_None) + eStyleAnimType_Custom) CSS_PROP_SVGRESET( flood-color, flood_color, diff --git a/layout/style/nsStyleAnimation.cpp b/layout/style/nsStyleAnimation.cpp index baa602b9bda0..66a7dc448ed6 100644 --- a/layout/style/nsStyleAnimation.cpp +++ b/layout/style/nsStyleAnimation.cpp @@ -248,6 +248,34 @@ nscoordToCSSValue(nscoord aCoord, nsCSSValue& aCSSValue) eCSSUnit_Pixel); } +static void +AppendCSSShadowValue(const nsCSSShadowItem *aShadow, + nsCSSValueList **&aResultTail) +{ + NS_ABORT_IF_FALSE(aShadow, "shadow expected"); + + // X, Y, Radius, Spread, Color, Inset + nsRefPtr 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; @@ -753,6 +781,8 @@ nsStyleAnimation::ComputeDistance(nsCSSProperty aProperty, aDistance = sqrt(squareDistance); return true; } + case eUnit_Filter: + // FIXME: Support paced animations for filter function interpolation. case eUnit_Transform: { return false; } @@ -1032,6 +1062,38 @@ AddCSSValuePixelPercentCalc(const uint32_t aValueRestrictions, 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, @@ -1547,6 +1609,121 @@ 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 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 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 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) @@ -2060,6 +2237,44 @@ nsStyleAnimation::AddWeighted(nsCSSProperty aProperty, aResultValue.SetAndAdoptCSSValueListValue(result.forget(), eUnit_Shadow); return true; } + + case eUnit_Filter: { + const nsCSSValueList *list1 = aValue1.GetCSSValueListValue(); + const nsCSSValueList *list2 = aValue2.GetCSSValueListValue(); + + nsAutoPtr 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(); @@ -2432,6 +2647,7 @@ nsStyleAnimation::UncomputeValue(nsCSSProperty aProperty, } break; case eUnit_Dasharray: case eUnit_Shadow: + case eUnit_Filter: case eUnit_Transform: case eUnit_BackgroundPosition: aSpecifiedValue. @@ -2544,12 +2760,27 @@ StyleCoordToCSSValue(const nsStyleCoord& aCoord, nsCSSValue& aCSSValue) 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; @@ -3012,6 +3243,63 @@ nsStyleAnimation::ExtractComputedValue(nsCSSProperty aProperty, break; } + case eCSSProperty_filter: { + const nsStyleSVGReset *svgReset = + static_cast(styleStruct); + const nsTArray& filters = svgReset->mFilters; + nsAutoPtr 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 uriAsStringBuffer = + GetURIAsUtf16StringBuffer(filter.GetURL()); + nsRefPtr 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 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(styleStruct); @@ -3172,30 +3460,7 @@ nsStyleAnimation::ExtractComputedValue(nsCSSProperty aProperty, nsAutoPtr 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 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); @@ -3299,12 +3564,14 @@ nsStyleAnimation::Value::operator=(const Value& aOther) 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) { @@ -3453,8 +3720,9 @@ nsStyleAnimation::Value::SetAndAdoptCSSValueListValue( { 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 } @@ -3523,6 +3791,7 @@ nsStyleAnimation::Value::operator==(const Value& aOther) const case eUnit_CSSRect: return *mValue.mCSSRect == *aOther.mValue.mCSSRect; case eUnit_Dasharray: + case eUnit_Filter: case eUnit_Shadow: case eUnit_Transform: case eUnit_BackgroundPosition: diff --git a/layout/style/nsStyleAnimation.h b/layout/style/nsStyleAnimation.h index 2a5e5a69e476..6e4a61ee9807 100644 --- a/layout/style/nsStyleAnimation.h +++ b/layout/style/nsStyleAnimation.h @@ -221,6 +221,7 @@ public: 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) @@ -380,8 +381,9 @@ public: 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; diff --git a/layout/style/test/test_transitions_per_property.html b/layout/style/test/test_transitions_per_property.html index 05ab74279cc1..8e100f38c9a7 100644 --- a/layout/style/test/test_transitions_per_property.html +++ b/layout/style/test/test_transitions_per_property.html @@ -122,6 +122,7 @@ var supported_properties = { // 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 @@ -576,6 +577,156 @@ var transformTests = [ 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. @@ -995,6 +1146,85 @@ function test_border_color_transition(prop) { 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" : "";