summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibGfx/GradientPainting.cpp
blob: d5747311d65d08a4040857a92303eae47ac96e2a (plain)
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*
 * Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Math.h>
#include <LibGfx/Gradients.h>
#include <LibGfx/PaintStyle.h>
#include <LibGfx/Painter.h>

#if defined(AK_COMPILER_GCC)
#    pragma GCC optimize("O3")
#endif

namespace Gfx {

// Note: This file implements the CSS/Canvas gradients for LibWeb according to the spec.
// Please do not make ad-hoc changes that may break spec compliance!

static float color_stop_step(ColorStop const& previous_stop, ColorStop const& next_stop, float position)
{
    if (position < previous_stop.position)
        return 0;
    if (position > next_stop.position)
        return 1;
    // For any given point between the two color stops,
    // determine the point’s location as a percentage of the distance between the two color stops.
    // Let this percentage be P.
    auto stop_length = next_stop.position - previous_stop.position;
    // FIXME: Avoids NaNs... Still not quite correct?
    if (stop_length <= 0)
        return 1;
    auto p = (position - previous_stop.position) / stop_length;
    if (!next_stop.transition_hint.has_value())
        return p;
    if (*next_stop.transition_hint >= 1)
        return 0;
    if (*next_stop.transition_hint <= 0)
        return 1;
    // Let C, the color weighting at that point, be equal to P^(logH(.5)).
    auto c = AK::pow(p, AK::log<float>(0.5) / AK::log(*next_stop.transition_hint));
    // The color at that point is then a linear blend between the colors of the two color stops,
    // blending (1 - C) of the first stop and C of the second stop.
    return c;
}

enum class UsePremultipliedAlpha {
    Yes,
    No
};

class GradientLine {
public:
    GradientLine(int gradient_length, ReadonlySpan<ColorStop> color_stops, Optional<float> repeat_length, UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha::Yes)
        : m_repeating(repeat_length.has_value())
        , m_start_offset(round_to<int>((m_repeating ? color_stops.first().position : 0.0f) * gradient_length))
        , m_color_stops(color_stops)
        , m_use_premultiplied_alpha(use_premultiplied_alpha)
    {
        // Avoid generating excessive amounts of colors when the not enough shades to fill that length.
        auto necessary_length = min<int>((color_stops.size() - 1) * 255, gradient_length);
        m_sample_scale = float(necessary_length) / gradient_length;
        // Note: color_count will be < gradient_length for repeating gradients.
        auto color_count = round_to<int>(repeat_length.value_or(1.0f) * necessary_length);
        m_gradient_line_colors.resize(color_count);

        for (int loc = 0; loc < color_count; loc++) {
            auto relative_loc = float(loc + m_start_offset) / necessary_length;
            Color gradient_color = color_blend(color_stops[0].color, color_stops[1].color,
                color_stop_step(color_stops[0], color_stops[1], relative_loc));
            for (size_t i = 1; i < color_stops.size() - 1; i++) {
                gradient_color = color_blend(gradient_color, color_stops[i + 1].color,
                    color_stop_step(color_stops[i], color_stops[i + 1], relative_loc));
            }
            m_gradient_line_colors[loc] = gradient_color;
            if (gradient_color.alpha() < 255)
                m_requires_blending = true;
        }
    }

    Color color_blend(Color a, Color b, float amount) const
    {
        // Note: color.mixed_with() performs premultiplied alpha mixing when necessary as defined in:
        // https://drafts.csswg.org/css-images/#coloring-gradient-line
        if (m_use_premultiplied_alpha == UsePremultipliedAlpha::Yes)
            return a.mixed_with(b, amount);
        return a.interpolate(b, amount);
    };

    Color get_color(i64 index) const
    {
        if (index < 0)
            return m_color_stops.first().color;
        if (index >= static_cast<i64>(m_gradient_line_colors.size()))
            return m_color_stops.last().color;
        return m_gradient_line_colors[index];
    }

    Color sample_color(float loc) const
    {
        if (!isfinite(loc))
            return Color();
        if (m_sample_scale != 1.0f)
            loc *= m_sample_scale;
        auto repeat_wrap_if_required = [&](i64 loc) {
            if (m_repeating)
                return (loc + m_start_offset) % static_cast<i64>(m_gradient_line_colors.size());
            return loc;
        };
        auto int_loc = static_cast<i64>(floor(loc));
        auto blend = loc - int_loc;
        auto color = get_color(repeat_wrap_if_required(int_loc));
        // Blend between the two neighbouring colors (this fixes some nasty aliasing issues at small angles)
        if (blend >= 0.004f)
            color = color_blend(color, get_color(repeat_wrap_if_required(int_loc + 1)), blend);
        return color;
    }

