% File: hawkdraw-shadings.code.tex
% Copyright 2026 Jasper Habicht (mail(at)jasperhabicht.de).
%
% This work may be distributed and/or modified under the
% conditions of the LaTeX Project Public License version 1.3c,
% available at http://www.latex-project.org/lppl/.
%
% This file is part of the `hawkdraw' package (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% This work has the LPPL maintenance status `maintained'.
%
% BOF

% v0.4.0 2026-07-01

% =====

% Shadings

% ===

% Shadings are defined as rectangular PDF XObjects. There are various types of shadings, but we only define
% linear and radial shaings here having type 2 and 3 respectively. We define shadings as square with a size
% of 100bp times 100bp. Shadings are defined by functions.

% In order to allow for rotation of shadings, we use the PGF approach of defining the shadings twice the
% size of the path they should fill. Thus, we need to use multiple functions to define the shading, because
% the outer part of the shading object will be clipped away by the path. We do this by defining the
% functions in a nested way. The outer function is a type 3 function, which defines the inner functions.
% The inner functions depend on the type of the shading and the number of colors used.

% Every shading defined by its type, the used colors and whether or not it has some offset (for radial
% shadings) is defined only once and a PDF XObject is created for it. In the PDF, two XObjects are created,
% one that defines the shading using functions and one that represents a box referring to the shading. Upon
% use, the latter XObject is (re-)used by placing it in a box which is then scaled to twice the size of the
% path bounding box, rotated and placed at the center of the relevant path, then clipped by this path.

% ===

\msg_new:nnn { hawkdraw } { shading-unknown } {
    Shading ~ `#1` ~ unknown.
}

\box_new:N \l_hawkdraw_shading_box

\clist_new:N \l_hawkdraw_shading_colors_clist
\fp_new:N \l_hawkdraw_shading_rotate_fp
\fp_new:N \l_hawkdraw_shading_offset_fp
\bool_new:N \l_hawkdraw_shading_circular_bool

\dim_new:N \l_hawkdraw_shading_bbox_xmin_dim
\dim_new:N \l_hawkdraw_shading_bbox_xmax_dim
\dim_new:N \l_hawkdraw_shading_bbox_ymin_dim
\dim_new:N \l_hawkdraw_shading_bbox_ymax_dim

\exp_args_generate:n { NVVVV }

\cs_set:Npn \hawkdraw_scope_reset_bb_begin: {
    \draw_scope_begin:
        \__draw_scope_bb_begin:
}
\cs_set:Npn \hawkdraw_scope_reset_bb_end: {
        \__draw_scope_bb_end:
    \draw_scope_end:
}


