Find position for color in Color Picker?

Hi. I had to catch some pneumonia to get time to use Codea again. I’m writing a color picker, and wondered if anyone has made a solution to find a cursor position for a given color in a color picker? The other way around is easier. :slight_smile:

I want to call picker:setColor(c) and I want the UI to find a suitable place in the picker where the color c exists.

Here is the code for my Picker.

Edit: Updated with a solution

--# Main
-- Picker

function setup()
   -- displayMode(FULLSCREEN)
    p = Picker(200)
    parameter.color("C", color(255,255,255), function (c)
        p:setColor(c)
    end)
end

function draw()
    background(p.color)
    translate(WIDTH/2,HEIGHT/2)
    p:draw()
end

function touched(touch)
    p:touched(touch)
end

--# Picker
Picker = class()

function Picker:init(size)
    self.size = size or 200
    self.m = matrix()
    self:createHexagon()
    self:createShadeSlider()
    self:touched(vec2(0,0)) -- set start color white
end

function Picker:hexVector(i)
    local a = i/6 * math.pi * 2 + math.pi/6
    return vec2(math.cos(a), math.sin(a)) * self.size    
end

function Picker:setColor(c)
    local rv = self:hexVector(4) -- where red is 0
    local gv = self:hexVector(6) -- where green is 0
    local bv = self:hexVector(2) -- where blue is 0
    
    local m = math.max(c.r, c.g, c.b)
    self:setShade(m)
    local p = (rv * (m - c.r) + gv * (m - c.g) + bv * (m - c.b))/255
    self:touchedHexagon(p)
end

function Picker:createHexagon()
    local vs, cs, colors = {}, {}, {
        color(255, 255, 255, 255),
        color(255, 0, 0, 255),
        color(255, 255, 0, 255),
        color(0, 255, 0, 255),
        color(0, 255, 255, 255),
        color(0, 0, 255, 255),
        color(255, 0, 255, 255)
    }

    for i=1,6 do
        table.insert(vs, vec2(0,0))
        table.insert(cs, colors[1])
        table.insert(vs, self:hexVector(i))
        table.insert(cs, colors[i+1])
        table.insert(vs, self:hexVector(i+1))
        table.insert(cs, colors[i+2] or colors[2])
    end
    self.hexagon = mesh()
    self.hexagon.vertices = vs
    self.hexagon.colors = cs
    self.colors = cs
    self.hexagon.shader = self:circleShader()
end
function Picker:updateCenterColor(c)
    local cs = self.colors
    for i=1,6*3,3 do
       cs[i] = c
    end
    self.hexagon.colors = cs
    self:touchedHexagon(self.pos)
end

function Picker:createShadeSlider()
    local x, y = self.size*(math.sqrt(3)/2)+10, self.size
    local w, h = 44, y*2
    self.sliderCenterX = x + w/2
    local vs = {
        vec2(x,y),vec2(x+w,y), vec2(x+w,y-h),
        vec2(x,y),vec2(x+w,y-h),vec2(x,y-h)
    }
    local bc, wc = color(0,0,0,255), color(255,255,255,255)
    self.slider = mesh()
    self.slider.vertices = vs
    self.slider.colors = {wc,wc,bc,wc,bc,bc}
    self.slider.shader = self:circleShader()
    self.bpos = vec2(x+w/2,y)
    self.bcolor = wc
end
function Picker:setShade(v)
    local p = vec2(self.sliderCenterX, -self.size + self.size*2*v/256)
    self:touchedShadeSlider(p)
end

function Picker:draw()
    self.m = modelMatrix()
    self.hexagon.shader.p = self.pos
    self.hexagon.shader.c = self.color
    self.hexagon:draw()
    self.slider.shader.p = self.bpos
    self.slider.shader.c = self.bcolor
    self.slider:draw()
end

function Picker:touched(touch)
    local p = vec2(touch.x-self.m[13], touch.y-self.m[14])
    
    if not self:touchedHexagon(p) then
        self:touchedShadeSlider(p)
    end
end

function Picker:touchedHexagon(p)
    local c = self:touchedMesh(p, self.hexagon)
    if c then
        self.color, self.pos = c, p
        return true
    end
end

function Picker:touchedShadeSlider(p)
    local c = self:touchedMesh(p, self.slider)
    if c then
        p.x = self.sliderCenterX
        self.bcolor, self.bpos = c, p
        self:updateCenterColor(c)
    end
end

function Picker:touchedMesh(p, h)
    for i=1,#h.vertices,3 do
        local a, b, c = h.vertices[i], h.vertices[i+1], h.vertices[i+2]
        local inside, u, v = insideTriangle(a,b,c,vec3(p.x,p.y,0))
        if inside then
            local d, e, f = h.colors[i]*255, h.colors[i+1]*255, h.colors[i+2]*255
            return (1-u-v)*d + u*f + v*e -- color in triangle
        end
    end