    void paint_into_physical_rect(Painter& painter, IntRect rect, auto location_transform)
    {
        auto clipped_rect = rect.intersected(painter.clip_rect() * painter.scale());
        auto start_offset = clipped_rect.location() - rect.location();
        for (int y = 0; y < clipped_rect.height(); y++) {
            for (int x = 0; x < clipped_rect.width(); x++) {
                auto pixel = sample_color(location_transform(x + start_offset.x(), y + start_offset.y()));
                painter.set_physical_pixel(clipped_rect.location().translated(x, y), pixel, m_requires_blending);
            }
        }
    }

private:
    bool m_repeating { false };
    int m_start_offset { 0 };
    float m_sample_scale { 1 };
    ReadonlySpan<ColorStop> m_color_stops {};
    UsePremultipliedAlpha m_use_premultiplied_alpha { UsePremultipliedAlpha::Yes };

    Vector<Color, 1024> m_gradient_line_colors;
    bool m_requires_blending = false;
};

template<typename TransformFunction>
struct Gradient {
    Gradient(GradientLine gradient_line, TransformFunction transform_function)
        : m_gradient_line(move(gradient_line))
        , m_transform_function(move(transform_function))
    {
    }

    void paint(Painter& painter, IntRect rect)
    {
        m_gradient_line.paint_into_physical_rect(painter, rect, m_transform_function);
    }

