blob: dd15e3c3fed54ae4b8c6c05de963e22e02272eb6 [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2007 Alp Toker <alp@atoker.com>
* Copyright (C) 2019 Igalia S.L.
*
* 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 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 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 "config.h"
#include "Gradient.h"
#if USE(CAIRO)
#include "CairoOperations.h"
#include "CairoUtilities.h"
#include "GraphicsContext.h"
#include "PlatformContextCairo.h"
namespace WebCore {
void Gradient::platformDestroy()
{
}
static void addColorStopRGBA(cairo_pattern_t *gradient, Gradient::ColorStop stop, float globalAlpha)
{
if (stop.color.isExtended()) {
cairo_pattern_add_color_stop_rgba(gradient, stop.offset, stop.color.asExtended().red(), stop.color.asExtended().green(),
stop.color.asExtended().blue(), stop.color.asExtended().alpha() * globalAlpha);
} else {
float r, g, b, a;
stop.color.getRGBA(r, g, b, a);
cairo_pattern_add_color_stop_rgba(gradient, stop.offset, r, g, b, a * globalAlpha);
}
}
#if PLATFORM(GTK) || PLATFORM(WPE)
typedef struct point_t {
double x, y;
} point_t;
static void setCornerColorRGBA(cairo_pattern_t* gradient, int id, Gradient::ColorStop stop, float globalAlpha)
{
if (stop.color.isExtended()) {
cairo_mesh_pattern_set_corner_color_rgba(gradient, id, stop.color.asExtended().red(), stop.color.asExtended().green(),
stop.color.asExtended().blue(), stop.color.asExtended().alpha() * globalAlpha);
} else {
float r, g, b, a;
stop.color.getRGBA(r, g, b, a);
cairo_mesh_pattern_set_corner_color_rgba(gradient, id, r, g, b, a * globalAlpha);
}
}
static void addConicSector(cairo_pattern_t *gradient, float cx, float cy, float r, float angleRadians,
Gradient::ColorStop from, Gradient::ColorStop to, float globalAlpha)
{
const double angOffset = 0.25; // 90 degrees.
// Substract 90 degrees so angles start from top left.
// Convert to radians and add angleRadians offset.
double angleStart = ((from.offset - angOffset) * 2 * M_PI) + angleRadians;
double angleEnd = ((to.offset - angOffset) * 2 * M_PI) + angleRadians;
// Calculate center offset depending on quadrant.
//
// All sections belonging to the same quadrant share a common center. As we move
// along the circle, sections belonging to a new quadrant will have a different
// center. If all sections had the same center, the center will get overridden as
// the sections get painted.
double cxOffset, cyOffset;
if (from.offset >= 0 && from.offset < 0.25) {
cxOffset = 0;
cyOffset = -1;
} else if (from.offset >= 0.25 && from.offset < 0.50) {
cxOffset = 0;
cyOffset = 0;
} else if (from.offset >= 0.50 && from.offset < 0.75) {
cxOffset = -1;
cyOffset = 0;
} else if (from.offset >= 0.75 && from.offset < 1) {
cxOffset = -1;
cyOffset = -1;
} else {
cxOffset = 0;
cyOffset = -1;
}
// The center offset for each of the sections is 1 pixel, since in theory nothing
// can be smaller than 1 pixel. However, in high-resolution displays 1 pixel is
// too wide, and that makes the separation between sections clearly visible by a
// straight white line. To fix this issue, I set the size of the offset not to
// 1 pixel but 0.10. This has proved to work OK both in low-resolution displays
// as well as high-resolution displays.
const double offsetWidth = 0.1;
cx = cx + cxOffset * offsetWidth;
cy = cy + cyOffset * offsetWidth;
// Calculate starting point, ending point and control points of Bezier curve.
double f = 4 * tan((angleEnd - angleStart) / 4) / 3;
point_t p0 = {
x: cx + (r * cos(angleStart)),
y: cy + (r * sin(angleStart))
};
point_t p1 = {
x: cx + (r * cos(angleStart)) - f * (r * sin(angleStart)),
y: cy + (r * sin(angleStart)) + f * (r * cos(angleStart))
};
point_t p2 = {
x: cx + (r * cos(angleEnd)) + f * (r * sin(angleEnd)),
y: cy + (r * sin(angleEnd)) - f * (r * cos(angleEnd))
};
point_t p3 = {
x: cx + (r * cos(angleEnd)),
y: cy + (r * sin(angleEnd))
};
// Add patch with shape of the sector and gradient colors.
cairo_mesh_pattern_begin_patch(gradient);
cairo_mesh_pattern_move_to(gradient, cx, cy);
cairo_mesh_pattern_line_to(gradient, p0.x, p0.y);
cairo_mesh_pattern_curve_to(gradient, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
setCornerColorRGBA(gradient, 0, from, globalAlpha);
setCornerColorRGBA(gradient, 1, from, globalAlpha);
setCornerColorRGBA(gradient, 2, to, globalAlpha);
setCornerColorRGBA(gradient, 3, to, globalAlpha);
cairo_mesh_pattern_end_patch(gradient);
}
static Gradient::ColorStop interpolateColorStop(Gradient::ColorStop from, Gradient::ColorStop to)
{
float r1, g1, b1, a1;
float r2, g2, b2, a2;
if (from.color.isExtended()) {
r1 = from.color.asExtended().red();
g1 = from.color.asExtended().green();
b1 = from.color.asExtended().blue();
a1 = from.color.asExtended().alpha();
} else
from.color.getRGBA(r1, g1, b1, a1);
if (to.color.isExtended()) {
r2 = to.color.asExtended().red();
g2 = to.color.asExtended().green();
b2 = to.color.asExtended().blue();
a2 = to.color.asExtended().alpha();
} else
to.color.getRGBA(r2, g2, b2, a2);
float offset = from.offset + (to.offset - from.offset) * 0.5f;
float r = r1 + (r2 - r1) * 0.5f;
float g = g1 + (g2 - g1) * 0.5f;
float b = b1 + (b2 - b1) * 0.5f;
float a = a1 + (a2 - a1) * 0.5f;
return Gradient::ColorStop(offset, Color(r, g, b, a));
}
static cairo_pattern_t* createConic(float xo, float yo, float r, float angleRadians,
Gradient::ColorStopVector stops, float globalAlpha)
{
cairo_pattern_t* gradient = cairo_pattern_create_mesh();
Gradient::ColorStop from, to;
/* It's not possible to paint an entire circle with a single Bezier curve.
* To have a good approximation to a circle it's necessary to use at least
* four Bezier curves. So three additional stops with interpolated colors
* are added to force painting of four Bezier curves. */
if (stops.size() == 2) {
Gradient::ColorStop first = stops.at(0);
Gradient::ColorStop last = stops.at(1);
Gradient::ColorStop third = interpolateColorStop(first, last);
Gradient::ColorStop second = interpolateColorStop(first, third);
Gradient::ColorStop fourth = interpolateColorStop(third, last);
stops.insert(1, fourth);
stops.insert(1, third);
stops.insert(1, second);
}
// Add extra color stop at the beginning if first element offset is not zero.
if (stops.at(0).offset > 0)
stops.insert(0, Gradient::ColorStop(0, stops.at(0).color));
// Add extra color stop at the end if last element offset is not zero.
if (stops.at(stops.size() - 1).offset < 1)
stops.append(Gradient::ColorStop(1, stops.at(stops.size() - 1).color));
for (size_t i = 0; i < stops.size() - 1; i++) {
from = stops.at(i), to = stops.at(i + 1);
addConicSector(gradient, xo, yo, r, angleRadians, from, to, globalAlpha);
}
return gradient;
}
#endif
cairo_pattern_t* Gradient::createPlatformGradient(float globalAlpha)
{
cairo_pattern_t* gradient = WTF::switchOn(m_data,
[&] (const LinearData& data) -> cairo_pattern_t* {
return cairo_pattern_create_linear(data.point0.x(), data.point0.y(), data.point1.x(), data.point1.y());
},
[&] (const RadialData& data) -> cairo_pattern_t* {
return cairo_pattern_create_radial(data.point0.x(), data.point0.y(), data.startRadius, data.point1.x(), data.point1.y(), data.endRadius);
},
#if PLATFORM(GTK) || PLATFORM(WPE)
[&] (const ConicData& data) -> cairo_pattern_t* {
// FIXME: data passed for a Conic gradient doesn't contain a radius. That's apparently correct because the W3C spec
// (https://www.w3.org/TR/css-images-4/#conic-gradients) states a conic gradient is only defined by its position and angle.
// Thus, here I give the radius an extremely large value. The resulting gradient will be later clipped by fillRect.
// An alternative solution could be to change the API and pass a rect's width and height to optimize the computation of the radius.
const float radius = 4096;
return createConic(data.point0.x(), data.point0.y(), radius, data.angleRadians, stops(), globalAlpha);
#else
[&] (const ConicData&) -> cairo_pattern_t* {
// FIXME: implement conic gradient rendering.
return nullptr;
#endif
}
);
if (type() != Type::Conic) {
for (const auto& stop : stops()) {
addColorStopRGBA(gradient, stop, globalAlpha);
}
}
switch (m_spreadMethod) {
case SpreadMethodPad:
cairo_pattern_set_extend(gradient, CAIRO_EXTEND_PAD);
break;
case SpreadMethodReflect:
cairo_pattern_set_extend(gradient, CAIRO_EXTEND_REFLECT);
break;
case SpreadMethodRepeat:
cairo_pattern_set_extend(gradient, CAIRO_EXTEND_REPEAT);
break;
}
cairo_matrix_t matrix = toCairoMatrix(m_gradientSpaceTransformation);
cairo_matrix_invert(&matrix);
cairo_pattern_set_matrix(gradient, &matrix);
return gradient;
}
void Gradient::fill(GraphicsContext& context, const FloatRect& rect)
{
RefPtr<cairo_pattern_t> platformGradient = adoptRef(createPlatformGradient(1.0));
if (!platformGradient)
return;
ASSERT(context.hasPlatformContext());
auto& platformContext = *context.platformContext();
Cairo::save(platformContext);
Cairo::fillRect(platformContext, rect, platformGradient.get());
Cairo::restore(platformContext);
}
} // namespace WebCore
#endif // USE(CAIRO)