diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 9b96d7a8ca2..9d6603356bd 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -39,7 +39,7 @@ jobs: - name: Install Julia uses: julia-actions/setup-julia@v1 with: - version: 1.6 + version: '1' - name: Build and deploy docs env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab3c64f7551..8f98103de62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,6 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - run: sudo apt-get -y install xclip - uses: julia-actions/cache@v1 - name: Install Julia dependencies shell: julia --project=monorepo {0} diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index ca485e9b775..a418490ffba 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -25,7 +25,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - uses: julia-actions/setup-julia@v1 with: - version: nightly + version: '1' arch: x64 - uses: julia-actions/cache@v1 - name: Benchmark diff --git a/.gitignore b/.gitignore index 1df708581d4..413c3a4b6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ Manifest.toml /build \.DS_Store -CairoMakie/src/display.* WGLMakie/test/recorded_reference_images/ GLMakie/test/recorded_reference_images/ diff --git a/CairoMakie/README.md b/CairoMakie/README.md index 29e0fac8933..2f199b50853 100644 --- a/CairoMakie/README.md +++ b/CairoMakie/README.md @@ -38,7 +38,7 @@ function drawonto(canvas, figure) @guarded draw(canvas) do _ scene = figure.scene resize!(scene, Gtk.width(canvas), Gtk.height(canvas)) - screen = CairoMakie.CairoScreen(scene, Gtk.cairo_surface(canvas), getgc(canvas), nothing) + screen = CairoMakie.Screen(scene, Gtk.cairo_surface(canvas), getgc(canvas), nothing) CairoMakie.cairo_draw(screen, scene) end end diff --git a/CairoMakie/src/CairoMakie.jl b/CairoMakie/src/CairoMakie.jl index b169e8726de..b5995fe1f33 100644 --- a/CairoMakie/src/CairoMakie.jl +++ b/CairoMakie/src/CairoMakie.jl @@ -8,9 +8,8 @@ import Cairo using Makie: Scene, Lines, Text, Image, Heatmap, Scatter, @key_str, broadcast_foreach using Makie: convert_attribute, @extractvalue, LineSegments, to_ndim, NativeFont -using Makie: @info, @get_attribute, Combined +using Makie: @info, @get_attribute, Combined, MakieScreen using Makie: to_value, to_colormap, extrema_nan -using Makie: inline! using Makie.Observables using Makie: spaces, is_data_space, is_pixel_space, is_relative_space, is_clip_space using Makie: numbers_to_colors @@ -22,45 +21,19 @@ for name in names(Makie, all=true) @eval export $(name) end end -export inline! +include("cairo-extension.jl") +include("screen.jl") +include("display.jl") include("infrastructure.jl") include("utils.jl") -include("fonts.jl") include("primitives.jl") include("overrides.jl") function __init__() activate!() - Makie.register_backend!(Makie.current_backend[]) -end - -function display_path(type::String) - if !(type in ("svg", "png", "pdf", "eps")) - error("Only \"svg\", \"png\", \"eps\" and \"pdf\" are allowed for `type`. Found: $(type)") - end - return joinpath(@__DIR__, "display." * type) -end - -const _last_inline = Ref(true) -const _last_type = Ref("png") -const _last_px_per_unit = Ref(1.0) -const _last_pt_per_unit = Ref(0.75) -const _last_antialias = Ref(Cairo.ANTIALIAS_BEST) - -function activate!(; inline = _last_inline[], type = _last_type[], px_per_unit=_last_px_per_unit[], pt_per_unit=_last_pt_per_unit[], antialias = _last_antialias[]) - backend = CairoBackend(display_path(type); px_per_unit=px_per_unit, pt_per_unit=pt_per_unit, antialias = antialias) - Makie.current_backend[] = backend - Makie.use_display[] = !inline - _last_inline[] = inline - _last_type[] = type - _last_px_per_unit[] = px_per_unit - _last_pt_per_unit[] = pt_per_unit - _last_antialias[] = antialias - return end include("precompiles.jl") - end diff --git a/CairoMakie/src/cairo-extension.jl b/CairoMakie/src/cairo-extension.jl new file mode 100644 index 00000000000..6f96a926d3c --- /dev/null +++ b/CairoMakie/src/cairo-extension.jl @@ -0,0 +1,75 @@ +# TODO, move those to Cairo? + +function set_font_matrix(ctx, matrix) + ccall((:cairo_set_font_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) +end + +function get_font_matrix(ctx) + matrix = Cairo.CairoMatrix() + ccall((:cairo_get_font_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) + return matrix +end + +function cairo_font_face_destroy(font_face) + ccall( + (:cairo_font_face_destroy, Cairo.libcairo), + Cvoid, (Ptr{Cvoid},), + font_face + ) +end + +function set_ft_font(ctx, font) + + font_face = ccall( + (:cairo_ft_font_face_create_for_ft_face, Cairo.libcairo), + Ptr{Cvoid}, (Makie.FreeTypeAbstraction.FT_Face, Cint), + font, 0 + ) + + ccall((:cairo_set_font_face, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, font_face) + + return font_face +end + +struct CairoGlyph + index::Culong + x::Cdouble + y::Cdouble +end + +function show_glyph(ctx, glyph, x, y) + cg = Ref(CairoGlyph(glyph, x, y)) + ccall((:cairo_show_glyphs, Cairo.libcairo), + Nothing, (Ptr{Nothing}, Ptr{CairoGlyph}, Cint), + ctx.ptr, cg, 1) +end + +function glyph_path(ctx, glyph::Culong, x, y) + cg = Ref(CairoGlyph(glyph, x, y)) + ccall((:cairo_glyph_path, Cairo.libcairo), + Nothing, (Ptr{Nothing}, Ptr{CairoGlyph}, Cint), + ctx.ptr, cg, 1) +end + +function surface_set_device_scale(surf, device_x_scale, device_y_scale=device_x_scale) + # this sets a scaling factor on the lowest level that is "hidden" so its even + # enabled when the drawing space is reset for strokes + # that means it can be used to increase or decrease the image resolution + ccall( + (:cairo_surface_set_device_scale, Cairo.libcairo), + Cvoid, (Ptr{Nothing}, Cdouble, Cdouble), + surf.ptr, device_x_scale, device_y_scale) +end + +function set_miter_limit(ctx, limit) + ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, limit) +end + +function get_render_type(surface::Cairo.CairoSurface) + typ = ccall((:cairo_surface_get_type, Cairo.libcairo), Cint, (Ptr{Nothing},), surface.ptr) + typ == Cairo.CAIRO_SURFACE_TYPE_PDF && return PDF + typ == Cairo.CAIRO_SURFACE_TYPE_PS && return EPS + typ == Cairo.CAIRO_SURFACE_TYPE_SVG && return SVG + typ == Cairo.CAIRO_SURFACE_TYPE_IMAGE && return IMAGE + error("Unsupported surface type: $(typ)") +end diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl new file mode 100644 index 00000000000..eb7e28edba0 --- /dev/null +++ b/CairoMakie/src/display.jl @@ -0,0 +1,171 @@ + +######################################### +# Backend interface to Makie # +######################################### + +""" + tryrun(cmd::Cmd) +Try to run a command. Return `true` if `cmd` runs and is successful (exits with a code of `0`). +Return `false` otherwise. +""" +function tryrun(cmd::Cmd) + try + return success(cmd) + catch e + return false + end +end + +function openurl(url::String) + if Sys.isapple() + tryrun(`open $url`) && return + elseif Sys.iswindows() + tryrun(`powershell.exe start $url`) && return + elseif Sys.isunix() + tryrun(`xdg-open $url`) && return + tryrun(`gnome-open $url`) && return + end + tryrun(`python -mwebbrowser $(url)`) && return + # our last hope + tryrun(`python3 -mwebbrowser $(url)`) && return + @warn("Can't find a way to open a browser, open $(url) manually!") +end + +function display_path(type::String) + if !(type in ("svg", "png", "pdf", "eps")) + error("Only \"svg\", \"png\", \"eps\" and \"pdf\" are allowed for `type`. Found: $(type)") + end + return abspath(joinpath(@__DIR__, "display." * type)) +end + +function Base.display(screen::Screen{IMAGE}, scene::Scene; connect=false) + path = display_path("png") + cairo_draw(screen, scene) + Cairo.write_to_png(screen.surface, path) + if screen.visible + openurl("file:///" * path) + end +end + +function Makie.backend_show(screen::Screen{SVG}, io::IO, ::MIME"image/svg+xml", scene::Scene) + + cairo_draw(screen, scene) + Cairo.flush(screen.surface) + Cairo.finish(screen.surface) + + svg = String(take!(Makie.raw_io(screen.surface.stream))) + + # for some reason, in the svg, surfaceXXX ids keep counting up, + # even with the very same figure drawn again and again + # so we need to reset them to counting up from 1 + # so that the same figure results in the same svg and in the same salt + surfaceids = sort(unique(collect(m.match for m in eachmatch(r"surface\d+", svg)))) + + for (i, id) in enumerate(surfaceids) + svg = replace(svg, id => "surface$i") + end + + # salt svg ids with the first 8 characters of the base64 encoded + # sha512 hash to avoid collisions across svgs when embedding them on + # websites. the hash and therefore the salt will always be the same for the same file + # so the output is deterministic + salt = String(Base64.base64encode(SHA.sha512(svg)))[1:8] + + ids = sort(unique(collect(m[1] for m in eachmatch(r"id\s*=\s*\"([^\"]*)\"", svg)))) + + for id in ids + svg = replace(svg, id => "$id-$salt") + end + + print(io, svg) + return screen +end + +function Makie.backend_show(screen::Screen{PDF}, io::IO, ::MIME"application/pdf", scene::Scene) + cairo_draw(screen, scene) + Cairo.finish(screen.surface) + return screen +end + +function Makie.backend_show(screen::Screen{EPS}, io::IO, ::MIME"application/postscript", scene::Scene) + cairo_draw(screen, scene) + Cairo.finish(screen.surface) + return screen +end + +function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::MIME"image/png", scene::Scene) + cairo_draw(screen, scene) + Cairo.write_to_png(screen.surface, io) + return screen +end + +# Disabling mimes and showable + +const DISABLED_MIMES = Set{String}() +const SUPPORTED_MIMES = Set([ + "image/svg+xml", + "application/pdf", + "application/postscript", + "image/png" +]) + +function Makie.backend_showable(::Type{Screen}, ::MIME{SYM}) where SYM + supported_mimes = Base.setdiff(SUPPORTED_MIMES, DISABLED_MIMES) + return string(SYM) in supported_mimes +end + +""" + to_mime_string(mime::Union{String, Symbol, MIME}) + +Converts anything like `"png", :png, "image/png", MIME"image/png"()` to `"image/png"`. +""" +function to_mime_string(mime::Union{String, Symbol, MIME}) + if mime isa MIME + mime_str = string(mime) + if !(mime_str in SUPPORTED_MIMES) + error("Mime $(mime) not supported by CairoMakie") + end + return mime_str + else + mime_str = string(mime) + if !(mime_str in SUPPORTED_MIMES) + mime_str = string(to_mime(convert(RenderType, mime_str))) + end + return mime_str + end +end + +""" + disable_mime!(mime::Union{String, Symbol, MIME}...) + +The default is automatic, which lets the display system figure out the best mime. +If set to any other valid mime, will result in `showable(any_other_mime, figurelike)` to return false and only return true for `showable(preferred_mime, figurelike)`. +Depending on the display system used, this may result in nothing getting displayed. +""" +function disable_mime!(mimes::Union{String, Symbol, MIME}...) + empty!(DISABLED_MIMES) # always start from 0 + if isempty(mimes) + # Reset disabled mimes when called with no arguments + return + end + mime_strings = Set{String}() + for mime in mimes + push!(mime_strings, to_mime_string(mime)) + end + union!(DISABLED_MIMES, mime_strings) + return +end + +function enable_only_mime!(mimes::Union{String, Symbol, MIME}...) + empty!(DISABLED_MIMES) # always start from 0 + if isempty(mimes) + # Reset disabled mimes when called with no arguments + return + end + mime_strings = Set{String}() + for mime in mimes + push!(mime_strings, to_mime_string(mime)) + end + union!(DISABLED_MIMES, setdiff(SUPPORTED_MIMES, mime_strings)) + return +end diff --git a/CairoMakie/src/fonts.jl b/CairoMakie/src/fonts.jl deleted file mode 100644 index 254ff6fb6b0..00000000000 --- a/CairoMakie/src/fonts.jl +++ /dev/null @@ -1,46 +0,0 @@ -function set_font_matrix(ctx, matrix) - ccall((:cairo_set_font_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) -end - -function get_font_matrix(ctx) - matrix = Cairo.CairoMatrix() - ccall((:cairo_get_font_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) - return matrix -end - -function cairo_font_face_destroy(font_face) - ccall( - (:cairo_font_face_destroy, Cairo.libcairo), - Cvoid, (Ptr{Cvoid},), - font_face - ) -end - -function set_ft_font(ctx, font) - - font_face = ccall( - (:cairo_ft_font_face_create_for_ft_face, Cairo.libcairo), - Ptr{Cvoid}, (Makie.FreeTypeAbstraction.FT_Face, Cint), - font, 0 - ) - - ccall((:cairo_set_font_face, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, font_face) - - return font_face -end - -""" -Finds a font that can represent the unicode character! -Returns Makie.defaultfont() if not representable! -""" -function best_font(c::Char, font = Makie.defaultfont()) - if Makie.FreeType.FT_Get_Char_Index(font, c) == 0 - for afont in Makie.alternativefonts() - if Makie.FreeType.FT_Get_Char_Index(afont, c) != 0 - return afont - end - end - return Makie.defaultfont() - end - return font -end diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 0aa4216e353..2700dffd780 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -2,159 +2,13 @@ # Infrastructure # #################################################################################################### -################################################################################ -# Types # -################################################################################ - -@enum RenderType SVG PNG PDF EPS - -"The Cairo backend object. Used to dispatch to CairoMakie methods." -struct CairoBackend <: Makie.AbstractBackend - typ::RenderType - path::String - px_per_unit::Float64 - pt_per_unit::Float64 - antialias::Int # cairo_antialias_t -end - -""" - struct CairoScreen{S} <: AbstractScreen -A "screen" type for CairoMakie, which encodes a surface -and a context which are used to draw a Scene. -""" -struct CairoScreen{S} <: Makie.AbstractScreen - scene::Scene - surface::S - context::Cairo.CairoContext - pane::Nothing # TODO: GtkWindowLeaf -end - - -function CairoBackend(path::String; px_per_unit=1, pt_per_unit=1, antialias = Cairo.ANTIALIAS_BEST) - ext = splitext(path)[2] - typ = if ext == ".png" - PNG - elseif ext == ".svg" - SVG - elseif ext == ".pdf" - PDF - elseif ext == ".eps" - EPS - else - error("Unsupported extension: $ext") - end - CairoBackend(typ, path, px_per_unit, pt_per_unit, antialias) -end - -# we render the scene directly, since we have -# no screen dependent state like in e.g. opengl -Base.insert!(screen::CairoScreen, scene::Scene, plot) = nothing - -function Base.show(io::IO, ::MIME"text/plain", screen::CairoScreen{S}) where S - println(io, "CairoScreen{$S} with surface:") - println(io, screen.surface) -end - -# Default to ARGB Surface as backing device -# TODO: integrate Gtk into this, so we can have an interactive display -""" - CairoScreen(scene::Scene; antialias = Cairo.ANTIALIAS_BEST) -Create a CairoScreen backed by an image surface. -""" -function CairoScreen(scene::Scene; device_scaling_factor = 1, antialias = Cairo.ANTIALIAS_BEST) - w, h = round.(Int, scene.camera.resolution[] .* device_scaling_factor) - surf = Cairo.CairoARGBSurface(w, h) - - # this sets a scaling factor on the lowest level that is "hidden" so its even - # enabled when the drawing space is reset for strokes - # that means it can be used to increase or decrease the image resolution - ccall((:cairo_surface_set_device_scale, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble, Cdouble), - surf.ptr, device_scaling_factor, device_scaling_factor) - - ctx = Cairo.CairoContext(surf) - Cairo.set_antialias(ctx, antialias) - # Set the miter limit (when miter transitions to bezel) to mimic GLMakie behaviour - ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, 2.0) - - return CairoScreen(scene, surf, ctx, nothing) -end - -function get_type(surface::Cairo.CairoSurface) - return ccall((:cairo_surface_get_type, Cairo.libcairo), Cint, (Ptr{Nothing},), surface.ptr) -end - -is_vector_backend(ctx::Cairo.CairoContext) = is_vector_backend(ctx.surface) - -function is_vector_backend(surf::Cairo.CairoSurface) - typ = get_type(surf) - return typ in (Cairo.CAIRO_SURFACE_TYPE_PDF, Cairo.CAIRO_SURFACE_TYPE_PS, Cairo.CAIRO_SURFACE_TYPE_SVG) -end - -""" - CairoScreen( - scene::Scene, path::Union{String, IO}, mode::Symbol; - antialias = Cairo.ANTIALIAS_BEST - ) -Creates a CairoScreen pointing to a given output path, with some rendering type defined by `mode`. -""" -function CairoScreen(scene::Scene, path::Union{String, IO}, mode::Symbol; device_scaling_factor = 1, antialias = Cairo.ANTIALIAS_BEST) - - # the surface size is the scene size scaled by the device scaling factor - w, h = round.(Int, scene.camera.resolution[] .* device_scaling_factor) - - if mode == :svg - surf = Cairo.CairoSVGSurface(path, w, h) - elseif mode == :pdf - surf = Cairo.CairoPDFSurface(path, w, h) - elseif mode == :eps - surf = Cairo.CairoEPSSurface(path, w, h) - elseif mode == :png - surf = Cairo.CairoARGBSurface(w, h) - else - error("No available Cairo surface for mode $mode") - end - - # this sets a scaling factor on the lowest level that is "hidden" so its even - # enabled when the drawing space is reset for strokes - # that means it can be used to increase or decrease the image resolution - ccall((:cairo_surface_set_device_scale, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble, Cdouble), - surf.ptr, device_scaling_factor, device_scaling_factor) - - ctx = Cairo.CairoContext(surf) - Cairo.set_antialias(ctx, antialias) - # Set the miter limit (when miter transitions to bezel) to mimic GLMakie behaviour - ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, 2.0) - - return CairoScreen(scene, surf, ctx, nothing) -end - - -function Base.delete!(screen::CairoScreen, scene::Scene, plot::AbstractPlot) - # Currently, we rerender every time, so nothing needs - # to happen here. However, in the event that changes, - # e.g. if we integrate a Gtk window, we may need to - # do something here. -end - -"Convert a rendering type to a MIME type" -function to_mime(x::RenderType) - x == SVG && return MIME("image/svg+xml") - x == PDF && return MIME("application/pdf") - x == EPS && return MIME("application/postscript") - return MIME("image/png") -end -to_mime(x::CairoBackend) = to_mime(x.typ) - -################################################################################ -# Rendering pipeline # -################################################################################ - ######################################## # Drawing pipeline # ######################################## # The main entry point into the drawing pipeline -function cairo_draw(screen::CairoScreen, scene::Scene) +function cairo_draw(screen::Screen, scene::Scene) + Cairo.save(screen.context) draw_background(screen, scene) allplots = get_all_plots(scene) @@ -194,7 +48,7 @@ function cairo_draw(screen::CairoScreen, scene::Scene) end Cairo.restore(screen.context) end - + Cairo.restore(screen.context) return end @@ -206,7 +60,7 @@ function get_all_plots(scene, plots = AbstractPlot[]) plots end -function prepare_for_scene(screen::CairoScreen, scene::Scene) +function prepare_for_scene(screen::Screen, scene::Scene) # get the root area to correct for its pixel size when translating root_area = Makie.root(scene).px_area[] @@ -231,11 +85,11 @@ function prepare_for_scene(screen::CairoScreen, scene::Scene) return end -function draw_background(screen::CairoScreen, scene::Scene) +function draw_background(screen::Screen, scene::Scene) cr = screen.context Cairo.save(cr) if scene.clear[] - bg = to_color(theme(scene, :backgroundcolor)[]) + bg = scene.backgroundcolor[] Cairo.set_source_rgba(cr, red(bg), green(bg), blue(bg), alpha(bg)); r = pixelarea(scene)[] Cairo.rectangle(cr, origin(r)..., widths(r)...) # background @@ -245,7 +99,7 @@ function draw_background(screen::CairoScreen, scene::Scene) foreach(child_scene-> draw_background(screen, child_scene), scene.children) end -function draw_plot(scene::Scene, screen::CairoScreen, primitive::Combined) +function draw_plot(scene::Scene, screen::Screen, primitive::Combined) if to_value(get(primitive, :visible, true)) if isempty(primitive.plots) Cairo.save(screen.context) @@ -265,14 +119,14 @@ end # instead of the whole Scene # - Recognize when a screen is an image surface, and set scale to render the plot # at the scale of the device pixel -function draw_plot_as_image(scene::Scene, screen::CairoScreen, primitive::Combined, scale::Number = 1) +function draw_plot_as_image(scene::Scene, screen::Screen, primitive::Combined, scale::Number = 1) # you can provide `p.rasterize = scale::Int` or `p.rasterize = true`, both of which are numbers # Extract scene width in pixels w, h = Int.(scene.px_area[].widths) # Create a new Screen which renders directly to an image surface, # specifically for the plot's parent scene. - scr = CairoScreen(scene; device_scaling_factor = scale) + scr = Screen(scene; px_per_unit = scale) # Draw the plot to the screen, in the normal way draw_plot(scene, scr, primitive) @@ -295,132 +149,6 @@ function draw_plot_as_image(scene::Scene, screen::CairoScreen, primitive::Combin return end -function draw_atomic(::Scene, ::CairoScreen, x) +function draw_atomic(::Scene, ::Screen, x) @warn "$(typeof(x)) is not supported by cairo right now" end - -function clear(screen::CairoScreen) - ctx = screen.ctx - Cairo.save(ctx) - Cairo.set_operator(ctx, Cairo.OPERATOR_SOURCE) - Cairo.set_source_rgba(ctx, rgbatuple(screen.scene[:backgroundcolor])...); - Cairo.paint(ctx) - Cairo.restore(ctx) -end - -######################################### -# Backend interface to Makie # -######################################### - -function Makie.backend_display(x::CairoBackend, scene::Scene; kw...) - return open(x.path, "w") do io - Makie.backend_show(x, io, to_mime(x), scene) - end -end - -Makie.backend_showable(x::CairoBackend, ::MIME"image/svg+xml", scene::Scene) = x.typ == SVG -Makie.backend_showable(x::CairoBackend, ::MIME"application/pdf", scene::Scene) = x.typ == PDF -Makie.backend_showable(x::CairoBackend, ::MIME"application/postscript", scene::Scene) = x.typ == EPS -Makie.backend_showable(x::CairoBackend, ::MIME"image/png", scene::Scene) = x.typ == PNG - - -function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"image/svg+xml", scene::Scene) - proxy_io = IOBuffer() - pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit) - antialias = get(io, :antialias, x.antialias) - - screen = CairoScreen(scene, proxy_io, :svg; device_scaling_factor = pt_per_unit, antialias = antialias) - cairo_draw(screen, scene) - Cairo.flush(screen.surface) - Cairo.finish(screen.surface) - svg = String(take!(proxy_io)) - - # for some reason, in the svg, surfaceXXX ids keep counting up, - # even with the very same figure drawn again and again - # so we need to reset them to counting up from 1 - # so that the same figure results in the same svg and in the same salt - surfaceids = sort(unique(collect(m.match for m in eachmatch(r"surface\d+", svg)))) - - for (i, id) in enumerate(surfaceids) - svg = replace(svg, id => "surface$i") - end - - # salt svg ids with the first 8 characters of the base64 encoded - # sha512 hash to avoid collisions across svgs when embedding them on - # websites. the hash and therefore the salt will always be the same for the same file - # so the output is deterministic - salt = String(Base64.base64encode(SHA.sha512(svg)))[1:8] - - ids = sort(unique(collect(m[1] for m in eachmatch(r"id\s*=\s*\"([^\"]*)\"", svg)))) - - for id in ids - svg = replace(svg, id => "$id-$salt") - end - - print(io, svg) - return screen -end - -function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"application/pdf", scene::Scene) - - pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit) - antialias = get(io, :antialias, x.antialias) - - screen = CairoScreen(scene, io, :pdf; device_scaling_factor = pt_per_unit, antialias = antialias) - cairo_draw(screen, scene) - Cairo.finish(screen.surface) - return screen -end - - -function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"application/postscript", scene::Scene) - - pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit) - antialias = get(io, :antialias, x.antialias) - - screen = CairoScreen(scene, io, :eps; device_scaling_factor = pt_per_unit, antialias = antialias) - - cairo_draw(screen, scene) - Cairo.finish(screen.surface) - return screen -end - -function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"image/png", scene::Scene) - - # multiply the resolution of the png with this factor for more or less detail - # while relative line and font sizes are unaffected - px_per_unit = get(io, :px_per_unit, x.px_per_unit) - antialias = get(io, :antialias, x.antialias) - - # create an ARGB surface, to speed up drawing ops. - screen = CairoScreen(scene; device_scaling_factor = px_per_unit, antialias = antialias) - cairo_draw(screen, scene) - Cairo.write_to_png(screen.surface, io) - return screen -end - - -######################################## -# Fast colorbuffer for recording # -######################################## - -function Makie.colorbuffer(screen::CairoScreen) - # extract scene - scene = screen.scene - # get resolution - w, h = size(scene) - # preallocate an image matrix - img = Matrix{ARGB32}(undef, w, h) - # create an image surface to draw onto the image - surf = Cairo.CairoImageSurface(img) - # draw the scene onto the image matrix - ctx = Cairo.CairoContext(surf) - ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, 2.0) - - scr = CairoScreen(scene, surf, ctx, nothing) - - cairo_draw(scr, scene) - - # x and y are flipped - return the transpose - return permutedims(img) -end diff --git a/CairoMakie/src/overrides.jl b/CairoMakie/src/overrides.jl index 493fd9a177d..5dd37ed1829 100644 --- a/CairoMakie/src/overrides.jl +++ b/CairoMakie/src/overrides.jl @@ -7,7 +7,7 @@ Special method for polys so we don't fall back to atomic meshes, which are much more complex and slower to draw than standard paths with single color. """ -function draw_plot(scene::Scene, screen::CairoScreen, poly::Poly) +function draw_plot(scene::Scene, screen::Screen, poly::Poly) # dispatch on input arguments to poly to use smarter drawing methods than # meshes if possible draw_poly(scene, screen, poly, to_value.(poly.input_args)...) @@ -16,7 +16,7 @@ end """ Fallback method for args without special treatment. """ -function draw_poly(scene::Scene, screen::CairoScreen, poly, args...) +function draw_poly(scene::Scene, screen::Screen, poly, args...) draw_poly_as_mesh(scene, screen, poly) end @@ -27,16 +27,16 @@ end # in the rare case of per-vertex colors redirect to mesh drawing -function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Point2}, color::AbstractArray, model, strokecolor, strokewidth) +function draw_poly(scene::Scene, screen::Screen, poly, points::Vector{<:Point2}, color::AbstractArray, model, strokecolor, strokewidth) draw_poly_as_mesh(scene, screen, poly) end -function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Point2}) +function draw_poly(scene::Scene, screen::Screen, poly, points::Vector{<:Point2}) draw_poly(scene, screen, poly, points, poly.color[], poly.model[], poly.strokecolor[], poly.strokewidth[]) end # when color is a Makie.AbstractPattern, we don't need to go to Mesh -function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Point2}, color::Union{Symbol, Colorant, Makie.AbstractPattern}, +function draw_poly(scene::Scene, screen::Screen, poly, points::Vector{<:Point2}, color::Union{Symbol, Colorant, Makie.AbstractPattern}, model, strokecolor, strokewidth) space = to_value(get(poly, :space, :data)) points = project_position.(Ref(scene), space, points, Ref(model)) @@ -59,7 +59,7 @@ function draw_poly(scene::Scene, screen::CairoScreen, poly, points::Vector{<:Poi Cairo.stroke(screen.context) end -function draw_poly(scene::Scene, screen::CairoScreen, poly, points_list::Vector{<:Vector{<:Point2}}) +function draw_poly(scene::Scene, screen::Screen, poly, points_list::Vector{<:Vector{<:Point2}}) broadcast_foreach(points_list, poly.color[], poly.strokecolor[], poly.strokewidth[]) do points, color, strokecolor, strokewidth @@ -67,9 +67,9 @@ function draw_poly(scene::Scene, screen::CairoScreen, poly, points_list::Vector{ end end -draw_poly(scene::Scene, screen::CairoScreen, poly, rect::Rect2) = draw_poly(scene, screen, poly, [rect]) +draw_poly(scene::Scene, screen::Screen, poly, rect::Rect2) = draw_poly(scene, screen, poly, [rect]) -function draw_poly(scene::Scene, screen::CairoScreen, poly, rects::Vector{<:Rect2}) +function draw_poly(scene::Scene, screen::Screen, poly, rects::Vector{<:Rect2}) model = poly.model[] space = to_value(get(poly, :space, :data)) projected_rects = project_rect.(Ref(scene), space, rects, Ref(model)) @@ -124,7 +124,7 @@ function polypath(ctx, polygon) end end -function draw_poly(scene::Scene, screen::CairoScreen, poly, polygons::AbstractArray{<:Polygon}) +function draw_poly(scene::Scene, screen::Screen, poly, polygons::AbstractArray{<:Polygon}) model = poly.model[] space = to_value(get(poly, :space, :data)) projected_polys = project_polygon.(Ref(scene), space, polygons, Ref(model)) @@ -161,7 +161,7 @@ end # gradients as well via `mesh` we have to intercept the poly use # ################################################################################ -function draw_plot(scene::Scene, screen::CairoScreen, +function draw_plot(scene::Scene, screen::Screen, band::Band{<:Tuple{<:AbstractVector{<:Point2},<:AbstractVector{<:Point2}}}) if !(band.color[] isa AbstractArray) @@ -194,7 +194,7 @@ end # we override behavior and draw each band in one go # ################################################################################# -function draw_plot(scene::Scene, screen::CairoScreen, tric::Tricontourf) +function draw_plot(scene::Scene, screen::Screen, tric::Tricontourf) pol = only(tric.plots)::Poly colornumbers = pol.color[] @@ -220,4 +220,4 @@ function draw_plot(scene::Scene, screen::CairoScreen, tric::Tricontourf) draw_tripolys(projected_polys, colornumbers, colors) return -end \ No newline at end of file +end diff --git a/CairoMakie/src/precompiles.jl b/CairoMakie/src/precompiles.jl index 42a65f8cb25..dea7e5c54bd 100644 --- a/CairoMakie/src/precompiles.jl +++ b/CairoMakie/src/precompiles.jl @@ -3,15 +3,13 @@ using SnoopPrecompile macro compile(block) return quote figlike = $(esc(block)) - screen = Makie.backend_display(Makie.get_scene(figlike)) - Makie.colorbuffer(screen) + Makie.colorbuffer(figlike) end end let @precompile_all_calls begin CairoMakie.activate!() - CairoMakie.inline!(false) base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") include(shared_precompile) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 6ca6f022f27..3455acf7f64 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -2,7 +2,7 @@ # Lines, LineSegments # ################################################################################ -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Union{Lines, LineSegments})) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Union{Lines, LineSegments})) fields = @get_attribute(primitive, (color, linewidth, linestyle)) linestyle = Makie.convert_attribute(linestyle, Makie.key"linestyle"()) ctx = screen.context @@ -173,7 +173,7 @@ end # Scatter # ################################################################################ -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Scatter)) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Scatter)) fields = @get_attribute(primitive, (color, markersize, strokecolor, strokewidth, marker, marker_offset, rotations)) @get_attribute(primitive, (transform_marker,)) @@ -388,7 +388,7 @@ function p3_to_p2(p::Point3{T}) where T end end -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Text{<:Tuple{<:Union{AbstractArray{<:Makie.GlyphCollection}, Makie.GlyphCollection}}})) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Text{<:Tuple{<:Union{AbstractArray{<:Makie.GlyphCollection}, Makie.GlyphCollection}}})) ctx = screen.context @get_attribute(primitive, (rotation, model, space, markerspace, offset)) position = primitive.position[] @@ -512,25 +512,6 @@ function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, return end -struct CairoGlyph - index::Culong - x::Cdouble - y::Cdouble -end - -function show_glyph(ctx, glyph, x, y) - cg = Ref(CairoGlyph(glyph, x, y)) - ccall((:cairo_show_glyphs, Cairo.libcairo), - Nothing, (Ptr{Nothing}, Ptr{CairoGlyph}, Cint), - ctx.ptr, cg, 1) -end -function glyph_path(ctx, glyph::Culong, x, y) - cg = Ref(CairoGlyph(glyph, x, y)) - ccall((:cairo_glyph_path, Cairo.libcairo), - Nothing, (Ptr{Nothing}, Ptr{CairoGlyph}, Cint), - ctx.ptr, cg, 1) -end - ################################################################################ # Heatmap, Image # ################################################################################ @@ -565,7 +546,7 @@ premultiplied_rgba(a::AbstractArray{<:Color}) = RGBA.(a) premultiplied_rgba(r::RGBA) = RGBA(r.r * r.alpha, r.g * r.alpha, r.b * r.alpha, r.alpha) premultiplied_rgba(c::Colorant) = premultiplied_rgba(RGBA(c)) -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Union{Heatmap, Image})) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Union{Heatmap, Image})) ctx = screen.context image = primitive[3][] xs, ys = primitive[1][], primitive[2][] @@ -696,7 +677,7 @@ end ################################################################################ -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Makie.Mesh)) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.Mesh)) mesh = primitive[1][] if Makie.cameracontrols(scene) isa Union{Camera2D, Makie.PixelCamera, Makie.EmptyCamera} draw_mesh2D(scene, screen, primitive, mesh) @@ -933,7 +914,7 @@ end ################################################################################ -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Makie.Surface)) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.Surface)) # Pretend the surface plot is a mesh plot and plot that instead mesh = surface2mesh(primitive[1][], primitive[2][], primitive[3][]) old = primitive[:color] @@ -962,7 +943,7 @@ end ################################################################################ -function draw_atomic(scene::Scene, screen::CairoScreen, @nospecialize(primitive::Makie.MeshScatter)) +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.MeshScatter)) @get_attribute(primitive, (color, model, marker, markersize, rotations)) if color isa AbstractArray{<: Number} diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl new file mode 100644 index 00000000000..8487951810c --- /dev/null +++ b/CairoMakie/src/screen.jl @@ -0,0 +1,251 @@ +using Base.Docs: doc + +@enum RenderType SVG IMAGE PDF EPS + +Base.convert(::Type{RenderType}, ::MIME{SYM}) where SYM = mime_to_rendertype(SYM) +function Base.convert(::Type{RenderType}, type::String) + if type == "png" + return IMAGE + elseif type == "svg" + return SVG + elseif type == "pdf" + return PDF + elseif type == "eps" + return EPS + else + error("Unsupported cairo render type: $type") + end +end + +"Convert a rendering type to a MIME type" +function to_mime(type::RenderType) + type == SVG && return MIME("image/svg+xml") + type == PDF && return MIME("application/pdf") + type == EPS && return MIME("application/postscript") + return MIME("image/png") +end + +"convert a mime to a RenderType" +function mime_to_rendertype(mime::Symbol)::RenderType + if mime == Symbol("image/png") + return IMAGE + elseif mime == Symbol("image/svg+xml") + return SVG + elseif mime == Symbol("application/pdf") + return PDF + elseif mime == Symbol("application/postscript") + return EPS + else + error("Unsupported mime: $mime") + end +end + +function surface_from_output_type(mime::MIME{M}, io, w, h) where M + surface_from_output_type(M, io, w, h) +end + +function surface_from_output_type(mime::Symbol, io, w, h) + surface_from_output_type(mime_to_rendertype(mime), io, w, h) +end + +function surface_from_output_type(type::RenderType, io, w, h) + if type === SVG + return Cairo.CairoSVGSurface(io, w, h) + elseif type === PDF + return Cairo.CairoPDFSurface(io, w, h) + elseif type === EPS + return Cairo.CairoEPSSurface(io, w, h) + elseif type === IMAGE + img = fill(ARGB32(0, 0, 0, 0), w, h) + return Cairo.CairoImageSurface(img) + else + error("No available Cairo surface for mode $type") + end +end + +""" +Supported options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]` +""" +function to_cairo_antialias(sym::Symbol) + sym == :best && return Cairo.ANTIALIAS_BEST + sym == :good && return Cairo.ANTIALIAS_GOOD + sym == :subpixel && return Cairo.ANTIALIAS_SUBPIXEL + sym == :none && return Cairo.ANTIALIAS_NONE + error("Wrong antialias setting: $(sym). Allowed: :best, :good, :subpixel, :none") +end +to_cairo_antialias(aa::Int) = aa + +""" +* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). +* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). +* `antialias::Union{Symbol, Int} = :best`: antialias modus Cairo uses to draw. Applicable options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`. +* `visible::Bool`: if true, a browser/image viewer will open to display rendered output. +""" +struct ScreenConfig + px_per_unit::Float64 + pt_per_unit::Float64 + antialias::Symbol + visible::Bool + start_renderloop::Bool # Only used to satisfy the interface for record using `Screen(...; start_renderloop=false)` for GLMakie +end + +function device_scaling_factor(rendertype, sc::ScreenConfig) + isv = is_vector_backend(convert(RenderType, rendertype)) + return isv ? sc.pt_per_unit : sc.px_per_unit +end + +function device_scaling_factor(surface::Cairo.CairoSurface, sc::ScreenConfig) + return is_vector_backend(surface) ? sc.pt_per_unit : sc.px_per_unit +end + +""" + CairoMakie.activate!(; screen_config...) + +Sets CairoMakie as the currently active backend and also allows to quickly set the `screen_config`. +Note, that the `screen_config` can also be set permanently via `Makie.set_theme!(CairoMakie=(screen_config...,))`. + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) +""" +function activate!(; type="png", screen_config...) + Makie.set_screen_config!(CairoMakie, screen_config) + if type == "png" + # So this is a bit counter intuitive, since the display system doesn't let us prefer a mime. + # Instead, any IDE with rich output usually has a priority list of mimes, which it iterates to figure out the best mime. + # So, if we want to prefer the png mime, we disable the mimes that are usually higher up in the stack. + disable_mime!("svg", "pdf") + elseif type == "svg" + # SVG is usually pretty high up the priority, so we can just enable all mimes + # If we implement html display for CairoMakie, we might need to disable that. + disable_mime!() + else + enable_only_mime!(type) + end + + Makie.set_active_backend!(CairoMakie) + return +end + +""" + Screen(; screen_config...) + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) + +# Constructors: + +$(Base.doc(MakieScreen)) +""" +struct Screen{SurfaceRenderType} <: Makie.MakieScreen + scene::Scene + surface::Cairo.CairoSurface + context::Cairo.CairoContext + device_scaling_factor::Float64 + antialias::Int # cairo_antialias_t + visible::Bool +end + +function Base.empty!(screen::Screen) + ctx = screen.context + Cairo.save(ctx) + bg = rgbatuple(screen.scene.backgroundcolor[]) + Cairo.set_source_rgba(ctx, bg...) + Cairo.set_operator(ctx, Cairo.OPERATOR_CLEAR) + Cairo.rectangle(ctx, 0, 0, size(screen)...) + Cairo.paint_with_alpha(ctx, 1.0) + Cairo.restore(ctx) +end + +# function clear(screen::Screen) +# ctx = screen.context +# Cairo.save(ctx) +# Cairo.set_operator(ctx, Cairo.OPERATOR_SOURCE) +# Cairo.set_source_rgba(ctx, rgbatuple(screen.scene.backgroundcolor[])...) +# Cairo.paint_with_alpha(ctx, 0.0) +# Cairo.restore(ctx) +# end + +Base.size(screen::Screen) = round.(Int, (screen.surface.width, screen.surface.height)) +# we render the scene directly, since we have +# no screen dependent state like in e.g. opengl +Base.insert!(screen::Screen, scene::Scene, plot) = nothing +function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) + # Currently, we rerender every time, so nothing needs + # to happen here. However, in the event that changes, + # e.g. if we integrate a Gtk window, we may need to + # do something here. +end + +function Base.show(io::IO, ::MIME"text/plain", screen::Screen{S}) where S + println(io, "CairoMakie.Screen{$S}") +end + +function path_to_type(path) + type = splitext(path)[2][2:end] + return convert(RenderType, type) +end +to_mime(screen::Screen) = to_mime(screen.typ) + + +######################################## +# Constructor # +######################################## + +Screen(scene::Scene; screen_config...) = Screen(scene, nothing, IMAGE; screen_config...) + +function Screen(scene::Scene, io_or_path::Union{Nothing, String, IO}, typ::Union{MIME, Symbol, RenderType}; screen_config...) + config = Makie.merge_screen_config(ScreenConfig, screen_config) + # the surface size is the scene size scaled by the device scaling factor + w, h = round.(Int, size(scene) .* device_scaling_factor(typ, config)) + surface = surface_from_output_type(typ, io_or_path, w, h) + return Screen(scene, surface, config) +end + +function Screen(scene::Scene, ::Makie.ImageStorageFormat; screen_config...) + config = Makie.merge_screen_config(ScreenConfig, screen_config) + w, h = round.(Int, size(scene) .* config.px_per_unit) + # create an image surface to draw onto the image + img = fill(ARGB32(0, 0, 0, 0), w, h) + surface = Cairo.CairoImageSurface(img) + return Screen(scene, surface, config) +end + +function Screen(scene::Scene, surface::Cairo.CairoSurface, config::ScreenConfig) + # the surface size is the scene size scaled by the device scaling factor + dsf = device_scaling_factor(surface, config) + surface_set_device_scale(surface, dsf) + ctx = Cairo.CairoContext(surface) + aa = to_cairo_antialias(config.antialias) + Cairo.set_antialias(ctx, aa) + set_miter_limit(ctx, 2.0) + return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible) +end + +######################################## +# Fast colorbuffer for recording # +######################################## + +function Makie.colorbuffer(screen::Screen) + # extract scene + scene = screen.scene + # get resolution + w, h = size(screen) + # preallocate an image matrix + img = fill(ARGB32(0, 0, 0, 0), w, h) + # create an image surface to draw onto the image + surf = Cairo.CairoImageSurface(img) + s = Screen(scene, surf; device_scaling_factor=screen.device_scaling_factor, antialias=screen.antialias) + return colorbuffer(s) +end + +function Makie.colorbuffer(screen::Screen{IMAGE}) + empty!(screen) + cairo_draw(screen, screen.scene) + return PermutedDimsArray(screen.surface.data, (2, 1)) +end + +is_vector_backend(ctx::Cairo.CairoContext) = is_vector_backend(ctx.surface) +is_vector_backend(surf::Cairo.CairoSurface) = is_vector_backend(get_render_type(surf)) +is_vector_backend(rt::RenderType) = rt in (PDF, EPS, SVG) diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 7e100ed7c11..589a101c618 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -255,3 +255,19 @@ function Cairo.CairoPattern(color::Makie.AbstractPattern) cairopattern = Cairo.CairoPattern(cairoimage) return cairopattern end + +""" +Finds a font that can represent the unicode character! +Returns Makie.defaultfont() if not representable! +""" +function best_font(c::Char, font = Makie.defaultfont()) + if Makie.FreeType.FT_Get_Char_Index(font, c) == 0 + for afont in Makie.alternativefonts() + if Makie.FreeType.FT_Get_Char_Index(afont, c) != 0 + return afont + end + end + return Makie.defaultfont() + end + return font +end diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 75101be4867..29cbc02d29c 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -30,6 +30,29 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) @test showable("image/png", f) # see https://github.com/MakieOrg/Makie.jl/pull/2167 @test !showable("blaaa", f) + + CairoMakie.activate!(type="png") + @test showable("image/png", Scene()) + @test !showable("image/svg+xml", Scene()) + # setting svg should leave png as showable, since it's usually lower in the display stack priority + CairoMakie.activate!(type="svg") + @test showable("image/png", Scene()) + @test showable("image/svg+xml", Scene()) +end + +@testset "VideoStream & screen options" begin + N = 3 + points = Observable(Point2f[]) + f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(resolution=(600, 800),)) + vio = Makie.VideoStream(f; format="mp4", px_per_unit=2.0, backend=CairoMakie) + @test vio.screen isa CairoMakie.Screen{CairoMakie.IMAGE} + @test size(vio.screen) == size(f.scene) .* 2 + @test vio.screen.device_scaling_factor == 2.0 + + Makie.recordframe!(vio) + save("test.mp4", vio) + @test isfile("test.mp4") # Make sure no error etc + rm("test.mp4") end using ReferenceTests diff --git a/CairoMakie/test/saving.jl b/CairoMakie/test/saving.jl deleted file mode 100644 index 1ea09fac6e0..00000000000 --- a/CairoMakie/test/saving.jl +++ /dev/null @@ -1,25 +0,0 @@ -database = MakieGallery.load_database(["short_tests.jl"]); - -filter!(database) do example - !("3d" ∈ example.tags) -end - -format_save_path = joinpath(@__DIR__, "test_formats") -isdir(format_save_path) && rm(format_save_path, recursive = true) -mkpath(format_save_path) -savepath(uid, fmt) = joinpath(format_save_path, "$uid.$fmt") - -@testset "Saving formats" begin - for example in database - scene = MakieGallery.eval_example(example) - for fmt in ("png", "pdf", "svg") - @test try - save(savepath(example.title, fmt), scene) - true - catch e - @warn "Saving $(example.title) in format `$fmt` failed!" exception=(e, Base.catch_backtrace()) - false - end - end - end -end diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index e17ea83e115..7fb14e78a9a 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -7,7 +7,7 @@ layout(location=1) out uvec2 fragment_groupid; // // if transparency == true // layout(location=2) out float coverage; -// // if transparency == false && enable_SSAO[] = true +// // if transparency == false && ssao = true // layout(location=2) out vec3 fragment_position; // layout(location=3) out vec3 fragment_normal_occlusion; @@ -31,7 +31,7 @@ void write2framebuffer(vec4 color, uvec2 id){ // fragment_color.rgb = weight * color.rgb; // fragment_color.a = weight; - // // if transparency == false && enable_SSAO[] = true + // // if transparency == false && ssao = true // fragment_color = color; // fragment_position = o_view_pos; // fragment_normal_occlusion.xyz = o_normal; diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 120e1b40479..67657c15191 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -54,8 +54,6 @@ export gpu_data # gets the data of a gpu array as a Julia Array export RenderObject # An object which holds all GPU handles and datastructes to ready for rendering by calling render(obj) export prerender! # adds a function to a RenderObject, which gets executed befor setting the OpenGL render state export postrender! # adds a function to a RenderObject, which gets executed after setting the OpenGL render states -export std_renderobject # creates a renderobject with standard parameters -export instanced_renderobject # simplification for creating a RenderObject which renders instances export extract_renderable export set_arg! export GLVertexArray # VertexArray wrapper object diff --git a/GLMakie/src/GLAbstraction/GLRenderObject.jl b/GLMakie/src/GLAbstraction/GLRenderObject.jl index 284695c5397..fe644f5c586 100644 --- a/GLMakie/src/GLAbstraction/GLRenderObject.jl +++ b/GLMakie/src/GLAbstraction/GLRenderObject.jl @@ -1,16 +1,7 @@ -function RenderObject( - data::Dict{Symbol}, program, pre, - bbs=Observable(Rect3f(Vec3f(0), Vec3f(1))), - main=nothing - ) - RenderObject(convert(Dict{Symbol,Any}, data), program, pre, bbs, main) -end - function Base.show(io::IO, obj::RenderObject) println(io, "RenderObject with ID: ", obj.id) end - Base.getindex(obj::RenderObject, symbol::Symbol) = obj.uniforms[symbol] Base.setindex!(obj::RenderObject, value, symbol::Symbol) = obj.uniforms[symbol] = value @@ -94,19 +85,5 @@ struct EmptyPrerender end export EmptyPrerender export prerendertype -function instanced_renderobject(data, program, bb=Observable(Rect3f(Vec3f(0), Vec3f(1))), primitive::GLenum=GL_TRIANGLES, main=nothing) - pre = StandardPrerender() - robj = RenderObject(convert(Dict{Symbol,Any}, data), program, pre, nothing, bb, main) - robj.postrenderfunction = StandardPostrenderInstanced(main, robj.vertexarray, primitive) - robj -end - -function std_renderobject(data, program, bb=Observable(Rect3f(Vec3f(0), Vec3f(1))), primitive=GL_TRIANGLES, main=nothing) - pre = StandardPrerender() - robj = RenderObject(convert(Dict{Symbol,Any}, data), program, pre, nothing, bb, main) - robj.postrenderfunction = StandardPostrender(robj.vertexarray, primitive) - robj -end - prerendertype(::Type{RenderObject{Pre}}) where {Pre} = Pre prerendertype(::RenderObject{Pre}) where {Pre} = Pre diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 676f238f793..edbc7efff8c 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -87,14 +87,16 @@ end struct ShaderCache # path --> template keys # cache for template keys per file + context::Any template_cache::Dict{String, Vector{String}} # path --> Dict{template_replacements --> Shader) shader_cache::Dict{String, Dict{Any, Shader}} program_cache::Dict{Any, GLProgram} end -function ShaderCache() +function ShaderCache(context) ShaderCache( + context, Dict{String, Vector{String}}(), Dict{String, Dict{Any, Shader}}(), Dict{Any, GLProgram}() @@ -126,28 +128,29 @@ function compile_shader(source::Vector{UInt8}, typ, name) GLAbstraction.print_with_lines(String(source)) @warn("shader $(name) didn't compile. \n$(GLAbstraction.getinfolog(shaderid))") end - Shader(name, source, typ, shaderid) + return Shader(name, source, typ, shaderid) end function compile_shader(path, source_str::AbstractString) typ = shadertype(splitext(path)[2]) source = Vector{UInt8}(source_str) name = Symbol(path) - compile_shader(source, typ, name) + return compile_shader(source, typ, name) end function get_shader!(cache::ShaderCache, path, template_replacement) # this should always be in here, since we already have the template keys shader_dict = cache.shader_cache[path] - get!(shader_dict, template_replacement) do + return get!(shader_dict, template_replacement) do template_source = read(path, String) source = mustache_replace(template_replacement, template_source) - compile_shader(path, source) + ShaderAbstractions.switch_context!(cache.context) + return compile_shader(path, source) end::Shader end function get_template!(cache::ShaderCache, path, view, attributes) - get!(cache.template_cache, path) do + return get!(cache.template_cache, path) do _, ext = splitext(path) typ = shadertype(ext) @@ -161,7 +164,7 @@ function get_template!(cache::ShaderCache, path, view, attributes) # can't yet be in here, since we didn't even have template keys cache.shader_cache[path] = Dict(template_replacements => s) - template_keys + return template_keys end end @@ -212,6 +215,7 @@ function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data) paths = lazyshader.paths if all(x-> isa(x, Shader), paths) fragdatalocation = get(kw_dict, :fragdatalocation, Tuple{Int, String}[]) + ShaderAbstractions.switch_context!(cache.context) return compile_program([paths...], fragdatalocation) end v = get_view(kw_dict) @@ -227,8 +231,10 @@ function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data) shaders = map(paths) do source_typ source, typ = source_typ src, _ = template2source(source, v, data) + ShaderAbstractions.switch_context!(cache.context) compile_shader(Vector{UInt8}(src), typ, :from_string) end + ShaderAbstractions.switch_context!(cache.context) return compile_program([shaders...], fragdatalocation) end if !all(x -> isa(x, AbstractString), paths) @@ -251,6 +257,7 @@ function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data) tr = Dict(zip(template_keys[i], replacements[i])) shaders[i] = get_shader!(cache, path, tr) end + ShaderAbstractions.switch_context!(cache.context) compile_program(shaders, fragdatalocation) end end diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index b782f27aa7f..51521d9fdf6 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -52,6 +52,10 @@ Base.length(t::TextureBuffer) = length(t.buffer) bind(t::Texture) = glBindTexture(t.texturetype, t.id) bind(t::Texture, id) = glBindTexture(t.texturetype, id) ShaderAbstractions.switch_context!(t::TextureBuffer) = switch_context!(t.texture.context) +function unsafe_free(tb::TextureBuffer) + unsafe_free(tb.texture) + unsafe_free(tb.buffer) +end is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY is_texturebuffer(t::Texture) = t.texturetype == GL_TEXTURE_BUFFER diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index e6d7960ddd1..e888216dfd0 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -272,7 +272,6 @@ function Base.show(io::IO, vao::GLVertexArray) println(io, "\nGLVertexArray $(vao.id) indices: ", vao.indices) end - ################################################################################## const RENDER_OBJECT_ID_COUNTER = Ref(zero(UInt32)) @@ -283,17 +282,18 @@ function pack_bool(id, bool) end mutable struct RenderObject{Pre} - main # main object + context # OpenGL context uniforms::Dict{Symbol,Any} + observables::Vector{Observable} # for clean up vertexarray::GLVertexArray prerenderfunction::Pre postrenderfunction id::UInt32 - boundingbox # TODO, remove, basicaly deprecated function RenderObject{Pre}( - main, uniforms::Dict{Symbol,Any}, vertexarray::GLVertexArray, - prerenderfunctions, postrenderfunctions, - boundingbox + context, + uniforms::Dict{Symbol,Any}, observables::Vector{Observable}, + vertexarray::GLVertexArray, + prerenderfunctions, postrenderfunctions ) where Pre fxaa = to_value(pop!(uniforms, :fxaa, true)) RENDER_OBJECT_ID_COUNTER[] += one(UInt32) @@ -304,24 +304,28 @@ mutable struct RenderObject{Pre} # and since this is a UUID, it shouldn't matter id = pack_bool(RENDER_OBJECT_ID_COUNTER[], fxaa) new( - main, uniforms, vertexarray, + context, + uniforms, observables, vertexarray, prerenderfunctions, postrenderfunctions, - id, boundingbox + id ) end end - function RenderObject( data::Dict{Symbol,Any}, program, pre::Pre, post, - bbs=Observable(Rect3f(Vec3f(0), Vec3f(1))), - main=nothing + context=current_context() ) where Pre + + switch_context!(context) + targets = get(data, :gl_convert_targets, Dict()) delete!(data, :gl_convert_targets) passthrough = Dict{Symbol,Any}() # we also save a few non opengl related values in data + observables = Observable[] for (k, v) in data # convert everything to OpenGL compatible types + v isa Observable && push!(observables, v) # save for clean up if haskey(targets, k) # glconvert is designed to just convert everything to a fitting opengl datatype, but sometimes exceptions are needed # e.g. Texture{T,1} and GLBuffer{T} are both usable as an native conversion canditate for a Julia's Array{T, 1} type. @@ -352,12 +356,12 @@ function RenderObject( p = gl_convert(to_value(program), data) # "compile" lazyshader vertexarray = GLVertexArray(Dict(buffers), p) robj = RenderObject{Pre}( - main, + context, data, + observables, vertexarray, pre, post, - bbs ) # automatically integrate object ID, will be discarded if shader doesn't use it robj[:objectid] = robj.id diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl index 5e3ed8bce83..3ceb3a073a5 100644 --- a/GLMakie/src/GLMakie.jl +++ b/GLMakie/src/GLMakie.jl @@ -14,7 +14,7 @@ using Makie: Scene, Lines, Text, Image, Heatmap, Scatter using Makie: convert_attribute, @extractvalue, LineSegments using Makie: @get_attribute, to_value, to_colormap, extrema_nan using Makie: ClosedInterval, (..) -using Makie: inline!, to_native +using Makie: to_native using Makie: spaces, is_data_space, is_pixel_space, is_relative_space, is_clip_space import Makie: to_font, glyph_uv_width!, el32convert, Shape, CIRCLE, RECTANGLE, ROUNDED_RECTANGLE, DISTANCEFIELD, TRIANGLE import Makie: RelocatableFolders @@ -37,13 +37,9 @@ for name in names(Makie, all=true) end end -export inline! import ShaderAbstractions: Sampler, Buffer export Sampler, Buffer -struct GLBackend <: Makie.AbstractBackend -end - const GL_ASSET_DIR = RelocatableFolders.@path joinpath(@__DIR__, "..", "assets") const SHADER_DIR = RelocatableFolders.@path joinpath(GL_ASSET_DIR, "shader") loadshader(name) = joinpath(SHADER_DIR, name) @@ -51,19 +47,11 @@ loadshader(name) = joinpath(SHADER_DIR, name) # don't put this into try catch, to not mess with normal errors include("gl_backend.jl") -function activate!(use_display=true) - b = GLBackend() - Makie.register_backend!(b) - Makie.set_glyph_resolution!(Makie.High) - Makie.current_backend[] = b - Makie.inline!(!use_display) -end - function __init__() activate!() end -export set_window_config! +Base.@deprecate set_window_config!(; screen_config...) GLMakie.activate!(; screen_config...) include("precompiles.jl") diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index 7b9facd4d3c..5974c503762 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -1,55 +1,17 @@ -function Makie.backend_display(::GLBackend, scene::Scene; start_renderloop=true, visible=true, connect=true) - screen = singleton_screen(size(scene); start_renderloop=start_renderloop, visible=visible) - ShaderAbstractions.switch_context!(screen.glscreen) - display_loading_image(screen) - Makie.backend_display(screen, scene; connect=connect) - return screen -end - -function Makie.backend_display(screen::Screen, scene::Scene; connect=true) - ShaderAbstractions.switch_context!(screen.glscreen) +function Base.display(screen::Screen, scene::Scene; connect=true) empty!(screen) + resize!(screen, size(scene)...) # So, the GLFW window events are not guarantee to fire # when we close a window, so we ensure this here! - window_open = events(scene).window_open on(screen.window_open) do open - window_open[] = open + events(scene).window_open[] = open end connect && connect_screen(scene, screen) + Makie.push_screen!(scene, screen) pollevents(screen) insertplots!(screen, scene) pollevents(screen) return screen end -function Base.display(screen::Screen, fig::Makie.FigureLike) - scene = Makie.get_scene(fig) - Base.resize!(screen, size(scene)...) - Makie.backend_display(screen, scene) - return screen -end - -""" - scene2image(scene::Scene) - -Buffers the `scene` in an image buffer. -""" -function scene2image(scene::Scene) - screen = singleton_screen(size(scene), visible=false, start_renderloop=false) - ShaderAbstractions.switch_context!(screen.glscreen) - empty!(screen) - insertplots!(screen, scene) - return Makie.colorbuffer(screen) -end - -function Makie.backend_show(::GLBackend, io::IO, m::MIME"image/png", scene::Scene) - img = scene2image(scene) - FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) - return -end - -function Makie.backend_show(::GLBackend, io::IO, m::MIME"image/jpeg", scene::Scene) - img = scene2image(scene) - FileIO.save(FileIO.Stream{FileIO.format"JPEG"}(Makie.raw_io(io)), img) - return -end +Makie.backend_showable(::Type{Screen}, ::Union{MIME"image/jpeg", MIME"image/png"}) = true diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index f56204a86be..9d05119f4e3 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -116,7 +116,7 @@ function cached_robj!(robj_func, screen, scene, x::AbstractPlot) robj end -function Base.insert!(screen::GLScreen, scene::Scene, x::Combined) +function Base.insert!(screen::Screen, scene::Scene, x::Combined) ShaderAbstractions.switch_context!(screen.glscreen) # poll inside functions to make wait on compile less prominent pollevents(screen) @@ -192,7 +192,7 @@ function handle_intensities!(attributes) end end -function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::Union{Scatter, MeshScatter})) +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatter, MeshScatter})) return cached_robj!(screen, scene, x) do gl_attributes # signals not supported for shading yet gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) @@ -235,19 +235,19 @@ function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::Union{Scat delete!(gl_attributes, :color_norm) delete!(gl_attributes, :color_map) end - return draw_pixel_scatter(screen.shader_cache, positions, gl_attributes) + return draw_pixel_scatter(screen, positions, gl_attributes) else handle_intensities!(gl_attributes) if x isa MeshScatter - return draw_mesh_particle(screen.shader_cache, (marker, positions), gl_attributes) + return draw_mesh_particle(screen, (marker, positions), gl_attributes) else - return draw_scatter(screen.shader_cache, (marker, positions), gl_attributes) + return draw_scatter(screen, (marker, positions), gl_attributes) end end end end -function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::Lines)) +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) return cached_robj!(screen, scene, x) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) @@ -262,11 +262,11 @@ function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::Lines)) positions = apply_transform(transform_func_obs(x), positions) handle_intensities!(data) connect_camera!(data, scene.camera) - return draw_lines(screen.shader_cache, positions, data) + return draw_lines(screen, positions, data) end end -function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::LineSegments)) +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments)) return cached_robj!(screen, scene, x) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) @@ -288,11 +288,11 @@ function draw_atomic(screen::GLScreen, scene::Scene, @nospecialize(x::LineSegmen end connect_camera!(data, scene.camera) - return draw_linesegments(screen.shader_cache, positions, data) + return draw_linesegments(screen, positions, data) end end -function draw_atomic(screen::GLScreen, scene::Scene, +function draw_atomic(screen::Screen, scene::Scene, x::Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) return cached_robj!(screen, scene, x) do gl_attributes @@ -364,7 +364,7 @@ function draw_atomic(screen::GLScreen, scene::Scene, connect_camera!(gl_attributes, cam, markerspace) # Avoid julia#15276 - _robj = draw_scatter(screen.shader_cache, (DISTANCEFIELD, positions), gl_attributes) + _robj = draw_scatter(screen, (DISTANCEFIELD, positions), gl_attributes) return _robj end @@ -376,7 +376,7 @@ xy_convert(x::AbstractArray{Float32}, n) = copy(x) xy_convert(x::AbstractArray, n) = el32convert(x) xy_convert(x, n) = Float32[LinRange(extrema(x)..., n + 1);] -function draw_atomic(screen::GLScreen, scene::Scene, x::Heatmap) +function draw_atomic(screen::Screen, scene::Scene, x::Heatmap) return cached_robj!(screen, scene, x) do gl_attributes t = Makie.transform_func_obs(scene) mat = x[3] @@ -417,11 +417,11 @@ function draw_atomic(screen::GLScreen, scene::Scene, x::Heatmap) gl_attributes[:stroke_width] = pop!(gl_attributes, :thickness) connect_camera!(gl_attributes, scene.camera) - return draw_heatmap(screen.shader_cache, tex, gl_attributes) + return draw_heatmap(screen, tex, gl_attributes) end end -function draw_atomic(screen::GLScreen, scene::Scene, x::Image) +function draw_atomic(screen::Screen, scene::Scene, x::Image) return cached_robj!(screen, scene, x) do gl_attributes mesh = const_lift(x[1], x[2]) do x, y r = to_range(x, y) @@ -438,7 +438,7 @@ function draw_atomic(screen::GLScreen, scene::Scene, x::Image) gl_attributes[:color] = x[3] gl_attributes[:shading] = false connect_camera!(gl_attributes, scene.camera) - return mesh_inner(screen.shader_cache, mesh, transform_func_obs(x), gl_attributes) + return mesh_inner(screen, mesh, transform_func_obs(x), gl_attributes) end end @@ -449,7 +449,7 @@ function update_positions(mesh::GeometryBasics.Mesh, positions) return GeometryBasics.Mesh(meta(positions; attr...), faces(mesh)) end -function mesh_inner(shader_cache, mesh, transfunc, gl_attributes) +function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes) # signals not supported for shading yet gl_attributes[:shading] = to_value(pop!(gl_attributes, :shading)) color = pop!(gl_attributes, :color) @@ -481,18 +481,18 @@ function mesh_inner(shader_cache, mesh, transfunc, gl_attributes) end return mesh end - return draw_mesh(shader_cache, mesh, gl_attributes) + return draw_mesh(screen, mesh, gl_attributes) end -function draw_atomic(screen::GLScreen, scene::Scene, meshplot::Mesh) +function draw_atomic(screen::Screen, scene::Scene, meshplot::Mesh) return cached_robj!(screen, scene, meshplot) do gl_attributes t = transform_func_obs(meshplot) connect_camera!(gl_attributes, scene.camera) - return mesh_inner(screen.shader_cache, meshplot[1], t, gl_attributes) + return mesh_inner(screen, meshplot[1], t, gl_attributes) end end -function draw_atomic(screen::GLScreen, scene::Scene, x::Surface) +function draw_atomic(screen::Screen, scene::Scene, x::Surface) robj = cached_robj!(screen, scene, x) do gl_attributes color = pop!(gl_attributes, :color) img = nothing @@ -547,20 +547,20 @@ function draw_atomic(screen::GLScreen, scene::Scene, x::Surface) if isnothing(img) gl_attributes[:image] = args[3] end - return draw_surface(screen.shader_cache, args, gl_attributes) + return draw_surface(screen, args, gl_attributes) else gl_attributes[:ranges] = to_range.(to_value.(x[1:2])) z_data = Texture(el32convert(x[3]); minfilter=:linear) if isnothing(img) gl_attributes[:image] = z_data end - return draw_surface(screen.shader_cache, z_data, gl_attributes) + return draw_surface(screen, z_data, gl_attributes) end end return robj end -function draw_atomic(screen::GLScreen, scene::Scene, vol::Volume) +function draw_atomic(screen::Screen, scene::Scene, vol::Volume) robj = cached_robj!(screen, scene, vol) do gl_attributes model = vol[:model] x, y, z = vol[1], vol[2], vol[3] @@ -577,6 +577,6 @@ function draw_atomic(screen::GLScreen, scene::Scene, vol::Volume) return convert(Mat4f, m) * m2 end connect_camera!(gl_attributes, scene.camera) - return draw_volume(screen.shader_cache, vol[4], gl_attributes) + return draw_volume(screen, vol[4], gl_attributes) end end diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 0b3cc5ac0b9..914e577adf9 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -68,12 +68,12 @@ end function Makie.window_area(scene::Scene, screen::Screen) disconnect!(screen, window_area) - + updater = WindowAreaUpdater( to_native(screen), scene.events.window_dpi, scene.events.window_area ) on(updater, screen.render_tick) - + return end @@ -223,18 +223,6 @@ function Makie.mouse_position(scene::Scene, screen::Screen) to_native(screen), scene.events.mouseposition, scene.events.hasfocus ) on(updater, screen.render_tick) - - # function cursorposition(window, w::Cdouble, h::Cdouble) - # @print_error begin - # pos = correct_mouse(window, w, h) - # @timeit "triggerless mouseposition" begin - # e.mouseposition.val = pos - # end - # return - # end - # end - # GLFW.SetCursorPosCallback(window, cursorposition) - return end function Makie.disconnect!(screen::Screen, ::typeof(mouse_position)) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index f74aa49f4da..559b8a4137b 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -54,16 +54,9 @@ function get_texture!(atlas) return tex end -# TODO -# find a better way to handle this -# enable_SSAO and FXAA adjust the rendering pipeline and are currently per screen -const enable_SSAO = Ref(false) -const enable_FXAA = Ref(true) -# This adjusts a factor in the rendering shaders for order independent -# transparency. This should be the same for all of them (within one rendering -# pipeline) otherwise depth "order" will be broken. -const transparency_weight_scale = Ref(1000f0) - +include("glwindow.jl") +include("postprocessing.jl") +include("screen.jl") include("glshaders/visualize_interface.jl") include("glshaders/lines.jl") include("glshaders/image_like.jl") @@ -71,9 +64,6 @@ include("glshaders/mesh.jl") include("glshaders/particles.jl") include("glshaders/surface.jl") -include("glwindow.jl") -include("postprocessing.jl") -include("screen.jl") include("picking.jl") include("rendering.jl") include("events.jl") diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index 117a4a400f5..ec21243a53f 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -31,7 +31,7 @@ end """ A matrix of Intensities will result in a contourf kind of plot """ -function draw_heatmap(shader_cache, main, data::Dict) +function draw_heatmap(screen, main, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) @gen_defaults! data begin @@ -46,11 +46,11 @@ function draw_heatmap(shader_cache, main, data::Dict) stroke_color = RGBA{Float32}(0,0,0,0) transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "heatmap.vert", "heatmap.frag", view = Dict( - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) fxaa = false @@ -58,7 +58,7 @@ function draw_heatmap(shader_cache, main, data::Dict) return assemble_shader(data) end -function draw_volume(shader_cache, main::VolumeTypes, data::Dict) +function draw_volume(screen, main::VolumeTypes, data::Dict) geom = Rect3f(Vec3f(0), Vec3f(1)) to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) @gen_defaults! data begin @@ -76,15 +76,15 @@ function draw_volume(shader_cache, main::VolumeTypes, data::Dict) enable_depth = true transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "util.vert", "volume.vert", "volume.frag", view = Dict( "depth_init" => vol_depth_init(to_value(enable_depth)), "depth_default" => vol_depth_default(to_value(enable_depth)), "depth_main" => vol_depth_main(to_value(enable_depth)), "depth_write" => vol_depth_write(to_value(enable_depth)), - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) prerender = VolumePrerender(data[:transparency], data[:overdraw]) diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 198b6d37ac6..323d21e3d01 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -48,7 +48,7 @@ function ticks(points, resolution) end @nospecialize -function draw_lines(shader_cache, position::Union{VectorTypes{T}, MatTypes{T}}, data::Dict) where T<:Point +function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data::Dict) where T<:Point p_vec = if isa(position, GPUArray) position else @@ -74,11 +74,11 @@ function draw_lines(shader_cache, position::Union{VectorTypes{T}, MatTypes{T}}, end => to_index_buffer transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "util.vert", "lines.vert", "lines.geom", "lines.frag", view = Dict( - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) gl_primitive = GL_LINE_STRIP_ADJACENCY @@ -104,7 +104,7 @@ function draw_lines(shader_cache, position::Union{VectorTypes{T}, MatTypes{T}}, return assemble_shader(data) end -function draw_linesegments(shader_cache, positions::VectorTypes{T}, data::Dict) where T <: Point +function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where T <: Point @gen_defaults! data begin vertex = positions => GLBuffer color = default(RGBA, s, 1) => GLBuffer @@ -118,11 +118,11 @@ function draw_linesegments(shader_cache, positions::VectorTypes{T}, data::Dict) # TODO update boundingbox transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "util.vert", "line_segment.vert", "line_segment.geom", "lines.frag", view = Dict( - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) gl_primitive = GL_LINES diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index a327ce56e06..b64a91a5380 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -31,7 +31,7 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) return result end -function draw_mesh(shader_cache, @nospecialize(mesh), data::Dict) +function draw_mesh(screen, @nospecialize(mesh), data::Dict) to_opengl_mesh!(data, mesh) @gen_defaults! data begin shading = true @@ -47,12 +47,12 @@ function draw_mesh(shader_cache, @nospecialize(mesh), data::Dict) transparency = false interpolate_in_fragment_shader = true shader = GLVisualizeShader( - shader_cache, + screen, "util.vert", "mesh.vert", "mesh.frag", "fragment_output.frag", view = Dict( "light_calc" => light_calc(shading), - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) end diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index f051f650194..0e2ecdf6836 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -41,7 +41,7 @@ end """ This is the main function to assemble particles with a GLNormalMesh as a primitive """ -function draw_mesh_particle(shader_cache, p, data) +function draw_mesh_particle(screen, p, data) rot = get!(data, :rotation, Vec4f(0, 0, 0, 1)) rot = vec2quaternion(rot) delete!(data, :rotation) @@ -70,13 +70,13 @@ function draw_mesh_particle(shader_cache, p, data) shading = true transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "util.vert", "particles.vert", "mesh.frag", "fragment_output.frag", view = Dict( "position_calc" => position_calc(position, nothing, nothing, nothing, TextureBuffer), "light_calc" => light_calc(shading), - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) end @@ -92,7 +92,7 @@ end This is the most primitive particle system, which uses simple points as primitives. This is supposed to be the fastest way of displaying particles! """ -function draw_pixel_scatter(shader_cache, position::VectorTypes, data::Dict) +function draw_pixel_scatter(screen, position::VectorTypes, data::Dict) @gen_defaults! data begin vertex = position => GLBuffer color_map = nothing => Texture @@ -101,11 +101,11 @@ function draw_pixel_scatter(shader_cache, position::VectorTypes, data::Dict) scale = 2f0 transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "dots.vert", "dots.frag", view = Dict( - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) gl_primitive = GL_POINTS @@ -115,18 +115,18 @@ function draw_pixel_scatter(shader_cache, position::VectorTypes, data::Dict) end function draw_scatter( - shader_cache, p::Tuple{TOrSignal{Matrix{C}}, VectorTypes{P}}, data::Dict + screen, p::Tuple{TOrSignal{Matrix{C}}, VectorTypes{P}}, data::Dict ) where {C <: Colorant, P <: Point} data[:image] = p[1] # we don't want this to be overwritten by user @gen_defaults! data begin scale = lift(x-> Vec2f(size(x)), p[1]) offset = Vec2f(0) end - draw_scatter(shader_cache, (RECTANGLE, p[2]), data) + draw_scatter(screen, (RECTANGLE, p[2]), data) end function draw_scatter( - shader_cache, p::Tuple{VectorTypes{Matrix{C}}, VectorTypes{P}}, data::Dict + screen, p::Tuple{VectorTypes{Matrix{C}}, VectorTypes{P}}, data::Dict ) where {C <: Colorant, P <: Point} images = map(el32convert, to_value(p[1])) isempty(images) && error("Can not display empty vector of images as primitive") @@ -155,7 +155,7 @@ function draw_scatter( shape = RECTANGLE quad_offset = Vec2f(0) end - return draw_scatter(shader_cache, (RECTANGLE, p[2]), data) + return draw_scatter(screen, (RECTANGLE, p[2]), data) end function texture_distancefield(shape) @@ -168,7 +168,7 @@ end Main assemble functions for scatter particles. Sprites are anything like distance fields, images and simple geometries """ -function draw_scatter(shader_cache, (marker, position), data) +function draw_scatter(screen, (marker, position), data) rot = get!(data, :rotation, Vec4f(0, 0, 0, 1)) rot = vec2quaternion(rot) delete!(data, :rotation) @@ -202,13 +202,13 @@ function draw_scatter(shader_cache, (marker, position), data) fxaa = false transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "util.vert", "sprites.geom", "sprites.vert", "distance_shape.frag", view = Dict( "position_calc" => position_calc(position, nothing, nothing, nothing, GLBuffer), - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) scale_primitive = true diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index 5e7b1c5cf49..c4d5961fc1a 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -86,28 +86,28 @@ end @nospecialize # surface(::Matrix, ::Matrix, ::Matrix) -function draw_surface(shader_cache, main::Tuple{MatTypes{T}, MatTypes{T}, MatTypes{T}}, data::Dict) where T <: AbstractFloat +function draw_surface(screen, main::Tuple{MatTypes{T}, MatTypes{T}, MatTypes{T}}, data::Dict) where T <: AbstractFloat @gen_defaults! data begin position_x = main[1] => (Texture, "x position, must be a `Matrix{Float}`") position_y = main[2] => (Texture, "y position, must be a `Matrix{Float}`") position_z = main[3] => (Texture, "z position, must be a `Matrix{Float}`") scale = Vec3f(0) => "scale must be 0, for a surfacemesh" end - return draw_surface(shader_cache, position_z, data) + return draw_surface(screen, position_z, data) end # surface(Vector or Range, Vector or Range, ::Matrix) -function draw_surface(shader_cache, main::Tuple{VectorTypes{T}, VectorTypes{T}, MatTypes{T}}, data::Dict) where T <: AbstractFloat +function draw_surface(screen, main::Tuple{VectorTypes{T}, VectorTypes{T}, MatTypes{T}}, data::Dict) where T <: AbstractFloat @gen_defaults! data begin position_x = main[1] => (Texture, "x position, must be a `Vector{Float}`") position_y = main[2] => (Texture, "y position, must be a `Vector{Float}`") position_z = main[3] => (Texture, "z position, must be a `Matrix{Float}`") scale = Vec3f(0) => "scale must be 0, for a surfacemesh" end - return draw_surface(shader_cache, position_z, data) + return draw_surface(screen, position_z, data) end -function draw_surface(shader_cache, main, data::Dict) +function draw_surface(screen, main, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) @gen_defaults! data begin @@ -137,15 +137,15 @@ function draw_surface(shader_cache, main, data::Dict) instances = const_lift(x->(size(x,1)-1) * (size(x,2)-1), main) => "number of planes used to render the surface" transparency = false shader = GLVisualizeShader( - shader_cache, + screen, "fragment_output.frag", "util.vert", "surface.vert", "mesh.frag", view = Dict( "position_calc" => position_calc(position, position_x, position_y, position_z, Texture), "normal_calc" => normal_calc(normal, to_value(invert_normals)), "light_calc" => light_calc(shading), - "buffers" => output_buffers(to_value(transparency)), - "buffer_writes" => output_buffer_writes(to_value(transparency)) + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) ) end diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index 65c48feff49..018c6232882 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -65,11 +65,11 @@ function GLAbstraction.gl_convert_struct(g::Grid{1,T}, uniform_name::Symbol) whe end struct GLVisualizeShader <: AbstractLazyShader - shader_cache::GLAbstraction.ShaderCache + screen::Screen paths::Tuple kw_args::Dict{Symbol,Any} function GLVisualizeShader( - shader_cache::GLAbstraction.ShaderCache, paths::String...; + screen::Screen, paths::String...; view = Dict{String,String}(), kw_args... ) # TODO properly check what extensions are available @@ -80,45 +80,45 @@ struct GLVisualizeShader <: AbstractLazyShader args = Dict{Symbol, Any}(kw_args) args[:view] = view args[:fragdatalocation] = [(0, "fragment_color"), (1, "fragment_groupid")] - new(shader_cache, map(x -> loadshader(x), paths), args) + new(screen, map(x -> loadshader(x), paths), args) end end + function GLAbstraction.gl_convert(shader::GLVisualizeShader, data) - GLAbstraction.gl_convert(shader.shader_cache, shader, data) + GLAbstraction.gl_convert(shader.screen.shader_cache, shader, data) end -function assemble_robj(data, program, bb, primitive, pre_fun, post_fun) +function assemble_shader(data) + shader = data[:shader]::GLVisualizeShader + delete!(data, :shader) + primitive = get(data, :gl_primitive, GL_TRIANGLES) + pre_fun = get(data, :prerender, nothing) + post_fun = get(data, :postrender, nothing) + transp = get(data, :transparency, Observable(false)) overdraw = get(data, :overdraw, Observable(false)) + pre = if !isnothing(pre_fun) _pre_fun = GLAbstraction.StandardPrerender(transp, overdraw) ()->(_pre_fun(); pre_fun()) else GLAbstraction.StandardPrerender(transp, overdraw) end - robj = RenderObject(data, program, pre, nothing, bb, nothing) + + robj = RenderObject(data, shader, pre, shader.screen.glscreen) + post = if haskey(data, :instances) GLAbstraction.StandardPostrenderInstanced(data[:instances], robj.vertexarray, primitive) else GLAbstraction.StandardPostrender(robj.vertexarray, primitive) end + robj.postrenderfunction = if !isnothing(post_fun) () -> (post(); post_fun()) else post end - robj -end - -function assemble_shader(data) - shader = data[:shader] - delete!(data, :shader) - glp = get(data, :gl_primitive, GL_TRIANGLES) - return assemble_robj( - data, shader, Rect3f(), glp, - get(data, :prerender, nothing), - get(data, :postrender, nothing) - ) + return robj end """ @@ -148,17 +148,12 @@ to_index_buffer(x) = error( Please choose from Int, Vector{UnitRange{Int}}, Vector{Int} or a signal of either of them" ) -function visualize(@nospecialize(main), @nospecialize(s), @nospecialize(data)) - data = _default(main, s, copy(data)) - return assemble_shader(data) -end - -function output_buffers(transparency = false) +function output_buffers(screen::Screen, transparency = false) if transparency """ layout(location=2) out float coverage; """ - elseif enable_SSAO[] + elseif screen.config.ssao """ layout(location=2) out vec3 fragment_position; layout(location=3) out vec3 fragment_normal_occlusion; @@ -168,16 +163,16 @@ function output_buffers(transparency = false) end end -function output_buffer_writes(transparency = false) +function output_buffer_writes(screen::Screen, transparency = false) if transparency - scale = transparency_weight_scale[] + scale = screen.config.transparency_weight_scale """ float weight = color.a * max(0.01, $scale * pow((1 - gl_FragCoord.z), 3)); coverage = 1.0 - clamp(color.a, 0.0, 1.0); fragment_color.rgb = weight * color.rgb; fragment_color.a = weight; """ - elseif enable_SSAO[] + elseif screen.config.ssao """ fragment_color = color; fragment_position = o_view_pos; diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index 7c1d88eb3fd..a444e8c7998 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -6,7 +6,7 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - window_size = widths(screen) + window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) @@ -27,7 +27,7 @@ function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) sid = Base.RefValue{SelectionID{UInt32}}() - window_size = widths(screen) + window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) @@ -65,7 +65,7 @@ end # Skips one set of allocations function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = widths(screen) + w, h = size(screen) ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) x0, y0 = max.(1, floor.(Int, xy .- range)) @@ -95,7 +95,7 @@ end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = widths(screen) + w, h = size(screen) if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] end @@ -123,4 +123,4 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) idxs = sortperm(distances) permute!(selected, idxs) return map(id -> (screen.cache2plot[id.id], id.index), selected) -end \ No newline at end of file +end diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 2f9ce6e4ad8..3a27e585a24 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -5,7 +5,7 @@ macro compile(block) let figlike = $(esc(block)) screen = Screen(visible=false) - Makie.backend_display(screen, Makie.get_scene(figlike)) + display(screen, figlike) Makie.colorbuffer(screen) close(screen) end @@ -15,7 +15,6 @@ end let @precompile_all_calls begin GLMakie.activate!() - GLMakie.inline!(false) base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") include(shared_precompile) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index ea23022d61a..8ac30bfec48 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,93 +1,8 @@ -# TODO add render_tick event to scene events -function vsynced_renderloop(screen) - while isopen(screen) && !WINDOW_CONFIG.exit_renderloop[] - pollevents(screen) # GLFW poll - if WINDOW_CONFIG.pause_rendering[] - sleep(0.1) - else - ShaderAbstractions.switch_context!(screen.glscreen) - render_frame(screen) - GLFW.SwapBuffers(to_native(screen)) - yield() - end - end -end - -function fps_renderloop(screen::Screen, framerate=WINDOW_CONFIG.framerate[]) - time_per_frame = 1.0 / framerate - while isopen(screen) && !WINDOW_CONFIG.exit_renderloop[] - t = time_ns() - pollevents(screen) # GLFW poll - if WINDOW_CONFIG.pause_rendering[] - sleep(0.1) - else - ShaderAbstractions.switch_context!(screen.glscreen) - render_frame(screen) - GLFW.SwapBuffers(to_native(screen)) - t_elapsed = (time_ns() - t) / 1e9 - diff = time_per_frame - t_elapsed - if diff > 0.001 # can't sleep less than 0.001 - sleep(diff) - else # if we don't sleep, we still need to yield explicitely to other tasks - yield() - end - end - end -end - -function renderloop(screen; framerate=WINDOW_CONFIG.framerate[]) - isopen(screen) || error("Screen most be open to run renderloop!") - try - if WINDOW_CONFIG.vsync[] - GLFW.SwapInterval(1) - vsynced_renderloop(screen) - else - GLFW.SwapInterval(0) - fps_renderloop(screen, framerate) - end - catch e - showerror(stderr, e, catch_backtrace()) - println(stderr) - rethrow(e) - finally - destroy!(screen) - end -end - -const WINDOW_CONFIG = (renderloop = Ref{Function}(renderloop), - vsync = Ref(false), - framerate = Ref(30.0), - float = Ref(false), - pause_rendering = Ref(false), - focus_on_show = Ref(false), - decorated = Ref(true), - title = Ref("Makie"), - exit_renderloop = Ref(false),) - -""" - set_window_config!(; - renderloop = renderloop, - vsync = false, - framerate = 30.0, - float = false, - pause_rendering = false, - focus_on_show = false, - decorated = true, - title = "Makie" - ) -Updates the screen configuration, will only go into effect after closing the current -window and opening a new one! -""" -function set_window_config!(; kw...) - for (key, value) in kw - getfield(WINDOW_CONFIG, key)[] = value - end -end function setup!(screen) glEnable(GL_SCISSOR_TEST) if isopen(screen) - glScissor(0, 0, widths(screen)...) + glScissor(0, 0, size(screen)...) glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens @@ -118,11 +33,7 @@ Renders a single frame of a `window` """ function render_frame(screen::Screen; resize_buffers=true) nw = to_native(screen) - if !ShaderAbstractions.is_context_active(nw) - @debug("Current context does not match the current screen.") - return - end - + ShaderAbstractions.switch_context!(nw) function sortby(x) robj = x[3] plot = screen.cache2plot[robj.id] @@ -135,7 +46,7 @@ function render_frame(screen::Screen; resize_buffers=true) # NOTE # The transparent color buffer is reused by SSAO and FXAA. Changing the # render order here may introduce artifacts because of that. - + fb = screen.framebuffer if resize_buffers wh = Int.(framebuffer_size(nw)) @@ -219,7 +130,7 @@ function id2scene(screen, id1) return false, nothing end -function GLAbstraction.render(filter_elem_func, screen::GLScreen) +function GLAbstraction.render(filter_elem_func, screen::Screen) # Somehow errors in here get ignored silently!? try for (zindex, screenid, elem) in screen.renderlist diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 65050ef0e8b..65f00200d41 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -3,13 +3,142 @@ const ZIndex = Int # ID, Area, clear, is visible, background color const ScreenArea = Tuple{ScreenID, Scene} -abstract type GLScreen <: AbstractScreen end +function renderloop end -mutable struct Screen <: GLScreen - glscreen::GLFW.Window +""" +## Renderloop + +* `renderloop = GLMakie.renderloop`: sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. + + + !!! warning + The below are not effective if renderloop isn't set to `GLMakie.renderloop`, unless implemented in custom renderloop: + + +* `pause_renderloop = false`: creates a screen with paused renderlooop. Can be started with `GLMakie.start_renderloop!(screen)` or paused again with `GLMakie.pause_renderloop!(screen)`. +* `vsync = false`: enables vsync for the window. +* `framerate = 30.0`: sets the currently rendered frames per second. + +## GLFW window attributes +* `float = false`: Lets the opened window float above anything else. +* `focus_on_show = false`: Focusses the window when newly opened. +* `decorated = true`: shows the window decorations or not. +* `title::String = "Makie"`: Sets the window title. +* `fullscreen = false`: Starts the window in fullscreen. +* `debugging = false`: Starts the GLFW.Window/OpenGL context with debug output. +* `monitor::Union{Nothing, GLFW.Monitor} = nothing`: Sets the monitor on which the Window should be opened. + +## Postprocessor +* `oit = false`: Enles order independent transparency for the window. +* `fxaa = true`: Enables fxaa (anti-aliasing) for the window. +* `ssao = true`: Enables screen space ambient occlusion, which simulates natural shadowing at inner edges and crevices. +* `transparency_weight_scale = 1000f0`: This adjusts a factor in the rendering shaders for order independent transparency. + This should be the same for all of them (within one rendering pipeline) otherwise depth "order" will be broken. +""" +mutable struct ScreenConfig + # Renderloop + renderloop::Function + pause_renderloop::Bool + vsync::Bool + framerate::Float64 + + # GLFW window attributes + float::Bool + focus_on_show::Bool + decorated::Bool + title::String + fullscreen::Bool + debugging::Bool + monitor::Union{Nothing, GLFW.Monitor} + + # Postprocessor + oit::Bool + fxaa::Bool + ssao::Bool + transparency_weight_scale::Float32 + + function ScreenConfig( + # Renderloop + renderloop::Union{Makie.Automatic, Function}, + pause_renderloop::Bool, + vsync::Bool, + framerate::Number, + # GLFW window attributes + float::Bool, + focus_on_show::Bool, + decorated::Bool, + title::AbstractString, + fullscreen::Bool, + debugging::Bool, + monitor::Union{Nothing, GLFW.Monitor}, + # Preproccessor + oit::Bool, + fxaa::Bool, + ssao::Bool, + transparency_weight_scale::Number) + + return new( + # Renderloop + renderloop isa Makie.Automatic ? GLMakie.renderloop : renderloop, + pause_renderloop, + vsync, + framerate, + # GLFW window attributes + float, + focus_on_show, + decorated, + title, + fullscreen, + debugging, + monitor, + # Preproccessor + oit, + fxaa, + ssao, + transparency_weight_scale) + end +end + +""" + GLMakie.activate!(; screen_config...) + +Sets GLMakie as the currently active backend and also allows to quickly set the `screen_config`. +Note, that the `screen_config` can also be set permanently via `Makie.set_theme!(GLMakie=(screen_config...,))`. + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) +""" +function activate!(; screen_config...) + if haskey(screen_config, :pause_rendering) + error("pause_rendering got renamed to pause_renderloop.") + end + Makie.set_screen_config!(GLMakie, screen_config) + Makie.set_active_backend!(GLMakie) + Makie.set_glyph_resolution!(Makie.High) + return +end + + +""" + Screen(; screen_config...) + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) + +# Constructors: + +$(Base.doc(MakieScreen)) +""" +mutable struct Screen{GLWindow} <: MakieScreen + glscreen::GLWindow shader_cache::GLAbstraction.ShaderCache framebuffer::GLFramebuffer + config::ScreenConfig + stop_renderloop::Bool rendertask::RefValue{Task} + screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}} @@ -21,20 +150,26 @@ mutable struct Screen <: GLScreen window_open::Observable{Bool} function Screen( - glscreen::GLFW.Window, + glscreen::GLWindow, shader_cache::GLAbstraction.ShaderCache, framebuffer::GLFramebuffer, + config::ScreenConfig, + stop_renderloop::Bool, rendertask::RefValue{Task}, + screen2scene::Dict{WeakRef, ScreenID}, screens::Vector{ScreenArea}, renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}}, postprocessors::Vector{PostProcessor}, cache::Dict{UInt64, RenderObject}, cache2plot::Dict{UInt32, AbstractPlot}, - ) + ) where {GLWindow} + s = size(framebuffer) - return new( - glscreen, shader_cache, framebuffer, rendertask, screen2scene, + return new{GLWindow}( + glscreen, shader_cache, framebuffer, + config, stop_renderloop, rendertask, + screen2scene, screens, renderlist, postprocessors, cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(nothing), Observable(true) @@ -42,19 +177,121 @@ mutable struct Screen <: GLScreen end end -GeometryBasics.widths(x::Screen) = size(x.framebuffer) -pollevents(::GLScreen) = nothing +function Screen(; + resolution = (10, 10), + visible = true, + start_renderloop = true, + screen_config... + ) + # Screen config is managed by the current active theme, so managed by Makie + config = Makie.merge_screen_config(ScreenConfig, screen_config) + + # Somehow this constant isn't wrapped by glfw + GLFW_FOCUS_ON_SHOW = 0x0002000C + + windowhints = [ + (GLFW.SAMPLES, 0), + (GLFW.DEPTH_BITS, 0), + + # SETTING THE ALPHA BIT IS REALLY IMPORTANT ON OSX, SINCE IT WILL JUST KEEP SHOWING A BLACK SCREEN + # WITHOUT ANY ERROR -.- + (GLFW.ALPHA_BITS, 8), + (GLFW.RED_BITS, 8), + (GLFW.GREEN_BITS, 8), + (GLFW.BLUE_BITS, 8), + + (GLFW.STENCIL_BITS, 0), + (GLFW.AUX_BUFFERS, 0), + (GLFW_FOCUS_ON_SHOW, config.focus_on_show), + (GLFW.DECORATED, config.decorated), + (GLFW.FLOATING, config.float), + # (GLFW.TRANSPARENT_FRAMEBUFFER, true) + ] + + window = try + GLFW.Window( + resolution = resolution, + windowhints = windowhints, + visible = false, + # from config + name = config.title, + focus = config.focus_on_show, + fullscreen = config.fullscreen, + debugging = config.debugging, + monitor = config.monitor + ) + catch e + @warn(""" + GLFW couldn't create an OpenGL window. + This likely means, you don't have an OpenGL capable Graphic Card, + or you don't have an OpenGL 3.3 capable video driver installed. + Have a look at the troubleshooting section in the GLMakie readme: + https://github.com/MakieOrg/Makie.jl/tree/master/GLMakie#troubleshooting-opengl. + """) + rethrow(e) + end + + GLFW.SetWindowIcon(window, Makie.icon()) + + # tell GLAbstraction that we created a new context. + # This is important for resource tracking, and only needed for the first context + ShaderAbstractions.switch_context!(window) + shader_cache = GLAbstraction.ShaderCache(window) + push!(GLFW_WINDOWS, window) + + resize_native!(window, resolution...) + + fb = GLFramebuffer(resolution) + postprocessors = [ + config.ssao ? ssao_postprocessor(fb, shader_cache) : empty_postprocessor(), + config.oit ? OIT_postprocessor(fb, shader_cache) : empty_postprocessor(), + config.fxaa ? fxaa_postprocessor(fb, shader_cache) : empty_postprocessor(), + to_screen_postprocessor(fb, shader_cache) + ] + + screen = Screen( + window, shader_cache, fb, + config, !start_renderloop, + RefValue{Task}(), + Dict{WeakRef, ScreenID}(), + ScreenArea[], + Tuple{ZIndex, ScreenID, RenderObject}[], + postprocessors, + Dict{UInt64, RenderObject}(), + Dict{UInt32, AbstractPlot}(), + ) + + GLFW.SetWindowRefreshCallback(window, window -> refreshwindowcb(window, screen)) + + if start_renderloop + start_renderloop!(screen) + end + + # display window if visible! + if visible + GLFW.ShowWindow(window) + else + GLFW.HideWindow(window) + end + + return screen +end + function pollevents(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) notify(screen.render_tick) GLFW.PollEvents() end + Base.wait(x::Screen) = isassigned(x.rendertask) && wait(x.rendertask[]) Base.wait(scene::Scene) = wait(Makie.getscreen(scene)) + Base.show(io::IO, screen::Screen) = print(io, "GLMakie.Screen(...)") + +Base.isopen(x::Screen) = isopen(x.glscreen) Base.size(x::Screen) = size(x.framebuffer) -function Makie.insertplots!(screen::GLScreen, scene::Scene) +function Makie.insertplots!(screen::Screen, scene::Scene) ShaderAbstractions.switch_context!(screen.glscreen) get!(screen.screen2scene, WeakRef(scene)) do id = length(screen.screens) + 1 @@ -104,10 +341,31 @@ function Base.delete!(screen::Screen, scene::Scene) end end end - return end + +function destroy!(rob::RenderObject) + # These need explicit clean up because (some of) the source observables + # remain when the plot is deleted. + GLAbstraction.switch_context!(rob.context) + for (k, v) in rob.uniforms + if v isa Observable + for input in v.inputs + off(input) + end + elseif v isa GPUArray + GLAbstraction.free(v) + end + end + for obs in rob.observables + for input in obs.inputs + off(input) + end + end + GLAbstraction.free(rob.vertexarray) +end + function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) if !isempty(plot.plots) # this plot consists of children, so we flatten it and delete the children instead @@ -115,21 +373,13 @@ function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) delete!(screen, scene, cplot) end else - renderobject = get(screen.cache, objectid(plot)) do - error("Could not find $(typeof(plot)) in current GLMakie screen!") - end - - # These need explicit clean up because (some of) the source observables - # remain when the plot is deleted. - for k in (:normalmatrix, ) - if haskey(renderobject.uniforms, k) - n = renderobject.uniforms[k] - for input in n.inputs - off(input) - end - end + # I think we can double delete renderobjects, so this may be ok + # TODO, is it? + renderobject = get(screen.cache, objectid(plot), nothing) + if !isnothing(renderobject) + destroy!(renderobject) + filter!(x-> x[3] !== renderobject, screen.renderlist) end - filter!(x-> x[3] !== renderobject, screen.renderlist) end end @@ -144,29 +394,30 @@ function Base.empty!(screen::Screen) end const GLFW_WINDOWS = GLFW.Window[] - const SINGLETON_SCREEN = Screen[] const SINGLETON_SCREEN_NO_RENDERLOOP = Screen[] -function singleton_screen(resolution; visible=Makie.use_display[], start_renderloop=true) +function singleton_screen(resolution; visible=true, start_renderloop=true) screen_ref = if start_renderloop SINGLETON_SCREEN else SINGLETON_SCREEN_NO_RENDERLOOP end - if length(screen_ref) == 1 && isopen(screen_ref[1]) + screen = if length(screen_ref) == 1 && isopen(screen_ref[1]) screen = screen_ref[1] resize!(screen, resolution...) - return screen + screen else if !isempty(screen_ref) closeall(screen_ref) end screen = Screen(; resolution=resolution, visible=visible, start_renderloop=start_renderloop) push!(screen_ref, screen) - return screen + screen end + ShaderAbstractions.switch_context!(screen.glscreen) + return screen end function destroy!(screen::Screen) @@ -211,7 +462,7 @@ function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) wher glPixelStorei(GL_PACK_ALIGNMENT, 1) glGetTexImage(source.texturetype, 0, GL_RGB, GL_UNSIGNED_BYTE, dest) GLAbstraction.bind(source, 0) - nothing + return end """ @@ -258,26 +509,12 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma if format == Makie.GLNative return screen.framecache elseif format == Makie.JuliaNative - @static if VERSION < v"1.6" - bufc = copy(screen.framecache) - ind1, ind2 = axes(bufc) - n = first(ind2) + last(ind2) - for i in ind1 - @simd for j in ind2 - @inbounds bufc[i, n-j] = screen.framecache[i, j] - end - end - screen.framecache = bufc - else - reverse!(screen.framecache, dims = 2) - end - return PermutedDimsArray(screen.framecache, (2,1)) + img = screen.framecache + return PermutedDimsArray(view(img, :, size(img, 2):-1:1), (2, 1)) end end - -Base.isopen(x::Screen) = isopen(x.glscreen) -function Base.push!(screen::GLScreen, scene::Scene, robj) +function Base.push!(screen::Screen, scene::Scene, robj) # filter out gc'ed elements filter!(screen.screen2scene) do (k, v) k.value !== nothing @@ -293,22 +530,6 @@ end Makie.to_native(x::Screen) = x.glscreen -""" -OpenGL shares all data containers between shared contexts, but not vertexarrays -.- -So to share a robjs between a context, we need to rewrap the vertexarray into a new one for that -specific context. -""" -function rewrap(robj::RenderObject{Pre}) where Pre - RenderObject{Pre}( - robj.main, - robj.uniforms, - GLVertexArray(robj.vertexarray), - robj.prerenderfunction, - robj.postrenderfunction, - robj.boundingbox, - ) -end - """ Loads the makie loading icon and embedds it in an image the size of resolution """ @@ -352,109 +573,111 @@ function display_loading_image(screen::Screen) end end +function renderloop_running(screen::Screen) + return !screen.stop_renderloop && isassigned(screen.rendertask) && !istaskdone(screen.rendertask[]) +end -function Screen(; - resolution = (10, 10), visible = true, title = WINDOW_CONFIG.title[], - start_renderloop = true, - kw_args... - ) - # Somehow this constant isn't wrapped by glfw - GLFW_FOCUS_ON_SHOW = 0x0002000C - windowhints = [ - (GLFW.SAMPLES, 0), - (GLFW.DEPTH_BITS, 0), - - # SETTING THE ALPHA BIT IS REALLY IMPORTANT ON OSX, SINCE IT WILL JUST KEEP SHOWING A BLACK SCREEN - # WITHOUT ANY ERROR -.- - (GLFW.ALPHA_BITS, 8), - (GLFW.RED_BITS, 8), - (GLFW.GREEN_BITS, 8), - (GLFW.BLUE_BITS, 8), - - (GLFW.STENCIL_BITS, 0), - (GLFW.AUX_BUFFERS, 0), - (GLFW_FOCUS_ON_SHOW, WINDOW_CONFIG.focus_on_show[]), - (GLFW.DECORATED, WINDOW_CONFIG.decorated[]), - (GLFW.FLOATING, WINDOW_CONFIG.float[]), - # (GLFW.TRANSPARENT_FRAMEBUFFER, true) - ] - - window = try - GLFW.Window( - name = title, resolution = resolution, - windowhints = windowhints, - visible = false, - focus = false, - kw_args... - ) - catch e - @warn(""" - GLFW couldn't create an OpenGL window. - This likely means, you don't have an OpenGL capable Graphic Card, - or you don't have an OpenGL 3.3 capable video driver installed. - Have a look at the troubleshooting section in the GLMakie readme: - https://github.com/MakieOrg/Makie.jl/tree/master/GLMakie#troubleshooting-opengl. - """) - rethrow(e) +function start_renderloop!(screen::Screen) + if renderloop_running(screen) + screen.config.pause_renderloop = false + return + else + screen.stop_renderloop = false + task = @async screen.config.renderloop(screen) + yield() + if istaskstarted(task) + screen.rendertask[] = task + elseif istaskfailed(task) + fetch(task) + else + error("What's up with task $(task)") + end end +end - GLFW.SetWindowIcon(window, Makie.icon()) +function pause_renderloop!(screen::Screen) + screen.config.pause_renderloop = true +end - # tell GLAbstraction that we created a new context. - # This is important for resource tracking, and only needed for the first context - ShaderAbstractions.switch_context!(window) - shader_cache = GLAbstraction.ShaderCache() - push!(GLFW_WINDOWS, window) +function stop_renderloop!(screen::Screen) + screen.stop_renderloop = true + wait(screen.rendertask[]) # Make sure we quit! +end - resize_native!(window, resolution...) +function set_framerate!(screen::Screen, fps=30) + screen.config.framerate = fps +end - fb = GLFramebuffer(resolution) +function refreshwindowcb(window, screen) + screen.render_tick[] = nothing + render_frame(screen) + GLFW.SwapBuffers(window) + return +end - postprocessors = [ - enable_SSAO[] ? ssao_postprocessor(fb, shader_cache) : empty_postprocessor(), - OIT_postprocessor(fb, shader_cache), - enable_FXAA[] ? fxaa_postprocessor(fb, shader_cache) : empty_postprocessor(), - to_screen_postprocessor(fb, shader_cache) - ] +# Open an interactive window +Screen(scene::Scene; screen_config...) = singleton_screen(size(scene); visible=true, start_renderloop=true) - screen = Screen( - window, shader_cache, fb, - RefValue{Task}(), - Dict{WeakRef, ScreenID}(), - ScreenArea[], - Tuple{ZIndex, ScreenID, RenderObject}[], - postprocessors, - Dict{UInt64, RenderObject}(), - Dict{UInt32, AbstractPlot}(), - ) +# Screen to save a png/jpeg to file or io +function Screen(scene::Scene, io_or_path::Union{Nothing, String, IO}, typ::MIME; screen_config...) + return singleton_screen(size(scene); visible=false, start_renderloop=false) +end - GLFW.SetWindowRefreshCallback(window, window -> refreshwindowcb(window, screen)) - if start_renderloop - screen.rendertask[] = @async((WINDOW_CONFIG.renderloop[])(screen)) +# Screen that is efficient for `colorbuffer(screen)` +function Screen(scene::Scene, ::Makie.ImageStorageFormat; screen_config...) + return singleton_screen(size(scene); visible=false, start_renderloop=false) +end + +# TODO add render_tick event to scene events +function vsynced_renderloop(screen) + while isopen(screen) && !screen.stop_renderloop + if screen.config.pause_renderloop + pollevents(screen); sleep(0.1) + continue + end + pollevents(screen) # GLFW poll + render_frame(screen) + GLFW.SwapBuffers(to_native(screen)) + yield() end - # display window if visible! - if visible - GLFW.ShowWindow(window) - else - GLFW.HideWindow(window) +end + +function fps_renderloop(screen::Screen) + while isopen(screen) && !screen.stop_renderloop + if screen.config.pause_renderloop + pollevents(screen); sleep(0.1) + continue + end + time_per_frame = 1.0 / screen.config.framerate + t = time_ns() + pollevents(screen) # GLFW poll + render_frame(screen) + GLFW.SwapBuffers(to_native(screen)) + t_elapsed = (time_ns() - t) / 1e9 + diff = time_per_frame - t_elapsed + if diff > 0.001 # can't sleep less than 0.001 + sleep(diff) + else # if we don't sleep, we still need to yield explicitely to other tasks + yield() + end end - return screen end -function Screen(f, resolution) - screen = Screen(resolution = resolution, visible = false, start_renderloop=false) +function renderloop(screen) + isopen(screen) || error("Screen most be open to run renderloop!") try - return f(screen) + if screen.config.vsync + GLFW.SwapInterval(1) + vsynced_renderloop(screen) + else + GLFW.SwapInterval(0) + fps_renderloop(screen) + end + catch e + showerror(stderr, e, catch_backtrace()) + println(stderr) + rethrow(e) finally destroy!(screen) end end - - -function refreshwindowcb(window, screen) - ShaderAbstractions.switch_context!(screen.glscreen) - screen.render_tick[] = nothing - render_frame(screen) - GLFW.SwapBuffers(window) - return -end diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 3dbe135df5d..85406e4217a 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -71,7 +71,6 @@ end end @reference_test "Explicit frame rendering" begin - set_window_config!(renderloop=(screen) -> nothing) function update_loop(m, buff, screen) for i = 1:20 GLFW.PollEvents() @@ -83,17 +82,18 @@ end end end fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0) - screen = Makie.backend_display(GLMakie.GLBackend(), fig.scene) + screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false), fig.scene) buff = RNG.rand(Point3f, 10^4) .* 20f0; update_loop(meshplot, buff, screen) - set_window_config!(renderloop=GLMakie.renderloop) + @test !isassigned(screen.rendertask) + GLMakie.destroy!(screen) fig end @reference_test "Contour and isosurface with correct depth" begin # Make sure shaders can recompile GLMakie.closeall() - + fig = Figure() left = LScene(fig[1, 1]) contour!(left, [sin(i+j) * sin(j+k) * sin(i+k) for i in 1:10, j in 1:10, k in 1:10], enable_depth = true) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 5419944905c..4e4a7b89bff 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -10,11 +10,7 @@ reference_tests_dir = normpath(joinpath(dirname(pathof(Makie)), "..", "Reference Pkg.develop(PackageSpec(path = reference_tests_dir)) using ReferenceTests -GLMakie.activate!() -GLMakie.set_window_config!(; - framerate = 1.0, - pause_rendering = true -) +GLMakie.activate!(framerate=1.0) @testset "mimes" begin f, ax, pl = scatter(1:4) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 27b277414bf..4914f0065c2 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -1,4 +1,4 @@ -using GLMakie.Makie: backend_display, getscreen +using GLMakie.Makie: getscreen function project_sp(scene, point) point_px = Makie.project(scene, point) @@ -6,6 +6,10 @@ function project_sp(scene, point) return point_px .+ offset end +GLMakie.closeall(GLMakie.GLFW_WINDOWS) +GLMakie.closeall(GLMakie.SINGLETON_SCREEN) +GLMakie.closeall(GLMakie.SINGLETON_SCREEN_NO_RENDERLOOP) + @testset "unit tests" begin @testset "Window handling" begin # Without previous windows/figures everything should be empty/unassigned @@ -14,14 +18,13 @@ end @test !isassigned(GLMakie.SINGLETON_SCREEN_NO_RENDERLOOP) # A raw screen should be tracked in GLFW_WINDOWS - Makie.inline!(false) screen = GLMakie.Screen(resolution = (100, 100), visible = false) @test isopen(screen) @test length(GLMakie.GLFW_WINDOWS) == 1 && (GLMakie.GLFW_WINDOWS[1] === screen.glscreen) @test !isassigned(GLMakie.SINGLETON_SCREEN) @test !isassigned(GLMakie.SINGLETON_SCREEN_NO_RENDERLOOP) - # A displayed figure should create a singleton screen and leave other + # A displayed figure should create a singleton screen and leave other # screens untouched fig, ax, splot = scatter(1:4); screen2 = display(fig) @@ -61,6 +64,8 @@ end @test isopen(screen2) && (screen2 === GLMakie.SINGLETON_SCREEN[]) @test screen === screen2 @test screen2.glscreen.handle == ptr + close(screen) + close(screen2) end @testset "Pick a plot element or plot elements inside a rectangle" begin diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl index bcc5a1778ce..3a633fcb09d 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -9,7 +9,36 @@ abstract type Transformable end abstract type AbstractPlot{Typ} <: Transformable end abstract type AbstractScene <: Transformable end abstract type ScenePlot{Typ} <: AbstractPlot{Typ} end -abstract type AbstractScreen <: AbstractDisplay end + +""" +Screen constructors implemented by all backends: + +```julia +# Constructor aimed at showing the plot in a window. +Screen(scene::Scene; screen_config...) + +# Screen to save a png/jpeg to file or io +Screen(scene::Scene, io::IO, mime; screen_config...) + +# Screen that is efficient for `colorbuffer(screen, format)` +Screen(scene::Scene, format::Makie.ImageStorageFormat; screen_config...) +``` + +Interface implemented by all backends: + +```julia +# Needs to be overload: +size(screen) # Size in pixel +empty!(screen) # empties screen state to reuse the screen, or to close it + +# Optional +wait(screen) # waits as long window is open + +# Provided by Makie: +push_screen!(scene, screen) +``` +""" +abstract type MakieScreen <: AbstractDisplay end const SceneLike = Union{AbstractScene, ScenePlot} diff --git a/NEWS.md b/NEWS.md index 9d83bea1865..7833c046be7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,6 +18,8 @@ - **Breaking** Refactor `DataInspector` to use `tooltip`. This results in changes in the attributes of DataInspector. See pr [#2095](https://github.com/JuliaPlots/Makie.jl/pull/2095) - Add `inspector_label`, `inspector_hover` and `inspector_clear` as optional attributes. [#2095](https://github.com/JuliaPlots/Makie.jl/pull/2095) - Allow text to be copy/pasted into textbox [#2281](https://github.com/MakieOrg/Makie.jl/pull/2281) +- refactor `display`, `record`, `colorbuffer` and `screens` to be faster and more consistent [#2306](https://github.com/MakieOrg/Makie.jl/pull/2306#issuecomment-1275918061). + ## v0.17.13 diff --git a/RPRMakie/examples/materials.jl b/RPRMakie/examples/materials.jl index 68160f9b9cf..65e4a92283b 100644 --- a/RPRMakie/examples/materials.jl +++ b/RPRMakie/examples/materials.jl @@ -8,7 +8,7 @@ image = begin PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] fig = Figure(; resolution=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) - screen = RPRScreen(ax.scene; plugin=RPR.Northstar, iterations=400) + screen = Screen(ax.scene; plugin=RPR.Northstar, iterations=400) matsys = screen.matsys emissive = RPR.EmissiveMaterial(matsys) diff --git a/RPRMakie/examples/opengl_interop.jl b/RPRMakie/examples/opengl_interop.jl index 2e467b7ca25..40a08cb4d7a 100644 --- a/RPRMakie/examples/opengl_interop.jl +++ b/RPRMakie/examples/opengl_interop.jl @@ -13,7 +13,7 @@ lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), fig = Figure(; resolution=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) -screen = RPRMakie.RPRScreen(size(ax.scene); plugin=RPR.Tahoe) +screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Tahoe) material = RPR.UberMaterial(screen.matsys) surface!(ax, f.(u, v'), g.(u, v'), h.(u, v'); ambient=Vec3f(0.5), diffuse=Vec3f(1), specular=0.5, diff --git a/RPRMakie/examples/sea_cables.jl b/RPRMakie/examples/sea_cables.jl index c27f8ff95d7..4c9a6b14a0f 100644 --- a/RPRMakie/examples/sea_cables.jl +++ b/RPRMakie/examples/sea_cables.jl @@ -47,7 +47,6 @@ for i in 1:length(toLines) end earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/commons/5/56/Blue_Marble_Next_Generation_%2B_topography_%2B_bathymetry.jpg")) -Makie.inline!(true) # the actual plot ! RPRMakie.activate!(; iterations=100) scene = with_theme(theme_dark()) do diff --git a/RPRMakie/src/RPRMakie.jl b/RPRMakie/src/RPRMakie.jl index 8e983f15a24..b3ddead9794 100644 --- a/RPRMakie/src/RPRMakie.jl +++ b/RPRMakie/src/RPRMakie.jl @@ -5,13 +5,34 @@ using RadeonProRender using GeometryBasics using Colors using FileIO +using Makie: colorbuffer const RPR = RadeonProRender -using Makie: colorbuffer -const NUM_ITERATIONS = Ref(200) -const RENDER_RESOURCE = Ref(RPR.RPR_CREATION_FLAGS_ENABLE_GPU0) -const RENDER_PLUGIN = Ref(RPR.Tahoe) +""" +* `iterations = 200`: Iterations of light simulations. The more iterations, the less noisy the picture becomes, but higher numbers take much longer. For e.g. `iterations=10` one should expect rendering to take a couple of seconds but it will produce pretty noisy output. 200 is a good middle ground taking around ~30s on an old nvidia 1060. For highest quality output, numbers above 500 are required. +* `resource = RPR.RPR_CREATION_FLAGS_ENABLE_GPU0`: GPU or CPU to use. Multiple GPUs and CPUs can be used together by using `&` (e.g. `RPR.RPR_CREATION_FLAGS_ENABLE_GPU0 & RPR.RPR_CREATION_FLAGS_ENABLE_CPU`). +* `plugin = RPR.Tahoe`: + * `RPR.Tahoe`, the legacy RadeonProRender backend. It's the most stable, but doesn't have all new features (e.g. the `RPR.MatX` material), and may be slower than others + * `RPR.Northstar`, the new rewritten backend, faster and optimized for many iterations. Single iterations are much slower, so less usable for interactive display. Sometimes, Northstar just produces black, jiggly objects. It's not clear yet, if that's just a bug, or the result of using an unsupported/deprecated feature. Switch to `Tahoe` if that happens. + * `RPR.Hybrid`: Vulkan backend, fit for real time rendering, using AMDs and NVIDIAs new hardware accelerated ray tracing. Doesn't work reliably yet and only works with `RPR.Uber` material. + * `RPR.HybridPro`: The same as Hybrid, but works only for Radeon GPUs, using AMDs own hardware acceleration API. +""" +struct ScreenConfig + iterations::Int + max_recursion::Int + resource::RPR.rpr_creation_flags_t + plugin::RPR.PluginType +end + +function ScreenConfig(iterations::Int, max_recursion::Int, render_resource, render_plugin) + return ScreenConfig( + iterations, + max_recursion, + render_resource isa Makie.Automatic ? RPR.RPR_CREATION_FLAGS_ENABLE_GPU0 : render_resource, + render_plugin isa Makie.Automatic ? RPR.Tahoe : render_plugin + ) +end include("scene.jl") include("lines.jl") @@ -19,28 +40,19 @@ include("meshes.jl") include("volume.jl") """ - RPRMakie.activate!(; - iterations=200, - resource=RPR.RPR_CREATION_FLAGS_ENABLE_GPU0, - plugin=RPR.Tahoe) - -- iterations: Iterations of light simulations. The more iterations, the less noisy the picture becomes, but higher numbers take much longer. For e.g. `iterations=10` one should expect rendering to take a couple of seconds but it will produce pretty noisy output. 200 is a good middle ground taking around ~30s on an old nvidia 1060. For highest quality output, numbers above 500 are required. -- resource: GPU or CPU to use. Multiple GPUs and CPUs can be used together by using `&` (e.g. `RPR.RPR_CREATION_FLAGS_ENABLE_GPU0 & RPR.RPR_CREATION_FLAGS_ENABLE_CPU`) -- plugin: - * `RPR.Tahoe`, the legacy RadeonProRender backend. It's the most stable, but doesn't have all new features (e.g. the `RPR.MatX` material), and may be slower than others - * `RPR.Northstar`, the new rewritten backend, faster and optimized for many iterations. Single iterations are much slower, so less usable for interactive display. Sometimes, Northstar just produces black, jiggly objects. It's not clear yet, if that's just a bug, or the result of using an unsupported/deprecated feature. Switch to `Tahoe` if that happens. - * `RPR.Hybrid`: Vulkan backend, fit for real time rendering, using AMDs and NVIDIAs new hardware accelerated ray tracing. Doesn't work reliably yet and only works with `RPR.Uber` material. - * `RPR.HybridPro`: The same as Hybrid, but works only for Radeon GPUs, using AMDs own hardware acceleration API. + RPRMakie.activate!(; screen_config...) + + +Sets RPRMakie as the currently active backend and also allows to quickly set the `screen_config`. +Note, that the `screen_config` can also be set permanently via `Makie.set_theme!(RPRMakie=(screen_config...,))`. + +# Arguments one can pass via `screen_config`: +$(Base.doc(ScreenConfig)) """ -function activate!(; iterations=200, resource=RENDER_RESOURCE[], plugin=RENDER_PLUGIN[]) - NUM_ITERATIONS[] = iterations - RENDER_RESOURCE[] = resource - RENDER_PLUGIN[] = plugin - b = RPRBackend() - Makie.register_backend!(b) - Makie.current_backend[] = b - Makie.inline!(true) +function activate!(; screen_config...) + Makie.set_screen_config!(RPRMakie, screen_config) + Makie.set_active_backend!(RPRMakie) return end @@ -56,6 +68,6 @@ for name in names(Makie; all=true) end end -export RPRScreen, RPR, colorbuffer +export RPR, colorbuffer end diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index fbd52b1cf8a..81d02e0065b 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -99,7 +99,7 @@ function to_rpr_scene(context::RPR.Context, matsys, mscene::Makie.Scene) return scene end -function replace_scene_rpr!(scene::Makie.Scene, screen=RPRScreen(scene); refresh=Observable(nothing)) +function replace_scene_rpr!(scene::Makie.Scene, screen=Screen(scene); refresh=Observable(nothing)) context = screen.context matsys = screen.matsys screen.scene = scene @@ -140,9 +140,19 @@ function replace_scene_rpr!(scene::Makie.Scene, screen=RPRScreen(scene); refresh return context, task, rpr_scene end -struct RPRBackend <: Makie.AbstractBackend end -mutable struct RPRScreen <: Makie.AbstractScreen +""" + Screen(args...; screen_config...) + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) + +# Constructors: + +$(Base.doc(MakieScreen)) +""" +mutable struct Screen <: Makie.MakieScreen context::RPR.Context matsys::RPR.MaterialSystem framebuffer1::RPR.FrameBuffer @@ -155,26 +165,36 @@ mutable struct RPRScreen <: Makie.AbstractScreen cleared::Bool end -function RPRScreen(scene::Scene; kw...) +Base.size(screen::Screen) = screen.fb_size + +function Base.show(io::IO, ::MIME"image/png", screen::Screen) + img = colorbuffer(screen) + FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) +end + +function Screen(scene::Scene; screen_config...) fb_size = size(scene) - screen = RPRScreen(fb_size; kw...) + screen = Screen(fb_size; screen_config...) screen.scene = scene return screen end -function RPRScreen(fb_size::NTuple{2,<:Integer}; - iterations=NUM_ITERATIONS[], - resource=RENDER_RESOURCE[], - plugin=RENDER_PLUGIN[], max_recursion=10) - context = RPR.Context(; resource=resource, plugin=plugin) +Screen(scene::Scene, ::IO, ::MIME; screen_config...) = Screen(scene; screen_config...) +Screen(scene::Scene, ::Makie.ImageStorageFormat; screen_config...) = Screen(scene; screen_config...) + +function Screen(fb_size::NTuple{2,<:Integer}; screen_config...) + config = Makie.merge_screen_config(ScreenConfig, screen_config) + context = RPR.Context(; resource=config.resource, plugin=config.plugin) matsys = RPR.MaterialSystem(context, 0) set_standard_tonemapping!(context) - set!(context, RPR.RPR_CONTEXT_MAX_RECURSION, UInt(max_recursion)) + set!(context, RPR.RPR_CONTEXT_MAX_RECURSION, UInt(config.max_recursion)) framebuffer1 = RPR.FrameBuffer(context, RGBA, fb_size) framebuffer2 = RPR.FrameBuffer(context, RGBA, fb_size) set!(context, RPR.RPR_AOV_COLOR, framebuffer1) - return RPRScreen(context, matsys, framebuffer1, framebuffer2, fb_size, nothing, false, nothing, - iterations, false) + return Screen( + context, matsys, framebuffer1, framebuffer2, fb_size, + nothing, false, nothing, + config.iterations, false) end function render(screen; clear=true, iterations=screen.iterations) @@ -201,9 +221,9 @@ function render(screen; clear=true, iterations=screen.iterations) return framebuffer2 end -function Makie.colorbuffer(screen::RPRScreen) +function Makie.colorbuffer(screen::Screen) if !screen.setup_scene - Makie.backend_display(screen, screen.scene) + display(screen, screen.scene) end data_1d = RPR.get_data(render(screen)) r = reverse(reshape(data_1d, screen.fb_size), dims=2) @@ -213,13 +233,7 @@ function Makie.colorbuffer(screen::RPRScreen) end end -function Makie.backend_display(::RPRBackend, scene::Scene; kw...) - screen = RPRScreen(scene) - Makie.backend_display(screen, scene) - return screen -end - -function Makie.backend_display(screen::RPRScreen, scene::Scene) +function Base.display(screen::Screen, scene::Scene; display_kw...) screen.scene = scene rpr_scene = to_rpr_scene(screen.context, screen.matsys, scene) screen.rpr_scene = rpr_scene @@ -232,7 +246,7 @@ function Makie.backend_display(screen::RPRScreen, scene::Scene) return screen end -function Base.insert!(screen::RPRScreen, scene::Scene, plot::AbstractPlot) +function Base.insert!(screen::Screen, scene::Scene, plot::AbstractPlot) context = screen.context matsys = screen.matsys rpr_scene = screen.rpr_scene @@ -240,15 +254,4 @@ function Base.insert!(screen::RPRScreen, scene::Scene, plot::AbstractPlot) return screen end -function Makie.backend_show(b::RPRBackend, io::IO, ::MIME"image/png", scene::Scene) - screen = Makie.backend_display(b, scene) - img = Makie.colorbuffer(screen) - FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) - return screen -end - -function Base.show(io::IO, ::MIME"image/png", screen::RPRScreen) - img = Makie.colorbuffer(screen) - FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) - return screen -end +Makie.backend_showable(::Type{Screen}, ::Union{MIME"image/jpeg", MIME"image/png"}) = true diff --git a/RPRMakie/test/lines.jl b/RPRMakie/test/lines.jl index 81cca9617e7..e36c4698486 100644 --- a/RPRMakie/test/lines.jl +++ b/RPRMakie/test/lines.jl @@ -2,7 +2,7 @@ using GLMakie, GeometryBasics, RPRMakie, RadeonProRender using Colors, FileIO using Colors: N0f8 RPR = RadeonProRender -GLMakie.set_window_config!(float=true, focus_on_show=false) +GLMakie.activate!(float=true, focus_on_show=false) cat = load(GLMakie.assetpath("cat.obj")) begin diff --git a/RPRMakie/test/runtests.jl b/RPRMakie/test/runtests.jl index 8be168cd36d..15f846d28ed 100644 --- a/RPRMakie/test/runtests.jl +++ b/RPRMakie/test/runtests.jl @@ -5,5 +5,5 @@ RPRMakie.activate!(resource=RPR.RPR_CREATION_FLAGS_ENABLE_CPU, iterations=50) f, ax, pl = meshscatter(rand(Point3f, 100), color=:blue) out = joinpath(@__DIR__, "recorded") isdir(out) && rm(out) -mkdir("recorded") +mkdir(out) save(joinpath(out, "test.png"), ax.scene); diff --git a/ReferenceTests/src/runtests.jl b/ReferenceTests/src/runtests.jl index 129d7a9a2e8..13526823361 100644 --- a/ReferenceTests/src/runtests.jl +++ b/ReferenceTests/src/runtests.jl @@ -1,8 +1,3 @@ -function extract_frames(video, frame_folder) - path = joinpath(frame_folder, "frames%04d.png") - FFMPEG.ffmpeg_exe(`-loglevel quiet -i $video -y $path`) -end - function get_frames(a, b) return (get_frames(a), get_frames(b)) end @@ -11,7 +6,7 @@ function get_frames(video) mktempdir() do folder afolder = joinpath(folder, "a") mkpath(afolder) - extract_frames(video, afolder) + Makie.extract_frames(video, afolder) aframes = joinpath.(afolder, readdir(afolder)) if length(aframes) > 10 # we don't want to compare too many frames since it's time costly @@ -42,6 +37,13 @@ function compare_media(a, b; sigma=[1,1]) return compare_media(conv(imga), conv(imgb), sigma=sigma) elseif ext in (".mp4", ".gif") aframes, bframes = get_frames(a, b) + # Frames can differ in length, which usually shouldn't be the case but can happen + # when the implementation of record changes, or when the example changes its number of frames + # In that case, we just return inf + warn + if length(aframes) != length(bframes) + @warn "not the same number of frames in video, difference will be Inf" + return Inf + end return mean(compare_media.(aframes, bframes; sigma=sigma)) else error("Unknown media extension: $ext") diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 0c9435570fb..25540ff5527 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -630,7 +630,7 @@ end @reference_test "trimspine" begin with_theme(Axis = (limits = (0.5, 5.5, 0.3, 3.4), spinewidth = 8, topspinevisible = false, rightspinevisible = false)) do f = Figure(resolution = (800, 800)) - + for (i, ts) in enumerate([(true, true), (true, false), (false, true), (false, false)]) Label(f[0, i], string(ts), tellwidth = false) Axis(f[1, i], xtrimspine = ts) @@ -638,11 +638,11 @@ end Axis(f[3, i], xtrimspine = ts, xreversed = true) Axis(f[4, i], ytrimspine = ts, yreversed = true) end - + for (i, l) in enumerate(["x", "y", "x reversed", "y reversed"]) Label(f[i, 5], l, tellheight = false) end - + f end end @@ -702,14 +702,14 @@ end x = RNG.rand(300) y = RNG.rand(300) - + for (i, cellsize) in enumerate([0.1, 0.15, 0.2, 0.25]) ax = Axis(f[fldmod1(i, 2)...], title = "cellsize = $cellsize", aspect = DataAspect()) hexbin!(ax, x, y, cellsize = cellsize) wireframe!(ax, Rect2f(Point2f.(x, y)), color = :red) scatter!(ax, x, y, color = :red, markersize = 5) end - + f end diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index 17cfca3cf54..74faac9c3a3 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -8,6 +8,8 @@ using ReferenceTests.FileIO using ReferenceTests.Colors using ReferenceTests.LaTeXStrings using ReferenceTests.DelimitedFiles +using ReferenceTests.Test +using ReferenceTests.Colors: RGB, N0f8 using Makie: Record, volume @testset "primitives" begin diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index b6958ff17fc..485cd35ee85 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -1,4 +1,3 @@ -using ReferenceTests @reference_test "updating 2d primitives" begin fig = Figure() t = Observable(1) @@ -40,3 +39,65 @@ end notify(points) Makie.step!(st) end + +function generate_plot(N = 3) + points = Observable(Point2f[]) + color = Observable(RGBAf[]) + fig, ax, pl = scatter(points, color=color, markersize=1.0, marker=Circle, markerspace=:data, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(resolution=(800, 800),)) + function update_func(ij) + push!(points.val, Point2f(Tuple(ij))) + push!(color.val, RGBAf((Tuple(ij)./N)..., 0, 1)) + notify(color) + notify(points) + end + return fig, CartesianIndices((N, N)), update_func +end + +@reference_test "record test" begin + fig, iter, func = generate_plot(3) + Record(func, fig, iter) +end + +function load_frames(video, dir) + framedir = joinpath(dir, "frames") + isdir(framedir) && rm(framedir; recursive=true, force=true) + mkdir(framedir) + Makie.extract_frames(video, framedir) + return map(readdir(framedir; join=true)) do path + return convert(Matrix{RGB{N0f8}}, load(path)) + end +end + +function compare_videos(reference, vpath, dir) + to_compare = load_frames(vpath, dir) + n = length(to_compare) + @test n == length(reference) + + @test all(1:n) do i + v = ReferenceTests.compare_media(reference[i], to_compare[i]) + return v < 0.02 + end +end + +# To not spam our reference image comparison with lots of frames, we do a manual frame comparison between the formats and only add the mkv reference to the reference image tests +@reference_test "record test formats" begin + mktempdir() do dir + fig, iter, func = generate_plot(2) + record(func, fig, joinpath(dir, "reference.mkv"), iter) + reference = load_frames(joinpath(dir, "reference.mkv"), dir) + @testset "$format" for format in ["mp4", "mkv", "webm"] + path = joinpath(dir, "test.$format") + fig, iter, func = generate_plot(2) + record(func, fig, path, iter) + compare_videos(reference, path, dir) + + fig, iter, func = generate_plot(2) + vso = Makie.Record(func, fig, iter; format="mkv") + path = joinpath(dir, "test.$format") + save(path, vso) + compare_videos(reference, path, dir) + end + # We re-use ramstepper, to add our array of frames to the reference image comparison + return Makie.RamStepper(fig, Makie.current_backend().Screen(fig.scene), reference, :png) + end +end diff --git a/WGLMakie/src/WGLMakie.jl b/WGLMakie/src/WGLMakie.jl index 20909f4ef40..1c80b9a9d32 100644 --- a/WGLMakie/src/WGLMakie.jl +++ b/WGLMakie/src/WGLMakie.jl @@ -26,11 +26,9 @@ using Makie: get_texture_atlas, glyph_uv_width!, SceneSpace, Pixel using Makie: attribute_per_char, glyph_uv_width!, layout_text using Makie: MouseButtonEvent, KeyEvent using Makie: apply_transform, transform_func_obs -using Makie: inline! using Makie: spaces, is_data_space, is_pixel_space, is_relative_space, is_clip_space struct WebGL <: ShaderAbstractions.AbstractContext end -struct WGLBackend <: Makie.AbstractBackend end const THREE = Dependency(:THREE, ["https://unpkg.com/three@0.136.0/build/three.js"]) const WGL = Dependency(:WGLMakie, [@path joinpath(@__DIR__, "wglmakie.js")]) @@ -45,20 +43,20 @@ include("meshes.jl") include("imagelike.jl") include("display.jl") -const CONFIG = ( - fps = Ref(30), -) """ - activate!(; fps=30) + WGLMakie.activate!(; screen_config...) -Set fps (frames per second) to a higher number for smoother animations, or to a lower to use less resources. +Sets WGLMakie as the currently active backend and also allows to quickly set the `screen_config`. +Note, that the `screen_config` can also be set permanently via `Makie.set_theme!(WGLMakie=(screen_config...,))`. + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) """ -function activate!(; fps=30) - CONFIG.fps[] = fps - b = WGLBackend() - Makie.register_backend!(b) - Makie.current_backend[] = b +function activate!(; screen_config...) + Makie.set_active_backend!(WGLMakie) + Makie.set_screen_config!(WGLMakie, screen_config) Makie.set_glyph_resolution!(Makie.Low) return end @@ -68,8 +66,6 @@ const TEXTURE_ATLAS_CHANGED = Ref(false) function __init__() # Activate WGLMakie as backend! activate!() - browser_display = JSServe.BrowserDisplay() in Base.Multimedia.displays - Makie.inline!(!browser_display) # We need to update the texture atlas whenever it changes! # We do this in three_plot! Makie.font_render_callback!() do sd, uv @@ -84,7 +80,6 @@ for name in names(Makie, all=true) @eval export $(name) end end -export inline! include("precompiles.jl") diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 6a02d3e4fed..a09bd8df271 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -1,6 +1,6 @@ function JSServe.jsrender(session::Session, scene::Scene) - three, canvas = WGLMakie.three_display(session, scene) + three, canvas = three_display(session, scene) Makie.push_screen!(scene, three) return canvas end @@ -10,12 +10,39 @@ function JSServe.jsrender(session::Session, fig::Makie.FigureLike) return JSServe.jsrender(session, Makie.get_scene(fig)) end -const WEB_MIMES = (MIME"text/html", MIME"application/vnd.webio.application+html", - MIME"application/prs.juno.plotpane+html", MIME"juliavscode/html") +const WEB_MIMES = ( + MIME"text/html", + MIME"application/vnd.webio.application+html", + MIME"application/prs.juno.plotpane+html", + MIME"juliavscode/html") + + +""" +* `framerate = 30`: Set framerate (frames per second) to a higher number for smoother animations, or to a lower to use less resources. +""" +struct ScreenConfig + framerate::Float64 # =30.0 +end + +""" + Screen(args...; screen_config...) + +# Arguments one can pass via `screen_config`: + +$(Base.doc(ScreenConfig)) + +# Constructors: + +$(Base.doc(MakieScreen)) +""" +mutable struct Screen <: Makie.MakieScreen + three::Union{Nothing, ThreeDisplay} + display::Any +end for M in WEB_MIMES @eval begin - function Makie.backend_show(::WGLBackend, io::IO, m::$M, scene::Scene) + function Makie.backend_show(screen::Screen, io::IO, m::$M, scene::Scene) three = nothing inline_display = App() do session::Session three, canvas = three_display(session, scene) @@ -23,75 +50,64 @@ for M in WEB_MIMES return canvas end Base.show(io, m, inline_display) - return three + screen.three = three + return screen end end end -function scene2image(scene::Scene) - if !JSServe.has_html_display() - error(""" - There is no Display that can show HTML. - If in the REPL and you have a browser, - you can always run `JSServe.browser_display()` to show plots in the browser - """) - end - three = nothing - session = nothing - app = App() do s::Session - session = s - three, canvas = three_display(s, scene) - return canvas - end - # display in current - display(app) - done = Base.timedwait(()-> isready(session.js_fully_loaded), 30.0) - if done == :timed_out - error("JS Session not ready after 30s waiting, possibly errored while displaying") - end - return Makie.colorbuffer(three) +function Makie.backend_showable(::Type{Screen}, ::T) where {T<:MIME} + return T in WEB_MIMES end -function Makie.backend_show(::WGLBackend, io::IO, m::MIME"image/png", - scene::Scene) - img = scene2image(scene) - return FileIO.save(FileIO.Stream{FileIO.format"PNG"}(io), img) +function Base.size(screen::Screen) + return size(get_three(screen)) end -function Makie.backend_show(::WGLBackend, io::IO, m::MIME"image/jpeg", - scene::Scene) - img = scene2image(scene) - return FileIO.save(FileIO.Stream{FileIO.format"JPEG"}(io), img) +function get_three(screen::Screen)::ThreeDisplay + if isnothing(screen.three) + error("WGLMakie screen not yet shown in browser.") + end + return screen.three end -function Makie.backend_showable(::WGLBackend, ::T, scene::Scene) where {T<:MIME} - return T in WEB_MIMES -end +Screen() = Screen(nothing, nothing) -struct WebDisplay <: Makie.AbstractScreen - three::Base.RefValue{ThreeDisplay} - display::Any +# TODO, create optimized screens, forward more options to JS/WebGL +Screen(::Scene; kw...) = Screen() +Screen(::Scene, ::IO, ::MIME; kw...) = Screen() +Screen(::Scene, ::Makie.ImageStorageFormat; kw...) = Screen() +function Base.empty!(::WGLMakie.Screen) + # TODO, empty state in JS, to be able to reuse screen end -function Makie.backend_display(::WGLBackend, scene::Scene; kw...) +function Base.display(screen::Screen, scene::Scene; kw...) + Makie.push_screen!(scene, screen) # Reference to three object which gets set once we serve this to a browser three_ref = Base.RefValue{ThreeDisplay}() - app = App() do s, request - three, canvas = three_display(s, scene) + app = App() do session, request + three, canvas = three_display(session, scene) three_ref[] = three return canvas end actual_display = display(app) - return WebDisplay(three_ref, actual_display) + screen.three = wait_for_three(three_ref) + screen.display = actual_display + return screen end -function Base.delete!(td::WebDisplay, scene::Scene, plot::AbstractPlot) +function Base.delete!(td::Screen, scene::Scene, plot::AbstractPlot) delete!(get_three(td), scene, plot) end function session2image(sessionlike) + yield() s = JSServe.session(sessionlike) - to_data = js"document.querySelector('canvas').toDataURL()" + to_data = js"""function (){ + $(WGL).current_renderloop() + return document.querySelector('canvas').toDataURL() + }() + """ picture_base64 = JSServe.evaljs_value(s, to_data; time_out=100) picture_base64 = replace(picture_base64, "data:image/png;base64," => "") bytes = JSServe.Base64.base64decode(picture_base64) @@ -102,13 +118,17 @@ function Makie.colorbuffer(screen::ThreeDisplay) return session2image(screen) end -function get_three(screen::WebDisplay; timeout = 30)::Union{Nothing, ThreeDisplay} - # WebDisplay is not guaranteed to get displayed in the browser, so we wait a while +function Makie.colorbuffer(screen::Screen) + return session2image(get_three(screen)) +end + +function wait_for_three(three_ref::Base.RefValue{ThreeDisplay}; timeout = 30)::Union{Nothing, ThreeDisplay} + # Screen is not guaranteed to get displayed in the browser, so we wait a while # to see if anything gets displayed! tstart = time() while time() - tstart < timeout - if isassigned(screen.three) - three = screen.three[] + if isassigned(three_ref) + three = three_ref[] session = JSServe.session(three) if isready(session.js_fully_loaded) # Error on js during init! We can't continue like this :'( @@ -123,11 +143,7 @@ function get_three(screen::WebDisplay; timeout = 30)::Union{Nothing, ThreeDispla return nothing end -function Makie.colorbuffer(screen::WebDisplay) - return session2image(get_three(screen)) -end - -function Base.insert!(td::WebDisplay, scene::Scene, plot::Combined) +function Base.insert!(td::Screen, scene::Scene, plot::Combined) disp = get_three(td) disp === nothing && error("Plot needs to be displayed to insert additional plots") insert!(disp, scene, plot) diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index 29fee1250c6..14240ed0913 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -1,15 +1,23 @@ -struct ThreeDisplay <: Makie.AbstractScreen +struct ThreeDisplay <: Makie.MakieScreen session::JSServe.Session end JSServe.session(td::ThreeDisplay) = td.session +function Base.size(screen::ThreeDisplay) + # look at d.qs().clientWidth for displayed width + width, height = round.(Int, WGLMakie.JSServe.evaljs_value(screen.session, WGLMakie.JSServe.js"[document.querySelector('canvas').width, document.querySelector('canvas').height]"; time_out=100)) + return (width, height) +end + # We use objectid to find objects on the js side js_uuid(object) = string(objectid(object)) function Base.insert!(td::ThreeDisplay, scene::Scene, plot::Combined) plot_data = serialize_plots(scene, [plot]) - WGL.insert_plot(td.session, js_uuid(scene), plot_data) + JSServe.evaljs_value(td.session, js""" + $(WGL).insert_plot($(js_uuid(scene)), $plot_data) + """) return end @@ -44,13 +52,15 @@ function find_plots(session::Session, plot::AbstractPlot) return WGL.find_plots(session, uuids) end - function JSServe.print_js_code(io::IO, plot::AbstractPlot, context) uuids = js_uuid.(Makie.flatten_plots(plot)) JSServe.print_js_code(io, js"$(WGL).find_plots($(uuids))", context) end -function three_display(session::Session, scene::Scene) +function three_display(session::Session, scene::Scene; screen_config...) + + config = Makie.merge_screen_config(ScreenConfig, screen_config)::ScreenConfig + serialized = serialize_scene(scene) if TEXTURE_ATLAS_CHANGED[] @@ -82,7 +92,7 @@ function three_display(session::Session, scene::Scene) if ( renderer ) { const three_scenes = scenes.map(x=> $(WGL).deserialize_scene(x, canvas)) const cam = new $(THREE).PerspectiveCamera(45, 1, 0, 100) - $(WGL).start_renderloop(renderer, three_scenes, cam, $(CONFIG.fps[])) + $(WGL).start_renderloop(renderer, three_scenes, cam, $(config.framerate)) JSServe.on_update($canvas_width, w_h => { // `renderer.setSize` correctly updates `canvas` dimensions const pixelRatio = renderer.getPixelRatio(); diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 54b5517e335..4988afa5473 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -4,6 +4,9 @@ const WGLMakie = (function () { // e.g. insert!(scene, plot) / delete!(scene, plot) const scene_cache = {}; const plot_cache = {}; + let current_renderloop = () => 0; + + const exports = {current_renderloop}; function add_scene(scene_id, three_scene) { scene_cache[scene_id] = three_scene; @@ -635,6 +638,9 @@ const WGLMakie = (function () { } // render one time before starting loop, so that we don't wait 30ms before first render render_scenes(renderer, three_scenes, cam); + exports.current_renderloop = function (){ + render_scenes(renderer, three_scenes, cam); + } renderloop(); } @@ -741,19 +747,18 @@ const WGLMakie = (function () { return renderer; } - return { - deserialize_scene, - threejs_module, - start_renderloop, - deserialize_three, - render_scenes, - delete_plots, - insert_plot, - find_plots, - delete_scene, - find_scene, - scene_cache, - plot_cache, - delete_scenes, - }; + exports.deserialize_scene = deserialize_scene; + exports.threejs_module = threejs_module; + exports.start_renderloop = start_renderloop; + exports.deserialize_three = deserialize_three; + exports.render_scenes = render_scenes; + exports.delete_plots = delete_plots; + exports.insert_plot = insert_plot; + exports.find_plots = find_plots; + exports.delete_scene = delete_scene; + exports.find_scene = find_scene; + exports.scene_cache = scene_cache; + exports.plot_cache = plot_cache; + exports.delete_scenes = delete_scenes; + return exports })(); diff --git a/docs/documentation/backends/cairomakie.md b/docs/documentation/backends/cairomakie.md index 7d772bd19ed..d31748bcd57 100644 --- a/docs/documentation/backends/cairomakie.md +++ b/docs/documentation/backends/cairomakie.md @@ -3,7 +3,17 @@ [CairoMakie](https://github.com/MakieOrg/Makie.jl/tree/master/CairoMakie) uses Cairo.jl to draw vector graphics to SVG and PDF. You should use it if you want to achieve the highest-quality plots for publications, as the rendering process of the GL backends works via bitmaps and is geared more towards speed than pixel-perfection. -### Special CairoMakie Properties +## Activation and screen config + +Activate the backend by calling `CairoMakie.activate!()` with the following options: +```julia:docs +# hideall +using CairoMakie, Markdown +println("~~~") +println(Markdown.html(@doc CairoMakie.activate!)) +println("~~~") +``` +\textoutput{docs} #### Inline Plot Type diff --git a/docs/documentation/backends/glmakie.md b/docs/documentation/backends/glmakie.md index bce9edda209..c270749dc8c 100644 --- a/docs/documentation/backends/glmakie.md +++ b/docs/documentation/backends/glmakie.md @@ -3,24 +3,17 @@ [GLMakie](https://github.com/MakieOrg/Makie.jl/tree/master/GLMakie) is the native, desktop-based backend, and is the most feature-complete. It requires an OpenGL enabled graphics card with OpenGL version 3.3 or higher. -### Special GLMakie Properties - -#### Window Parameters - -You can set parameters of the window with the function `set_window_config!` which only takes effect when opening a new window. - -```julia -set_window_config!(; - renderloop = renderloop, - vsync = false, - framerate = 30.0, - float = false, - pause_rendering = false, - focus_on_show = false, - decorated = true, - title = "Makie" -) +## Activation and screen config + +Activate the backend by calling `GLMakie.activate!()` with the following options: +```julia:docs +# hideall +using GLMakie, Markdown +println("~~~") +println(Markdown.html(@doc GLMakie.activate!)) +println("~~~") ``` +\textoutput{docs} #### Multiple Windows diff --git a/docs/documentation/backends/rprmakie.md b/docs/documentation/backends/rprmakie.md index 7d097267c69..a9081d67362 100644 --- a/docs/documentation/backends/rprmakie.md +++ b/docs/documentation/backends/rprmakie.md @@ -11,10 +11,9 @@ using RadeonProRender RadeonProRender.Context() ``` -## Activating and working with RPMakie - -To use the backend, just call `activate!` like with all other backends. There are a few extra parameters for RPRMakie: +## Activation and screen config +Activate the backend by calling `RPRMakie.activate!()` with the following options: ```julia:docs # hideall using RPRMakie, Markdown @@ -42,9 +41,9 @@ lights = [ ax = LScene(fig[1, 1]; scenekw=(lights=lights,)) # to create materials, one needs access to the RPR context. -# Note, if you create an RPRScreen manually, don't display the scene or fig anymore, since that would create a new RPR context, in which resources from the manually created Context would be invalid. Since RPRs error handling is pretty bad, this usually results in Segfaults. +# Note, if you create an Screen manually, don't display the scene or fig anymore, since that would create a new RPR context, in which resources from the manually created Context would be invalid. Since RPRs error handling is pretty bad, this usually results in Segfaults. # See below how to render a picture with a manually created context -screen = RPRScreen(ax.scene; iterations=10, plugin=RPR.Northstar) +screen = Screen(ax.scene; iterations=10, plugin=RPR.Northstar) matsys = screen.matsys context = screen.context # You can use lots of materials from RPR. @@ -55,16 +54,16 @@ mat = RPR.Chrome(matsys) mesh!(ax, Sphere(Point3f, 0), material=mat) # There are three main ways to turn a Makie scene into a picture: -# Get the colorbuffer of the RPRScreen. RPRScreen also has `show` overloaded for the mime `image\png` so it should display in IJulia/Jupyter/VSCode. +# Get the colorbuffer of the Screen. Screen also has `show` overloaded for the mime `image\png` so it should display in IJulia/Jupyter/VSCode. image = colorbuffer(screen)::Matrix{RGB{N0f8}} -# Replace a specific (sub) LScene with RPR, and display the whole scene interactively in GLMakie -using GLMakie +# Replace a specific (sub) LScene with RPR, and display the whole scene interactively in RPRMakie +using RPRMakie refres = Observable(nothing) # Optional observable that triggers -GLMakie.activate!(); display(fig) # Make sure to display scene first in GLMakie +RPRMakie.activate!(); display(fig) # Make sure to display scene first in RPRMakie # Replace the scene with an interactively rendered RPR output. -# See more about this in the GLMakie interop example +# See more about this in the RPRMakie interop example context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh) -# If one doesn't create the RPRScreen manually to create custom materials, +# If one doesn't create the Screen manually to create custom materials, # display(ax.scene), show(io, MIME"image/png", ax.scene), save("rpr.png", ax.scene) # Should work just like with other backends. # Note, that only the scene from LScene can be displayed directly, but soon, `display(fig)` should also work. @@ -92,7 +91,7 @@ lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] fig = Figure(; resolution=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights)) -screen = RPRScreen(ax.scene; plugin=RPR.Northstar, iterations=400) +screen = Screen(ax.scene; plugin=RPR.Northstar, iterations=400) matsys = screen.matsys emissive = RPR.EmissiveMaterial(matsys) @@ -206,7 +205,7 @@ save("topographie.png", ax.scene) ~~~ -## GLMakie interop (opengl_interop.jl) +## RPRMakie interop (opengl_interop.jl) RPRMakie doesn't support layouting and sub scenes yet, but you can replace a single scene with a RPR rendered, interactive window. This is especially handy, to show 2d graphics and interactive UI elements next to a ray traced scene and interactively tune camera and material parameters. @@ -215,7 +214,7 @@ This is especially handy, to show 2d graphics and interactive UI elements next t ~~~ ```julia -using GLMakie, GeometryBasics, RPRMakie, RadeonProRender +using RPRMakie, GeometryBasics, RPRMakie, RadeonProRender using Colors, FileIO using Colors: N0f8 @@ -230,7 +229,7 @@ lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), fig = Figure(; resolution=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights)) -screen = RPRMakie.RPRScreen(size(ax.scene); plugin=RPR.Tahoe) +screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Tahoe) material = RPR.UberMaterial(screen.matsys) surface!(ax, f.(u, v'), g.(u, v'), h.(u, v'); ambient=Vec3f(0.5), diffuse=Vec3f(1), specular=0.5, @@ -286,7 +285,7 @@ for (key, (obs, input)) in pairs(sliders) end fig[1, 2] = grid!(hcat(labels, inputs); width=500) -GLMakie.activate!() +RPRMakie.activate!() cam = cameracontrols(ax.scene) cam.eyeposition[] = Vec3f(22, 0, 17) @@ -485,7 +484,6 @@ for i in 1:length(toLines) end earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/commons/5/56/Blue_Marble_Next_Generation_%2B_topography_%2B_bathymetry.jpg")) -Makie.inline!(true) # the actual plot ! RPRMakie.activate!(; iterations=100) scene = with_theme(theme_dark()) do diff --git a/docs/documentation/backends/wglmakie.md b/docs/documentation/backends/wglmakie.md index 6fc988723d5..924dee48bec 100644 --- a/docs/documentation/backends/wglmakie.md +++ b/docs/documentation/backends/wglmakie.md @@ -3,7 +3,7 @@ [WGLMakie](https://github.com/MakieOrg/Makie.jl/tree/master/WGLMakie) is the Web-based backend, and is still experimental (though relatively feature-complete). WGLMakie uses [JSServe](https://github.com/SimonDanisch/JSServe.jl) to generate the HTML and JS for the Makie plots. -## Activation +## Activation and screen config Activate the backend by calling `WGLMakie.activate!()` with the following options: ```julia:docs diff --git a/docs/documentation/blocks.md b/docs/documentation/blocks.md index 3f672cc93a6..e011e8caf7d 100644 --- a/docs/documentation/blocks.md +++ b/docs/documentation/blocks.md @@ -17,7 +17,7 @@ Here's one way to add a `Block`, in this case an `Axis`, to a Figure. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() ax = Axis(f[1, 1]) @@ -38,7 +38,7 @@ Here's an example where two axes are placed manually: ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f, bbox = BBox(100, 300, 100, 500), title = "Axis 1") diff --git a/docs/documentation/lighting.md b/docs/documentation/lighting.md index 1260a04522b..094f3ac56d6 100644 --- a/docs/documentation/lighting.md +++ b/docs/documentation/lighting.md @@ -27,7 +27,7 @@ GLMakie also implements [_screen-space ambient occlusion_](https://learnopengl.c a good compromise. !!! note - The SSAO postprocessor is turned off by default to save on resources. To turn it on, set `GLMakie.enable_SSAO[] = true`, close any existing GLMakie window and reopen it. + The SSAO postprocessor is turned off by default to save on resources. To turn it on, set `GLMakie.activate!(ssao=true)`, close any existing GLMakie window and reopen it. ## Matcap @@ -95,8 +95,7 @@ app \begin{examplefigure}{} ```julia using GLMakie -GLMakie.activate!() # hide -GLMakie.enable_SSAO[] = true +GLMakie.activate!(ssao=true) # hide GLMakie.closeall() # close any open screen fig = Figure() @@ -113,7 +112,7 @@ fig \end{examplefigure} ```julia:disable-ssao -GLMakie.enable_SSAO[] = false # hide +GLMakie.activate!(ssao=false) # hide GLMakie.closeall() # hide ``` diff --git a/docs/documentation/theming.md b/docs/documentation/theming.md index cc7820b12d7..3b7ad8bdfba 100644 --- a/docs/documentation/theming.md +++ b/docs/documentation/theming.md @@ -22,7 +22,7 @@ Let's create a plot with the default theme: ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + function example_plot() f = Figure() @@ -152,7 +152,7 @@ with_theme( Theme( palette = (color = [:red, :blue], marker = [:circle, :xcross]), Scatter = (cycle = [:color, :marker],) - )) do + )) do scatter(fill(1, 10)) scatter!(fill(2, 10)) scatter!(fill(3, 10)) @@ -192,7 +192,7 @@ For example ```julia with_theme( Theme( - palette = (color = [:red, :blue], linestyle = [:dash, :dot]), + palette = (color = [:red, :blue], linestyle = [:dash, :dot]), Lines = (cycle = Cycle([:color, :linestyle], covary = true),) )) do lines(fill(5, 10)) @@ -217,7 +217,7 @@ The cycler's internal counter is not advanced when using `Cycled` for any attrib ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -247,7 +247,7 @@ Here's an example that shows how density plots react to different palette option ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + set_theme!() # hide diff --git a/docs/examples/blocks/axis.md b/docs/examples/blocks/axis.md index b1a473c378a..fbb2acc1b95 100644 --- a/docs/examples/blocks/axis.md +++ b/docs/examples/blocks/axis.md @@ -43,7 +43,7 @@ You can also remove all plots with `empty!(ax)`. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -75,7 +75,7 @@ but they will be changed to fit the chosen ratio. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -105,7 +105,7 @@ You can set half limits by either giving one argument as `nothing` or by using t ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -143,7 +143,7 @@ The user-defined limits are stored in `ax.limits`. This can either be a tuple wi ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -166,7 +166,7 @@ The gap between title and subtitle is set with `subtitlegap` and the gap between ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -226,7 +226,7 @@ Note that the number is only a target, the actual number of ticks can be higher ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + fig = Figure() for (i, n) in enumerate([2, 4, 6]) @@ -250,7 +250,7 @@ A common scenario is plotting a trigonometric function which should be marked at ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + lines(0..20, sin, axis = (xticks = MultiplesTicks(4, pi, "π"),)) ``` @@ -262,7 +262,7 @@ lines(0..20, sin, axis = (xticks = MultiplesTicks(4, pi, "π"),)) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + lines(0..20, sin, axis = (xticks = 0:3:18,)) ``` @@ -274,7 +274,7 @@ lines(0..20, sin, axis = (xticks = 0:3:18,)) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + values = [0, 5, 10, 15, 20] labels = ["zero", "five", "ten", "fifteen", "twenty"] @@ -294,7 +294,7 @@ This can be useful for log plots where all powers of 10 should be shown. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + struct IntegerTicks end @@ -319,7 +319,7 @@ For example, you could append "k" for numbers larger than one thousand: ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + function k_suffix(values) map(values) do v @@ -346,7 +346,7 @@ Color, size and alignment of the mirrored ticks are the same as for the normal t ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1], @@ -375,7 +375,7 @@ Here are the same ticks with different format strings: ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() for (i, str) in enumerate(["{:.1f}", "{:.2f}", "t = {:.4f}ms"]) @@ -412,7 +412,7 @@ The idea is to not actually implement new tick finding, just to rescale the valu ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + struct TimeTicks end @@ -464,7 +464,7 @@ The default minor tick type is `IntervalsBetween(n, mirror = true)` where `n` gi ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + theme = Attributes( Axis = ( @@ -494,7 +494,7 @@ Minor ticks can also be given as an `AbstractVector` of real numbers. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + lines(1..10, sin, axis = ( yminorgridvisible = true, @@ -517,7 +517,7 @@ To hide spines, you can use `hidespines!`. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -539,7 +539,7 @@ It's common, e.g., to hide everything but the grid lines in facet plots. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -563,7 +563,7 @@ The attributes `xtrimspine` and `ytrimspine` can be used to limit the respective ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + hist(randn(100) ./ 4 .+ 5, strokewidth = 1, @@ -591,7 +591,7 @@ Take care that the axis limits always stay inside the limits appropriate for the ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + data = LinRange(0.01, 0.99, 200) @@ -618,7 +618,7 @@ Some plotting functions, like barplots or density plots, have offset parameters ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + processors = ["VAX-11/780", "Sun-4/260", "PowerPC 604", "Alpha 21164", "Intel Pentium III", "Intel Xeon"] @@ -642,7 +642,7 @@ Another option for symmetric log scales including zero is the symmetric log scal ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure(resolution = (800, 700)) @@ -678,7 +678,7 @@ it necessarily has to break the layout a little bit. using CairoMakie using FileIO CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random # hide Random.seed!(1) # hide @@ -794,7 +794,7 @@ separately. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -835,7 +835,7 @@ Note how x and y labels are misaligned in this figure due to different tick labe ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() diff --git a/docs/examples/blocks/axis3.md b/docs/examples/blocks/axis3.md index 0c95f04e9ba..504aa7f2ace 100644 --- a/docs/examples/blocks/axis3.md +++ b/docs/examples/blocks/axis3.md @@ -9,7 +9,7 @@ The two attributes `azimuth` and `elevation` control the angles from which the p using GLMakie using FileIO GLMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -52,7 +52,7 @@ You can also set it to a three-tuple, where each number gives the relative lengt using GLMakie using FileIO GLMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -88,7 +88,7 @@ On the other hand, it uses the available space most efficiently. ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -122,7 +122,7 @@ A value of 0 looks like an orthographic projection (it is only approximate to a ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure(resolution = (1200, 800), fontsize = 14) @@ -139,4 +139,4 @@ end f ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/blocks/intervalslider.md b/docs/examples/blocks/intervalslider.md index 1ab23f9e745..79bcf559227 100644 --- a/docs/examples/blocks/intervalslider.md +++ b/docs/examples/blocks/intervalslider.md @@ -21,7 +21,7 @@ If you set the attribute `snap = false`, the slider will move continously while \begin{examplefigure}{} ```julia using CairoMakie -Makie.inline!(true) # hide + CairoMakie.activate!() # hide f = Figure() @@ -55,4 +55,4 @@ scatter!(points, color = colors, colormap = [:black, :orange], strokewidth = 0) f ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/arrows.md b/docs/examples/plotting_functions/arrows.md index e6782ab769d..07e0f621e85 100644 --- a/docs/examples/plotting_functions/arrows.md +++ b/docs/examples/plotting_functions/arrows.md @@ -40,7 +40,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure(resolution = (800, 800)) Axis(f[1, 1], backgroundcolor = "black") @@ -62,7 +62,7 @@ f ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + ps = [Point3f(x, y, z) for x in -5:2:5 for y in -5:2:5 for z in -5:2:5] ns = map(p -> 0.1 * Vec3f(p[2], p[3], p[1]), ps) @@ -79,7 +79,7 @@ arrows( ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + using LinearAlgebra ps = [Point3f(x, y, z) for x in -5:2:5 for y in -5:2:5 for z in -5:2:5] diff --git a/docs/examples/plotting_functions/band.md b/docs/examples/plotting_functions/band.md index f34c307ff8e..bacda2247c4 100644 --- a/docs/examples/plotting_functions/band.md +++ b/docs/examples/plotting_functions/band.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -29,7 +29,7 @@ f using Statistics using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -50,7 +50,7 @@ f ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + lower = fill(Point3f(0,0,0), 100) upper = [Point3f(sin(x), cos(x), 1.0) for x in range(0,2pi, length=100)] col = repeat([1:50;50:-1:1],outer=2) diff --git a/docs/examples/plotting_functions/barplot.md b/docs/examples/plotting_functions/barplot.md index c8be8c841f3..8c7c681b584 100644 --- a/docs/examples/plotting_functions/barplot.md +++ b/docs/examples/plotting_functions/barplot.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -27,7 +27,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = 1:0.2:10 ys = 0.5 .* sin.(xs) @@ -40,7 +40,7 @@ barplot(xs, ys, gap = 0, color = :gray85, strokecolor = :black, strokewidth = 1) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + tbl = (x = [1, 1, 1, 2, 2, 2, 3, 3, 3], height = 0.1:0.1:0.9, diff --git a/docs/examples/plotting_functions/boxplot.md b/docs/examples/plotting_functions/boxplot.md index 3578347d081..5b26cbce7a6 100644 --- a/docs/examples/plotting_functions/boxplot.md +++ b/docs/examples/plotting_functions/boxplot.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = rand(1:3, 1000) ys = randn(1000) @@ -21,7 +21,7 @@ boxplot(xs, ys) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = rand(1:3, 1000) ys = randn(1000) @@ -44,7 +44,7 @@ same color as their box, as shown above. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = rand(1:3, 1000) ys = randn(1000) @@ -60,7 +60,7 @@ boxplot(xs, ys, dodge = dodge, show_notch = true, color = map(d->d==1 ? :blue : ```julia using CairoMakie, Distributions CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 100_000 x = rand(1:3, N) @@ -75,4 +75,4 @@ boxplot(fig[1,2], x, y, weights = w) fig ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/contour.md b/docs/examples/plotting_functions/contour.md index c22d05b8369..9b4beaa9539 100644 --- a/docs/examples/plotting_functions/contour.md +++ b/docs/examples/plotting_functions/contour.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -29,7 +29,7 @@ Omitting the `xs` and `ys` results in the indices of `zs` being used. We can als ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) diff --git a/docs/examples/plotting_functions/contour3d.md b/docs/examples/plotting_functions/contour3d.md index 6ca86f033a3..239050c7573 100644 --- a/docs/examples/plotting_functions/contour3d.md +++ b/docs/examples/plotting_functions/contour3d.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis3(f[1, 1], aspect=(0.5,0.5,1), perspectiveness=0.75) @@ -29,7 +29,7 @@ Omitting the `xs` and `ys` results in the indices of `zs` being used. We can als ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis3(f[1, 1], aspect=(0.5,0.5,1), perspectiveness=0.75) @@ -42,4 +42,4 @@ contour3d!(+zs, levels= .025:0.05:.475, linewidth=2, color=:red2) f ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/contourf.md b/docs/examples/plotting_functions/contourf.md index 9ec769c1bcc..bf8539a0b49 100644 --- a/docs/examples/plotting_functions/contourf.md +++ b/docs/examples/plotting_functions/contourf.md @@ -7,7 +7,7 @@ using CairoMakie using DelimitedFiles CairoMakie.activate!() # hide -Makie.inline!(true) # hide + volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) @@ -27,7 +27,7 @@ f using CairoMakie using DelimitedFiles CairoMakie.activate!() # hide -Makie.inline!(true) # hide + volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) @@ -51,7 +51,7 @@ f using CairoMakie using DelimitedFiles CairoMakie.activate!() # hide -Makie.inline!(true) # hide + volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) @@ -81,7 +81,7 @@ to `:relative` and specify the levels from 0 to 1, relative to the current minim using CairoMakie using DelimitedFiles CairoMakie.activate!() # hide -Makie.inline!(true) # hide + volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) diff --git a/docs/examples/plotting_functions/crossbar.md b/docs/examples/plotting_functions/crossbar.md index 3d82c0ae54d..0950f500584 100644 --- a/docs/examples/plotting_functions/crossbar.md +++ b/docs/examples/plotting_functions/crossbar.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = [1, 1, 2, 2, 3, 3] ys = rand(6) diff --git a/docs/examples/plotting_functions/density.md b/docs/examples/plotting_functions/density.md index 36bf9ad0323..18338ff82fb 100644 --- a/docs/examples/plotting_functions/density.md +++ b/docs/examples/plotting_functions/density.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -23,7 +23,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -38,7 +38,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -54,7 +54,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -78,7 +78,7 @@ You can color density plots with gradients by choosing `color = :x` or `:y`, dep ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", @@ -105,7 +105,7 @@ Due to technical limitations, if you color the `:vertical` dimension (or :horizo ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -123,7 +123,7 @@ f ```julia using CairoMakie, Distributions CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 100_000 x = rand(Uniform(-2, 2), N) @@ -136,4 +136,4 @@ density(fig[1,2], x, weights = w) fig ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/ecdf.md b/docs/examples/plotting_functions/ecdf.md index 5c831afae9c..d1be0b69f59 100644 --- a/docs/examples/plotting_functions/ecdf.md +++ b/docs/examples/plotting_functions/ecdf.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -23,7 +23,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -40,13 +40,13 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) x = rand(200) -w = @. x^2 * (1 - x)^2 +w = @. x^2 * (1 - x)^2 ecdfplot!(x) ecdfplot!(x; weights = w, color=:orange) diff --git a/docs/examples/plotting_functions/errorbars.md b/docs/examples/plotting_functions/errorbars.md index b0a9ae5bfdf..ca6e04d3ccb 100644 --- a/docs/examples/plotting_functions/errorbars.md +++ b/docs/examples/plotting_functions/errorbars.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -32,7 +32,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -58,7 +58,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) diff --git a/docs/examples/plotting_functions/heatmap.md b/docs/examples/plotting_functions/heatmap.md index 2fe0e7469f4..3192fc87548 100644 --- a/docs/examples/plotting_functions/heatmap.md +++ b/docs/examples/plotting_functions/heatmap.md @@ -10,7 +10,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = range(0, 10, length = 25) @@ -27,7 +27,7 @@ heatmap(xs, ys, zs) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + function mandelbrot(x, y) z = c = x + y*im @@ -47,7 +47,7 @@ There must be no duplicate combinations of x and y, but it is allowed to leave o ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = [1, 2, 3, 1, 2, 3, 1, 2, 3] @@ -60,15 +60,15 @@ heatmap(xs, ys, zs) ### Colorbar for single heatmap -To get a scale for what the colors represent, add a colorbar. The colorbar is -placed within the figure in the first argument, and the scale and colormap can be +To get a scale for what the colors represent, add a colorbar. The colorbar is +placed within the figure in the first argument, and the scale and colormap can be conveniently set by passing the relevant heatmap to it. \begin{examplefigure}{} ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = range(0, 2π, length=100) ys = range(0, 2π, length=100) @@ -84,20 +84,20 @@ fig ### Colorbar for multiple heatmaps When there are several heatmaps in a single figure, it can be useful -to have a single colorbar represent all of them. It is important to then +to have a single colorbar represent all of them. It is important to then have synchronized scales and colormaps for the heatmaps and colorbar. This is done by -setting the colorrange explicitly, so that it is independent of the data shown by +setting the colorrange explicitly, so that it is independent of the data shown by that particular heatmap. -Since the heatmaps in the example below have the same colorrange and colormap, any of them -can be passed to `Colorbar` to give the colorbar the same attributes. Alternativly, +Since the heatmaps in the example below have the same colorrange and colormap, any of them +can be passed to `Colorbar` to give the colorbar the same attributes. Alternativly, the colorbar attributes can be set explicitly. \begin{examplefigure}{} ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = range(0, 2π, length=100) ys = range(0, 2π, length=100) diff --git a/docs/examples/plotting_functions/hexbin.md b/docs/examples/plotting_functions/hexbin.md index 09372ef942e..76c6ad91eb1 100644 --- a/docs/examples/plotting_functions/hexbin.md +++ b/docs/examples/plotting_functions/hexbin.md @@ -13,7 +13,7 @@ The minimum number of bins in one dimension is 2. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -39,7 +39,7 @@ You can also pass a tuple of integers to control x and y separately. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -71,7 +71,7 @@ This is why setting the same size for x and y will result in uneven hexagons. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -99,7 +99,7 @@ Note that the visual appearance of the hexagons will only be even if the x and y ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -127,7 +127,7 @@ All hexagons with a count lower than `threshold` will be removed: ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -153,7 +153,7 @@ You can pass a scale function to via the `scale` keyword, which will be applied ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -178,10 +178,10 @@ In this example, we add a transparent color to the start of the colormap and str ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using DelimitedFiles CairoMakie.activate!() # hide -Makie.inline!(true) # hide + a = map(Point2f, eachrow(readdlm(assetpath("airportlocations.csv")))) diff --git a/docs/examples/plotting_functions/hist.md b/docs/examples/plotting_functions/hist.md index ea4f850f093..6df7ff2520a 100644 --- a/docs/examples/plotting_functions/hist.md +++ b/docs/examples/plotting_functions/hist.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + data = randn(1000) @@ -63,7 +63,7 @@ fig ```julia using CairoMakie, Distributions CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 100_000 x = rand(Uniform(-5, 5), N) @@ -76,4 +76,4 @@ hist(fig[1,2], x, weights = w) fig ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/image.md b/docs/examples/plotting_functions/image.md index 532b151efc9..3ad2dd9815c 100644 --- a/docs/examples/plotting_functions/image.md +++ b/docs/examples/plotting_functions/image.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using FileIO img = load(assetpath("cow.png")) diff --git a/docs/examples/plotting_functions/lines.md b/docs/examples/plotting_functions/lines.md index 0b6eec27cbd..bae56aa59c3 100644 --- a/docs/examples/plotting_functions/lines.md +++ b/docs/examples/plotting_functions/lines.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -31,7 +31,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -50,4 +50,4 @@ end f ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/linesegments.md b/docs/examples/plotting_functions/linesegments.md index a674cdcfebd..783f4bde81d 100644 --- a/docs/examples/plotting_functions/linesegments.md +++ b/docs/examples/plotting_functions/linesegments.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) diff --git a/docs/examples/plotting_functions/mesh.md b/docs/examples/plotting_functions/mesh.md index 5223389aa7c..9f7941e0cc4 100644 --- a/docs/examples/plotting_functions/mesh.md +++ b/docs/examples/plotting_functions/mesh.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + vertices = [ 0.0 0.0; @@ -33,7 +33,7 @@ scene = mesh(vertices, faces, color = colors, shading = false) using FileIO using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + brain = load(assetpath("brain.stl")) @@ -53,7 +53,7 @@ We can also create a mesh, to specify normals, uv coordinates: ```julia:mesh using GeometryBasics, LinearAlgebra, GLMakie, FileIO GLMakie.activate!() # hide -Makie.inline!(true) # hide + # Create vertices for a Sphere r = 0.5f0 diff --git a/docs/examples/plotting_functions/meshscatter.md b/docs/examples/plotting_functions/meshscatter.md index 3abea123eeb..ff2b25f8367 100644 --- a/docs/examples/plotting_functions/meshscatter.md +++ b/docs/examples/plotting_functions/meshscatter.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + xs = cos.(1:0.5:20) ys = sin.(1:0.5:20) diff --git a/docs/examples/plotting_functions/pie.md b/docs/examples/plotting_functions/pie.md index fac01fbea20..78cee3f26f0 100644 --- a/docs/examples/plotting_functions/pie.md +++ b/docs/examples/plotting_functions/pie.md @@ -8,7 +8,7 @@ ### Generic -- `normalize = true` sets whether the data will be normalized to the range [0, 2π]. +- `normalize = true` sets whether the data will be normalized to the range [0, 2π]. - `color` sets the color of the pie segments. It can be given as a single named color or a vector of the same length as the input data - `strokecolor = :black` sets the color of the outline around the segments. - `strokewidth = 1` sets the width of the outline around the segments. @@ -29,7 +29,7 @@ Set the axis properties `autolimitaspect = 1` or `aspect = DataAspect()` to ensu ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + data = [36, 12, 68, 5, 42, 27] colors = [:yellow, :orange, :red, :blue, :purple, :green] @@ -40,7 +40,7 @@ f, ax, plt = pie(data, inner_radius = 2, strokecolor = :white, strokewidth = 5, - axis = (autolimitaspect = 1, ) + axis = (autolimitaspect = 1, ) ) f @@ -52,7 +52,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f, ax, plt = pie([π/2, 2π/3, π/4], normalize=false, diff --git a/docs/examples/plotting_functions/poly.md b/docs/examples/plotting_functions/poly.md index 634015c6833..2f946fcd1a3 100644 --- a/docs/examples/plotting_functions/poly.md +++ b/docs/examples/plotting_functions/poly.md @@ -9,7 +9,7 @@ using CairoMakie CairoMakie.activate!() # hide using Makie.GeometryBasics -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -25,7 +25,7 @@ f using CairoMakie CairoMakie.activate!() # hide using Makie.GeometryBasics -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -47,7 +47,7 @@ f using CairoMakie CairoMakie.activate!() # hide using Makie.GeometryBasics -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -68,7 +68,7 @@ f using CairoMakie CairoMakie.activate!() # hide using Makie.GeometryBasics -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1], aspect = DataAspect()) @@ -85,7 +85,7 @@ f using CairoMakie CairoMakie.activate!() # hide using Makie.GeometryBasics -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]; backgroundcolor = :gray15) diff --git a/docs/examples/plotting_functions/qqplot.md b/docs/examples/plotting_functions/qqplot.md index 6d605b059c7..402fe35ea37 100644 --- a/docs/examples/plotting_functions/qqplot.md +++ b/docs/examples/plotting_functions/qqplot.md @@ -11,7 +11,7 @@ Test if `xs` and `ys` follow the same distribution. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = randn(100) ys = randn(100) @@ -26,7 +26,7 @@ Test if `ys` is normally distributed. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + ys = 2 .* randn(100) .+ 3 diff --git a/docs/examples/plotting_functions/rainclouds.md b/docs/examples/plotting_functions/rainclouds.md index 15135a87e88..37d6efdade9 100644 --- a/docs/examples/plotting_functions/rainclouds.md +++ b/docs/examples/plotting_functions/rainclouds.md @@ -9,7 +9,7 @@ three together can make an appealing and informative visual, particularly for la ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random using Makie: rand_localized @@ -68,7 +68,7 @@ category_labels, data_array = mockup_categories_and_data_array(3) colors = Makie.wong_colors() rainclouds(category_labels, data_array; xlabel = "Categories of Distributions", ylabel = "Samples", title = "My Title", - plot_boxplots = false, cloud_width=0.5, clouds=hist, hist_bins=50, + plot_boxplots = false, cloud_width=0.5, clouds=hist, hist_bins=50, color = colors[indexin(category_labels, unique(category_labels))]) ``` \end{examplefigure} diff --git a/docs/examples/plotting_functions/rangebars.md b/docs/examples/plotting_functions/rangebars.md index 49ad829ce8e..42580593d28 100644 --- a/docs/examples/plotting_functions/rangebars.md +++ b/docs/examples/plotting_functions/rangebars.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -27,7 +27,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) diff --git a/docs/examples/plotting_functions/scatter.md b/docs/examples/plotting_functions/scatter.md index e333598070d..2009d2c5ebf 100644 --- a/docs/examples/plotting_functions/scatter.md +++ b/docs/examples/plotting_functions/scatter.md @@ -12,7 +12,7 @@ Scatters can be constructed by passing a list of x and y coordinates. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = range(0, 10, length = 30) ys = 0.5 .* sin.(xs) @@ -32,7 +32,7 @@ If you pass a vector of numbers for `color`, the attribute `colorrange` which is ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = range(0, 10, length = 30) ys = 0.5 .* sin.(xs) @@ -61,7 +61,7 @@ Here is an example plot showing different shapes that are accessible by `Symbol` ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + markers_labels = [ (:circle, ":circle"), @@ -118,7 +118,7 @@ For `Char` markers, `markersize` is equivalent to the font size when displaying ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f, ax, sc = scatter(1, 1, marker = 'A', markersize = 50) text!(2, 1, text = "A", textsize = 50, align = (:center, :center)) @@ -137,7 +137,7 @@ You can see that only the special markers `Circle` and `Rect` match the line wid ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f, ax, l = lines([0, 1], [1, 1], linewidth = 50, color = :gray80) for (marker, x) in zip(['X', 'x', :circle, :rect, :utriangle, Circle, Rect], range(0.1, 0.9, length = 7)) @@ -156,7 +156,7 @@ Here, we construct a hexagon polygon with radius `1`, which we can then use to t ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + hexagon = Makie.Polygon([Point2f(cos(a), sin(a)) for a in range(1/6 * pi, 13/6 * pi, length = 7)]) @@ -189,7 +189,7 @@ Here is an example with a simple arrow that is centered on its tip, built from p ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + arrow_path = BezierPath([ MoveTo(Point(0, 0)), @@ -221,7 +221,7 @@ For example, a circle with a square cut out can be made by one `EllipticalArc` t ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + circle_with_hole = BezierPath([ MoveTo(Point(1, 0)), @@ -255,7 +255,7 @@ Here's an example with an svg string that contains the bat symbol: ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + batsymbol_string = "M96.84 141.998c-4.947-23.457-20.359-32.211-25.862-13.887-11.822-22.963-37.961-16.135-22.041 6.289-3.005-1.295-5.872-2.682-8.538-4.191-8.646-5.318-15.259-11.314-19.774-17.586-3.237-5.07-4.994-10.541-4.994-16.229 0-19.774 21.115-36.758 50.861-43.694.446-.078.909-.154 1.372-.231-22.657 30.039 9.386 50.985 15.258 24.645l2.528-24.367 5.086 6.52H103.205l5.07-6.52 2.543 24.367c5.842 26.278 37.746 5.502 15.414-24.429 29.777 6.951 50.891 23.936 50.891 43.709 0 15.136-12.406 28.651-31.609 37.267 14.842-21.822-10.867-28.266-22.549-5.549-5.502-18.325-21.147-9.341-26.125 13.886z" @@ -277,7 +277,7 @@ In this example, a small circle is cut out of a larger circle: ```julia using CairoMakie, GeometryBasics CairoMakie.activate!() # hide -Makie.inline!(true) # hide + p_big = decompose(Point2f, Circle(Point2f(0), 1)) p_small = decompose(Point2f, Circle(Point2f(0), 0.5)) @@ -293,7 +293,7 @@ Markers can be rotated using the `rotations` attribute, which also allows to pas ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + points = [Point2f(x, y) for y in 1:10 for x in 1:10] rotations = range(0, 2pi, length = length(points)) @@ -310,7 +310,7 @@ You can scale x and y dimension of markers separately by passing a `Vec`. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() ax = Axis(f[1, 1]) @@ -338,7 +338,7 @@ By default marker sizes are given in pixel units. You can change this by adjusti ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() ax = Axis(f[1, 1]) @@ -362,7 +362,7 @@ f using CairoMakie using DelimitedFiles CairoMakie.activate!() # hide -Makie.inline!(true) # hide + a = readdlm(assetpath("airportlocations.csv")) diff --git a/docs/examples/plotting_functions/scatterlines.md b/docs/examples/plotting_functions/scatterlines.md index c6a95fff9d2..9e20662e60c 100644 --- a/docs/examples/plotting_functions/scatterlines.md +++ b/docs/examples/plotting_functions/scatterlines.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) diff --git a/docs/examples/plotting_functions/series.md b/docs/examples/plotting_functions/series.md index 0bf05fc7924..f071508b96e 100644 --- a/docs/examples/plotting_functions/series.md +++ b/docs/examples/plotting_functions/series.md @@ -10,7 +10,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + data = cumsum(randn(4, 101), dims = 2) diff --git a/docs/examples/plotting_functions/spy.md b/docs/examples/plotting_functions/spy.md index 255058d3b4c..00311dfb5bf 100644 --- a/docs/examples/plotting_functions/spy.md +++ b/docs/examples/plotting_functions/spy.md @@ -9,7 +9,7 @@ ```julia using CairoMakie, SparseArrays CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 100 # dimension of the sparse matrix p = 0.1 # independent probability that an entry is zero diff --git a/docs/examples/plotting_functions/stairs.md b/docs/examples/plotting_functions/stairs.md index f69e928df03..e2192912544 100644 --- a/docs/examples/plotting_functions/stairs.md +++ b/docs/examples/plotting_functions/stairs.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() diff --git a/docs/examples/plotting_functions/stem.md b/docs/examples/plotting_functions/stem.md index 991f83a5cf4..e86e477c547 100644 --- a/docs/examples/plotting_functions/stem.md +++ b/docs/examples/plotting_functions/stem.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -25,7 +25,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -46,7 +46,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() Axis(f[1, 1]) @@ -66,7 +66,7 @@ f ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() diff --git a/docs/examples/plotting_functions/streamplot.md b/docs/examples/plotting_functions/streamplot.md index 920e09c547c..3bd5a8734d9 100644 --- a/docs/examples/plotting_functions/streamplot.md +++ b/docs/examples/plotting_functions/streamplot.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + struct FitzhughNagumo{T} ϵ::T diff --git a/docs/examples/plotting_functions/surface.md b/docs/examples/plotting_functions/surface.md index d30ede0a997..ffaa7f07926 100644 --- a/docs/examples/plotting_functions/surface.md +++ b/docs/examples/plotting_functions/surface.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + xs = LinRange(0, 10, 100) ys = LinRange(0, 15, 100) @@ -23,7 +23,7 @@ surface(xs, ys, zs, axis=(type=Axis3,)) using GLMakie using DelimitedFiles GLMakie.activate!() # hide -Makie.inline!(true) # hide + volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) @@ -40,7 +40,7 @@ using SparseArrays using LinearAlgebra using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + # This example was provided by Moritz Schauer (@mschauer). diff --git a/docs/examples/plotting_functions/text.md b/docs/examples/plotting_functions/text.md index b90a6b17e8c..774a9ca98c1 100644 --- a/docs/examples/plotting_functions/text.md +++ b/docs/examples/plotting_functions/text.md @@ -15,7 +15,7 @@ You can either plot one string with one position, or a vector of strings with a ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -47,7 +47,7 @@ This means that the boundingbox of the text in data coordinates will include eve ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() LScene(f[1, 1]) @@ -74,7 +74,7 @@ Text can be aligned with the horizontal alignments `:left`, `:center`, `:right` ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + aligns = [(h, v) for v in [:bottom, :baseline, :center, :top] for h in [:left, :center, :right]] @@ -96,7 +96,7 @@ You can override this with the `justification` attribute. ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + scene = Scene(camera = campixel!, resolution = (800, 800)) @@ -107,7 +107,7 @@ symbols = (:left, :center, :right) for ((justification, halign), point) in zip(Iterators.product(symbols, symbols), points) - t = text!(scene, + t = text!(scene, point, text = "a\nshort\nparagraph", color = (:black, 0.5), @@ -142,7 +142,7 @@ You can specify the end of the barplots in data coordinates, and then offset the ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() @@ -169,7 +169,7 @@ Makie can render LaTeX strings from the LaTeXStrings.jl package using [MathTeXEn ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + lines(0.5..20, x -> sin(x) / sqrt(x), color = :black) text!(7, 0.38, text = L"\frac{\sin(x)}{\sqrt{x}}", color = :black) @@ -184,7 +184,7 @@ You can also pass L-strings to many objects that use text, for example as labels ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + f = Figure() ax = Axis(f[1, 1]) diff --git a/docs/examples/plotting_functions/tricontourf.md b/docs/examples/plotting_functions/tricontourf.md index f9f68291260..3adfadd50df 100644 --- a/docs/examples/plotting_functions/tricontourf.md +++ b/docs/examples/plotting_functions/tricontourf.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -27,7 +27,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -48,7 +48,7 @@ f ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(123) @@ -84,7 +84,7 @@ to `:relative` and specify the levels from 0 to 1, relative to the current minim ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + using Random Random.seed!(1234) @@ -97,4 +97,4 @@ scatter!(x, y, color = z, strokewidth = 1, strokecolor = :black) Colorbar(f[1, 2], tr) f ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/violin.md b/docs/examples/plotting_functions/violin.md index 6e02328083f..cea6a1db0b0 100644 --- a/docs/examples/plotting_functions/violin.md +++ b/docs/examples/plotting_functions/violin.md @@ -8,7 +8,7 @@ ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = rand(1:3, 1000) ys = randn(1000) @@ -21,7 +21,7 @@ violin(xs, ys) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + xs = rand(1:3, 1000) ys = map(xs) do x @@ -36,7 +36,7 @@ violin(xs, ys, datalimits = extrema) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 1000 xs = rand(1:3, N) @@ -55,7 +55,7 @@ violin(xs, ys, dodge = dodge, side = side, color = color) ```julia using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 1000 xs = rand(1:3, N) @@ -78,7 +78,7 @@ violin(xs, ys, side = side, color = color) ```julia using CairoMakie, Distributions CairoMakie.activate!() # hide -Makie.inline!(true) # hide + N = 100_000 x = rand(1:3, N) @@ -93,4 +93,4 @@ violin(fig[1,2], x, y, weights = w) fig ``` -\end{examplefigure} \ No newline at end of file +\end{examplefigure} diff --git a/docs/examples/plotting_functions/volumeslices.md b/docs/examples/plotting_functions/volumeslices.md index 391ea775a97..0d53a0b05de 100644 --- a/docs/examples/plotting_functions/volumeslices.md +++ b/docs/examples/plotting_functions/volumeslices.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + fig = Figure() ax = LScene(fig[1, 1], show_axis=false) diff --git a/docs/examples/plotting_functions/wireframe.md b/docs/examples/plotting_functions/wireframe.md index cafa93bbdb1..0b9b6d04b3c 100644 --- a/docs/examples/plotting_functions/wireframe.md +++ b/docs/examples/plotting_functions/wireframe.md @@ -8,7 +8,7 @@ ```julia using GLMakie GLMakie.activate!() # hide -Makie.inline!(true) # hide + x, y = collect(-8:0.5:8), collect(-8:0.5:8) z = [sinc(√(X^2 + Y^2) / π) for X ∈ x, Y ∈ y] diff --git a/docs/makedocs.jl b/docs/makedocs.jl index 877953ab58e..cb02544b617 100644 --- a/docs/makedocs.jl +++ b/docs/makedocs.jl @@ -118,6 +118,9 @@ function make_links_relative() end end +using GLMakie +GLMakie.activate!(pause_renderloop=true) + serve(; single=true, cleanup=false, fail_on_warning=true) # for interactive development of the docs, use: # cd(@__DIR__); serve(single=false, cleanup=true, clear=true, fail_on_warning = false) diff --git a/docs/misc/banner/banner.jl b/docs/misc/banner/banner.jl index 7126c80a560..f2c9e9bb6b5 100644 --- a/docs/misc/banner/banner.jl +++ b/docs/misc/banner/banner.jl @@ -1,8 +1,6 @@ using GLMakie GLMakie.activate!() -set_window_config!(pause_rendering = false) - ## function copy_scene_settings(s) @@ -71,4 +69,4 @@ end using CairoMakie CairoMakie.activate!() -save(joinpath(@__DIR__, "bannermesh.png"), s, px_per_unit = 2) \ No newline at end of file +save(joinpath(@__DIR__, "bannermesh.png"), s, px_per_unit = 2) diff --git a/docs/tutorials/basic-tutorial.md b/docs/tutorials/basic-tutorial.md index 036fb44f17c..8d057b48d0c 100644 --- a/docs/tutorials/basic-tutorial.md +++ b/docs/tutorials/basic-tutorial.md @@ -28,7 +28,7 @@ First, we import CairoMakie. This makes all the exported symbols from `Makie.jl` ```julia:setup using CairoMakie CairoMakie.activate!() # hide -Makie.inline!(true) # hide + nothing # hide ``` diff --git a/docs/tutorials/scenes.md b/docs/tutorials/scenes.md index 9c6218ce28c..e7a71e027c0 100644 --- a/docs/tutorials/scenes.md +++ b/docs/tutorials/scenes.md @@ -106,6 +106,7 @@ In GLMakie, we can actually take a look at the depthbuffer, to see how it looks GLMakie.activate!() # hide screen = display(scene) # use display, to get a reference to the screen object depth_color = GLMakie.depthbuffer(screen) +close(screen) # Look at result: f, ax, pl = heatmap(depth_color) Colorbar(f[1, 2], pl) diff --git a/docs/utils.jl b/docs/utils.jl index 543b7d6efba..9539e1a0273 100644 --- a/docs/utils.jl +++ b/docs/utils.jl @@ -10,13 +10,6 @@ using Pkg include("colormap_generation.jl") -# Pause renderloop for slow software rendering. -# This way, we only render if we actualy save e.g. an image -GLMakie.set_window_config!(; - framerate = 1.0, - pause_rendering = true -) - # copy NEWS file over to documentation cp( joinpath(@__DIR__, "..", "NEWS.md"), diff --git a/gallery.jl b/gallery.jl index 6ffb45fc2f6..c6bf2780152 100644 --- a/gallery.jl +++ b/gallery.jl @@ -4,7 +4,6 @@ using CSV using DataFrames using GLMakie using LinearAlgebra -GLMakie.inline!(false) url = "https://raw.githubusercontent.com/plotly/datasets/master/vortex.csv" df = CSV.File(HTTP.get(url).body)|> DataFrame; diff --git a/metrics/ttfp/benchmark-ttfp.jl b/metrics/ttfp/benchmark-ttfp.jl index e8dbae7f15e..1ce72fdc0cc 100644 --- a/metrics/ttfp/benchmark-ttfp.jl +++ b/metrics/ttfp/benchmark-ttfp.jl @@ -7,8 +7,17 @@ macro ctime(x) end end t_using = @ctime @eval using $Package -Makie.inline!(false) # needed for cairomakie to return a screen +function get_colorbuffer(fig) + # We need to handle old versions of Makie + if isdefined(Makie, :CURRENT_BACKEND) # new version after display refactor + return Makie.colorbuffer(fig) # easy :) + else + Makie.inline!(false) + screen = display(fig; visible=false) + return Makie.colorbuffer(screen) + end +end if Package == :WGLMakie using ElectronDisplay @@ -18,7 +27,7 @@ if Package == :WGLMakie end create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @ctime Makie.colorbuffer(display(fig)) +display_time = @ctime get_colorbuffer(fig) using BenchmarkTools using BenchmarkTools.JSON @@ -32,7 +41,7 @@ old = isfile(result) ? JSON.parse(read(result, String)) : [[], [], [], [], []] push!.(old[1:3], [t_using, create_time, display_time]) b1 = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -b2 = @benchmark Makie.colorbuffer(display(fig)) +b2 = @benchmark get_colorbuffer(fig) using Statistics diff --git a/src/Makie.jl b/src/Makie.jl index 2bdab4b8a65..94bb16a27c2 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -67,7 +67,7 @@ using Base.Iterators: repeated, drop import Base: getindex, setindex!, push!, append!, parent, get, get!, delete!, haskey using Observables: listeners, to_value, notify -using MakieCore: SceneLike, AbstractScreen, ScenePlot, AbstractScene, AbstractPlot, Transformable, Attributes, Combined, Theme, Plot +using MakieCore: SceneLike, MakieScreen, ScenePlot, AbstractScene, AbstractPlot, Transformable, Attributes, Combined, Theme, Plot using MakieCore: Heatmap, Image, Lines, LineSegments, Mesh, MeshScatter, Scatter, Surface, Text, Volume using MakieCore: ConversionTrait, NoConversion, PointBased, SurfaceLike, ContinuousSurface, DiscreteSurface, VolumeLike using MakieCore: Key, @key_str, Automatic, automatic, @recipe @@ -173,6 +173,9 @@ include("interaction/inspector.jl") # documentation and help functions include("documentation/documentation.jl") include("display.jl") +include("ffmpeg-util.jl") +include("recording.jl") +include("event-recorder.jl") # bezier paths export BezierPath, MoveTo, LineTo, CurveTo, EllipticalArc, ClosePath @@ -181,7 +184,7 @@ export BezierPath, MoveTo, LineTo, CurveTo, EllipticalArc, ClosePath export help, help_attributes, help_arguments # Abstract/Concrete scene + plot types -export AbstractScene, SceneLike, Scene, AbstractScreen +export AbstractScene, SceneLike, Scene, MakieScreen export AbstractPlot, Combined, Atomic, OldAxis # Theming, working with Plots @@ -262,7 +265,7 @@ export abline! # until deprecation removal export Stepper, replay_events, record_events, RecordEvents, record, VideoStream -export VideoStream, recordframe!, record +export VideoStream, recordframe!, record, Record export save # colormap stuff from PlotUtils, and showgradients diff --git a/src/deprecated.jl b/src/deprecated.jl new file mode 100644 index 00000000000..9c267e49c6d --- /dev/null +++ b/src/deprecated.jl @@ -0,0 +1,15 @@ +function inline!(val=true) + @warn("inline!(false/true) has been deprecated, since it was never working properly. + Use `display([Backend.Screen()], figure)` to open a window, + and otherwise any plot on any backend should be inlined into the plotpane/output by default.") +end + +function register_backend!(backend) + @warn("`register_backend!` is an internal deprecated function, which shouldn't be used outside Makie. + if you must really use this function, it's now `set_active_backend!(::Module)") +end + +function backend_display(args...) + @warn("`backend_display` is an internal deprecated function, which shouldn't be used outside Makie. + if you must really use this function, it's now just `display(::Backend.Screen, figlike)`") +end diff --git a/src/display.jl b/src/display.jl index 4862095c26f..4ae4adb23bf 100644 --- a/src/display.jl +++ b/src/display.jl @@ -1,39 +1,40 @@ -abstract type AbstractBackend end -function backend_display end - @enum ImageStorageFormat JuliaNative GLNative +update_state_before_display!(_) = nothing + +function backend_show end + """ Current backend """ -const current_backend = Ref{Union{Missing,AbstractBackend}}(missing) -const use_display = Ref{Bool}(true) - -function inline!(inline = true) - use_display[] = !inline - return -end +const CURRENT_BACKEND = Ref{Union{Missing, Module}}(missing) +current_backend() = CURRENT_BACKEND[] -function register_backend!(backend::AbstractBackend) - current_backend[] = backend +function set_active_backend!(backend::Union{Missing, Module}) + CURRENT_BACKEND[] = backend return end function push_screen!(scene::Scene, display) - # Ok lets leave a warning here until we fix CairoMakie! - @debug("Backend doesn't return screen from show methods. This needs fixing!") + error("$(display) not a valid Makie display.") end -function push_screen!(scene::Scene, display::AbstractDisplay) - if !any(x -> x === display, scene.current_screens) - push!(scene.current_screens, display) +""" + push_screen!(scene::Scene, screen::MakieScreen) + +Adds a screen to the scene and registeres a clean up event when screen closes. +""" +function push_screen!(scene::Scene, screen::MakieScreen) + if !any(x -> x === screen, scene.current_screens) + push!(scene.current_screens, screen) deregister = nothing deregister = on(events(scene).window_open, priority=typemax(Int)) do is_open # when screen closes, it should set the scene isopen event to false - # so that's when we can remove the display + # so that's when we can remove the screen if !is_open - filter!(x-> x !== display, scene.current_screens) - deregister !== nothing && off(deregister) + delete_screen!(scene, screen) + # deregister itself + !isnothing(deregister) && off(deregister) end return Consume(false) end @@ -41,58 +42,91 @@ function push_screen!(scene::Scene, display::AbstractDisplay) return end -function delete_screen!(scene::Scene, display::AbstractDisplay) - filter!(x-> x !== display, scene.current_screens) +""" + delete_screen!(scene::Scene, screen::MakieScreen) + +Removes screen from scene and cleans up screen +""" +function delete_screen!(scene::Scene, screen::MakieScreen) + delete!(screen, scene) + empty!(screen) + filter!(x-> x !== screen, scene.current_screens) return end -function backend_display(s::FigureLike; kw...) - update_state_before_display!(s) - backend_display(current_backend[], get_scene(s); kw...) +function set_screen_config!(backend::Module, new_values) + key = nameof(backend) + backend_defaults = CURRENT_DEFAULT_THEME[key] + bkeys = keys(backend_defaults) + for (k, v) in pairs(new_values) + if !(k in bkeys) + error("$k is not a valid screen config. Applicable options: $(keys(backend_defaults)). For help, check `?$(backend).ScreenCofig`") + end + backend_defaults[k] = v + end + return backend_defaults end -function backend_display(::Missing, ::Scene; kw...) - error(""" - No backend available! - Make sure to also `import/using` a backend (GLMakie, CairoMakie, WGLMakie). - - If you imported GLMakie, it may have not built correctly. - In that case, try `]build GLMakie` and watch out for any warnings. - """) +function merge_screen_config(::Type{Config}, screen_config_kw) where Config + backend = parentmodule(Config) + key = nameof(backend) + backend_defaults = CURRENT_DEFAULT_THEME[key] + kw_nt = values(screen_config_kw) + arguments = map(fieldnames(Config)) do name + if haskey(kw_nt, name) + return getfield(kw_nt, name) + else + return to_value(backend_defaults[name]) + end + end + return Config(arguments...) end -update_state_before_display!(_) = nothing +""" + Base.display(figlike::FigureLike; backend=current_backend(), screen_config...) -function Base.display(fig::FigureLike; kw...) - scene = get_scene(fig) - if !use_display[] - update_state_before_display!(fig) - return Core.invoke(display, Tuple{Any}, scene) - else - screen = backend_display(fig; kw...) - push_screen!(scene, screen) - return screen +Displays the figurelike in a window or the browser, depending on the backend. + +The parameters for `screen_config` are backend dependend, +see `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options. +""" +function Base.display(figlike::FigureLike; backend=current_backend(), screen_config...) + if ismissing(backend) + error(""" + No backend available! + Make sure to also `import/using` a backend (GLMakie, CairoMakie, WGLMakie). + + If you imported GLMakie, it may have not built correctly. + In that case, try `]build GLMakie` and watch out for any warnings. + """) end + screen = backend.Screen(get_scene(figlike); screen_config...) + return display(screen, figlike) end -function Base.showable(mime::MIME{M}, scene::Scene) where M - backend_showable(current_backend[], mime, scene) -end -# ambig -function Base.showable(mime::MIME"application/json", scene::Scene) - backend_showable(current_backend[], mime, scene) -end -function Base.showable(mime::MIME{M}, fig::FigureLike) where M - backend_showable(current_backend[], mime, get_scene(fig)) -end -# ambig -function Base.showable(mime::MIME"application/json", fig::FigureLike) - backend_showable(current_backend[], mime, get_scene(fig)) +# Backends overload display(::Backend.Screen, scene::Scene), while Makie overloads the below, +# so that they don't need to worry +# about stuff like `update_state_before_display!` +function Base.display(screen::MakieScreen, figlike::FigureLike; display_attributes...) + update_state_before_display!(figlike) + scene = get_scene(figlike) + display(screen, scene; display_attributes...) + return screen end -function backend_showable(::Backend, ::Mime, ::Scene) where {Backend, Mime <: MIME} - hasmethod(backend_show, Tuple{Backend, IO, Mime, Scene}) +function _backend_showable(mime::MIME{SYM}) where SYM + Backend = current_backend() + if ismissing(Backend) + return Symbol("text/plain") == SYM + end + return backend_showable(Backend.Screen, mime) end +Base.showable(mime::MIME, fig::FigureLike) = _backend_showable(mime) + +# need to define this to resolve ambiguoity issue +Base.showable(mime::MIME"application/json", fig::FigureLike) = _backend_showable(mime) + +backend_showable(@nospecialize(screen), @nospecialize(mime)) = false # fallback show when no backend is selected function backend_show(backend, io::IO, ::MIME"text/plain", scene::Scene) @@ -107,91 +141,36 @@ function backend_show(backend, io::IO, ::MIME"text/plain", scene::Scene) return end -function Base.show(io::IO, ::MIME"text/plain", scene::Scene; kw...) - show(io, scene; kw...) +function Base.show(io::IO, ::MIME"text/plain", scene::Scene) + show(io, scene) end function Base.show(io::IO, m::MIME, figlike::FigureLike) - ioc = IOContext(io, :full_fidelity => true) - update_state_before_display!(figlike) - backend_show(current_backend[], ioc, m, get_scene(figlike)) - return -end - -""" - Stepper(scene, path; format = :jpg) - -Creates a Stepper for generating progressive plot examples. - -Each "step" is saved as a separate file in the folder -pointed to by `path`, and the format is customizable by -`format`, which can be any output type your backend supports. - -Notice that the relevant `Makie.step!` is not -exported and should be accessed by module name. -""" -mutable struct FolderStepper - figlike::FigureLike - folder::String - format::Symbol - step::Int -end - -mutable struct RamStepper - figlike::FigureLike - images::Vector{Matrix{RGBf}} - format::Symbol -end - -Stepper(figlike::FigureLike, path::String, step::Int; format=:png) = FolderStepper(figlike, path, format, step) -Stepper(figlike::FigureLike; format=:png) = RamStepper(figlike, Matrix{RGBf}[], format) - -function Stepper(figlike::FigureLike, path::String; format = :png) - ispath(path) || mkpath(path) - FolderStepper(figlike, path, format, 1) -end - -""" - step!(s::Stepper) - -steps through a `Makie.Stepper` and outputs a file with filename `filename-step.jpg`. -This is useful for generating progressive plot examples. -""" -function step!(s::FolderStepper) - FileIO.save(joinpath(s.folder, basename(s.folder) * "-$(s.step).$(s.format)"), s.figlike) - s.step += 1 - return s -end - -function step!(s::RamStepper) - img = convert(Matrix{RGBf}, colorbuffer(s.figlike)) - push!(s.images, img) - return s -end - -function FileIO.save(dir::String, s::RamStepper) - if !isdir(dir) - mkpath(dir) - end - for (i, img) in enumerate(s.images) - FileIO.save(joinpath(dir, "step-$i.$(s.format)"), img) + scene = get_scene(figlike) + backend = current_backend() + # get current screen the scene is already displayed on, or create a new screen + screen = getscreen(scene, backend) do + # only update fig if not already displayed + update_state_before_display!(figlike) + return backend.Screen(scene, io, m) end + backend_show(screen, io, m, scene) + return end -format2mime(::Type{FileIO.format"PNG"}) = MIME("image/png") -format2mime(::Type{FileIO.format"SVG"}) = MIME("image/svg+xml") +format2mime(::Type{FileIO.format"PNG"}) = MIME("image/png") +format2mime(::Type{FileIO.format"SVG"}) = MIME("image/svg+xml") format2mime(::Type{FileIO.format"JPEG"}) = MIME("image/jpeg") format2mime(::Type{FileIO.format"TIFF"}) = MIME("image/tiff") format2mime(::Type{FileIO.format"BMP"}) = MIME("image/bmp") -format2mime(::Type{FileIO.format"PDF"}) = MIME("application/pdf") -format2mime(::Type{FileIO.format"TEX"}) = MIME("application/x-tex") -format2mime(::Type{FileIO.format"EPS"}) = MIME("application/postscript") +format2mime(::Type{FileIO.format"PDF"}) = MIME("application/pdf") +format2mime(::Type{FileIO.format"TEX"}) = MIME("application/x-tex") +format2mime(::Type{FileIO.format"EPS"}) = MIME("application/postscript") format2mime(::Type{FileIO.format"HTML"}) = MIME("text/html") filetype(::FileIO.File{F}) where F = F # Allow format to be overridden with first argument - """ FileIO.save(filename, scene; resolution = size(scene), pt_per_unit = 0.75, px_per_unit = 1.0) @@ -223,14 +202,13 @@ end function FileIO.save( file::FileIO.Formatted, fig::FigureLike; resolution = size(get_scene(fig)), - pt_per_unit = 0.75, - px_per_unit = 1.0, + backend = current_backend(), + screen_config... ) scene = get_scene(fig) if resolution != size(scene) resize!(scene, resolution) end - filename = FileIO.filename(file) # Delete previous file if it exists and query only the file string for type. # We overwrite existing files anyway, so this doesn't change the behavior. @@ -240,113 +218,24 @@ function FileIO.save( isfile(filename) && rm(filename) # query the filetype only from the file extension F = filetype(file) - - open(filename, "w") do s - iocontext = IOContext(s, - :full_fidelity => true, - :pt_per_unit => pt_per_unit, - :px_per_unit => px_per_unit - ) - show(iocontext, format2mime(F), fig) + mime = format2mime(F) + open(filename, "w") do io + # If the scene already got displayed, we get the current screen its displayed on + # Else, we create a new scene and update the state of the fig + screen = getscreen(scene, backend) do + update_state_before_display!(fig) + return backend.Screen(scene, io, mime; screen_config...) + end + backend_show(screen, io, mime, scene) end end +# Methods are used in backends to unwrap raw_io(io::IO) = io raw_io(io::IOContext) = raw_io(io.io) -""" - record_events(f, scene::Scene, path::String) - -Records all window events that happen while executing function `f` -for `scene` and serializes them to `path`. -""" -function record_events(f, scene::Scene, path::String) - display(scene) - result = Vector{Pair{Float64, Pair{Symbol, Any}}}() - for field in fieldnames(Events) - # These are not Observables - (field == :mousebuttonstate || field == :keyboardstate) && continue - on(getfield(scene.events, field), priority = typemax(Int)) do value - value = isa(value, Set) ? copy(value) : value - push!(result, time() => (field => value)) - return Consume(false) - end - end - f() - open(path, "w") do io - serialize(io, result) - end -end - - -""" - replay_events(f, scene::Scene, path::String) - replay_events(scene::Scene, path::String) - -Replays the serialized events recorded with `record_events` in `path` in `scene`. -""" -replay_events(scene::Scene, path::String) = replay_events(()-> nothing, scene, path) -function replay_events(f, scene::Scene, path::String) - events = open(io-> deserialize(io), path) - sort!(events, by = first) - for i in 1:length(events) - t1, (field, value) = events[i] - (field == :mousebuttonstate || field == :keyboardstate) && continue - Base.invokelatest() do - getfield(scene.events, field)[] = value - end - f() - if i < length(events) - t2, (field, value) = events[i + 1] - # min sleep time 0.001 - if (t2 - t1 > 0.001) - sleep(t2 - t1) - else - yield() - end - end - end -end - -struct RecordEvents - scene::Scene - path::String -end - -Base.display(re::RecordEvents) = display(re.scene) - -struct VideoStream - io - process - screen - path::String -end - -""" - VideoStream(scene::Scene; framerate = 24, visible=false, connect=false) - -Returns a stream and a buffer that you can use, which don't allocate for new frames. -Use [`recordframe!(stream)`](@ref) to add new video frames to the stream, and -[`save(path, stream)`](@ref) to save the video. - -* visible=false: make window visible or not -* connect=false: connect window events or not -""" -function VideoStream(fig::FigureLike; framerate::Integer=24, visible=false, connect=false) - #codec = `-codec:v libvpx -quality good -cpu-used 0 -b:v 500k -qmin 10 -qmax 42 -maxrate 500k -bufsize 1000k -threads 8` - dir = mktempdir() - path = joinpath(dir, "$(gensym(:video)).mkv") - scene = get_scene(fig) - screen = backend_display(fig; start_renderloop=false, visible=visible, connect=connect) - _xdim, _ydim = size(scene) - xdim = iseven(_xdim) ? _xdim : _xdim + 1 - ydim = iseven(_ydim) ? _ydim : _ydim + 1 - process = @ffmpeg_env open(`$(FFMPEG.ffmpeg) -framerate $(framerate) -loglevel quiet -f rawvideo -pixel_format rgb24 -r $framerate -s:v $(xdim)x$(ydim) -i pipe:0 -vf vflip -y $path`, "w") - return VideoStream(process.in, process, screen, abspath(path)) -end - # This has to be overloaded by the backend for its screen type. -function colorbuffer(x::AbstractScreen) +function colorbuffer(x::MakieScreen) error("colorbuffer not implemented for screen $(typeof(x))") end @@ -364,25 +253,37 @@ function jl_to_gl_format(image) return bufc else reverse!(image; dims=1) - return collect(PermutedDimsArray(image, (2, 1))) + return PermutedDimsArray(image, (2, 1)) end end # less specific for overloading by backends -function colorbuffer(screen::Any, format::ImageStorageFormat = JuliaNative) +function colorbuffer(screen::MakieScreen, format::ImageStorageFormat) image = colorbuffer(screen) if format == GLNative - if string(typeof(screen)) == "GLMakie.Screen" - @warn "Inefficient re-conversion back to GLNative buffer format. Update GLMakie to support direct buffer access" maxlog=1 - end return jl_to_gl_format(image) elseif format == JuliaNative return image end end +function getscreen(f::Function, scene::Scene, backend::Module) + screen = getscreen(scene) + if !isnothing(screen) && parentmodule(typeof(screen)) == backend + return screen + end + if ismissing(backend) + error(""" + You have not loaded a backend. Please load one (`using GLMakie` or `using CairoMakie`) + before trying to render a Scene. + """) + else + return f() + end +end + """ - colorbuffer(scene, format::ImageStorageFormat = JuliaNative) + colorbuffer(scene, format::ImageStorageFormat = JuliaNative; backend=current_backend(), screen_config...) colorbuffer(screen, format::ImageStorageFormat = JuliaNative) Returns the content of the given scene or screen rasterised to a Matrix of @@ -392,222 +293,29 @@ or RGBA. - `format = JuliaNative` : Returns a buffer in the format of standard julia images (dims permuted and one reversed) - `format = GLNative` : Returns a more efficient format buffer for GLMakie which can be directly used in FFMPEG without conversion +- `screen_config`: Backend dependend, look up via `?Backend.Screen`/`Base.doc(Backend.Screen)` """ -function colorbuffer(fig::FigureLike, format::ImageStorageFormat = JuliaNative) +function colorbuffer(fig::FigureLike, format::ImageStorageFormat = JuliaNative; backend = current_backend(), screen_config...) scene = get_scene(fig) - screen = getscreen(scene) - if isnothing(screen) - if ismissing(current_backend[]) - error(""" - You have not loaded a backend. Please load one (`using GLMakie` or `using CairoMakie`) - before trying to render a Scene. - """) - else - return colorbuffer(backend_display(fig; visible=false, start_renderloop=false, connect=false), format) - end + screen = getscreen(scene, backend) do + screen = backend.Screen(scene, format; start_renderloop=false, visible=false, screen_config...) + display(screen, fig; connect=false) + return screen end return colorbuffer(screen, format) end -""" - recordframe!(io::VideoStream) - -Adds a video frame to the VideoStream `io`. -""" -function recordframe!(io::VideoStream) - frame = convert(Matrix{RGB{N0f8}}, colorbuffer(io.screen, GLNative)) - _xdim, _ydim = size(frame) - if isodd(_xdim) || isodd(_ydim) - xdim = iseven(_xdim) ? _xdim : _xdim + 1 - ydim = iseven(_ydim) ? _ydim : _ydim + 1 - padded = fill(zero(eltype(frame)), (xdim, ydim)) - padded[1:_xdim, 1:_ydim] = frame - frame = padded - end - write(io.io, frame) +# Fallback for any backend that will just use colorbuffer to write out an image +function backend_show(screen::MakieScreen, io::IO, m::MIME"image/png", scene::Scene) + display(screen, scene; connect=false) + img = colorbuffer(screen) + FileIO.save(FileIO.Stream{FileIO.format"PNG"}(Makie.raw_io(io)), img) return end -""" - save(path::String, io::VideoStream[; kwargs...]) - -Flushes the video stream and converts the file to the extension found in `path`, -which can be one of the following: -- `.mkv` (the default, doesn't need to convert) -- `.mp4` (good for Web, most supported format) -- `.webm` (smallest file size) -- `.gif` (largest file size for the same quality) - -`.mp4` and `.mk4` are marginally bigger and `.gif`s are up to -6 times bigger with the same quality! - -See the docs of [`VideoStream`](@ref) for how to create a VideoStream. -If you want a simpler interface, consider using [`record`](@ref). - -### Keyword Arguments: -- `framerate = 24`: The target framerate. -- `compression = 0`: Controls the video compression with `0` being lossless and - `51` being the highest compression. Note that `compression = 0` - only works with `.mp4` if `profile = high444`. -- `profile = "high422"`: A ffmpeg compatible profile. Currently only applies to - `.mp4`. If you have issues playing a video, try - `profile = "high"` or `profile = "main"`. -- `pixel_format = "yuv420p"`: A ffmpeg compatible pixel format (pix_fmt). Currently - only applies to `.mp4`. Defaults to `yuv444p` for - `profile = high444`. -""" -function save( - path::String, io::VideoStream; - framerate::Int = 24, compression = 20, profile = "high422", - pixel_format = profile == "high444" ? "yuv444p" : "yuv420p" - ) - - close(io.process) - wait(io.process) - p, typ = splitext(path) - if typ == ".mkv" - cp(io.path, path, force=true) - else - mktempdir() do dir - out = joinpath(dir, "out$(typ)") - if typ == ".mp4" - # ffmpeg_exe(`-loglevel quiet -i $(io.path) -crf $compression -c:v libx264 -preset slow -r $framerate -pix_fmt yuv420p -c:a libvo_aacenc -b:a 128k -y $out`) - ffmpeg_exe(`-loglevel quiet -i $(io.path) -crf $compression -c:v libx264 -preset slow -r $framerate -profile:v $profile -pix_fmt $pixel_format -c:a libvo_aacenc -b:a 128k -y $out`) - elseif typ == ".webm" - ffmpeg_exe(`-loglevel quiet -i $(io.path) -crf $compression -c:v libvpx-vp9 -threads 16 -b:v 2000k -c:a libvorbis -threads 16 -r $framerate -vf scale=iw:ih -y $out`) - elseif typ == ".gif" - filters = "fps=$framerate,scale=iw:ih:flags=lanczos" - palette_path = dirname(io.path) - pname = joinpath(palette_path, "palette.bmp") - isfile(pname) && rm(pname, force = true) - ffmpeg_exe(`-loglevel quiet -i $(io.path) -vf "$filters,palettegen" -y $pname`) - ffmpeg_exe(`-loglevel quiet -i $(io.path) -i $pname -lavfi "$filters [x]; [x][1:v] paletteuse" -y $out`) - rm(pname, force = true) - else - rm(io.path) - error("Video type $typ not known") - end - cp(out, path, force=true) - end - end - rm(io.path) - return path -end - -""" - record(func, figure, path; framerate = 24, compression = 20, kwargs...) - record(func, figure, path, iter; framerate = 24, compression = 20, kwargs...) - -The first signature provides `func` with a VideoStream, which it should call -`recordframe!(io)` on when recording a frame. - -The second signature iterates `iter`, calling `recordframe!(io)` internally -after calling `func` with the current iteration element. - -Both notations require a Figure, FigureAxisPlot or Scene `figure` to work. - -The animation is then saved to `path`, with the format determined by `path`'s -extension. Allowable extensions are: -- `.mkv` (the default, doesn't need to convert) -- `.mp4` (good for Web, most supported format) -- `.webm` (smallest file size) -- `.gif` (largest file size for the same quality) - -`.mp4` and `.mk4` are marginally bigger than `webm` and `.gif`s are up to -6 times bigger with the same quality! - -The `compression` argument controls the compression ratio; `51` is the -highest compression, and `0` or `1` is the lowest (with `0` being lossless). - -Typical usage patterns would look like: - -```julia -record(figure, "video.mp4", itr) do i - func(i) # or some other manipulation of the figure -end -``` - -or, for more tweakability, - -```julia -record(figure, "test.gif") do io - for i = 1:100 - func!(figure) # animate figure - recordframe!(io) # record a new frame - end -end -``` - -If you want a more tweakable interface, consider using [`VideoStream`](@ref) and -[`save`](@ref). - -## Extended help -### Examples - -```julia -fig, ax, p = lines(rand(10)) -record(fig, "test.gif") do io - for i in 1:255 - p[:color] = RGBf(i/255, (255 - i)/255, 0) # animate figure - recordframe!(io) - end -end -``` -or -```julia -fig, ax, p = lines(rand(10)) -record(fig, "test.gif", 1:255) do i - p[:color] = RGBf(i/255, (255 - i)/255, 0) # animate figure -end -``` - -### Keyword Arguments: -- `framerate = 24`: The target framerate. -- `compression = 0`: Controls the video compression with `0` being lossless and - `51` being the highest compression. Note that `compression = 0` - only works with `.mp4` if `profile = high444`. -- `profile = "high422`: A ffmpeg compatible profile. Currently only applies to - `.mp4`. If you have issues playing a video, try - `profile = "high"` or `profile = "main"`. -- `pixel_format = "yuv420p"`: A ffmpeg compatible pixel format (pix_fmt). Currently - only applies to `.mp4`. Defaults to `yuv444p` for - `profile = high444`. -""" -function record(func, figlike, path; framerate::Int = 24, kwargs...) - io = Record(func, figlike, framerate = framerate) - save(path, io, framerate = framerate; kwargs...) -end - -function Record(func, figlike; framerate=24) - io = VideoStream(figlike; framerate = framerate) - func(io) - return io -end - -function record(func, figlike, path, iter; framerate::Int = 24, kwargs...) - io = Record(func, figlike, iter; framerate=framerate) - save(path, io, framerate = framerate; kwargs...) -end - -function Record(func, figlike, iter; framerate::Int = 24) - io = VideoStream(figlike; framerate=framerate) - for i in iter - func(i) - recordframe!(io) - @debug "Recording" progress=i/length(iter) - yield() - end - return io -end - -function Base.show(io::IO, ::MIME"text/html", vs::VideoStream) - mktempdir() do dir - path = save(joinpath(dir, "video.mp4"), vs) - print( - io, - """""" - ) - end +function backend_show(screen::MakieScreen, io::IO, m::MIME"image/jpeg", scene::Scene) + display(screen, scene; connect=false) + img = colorbuffer(scene) + FileIO.save(FileIO.Stream{FileIO.format"JPEG"}(Makie.raw_io(io)), img) + return end diff --git a/src/documentation/documentation.jl b/src/documentation/documentation.jl index a2b2f8967d5..ce83835cd4c 100644 --- a/src/documentation/documentation.jl +++ b/src/documentation/documentation.jl @@ -32,7 +32,7 @@ function _help(io::IO, input::Type{T}; extended = false) where T <: AbstractPlot str = to_string(input) # Print docstrings - println(io, Base.Docs.doc(func)) + println(io, Base.doc(func)) # Arguments help_arguments(io, func) diff --git a/src/event-recorder.jl b/src/event-recorder.jl new file mode 100644 index 00000000000..4fe6eab44d7 --- /dev/null +++ b/src/event-recorder.jl @@ -0,0 +1,60 @@ + +""" +record_events(f, scene::Scene, path::String) + +Records all window events that happen while executing function `f` +for `scene` and serializes them to `path`. +""" +function record_events(f, scene::Scene, path::String) + display(scene) + result = Vector{Pair{Float64,Pair{Symbol,Any}}}() + for field in fieldnames(Events) + # These are not Observables + (field == :mousebuttonstate || field == :keyboardstate) && continue + on(getfield(scene.events, field); priority=typemax(Int)) do value + value = isa(value, Set) ? copy(value) : value + push!(result, time() => (field => value)) + return Consume(false) + end + end + f() + open(path, "w") do io + return serialize(io, result) + end +end + +""" +replay_events(f, scene::Scene, path::String) +replay_events(scene::Scene, path::String) + +Replays the serialized events recorded with `record_events` in `path` in `scene`. +""" +replay_events(scene::Scene, path::String) = replay_events(() -> nothing, scene, path) +function replay_events(f, scene::Scene, path::String) + events = open(io -> deserialize(io), path) + sort!(events; by=first) + for i in 1:length(events) + t1, (field, value) = events[i] + (field == :mousebuttonstate || field == :keyboardstate) && continue + Base.invokelatest() do + return getfield(scene.events, field)[] = value + end + f() + if i < length(events) + t2, (field, value) = events[i + 1] + # min sleep time 0.001 + if (t2 - t1 > 0.001) + sleep(t2 - t1) + else + yield() + end + end + end +end + +struct RecordEvents + scene::Scene + path::String +end + +Base.display(re::RecordEvents) = display(re.scene) diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl new file mode 100644 index 00000000000..b354985d6e6 --- /dev/null +++ b/src/ffmpeg-util.jl @@ -0,0 +1,271 @@ +# TODO move to something like FFMPEGUtil.jl ? + +""" +- `format = "mkv"`: The format of the video. If a path is present, will be inferred form the file extension. + Can be one of the following: + * `"mkv"` (open standard, the default) + * `"mp4"` (good for Web, most supported format) + * `"webm"` (smallest file size) + * `"gif"` (largest file size for the same quality) + + `mp4` and `mk4` are marginally bigger than `webm`. `gif`s can be significantly (as much as + 6x) larger with worse quality (due to the limited color palette) and only should be used + as a last resort, for playing in a context where videos aren't supported. +- `framerate = 24`: The target framerate. +- `compression = 20`: Controls the video compression via `ffmpeg`'s `-crf` option, with + smaller numbers giving higher quality and larger file sizes (lower compression), and and + higher numbers giving lower quality and smaller file sizes (higher compression). The + minimum value is `0` (lossless encoding). + - For `mp4`, `51` is the maximum. Note that `compression = 0` only works with `mp4` if + `profile = high444`. + - For `webm`, `63` is the maximum. + - `compression` has no effect on `mkv` and `gif` outputs. +- `profile = "high422"`: A ffmpeg compatible profile. Currently only applies to `mp4`. If +you have issues playing a video, try `profile = "high"` or `profile = "main"`. +- `pixel_format = "yuv420p"`: A ffmpeg compatible pixel format (`-pix_fmt`). Currently only +applies to `mp4`. Defaults to `yuv444p` for `profile = high444`. + + !!! warning + `profile` and `pixel_format` are only used when `format` is `"mp4"`; a warning will be issued if `format` + is not `"mp4"` and those two arguments are not `nothing`. Similarly, `compression` is only + valid when `format` is `"mp4"` or `"webm"`. +""" +struct VideoStreamOptions + format::String + framerate::Int + compression::Union{Nothing,Int} + profile::Union{Nothing,String} + pixel_format::Union{Nothing,String} + + loglevel::String + input::String + rawvideo::Bool + + function VideoStreamOptions( + format::AbstractString, framerate::Integer, compression, profile, + pixel_format, loglevel::String, input::String, rawvideo::Bool=true) + + if format == "mp4" + (profile === nothing) && (profile = "high422") + (pixel_format === nothing) && (pixel_format = (profile == "high444" ? "yuv444p" : "yuv420p")) + end + + if format in ("mp4", "webm") + (compression === nothing) && (compression = 20) + end + + # items are name, value, allowed_formats + allowed_kwargs = [("compression", compression, ("mp4", "webm")), + ("profile", profile, ("mp4",)), + ("pixel_format", pixel_format, ("mp4",))] + + for (name, value, allowed_formats) in allowed_kwargs + if !(format in allowed_formats) && value !== nothing + @warn("""`$name`, with value $(repr(value)) + was passed as a keyword argument to `record` or `VideoStream`, + which only has an effect when the output video's format is one of: $(collect(allowed_formats)). + But the actual video format was $(repr(format)). + Keyword arg `$name` will be ignored. + """) + end + end + + if !((input == "pipe:0" || isfile(input))) + error("file needs to be \"pipe:0\" or a valid file path") + end + + loglevels = Set([ + "quiet", + "panic", + "fatal", + "error", + "warning", + "info", + "verbose", + "debug"]) + + if !(loglevel in loglevels) + error("loglevel needs to be one of $(loglevels)") + end + return new(format, framerate, compression, profile, pixel_format, loglevel, input, rawvideo) + end +end + +function VideoStreamOptions(; format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loglevel="quiet", input="pipe:0", rawvideo=true) + return VideoStreamOptions(format, framerate, compression, profile, pixel_format, loglevel, input, rawvideo) +end + +function to_ffmpeg_cmd(vso::VideoStreamOptions, xdim::Integer=0, ydim::Integer=0) + # explanation of ffmpeg args. note that the order of args is important; args pertaining + # to the input have to go before -i and args pertaining to the output have to go after. + # -y: "yes", overwrite any existing without confirmation + # -f: format is raw video (from frames) + # -framerate: set the input framerate + # -pixel_format: the buffer we're sending ffmpeg via stdin contains rgb24 pixels + # -s:v: sets the dimensions of the input to xdim × ydim + # -i: read input from stdin (pipe:0) + # -vf: video filter for the output + # -c:v, -c:a: video and audio codec, respectively + # -b:v, -b:a: video and audio bitrate, respectively + # -crf: "constant rate factor", the lower the better the quality (0 is lossless, 51 is + # maximum compression) + # -pix_fmt: (mp4 only) the output pixel format + # -profile:v: (mp4 only) the output video profile + # -an: no audio in output + (format, framerate, compression, profile, pixel_format) = (vso.format, vso.framerate, vso.compression, vso.profile, vso.pixel_format) + + cpu_cores = length(Sys.cpu_info()) + ffmpeg_prefix = ` + $(FFMPEG.ffmpeg) + -y + -loglevel $(vso.loglevel) + -threads $(cpu_cores)` + # options for raw video input via pipe + if vso.rawvideo + ffmpeg_prefix = ` + $ffmpeg_prefix + -framerate $(framerate) + -pixel_format rgb24 + -f rawvideo` + end + xdim > 0 && ydim > 0 && (ffmpeg_prefix = `$ffmpeg_prefix -s:v $(xdim)x$(ydim)`) + ffmpeg_prefix = `$ffmpeg_prefix -r $(framerate) -i $(vso.input)` + # Sigh, it's not easy to specify this for all + if vso.rawvideo && format != "gif" + ffmpeg_prefix = `$ffmpeg_prefix -vf vflip` + end + ffmpeg_options = if format == "mkv" + `-an` + elseif format == "mp4" + `-profile:v $(profile) + -crf $(compression) + -preset slow + -c:v libx264 + -pix_fmt $(pixel_format) + -an + ` + elseif format == "webm" + # this may need improvement, see here: https://trac.ffmpeg.org/wiki/Encode/VP9 + `-crf $(compression) + -c:v libvpx-vp9 + -b:v 0 + -an + ` + elseif format == "gif" + # from https://superuser.com/a/556031 + # avoids creating a PNG file of the palette + if vso.rawvideo + `-vf "vflip,fps=$(framerate),scale=$(xdim):-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"` + else + `-vf "fps=$(framerate),scale=$(xdim):-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"` + end + else + error("Video type $(format) not known") + end + + return `$(ffmpeg_prefix) $(ffmpeg_options)` +end + + +struct VideoStream + io::Base.PipeEndpoint + process::Base.Process + screen::MakieScreen + buffer::Matrix{RGB{N0f8}} + path::String + options::VideoStreamOptions +end + +""" + VideoStream(fig::FigureLike; + format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loglevel="quiet", + visible=false, connect=false, backend=current_backend(), + screen_config...) + +Returns a `VideoStream` which can pipe new frames into the ffmpeg process with few allocations via [`recordframe!(stream)`](@ref). +When done, use [`save(path, stream)`](@ref) to write the video out to a file. + +# Arguments + +## Video options + +$(Base.doc(VideoStreamOptions)) + +## Backend options + +* `backend=current_backend()`: backend used to record frames +* `visible=false`: make window visible or not +* `connect=false`: connect window events or not +* `screen_config...`: See `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options that can be passed and forwarded to the backend. +""" +function VideoStream(fig::FigureLike; + format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loglevel="quiet", + visible=false, connect=false, backend=current_backend(), + screen_config...) + + dir = mktempdir() + path = joinpath(dir, "$(gensym(:video)).$(format)") + scene = get_scene(fig) + screen = backend.Screen(scene, GLNative; visible=visible, start_renderloop=false, screen_config...) + display(screen, fig; connect=connect) + _xdim, _ydim = size(screen) + xdim = iseven(_xdim) ? _xdim : _xdim + 1 + ydim = iseven(_ydim) ? _ydim : _ydim + 1 + buffer = Matrix{RGB{N0f8}}(undef, xdim, ydim) + vso = VideoStreamOptions(format, framerate, compression, profile, pixel_format, loglevel, "pipe:0", true) + cmd = to_ffmpeg_cmd(vso, xdim, ydim) + process = @ffmpeg_env open(`$cmd $path`, "w") + return VideoStream(process.in, process, screen, buffer, abspath(path), vso) +end + +""" + recordframe!(io::VideoStream) + +Adds a video frame to the VideoStream `io`. +""" +function recordframe!(io::VideoStream) + glnative = colorbuffer(io.screen, GLNative) + # Make no copy if already Matrix{RGB{N0f8}} + # There may be a 1px padding for odd dimensions + xdim, ydim = size(glnative) + copy!(view(io.buffer, 1:xdim, 1:ydim), glnative) + write(io.io, io.buffer) + return +end + +""" + save(path::String, io::VideoStream) + +Flushes the video stream and saves it to `path`. Ideally, `path`'s file extension is the same as +the format that the `VideoStream` was created with (e.g., if created with format "mp4" then +`path`'s file extension must be ".mp4"). Otherwise, the video will get converted to the target format. +If using [`record`](@ref) then this is handled for you, +as the `VideoStream`'s format is deduced from the file extension of the path passed to `record`. +""" +function save(path::String, io::VideoStream; video_options...) + close(io.process) + wait(io.process) + p, typ = splitext(path) + video_fmt = io.options.format + if typ != ".$(video_fmt)" || !isempty(video_options) + # Maybe warn? + convert_video(io.path, path; video_options...) + else + cp(io.path, path; force=true) + end + rm(io.path) + return path +end + +function convert_video(input_path, output_path; video_options...) + p, typ = splitext(output_path) + format = lstrip(typ, '.') + vso = VideoStreamOptions(; format=format, input=input_path, rawvideo=false, video_options...) + cmd = to_ffmpeg_cmd(vso) + @ffmpeg_env run(`$cmd $output_path`) +end + +function extract_frames(video, frame_folder; loglevel="quiet") + path = joinpath(frame_folder, "frame%04d.png") + FFMPEG.ffmpeg_exe(`-loglevel $(loglevel) -i $video -y $path`) +end diff --git a/src/interaction/events.jl b/src/interaction/events.jl index abd97bf4589..c50db19b3e6 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -34,8 +34,8 @@ function connect_screen(scene::Scene, screen) return end -to_native(window::AbstractScreen) = error("to_native(window) not implemented for $(typeof(window)).") -disconnect!(window::AbstractScreen, signal) = disconnect!(to_native(window), signal) +to_native(window::MakieScreen) = error("to_native(window) not implemented for $(typeof(window)).") +disconnect!(window::MakieScreen, signal) = disconnect!(to_native(window), signal) function disconnect_screen(scene::Scene, screen) delete_screen!(scene, screen) diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index 0d49dbc8527..f62afd9a049 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -241,7 +241,6 @@ function projectionmatrix(viewmatrix, limits, eyepos, radius, azim, elev, angle, if viewmode in (:fitzoom, :stretch) points = decompose(Point3f, limits) - # @show points projpoints = Ref(pm * viewmatrix) .* to_ndim.(Point4f, points, 1) maxx = maximum(x -> abs(x[1] / x[4]), projpoints) diff --git a/src/recording.jl b/src/recording.jl new file mode 100644 index 00000000000..6ed450aa730 --- /dev/null +++ b/src/recording.jl @@ -0,0 +1,185 @@ + +""" + Stepper(scene, path; format = :jpg) + +Creates a Stepper for generating progressive plot examples. + +Each "step" is saved as a separate file in the folder +pointed to by `path`, and the format is customizable by +`format`, which can be any output type your backend supports. + +Notice that the relevant `Makie.step!` is not +exported and should be accessed by module name. +""" +mutable struct FolderStepper + figlike::FigureLike + screen::MakieScreen + folder::String + format::Symbol + step::Int +end + +mutable struct RamStepper + figlike::FigureLike + screen::MakieScreen + images::Vector{Matrix{RGBf}} + format::Symbol +end + +function Stepper(figlike::FigureLike; backend=current_backend(), format=:png, visible=false, connect=false, srceen_kw...) + screen = backend.Screen(get_scene(figlike), JuliaNative; visible=visible, start_renderloop=false, srceen_kw...) + display(screen, figlike; connect=connect) + return RamStepper(figlike, screen, Matrix{RGBf}[], format) +end + +function Stepper(figlike::FigureLike, path::String, step::Int; format=:png, backend=current_backend(), visible=false, connect=false, screen_config...) + screen = backend.Screen(get_scene(figlike), JuliaNative; visible=visible, start_renderloop=false, screen_config...) + display(screen, figlike; connect=connect) + return FolderStepper(figlike, screen, path, format, step) +end + +function Stepper(figlike::FigureLike, path::String; kw...) + ispath(path) || mkpath(path) + return Stepper(figlike, path, 1; kw...) +end + +""" + step!(s::Stepper) + +steps through a `Makie.Stepper` and outputs a file with filename `filename-step.jpg`. +This is useful for generating progressive plot examples. +""" +function step!(s::FolderStepper) + update_state_before_display!(s.figlike) + FileIO.save(joinpath(s.folder, basename(s.folder) * "-$(s.step).$(s.format)"), colorbuffer(s.screen)) + s.step += 1 + return s +end + +function step!(s::RamStepper) + update_state_before_display!(s.figlike) + img = convert(Matrix{RGBf}, colorbuffer(s.screen)) + push!(s.images, img) + return s +end + +function FileIO.save(dir::String, s::RamStepper) + if !isdir(dir) + mkpath(dir) + end + for (i, img) in enumerate(s.images) + FileIO.save(joinpath(dir, "step-$i.$(s.format)"), img) + end +end + +""" + record(func, figurelike, path; backend=current_backend(), kwargs...) + record(func, figurelike, path, iter; backend=current_backend(), kwargs...) + +The first signature provides `func` with a VideoStream, which it should call +`recordframe!(io)` on when recording a frame. + +The second signature iterates `iter`, calling `recordframe!(io)` internally +after calling `func` with the current iteration element. + +Both notations require a Figure, FigureAxisPlot or Scene `figure` to work. +The animation is then saved to `path`, with the format determined by `path`'s +extension. + +Under the hood, `record` is just `video_io = Record(func, figurelike, [iter]; same_kw...); save(path, video_io)`. +`Record` can be used directly as well to do the saving at a later point, or to inline a video directly into a Notebook (the video supports, `show(video_io, "text/html")` for that purpose). + +# Options one can pass via `kwargs...`: + +* `backend::Module = current_backend()`: set the backend to write out video, can be set to `CairoMakie`, `GLMakie`, `WGLMakie`, `RPRMakie`. +### Backend options + +See `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options that can be passed and forwarded to the backend. + +### Video options + +$(Base.doc(VideoStreamOptions)) + +# Typical usage + +```julia +record(figure, "video.mp4", itr) do i + func(i) # or some other manipulation of the figure +end +``` +or, for more tweakability, +```julia +record(figure, "test.gif") do io + for i = 1:100 + func!(figure) # animate figure + recordframe!(io) # record a new frame + end +end +``` +If you want a more tweakable interface, consider using [`VideoStream`](@ref) and +[`save`](@ref). +## Extended help +### Examples +```julia +fig, ax, p = lines(rand(10)) +record(fig, "test.gif") do io + for i in 1:255 + p[:color] = RGBf(i/255, (255 - i)/255, 0) # animate figure + recordframe!(io) + end +end +``` +or +```julia +fig, ax, p = lines(rand(10)) +record(fig, "test.gif", 1:255) do i + p[:color] = RGBf(i/255, (255 - i)/255, 0) # animate figure +end +``` +""" +function record(func, figlike::FigureLike, path::AbstractString; kw_args...) + format = lstrip(splitext(path)[2], '.') + io = Record(func, figlike; format=format, kw_args...) + save(path, io) +end + +function record(func, figlike::FigureLike, path::AbstractString, iter; kw_args...) + format = lstrip(splitext(path)[2], '.') + io = Record(func, figlike, iter; format=format, kw_args...) + save(path, io) +end + + +""" + Record(func, figlike, [iter]; kw_args...) + +Check [`Makie.record`](@ref) for documentation. +""" +function Record(func, figlike; kw_args...) + io = VideoStream(figlike; kw_args...) + func(io) + return io +end + +function Record(func, figlike, iter; kw_args...) + io = VideoStream(figlike; kw_args...) + for i in iter + func(i) + recordframe!(io) + @debug "Recording" progress=i/length(iter) + yield() + end + return io +end + +function Base.show(io::IO, ::MIME"text/html", vs::VideoStream) + mktempdir() do dir + path = save(joinpath(dir, "video.mp4"), vs) + print( + io, + """""" + ) + end +end diff --git a/src/scenes.jl b/src/scenes.jl index 04a0c909f6c..29633cf39d5 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -106,7 +106,7 @@ mutable struct Scene <: AbstractScene """ The Screens which the Scene is displayed to. """ - current_screens::Vector{AbstractScreen} + current_screens::Vector{MakieScreen} # Attributes backgroundcolor::Observable{RGBAf} @@ -154,7 +154,7 @@ function Scene(; plots::Vector{AbstractPlot} = AbstractPlot[], theme::Attributes = Attributes(), children::Vector{Scene} = Scene[], - current_screens::Vector{AbstractScreen} = AbstractScreen[], + current_screens::Vector{MakieScreen} = MakieScreen[], parent = nothing, visible = Observable(true), ssao = SSAO(), @@ -363,7 +363,7 @@ function Base.empty!(scene::Scene) empty!(scene.plots) empty!(scene.theme) - merge!(scene.theme, _current_default_theme) + merge!(scene.theme, CURRENT_DEFAULT_THEME) disconnect!(scene.camera) scene.camera_controls = EmptyCamera() @@ -410,10 +410,10 @@ function Base.push!(scene::Scene, plot::AbstractPlot) end end -function Base.delete!(screen::AbstractScreen, ::Scene, ::AbstractPlot) +function Base.delete!(screen::MakieScreen, ::Scene, ::AbstractPlot) @warn "Deleting plots not implemented for backend: $(typeof(screen))" end -function Base.delete!(screen::AbstractScreen, ::Scene) +function Base.delete!(screen::MakieScreen, ::Scene) # This may not be necessary for every backed @debug "Deleting scenes not implemented for backend: $(typeof(screen))" end diff --git a/src/theming.jl b/src/theming.jl index 27fc6bec109..ea27dee7611 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -91,13 +91,58 @@ const minimal_default = Attributes( ), ambient = RGBf(0.55, 0.55, 0.55), lightposition = :eyeposition, - inspectable = true + inspectable = true, + + CairoMakie = Attributes( + px_per_unit = 1.0, + pt_per_unit = 0.75, + antialias = :best, + visible = true, + start_renderloop = false + ), + + GLMakie = Attributes( + # Renderloop + renderloop = automatic, + pause_renderloop = false, + vsync = false, + framerate = 30.0, + + # GLFW window attributes + float = false, + focus_on_show = false, + decorated = true, + title = "Makie", + fullscreen = false, + debugging = false, + monitor = nothing, + + # Postproccessor + oit = true, + fxaa = true, + ssao = false, + # This adjusts a factor in the rendering shaders for order independent + # transparency. This should be the same for all of them (within one rendering + # pipeline) otherwise depth "order" will be broken. + transparency_weight_scale = 1000f0 + ), + + WGLMakie = Attributes( + framerate = 30.0 + ), + + RPRMakie = Attributes( + iterations = 200, + resource = automatic, + plugin = automatic, + max_recursion = 10 + ) ) -const _current_default_theme = deepcopy(minimal_default) +const CURRENT_DEFAULT_THEME = deepcopy(minimal_default) function current_default_theme(; kw_args...) - return merge!(Attributes(kw_args), deepcopy(_current_default_theme)) + return merge!(Attributes(kw_args), deepcopy(CURRENT_DEFAULT_THEME)) end """ @@ -107,10 +152,10 @@ Set the global default theme to `theme` and add / override any attributes given as keyword arguments. """ function set_theme!(new_theme = Theme()::Attributes; kwargs...) - empty!(_current_default_theme) + empty!(CURRENT_DEFAULT_THEME) new_theme = merge!(deepcopy(new_theme), deepcopy(minimal_default)) new_theme = merge!(Theme(kwargs), new_theme) - merge!(_current_default_theme, new_theme) + merge!(CURRENT_DEFAULT_THEME, new_theme) return end @@ -152,7 +197,7 @@ Nested attributes are either also updated incrementally, or replaced if they are """ function update_theme!(with_theme = Theme()::Attributes; kwargs...) new_theme = merge!(with_theme, Theme(kwargs)) - _update_attrs!(_current_default_theme, new_theme) + _update_attrs!(CURRENT_DEFAULT_THEME, new_theme) return end diff --git a/test/Project.toml b/test/Project.toml index a245f204ca2..42c77e8b94a 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,8 +2,10 @@ CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -11,4 +13,3 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" diff --git a/test/events.jl b/test/events.jl index 6f6f83b598c..31b2b484eb8 100644 --- a/test/events.jl +++ b/test/events.jl @@ -1,6 +1,6 @@ using Makie: MouseButtonEvent, KeyEvent, Figure, Textbox using Makie: Not, And, Or -using InteractiveUtils: clipboard +using InteractiveUtils # rudimentary equality for tests Base.:(==)(l::Exclusively, r::Exclusively) = l.x == r.x @@ -126,7 +126,14 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right @test x | false == x end end - + # Okay, this is hacky, + # but we're not going to install a whole linux desktop environment on the CI just to test the clipboard + # (what the hell xclip, y u need all that) + @eval InteractiveUtils begin + const CLIP = Ref{String}() + clipboard(str::String) = (CLIP[] = str) + clipboard() = CLIP[] + end @testset "copy_paste" begin f = Figure(resolution=(640,480)) tb = Textbox(f[1,1], placeholder="Copy/paste into me") @@ -155,7 +162,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right @test tb.stored_string[] == "test string" - + # Refresh figure to test right control + v combination empty!(f) diff --git a/test/record.jl b/test/record.jl new file mode 100644 index 00000000000..c9a57e72126 --- /dev/null +++ b/test/record.jl @@ -0,0 +1,74 @@ +using Logging + +module VideoBackend + using Makie + struct Screen <: MakieScreen + size::Tuple{Int, Int} + end + Base.size(screen::Screen) = screen.size + Screen(scene::Scene, ::Makie.ImageStorageFormat; screen_config...) = Screen(size(scene)) + Makie.backend_showable(::Type{Screen}, ::MIME"text/html") = true + Makie.backend_showable(::Type{Screen}, ::MIME"image/png") = true + Makie.colorbuffer(screen::Screen) = zeros(RGBf, reverse(screen.size)...) + Base.display(::Screen, ::Scene; kw...) = nothing +end +Makie.set_active_backend!(VideoBackend) + +mktempdir() do tempdir + @testset "Video encoding" begin + n = 2 + x = 0:(n - 1) + fig, ax, _ = lines(x, zeros(size(x))) + # test for no throwing when encoding + @testset "Encoding" begin + for fmt in ("mkv", "mp4", "webm", "gif") + dst = joinpath(tempdir, "out.$fmt") + @test begin + record(fig, dst, 1:n) do i + lines!(ax, sin.(i .* x)) + return nothing + end + true + end + end + end + + # test that the proper warnings are thrown + @testset "Warnings" begin + function run_record(dst; kwargs...) + record(fig, dst, 1:n; kwargs...) do i + lines!(ax, sin.(i .* x)) + return nothing + end + end + + # kwarg => (value, (should_warn => format)) + warn_tests = [ + (:compression, 20, ["mkv", "gif"], ["mp4", "webm"]), + (:profile, "high422", ["mkv", "webm", "gif"], ["mp4"]), + ( + kwarg=:pixel_format, + value="yuv420p", + warn_fmts=["mkv", "webm", "gif"], + no_warn_fmts=["mp4"], + ), + ] + + for (kwarg, value, warn_fmts, no_warn_fmts) in warn_tests + kwargs = Dict(kwarg => value) + warning_re = Regex("^`$(kwarg)`, with value $(repr(value))") + + for fmt in warn_fmts + dst = joinpath(tempdir, "out.$fmt") + @test_logs (:warn, warning_re) run_record(dst; kwargs...) + end + + for fmt in no_warn_fmts + dst = joinpath(tempdir, "out.$fmt") + @test_logs min_level = Logging.Warn run_record(dst; kwargs...) + end + end + end + end +end +Makie.set_active_backend!(missing) diff --git a/test/runtests.jl b/test/runtests.jl index fe12cca7f4f..b59b768a46d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,6 +18,7 @@ using Makie: volume @test all(hi .>= (8,8,10)) end + include("record.jl") include("scenes.jl") include("conversions.jl") include("quaternions.jl")