Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve CairoMakie lines with array colors #3141

Merged
merged 4 commits into from Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
121 changes: 103 additions & 18 deletions CairoMakie/src/primitives.jl
Expand Up @@ -58,8 +58,10 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio
# stroke each segment separately, this means disjointed segments with probably
# wonky dash patterns if segments are short

# we can hide the gaps by setting the line cap to round
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_ROUND)
# Butted segments look the best for varying colors, at least when connection angles are small.
# While round style has nicer sharp joins, it looks bad with alpha colors (double paint) and
# also messes with dash patterns (they are too long because of the caps)
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_BUTT)
draw_multi(
primitive, ctx,
projected_positions,
Expand Down Expand Up @@ -158,30 +160,22 @@ function draw_multi(primitive, ctx, positions, color, linewidths::AbstractArray,
draw_multi(primitive, ctx, positions, [color for l in linewidths], linewidths, dash)
end

function draw_multi(primitive::Union{Lines, LineSegments}, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, dash)
if primitive isa LineSegments
@assert iseven(length(positions))
end
function draw_multi(primitive::LineSegments, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, dash)
@assert iseven(length(positions))
@assert length(positions) == length(colors)
@assert length(linewidths) == length(colors)

iterator = if primitive isa Lines
1:length(positions)-1
elseif primitive isa LineSegments
1:2:length(positions)
end

for i in iterator
for i in 1:2:length(positions)
if isnan(positions[i+1]) || isnan(positions[i])
continue
end
Cairo.move_to(ctx, positions[i]...)

Cairo.line_to(ctx, positions[i+1]...)
if linewidths[i] != linewidths[i+1]
error("Cairo doesn't support two different line widths ($(linewidths[i]) and $(linewidths[i+1])) at the endpoints of a line.")
end
Cairo.move_to(ctx, positions[i]...)
Cairo.line_to(ctx, positions[i+1]...)
Cairo.set_line_width(ctx, linewidths[i])

!isnothing(dash) && Cairo.set_dash(ctx, dash .* linewidths[i])
c1 = colors[i]
c2 = colors[i+1]
Expand All @@ -199,8 +193,99 @@ function draw_multi(primitive::Union{Lines, LineSegments}, ctx, positions, color
Cairo.destroy(pat)
end
end
# force clearing of path in case of skipped NaN
Cairo.new_path(ctx)
end

function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, dash)
@assert length(positions) == length(colors)
@assert length(linewidths) == length(colors)

prev_color = colors[begin]
prev_linewidth = linewidths[begin]
prev_position = positions[begin]
prev_nan = isnan(prev_position)
prev_continued = false

if !prev_nan
# first is not nan, move_to
Cairo.move_to(ctx, positions[begin]...)
else
# first is nan, do nothing
end

for i in eachindex(positions)[2:end]
this_position = positions[i]
this_color = colors[i]
this_nan = isnan(this_position)
this_linewidth = linewidths[i]
if this_nan
# this is nan
if prev_continued
# and this is prev_continued, so set source and stroke to finish previous line
Cairo.set_line_width(ctx, this_linewidth)
!isnothing(dash) && Cairo.set_dash(ctx, dash .* this_linewidth)
Cairo.set_source_rgba(ctx, red(prev_color), green(prev_color), blue(prev_color), alpha(prev_color))
Cairo.stroke(ctx)
else
# but this is not prev_continued, so do nothing
end
end
if prev_nan
# previous was nan
if !this_nan
# but this is not nan, so move to this position
Cairo.move_to(ctx, this_position...)
else
# and this is also nan, do nothing
end
else
if this_color == prev_color
# this color is like the previous
if !this_nan
# and this is not nan, so line_to and set prev_continued
this_linewidth != prev_linewidth && error("Encountered two different linewidth values $prev_linewidth and $this_linewidth in `lines` at index $(i-1). Different linewidths in one line are only permitted in CairoMakie when separated by a NaN point.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a bit off-topic, but is this a restriction from Cairo or could we have per-element linewidths here? GLMakie supports those so it would be nice if it worked in CairoMakie too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do it via polygons I guess, then we would have to handle all joins manually. But normal strokes don't do it

Cairo.line_to(ctx, this_position...)
prev_continued = true

if i == lastindex(positions)
# this is the last element so stroke this
Cairo.set_line_width(ctx, this_linewidth)
!isnothing(dash) && Cairo.set_dash(ctx, dash .* this_linewidth)
Cairo.set_source_rgba(ctx, red(this_color), green(this_color), blue(this_color), alpha(this_color))
Cairo.stroke(ctx)
end
else
# but this is nan, so do nothing
end
else
prev_continued = false
if !this_nan
this_linewidth != prev_linewidth && error("Encountered two different linewidth values $prev_linewidth and $this_linewidth in `lines` at index $(i-1). Different linewidths in one line are only permitted in CairoMakie when separated by a NaN point.")
# this is not nan
# and this color is different than the previous, so move_to prev and line_to this
# create gradient pattern and stroke
Cairo.move_to(ctx, prev_position...)
Cairo.line_to(ctx, this_position...)
!isnothing(dash) && Cairo.set_dash(ctx, dash .* this_linewidth)
Cairo.set_line_width(ctx, this_linewidth)

pat = Cairo.pattern_create_linear(prev_position..., this_position...)
Cairo.pattern_add_color_stop_rgba(pat, 0, red(prev_color), green(prev_color), blue(prev_color), alpha(prev_color))
Cairo.pattern_add_color_stop_rgba(pat, 1, red(this_color), green(this_color), blue(this_color), alpha(this_color))
Cairo.set_source(ctx, pat)
Cairo.stroke(ctx)
Cairo.destroy(pat)

Cairo.move_to(ctx, this_position...)
else
# this is nan, do nothing
end
end
end
prev_nan = this_nan
prev_color = this_color
prev_linewidth = linewidths[i]
prev_position = this_position
end
end

################################################################################
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Expand Up @@ -2,6 +2,7 @@

## master

- Improved CairoMakie rendering of `lines` with repeating colors in an array [#3141](https://github.com/MakieOrg/Makie.jl/pull/3141).
- Added `strokecolormap` to poly. [#3145](https://github.com/MakieOrg/Makie.jl/pull/3145)
- Added `xreversed`, `yreversed` and `zreversed` attributes to `Axis3` [#3138](https://github.com/MakieOrg/Makie.jl/pull/3138).
- Fixed incorrect placement of contourlabels with transform functions [#3083](https://github.com/MakieOrg/Makie.jl/pull/3083)
Expand Down
26 changes: 26 additions & 0 deletions ReferenceTests/src/tests/examples2d.jl
Expand Up @@ -1119,3 +1119,29 @@ end
# Display
fig
end

@reference_test "lines (some with NaNs) with array colors" begin
f = Figure()
ax = Axis(f[1, 1])
hidedecorations!(ax)
hidespines!(ax)
lines!(ax, 1:10, 1:10, color = fill(RGBAf(1, 0, 0, 0.5), 10), linewidth = 5)
lines!(ax, 1:10, 2:11, color = [fill(RGBAf(1, 0, 0, 0.5), 5); fill(RGBAf(0, 0, 1, 0.5), 5)], linewidth = 5)
lines!(ax, 1:10, [3, 4, NaN, 6, 7, NaN, 9, 10, 11, NaN], color = [fill(RGBAf(1, 0, 0, 0.5), 5); fill(RGBAf(0, 0, 1, 0.5), 5)], linewidth = 5)
lines!(ax, 1:10, 4:13, color = repeat([RGBAf(1, 0, 0, 0.5), RGBAf(0, 0, 1, 0.5)], 5), linewidth = 5)
lines!(ax, 1:10, fill(NaN, 10), color = repeat([RGBAf(1, 0, 0, 0.5), RGBAf(0, 0, 1, 0.5)], 5), linewidth = 5)
lines!(ax, 1:10, [6, 7, 8, NaN, 10, 11, 12, 13, 14, 15], color = [:red, :blue, fill(:red, 8)...], linewidth = 5)
lines!(ax, 1:3, [7, 8, 9], color = [:red, :red, :blue], linewidth = 5)
lines!(ax, 1:3, [8, 9, NaN], color = [:red, :red, :blue], linewidth = 5)
lines!(ax, 1:3, [NaN, 10, 11], color = [:red, :red, :blue], linewidth = 5)
lines!(ax, 1:5, [10, 11, NaN, 13, 14], color = [:red, :red, :blue, :blue, :blue], linewidth = [5, 5, 5, 10, 10])
lines!(ax, 1:10, 11:20, color = [fill(RGBAf(1, 0, 0, 0.5), 5); fill(RGBAf(0, 0, 1, 0.5), 5)], linewidth = 5, linestyle = :dot)
lines!(ax, 1:10, 12:21, color = fill(RGBAf(1, 0, 0, 0.5), 10), linewidth = 5, linestyle = :dot)
f
end

@reference_test "contour with single alpha color" begin
x = range(-π, π; length=50)
z = @. sin(x) * cos(x')
fig, ax = contour(x, x, z, color=RGBAf(1,0,0,0.4), linewidth=6)
end