fortplot_raster_impl.f90 Source File


Source Code

submodule (fortplot_raster) fortplot_raster_impl
    !! Implementation of drawing primitives for raster_context.
    !! Contains: line, color, line_width, line_style, marker, arrow.

    use fortplot_raster_core, only: pt2px
    use fortplot_3d_axes, only: draw_3d_axes

contains

    !! Drawing primitives

    module subroutine raster_draw_line(this, x1, y1, x2, y2)
        use fortplot_line_styles, only: scale_pattern_to_pixels, get_pattern_length
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: x1, y1, x2, y2
        real(wp) :: px1, py1, px2, py2
        real(wp) :: scaled_pattern(20), scaled_length
        ! Transform coordinates to plot area (like matplotlib)
        ! Note: Raster Y=0 at top, so we need to flip Y coordinates
        px1 = (x1 - this%x_min)/(this%x_max - this%x_min)* &
              real(this%plot_area%width, wp) + real(this%plot_area%left, wp)
        py1 = real(this%plot_area%bottom + this%plot_area%height, wp) - &
              (y1 - this%y_min)/(this%y_max - &
                                 this%y_min)*real(this%plot_area%height, wp)
        px2 = (x2 - this%x_min)/(this%x_max - this%x_min)* &
              real(this%plot_area%width, wp) + real(this%plot_area%left, wp)
        py2 = real(this%plot_area%bottom + this%plot_area%height, wp) - &
              (y2 - this%y_min)/(this%y_max - &
                                 this%y_min)*real(this%plot_area%height, wp)

        ! Scale the point-based pattern to pixels for this line width and DPI,
        ! matching matplotlib (pattern_pt * line_width_pt * dpi / 72).
        scaled_pattern = this%raster%line_pattern
        call scale_pattern_to_pixels(scaled_pattern, this%raster%pattern_size, &
                                     this%raster%dpi, this%raster%current_line_width_pt)
        scaled_length = get_pattern_length(scaled_pattern, this%raster%pattern_size)

        ! Draw line with pattern support
        call draw_styled_line(this%raster%image_data, this%width, this%height, &
                              px1, py1, px2, py2, &
                              this%raster%current_r, this%raster%current_g, &
                              this%raster%current_b, &
                              this%raster%current_line_width, &
                              this%raster%line_style, scaled_pattern, &
                              this%raster%pattern_size, scaled_length, &
                              this%raster%pattern_distance)
    end subroutine raster_draw_line

    module subroutine raster_set_color_context(this, r, g, b)
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: r, g, b

        call this%raster%set_color(r, g, b)
    end subroutine raster_set_color_context

    module subroutine raster_set_line_width(this, width)
        !! Set line width for raster drawing with DPI-aware point-to-pixel scaling.
        !! Input width is in points (1pt = 1/72 inch). Converted to pixels via DPI.
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: width
        real(wp) :: px

        if (width <= 0.0_wp) then
            this%raster%current_line_width = 1.0_wp
            this%raster%current_line_width_pt = 1.0_wp
        else
            px = pt2px(width, this%raster%dpi)
            this%raster%current_line_width = max(1.0_wp, px)
            this%raster%current_line_width_pt = width
        end if
    end subroutine raster_set_line_width

    module subroutine raster_set_line_style_context(this, style)
        !! Set line style for raster context
        class(raster_context), intent(inout) :: this
        character(len=*), intent(in) :: style

        call this%raster%set_line_style(style)
    end subroutine raster_set_line_style_context

    !! Marker drawing

    module subroutine raster_draw_marker(this, x, y, style, size)
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: style
        real(wp), intent(in), optional :: size
        real(wp) :: px, py, scale
        integer(1) :: r, g, b
        associate (dsl => len_trim(style)); end associate

        scale = 1.0_wp
        if (present(size)) scale = marker_size_scale(size)

        ! Transform coordinates to plot area
        px = (x - this%x_min)/(this%x_max - this%x_min)*real(this%plot_area%width, wp) &
             + real(this%plot_area%left, wp)
        py = real(this%plot_area%bottom + this%plot_area%height, wp) - &
             (y - this%y_min)/(this%y_max - this%y_min)*real(this%plot_area%height, wp)

        call this%raster%get_color_bytes(r, g, b)

        ! Fixed marker centering for Issue #333: align markers with line centers
        ! Apply sub-pixel adjustment to match line drawing coordinate conventions
        call draw_raster_marker_by_style(this, px - 0.5_wp, py - 0.5_wp, style, scale)
    end subroutine raster_draw_marker

    module subroutine draw_raster_marker_by_style(this, px, py, style, scale)
        !! Draw marker using shared style dispatch logic (DRY compliance)
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: px, py
        character(len=*), intent(in) :: style
        real(wp), intent(in) :: scale
        real(wp) :: marker_size

        marker_size = get_marker_size(style) * scale * this%raster%dpi / REFERENCE_DPI

        select case (trim(style))
        case (MARKER_CIRCLE)
            call draw_circle_with_edge_face(this%raster%image_data, this%width, &
                                            this%height, px, py, marker_size, &
                                            this%raster%marker_edge_r, &
                                            this%raster%marker_edge_g, &
                                            this%raster%marker_edge_b, &
                                            this%raster%marker_edge_alpha, &
                                            this%raster%marker_face_r, &
                                            this%raster%marker_face_g, &
                                            this%raster%marker_face_b, &
                                            this%raster%marker_face_alpha, &
                                            this%raster%current_line_width)
        case (MARKER_SQUARE)
            call draw_square_with_edge_face(this%raster%image_data, this%width, &
                                            this%height, px, py, marker_size, &
                                            this%raster%marker_edge_r, &
                                            this%raster%marker_edge_g, &
                                            this%raster%marker_edge_b, &
                                            this%raster%marker_edge_alpha, &
                                            this%raster%marker_face_r, &
                                            this%raster%marker_face_g, &
                                            this%raster%marker_face_b, &
                                            this%raster%marker_face_alpha, &
                                            this%raster%current_line_width)
        case (MARKER_DIAMOND)
            call draw_diamond_with_edge_face(this%raster%image_data, this%width, &
                                             this%height, px, py, marker_size, &
                                             this%raster%marker_edge_r, &
                                             this%raster%marker_edge_g, &
                                             this%raster%marker_edge_b, &
                                             this%raster%marker_edge_alpha, &
                                             this%raster%marker_face_r, &
                                             this%raster%marker_face_g, &
                                             this%raster%marker_face_b, &
                                             this%raster%marker_face_alpha, &
                                             this%raster%current_line_width)
        case (MARKER_CROSS)
            call draw_x_marker(this%raster%image_data, this%width, &
                               this%height, px, &
                               py, marker_size, &
                               this%raster%marker_edge_r, &
                               this%raster%marker_edge_g, &
                               this%raster%marker_edge_b, &
                               this%raster%marker_edge_alpha, &
                               this%raster%current_line_width)
        end select
    end subroutine draw_raster_marker_by_style

    module subroutine raster_set_marker_colors(this, edge_r, edge_g, edge_b, face_r, &
                                        face_g, face_b)
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: edge_r, edge_g, edge_b
        real(wp), intent(in) :: face_r, face_g, face_b

        this%raster%marker_edge_r = edge_r
        this%raster%marker_edge_g = edge_g
        this%raster%marker_edge_b = edge_b
        this%raster%marker_edge_alpha = 1.0_wp  ! Default to opaque
        this%raster%marker_face_r = face_r
        this%raster%marker_face_g = face_g
        this%raster%marker_face_b = face_b
        this%raster%marker_face_alpha = 1.0_wp  ! Default to opaque
    end subroutine raster_set_marker_colors

    module subroutine raster_set_marker_colors_with_alpha(this, edge_r, edge_g, edge_b, &
                                                       edge_alpha, &
                                                       face_r, face_g, face_b, face_alpha)
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: edge_r, edge_g, edge_b, edge_alpha
        real(wp), intent(in) :: face_r, face_g, face_b, face_alpha

        this%raster%marker_edge_r = edge_r
        this%raster%marker_edge_g = edge_g
        this%raster%marker_edge_b = edge_b
        this%raster%marker_edge_alpha = edge_alpha
        this%raster%marker_face_r = face_r
        this%raster%marker_face_g = face_g
        this%raster%marker_face_b = face_b
        this%raster%marker_face_alpha = face_alpha
    end subroutine raster_set_marker_colors_with_alpha

    !! Arrow drawing

    module subroutine raster_draw_arrow(this, x, y, dx, dy, size, style)
        !! Draw arrow head for streamplot arrows in raster backend
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: x, y, dx, dy, size
        character(len=*), intent(in) :: style

        real(wp) :: arrow_length, arrow_width, norm_dx, norm_dy
        real(wp) :: perp_x, perp_y  ! Perpendicular vector for arrow width
        real(wp) :: x1, y1, x2, y2, x3, y3  ! Triangle vertices
        real(wp) :: shaft_base_x, shaft_base_y
        real(wp) :: magnitude
        integer(1) :: r, g, b
        real(wp) :: px, py, ddx, ddy
        associate (dsl => len_trim(style)); end associate

        ! Transform data coordinates to pixel coordinates
        px = (x - this%x_min)/(this%x_max - this%x_min)*real(this%plot_area%width, wp) &
             + real(this%plot_area%left, wp)
        py = real(this%plot_area%bottom + this%plot_area%height, wp) - &
             (y - this%y_min)/(this%y_max - this%y_min)*real(this%plot_area%height, wp)

        ! Scale direction components to pixel units (account for inverted Y)
        ddx = dx*(real(this%plot_area%width, wp)/max(1.0_wp, (this%x_max - this%x_min)))
        ddy = -dy*(real(this%plot_area%height, wp)/max(1.0_wp, (this%y_max - &
                                                                this%y_min)))

        ! Normalize direction vector
        magnitude = sqrt(ddx*ddx + ddy*ddy)
        if (magnitude < 1e-10_wp) return  ! Avoid division by zero

        norm_dx = ddx/magnitude
        norm_dy = ddy/magnitude

        ! Calculate arrow dimensions based on size. Slim head aspect
        ! (full width ~0.7*length) matches matplotlib's head_width/head_length.
        arrow_length = size*8.0_wp  ! Scale factor for reasonable arrow size
        arrow_width = arrow_length*0.35_wp

        ! Calculate perpendicular vector for arrow width
        perp_x = -norm_dy
        perp_y = norm_dx

        ! Calculate triangle vertices for arrow head (tip at pixel coords)
        x1 = px
        y1 = py

        ! Base vertices of arrow head
        x2 = px - arrow_length*norm_dx + arrow_width*perp_x
        y2 = py - arrow_length*norm_dy + arrow_width*perp_y

        x3 = px - arrow_length*norm_dx - arrow_width*perp_x
        y3 = py - arrow_length*norm_dy - arrow_width*perp_y

        ! Shaft base: vector start = tip - magnitude*norm (shaft = magnitude - arrow_length)
        shaft_base_x = px - magnitude*norm_dx
        shaft_base_y = py - magnitude*norm_dy

        ! Get current color for filling
        call this%raster%get_color_bytes(r, g, b)

        call draw_line_distance_aa(this%raster%image_data, this%width, this%height, &
                                   shaft_base_x, shaft_base_y, px, py, &
                                   real(r, wp)/255.0_wp, real(g, wp)/255.0_wp, &
                                   real(b, wp)/255.0_wp, 1.0_wp)

        ! Draw filled triangle arrow head
        call fill_triangle(this%raster%image_data, this%width, this%height, &
                           x1, y1, x2, y2, x3, y3, r, g, b)

        ! Mark that arrows have been rendered
        this%has_rendered_arrows = .true.
        this%uses_vector_arrows = .false.
        this%has_triangular_arrows = .true.
    end subroutine raster_draw_arrow

    module subroutine raster_draw_arrowhead(this, x, y, dx, dy, size, style)
        !! Draw a streamplot-style open arrowhead at (x, y), matching
        !! matplotlib's FancyArrowPatch "->" glyph: two thin strokes meeting
        !! at the tip, in the streamline color and line width. (dx, dy) is a
        !! unit direction in data space; size is matplotlib's arrowsize and
        !! controls the head length in pixels, independent of the data range.
        class(raster_context), intent(inout) :: this
        real(wp), intent(in) :: x, y, dx, dy, size
        character(len=*), intent(in) :: style

        real(wp), parameter :: head_half_angle = 0.5235987756_wp  ! 30 degrees
        real(wp) :: head_length, line_w
        real(wp) :: norm_dx, norm_dy, magnitude
        real(wp) :: px, py, ddx, ddy
        real(wp) :: cos_a, sin_a
        real(wp) :: lx, ly, rx, ry
        associate (dsl => len_trim(style)); end associate

        ! Transform tip position to pixel coordinates.
        px = (x - this%x_min)/(this%x_max - this%x_min)*real(this%plot_area%width, wp) &
             + real(this%plot_area%left, wp)
        py = real(this%plot_area%bottom + this%plot_area%height, wp) - &
             (y - this%y_min)/(this%y_max - this%y_min)*real(this%plot_area%height, wp)

        ! Convert the data-space unit direction to a screen-space direction
        ! (the Y axis is flipped and the axes have independent pixel scales),
        ! then renormalize so the head length is a fixed pixel quantity.
        ddx = dx*(real(this%plot_area%width, wp)/max(1.0_wp, (this%x_max - this%x_min)))
        ddy = -dy*(real(this%plot_area%height, wp)/max(1.0_wp, (this%y_max - &
                                                                this%y_min)))
        magnitude = sqrt(ddx*ddx + ddy*ddy)
        if (magnitude < 1.0e-10_wp) return
        norm_dx = ddx/magnitude
        norm_dy = ddy/magnitude

        ! matplotlib draws the "->" head from the tip backwards. mutation_scale
        ! is 10*arrowsize points; the visible head spans roughly half of that.
        head_length = size*6.0_wp
        line_w = max(1.0_wp, this%raster%current_line_width)

        cos_a = cos(head_half_angle)
        sin_a = sin(head_half_angle)

        ! Two backward strokes rotated by +/- the half angle from the
        ! reverse direction, forming an open V whose vertex is the tip.
        lx = px - head_length*(norm_dx*cos_a - norm_dy*sin_a)
        ly = py - head_length*(norm_dy*cos_a + norm_dx*sin_a)
        rx = px - head_length*(norm_dx*cos_a + norm_dy*sin_a)
        ry = py - head_length*(norm_dy*cos_a - norm_dx*sin_a)

        call draw_line_distance_aa(this%raster%image_data, this%width, this%height, &
                                   px, py, lx, ly, this%raster%current_r, &
                                   this%raster%current_g, this%raster%current_b, line_w)
        call draw_line_distance_aa(this%raster%image_data, this%width, this%height, &
                                   px, py, rx, ry, this%raster%current_r, &
                                   this%raster%current_g, this%raster%current_b, line_w)

        this%has_rendered_arrows = .true.
        this%uses_vector_arrows = .true.
        this%has_triangular_arrows = .false.
    end subroutine raster_draw_arrowhead

end submodule fortplot_raster_impl