JuliaGraphics / ColorTypes.jl

Basic color definitions and traits
Other
77 stars 35 forks source link

[RFC] Distinguishing between `AbstractGray{T}` and `Color{T, 1}` #230

Closed kimikage closed 3 years ago

kimikage commented 3 years ago

Currently, AbstractGray{T} is the alias for Color{T, 1}, but this is a bit weird. (cf. https://github.com/JuliaGraphics/ColorTypes.jl/issues/145#issuecomment-573071205)

Here is an extreme example.

struct Lightness{T} <: Color{T, 1}
     v::T
end

struct Darkness{T} <: Color{T, 1}
     v::T
end

ColorTypes.gray(c::Lightness) = c.v
ColorTypes.gray(c::Darkness) = oneunit(c.v) - c.v
ColorTypes.comp1(c::Darkness) = c.v
julia> Darkness(0.2) == Lightness(0.8) # OK
true

julia> Darkness(0.2) > Lightness(0.3) # Uh, yes, that's correct.
true

julia> Darkness(0.2) == 0.8 # What? Oh, hmm.
true 

Colored tones such as sepia and cyanotype can be prevented from strange reinterpretations by not implementing gray(). However, when you implement gray() for a "gray" scale, strange things happen.

That is, color types which have difference between comp1 and gray are inherently incompatible with AbstractGray.

IMO, AbstractGray should be an abstract type as well as AbsractRGB.

kimikage commented 3 years ago

This is an experimental type (it does not conform to any specific standard).

struct Cyanotype{T <: Real} <: Color{T,1}
   value::T
end
function Base.convert(::Type{Cout}, c::C) where {Cout <: AbstractRGB, T, C <: Cyanotype{T}}
    r = max(1 - 1.5 * c.value, 0)
    g = 0.98 - 0.7 * c.value
    b = 0.88 - 0.5 * c.value^2
    Cout(T(r), T(g), T(b))
end

It would be a step forward if ColorTypes.jl and its downstream packages did not misidentify this as a "gray" type. And that's technically easy to do.

cyano

kimikage commented 3 years ago

The following are obviously not equal, as they belong to different color spaces.

Cyanotype{Float32}(0.5) == Gray{Float32}(0.5) # -> false

We treat gray as a real number.

0.5 == Gray{Float32}(0.5) # -> true

However, it is debatable whether the following are equal. The following equality relation itself is reasonable, but by accepting it, the transitivity is no longer satisfied.

Cyanotype{Float32}(0.5) == 0.5 # -> false?

If we don't allow comparisons with real numbers, then defining real is also debatable. However, since real is called explicitly, the conversion to real numbers seems to be fine.

real(Cyanotype{Float32}(0.5)) # -> 0.5f0?

More problematic is the convert. If we support conversion to real numbers, something strange should happen with the default constructor for color types.

convert(Real, Cyanotype{Float32}(0.5)) # -> MethodError?

IMO, only AbstractGray should support "inter"-conversion with real numbers.