\keys_define:nn { hawkdraw / path } {
    fill ~ shading          .code:n         = {
        \__hawkdraw_error_unsupported:n { Shading }
        \tl_put_right:Nn \l_hawkdraw_path_preactions_tl {
            \hawkdraw_scope_reset_bb_begin:
                \tl_trim_spaces_apply:nN { \g_hawkdraw_softpath_original_tl } \__hawkdraw_path_process:o
                \tl_use:N \g_hawkdraw_softpath_original_tl
                \draw_path_use_clear:n { clip }
                \dim_set_eq:NN \l_hawkdraw_shading_bbox_xmin_dim
                    \g_draw_bb_xmin_dim
                \dim_set_eq:NN \l_hawkdraw_shading_bbox_xmax_dim
                    \g_draw_bb_xmax_dim
                \dim_set_eq:NN \l_hawkdraw_shading_bbox_ymin_dim
                    \g_draw_bb_ymin_dim
                \dim_set_eq:NN \l_hawkdraw_shading_bbox_ymax_dim
                    \g_draw_bb_ymax_dim
                \hawkdraw_shading_fill:n {#1}
                \bool_gset_false:N \g__hawkdraw_path_use_clip_bool
            \hawkdraw_scope_reset_bb_end:
        }
    } ,
    shading ~ colors        .clist_set:N    = \l_hawkdraw_shading_colors_clist ,
    shading ~ rotate        .fp_set:N       = \l_hawkdraw_shading_rotate_fp ,
    shading ~ rotate        .initial:n      = { 0.0 } ,
    shading ~ offset        .fp_set:N       = \l_hawkdraw_shading_offset_fp ,
    shading ~ offset        .initial:n      = { 0.0 } ,
    shading ~ circular      .bool_set:N     = \l_hawkdraw_shading_circular_bool ,
    shading ~ circular      .default:n      = { true } ,
    shading ~ circular      .initial:n      = { false } ,
}

\tl_new:N \l__hawkdraw_shading_color_value_tmp_tl
\clist_new:N \l__hawkdraw_shading_color_values_clist
\box_new:N \l__hawkdraw_shading_object_box

\cs_generate_variant:Nn \color_export:nnN { e }

\cs_new_protected:Npn \hawkdraw_shading_set:nn #1#2 {
    \hbox_set_to_wd:Nnn \l__hawkdraw_shading_object_box { 100bp } {
        \vbox_to_ht:nn { 100bp } {
            \skip_vertical:n { 0pt plus 1fil }
            \hawkdraw_support_pdfliteral:n { /Sh ~ sh }
        }
        \skip_horizontal:n { 0pt plus 1fil }
    }
    \hawkdraw_support_pdfxform:nnN { } {
        /Shading ~ << ~
            /Sh ~ << ~
                #2
            >> ~
        >>
    } \l__hawkdraw_shading_object_box
    \int_const:cn {
        c__hawkdraw_shading_ #1 _int
    } {
        \hawkdraw_support_pdflastxform:
    }
}

\cs_new_protected:Npn \hawkdraw_shading_use:n #1 {
    \hawkdraw_support_pdfrefxform:c {
        c__hawkdraw_shading_ #1 _int
    }
}

\cs_new_protected:Npn \hawkdraw_shading_color_set_values:Nn #1#2 {
    \clist_map_inline:nn {#2} {
        \color_export:enN {##1} { space-sep-rgb }
            \l__hawkdraw_shading_color_value_tmp_tl
        \clist_put_right:NV #1 \l__hawkdraw_shading_color_value_tmp_tl
    }
}
\cs_generate_variant:Nn \hawkdraw_shading_color_set_values:Nn { NV }

