GradientCairo.cpp   [plain text]


/*
 * 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)