blob: 5392a917c1ba3a29821953dddc55d8b9c6678086 [file] [log] [blame]
/*
* 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
}
}