module ColourApproximations using Colors using GLMakie using Makie.GeometryBasics: Rect, HyperRectangle using Observables # Utilities const colours3bit = # xterm colours Colorant[colorant"rgb(0, 0, 0)", colorant"rgb(205, 0, 0)", colorant"rgb(0, 205, 0)", colorant"rgb(205, 205, 0)", colorant"rgb(0, 0, 238)", colorant"rgb(205, 0, 205)", colorant"rgb(0, 205, 205)", colorant"rgb(229, 229, 229)"] const colours4bit = # xterm again vcat(colours3bit, Colorant[colorant"rgb(127, 127, 127)", colorant"rgb(255, 0, 0)", colorant"rgb(0, 252, 0)", colorant"rgb(255, 255, 0)", colorant"rgb(0, 0, 252)", colorant"rgb(255, 0, 255)", colorant"rgb(0, 255, 255)", colorant"rgb(255, 255, 255)"]) const colour6cube = Colorant[RGB(r, g, b) for r in range(0, 1, length=6) for g in range(0, 1, length=6) for b in range(0, 1, length=6)] const colour24greys = Colorant[RGB(w, w, w) for w in range(0, 1, length=24)] const colours8bit = vcat(colours4bit, colour6cube, colour24greys) const coloursets = [Symbol("3-bit") => colours3bit, Symbol("4-bit") => colours4bit, Symbol("8-bit") => colours8bit] nearestcolour(metric::Colors.DifferenceMetric, options::Vector{<:Colorant}, colour::Colorant) = options[argmin(o -> colordiff(options[o], colour; metric), axes(options, 1))] const nearcolourcache = Dict{Colors.DifferenceMetric, Dict{Symbol, Dict{Colorant, Colorant}}}() function nearestcolour(metric::Colors.DifferenceMetric, options::Symbol) if !haskey(nearcolourcache, metric) nearcolourcache[metric] = Dict{Symbol, Dict{Colorant, Colorant}}() end if !haskey(nearcolourcache[metric], options) nearcolourcache[metric][options] = Dict{Colorant, Colorant}() end relevantcache = nearcolourcache[metric][options] colourset = coloursets[findfirst(s -> first(s) == options, coloursets)] |> last function (c::Colorant) cached = get(relevantcache, c, nothing) if isnothing(cached) cached = relevantcache[c] = nearestcolour(metric, colourset, c) end cached end end # Extra colour differences struct DE_RGB <: Colors.EuclideanDifferenceMetric{RGB} end struct DE_HSL <: Colors.EuclideanDifferenceMetric{HSL} end colour_distance_metrics = ["CIEΔE2000" => DE_2000(), "CIEΔE94" => DE_94(), "BFD" => DE_BFD(), "CMC" => DE_CMC(), "JPC79" => DE_JPC79(), "l2 LAB" => DE_AB(), "l2 DIN99" => DE_DIN99(), "l2 DIN99d" => DE_DIN99d(), "l2 DIN99o" => DE_DIN99o(), "l2 RGB" => DE_RGB(), "l2 HSL" => DE_HSL()] # The control observables saturation = Observable(1.0) x_granularity = Observable(256) y_granularity = Observable(128) diffmetric = Observable{Colors.DifferenceMetric}(last(first(colour_distance_metrics))) # Plot setup fig = Figure() controlrow1 = fig[1,1] = GridLayout() xgrad_slider = Slider(controlrow1[1,2], range=32:32:1024, startvalue=x_granularity[]) Label(controlrow1[1,1], text="X divisions") ygrad_slider = Slider(controlrow1[1,4], range=16:16:512, startvalue=y_granularity[]) Label(controlrow1[1,3], text="Y divisions") metric_slider = Slider(controlrow1[1,6], range=1:length(colour_distance_metrics)) Label(controlrow1[1,5], text=@lift(colour_distance_metrics[$(metric_slider.value)] |> first)) connect!(x_granularity, xgrad_slider.value) connect!(y_granularity, ygrad_slider.value) # colsize!(controlrow1, 1, Auto(true, 0.0)) # colsize!(controlrow1, 2, Fixed(120)) # colsize!(controlrow1, 3, Auto(true, 0.0)) # colsize!(controlrow1, 4, Fixed(120)) # colsize!(controlrow1, 5, Auto(true, 0.0)) # colsize!(controlrow1, 6, Fixed(160)) ax = Axis(fig[2,1]) hidedecorations!(ax) hidespines!(ax) controlrow2 = fig[3,1] = GridLayout() saturation_slider = Slider(controlrow2[1,2], range = 0:0.01:1, startvalue=saturation[]) Label(controlrow2[1,1], text="Saturation") colourset_slider = Slider(controlrow2[1,4], range=1:length(coloursets)) Label(controlrow2[1,3], text=@lift(coloursets[$(colourset_slider.value)] |> first |> string)) round_colours = Toggle(controlrow2[1,6]) Label(controlrow2[1,5], text="Round") colsize!(controlrow2, 1, Auto(true, 0.0)) colsize!(controlrow2, 2, Auto(false, 0.8)) colsize!(controlrow2, 3, Auto(true, 0.0)) colsize!(controlrow2, 4, Fixed(100)) colsize!(controlrow2, 5, Auto(true, 0.0)) colsize!(controlrow2, 6, Fixed(60)) # Plotting connect!(saturation, saturation_slider.value) map!(diffmetric, metric_slider.value) do v colour_distance_metrics[v] |> last end roundfun = Observable{Function}(identity) map!(roundfun, diffmetric, colourset_slider.value) do metric, cset nearestcolour(metric, coloursets[cset] |> first) end rect_list = Observable(Vector{HyperRectangle}()) rect_list = map(x_granularity, y_granularity) do x, y [Rect(i, j, 1, 1) for i in 1:x for j in 1:y] end colour_list_unprocessed = map(saturation) do s [HSL(h, s, l) for h in range(0, 360, length=x_granularity[]) for l in range(0, 1, length=y_granularity[])] end colour_list = Observable(Vector{Colorant}()) map!(colour_list, colour_list_unprocessed, roundfun, round_colours.active) do clist, cround, croundp if croundp cround.(clist) else clist end end # The main event p = poly!(ax, rect_list, color = colour_list) on(rect_list, priority=1) do delete!(ax, p) p = poly!(ax, rect_list, color = colour_list) end fig end