\cs_new_protected:Npn \hawkdraw_shading_fill:n #1 {
    \hawkdraw_shading_create:nV {#1} \l_hawkdraw_shading_colors_clist
    \hbox_set:Nn \l_hawkdraw_shading_box {
        \hawkdraw_shading_use:n {
            #1
            . \clist_use:N \l_hawkdraw_shading_colors_clist
            \str_if_eq:nnT {#1} { radial } {
                . \fp_to_decimal:n { \l_hawkdraw_shading_offset_fp }
            }
        }
    }
    % scale to 2 * width of current bbox, 2 * height of current bbox
    % translate to center of current bbox
    \draw_transform_shift:n {
        (
            \l_hawkdraw_shading_bbox_xmin_dim + (
                \l_hawkdraw_shading_bbox_xmax_dim -
                \l_hawkdraw_shading_bbox_xmin_dim
            ) / 2
        ,
            \l_hawkdraw_shading_bbox_ymin_dim + (
                \l_hawkdraw_shading_bbox_ymax_dim -
                \l_hawkdraw_shading_bbox_ymin_dim
            ) / 2
        )
    }
    \draw_transform_rotate:n {
        \l_hawkdraw_shading_rotate_fp
    }
    \bool_lazy_and:nnT {
        \l_hawkdraw_shading_circular_bool
    } {
        \str_if_eq_p:nn {#1} { radial }
    } {
        \dim_set:Nn \l_hawkdraw_shading_bbox_xmax_dim {
            \dim_max:nn {
                \l_hawkdraw_shading_bbox_xmax_dim
            } {
                \l_hawkdraw_shading_bbox_ymax_dim
            }
        }
        \dim_set:Nn \l_hawkdraw_shading_bbox_xmin_dim {
            \dim_min:nn {
                \l_hawkdraw_shading_bbox_xmin_dim
            } {
                \l_hawkdraw_shading_bbox_ymin_dim
            }
        }
        \dim_set_eq:NN \l_hawkdraw_shading_bbox_ymax_dim
            \l_hawkdraw_shading_bbox_xmax_dim
        \dim_set_eq:NN \l_hawkdraw_shading_bbox_ymin_dim
            \l_hawkdraw_shading_bbox_xmin_dim
    }
    \box_resize_to_wd_and_ht:Nnn \l_hawkdraw_shading_box {
        \fp_to_dim:n {
            (
                \l_hawkdraw_shading_bbox_xmax_dim -
                \l_hawkdraw_shading_bbox_xmin_dim
            ) * 2
        }
    } {
        \fp_to_dim:n {
            (
                \l_hawkdraw_shading_bbox_ymax_dim -
                \l_hawkdraw_shading_bbox_ymin_dim
            ) * 2
        }
    }
    \draw_box_use:Nn \l_hawkdraw_shading_box {
        (
            \l_hawkdraw_shading_bbox_xmin_dim -
            \l_hawkdraw_shading_bbox_xmax_dim
        ,
            \l_hawkdraw_shading_bbox_ymin_dim -
            \l_hawkdraw_shading_bbox_ymax_dim
        )
    }
}

\cs_new_protected:Npn \hawkdraw_shading_create:nn #1#2 {
    \cs_if_exist_use:cTF { hawkdraw_shading_ #1 :n } {
        {#2}
    } {
        \msg_warning:nnn { hawkdraw } { shading-unknown } {#1}
    }
}
\cs_generate_variant:Nn \hawkdraw_shading_create:nn { nV }

% ===

\cs_set:Npn \__hawkdraw_shading_linear_bounds:n #1 {
    \fp_format:nn { 25 } { .5 } ~
    \int_step_tokens:nnn { 2 } { \clist_count:n {#1} - 1 } {
        \__hawkdraw_shading_linear_bounds_aux:nn { \clist_count:n {#1} }
    }
    \fp_format:nn { 75 } { .5 } ~
}

\cs_set:Npn \__hawkdraw_shading_linear_bounds_aux:nn #1#2 {
    \fp_format:nn { ( 50 / ( #1 - 1 ) ) * ( #2 - 1 ) + 25 } { .5 } ~
}

\cs_new:Npn \__hawkdraw_shading_linear_functions:n #1 {
    << ~
        /FunctionType ~ 2 ~
        /Domain ~ [ 0.0 ~ 100.0 ] ~
        /C0 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { 1 }
        ] ~
        /C1 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { 1 }
        ] ~
        /N ~ 1 ~
    >> ~
    \int_step_function:nnN { 2 } { \clist_count:N \l__hawkdraw_shading_color_values_clist }
        \__hawkdraw_shading_linear_functions_aux:n
    << ~
        /FunctionType ~ 2 ~
        /Domain ~ [ 0.0 ~ 100.0 ] ~
        /C0 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { -1 }
        ] ~
        /C1 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { -1 }
        ] ~
        /N ~ 1 ~
    >> ~
}
\cs_generate_variant:Nn \__hawkdraw_shading_linear_functions:n { V }

\cs_new:Npn \__hawkdraw_shading_linear_functions_aux:n #1 {
    << ~
        /FunctionType ~ 2 ~
        /Domain ~ [ 0.0 ~ 100.0 ] ~
        /C0 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { #1 - 1 }
        ] ~
        /C1 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                {#1}
        ] ~
        /N ~ 1 ~
    >> ~
}

