2023-01-10 01:07:06 +00:00
/*
* Copyright ( c ) 2022 - 2023 , MacDue < macdue @ dueutil . tech >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/Math.h>
# include <LibGfx/Gradients.h>
2023-01-15 22:10:36 +00:00
# include <LibGfx/PaintStyle.h>
2023-01-10 01:07:06 +00:00
# include <LibGfx/Painter.h>
# if defined(AK_COMPILER_GCC)
# pragma GCC optimize("O3")
# endif
2023-01-15 22:10:36 +00:00
2023-01-10 01:07:06 +00:00
namespace Gfx {
2023-01-17 19:52:02 +00:00
// Note: This file implements the CSS/Canvas gradients for LibWeb according to the spec.
2023-01-10 01:07:06 +00:00
// 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 ;
}
2023-01-15 22:10:36 +00:00
enum class UsePremultipliedAlpha {
Yes ,
No
} ;
2023-01-10 01:07:06 +00:00
class GradientLine {
public :
2023-01-15 22:10:36 +00:00
GradientLine ( int gradient_length , Span < ColorStop const > color_stops , Optional < float > repeat_length , UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha : : Yes )
2023-01-29 13:29:55 +00:00
: 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 )
2023-01-10 01:07:06 +00:00
{
// 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 ) ;
2023-01-15 22:10:36 +00:00
2023-01-10 01:07:06 +00:00
for ( int loc = 0 ; loc < color_count ; loc + + ) {
auto relative_loc = float ( loc + m_start_offset ) / necessary_length ;
2023-01-15 22:10:36 +00:00
Color gradient_color = color_blend ( color_stops [ 0 ] . color , color_stops [ 1 ] . color ,
2023-01-10 01:07:06 +00:00
color_stop_step ( color_stops [ 0 ] , color_stops [ 1 ] , relative_loc ) ) ;
for ( size_t i = 1 ; i < color_stops . size ( ) - 1 ; i + + ) {
2023-01-15 22:10:36 +00:00
gradient_color = color_blend ( gradient_color , color_stops [ i + 1 ] . color ,
2023-01-10 01:07:06 +00:00
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 ;
}
}
2023-01-23 00:35:57 +00:00
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 ) ;
} ;
2023-01-10 01:07:06 +00:00
Color get_color ( i64 index ) const
{
2023-01-23 00:35:57 +00:00
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 ] ;
2023-01-10 01:07:06 +00:00
}
Color sample_color ( float loc ) const
{
2023-01-17 19:52:02 +00:00
if ( ! isfinite ( loc ) )
return Color ( ) ;
2023-01-10 01:07:06 +00:00
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 )
2023-01-23 00:35:57 +00:00
color = color_blend ( color , get_color ( repeat_wrap_if_required ( int_loc + 1 ) ) , blend ) ;
2023-01-10 01:07:06 +00:00
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 :
2023-01-23 00:35:57 +00:00
bool m_repeating { false } ;
int m_start_offset { 0 } ;
2023-01-10 01:07:06 +00:00
float m_sample_scale { 1 } ;
2023-01-23 00:35:57 +00:00
Span < ColorStop const > m_color_stops { } ;
UsePremultipliedAlpha m_use_premultiplied_alpha { UsePremultipliedAlpha : : Yes } ;
2023-01-10 01:07:06 +00:00
2023-01-23 00:35:57 +00:00
Vector < Color , 1024 > m_gradient_line_colors ;
2023-01-10 01:07:06 +00:00
bool m_requires_blending = false ;
} ;
2023-01-15 22:10:36 +00:00
template < typename TransformFunction >
struct Gradient {
Gradient ( GradientLine gradient_line , TransformFunction transform_function )
: m_gradient_line ( move ( gradient_line ) )
, m_transform_function ( move ( transform_function ) )
{
}
2023-01-10 01:07:06 +00:00
2023-01-15 22:10:36 +00:00
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 ;
} ;
2023-01-29 13:28:36 +00:00
static auto create_linear_gradient ( IntRect const & physical_rect , Span < ColorStop const > color_stops , float angle , Optional < float > repeat_length )
2023-01-15 22:10:36 +00:00
{
2023-01-10 01:07:06 +00:00
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
2023-01-15 22:10:36 +00:00
auto gradient_length = calculate_gradient_length ( physical_rect . size ( ) , sin_angle , cos_angle ) ;
2023-01-10 01:07:06 +00:00
IntPoint offset { cos_angle * ( gradient_length / 2 ) , sin_angle * ( gradient_length / 2 ) } ;
2023-01-15 22:10:36 +00:00
auto center = physical_rect . translated ( - physical_rect . location ( ) ) . center ( ) ;
2023-01-10 01:07:06 +00:00
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 ) ;
2023-01-15 22:10:36 +00:00
return Gradient {
move ( gradient_line ) ,
[ = ] ( int x , int y ) {
return ( x * cos_angle - ( physical_rect . height ( ) - y ) * - sin_angle ) - rotated_start_point_x ;
}
} ;
2023-01-10 01:07:06 +00:00
}
2023-01-29 13:28:36 +00:00
static auto create_conic_gradient ( Span < ColorStop const > color_stops , FloatPoint center_point , float start_angle , Optional < float > repeat_length , UsePremultipliedAlpha use_premultiplied_alpha = UsePremultipliedAlpha : : Yes )
2023-01-10 01:07:06 +00:00
{
// FIXME: Do we need/want sub-degree accuracy for the gradient line?
2023-01-15 22:10:36 +00:00
GradientLine gradient_line ( 360 , color_stops , repeat_length , use_premultiplied_alpha ) ;
2023-01-10 01:07:06 +00:00
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 ;
}
}
2023-01-15 22:10:36 +00:00
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 ;
}
} ;
}
2023-01-29 13:28:36 +00:00
static auto create_radial_gradient ( IntRect const & physical_rect , Span < ColorStop const > color_stops , IntPoint center , IntSize size , Optional < float > repeat_length )
2023-01-15 22:10:36 +00:00
{
// 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 ;
}
} ;
}
2023-01-29 13:28:36 +00:00
void Painter : : fill_rect_with_linear_gradient ( IntRect const & rect , Span < ColorStop const > color_stops , float angle , Optional < float > repeat_length )
2023-01-15 22:10:36 +00:00
{
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 ) ;
}
2023-01-29 13:28:36 +00:00
void Painter : : fill_rect_with_conic_gradient ( IntRect const & rect , Span < ColorStop const > color_stops , IntPoint center , float start_angle , Optional < float > repeat_length )
2023-01-15 22:10:36 +00:00
{
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 ) ;
2023-01-10 01:07:06 +00:00
}
2023-01-29 13:28:36 +00:00
void Painter : : fill_rect_with_radial_gradient ( IntRect const & rect , Span < ColorStop const > color_stops , IntPoint center , IntSize size , Optional < float > repeat_length )
2023-01-10 01:07:06 +00:00
{
auto a_rect = to_physical ( rect ) ;
if ( a_rect . intersected ( clip_rect ( ) * scale ( ) ) . is_empty ( ) )
return ;
2023-01-15 22:10:36 +00:00
auto radial_gradient = create_radial_gradient ( a_rect , color_stops , center * scale ( ) , size * scale ( ) , repeat_length ) ;
radial_gradient . paint ( * this , a_rect ) ;
}
2023-01-10 01:07:06 +00:00
2023-01-15 22:10:36 +00:00
// 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 ( ) ) ;
2023-01-10 01:07:06 +00:00
}
2023-01-17 19:52:02 +00:00
// 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).
2023-01-23 00:35:57 +00:00
int gradient_length = AK : : ceil ( center_dist + end_radius + start_radius ) ;
2023-01-17 19:52:02 +00:00
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 ( ) ) ) ;
}
2023-01-10 01:07:06 +00:00
}