| /* |
| * Copyright (C) 2021 Apple 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 AND ITS CONTRIBUTORS "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 OR ITS 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 "config.h" |
| #include "GradientRendererCG.h" |
| |
| #include "ColorInterpolation.h" |
| #include "ColorSpaceCG.h" |
| #include "GradientColorStops.h" |
| #include <pal/spi/cg/CoreGraphicsSPI.h> |
| |
| namespace WebCore { |
| |
| GradientRendererCG::GradientRendererCG(ColorInterpolationMethod colorInterpolationMethod, const GradientColorStops& stops) |
| : m_strategy { pickStrategy(colorInterpolationMethod, stops) } |
| { |
| } |
| |
| // MARK: - Strategy selection. |
| |
| #if !HAVE(CORE_GRAPHICS_PREMULTIPLIED_INTERPOLATION_GRADIENT) |
| |
| enum class EmulatedAlphaPremuliplicationAnalysisResult { |
| UseGradientAsIs, |
| UseGradientWithAlphaTransforms, |
| UseShading |
| }; |
| |
| enum class AlphaType { |
| Opaque, |
| PartiallyTransparent, |
| FullyTransparent |
| }; |
| |
| static constexpr AlphaType classifyAlphaType(float alpha) |
| { |
| if (alpha == 1.0f) |
| return AlphaType::Opaque; |
| if (alpha == 0.0f) |
| return AlphaType::FullyTransparent; |
| return AlphaType::PartiallyTransparent; |
| } |
| |
| static EmulatedAlphaPremuliplicationAnalysisResult analyzeColorStopsForEmulatedAlphaPremuliplicationOppertunity(const GradientColorStops& stops) |
| { |
| if (stops.size() < 2) |
| return EmulatedAlphaPremuliplicationAnalysisResult::UseGradientAsIs; |
| |
| bool uniformAlpha = true; |
| |
| auto& initialStop = *stops.begin(); |
| auto previousStopAlpha = initialStop.color.alphaAsFloat(); |
| auto previousStopAlphaType = classifyAlphaType(previousStopAlpha); |
| |
| auto stopIterator = stops.begin(); |
| ++stopIterator; |
| |
| while (stopIterator != stops.end()) { |
| auto& stop = *stopIterator; |
| |
| auto stopAlpha = stop.color.alphaAsFloat(); |
| auto stopAlphaType = classifyAlphaType(stopAlpha); |
| |
| switch (stopAlphaType) { |
| case AlphaType::Opaque: |
| switch (previousStopAlphaType) { |
| case AlphaType::Opaque: |
| break; |
| case AlphaType::PartiallyTransparent: |
| return EmulatedAlphaPremuliplicationAnalysisResult::UseShading; |
| case AlphaType::FullyTransparent: |
| uniformAlpha = false; |
| break; |
| } |
| break; |
| |
| case AlphaType::PartiallyTransparent: |
| switch (previousStopAlphaType) { |
| case AlphaType::Opaque: |
| return EmulatedAlphaPremuliplicationAnalysisResult::UseShading; |
| case AlphaType::PartiallyTransparent: |
| if (previousStopAlpha != stopAlpha) |
| return EmulatedAlphaPremuliplicationAnalysisResult::UseShading; |
| break; |
| case AlphaType::FullyTransparent: |
| uniformAlpha = false; |
| break; |
| } |
| break; |
| |
| case AlphaType::FullyTransparent: |
| switch (previousStopAlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| uniformAlpha = false; |
| break; |
| |
| case AlphaType::FullyTransparent: |
| break; |
| } |
| break; |
| } |
| |
| previousStopAlpha = stopAlpha; |
| previousStopAlphaType = stopAlphaType; |
| |
| ++stopIterator; |
| } |
| |
| return uniformAlpha ? EmulatedAlphaPremuliplicationAnalysisResult::UseGradientAsIs : EmulatedAlphaPremuliplicationAnalysisResult::UseGradientWithAlphaTransforms; |
| } |
| |
| static GradientColorStops alphaTransformStopsToEmulateAlphaPremuliplication(const GradientColorStops& stops) |
| { |
| // The following is the set of transforms that can be performed on a color stop list to transform it from one used with a premuliplied alpha gradient |
| // implmentation to one used by with an un-premultiplied gradient implementation. |
| |
| // ... Opaque -> Transparent -> Opaque ... ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} | ADDITION{Transparent(NextChannels)} -> Opaque ... |
| // ... Partial -> Transparent -> Partial ... ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} | ADDITION{Transparent(NextChannels)} -> Partial ... |
| // ... Opaque -> Transparent -> Partial ... ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} | ADDITION{Transparent(NextChannels)} -> Partial ... |
| // ... Partial -> Transparent -> Opaque ... ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} | ADDITION{Transparent(NextChannels)} -> Opaque ... |
| // |
| // ... Opaque -> Transparent -> Transparent -> Opaque ... ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} -> TRANSFORM{Transparent(NextChannels)} -> Opaque ... |
| // ... Partial -> Transparent -> Transparent -> Partial ... ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} -> TRANSFORM{Transparent(NextChannels)} -> Partial ... |
| // ... Opaque -> Transparent -> Transparent -> Partial ... ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} -> TRANSFORM{Transparent(NextChannels)} -> Partial ... |
| // ... Partial -> Transparent -> Transparent -> Opaque ... ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} -> TRANSFORM{Transparent(NextChannels)} -> Opaque ... |
| // |
| // ... Opaque -> Transparent -> Transparent -> Transparent -> Opaque ... ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} -> Transparent -> TRANSFORM{Transparent(NextChannels)} -> Opaque ... |
| // ... Partial -> Transparent -> Transparent -> Transparent -> Partial ... ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} -> Transparent -> TRANSFORM{Transparent(NextChannels)} -> Partial ... |
| // ... Opaque -> Transparent -> Transparent -> Transparent -> Partial ... ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} -> Transparent -> TRANSFORM{Transparent(NextChannels)} -> Partial ... |
| // ... Partial -> Transparent -> Transparent -> Transparent -> Opaque ... ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} -> Transparent -> TRANSFORM{Transparent(NextChannels)} -> Opaque ... |
| // |
| // [ Transparent -> Opaque ... ==> [ TRANSFORM{Transparent(NextChannels)} -> Opaque ... |
| // [ Transparent -> Partial ... ==> [ TRANSFORM{Transparent(NextChannels)} -> Partial ... |
| // [ Transparent -> Transparent ... ==> [ Transparent -> Transparent ... |
| // |
| // ... Opaque -> Transparent ] ==> ... Opaque -> TRANSFORM{Transparent(PreviousChannels)} ] |
| // ... Partial -> Transparent ] ==> ... Partial -> TRANSFORM{Transparent(PreviousChannels)} ] |
| // ... Transparent -> Transparent ] ==> ... Transparent -> Transparent ] |
| |
| // For each stop the following actions are possible: |
| // - Default Append self |
| // - Steal previous Append previous.colorWithAlpha(0.0f) |
| // - Steal next Append next.colorWithAlpha(0.0f) |
| // - Split Append previous.colorWithAlpha(0.0f) + next.colorWithAlpha(0.0f) |
| |
| ASSERT(stops.size() > 1); |
| |
| // Special case when we only have two stops to avoid complicating the cases with more. |
| if (stops.size() == 2) { |
| // Illegal pairs (ruled out in analysis) |
| // Opaque -> Partial |
| // Partial -> Opaque |
| // |
| // Possible pairs |
| // Opaque -> Opaque (default, default) |
| // Partial -> Partial (default, default) |
| // Transparent -> Transparent (default, default) |
| // Opaque -> Transparent (default, steal previous) |
| // Partial -> Transparent (default, steal previous) |
| // Transparent -> Opaque (steals next, default) |
| // Transparent -> Partial (steals next, default) |
| |
| GradientColorStops::StopVector result; |
| result.reserveInitialCapacity(2); |
| |
| auto& stop1 = stops.stops()[0]; |
| auto& stop2 = stops.stops()[1]; |
| |
| auto stop1AlphaType = classifyAlphaType(stop1.color.alphaAsFloat()); |
| auto stop2AlphaType = classifyAlphaType(stop2.color.alphaAsFloat()); |
| |
| switch (stop1AlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| // ACTION (stop1): Default. |
| result.uncheckedAppend(stop1); |
| |
| switch (stop2AlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| // ACTION (stop2): Default. |
| result.uncheckedAppend(stop2); |
| break; |
| case AlphaType::FullyTransparent: |
| // ACTION (stop2): Steal previous. |
| result.uncheckedAppend({ stop2.offset, stop1.color.colorWithAlpha(0.0f) }); |
| break; |
| } |
| |
| break; |
| |
| case AlphaType::FullyTransparent: |
| switch (stop2AlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| // ACTION (stop1): Steal next. |
| result.uncheckedAppend({ stop1.offset, stop2.color.colorWithAlpha(0.0f) }); |
| break; |
| case AlphaType::FullyTransparent: |
| // ACTION (stop1): Default. |
| result.uncheckedAppend(stop1); |
| break; |
| } |
| // ACTION (stop2): Default. |
| result.uncheckedAppend(stop2); |
| break; |
| } |
| |
| return GradientColorStops::Sorted { WTFMove(result) }; |
| } |
| |
| GradientColorStops::StopVector result; |
| |
| // 1. Handle first stop. |
| // |
| // [first stop matters] |
| // |
| // Illegal pairs (ruled out in analysis) |
| // Opaque -> Partial |
| // Partial -> Opaque |
| // |
| // Possible pairs |
| // Opaque -> Opaque (default) |
| // Partial -> Partial (default) |
| // Transparent -> Transparent (default) |
| // Opaque -> Transparent (default) |
| // Partial -> Transparent (default) |
| // Transparent -> Opaque (steals next) |
| // Transparent -> Partial (steals next) |
| |
| auto& firstStop = stops.stops()[0]; |
| auto& secondStop = stops.stops()[1]; |
| |
| auto firstStopAlphaType = classifyAlphaType(firstStop.color.alphaAsFloat()); |
| auto secondStopAlphaType = classifyAlphaType(secondStop.color.alphaAsFloat()); |
| |
| if (firstStopAlphaType == AlphaType::FullyTransparent && secondStopAlphaType != AlphaType::FullyTransparent) { |
| // ACTION: Steal next. |
| result.append({ firstStop.offset, secondStop.color.colorWithAlpha(0.0f) }); |
| } else { |
| // ACTION: Default. |
| result.append(firstStop); |
| } |
| |
| // 2. Handle middle stops. |
| // |
| // [middle stop matters] |
| // |
| // Illegal triplets (ruled out in analysis) |
| // Opaque -> Opaque -> Partial |
| // Opaque -> Partial -> Partial |
| // Opaque -> Partial -> Opaque |
| // Partial -> Opaque -> Opaque |
| // Partial -> Partial -> Opaque |
| // Partial -> Opaque -> Partial |
| // |
| // Possible triplets |
| // Opaque -> Opaque -> Opaque (default) |
| // Partial -> Partial -> Partial (default) |
| // Opaque -> Opaque -> Transparent (default) |
| // Opaque -> Partial -> Transparent (default) |
| // Partial -> Partial -> Transparent (default) |
| // Partial -> Opaque -> Transparent (default) |
| // Transparent -> Opaque -> Transparent (default) |
| // Transparent -> Partial -> Transparent (default) |
| // Transparent -> Transparent -> Transparent (default) |
| // Opaque -> Transparent -> Opaque (splits, steals previous + next) |
| // Opaque -> Transparent -> Partial (splits, steals previous + next) |
| // Partial -> Transparent -> Partial (splits, steals previous + next) |
| // Partial -> Transparent -> Opaque (splits, steals previous + next) |
| // Transparent -> Transparent -> Opaque (steals next) |
| // Transparent -> Transparent -> Partial (steals next) |
| // Opaque -> Transparent -> Transparent (steals previous) |
| // Partial -> Transparent -> Transparent (steals previous) |
| |
| size_t previousStopIndex = 0; |
| size_t stopIndex = 1; |
| size_t nextStopIndex = 2; |
| |
| for (; nextStopIndex < stops.size(); ++previousStopIndex, ++stopIndex, ++nextStopIndex) { |
| auto& previousStop = stops.stops()[previousStopIndex]; |
| auto& stop = stops.stops()[stopIndex]; |
| auto& nextStop = stops.stops()[nextStopIndex]; |
| |
| auto previousStopAlphaType = classifyAlphaType(previousStop.color.alphaAsFloat()); |
| auto stopAlphaType = classifyAlphaType(stop.color.alphaAsFloat()); |
| auto nextStopAlphaType = classifyAlphaType(nextStop.color.alphaAsFloat()); |
| |
| if (stopAlphaType == AlphaType::FullyTransparent) { |
| switch (previousStopAlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| switch (nextStopAlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| // ACTION: Split. |
| result.append({ stop.offset, previousStop.color.colorWithAlpha(0.0f) }); |
| result.append({ stop.offset, nextStop.color.colorWithAlpha(0.0f) }); |
| break; |
| |
| case AlphaType::FullyTransparent: |
| // ACTION: Steal previous. |
| result.append({ stop.offset, previousStop.color.colorWithAlpha(0.0f) }); |
| break; |
| } |
| break; |
| |
| case AlphaType::FullyTransparent: |
| switch (nextStopAlphaType) { |
| case AlphaType::Opaque: |
| case AlphaType::PartiallyTransparent: |
| // ACTION: Steal next. |
| result.append({ stop.offset, nextStop.color.colorWithAlpha(0.0f) }); |
| break; |
| |
| case AlphaType::FullyTransparent: |
| // ACTION: Default. |
| result.append(stop); |
| break; |
| } |
| break; |
| } |
| } else { |
| // ACTION: Default. |
| result.append(stop); |
| } |
| } |
| |
| ASSERT(nextStopIndex == stops.size()); |
| |
| // 3. Handle last stop. |
| // |
| // [last stop matters] |
| // |
| // Illegal pairs (ruled out in analysis phase) |
| // Opaque -> Partial |
| // Partial -> Opaque |
| // |
| // Possible pairs |
| // Opaque -> Opaque (default) |
| // Partial -> Partial (default) |
| // Transparent -> Transparent (default) |
| // Opaque -> Transparent (steals previous) |
| // Partial -> Transparent (steals previous) |
| // Transparent -> Opaque (default) |
| // Transparent -> Partial (default) |
| |
| auto& secondToLastStop = stops.stops()[previousStopIndex]; |
| auto& lastStop = stops.stops()[stopIndex]; |
| |
| auto secondToLastStopAlphaType = classifyAlphaType(secondToLastStop.color.alphaAsFloat()); |
| auto lastStopAlphaType = classifyAlphaType(lastStop.color.alphaAsFloat()); |
| |
| if (lastStopAlphaType == AlphaType::FullyTransparent && secondToLastStopAlphaType != AlphaType::FullyTransparent) { |
| // ACTION: Steal previous. |
| result.append({ lastStop.offset, secondToLastStop.color.colorWithAlpha(0.0f) }); |
| } else { |
| // ACTION: Default. |
| result.append(lastStop); |
| } |
| |
| return GradientColorStops::Sorted { WTFMove(result) }; |
| } |
| #endif |
| |
| static bool anyComponentIsNone(const GradientColorStops& stops) |
| { |
| for (auto& stop : stops) { |
| if (stop.color.anyComponentIsNone()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| GradientRendererCG::Strategy GradientRendererCG::pickStrategy(ColorInterpolationMethod colorInterpolationMethod, const GradientColorStops& stops) const |
| { |
| return WTF::switchOn(colorInterpolationMethod.colorSpace, |
| [&] (const ColorInterpolationMethod::SRGB&) -> Strategy { |
| // FIXME: As an optimization we can precompute 'none' replacements and create a transformed stop list rather than falling back on CGShadingRef. |
| if (anyComponentIsNone(stops)) |
| return makeShading(colorInterpolationMethod, stops); |
| |
| switch (colorInterpolationMethod.alphaPremultiplication) { |
| case AlphaPremultiplication::Unpremultiplied: |
| return makeGradient(colorInterpolationMethod, stops); |
| case AlphaPremultiplication::Premultiplied: |
| #if HAVE(CORE_GRAPHICS_PREMULTIPLIED_INTERPOLATION_GRADIENT) |
| return makeGradient(colorInterpolationMethod, stops); |
| #else |
| switch (analyzeColorStopsForEmulatedAlphaPremuliplicationOppertunity(stops)) { |
| case EmulatedAlphaPremuliplicationAnalysisResult::UseGradientAsIs: |
| return makeGradient({ ColorInterpolationMethod::SRGB { }, AlphaPremultiplication::Unpremultiplied }, stops); |
| case EmulatedAlphaPremuliplicationAnalysisResult::UseGradientWithAlphaTransforms: |
| return makeGradient({ ColorInterpolationMethod::SRGB { }, AlphaPremultiplication::Unpremultiplied }, alphaTransformStopsToEmulateAlphaPremuliplication(stops)); |
| case EmulatedAlphaPremuliplicationAnalysisResult::UseShading: |
| return makeShading(colorInterpolationMethod, stops); |
| } |
| #endif |
| } |
| }, |
| [&] (const auto&) -> Strategy { |
| return makeShading(colorInterpolationMethod, stops); |
| } |
| ); |
| } |
| |
| // MARK: - Gradient strategy. |
| |
| GradientRendererCG::Strategy GradientRendererCG::makeGradient(ColorInterpolationMethod colorInterpolationMethod, const GradientColorStops& stops) const |
| { |
| ASSERT_UNUSED(colorInterpolationMethod, std::holds_alternative<ColorInterpolationMethod::SRGB>(colorInterpolationMethod.colorSpace)); |
| #if !HAVE(CORE_GRAPHICS_PREMULTIPLIED_INTERPOLATION_GRADIENT) |
| ASSERT_UNUSED(colorInterpolationMethod, colorInterpolationMethod.alphaPremultiplication == AlphaPremultiplication::Unpremultiplied); |
| #endif |
| |
| #if HAVE(CORE_GRAPHICS_GRADIENT_CREATE_WITH_OPTIONS) |
| #if HAVE(CORE_GRAPHICS_PREMULTIPLIED_INTERPOLATION_GRADIENT) |
| auto gradientInterpolatesPremultipliedOptionsDictionary = [] () -> CFDictionaryRef { |
| static CFTypeRef keys[] = { kCGGradientInterpolatesPremultiplied }; |
| static CFTypeRef values[] = { kCFBooleanTrue }; |
| static CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, keys, values, WTF_ARRAY_LENGTH(keys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); |
| |
| return options; |
| }; |
| |
| auto gradientOptionsDictionary = [&] (auto colorInterpolationMethod) -> CFDictionaryRef { |
| switch (colorInterpolationMethod.alphaPremultiplication) { |
| case AlphaPremultiplication::Unpremultiplied: |
| return nullptr; |
| case AlphaPremultiplication::Premultiplied: |
| return gradientInterpolatesPremultipliedOptionsDictionary(); |
| } |
| }; |
| #else |
| auto gradientOptionsDictionary = [] (auto) -> CFDictionaryRef { |
| return nullptr; |
| }; |
| #endif |
| #endif |
| |
| auto hasOnlyBoundedSRGBColorStops = [] (const auto& stops) { |
| for (const auto& stop : stops) { |
| if (stop.color.colorSpace() != ColorSpace::SRGB) |
| return false; |
| } |
| return true; |
| }; |
| |
| auto numberOfStops = stops.size(); |
| |
| static constexpr auto reservedStops = 3; |
| Vector<CGFloat, reservedStops> locations; |
| locations.reserveInitialCapacity(numberOfStops); |
| |
| Vector<CGFloat, 4 * reservedStops> colorComponents; |
| colorComponents.reserveInitialCapacity(numberOfStops * 4); |
| |
| auto cgColorSpace = [&] { |
| // FIXME: Now that we only ever use CGGradientCreateWithColorComponents, we should investigate |
| // if there is any real benefit to using sRGB when all the stops are bounded vs just using |
| // extended sRGB for all gradients. |
| if (hasOnlyBoundedSRGBColorStops(stops)) { |
| for (const auto& stop : stops) { |
| auto [r, g, b, a] = stop.color.toColorTypeLossy<SRGBA<float>>().resolved(); |
| colorComponents.uncheckedAppend(r); |
| colorComponents.uncheckedAppend(g); |
| colorComponents.uncheckedAppend(b); |
| colorComponents.uncheckedAppend(a); |
| |
| locations.uncheckedAppend(stop.offset); |
| } |
| return cachedCGColorSpace<ColorSpaceFor<SRGBA<float>>>(); |
| } |
| |
| using OutputSpaceColorType = std::conditional_t<HasCGColorSpaceMapping<ColorSpace::ExtendedSRGB>, ExtendedSRGBA<float>, SRGBA<float>>; |
| for (const auto& stop : stops) { |
| auto [r, g, b, a] = stop.color.toColorTypeLossy<OutputSpaceColorType>().resolved(); |
| colorComponents.uncheckedAppend(r); |
| colorComponents.uncheckedAppend(g); |
| colorComponents.uncheckedAppend(b); |
| colorComponents.uncheckedAppend(a); |
| |
| locations.uncheckedAppend(stop.offset); |
| } |
| return cachedCGColorSpace<ColorSpaceFor<OutputSpaceColorType>>(); |
| }(); |
| |
| #if HAVE(CORE_GRAPHICS_GRADIENT_CREATE_WITH_OPTIONS) |
| return Gradient { adoptCF(CGGradientCreateWithColorComponentsAndOptions(cgColorSpace, colorComponents.data(), locations.data(), numberOfStops, gradientOptionsDictionary(colorInterpolationMethod))) }; |
| #else |
| return Gradient { adoptCF(CGGradientCreateWithColorComponents(cgColorSpace, colorComponents.data(), locations.data(), numberOfStops)) }; |
| #endif |
| } |
| |
| // MARK: - Shading strategy. |
| |
| template<typename InterpolationSpace, AlphaPremultiplication alphaPremultiplication> |
| void GradientRendererCG::Shading::shadingFunction(void* info, const CGFloat* in, CGFloat* out) |
| { |
| using InterpolationSpaceColorType = typename InterpolationSpace::ColorType; |
| using OutputSpaceColorType = std::conditional_t<HasCGColorSpaceMapping<ColorSpace::ExtendedSRGB>, ExtendedSRGBA<float>, SRGBA<float>>; |
| |
| auto* data = static_cast<GradientRendererCG::Shading::Data*>(info); |
| |
| // Compute color at offset 'in[0]' and assign the components to out[0 -> 3]. |
| |
| float requestedOffset = in[0]; |
| |
| // 1. Find stops that bound the requested offset. |
| auto [stop0, stop1] = [&] { |
| for (size_t stop = 1; stop < data->stops().size(); ++stop) { |
| if (requestedOffset <= data->stops()[stop].offset) |
| return std::tie(data->stops()[stop - 1], data->stops()[stop]); |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| }(); |
| |
| // 2. Compute percentage offset between the two stops. |
| float offset = (stop1.offset == stop0.offset) ? 0.0f : (requestedOffset - stop0.offset) / (stop1.offset - stop0.offset); |
| |
| // 3. Interpolate the two stops' colors by the computed offset. |
| // FIXME: We don't want to due hue fixup for each call, so we should figure out how to precompute that. |
| auto interpolatedColor = interpolateColorComponents<alphaPremultiplication>( |
| std::get<InterpolationSpace>(data->colorInterpolationMethod().colorSpace), |
| makeFromComponents<InterpolationSpaceColorType>(stop0.colorComponents), 1.0f - offset, |
| makeFromComponents<InterpolationSpaceColorType>(stop1.colorComponents), offset); |
| |
| // 4. Convert to the output color space. |
| auto interpolatedColorConvertedToOutputSpace = asColorComponents(convertColor<OutputSpaceColorType>(interpolatedColor).resolved()); |
| |
| // 5. Write color components to 'out' pointer. |
| for (size_t componentIndex = 0; componentIndex < interpolatedColorConvertedToOutputSpace.size(); ++componentIndex) |
| out[componentIndex] = interpolatedColorConvertedToOutputSpace[componentIndex]; |
| } |
| |
| GradientRendererCG::Strategy GradientRendererCG::makeShading(ColorInterpolationMethod colorInterpolationMethod, const GradientColorStops& stops) const |
| { |
| using OutputSpaceColorType = std::conditional_t<HasCGColorSpaceMapping<ColorSpace::ExtendedSRGB>, ExtendedSRGBA<float>, SRGBA<float>>; |
| |
| auto makeData = [&] (auto colorInterpolationMethod, auto& stops) { |
| auto convertColorToColorInterpolationSpace = [&] (const Color& color, auto colorInterpolationMethod) -> ColorComponents<float, 4> { |
| return WTF::switchOn(colorInterpolationMethod.colorSpace, |
| [&] (auto& colorSpace) -> ColorComponents<float, 4> { |
| using ColorType = typename std::remove_reference_t<decltype(colorSpace)>::ColorType; |
| return asColorComponents(color.template toColorTypeLossy<ColorType>().unresolved()); |
| } |
| ); |
| }; |
| |
| auto totalNumberOfStops = stops.size(); |
| bool hasZero = false; |
| bool hasOne = false; |
| |
| for (const auto& stop : stops) { |
| auto offset = stop.offset; |
| ASSERT(offset >= 0); |
| ASSERT(offset <= 1); |
| |
| if (offset == 0) |
| hasZero = true; |
| else if (offset == 1) |
| hasOne = true; |
| } |
| |
| if (!hasZero) |
| totalNumberOfStops++; |
| if (!hasOne) |
| totalNumberOfStops++; |
| |
| // FIXME: To avoid duplicate work in the shader function, we could precompute a few things: |
| // - If we have a polar coordinate color space, we can pre-fixup the hues, inserting an extra stop at the same offset if both the fixup on the left and right require different results. |
| // - If we have 'none' components, we can precompute 'none' replacements, inserting an extra stop at the same offset if the replacements on the left and right are different. |
| |
| Vector<ColorConvertedToInterpolationColorSpaceStop> convertedStops; |
| convertedStops.reserveInitialCapacity(totalNumberOfStops); |
| |
| if (!hasZero) |
| convertedStops.uncheckedAppend({ 0.0f, { 0.0f, 0.0f, 0.0f, 0.0f } }); |
| |
| for (const auto& stop : stops) |
| convertedStops.uncheckedAppend({ stop.offset, convertColorToColorInterpolationSpace(stop.color, colorInterpolationMethod) }); |
| |
| if (!hasOne) |
| convertedStops.uncheckedAppend({ 1.0f, convertedStops.last().colorComponents }); |
| |
| if (!hasZero) |
| convertedStops[0].colorComponents = convertedStops[1].colorComponents; |
| |
| return Shading::Data::create(colorInterpolationMethod, WTFMove(convertedStops)); |
| }; |
| |
| auto makeFunction = [&] (auto colorInterpolationMethod, auto& data) { |
| auto makeEvaluateCallback = [&] (auto colorInterpolationMethod) -> CGFunctionEvaluateCallback { |
| return WTF::switchOn(colorInterpolationMethod.colorSpace, |
| [&] (auto& colorSpace) -> CGFunctionEvaluateCallback { |
| using InterpolationMethodColorSpace = typename std::remove_reference_t<decltype(colorSpace)>; |
| |
| switch (colorInterpolationMethod.alphaPremultiplication) { |
| case AlphaPremultiplication::Unpremultiplied: |
| return &Shading::shadingFunction<InterpolationMethodColorSpace, AlphaPremultiplication::Unpremultiplied>; |
| case AlphaPremultiplication::Premultiplied: |
| return &Shading::shadingFunction<InterpolationMethodColorSpace, AlphaPremultiplication::Premultiplied>; |
| } |
| } |
| ); |
| }; |
| |
| const CGFunctionCallbacks callbacks = { |
| 0, |
| makeEvaluateCallback(colorInterpolationMethod), |
| [] (void* info) { |
| static_cast<GradientRendererCG::Shading::Data*>(info)->deref(); |
| } |
| }; |
| |
| constexpr auto outputSpaceComponentInfo = OutputSpaceColorType::Model::componentInfo; |
| |
| static constexpr std::array<CGFloat, 2> domain = { 0, 1 }; |
| static constexpr std::array<CGFloat, 8> range = { |
| outputSpaceComponentInfo[0].min, outputSpaceComponentInfo[0].max, |
| outputSpaceComponentInfo[1].min, outputSpaceComponentInfo[1].max, |
| outputSpaceComponentInfo[2].min, outputSpaceComponentInfo[2].max, |
| 0, 1 |
| }; |
| |
| Ref dataRefCopy = data; |
| return adoptCF(CGFunctionCreate(&dataRefCopy.leakRef(), domain.size() / 2, domain.data(), range.size() / 2, range.data(), &callbacks)); |
| }; |
| |
| auto data = makeData(colorInterpolationMethod, stops); |
| auto function = makeFunction(colorInterpolationMethod, data); |
| |
| // FIXME: Investigate using bounded sRGB when the input stops are all bounded sRGB. |
| auto colorSpace = cachedCGColorSpace<ColorSpaceFor<OutputSpaceColorType>>(); |
| |
| return Shading { WTFMove(data), WTFMove(function), colorSpace }; |
| } |
| |
| // MARK: - Drawing functions. |
| |
| void GradientRendererCG::drawLinearGradient(CGContextRef platformContext, CGPoint startPoint, CGPoint endPoint, CGGradientDrawingOptions options) |
| { |
| WTF::switchOn(m_strategy, |
| [&] (Gradient& gradient) { |
| CGContextDrawLinearGradient(platformContext, gradient.gradient.get(), startPoint, endPoint, options); |
| }, |
| [&] (Shading& shading) { |
| bool startExtend = (options & kCGGradientDrawsBeforeStartLocation) != 0; |
| bool endExtend = (options & kCGGradientDrawsAfterEndLocation) != 0; |
| |
| CGContextDrawShading(platformContext, adoptCF(CGShadingCreateAxial(shading.colorSpace.get(), startPoint, endPoint, shading.function.get(), startExtend, endExtend)).get()); |
| } |
| ); |
| } |
| |
| void GradientRendererCG::drawRadialGradient(CGContextRef platformContext, CGPoint startCenter, CGFloat startRadius, CGPoint endCenter, CGFloat endRadius, CGGradientDrawingOptions options) |
| { |
| WTF::switchOn(m_strategy, |
| [&] (Gradient& gradient) { |
| CGContextDrawRadialGradient(platformContext, gradient.gradient.get(), startCenter, startRadius, endCenter, endRadius, options); |
| }, |
| [&] (Shading& shading) { |
| bool startExtend = (options & kCGGradientDrawsBeforeStartLocation) != 0; |
| bool endExtend = (options & kCGGradientDrawsAfterEndLocation) != 0; |
| |
| CGContextDrawShading(platformContext, adoptCF(CGShadingCreateRadial(shading.colorSpace.get(), startCenter, startRadius, endCenter, endRadius, shading.function.get(), startExtend, endExtend)).get()); |
| } |
| ); |
| } |
| |
| void GradientRendererCG::drawConicGradient(CGContextRef platformContext, CGPoint center, CGFloat angle) |
| { |
| #if HAVE(CORE_GRAPHICS_CONIC_GRADIENTS) |
| WTF::switchOn(m_strategy, |
| [&] (Gradient& gradient) { |
| CGContextDrawConicGradient(platformContext, gradient.gradient.get(), center, angle); |
| }, |
| [&] (Shading& shading) { |
| CGContextDrawShading(platformContext, adoptCF(CGShadingCreateConic(shading.colorSpace.get(), center, angle, shading.function.get())).get()); |
| } |
| ); |
| #else |
| UNUSED_PARAM(platformContext); |
| UNUSED_PARAM(center); |
| UNUSED_PARAM(angle); |
| #endif |
| } |
| |
| } |