    PaintStyle::SamplerFunction sample_function()
    {
        return [this](IntPoint point) {
            return m_gradient_line.sample_color(m_transform_function(point.x(), point.y()));
        };
    }

private:
    GradientLine m_gradient_line;
    TransformFunction m_transform_function;
};

static auto create_linear_gradient(IntRect const& physical_rect, ReadonlySpan<ColorStop> color_stops, float angle, Optional<float> repeat_length)
{
    float normalized_angle = normalized_gradient_angle_radians(angle);
    float sin_angle, cos_angle;
    AK::sincos(normalized_angle, sin_angle, cos_angle);

    // Full length of the gradient
    auto gradient_length = calculate_gradient_length(physical_rect.size(), sin_angle, cos_angle);
    IntPoint offset { cos_angle * (gradient_length / 2), sin_angle * (gradient_length / 2) };
    auto center = physical_rect.translated(-physical_rect.location()).center();
    auto start_point = center - offset;
    // Rotate gradient line to be horizontal
    auto rotated_start_point_x = start_point.x() * cos_angle - start_point.y() * -sin_angle;

    GradientLine gradient_line(gradient_length, color_stops, repeat_length);
    return Gradient {
        move(gradient_line),
        [=](int x, int y) {
            return (x * cos_angle - (physical_rect.height() - y) * -sin_angle) - rotated_start_point_x;
        }
    };
}

static auto create_conic_gradient(ReadonlySpan<ColorStop> color_stops, FloatPoint center_point, float start_angle, Optional<float> repeat_length, UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha::Yes)
{
    // FIXME: Do we need/want sub-degree accuracy for the gradient line?
    GradientLine gradient_line(360, color_stops, repeat_length, use_premultiplied_alpha);
    float normalized_start_angle = (360.0f - start_angle) + 90.0f;
    // The flooring can make gradients that want soft edges look worse, so only floor if we have hard edges.
    // Which makes sure the hard edge stay hard edges :^)
    bool should_floor_angles = false;
    for (size_t i = 0; i < color_stops.size() - 1; i++) {
        if (color_stops[i + 1].position - color_stops[i].position <= 0.01f) {
            should_floor_angles = true;
            break;
        }
    }
    return Gradient {
        move(gradient_line),
        [=](int x, int y) {
            auto point = FloatPoint { x, y } - center_point;
            // FIXME: We could probably get away with some approximation here:
            auto loc = fmod((AK::atan2(point.y(), point.x()) * 180.0f / AK::Pi<float> + 360.0f + normalized_start_angle), 360.0f);
            return should_floor_angles ? floor(loc) : loc;
        }
    };
}

static auto create_radial_gradient(IntRect const& physical_rect, ReadonlySpan<ColorStop> color_stops, IntPoint center, IntSize size, Optional<float> repeat_length)
{
    // A conservative guesstimate on how many colors we need to generate:
    auto max_dimension = max(physical_rect.width(), physical_rect.height());
    auto max_visible_gradient = max(max_dimension / 2, min(size.width(), max_dimension));
    GradientLine gradient_line(max_visible_gradient, color_stops, repeat_length);
    auto center_point = FloatPoint { center }.translated(0.5, 0.5);
    return Gradient {
        move(gradient_line),
        [=](int x, int y) {
            // FIXME: See if there's a more efficient calculation we do there :^)
            auto point = FloatPoint(x, y) - center_point;
            auto gradient_x = point.x() / size.width();
            auto gradient_y = point.y() / size.height();
            return AK::sqrt(gradient_x * gradient_x + gradient_y * gradient_y) * max_visible_gradient;
        }
    };
}

void Painter::fill_rect_with_linear_gradient(IntRect const& rect, ReadonlySpan<ColorStop> color_stops, float angle, Optional<float> repeat_length)
{
    auto a_rect = to_physical(rect);
    if (a_rect.intersected(clip_rect() * scale()).is_empty())
        return;
    auto linear_gradient = create_linear_gradient(a_rect, color_stops, angle, repeat_length);
    linear_gradient.paint(*this, a_rect);
}

static FloatPoint pixel_center(IntPoint point)
{
    return point.to_type<float>().translated(0.5f, 0.5f);
}

void Painter::fill_rect_with_conic_gradient(IntRect const& rect, ReadonlySpan<ColorStop> color_stops, IntPoint center, float start_angle, Optional<float> repeat_length)
{
    auto a_rect = to_physical(rect);
    if (a_rect.intersected(clip_rect() * scale()).is_empty())
        return;
    // Translate position/center to the center of the pixel (avoids some funky painting)
    auto center_point = pixel_center(center * scale());
    auto conic_gradient = create_conic_gradient(color_stops, center_point, start_angle, repeat_length);
    conic_gradient.paint(*this, a_rect);
}

void Painter::fill_rect_with_radial_gradient(IntRect const& rect, ReadonlySpan<ColorStop> color_stops, IntPoint center, IntSize size, Optional<float> repeat_length)
{
    auto a_rect = to_physical(rect);
    if (a_rect.intersected(clip_rect() * scale()).is_empty())
        return;
    auto radial_gradient = create_radial_gradient(a_rect, color_stops, center * scale(), size * scale(), repeat_length);
    radial_gradient.paint(*this, a_rect);
}

// TODO: Figure out how to handle scale() here... Not important while not supported by fill_path()

void LinearGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
{
    VERIFY(color_stops().size() > 2);
    auto linear_gradient = create_linear_gradient(physical_bounding_box, color_stops(), m_angle, repeat_length());
    paint(linear_gradient.sample_function());
}

void ConicGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
{
    VERIFY(color_stops().size() > 2);
    (void)physical_bounding_box;
    auto conic_gradient = create_conic_gradient(color_stops(), pixel_center(m_center), m_start_angle, repeat_length());
    paint(conic_gradient.sample_function());
}

void RadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
{
    VERIFY(color_stops().size() > 2);
    auto radial_gradient = create_radial_gradient(physical_bounding_box, color_stops(), m_center, m_size, repeat_length());
    paint(radial_gradient.sample_function());
}

// The following implements the gradient fill/stoke styles for the HTML canvas: https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles

static auto make_sample_non_relative(IntPoint draw_location, auto sample)
{
    return [=, sample = move(sample)](IntPoint point) { return sample(point.translated(draw_location)); };
}

void CanvasLinearGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
{
    // If x0 = x1 and y0 = y1, then the linear gradient must paint nothing.
    if (m_p0 == m_p1)
        return;
    if (color_stops().is_empty())
        return;
    if (color_stops().size() < 2)
        return paint([this](IntPoint) { return color_stops().first().color; });

    auto delta = m_p1 - m_p0;
    auto angle = AK::atan2(delta.y(), delta.x());
    float sin_angle, cos_angle;
    AK::sincos(angle, sin_angle, cos_angle);
    int gradient_length = ceilf(m_p1.distance_from(m_p0));
    auto rotated_start_point_x = m_p0.x() * cos_angle - m_p0.y() * -sin_angle;

    Gradient linear_gradient {
        GradientLine(gradient_length, color_stops(), repeat_length(), UsePremultipliedAlpha::No),
        [=](int x, int y) {
            return (x * cos_angle - y * -sin_angle) - rotated_start_point_x;
        }
    };

    paint(make_sample_non_relative(physical_bounding_box.location(), linear_gradient.sample_function()));
}

void CanvasConicGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
{
    if (color_stops().is_empty())
        return;
    if (color_stops().size() < 2)
        return paint([this](IntPoint) { return color_stops().first().color; });

    // Follows the same rendering rule as CSS 'conic-gradient' and it is equivalent to CSS
    // 'conic-gradient(from adjustedStartAnglerad at xpx ypx, angularColorStopList)'.
    //  Here:
    //      adjustedStartAngle is given by startAngle + π/2;
    auto conic_gradient = create_conic_gradient(color_stops(), m_center, m_start_angle + 90.0f, repeat_length(), UsePremultipliedAlpha::No);
    paint(make_sample_non_relative(physical_bounding_box.location(), conic_gradient.sample_function()));
}

void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
{
    // 1. If x0 = x1 and y0 = y1 and r0 = r1, then the radial gradient must paint nothing. Return.
    if (m_start_center == m_end_center && m_start_radius == m_end_radius)
        return;
    if (color_stops().is_empty())
        return;
    if (color_stops().size() < 2)
        return paint([this](IntPoint) { return color_stops().first().color; });

    auto start_radius = m_start_radius;
    auto start_center = m_start_center;
    auto end_radius = m_end_radius;
    auto end_center = m_end_center;

    if (end_radius == 0 && start_radius == 0)
        return;

    if (fabs(start_radius - end_radius) < 1)
        start_radius += 1;

    // Needed for the start circle > end circle case, but FIXME, this seems kind of hacky.
    bool reverse_gradient = end_radius < start_radius;
    if (reverse_gradient) {
        swap(end_radius, start_radius);
        swap(end_center, start_center);
    }

    // Spec steps: Useless for writing an actual implementation (give it a go :P):
    //
    // 2. Let x(ω) = (x1-x0)ω + x0
    //    Let y(ω) = (y1-y0)ω + y0
    //    Let r(ω) = (r1-r0)ω + r0
    // Let the color at ω be the color at that position on the gradient
    // (with the colors coming from the interpolation and extrapolation described above).
    //
    // 3. For all values of ω where r(ω) > 0, starting with the value of ω nearest to positive infinity and
    // ending with the value of ω nearest to negative infinity, draw the circumference of the circle with
    // radius r(ω) at position (x(ω), y(ω)), with the color at ω, but only painting on the parts of the
    // bitmap that have not yet been painted on by earlier circles in this step for this rendering of the gradient.

    auto center_delta = end_center - start_center;
    auto center_dist = end_center.distance_from(start_center);
    bool inner_contained = ((center_dist + start_radius) < end_radius);

    auto start_point = start_center;
    if (!inner_contained) {
        // The intersection point of the direct common tangents of the start/end circles.
        start_point = FloatPoint {
            (start_radius * end_center.x() - end_radius * start_center.x()) / (start_radius - end_radius),
            (start_radius * end_center.y() - end_radius * start_center.y()) / (start_radius - end_radius)
        };
    }

    // This is just an approximate upperbound (the gradient line class will shorten this if necessary).
    int gradient_length = AK::ceil(center_dist + end_radius + start_radius);
    GradientLine gradient_line(gradient_length, color_stops(), repeat_length(), UsePremultipliedAlpha::No);

    auto radius2 = end_radius * end_radius;
    center_delta = end_center - start_point;
    auto dx2_factor = (radius2 - center_delta.y() * center_delta.y());
    auto dy2_factor = (radius2 - center_delta.x() * center_delta.x());

    // If you can simplify this please do, this is "best guess" implementation due to lack of specification.
    // It was implemented to visually match chrome/firefox in all cases:
    //      - Start circle inside end circle
    //      - Start circle outside end circle
    //      - Start circle radius == end circle radius
    //      - Start circle larger than end circle (inside end circle)
    //      - Start circle larger than end circle (outside end circle)
    //      - Start cirlce or end circle radius == 0

    Gradient radial_gradient {
        move(gradient_line),
        [=](int x, int y) {
            auto get_gradient_location = [&] {
                FloatPoint point { x, y };
                auto dist = point.distance_from(start_point);
                if (dist == 0)
                    return 0.0f;
                auto vec = (point - start_point) / dist;
                auto dx2 = vec.x() * vec.x();
                auto dy2 = vec.y() * vec.y();
                // This works out the distance to the nearest point on the end circle in the direction of the "vec" vector.
                // The "vec" vector points from the center of the start circle to the current point.
                auto root = sqrtf(dx2 * dx2_factor + dy2 * dy2_factor
                    + 2 * vec.x() * vec.y() * center_delta.x() * center_delta.y());
                auto dot = vec.x() * center_delta.x() + vec.y() * center_delta.y();
                // Note: When reversed we always want the farthest point
                auto edge_dist = (((inner_contained || reverse_gradient ? root : -root) + dot) / (dx2 + dy2));
                auto start_offset = inner_contained ? start_radius : (edge_dist / end_radius) * start_radius;
                // FIXME: Returning nan is a hack for "Don't paint me!"
                if (edge_dist < 0)
                    return AK::NaN<float>;
                if (edge_dist - start_offset < 0)
                    return float(gradient_length);
                return ((dist - start_offset) / (edge_dist - start_offset));
            };
            auto loc = get_gradient_location();
            if (reverse_gradient)
                loc = 1.0f - loc;
            return loc * gradient_length;
        }
    };

    paint(make_sample_non_relative(physical_bounding_box.location(), radial_gradient.sample_function()));
}

}