/* * Copyright (c) 2022-2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #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(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 color_stops, Optional repeat_length, UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha::Yes) : m_repeating(repeat_length.has_value()) , m_start_offset(round_to((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((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(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(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(m_gradient_line_colors.size()); return loc; }; auto int_loc = static_cast(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 m_color_stops {}; UsePremultipliedAlpha m_use_premultiplied_alpha { UsePremultipliedAlpha::Yes }; Vector m_gradient_line_colors; bool m_requires_blending = false; }; template 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 color_stops, float angle, Optional 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 color_stops, FloatPoint center_point, float start_angle, Optional 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 + 360.0f + normalized_start_angle), 360.0f); return should_floor_angles ? floor(loc) : loc; } }; } static auto create_radial_gradient(IntRect const& physical_rect, ReadonlySpan color_stops, IntPoint center, IntSize size, Optional 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 color_stops, float angle, Optional 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().translated(0.5f, 0.5f); } void Painter::fill_rect_with_conic_gradient(IntRect const& rect, ReadonlySpan color_stops, IntPoint center, float start_angle, Optional 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 color_stops, IntPoint center, IntSize size, Optional 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; 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())); } }