File: GradientCairo.cpp

package info (click to toggle)
webkit2gtk 2.48.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 429,764 kB
  • sloc: cpp: 3,697,587; javascript: 194,444; ansic: 169,997; python: 46,499; asm: 19,295; ruby: 18,528; perl: 16,602; xml: 4,650; yacc: 2,360; sh: 2,098; java: 1,993; lex: 1,327; pascal: 366; makefile: 298
file content (278 lines) | stat: -rw-r--r-- 11,469 bytes parent folder | download | duplicates (9)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
 * 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 "AnimationUtilities.h"
#include "CairoOperations.h"
#include "CairoUtilities.h"
#include "ColorBlending.h"
#include "GraphicsContextCairo.h"
#include <wtf/MathExtras.h>

namespace WebCore {

void Gradient::stopsChanged()
{
}

static void addColorStopRGBA(cairo_pattern_t *gradient, GradientColorStop stop, float globalAlpha)
{
    auto [r, g, b, a] = stop.color.toColorTypeLossy<SRGBA<float>>().resolved();
    cairo_pattern_add_color_stop_rgba(gradient, stop.offset, r, g, b, a * globalAlpha);
}

typedef struct point_t {
    double x, y;
} point_t;

static void setCornerColorRGBA(cairo_pattern_t* gradient, int id, GradientColorStop stop, float globalAlpha)
{
    auto [r, g, b, a] = stop.color.toColorTypeLossy<SRGBA<float>>().resolved();
    cairo_mesh_pattern_set_corner_color_rgba(gradient, id, r, g, b, a * globalAlpha);
}

static constexpr double deg0 = 0;
static constexpr double deg90 = piDouble / 2;
static constexpr double deg180 = piDouble;
static constexpr double deg270 = 3 * piDouble / 2;
static constexpr double deg360 = 2 * piDouble;

static double normalizeAngle(double angle)
{
    double tmp = std::fmod(angle, deg360);
    if (tmp < 0)
        tmp += deg360;
    return tmp;
}

static void addConicSector(cairo_pattern_t *gradient, float cx, float cy, float r, float angleRadians,
    GradientColorStop from, GradientColorStop 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 * piDouble) + angleRadians;
    double angleEnd = ((to.offset - angOffset) * 2 * piDouble) + 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;
    auto actualAngleStart = normalizeAngle(angleStart);
    if (actualAngleStart >= deg0 && actualAngleStart < deg90) {
        cxOffset = 0;
        cyOffset = 0;
    } else if (actualAngleStart >= deg90 && actualAngleStart < deg180) {
        cxOffset = -1;
        cyOffset = 0;
    } else if (actualAngleStart >= deg180 && actualAngleStart < deg270) {
        cxOffset = -1;
        cyOffset = -1;
    } else if (actualAngleStart >= deg270 && actualAngleStart < deg360) {
        cxOffset = 0;
        cyOffset = -1;
    } else {
        cxOffset = 0;
        cyOffset = 0;
    }
    // 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 = {
        cx + (r * cos(angleStart)),
        cy + (r * sin(angleStart)),
    };
    point_t p1 = {
        cx + (r * cos(angleStart)) - f * (r * sin(angleStart)),
        cy + (r * sin(angleStart)) + f * (r * cos(angleStart)),
    };
    point_t p2 = {
        cx + (r * cos(angleEnd)) + f * (r * sin(angleEnd)),
        cy + (r * sin(angleEnd)) - f * (r * cos(angleEnd)),
    };
    point_t p3 = {
        cx + (r * cos(angleEnd)),
        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 RefPtr<cairo_pattern_t> createConic(float xo, float yo, float r, float angleRadians,
    GradientColorStops::StopVector stops, float globalAlpha)
{
    // Locate last stop with offset 0.
    size_t i = stops.size() - 1;
    for (; i > 0; i--) {
        if (!stops[i].offset)
            break;
    }
    // Remove stops with offset zero before last one.
    if (i > 0) {
        GradientColorStops::StopVector newStops;
        for (; i < stops.size(); i++)
            newStops.append(stops[i]);
        stops = newStops;
    }

    // Degenerated gradients with two stops at the same offset arrive with a single stop at 0.0
    // Add another point here so it can be interpolated properly below.
    if (stops.size() == 1)
        stops = { stops.first(), stops.first() };

    // 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 add three additional interpolated stops, allowing for four Bezier curves.
    if (stops.size() == 2) {
        // The first two checks avoid degenerated interpolations. These interpolations
        // may cause Cairo to enter really slow operations with huge bezier parameters.
        if (stops.first().offset == 1.0) {
            auto first = stops.first();
            stops = {
                {0, first.color}, {0.25, first.color}, {0.5, first.color}, {0.75, first.color}, first
            };
        } else if (stops.last().offset == 0.0) {
            auto last = stops.last();
            stops = {
                last, {0.25, last.color}, {0.5, last.color}, {0.75, last.color}, {1.0, last.color}
            };
        } else {
            auto interpolatedStop = [&] (double fraction) -> GradientColorStop {
                auto offset = blend(stops.first().offset, stops.last().offset, fraction);
                auto interpColor = blendWithoutPremultiply(stops.first().color, stops.last().color, fraction);
                return { offset, interpColor };
            };
            stops = { stops.first(), interpolatedStop(0.25), interpolatedStop(0.5), interpolatedStop(0.75), stops.last() };
        }
    }

    if (stops.first().offset > 0.0f)
        stops.insert(0, { 0.0f, stops.first().color });
    if (stops.last().offset < 1.0f)
        stops.append({ 1.0f, stops.last().color });

    auto gradient = adoptRef(cairo_pattern_create_mesh());
    for (size_t i = 0; i < stops.size() - 1; i++)
        addConicSector(gradient.get(), xo, yo, r, angleRadians, stops[i], stops[i + 1], globalAlpha);
    return gradient;
}

RefPtr<cairo_pattern_t> Gradient::createPattern(float globalAlpha, const AffineTransform& gradientSpaceTransform)
{
    cairo_matrix_t matrix = toCairoMatrix(gradientSpaceTransform);
    cairo_matrix_invert(&matrix);

    auto gradient = WTF::switchOn(m_data,
        [&] (const LinearData& data) {
            auto gradient = adoptRef(cairo_pattern_create_linear(data.point0.x(), data.point0.y(), data.point1.x(), data.point1.y()));
            for (auto& stop : stops())
                addColorStopRGBA(gradient.get(), stop, globalAlpha);
            return gradient;
        },
        [&] (const RadialData& data) {
            auto gradient = adoptRef(cairo_pattern_create_radial(data.point0.x(), data.point0.y(), data.startRadius, data.point1.x(), data.point1.y(), data.endRadius));
            for (auto& stop : stops())
                addColorStopRGBA(gradient.get(), stop, globalAlpha);

            if (data.aspectRatio != 1) {
                cairo_matrix_translate(&matrix, data.point0.x(), data.point0.y());
                cairo_matrix_scale(&matrix, 1.0, data.aspectRatio);
                cairo_matrix_translate(&matrix, -data.point0.x(), -data.point0.y());
            }

            return gradient;
        },
        [&] (const ConicData& data) {
            // 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().stops(), globalAlpha);
        }
    );

    switch (m_spreadMethod) {
    case GradientSpreadMethod::Pad:
        cairo_pattern_set_extend(gradient.get(), CAIRO_EXTEND_PAD);
        break;
    case GradientSpreadMethod::Reflect:
        cairo_pattern_set_extend(gradient.get(), CAIRO_EXTEND_REFLECT);
        break;
    case GradientSpreadMethod::Repeat:
        cairo_pattern_set_extend(gradient.get(), CAIRO_EXTEND_REPEAT);
        break;
    }

    cairo_pattern_set_matrix(gradient.get(), &matrix);

    return gradient;
}

void Gradient::fill(GraphicsContext& context, const FloatRect& rect)
{
    auto pattern = createPattern(1.0, context.fillGradientSpaceTransform());
    if (!pattern)
        return;

    ASSERT(context.hasPlatformContext());
    auto& platformContext = *context.platformContext();

    platformContext.save();
    Cairo::fillRect(platformContext, rect, pattern.get());
    platformContext.restore();
}

} // namespace WebCore

#endif // USE(CAIRO)