end

function Picker:circleShader()
    return shader([[
        uniform mat4 modelViewProjection;
        attribute vec4 position;
        varying highp vec2 vTexCoord;
        attribute vec4 color;
        varying lowp vec4 vColor;
        
        void main() {
            vTexCoord = position.xy;
            vColor = color;
            gl_Position = modelViewProjection * position;
        }
    ]],[[
        precision highp float;
        varying lowp vec4 vColor;
        varying highp vec2 vTexCoord;
        uniform vec2 p;
        uniform vec4 c;
        
        void main() {
            lowp vec4 col = vColor;
            float l = length(vTexCoord - p);
            if(l < 20.) {
                col = c;
            } else if(l < 22.) {
                // complementary brightness for circle border
                float y = 1. - (0.2126*col.x + 0.7152*col.y + 0.0722*col.z);
                col = vec4(y, y, y, 1.);
            }
            gl_FragColor = col;
        }
    ]])
end

--# Utils
-- texture coordinates for p in the triangle a,b,c
function uv(a, b, c, p)
    local v0 = c - a
    local v1 = b - a
    local v2 = p - a
    local dot00 = v0:dot(v0)
    local dot01 = v0:dot(v1)
    local dot02 = v0:dot(v2)
    local dot11 = v1:dot(v1)
    local dot12 = v1:dot(v2)
    
    -- Compute barycentric coordinates
    local invDenom = 1 / (dot00 * dot11 - dot01 * dot01)
    local u = (dot11 * dot02 - dot01 * dot12) * invDenom
    local v = (dot00 * dot12 - dot01 * dot02) * invDenom
    
    return u, v
end

function insideTriangle(a, b, c, p)
    local u, v = uv(a,b,c,p)
    return (u >= 0) and (v >= 0) and (u + v < 1), u, v
end

I haven’t looked at your code so haven’t seen whether you’re using a hexagon (which is correct) or circle (which isn’t) as the basis for your colour selector.

Here’s my code for setting the colour of a hexagonal picker:

function ColourWheel:setFromColour(rc)
   local c = color(rc.r,rc.g,rc.b,rc.a) -- clone the colour
    self.alpha = c.a -- set our alpha
    local x,y = math.min(c.r,c.g,c.b)/255,math.max(c.r,c.g,c.b)/255 -- get the range of the colour values (renormalised to [0,1])
    self.ratio = vec2(x,y)
    local i,j,ar
    if x == y then -- our colour was a grey, so we pick red as an arbitrary rim colour
        self.rimcolour = Colour.svg.Red
        self.angle = math.pi/3
    else
-- we got an actual colour, so we saturate the colours
        c.r = (c.r - x*255)/(y - x)
        c.g = (c.g - x*255)/(y - x)
        c.b = (c.b - x*255)/(y - x)
        c.a = 255
-- this is our rim colour
        self.rimcolour = c
-- next step is to locate that colour on the rim
-- this will determine how far along a particular side we are
        ar = (c.r + c.g + c.b)/255 - 1
-- we find the two primary/secondary colours on either side of it
        if c.r >= c.g and c.r >= c.b then
-- red is the dominant colour, so either we're red-yellow or red-purple
            i = 1
            if c.g >= c.b then
-- red-yellow
                j = 2
            else
-- red-purple
                j = 6
            end
        elseif c.g >= c.b then
-- green is the dominant colour (already discounted red)
            i = 3
            if c.b >= c.r then
-- green-cyan
                j = 4
            else
-- green-yellow
                j = 2
            end
        else
-- blue is the dominant colour
            i = 5
            if c.r >= c.g then
-- blue-purple
                j = 6
            else
-- blue-cyan
                j = 4
            end
        end
-- we linearly interpolate the angle between the starting and ending colours
-- to be honest, we should probably linearly interpolate the positions and then take the atan2 of that
        self.angle = (i*(1-ar) + ar*j)*math.pi/3
    end
-- now bake the colours into the mesh
    self.meshsqr:color(2,self.rimcolour)
    self.meshsqr:color(5,Colour.complement(self.rimcolour,false))
    self:setColour()
end

Thanks for getting me thinking in the right direction. :slight_smile:

My solution now looks like this where hexVector creates a vector that point to the colors in the hexagon where red/green or blue part is zero, so that they can be used to calculate the vector for a color. And setShade sets the color in the center of the hexagon. Don’t know if I’ve used the hexagon in the “correct” way, but I like it. :slight_smile:

function Picker:setColor(c)
    local rv = self:hexVector(4) -- where red is 0
    local gv = self:hexVector(6) -- where green is 0
    local bv = self:hexVector(2) -- where blue is 0
    
    local m = math.max(c.r, c.g, c.b)
    self:setShade(m)
    local p = (rv * (m - c.r) + gv * (m - c.g) + bv * (m - c.b))/255
    self:touchedHexagon(p)
end