| /* |
| * Copyright (C) 2008 Apple Inc. All rights reserved. |
| * Copyright (C) 2015 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "core/css/CSSGradientValue.h" |
| |
| #include "core/CSSValueKeywords.h" |
| #include "core/css/CSSCalculationValue.h" |
| #include "core/css/CSSToLengthConversionData.h" |
| #include "core/css/CSSValuePair.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/dom/TextLinkColors.h" |
| #include "core/layout/LayoutObject.h" |
| #include "platform/geometry/IntSize.h" |
| #include "platform/graphics/Gradient.h" |
| #include "platform/graphics/GradientGeneratedImage.h" |
| #include "platform/graphics/Image.h" |
| #include "platform/graphics/skia/SkiaUtils.h" |
| #include "wtf/text/StringBuilder.h" |
| #include "wtf/text/WTFString.h" |
| #include <algorithm> |
| #include <utility> |
| |
| namespace blink { |
| |
| DEFINE_TRACE(CSSGradientColorStop) |
| { |
| visitor->trace(m_position); |
| visitor->trace(m_color); |
| } |
| |
| PassRefPtr<Image> CSSGradientValue::image(const LayoutObject* layoutObject, const IntSize& size) |
| { |
| if (size.isEmpty()) |
| return nullptr; |
| |
| bool cacheable = isCacheable(); |
| if (cacheable) { |
| if (!clients().contains(layoutObject)) |
| return nullptr; |
| |
| // Need to look up our size. Create a string of width*height to use as a hash key. |
| Image* result = getImage(layoutObject, size); |
| if (result) |
| return result; |
| } |
| |
| // We need to create an image. |
| RefPtr<Gradient> gradient; |
| |
| const ComputedStyle* rootStyle = layoutObject->document().documentElement()->computedStyle(); |
| CSSToLengthConversionData conversionData(layoutObject->style(), rootStyle, layoutObject->view(), layoutObject->style()->effectiveZoom()); |
| if (isLinearGradientValue()) |
| gradient = toCSSLinearGradientValue(this)->createGradient(conversionData, size, *layoutObject); |
| else |
| gradient = toCSSRadialGradientValue(this)->createGradient(conversionData, size, *layoutObject); |
| |
| RefPtr<Image> newImage = GradientGeneratedImage::create(gradient, size); |
| if (cacheable) |
| putImage(size, newImage); |
| |
| return newImage.release(); |
| } |
| |
| // Should only ever be called for deprecated gradients. |
| static inline bool compareStops(const CSSGradientColorStop& a, const CSSGradientColorStop& b) |
| { |
| double aVal = a.m_position->getDoubleValue(); |
| double bVal = b.m_position->getDoubleValue(); |
| |
| return aVal < bVal; |
| } |
| |
| struct GradientStop { |
| Color color; |
| float offset; |
| bool specified; |
| |
| GradientStop() |
| : offset(0) |
| , specified(false) |
| { } |
| }; |
| |
| static void replaceColorHintsWithColorStops(Vector<GradientStop>& stops, const WillBeHeapVector<CSSGradientColorStop, 2>& cssGradientStops) |
| { |
| // This algorithm will replace each color interpolation hint with 9 regular |
| // color stops. The color values for the new color stops will be calculated |
| // using the color weighting formula defined in the spec. The new color |
| // stops will be positioned in such a way that all the pixels between the two |
| // user defined color stops have color values close to the interpolation curve. |
| // If the hint is closer to the left color stop, add 2 stops to the left and |
| // 6 to the right, else add 6 stops to the left and 2 to the right. |
| // The color stops on the side with more space start midway because |
| // the curve approximates a line in that region. |
| // Using this aproximation, it is possible to discern the color steps when |
| // the gradient is large. If this becomes an issue, we can consider improving |
| // the algorithm, or adding support for color interpolation hints to skia shaders. |
| |
| int indexOffset = 0; |
| |
| // The first and the last color stops cannot be color hints. |
| for (size_t i = 1; i < cssGradientStops.size() - 1; ++i) { |
| if (!cssGradientStops[i].isHint()) |
| continue; |
| |
| // The current index of the stops vector. |
| size_t x = i + indexOffset; |
| ASSERT(x >= 1); |
| |
| // offsetLeft offset offsetRight |
| // |-------------------|---------------------------------| |
| // leftDist rightDist |
| |
| float offsetLeft = stops[x - 1].offset; |
| float offsetRight = stops[x + 1].offset; |
| float offset = stops[x].offset; |
| float leftDist = offset - offsetLeft; |
| float rightDist = offsetRight - offset; |
| float totalDist = offsetRight - offsetLeft; |
| |
| Color leftColor = stops[x - 1].color; |
| Color rightColor = stops[x + 1].color; |
| |
| ASSERT(offsetLeft <= offset && offset <= offsetRight); |
| |
| if (WebCoreFloatNearlyEqual(leftDist, rightDist)) { |
| stops.remove(x); |
| --indexOffset; |
| continue; |
| } |
| |
| if (WebCoreFloatNearlyEqual(leftDist, .0f)) { |
| stops[x].color = rightColor; |
| continue; |
| } |
| |
| if (WebCoreFloatNearlyEqual(rightDist, .0f)) { |
| stops[x].color = leftColor; |
| continue; |
| } |
| |
| GradientStop newStops[9]; |
| // Position the new color stops. |
| if (leftDist > rightDist) { |
| for (size_t y = 0; y < 7; ++y) |
| newStops[y].offset = offsetLeft + leftDist * (7 + y) / 13; |
| newStops[7].offset = offset + rightDist / 3; |
| newStops[8].offset = offset + rightDist * 2 / 3; |
| } else { |
| newStops[0].offset = offsetLeft + leftDist / 3; |
| newStops[1].offset = offsetLeft + leftDist * 2 / 3; |
| for (size_t y = 0; y < 7; ++y) |
| newStops[y + 2].offset = offset + rightDist * y / 13; |
| } |
| |
| // calculate colors for the new color hints. |
| // The color weighting for the new color stops will be pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)). |
| float hintRelativeOffset = leftDist / totalDist; |
| for (size_t y = 0; y < 9; ++y) { |
| float pointRelativeOffset = (newStops[y].offset - offsetLeft) / totalDist; |
| float weighting = powf(pointRelativeOffset, logf(.5f) / logf(hintRelativeOffset)); |
| newStops[y].color = blend(leftColor, rightColor, weighting); |
| } |
| |
| // Replace the color hint with the new color stops. |
| stops.remove(x); |
| stops.insert(x, newStops, 9); |
| indexOffset += 8; |
| } |
| } |
| |
| static Color resolveStopColor(const CSSValue& stopColor, const LayoutObject& object) |
| { |
| return object.document().textLinkColors().colorFromCSSValue(stopColor, object.resolveColor(CSSPropertyColor)); |
| } |
| |
| void CSSGradientValue::addDeprecatedStops(Gradient* gradient, const LayoutObject& object) |
| { |
| ASSERT(m_gradientType == CSSDeprecatedLinearGradient || m_gradientType == CSSDeprecatedRadialGradient); |
| |
| if (!m_stopsSorted) { |
| if (m_stops.size()) |
| std::stable_sort(m_stops.begin(), m_stops.end(), compareStops); |
| m_stopsSorted = true; |
| } |
| |
| for (const auto& stop : m_stops) { |
| float offset; |
| if (stop.m_position->isPercentage()) |
| offset = stop.m_position->getFloatValue() / 100; |
| else |
| offset = stop.m_position->getFloatValue(); |
| |
| gradient->addColorStop(offset, resolveStopColor(*stop.m_color, object)); |
| } |
| } |
| |
| static bool requiresStopsNormalization(const Vector<GradientStop>& stops, const Gradient* gradient) |
| { |
| // We need at least two stops to normalize |
| if (stops.size() < 2) |
| return false; |
| |
| // Repeating gradients are implemented using a normalized stop offset range |
| // with the point/radius pairs aligned on the interval endpoints. |
| if (gradient->spreadMethod() == SpreadMethodRepeat) |
| return true; |
| |
| // Degenerate stops |
| if (stops.first().offset < 0 || stops.last().offset > 1) |
| return true; |
| |
| return false; |
| } |
| |
| // Redistribute the stops such that they fully cover [0 , 1] and add them to the gradient. |
| static bool normalizeAndAddStops(const Vector<GradientStop>& stops, Gradient* gradient) |
| { |
| ASSERT(stops.size() > 1); |
| |
| const float firstOffset = stops.first().offset; |
| const float lastOffset = stops.last().offset; |
| const float span = lastOffset - firstOffset; |
| |
| if (fabs(span) < std::numeric_limits<float>::epsilon()) { |
| // All stops are coincident -> use a single clamped offset value. |
| const float clampedOffset = std::min(std::max(firstOffset, 0.f), 1.f); |
| |
| // For repeating gradients, a coincident stop set defines a solid-color image with the color |
| // of the last color-stop in the rule. |
| // For non-repeating gradients, both the first color and the last color can be significant |
| // (padding on both sides of the offset). |
| if (gradient->spreadMethod() != SpreadMethodRepeat) |
| gradient->addColorStop(clampedOffset, stops.first().color); |
| gradient->addColorStop(clampedOffset, stops.last().color); |
| |
| return false; |
| } |
| |
| ASSERT(span > 0); |
| |
| for (size_t i = 0; i < stops.size(); ++i) { |
| const float normalizedOffset = (stops[i].offset - firstOffset) / span; |
| |
| // stop offsets should be monotonically increasing in [0 , 1] |
| ASSERT(normalizedOffset >= 0 && normalizedOffset <= 1); |
| ASSERT(i == 0 || normalizedOffset >= (stops[i - 1].offset - firstOffset) / span); |
| |
| gradient->addColorStop(normalizedOffset, stops[i].color); |
| } |
| |
| return true; |
| } |
| |
| // Collapse all negative-offset stops to 0 and compute an interpolated color value for that point. |
| static void clampNegativeOffsets(Vector<GradientStop>& stops) |
| { |
| float lastNegativeOffset = 0; |
| |
| for (size_t i = 0; i < stops.size(); ++i) { |
| const float currentOffset = stops[i].offset; |
| if (currentOffset >= 0) { |
| if (i > 0) { |
| // We found the negative -> positive offset transition: compute an interpolated |
| // color value for 0 and use it with the last clamped stop. |
| ASSERT(lastNegativeOffset < 0); |
| float lerpRatio = -lastNegativeOffset / (currentOffset - lastNegativeOffset); |
| stops[i - 1].color = blend(stops[i - 1].color, stops[i].color, lerpRatio); |
| } |
| |
| break; |
| } |
| |
| // Clamp all negative stops to 0. |
| stops[i].offset = 0; |
| lastNegativeOffset = currentOffset; |
| } |
| } |
| |
| // Update the linear gradient points to align with the given offset range. |
| static void adjustGradientPointsForOffsetRange(Gradient* gradient, float firstOffset, float lastOffset) |
| { |
| ASSERT(!gradient->isRadial()); |
| ASSERT(firstOffset <= lastOffset); |
| |
| const FloatPoint p0 = gradient->p0(); |
| const FloatPoint p1 = gradient->p1(); |
| const FloatSize d(p1 - p0); |
| |
| // Linear offsets are relative to the [p0 , p1] segment. |
| gradient->setP0(p0 + d * firstOffset); |
| gradient->setP1(p0 + d * lastOffset); |
| } |
| |
| // Update the radial gradient radii to align with the given offset range. |
| static void adjustGradientRadiiForOffsetRange(Gradient* gradient, float firstOffset, float lastOffset) |
| { |
| ASSERT(gradient->isRadial()); |
| ASSERT(firstOffset <= lastOffset); |
| |
| // Radial offsets are relative to the [0 , endRadius] segment. |
| float adjustedR0 = gradient->endRadius() * firstOffset; |
| float adjustedR1 = gradient->endRadius() * lastOffset; |
| ASSERT(adjustedR0 <= adjustedR1); |
| |
| // Unlike linear gradients (where we can adjust the points arbitrarily), |
| // we cannot let our radii turn negative here. |
| if (adjustedR0 < 0) { |
| // For the non-repeat case, this can never happen: clampNegativeOffsets() ensures we don't |
| // have to deal with negative offsets at this point. |
| ASSERT(gradient->spreadMethod() == SpreadMethodRepeat); |
| |
| // When in repeat mode, we deal with it by repositioning both radii in the positive domain - |
| // shifting them by a multiple of the radius span (which is the period of our repeating |
| // gradient -> hence no visible side effects). |
| const float radiusSpan = adjustedR1 - adjustedR0; |
| const float shiftToPositive = radiusSpan * ceilf(-adjustedR0 / radiusSpan); |
| adjustedR0 += shiftToPositive; |
| adjustedR1 += shiftToPositive; |
| } |
| ASSERT(adjustedR0 >= 0); |
| ASSERT(adjustedR1 >= adjustedR0); |
| |
| gradient->setStartRadius(adjustedR0); |
| gradient->setEndRadius(adjustedR1); |
| } |
| |
| void CSSGradientValue::addStops(Gradient* gradient, const CSSToLengthConversionData& conversionData, |
| const LayoutObject& object) |
| { |
| if (m_gradientType == CSSDeprecatedLinearGradient || m_gradientType == CSSDeprecatedRadialGradient) { |
| addDeprecatedStops(gradient, object); |
| return; |
| } |
| |
| size_t numStops = m_stops.size(); |
| |
| Vector<GradientStop> stops(numStops); |
| |
| bool hasHints = false; |
| |
| FloatPoint gradientStart = gradient->p0(); |
| FloatPoint gradientEnd; |
| if (isLinearGradientValue()) |
| gradientEnd = gradient->p1(); |
| else if (isRadialGradientValue()) |
| gradientEnd = gradientStart + FloatSize(gradient->endRadius(), 0); |
| float gradientLength = FloatSize(gradientStart - gradientEnd).diagonalLength(); |
| |
| for (size_t i = 0; i < numStops; ++i) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| |
| if (stop.isHint()) |
| hasHints = true; |
| else |
| stops[i].color = resolveStopColor(*stop.m_color, object); |
| |
| if (stop.m_position) { |
| if (stop.m_position->isPercentage()) |
| stops[i].offset = stop.m_position->getFloatValue() / 100; |
| else if (stop.m_position->isLength() || stop.m_position->isCalculatedPercentageWithLength()) { |
| float length; |
| if (stop.m_position->isLength()) |
| length = stop.m_position->computeLength<float>(conversionData); |
| else |
| length = stop.m_position->cssCalcValue()->toCalcValue(conversionData)->evaluate(gradientLength); |
| stops[i].offset = (gradientLength > 0) ? length / gradientLength : 0; |
| } else { |
| ASSERT_NOT_REACHED(); |
| stops[i].offset = 0; |
| } |
| stops[i].specified = true; |
| } else { |
| // If the first color-stop does not have a position, its position defaults to 0%. |
| // If the last color-stop does not have a position, its position defaults to 100%. |
| if (!i) { |
| stops[i].offset = 0; |
| stops[i].specified = true; |
| } else if (numStops > 1 && i == numStops - 1) { |
| stops[i].offset = 1; |
| stops[i].specified = true; |
| } |
| } |
| |
| // If a color-stop has a position that is less than the specified position of any |
| // color-stop before it in the list, its position is changed to be equal to the |
| // largest specified position of any color-stop before it. |
| if (stops[i].specified && i > 0) { |
| size_t prevSpecifiedIndex; |
| for (prevSpecifiedIndex = i - 1; prevSpecifiedIndex; --prevSpecifiedIndex) { |
| if (stops[prevSpecifiedIndex].specified) |
| break; |
| } |
| |
| if (stops[i].offset < stops[prevSpecifiedIndex].offset) |
| stops[i].offset = stops[prevSpecifiedIndex].offset; |
| } |
| } |
| |
| ASSERT(stops.first().specified && stops.last().specified); |
| |
| // If any color-stop still does not have a position, then, for each run of adjacent |
| // color-stops without positions, set their positions so that they are evenly spaced |
| // between the preceding and following color-stops with positions. |
| if (numStops > 2) { |
| size_t unspecifiedRunStart = 0; |
| bool inUnspecifiedRun = false; |
| |
| for (size_t i = 0; i < numStops; ++i) { |
| if (!stops[i].specified && !inUnspecifiedRun) { |
| unspecifiedRunStart = i; |
| inUnspecifiedRun = true; |
| } else if (stops[i].specified && inUnspecifiedRun) { |
| size_t unspecifiedRunEnd = i; |
| |
| if (unspecifiedRunStart < unspecifiedRunEnd) { |
| float lastSpecifiedOffset = stops[unspecifiedRunStart - 1].offset; |
| float nextSpecifiedOffset = stops[unspecifiedRunEnd].offset; |
| float delta = (nextSpecifiedOffset - lastSpecifiedOffset) / (unspecifiedRunEnd - unspecifiedRunStart + 1); |
| |
| for (size_t j = unspecifiedRunStart; j < unspecifiedRunEnd; ++j) |
| stops[j].offset = lastSpecifiedOffset + (j - unspecifiedRunStart + 1) * delta; |
| } |
| |
| inUnspecifiedRun = false; |
| } |
| } |
| } |
| |
| ASSERT(stops.size() == m_stops.size()); |
| if (hasHints) { |
| replaceColorHintsWithColorStops(stops, m_stops); |
| } |
| |
| // At this point we have a fully resolved set of stops. Time to perform adjustments for |
| // repeat gradients and degenerate values if needed. |
| if (requiresStopsNormalization(stops, gradient)) { |
| // Negative offsets are only an issue for non-repeating radial gradients: linear gradient |
| // points can be repositioned arbitrarily, and for repeating radial gradients we shift |
| // the radii into equivalent positive values. |
| if (isRadialGradientValue() && !m_repeating) |
| clampNegativeOffsets(stops); |
| |
| if (normalizeAndAddStops(stops, gradient)) { |
| if (isLinearGradientValue()) { |
| adjustGradientPointsForOffsetRange(gradient, stops.first().offset, stops.last().offset); |
| } else { |
| adjustGradientRadiiForOffsetRange(gradient, stops.first().offset, stops.last().offset); |
| } |
| } else { |
| // Normalization failed because the stop set is coincident. |
| } |
| } else { |
| // No normalization required, just add the current stops. |
| for (const auto& stop : stops) |
| gradient->addColorStop(stop.offset, stop.color); |
| } |
| } |
| |
| static float positionFromValue(CSSValue* value, const CSSToLengthConversionData& conversionData, const IntSize& size, bool isHorizontal) |
| { |
| int origin = 0; |
| int sign = 1; |
| int edgeDistance = isHorizontal ? size.width() : size.height(); |
| |
| // In this case the center of the gradient is given relative to an edge in the form of: |
| // [ top | bottom | right | left ] [ <percentage> | <length> ]. |
| if (value->isValuePair()) { |
| CSSValuePair& pair = toCSSValuePair(*value); |
| CSSValueID originID = toCSSPrimitiveValue(pair.first()).getValueID(); |
| value = &pair.second(); |
| |
| if (originID == CSSValueRight || originID == CSSValueBottom) { |
| // For right/bottom, the offset is relative to the far edge. |
| origin = edgeDistance; |
| sign = -1; |
| } |
| } |
| |
| CSSPrimitiveValue* primitiveValue = toCSSPrimitiveValue(value); |
| |
| if (primitiveValue->isNumber()) |
| return origin + sign * primitiveValue->getFloatValue() * conversionData.zoom(); |
| |
| if (primitiveValue->isPercentage()) |
| return origin + sign * primitiveValue->getFloatValue() / 100.f * edgeDistance; |
| |
| if (primitiveValue->isCalculatedPercentageWithLength()) |
| return origin + sign * primitiveValue->cssCalcValue()->toCalcValue(conversionData)->evaluate(edgeDistance); |
| |
| switch (primitiveValue->getValueID()) { |
| case CSSValueTop: |
| ASSERT(!isHorizontal); |
| return 0; |
| case CSSValueLeft: |
| ASSERT(isHorizontal); |
| return 0; |
| case CSSValueBottom: |
| ASSERT(!isHorizontal); |
| return size.height(); |
| case CSSValueRight: |
| ASSERT(isHorizontal); |
| return size.width(); |
| default: |
| break; |
| } |
| |
| return origin + sign * primitiveValue->computeLength<float>(conversionData); |
| } |
| |
| FloatPoint CSSGradientValue::computeEndPoint(CSSValue* horizontal, CSSValue* vertical, const CSSToLengthConversionData& conversionData, const IntSize& size) |
| { |
| FloatPoint result; |
| |
| if (horizontal) |
| result.setX(positionFromValue(horizontal, conversionData, size, true)); |
| |
| if (vertical) |
| result.setY(positionFromValue(vertical, conversionData, size, false)); |
| |
| return result; |
| } |
| |
| bool CSSGradientValue::isCacheable() const |
| { |
| for (size_t i = 0; i < m_stops.size(); ++i) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| |
| if (!stop.isHint() && stop.m_color->isPrimitiveValue() && toCSSPrimitiveValue(*stop.m_color).colorIsDerivedFromElement()) |
| return false; |
| |
| if (!stop.m_position) |
| continue; |
| |
| if (stop.m_position->isFontRelativeLength()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool CSSGradientValue::knownToBeOpaque(const LayoutObject* object) const |
| { |
| ASSERT(object); |
| for (auto& stop : m_stops) { |
| if (!stop.isHint() && resolveStopColor(*stop.m_color, *object).hasAlpha()) |
| return false; |
| } |
| return true; |
| } |
| |
| void CSSGradientValue::getStopColors(WillBeHeapVector<Color>& stopColors, const LayoutObject* object) const |
| { |
| ASSERT(object); |
| for (auto& stop : m_stops) { |
| if (!stop.isHint()) |
| stopColors.append(resolveStopColor(*stop.m_color, *object)); |
| } |
| |
| } |
| |
| DEFINE_TRACE_AFTER_DISPATCH(CSSGradientValue) |
| { |
| #if ENABLE(OILPAN) |
| visitor->trace(m_firstX); |
| visitor->trace(m_firstY); |
| visitor->trace(m_secondX); |
| visitor->trace(m_secondY); |
| visitor->trace(m_stops); |
| #endif |
| CSSImageGeneratorValue::traceAfterDispatch(visitor); |
| } |
| |
| String CSSLinearGradientValue::customCSSText() const |
| { |
| StringBuilder result; |
| if (m_gradientType == CSSDeprecatedLinearGradient) { |
| result.appendLiteral("-webkit-gradient(linear, "); |
| result.append(m_firstX->cssText()); |
| result.append(' '); |
| result.append(m_firstY->cssText()); |
| result.appendLiteral(", "); |
| result.append(m_secondX->cssText()); |
| result.append(' '); |
| result.append(m_secondY->cssText()); |
| appendCSSTextForDeprecatedColorStops(result); |
| } else if (m_gradientType == CSSPrefixedLinearGradient) { |
| if (m_repeating) |
| result.appendLiteral("-webkit-repeating-linear-gradient("); |
| else |
| result.appendLiteral("-webkit-linear-gradient("); |
| |
| if (m_angle) |
| result.append(m_angle->cssText()); |
| else { |
| if (m_firstX && m_firstY) { |
| result.append(m_firstX->cssText()); |
| result.append(' '); |
| result.append(m_firstY->cssText()); |
| } else if (m_firstX || m_firstY) { |
| if (m_firstX) |
| result.append(m_firstX->cssText()); |
| |
| if (m_firstY) |
| result.append(m_firstY->cssText()); |
| } |
| } |
| |
| for (unsigned i = 0; i < m_stops.size(); i++) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| result.appendLiteral(", "); |
| result.append(stop.m_color->cssText()); |
| if (stop.m_position) { |
| result.append(' '); |
| result.append(stop.m_position->cssText()); |
| } |
| } |
| } else { |
| if (m_repeating) |
| result.appendLiteral("repeating-linear-gradient("); |
| else |
| result.appendLiteral("linear-gradient("); |
| |
| bool wroteSomething = false; |
| |
| if (m_angle && m_angle->computeDegrees() != 180) { |
| result.append(m_angle->cssText()); |
| wroteSomething = true; |
| } else if ((m_firstX || m_firstY) && !(!m_firstX && m_firstY && m_firstY->isPrimitiveValue() && toCSSPrimitiveValue(m_firstY.get())->getValueID() == CSSValueBottom)) { |
| result.appendLiteral("to "); |
| if (m_firstX && m_firstY) { |
| result.append(m_firstX->cssText()); |
| result.append(' '); |
| result.append(m_firstY->cssText()); |
| } else if (m_firstX) |
| result.append(m_firstX->cssText()); |
| else |
| result.append(m_firstY->cssText()); |
| wroteSomething = true; |
| } |
| |
| if (wroteSomething) |
| result.appendLiteral(", "); |
| |
| for (unsigned i = 0; i < m_stops.size(); i++) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| if (i) |
| result.appendLiteral(", "); |
| if (stop.m_color) |
| result.append(stop.m_color->cssText()); |
| if (stop.m_color && stop.m_position) |
| result.append(' '); |
| if (stop.m_position) |
| result.append(stop.m_position->cssText()); |
| } |
| |
| } |
| |
| result.append(')'); |
| return result.toString(); |
| } |
| |
| // Compute the endpoints so that a gradient of the given angle covers a box of the given size. |
| static void endPointsFromAngle(float angleDeg, const IntSize& size, FloatPoint& firstPoint, FloatPoint& secondPoint, CSSGradientType type) |
| { |
| // Prefixed gradients use "polar coordinate" angles, rather than "bearing" angles. |
| if (type == CSSPrefixedLinearGradient) |
| angleDeg = 90 - angleDeg; |
| |
| angleDeg = fmodf(angleDeg, 360); |
| if (angleDeg < 0) |
| angleDeg += 360; |
| |
| if (!angleDeg) { |
| firstPoint.set(0, size.height()); |
| secondPoint.set(0, 0); |
| return; |
| } |
| |
| if (angleDeg == 90) { |
| firstPoint.set(0, 0); |
| secondPoint.set(size.width(), 0); |
| return; |
| } |
| |
| if (angleDeg == 180) { |
| firstPoint.set(0, 0); |
| secondPoint.set(0, size.height()); |
| return; |
| } |
| |
| if (angleDeg == 270) { |
| firstPoint.set(size.width(), 0); |
| secondPoint.set(0, 0); |
| return; |
| } |
| |
| // angleDeg is a "bearing angle" (0deg = N, 90deg = E), |
| // but tan expects 0deg = E, 90deg = N. |
| float slope = tan(deg2rad(90 - angleDeg)); |
| |
| // We find the endpoint by computing the intersection of the line formed by the slope, |
| // and a line perpendicular to it that intersects the corner. |
| float perpendicularSlope = -1 / slope; |
| |
| // Compute start corner relative to center, in Cartesian space (+y = up). |
| float halfHeight = size.height() / 2; |
| float halfWidth = size.width() / 2; |
| FloatPoint endCorner; |
| if (angleDeg < 90) |
| endCorner.set(halfWidth, halfHeight); |
| else if (angleDeg < 180) |
| endCorner.set(halfWidth, -halfHeight); |
| else if (angleDeg < 270) |
| endCorner.set(-halfWidth, -halfHeight); |
| else |
| endCorner.set(-halfWidth, halfHeight); |
| |
| // Compute c (of y = mx + c) using the corner point. |
| float c = endCorner.y() - perpendicularSlope * endCorner.x(); |
| float endX = c / (slope - perpendicularSlope); |
| float endY = perpendicularSlope * endX + c; |
| |
| // We computed the end point, so set the second point, |
| // taking into account the moved origin and the fact that we're in drawing space (+y = down). |
| secondPoint.set(halfWidth + endX, halfHeight - endY); |
| // Reflect around the center for the start point. |
| firstPoint.set(halfWidth - endX, halfHeight + endY); |
| } |
| |
| PassRefPtr<Gradient> CSSLinearGradientValue::createGradient(const CSSToLengthConversionData& conversionData, const IntSize& size, const LayoutObject& object) |
| { |
| ASSERT(!size.isEmpty()); |
| |
| FloatPoint firstPoint; |
| FloatPoint secondPoint; |
| if (m_angle) { |
| float angle = m_angle->computeDegrees(); |
| endPointsFromAngle(angle, size, firstPoint, secondPoint, m_gradientType); |
| } else { |
| switch (m_gradientType) { |
| case CSSDeprecatedLinearGradient: |
| firstPoint = computeEndPoint(m_firstX.get(), m_firstY.get(), conversionData, size); |
| if (m_secondX || m_secondY) |
| secondPoint = computeEndPoint(m_secondX.get(), m_secondY.get(), conversionData, size); |
| else { |
| if (m_firstX) |
| secondPoint.setX(size.width() - firstPoint.x()); |
| if (m_firstY) |
| secondPoint.setY(size.height() - firstPoint.y()); |
| } |
| break; |
| case CSSPrefixedLinearGradient: |
| firstPoint = computeEndPoint(m_firstX.get(), m_firstY.get(), conversionData, size); |
| if (m_firstX) |
| secondPoint.setX(size.width() - firstPoint.x()); |
| if (m_firstY) |
| secondPoint.setY(size.height() - firstPoint.y()); |
| break; |
| case CSSLinearGradient: |
| if (m_firstX && m_firstY) { |
| // "Magic" corners, so the 50% line touches two corners. |
| float rise = size.width(); |
| float run = size.height(); |
| if (m_firstX && m_firstX->isPrimitiveValue() && toCSSPrimitiveValue(m_firstX.get())->getValueID() == CSSValueLeft) |
| run *= -1; |
| if (m_firstY && m_firstY->isPrimitiveValue() && toCSSPrimitiveValue(m_firstY.get())->getValueID() == CSSValueBottom) |
| rise *= -1; |
| // Compute angle, and flip it back to "bearing angle" degrees. |
| float angle = 90 - rad2deg(atan2(rise, run)); |
| endPointsFromAngle(angle, size, firstPoint, secondPoint, m_gradientType); |
| } else if (m_firstX || m_firstY) { |
| secondPoint = computeEndPoint(m_firstX.get(), m_firstY.get(), conversionData, size); |
| if (m_firstX) |
| firstPoint.setX(size.width() - secondPoint.x()); |
| if (m_firstY) |
| firstPoint.setY(size.height() - secondPoint.y()); |
| } else |
| secondPoint.setY(size.height()); |
| break; |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| } |
| |
| RefPtr<Gradient> gradient = Gradient::create(firstPoint, secondPoint); |
| |
| gradient->setSpreadMethod(m_repeating ? SpreadMethodRepeat : SpreadMethodPad); |
| gradient->setDrawsInPMColorSpace(true); |
| |
| // Now add the stops. |
| addStops(gradient.get(), conversionData, object); |
| |
| return gradient.release(); |
| } |
| |
| bool CSSLinearGradientValue::equals(const CSSLinearGradientValue& other) const |
| { |
| if (m_gradientType == CSSDeprecatedLinearGradient) |
| return other.m_gradientType == m_gradientType |
| && compareCSSValuePtr(m_firstX, other.m_firstX) |
| && compareCSSValuePtr(m_firstY, other.m_firstY) |
| && compareCSSValuePtr(m_secondX, other.m_secondX) |
| && compareCSSValuePtr(m_secondY, other.m_secondY) |
| && m_stops == other.m_stops; |
| |
| if (m_repeating != other.m_repeating) |
| return false; |
| |
| if (m_angle) |
| return compareCSSValuePtr(m_angle, other.m_angle) && m_stops == other.m_stops; |
| |
| if (other.m_angle) |
| return false; |
| |
| bool equalXandY = false; |
| if (m_firstX && m_firstY) |
| equalXandY = compareCSSValuePtr(m_firstX, other.m_firstX) && compareCSSValuePtr(m_firstY, other.m_firstY); |
| else if (m_firstX) |
| equalXandY = compareCSSValuePtr(m_firstX, other.m_firstX) && !other.m_firstY; |
| else if (m_firstY) |
| equalXandY = compareCSSValuePtr(m_firstY, other.m_firstY) && !other.m_firstX; |
| else |
| equalXandY = !other.m_firstX && !other.m_firstY; |
| |
| return equalXandY && m_stops == other.m_stops; |
| } |
| |
| DEFINE_TRACE_AFTER_DISPATCH(CSSLinearGradientValue) |
| { |
| visitor->trace(m_angle); |
| CSSGradientValue::traceAfterDispatch(visitor); |
| } |
| |
| inline void CSSGradientValue::appendCSSTextForDeprecatedColorStops(StringBuilder& result) const |
| { |
| for (unsigned i = 0; i < m_stops.size(); i++) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| result.appendLiteral(", "); |
| if (stop.m_position->getDoubleValue() == 0) { |
| result.appendLiteral("from("); |
| result.append(stop.m_color->cssText()); |
| result.append(')'); |
| } else if (stop.m_position->getDoubleValue() == 1) { |
| result.appendLiteral("to("); |
| result.append(stop.m_color->cssText()); |
| result.append(')'); |
| } else { |
| result.appendLiteral("color-stop("); |
| result.appendNumber(stop.m_position->getDoubleValue()); |
| result.appendLiteral(", "); |
| result.append(stop.m_color->cssText()); |
| result.append(')'); |
| } |
| } |
| } |
| |
| String CSSRadialGradientValue::customCSSText() const |
| { |
| StringBuilder result; |
| |
| if (m_gradientType == CSSDeprecatedRadialGradient) { |
| result.appendLiteral("-webkit-gradient(radial, "); |
| result.append(m_firstX->cssText()); |
| result.append(' '); |
| result.append(m_firstY->cssText()); |
| result.appendLiteral(", "); |
| result.append(m_firstRadius->cssText()); |
| result.appendLiteral(", "); |
| result.append(m_secondX->cssText()); |
| result.append(' '); |
| result.append(m_secondY->cssText()); |
| result.appendLiteral(", "); |
| result.append(m_secondRadius->cssText()); |
| appendCSSTextForDeprecatedColorStops(result); |
| } else if (m_gradientType == CSSPrefixedRadialGradient) { |
| if (m_repeating) |
| result.appendLiteral("-webkit-repeating-radial-gradient("); |
| else |
| result.appendLiteral("-webkit-radial-gradient("); |
| |
| if (m_firstX && m_firstY) { |
| result.append(m_firstX->cssText()); |
| result.append(' '); |
| result.append(m_firstY->cssText()); |
| } else if (m_firstX) |
| result.append(m_firstX->cssText()); |
| else if (m_firstY) |
| result.append(m_firstY->cssText()); |
| else |
| result.appendLiteral("center"); |
| |
| if (m_shape || m_sizingBehavior) { |
| result.appendLiteral(", "); |
| if (m_shape) { |
| result.append(m_shape->cssText()); |
| result.append(' '); |
| } else |
| result.appendLiteral("ellipse "); |
| |
| if (m_sizingBehavior) |
| result.append(m_sizingBehavior->cssText()); |
| else |
| result.appendLiteral("cover"); |
| |
| } else if (m_endHorizontalSize && m_endVerticalSize) { |
| result.appendLiteral(", "); |
| result.append(m_endHorizontalSize->cssText()); |
| result.append(' '); |
| result.append(m_endVerticalSize->cssText()); |
| } |
| |
| for (unsigned i = 0; i < m_stops.size(); i++) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| result.appendLiteral(", "); |
| result.append(stop.m_color->cssText()); |
| if (stop.m_position) { |
| result.append(' '); |
| result.append(stop.m_position->cssText()); |
| } |
| } |
| } else { |
| if (m_repeating) |
| result.appendLiteral("repeating-radial-gradient("); |
| else |
| result.appendLiteral("radial-gradient("); |
| |
| bool wroteSomething = false; |
| |
| // The only ambiguous case that needs an explicit shape to be provided |
| // is when a sizing keyword is used (or all sizing is omitted). |
| if (m_shape && m_shape->getValueID() != CSSValueEllipse && (m_sizingBehavior || (!m_sizingBehavior && !m_endHorizontalSize))) { |
| result.appendLiteral("circle"); |
| wroteSomething = true; |
| } |
| |
| if (m_sizingBehavior && m_sizingBehavior->getValueID() != CSSValueFarthestCorner) { |
| if (wroteSomething) |
| result.append(' '); |
| result.append(m_sizingBehavior->cssText()); |
| wroteSomething = true; |
| } else if (m_endHorizontalSize) { |
| if (wroteSomething) |
| result.append(' '); |
| result.append(m_endHorizontalSize->cssText()); |
| if (m_endVerticalSize) { |
| result.append(' '); |
| result.append(m_endVerticalSize->cssText()); |
| } |
| wroteSomething = true; |
| } |
| |
| if (m_firstX || m_firstY) { |
| if (wroteSomething) |
| result.append(' '); |
| result.appendLiteral("at "); |
| if (m_firstX && m_firstY) { |
| result.append(m_firstX->cssText()); |
| result.append(' '); |
| result.append(m_firstY->cssText()); |
| } else if (m_firstX) |
| result.append(m_firstX->cssText()); |
| else |
| result.append(m_firstY->cssText()); |
| wroteSomething = true; |
| } |
| |
| if (wroteSomething) |
| result.appendLiteral(", "); |
| |
| for (unsigned i = 0; i < m_stops.size(); i++) { |
| const CSSGradientColorStop& stop = m_stops[i]; |
| if (i) |
| result.appendLiteral(", "); |
| if (stop.m_color) |
| result.append(stop.m_color->cssText()); |
| if (stop.m_color && stop.m_position) |
| result.append(' '); |
| if (stop.m_position) |
| result.append(stop.m_position->cssText()); |
| } |
| |
| } |
| |
| result.append(')'); |
| return result.toString(); |
| } |
| |
| float CSSRadialGradientValue::resolveRadius(CSSPrimitiveValue* radius, const CSSToLengthConversionData& conversionData, float* widthOrHeight) |
| { |
| float result = 0; |
| if (radius->isNumber()) // Can the radius be a percentage? |
| result = radius->getFloatValue() * conversionData.zoom(); |
| else if (widthOrHeight && radius->isPercentage()) |
| result = *widthOrHeight * radius->getFloatValue() / 100; |
| else |
| result = radius->computeLength<float>(conversionData); |
| |
| return std::max(result, 0.0f); |
| } |
| |
| namespace { |
| |
| enum EndShapeType { |
| CircleEndShape, |
| EllipseEndShape |
| }; |
| |
| // Compute the radius to the closest/farthest side (depending on the compare functor). |
| FloatSize radiusToSide(const FloatPoint& point, const FloatSize& size, EndShapeType shape, |
| bool (*compare)(float, float)) |
| { |
| float dx1 = fabs(point.x()); |
| float dy1 = fabs(point.y()); |
| float dx2 = fabs(point.x() - size.width()); |
| float dy2 = fabs(point.y() - size.height()); |
| |
| float dx = compare(dx1, dx2) ? dx1 : dx2; |
| float dy = compare(dy1, dy2) ? dy1 : dy2; |
| |
| if (shape == CircleEndShape) |
| return compare(dx, dy) ? FloatSize(dx, dx) : FloatSize(dy, dy); |
| |
| ASSERT(shape == EllipseEndShape); |
| return FloatSize(dx, dy); |
| } |
| |
| // Compute the radius of an ellipse with center at 0,0 which passes through p, and has |
| // width/height given by aspectRatio. |
| inline FloatSize ellipseRadius(const FloatPoint& p, float aspectRatio) |
| { |
| // x^2/a^2 + y^2/b^2 = 1 |
| // a/b = aspectRatio, b = a/aspectRatio |
| // a = sqrt(x^2 + y^2/(1/r^2)) |
| float a = sqrtf(p.x() * p.x() + p.y() * p.y() * aspectRatio * aspectRatio); |
| return FloatSize(a, a / aspectRatio); |
| } |
| |
| // Compute the radius to the closest/farthest corner (depending on the compare functor). |
| FloatSize radiusToCorner(const FloatPoint& point, const FloatSize& size, EndShapeType shape, |
| bool (*compare)(float, float)) |
| { |
| const FloatRect rect(FloatPoint(), size); |
| const FloatPoint corners[] = { |
| rect.minXMinYCorner(), |
| rect.maxXMinYCorner(), |
| rect.maxXMaxYCorner(), |
| rect.minXMaxYCorner() |
| }; |
| |
| unsigned cornerIndex = 0; |
| float distance = (point - corners[cornerIndex]).diagonalLength(); |
| for (unsigned i = 1; i < WTF_ARRAY_LENGTH(corners); ++i) { |
| float newDistance = (point - corners[i]).diagonalLength(); |
| if (compare(newDistance, distance)) { |
| cornerIndex = i; |
| distance = newDistance; |
| } |
| } |
| |
| if (shape == CircleEndShape) |
| return FloatSize(distance, distance); |
| |
| ASSERT(shape == EllipseEndShape); |
| // If the end shape is an ellipse, the gradient-shape has the same ratio of width to height |
| // that it would if closest-side or farthest-side were specified, as appropriate. |
| const FloatSize sideRadius = radiusToSide(point, size, EllipseEndShape, compare); |
| |
| return ellipseRadius(FloatPoint(corners[cornerIndex] - point), sideRadius.aspectRatio()); |
| } |
| |
| } // anonymous namespace |
| |
| PassRefPtr<Gradient> CSSRadialGradientValue::createGradient(const CSSToLengthConversionData& conversionData, const IntSize& size, const LayoutObject& object) |
| { |
| ASSERT(!size.isEmpty()); |
| |
| FloatPoint firstPoint = computeEndPoint(m_firstX.get(), m_firstY.get(), conversionData, size); |
| if (!m_firstX) |
| firstPoint.setX(size.width() / 2); |
| if (!m_firstY) |
| firstPoint.setY(size.height() / 2); |
| |
| FloatPoint secondPoint = computeEndPoint(m_secondX.get(), m_secondY.get(), conversionData, size); |
| if (!m_secondX) |
| secondPoint.setX(size.width() / 2); |
| if (!m_secondY) |
| secondPoint.setY(size.height() / 2); |
| |
| float firstRadius = 0; |
| if (m_firstRadius) |
| firstRadius = resolveRadius(m_firstRadius.get(), conversionData); |
| |
| FloatSize secondRadius(0, 0); |
| if (m_secondRadius) { |
| secondRadius.setWidth(resolveRadius(m_secondRadius.get(), conversionData)); |
| secondRadius.setHeight(secondRadius.width()); |
| } else if (m_endHorizontalSize) { |
| float width = size.width(); |
| float height = size.height(); |
| secondRadius.setWidth(resolveRadius(m_endHorizontalSize.get(), conversionData, &width)); |
| secondRadius.setHeight(m_endVerticalSize |
| ? resolveRadius(m_endVerticalSize.get(), conversionData, &height) |
| : secondRadius.width()); |
| } else { |
| EndShapeType shape = (m_shape && m_shape->getValueID() == CSSValueCircle) || |
| (!m_shape && !m_sizingBehavior && m_endHorizontalSize && !m_endVerticalSize) |
| ? CircleEndShape |
| : EllipseEndShape; |
| |
| FloatSize floatSize(size); |
| switch (m_sizingBehavior ? m_sizingBehavior->getValueID() : 0) { |
| case CSSValueContain: |
| case CSSValueClosestSide: |
| secondRadius = radiusToSide(secondPoint, floatSize, shape, |
| [] (float a, float b) { return a < b; }); |
| break; |
| case CSSValueFarthestSide: |
| secondRadius = radiusToSide(secondPoint, floatSize, shape, |
| [] (float a, float b) { return a > b; }); |
| break; |
| case CSSValueClosestCorner: |
| secondRadius = radiusToCorner(secondPoint, floatSize, shape, |
| [] (float a, float b) { return a < b; }); |
| break; |
| default: |
| secondRadius = radiusToCorner(secondPoint, floatSize, shape, |
| [] (float a, float b) { return a > b; }); |
| break; |
| } |
| } |
| |
| bool isDegenerate = !secondRadius.width() || !secondRadius.height(); |
| RefPtr<Gradient> gradient = Gradient::create(firstPoint, firstRadius, secondPoint, |
| isDegenerate ? 0 : secondRadius.width(), isDegenerate ? 1 : secondRadius.aspectRatio()); |
| |
| gradient->setSpreadMethod(m_repeating ? SpreadMethodRepeat : SpreadMethodPad); |
| gradient->setDrawsInPMColorSpace(true); |
| |
| // Now add the stops. |
| addStops(gradient.get(), conversionData, object); |
| |
| return gradient.release(); |
| } |
| |
| bool CSSRadialGradientValue::equals(const CSSRadialGradientValue& other) const |
| { |
| if (m_gradientType == CSSDeprecatedRadialGradient) |
| return other.m_gradientType == m_gradientType |
| && compareCSSValuePtr(m_firstX, other.m_firstX) |
| && compareCSSValuePtr(m_firstY, other.m_firstY) |
| && compareCSSValuePtr(m_secondX, other.m_secondX) |
| && compareCSSValuePtr(m_secondY, other.m_secondY) |
| && compareCSSValuePtr(m_firstRadius, other.m_firstRadius) |
| && compareCSSValuePtr(m_secondRadius, other.m_secondRadius) |
| && m_stops == other.m_stops; |
| |
| if (m_repeating != other.m_repeating) |
| return false; |
| |
| bool equalXandY = false; |
| if (m_firstX && m_firstY) |
| equalXandY = compareCSSValuePtr(m_firstX, other.m_firstX) && compareCSSValuePtr(m_firstY, other.m_firstY); |
| else if (m_firstX) |
| equalXandY = compareCSSValuePtr(m_firstX, other.m_firstX) && !other.m_firstY; |
| else if (m_firstY) |
| equalXandY = compareCSSValuePtr(m_firstY, other.m_firstY) && !other.m_firstX; |
| else |
| equalXandY = !other.m_firstX && !other.m_firstY; |
| |
| if (!equalXandY) |
| return false; |
| |
| bool equalShape = true; |
| bool equalSizingBehavior = true; |
| bool equalHorizontalAndVerticalSize = true; |
| |
| if (m_shape) |
| equalShape = compareCSSValuePtr(m_shape, other.m_shape); |
| else if (m_sizingBehavior) |
| equalSizingBehavior = compareCSSValuePtr(m_sizingBehavior, other.m_sizingBehavior); |
| else if (m_endHorizontalSize && m_endVerticalSize) |
| equalHorizontalAndVerticalSize = compareCSSValuePtr(m_endHorizontalSize, other.m_endHorizontalSize) && compareCSSValuePtr(m_endVerticalSize, other.m_endVerticalSize); |
| else { |
| equalShape = !other.m_shape; |
| equalSizingBehavior = !other.m_sizingBehavior; |
| equalHorizontalAndVerticalSize = !other.m_endHorizontalSize && !other.m_endVerticalSize; |
| } |
| return equalShape && equalSizingBehavior && equalHorizontalAndVerticalSize && m_stops == other.m_stops; |
| } |
| |
| DEFINE_TRACE_AFTER_DISPATCH(CSSRadialGradientValue) |
| { |
| visitor->trace(m_firstRadius); |
| visitor->trace(m_secondRadius); |
| visitor->trace(m_shape); |
| visitor->trace(m_sizingBehavior); |
| visitor->trace(m_endHorizontalSize); |
| visitor->trace(m_endVerticalSize); |
| CSSGradientValue::traceAfterDispatch(visitor); |
| } |
| |
| } // namespace blink |