\cs_new_protected:Npn \hawkdraw_shading_linear:n #1 {
    \int_if_exist:cF {
        c__hawkdraw_shading_
        linear
        . #1
        _int
    } {
        \hawkdraw_shading_color_set_values:Nn
            \l__hawkdraw_shading_color_values_clist
            {#1}
        \hawkdraw_shading_set:nn {
            linear
            . #1
        } {
            /ShadingType ~ 2 ~
            /ColorSpace ~ /DeviceRGB ~
            /Domain ~ [ 0.0 ~ 100.0 ] ~
            /Coords ~ [ 0.0 ~ 0.0 ~ 0.0 ~ 100.0 ] ~
            /Extend ~ [ false ~ false ] ~
            /Function ~ << ~
                /FunctionType ~ 3 ~
                /Domain ~ [ 0.0 ~ 100.0 ] ~
                /Functions ~ [ ~
                    \__hawkdraw_shading_linear_functions:V
                        \l__hawkdraw_shading_color_values_clist
                ] ~
                /Bounds ~ [
                    \__hawkdraw_shading_linear_bounds:n {#1}
                ] ~
                /Encode ~ [
                    \prg_replicate:nn { \clist_count:n {#1} + 1 } {
                        0 ~ 1 ~
                    }
                ] ~
            >> ~
        }
    }
}

% ===

\cs_new:Npn \__hawkdraw_shading_radial_bounds:n #1 {
    \int_step_tokens:nn { \clist_count:n {#1} - 1 } {
        \__hawkdraw_shading_radial_bounds_aux:nn { \clist_count:n {#1} }
    }
}

\cs_new:Npn \__hawkdraw_shading_radial_bounds_aux:nn #1#2 {
    \fp_format:nn { ( 50 / ( #1 + 1 ) ) * ( #2 ) } { .3 } ~
}

\cs_new:Npn \__hawkdraw_shading_radial_functions:n #1 {
    \int_step_function:nN { \clist_count:N \l__hawkdraw_shading_color_values_clist - 1 }
        \__hawkdraw_shading_radial_functions_aux:n
    << ~
        /FunctionType ~ 2 ~
        /Domain ~ [ 0.0 ~ 50.0 ] ~
        /C0 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { -1 }
        ] ~
        /C1 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { -1 }
        ] ~
        /N ~ 1 ~
    >> ~
}
\cs_generate_variant:Nn \__hawkdraw_shading_radial_functions:n { V }

\cs_new:Npn \__hawkdraw_shading_radial_functions_aux:n #1 {
    << ~
        /FunctionType ~ 2 ~
        /Domain ~ [ 0.0 ~ 50.0 ] ~
        /C0 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                {#1}
        ] ~
        /C1 ~ [
            \clist_item:Nn \l__hawkdraw_shading_color_values_clist
                { #1 + 1 }
        ] ~
        /N ~ 1 ~
    >> ~
}

\cs_new:Npn \hawkdraw_shading_radial_offset:n #1 {
    \fp_format:nn { 50 + ( #1 * 25 ) } { .5 } ~
}

\cs_new_protected:Npn \hawkdraw_shading_radial:n #1 {
    \int_if_exist:cF {
        c__hawkdraw_shading_
        radial
        . #1
        . \fp_to_decimal:n { \l_hawkdraw_shading_offset_fp }
        _int
    } {
        \hawkdraw_shading_color_set_values:Nn
            \l__hawkdraw_shading_color_values_clist
            {#1}
        \hawkdraw_shading_set:nn {
            radial
            . #1
            . \fp_to_decimal:n { \l_hawkdraw_shading_offset_fp }
        } {
            /ShadingType ~ 3 ~
            /ColorSpace ~ /DeviceRGB ~
            /Domain ~ [ 0.0 ~ 50.0 ] ~
            /Coords ~ [
                \hawkdraw_shading_radial_offset:n {
                    \l_hawkdraw_shading_offset_fp
                }
                50.0 ~ 0.0 ~ 50.0 ~ 50.0 ~ 50.0
            ] ~
            /Extend ~ [ true ~ false ] ~
            /Function ~ << ~
                /FunctionType ~ 3 ~
                /Domain ~ [ 0.0 ~ 50.0 ] ~
                /Functions ~ [ ~
                    \__hawkdraw_shading_radial_functions:V
                        \l__hawkdraw_shading_color_values_clist
                ] ~
                /Bounds ~ [
                    \__hawkdraw_shading_radial_bounds:n {#1}
                ] ~
                /Encode ~ [
                    \prg_replicate:nn { \clist_count:n {#1} } {
                        0 ~ 1 ~
                    }
                ] ~
            >> ~
        }
    }
}

%EOF