Yet Another Color Picker edit
bll 2017-4-23 : I dislike the default color picker that comes with Tk, and the other implementations did not appeal to me. I wrote a simple HSV color picker. It's not difficult to make this work with RGB or HSL (and I believe I have code available if you want it), but I don't think I ever got CIELUV working. 2018-1-2: reordered pre-selectable colors. 2018-4-22: added HSL and RGB.
yacp.tcl accepts the initial color from the command line, and outputs the new color (or initial color if closed) to stdout.
Examples:tclsh yacp.tcl # defaults to HSV
tclsh yacp.tcl -model hsl
tclsh yacp.tcl -model rgb # (-mode dynamic) same as Tk's color picker.
tclsh yacp.tcl -model rgb -mode static '#80a0a0'
yacp.tcl#!/usr/bin/tclsh
#
# yet another color picker
#
# Copyright 2012-2018 Brad Lanam Walnut Creek CA USA
#
# Algorithms from:
# http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
# http://www.easyrgb.com/index.php?X=MATH&H=02#text2
# http://www.brucelindbloom.com/
# http://en.wikipedia.org/wiki/CIELUV
#
package require Tk 8.5-
lappend ::auto_path .
package require colorutils
variable vars
# When HSL is used, val=luminosity
# When RGB is used, hue=red, sat=green, val=blue
# Variables:
# rgbtextvar : the hex value variable [traced]
# base,{hue|sat|val} : the base value for creating pure colors
# height : the height of the color selection canvas
# width : the width of the color selection canvas.
# This should be set to 255 or 360
# selval,{hue|sat|val} : the selected value from the canvas [traced]
# This value is from 0 to <width>
# useval,{hue|sat|val} : the scaled value used internally
# dispval,{hue|sat|val} : the display value for the left side boxes
# Value is from 0 to <width>
# olddisp,{hue|sat|val} : the old display value. Used to check for changes.
# seltodispscale : the value to convert a selected value to a
# display value. The selected value is divided by the width
# of the canvas, then multiplied by this value.
# cvt,{hsv,hsl,rgb} : conversion factor
# ctype : HSV or HSL or RGB
# cvttype : int or double
#
proc _grabScreen { image } {
set pipe [open {|xwd -root -silent | convert xwd:- ppm:-} rb]
$image put [read $pipe]
close $pipe
}
proc _getPixel { } {
set buffer [image create photo]
_grabScreen $buffer
set data [$buffer get {*}[winfo pointerxy .]]
image delete $buffer
return $data
}
proc _hexValueChange { args } {
variable vars
if { [regexp {^#?[[:xdigit:]]{6}$} $vars(rgbtextvar)] } {
set vlist [colorutils::fromRgbText $vars(rgbtextvar) $vars(ctype)]
set nvlist [_createSelValues $vlist]
foreach {i k} {0 hue 1 sat 2 val} {
set vars(selval,$k) [lindex $nvlist $i]
set vars(olddisp,$k) -1
}
_setColors
}
}
proc _colorChange { args } {
_setColors
}
proc _drawMarker { cw x } {
variable vars
set rw [expr {round(1.0 / $vars(width.d))}]
set hh [expr {ceil(double($vars(height))/2.0)}]
$cw create rectangle \
$x 0 [expr {$rw + $x}] $hh \
-fill #ffffff -outline {}
$cw create rectangle \
$x $hh [expr {$rw + $x}] $vars(height) \
-fill #000000 -outline {}
}
proc _setColors { } {
variable vars
set w .
set rw [expr {round(1.0 / $vars(width.d))}]
foreach {k} {hue sat val} {
if { $vars(selval,$k) eq "" } {
return
}
# normalize the selected value so that mouse motion outside of the
# canvas doesn't create strange values.
_selTraceOff $k
if { $vars(selval,$k) < 0 } {
set vars(selval,$k) 0
}
if { $vars(selval,$k) > $vars(width) } {
set vars(selval,$k) $vars(width)
}
_selTraceOn $k
set vars(dispval,$k) [expr {int(round(double($vars(selval,$k)) / \
$vars(width.d) * $vars(cvt,$vars(ctype)) * $vars(seltodispscale)))}]
set vars(useval,$k) [expr {double($vars(selval,$k)) /
$vars(width.d) * $vars(cvt,$vars(ctype))}]
if { $vars(cvttype) eq "int" } {
set vars(useval,$k) [expr {int($vars(useval,$k))}]
}
}
if { $vars(olddisp,hue) != $vars(dispval,hue) } {
.canv_hue delete all
for {set x 0} {$x < $vars(width)} {incr x 1} {
if { $vars(selval,hue) == $x } {
_drawMarker .canv_hue $x
} else {
set x1 [expr {double($x) / $vars(width.d) * $vars(cvt,$vars(ctype))}]
if { $vars(cvttype) eq "int" } {
set x1 [expr {int(round($x1))}]
}
set c [colorutils::toRgbText \
[list $x1 $vars(base,sat) $vars(base,val)] $vars(ctype)]
.canv_hue create rectangle \
$x 0 [expr {$rw + $x}] $vars(height) \
-fill $c -outline {}
}
}
}
if { $vars(mode) ne "dynamic" } {
set h $vars(base,hue)
} else {
set h $vars(useval,hue)
}
.canv_sat delete all
.canv_val delete all
for {set x 0} {$x < $vars(width)} {incr x 1} {
set x1 [expr {double($x) / $vars(width.d) * $vars(cvt,$vars(ctype))}]
set v1 $vars(useval,val)
if { $vars(mode) ne "dynamic" } {
set v1 $vars(base,val)
}
if { $vars(cvttype) eq "int" } {
set x1 [expr {int(round($x1))}]
}
if { $vars(selval,sat) == $x } {
_drawMarker .canv_sat $x
} else {
set c [colorutils::toRgbText [list $h $x1 $v1] $vars(ctype)]
.canv_sat create rectangle $x 0 [expr {$rw+$x}] $vars(height) \
-fill $c -outline {}
}
set s1 $vars(useval,sat)
if { $vars(mode) ne "dynamic" } {
set s1 $vars(base,sat)
}
if { $vars(selval,val) == $x } {
_drawMarker .canv_val $x
} else {
set c [colorutils::toRgbText [list $h $s1 $x1] $vars(ctype)]
.canv_val create rectangle $x 0 [expr {$rw+$x}] $vars(height) \
-fill $c -outline {}
}
}
set h $vars(useval,hue)
# main sample display
set c [colorutils::toRgbText \
[list $vars(useval,hue) $vars(useval,sat) $vars(useval,val)] $vars(ctype)]
set sc $vars(sampcanv)
$sc configure -background $c
_hexTraceOff
set vars(rgbtextvar) $c
_hexTraceOn
foreach {k} {hue sat val} {
set vars(olddisp,$k) $vars(dispval,$k)
}
}
proc _exit { selflag val } {
variable vars
if { $selflag } {
puts [colorutils::toRgbText [list $vars(useval,hue) $vars(useval,sat) \
$vars(useval,val)] $vars(ctype)]
} else {
puts $val
}
destroy .
exit
}
proc _createSelValues { vlist } {
variable vars
set nvlist {}
# build a new list of values for hue, sat, and val.
for {set i 0} {$i < 3} {incr i} {
set x1 [expr {round([lindex $vlist $i] /
$vars(cvt,$vars(ctype)) * $vars(width.d))}]
lappend nvlist $x1
}
return $nvlist
}
proc _startMotion { key v } {
variable vars
set vars(selval,$key) $v
set vars(motion$key) true
}
proc _endMotion { key } {
variable vars
set vars(motion$key) false
}
proc _doMotion { key v } {
variable vars
if { $vars(motion$key) && $v >= 0 && $v <= $vars(width) } {
set vars(selval,$key) $v
}
}
proc _selTraceOn { key } {
variable vars
if { [trace info variable vars(selval,$key)] eq "" } {
trace add variable vars(selval,$key) write _colorChange
}
}
proc _selTraceOff { key } {
variable vars
trace remove variable vars(selval,$key) write _colorChange
}
proc _hexTraceOn { } {
variable vars
if { [trace info variable vars(rgbtextvar)] eq "" } {
trace add variable vars(rgbtextvar) write _hexValueChange
}
}
proc _hexTraceOff { } {
variable vars
trace remove variable vars(rgbtextvar) write _hexValueChange
}
proc _preselColor { hexstr } {
variable vars
set vars(rgbtextvar) $hexstr
}
proc chooseColor { val } {
variable vars
set vlist [colorutils::fromRgbText $val $vars(ctype)]
_hexTraceOff
set vars(rgbtextvar) $val
_hexTraceOn
set vars(useval,hue) [lindex $vlist 0]
set vars(useval,sat) [lindex $vlist 1]
set vars(useval,val) [lindex $vlist 2]
foreach {k} {hue sat val} {
# scale from use to selected.
_selTraceOff $k
set vars(selval,$k) [expr {round($vars(useval,$k) /
$vars(cvt,$vars(ctype)) * $vars(width.d))}]
_selTraceOn $k
set vars(dispval,$k) [expr {int(round(double($vars(selval,$k)) / \
$vars(width.d) / $vars(cvt,$vars(ctype)) * $vars(seltodispscale)))}]
}
set w .
wm title $w {Choose Color}
set tw {}
foreach {k} {hue sat val} {
canvas .canv_$k -width $vars(width) \
-height $vars(height) -borderwidth 1 \
-relief sunken -highlightthickness 0
grid .canv_$k -in $w -sticky {} -padx 5p -pady 3p
}
set vars(sampcanv) [frame $tw.samp \
-borderwidth 1 \
-relief sunken \
-highlightthickness 0]
grid $vars(sampcanv) -in $w -column 1 -row 0 -rowspan 2 \
-sticky news -padx 5p -pady 3p
set vars(hexdisp) $tw.hexdisp
ttk::entry $vars(hexdisp) -width 8 -textvariable vars(rgbtextvar) \
-justify left \
-font fixedentry
grid $vars(hexdisp) -in $w -column 1 -row 2 \
-sticky ew -padx 5p
ttk::frame $tw.bot
grid $tw.bot -in $w -sticky ew -columnspan 2
ttk::frame $tw.presel
ttk::frame $tw.bb
grid $tw.presel $tw.bb -in $tw.bot -sticky e
grid configure $tw.presel -sticky ew
grid columnconfigure $tw.bot 0 -weight 1
ttk::button $tw.close -text Close \
-command [list _exit false $val] \
-style Menu.TButton
ttk::button $tw.select -text Select \
-command [list _exit true $val] \
-style Menu.TButton
grid $tw.select $tw.close -in $tw.bb -padx 2p -pady 1p
ttk::frame $tw.pref1
# as HSV
# magenta = fuschia
foreach {h s v colname} [list \
1.0 1.0 1.0 red \
0.083333 1.0 0.5 brown \
0.083333 1.0 1.0 orange \
0.16666 1.0 1.0 yellow \
0.33333 1.0 1.0 green \
0.5 1.0 1.0 cyan \
0.66666 1.0 1.0 blue \
0.75 1.0 1.0 purple \
0.83333 1.0 1.0 magenta \
0.0 0.0 0.0 black \
0.0 0.0 1.0 white \
] {
set c [colorutils::toRgbText [list $h $s $v] HSV]
set pw [frame $tw.pre$c \
-background $c -relief raised \
-borderwidth 2 \
-width $vars(pwidth) \
-height $vars(pwidth)]
lappend presellist $pw
bind $pw <ButtonRelease-1> [list _preselColor $c]
}
ttk::frame $tw.pref2
grid $tw.pref1 {*}$presellist $tw.pref2 -in $tw.presel -padx 2p -pady 3p
grid configure $tw.pref1 -sticky ew
grid columnconfigure $tw.presel 0 -weight 1
grid columnconfigure $tw.presel 12 -weight 1
update
_setColors
foreach {key} {hue sat val} {
bind .canv_$key <ButtonPress-1> "_startMotion $key %x"
bind .canv_$key <ButtonRelease-1> "_endMotion $key"
bind .canv_$key <Motion> "_doMotion $key %x"
}
wm protocol . WM_DELETE_WINDOW "_exit false $val"
}
proc main { } {
variable vars
variable opts
set vars(rgbtextvar) ""
set vars(ctype) HSV ; # default
set vars(mode) dynamic
# preselect width/height
set vars(pwidth) [expr {2*[font measure default 0]}]
# width of canvas color selection bar
set vars(width) [expr {36*[font measure default 0]}]
set vars(width.d) [expr {double($vars(width))}]
# height of canvas color selection bar
set vars(height) [expr {2*[font measure default 0]}]
foreach {k} {hue sat val} {
set vars(motion$k) false
}
set aidx 0
set didx {}
set a0 {}
foreach {a} $::argv {
switch -exact -- $a {
-model {
set didx $a
}
-mode {
set didx $a
}
default {
if { $didx ne {} } {
set vars($didx) $a
set didx {}
} else {
set a0 $a
}
}
}
incr aidx
}
if { [info exists vars(-model)] } {
set vars(ctype) [string toupper $vars(-model)]
if { $vars(ctype) ne "HSV" &&
$vars(ctype) ne "HSL" &&
$vars(ctype) ne "RGB" } {
set vars(ctype) HSV
}
}
if { [info exists vars(-mode)] } {
set vars(mode) $vars(-mode)
if { $vars(mode) ne "dynamic" && $vars(mode) ne "static" } {
set vars(mode) dynamic
}
}
if { $vars(ctype) ne "RGB" } {
set vars(mode) dynamic
}
set vars(cvt,$vars(ctype)) 1.0
set vars(cvttype) double
# base values are for creating "pure" colors:
# fully saturated, neither light nor dark.
set base 1.0
if { $vars(ctype) eq "RGB" } {
set base 0
}
foreach {k} {hue sat val} {
set vars(base,$k) $base
set vars(olddisp,$k) -1
}
if { $vars(ctype) eq "HSL" } {
set vars(base,val) 0.5
}
set vars(seltodispscale) 360.0
if { $vars(ctype) eq "RGB" } {
set vars(seltodispscale) 255.0
set vars(cvt,RGB) 255.0
set vars(cvttype) int
}
if { [regexp {^#[[:xdigit:]]{6}$} $a0] } {
chooseColor $a0
} else {
chooseColor {#ffffff}
}
}
main
colorutils.tcl#!/usr/bin/tclsh
#
# Copyright 2012-2016 Brad Lanam Walnut Creek CA USA
# MIT License
#
namespace eval ::colorutils {
variable vars
set vars(onethird) [expr {1.0/3.0}]
set vars(twothirds) [expr {2.0/3.0}]
proc rgbToHexStr { rgblist } {
foreach {i} {0 1 2} {
set v [lindex $rgblist $i]
if { ! [regexp {^\d{1,3}$} $v] || $v < 0 || $v > 255} {
return ""
}
}
set t [format #%02x%02x%02x {*}$rgblist]
return $t
}
proc hexStrToRgb { rgbtext } {
# rgbtext is format: #aabbcc or aabbcc
if { [regexp {^#?[[:xdigit:]]{6}$} $rgbtext] } {
scan $rgbtext "#%2x%2x%2x" r g b
return [list $r $g $b]
} else {
return false
}
}
proc toRgbText { vlist {type HSV} } {
variable vars
set proc ${type}toRGB
set rgblist [$proc $vlist]
return [rgbToHexStr $rgblist]
}
proc fromRgbText { rgbtext {type HSV} } {
variable vars
set proc RGBto${type}
set rgblist [hexStrToRgb $rgbtext]
if { $rgblist != false } {
return [$proc $rgblist]
}
return false
}
# RGB
proc RGBtoRGB { rgblist } {
return $rgblist
}
# HSV
proc RGBtoHSV { rgblist } {
set r [expr {double([lindex $rgblist 0]) / 255.0}]
set g [expr {double([lindex $rgblist 1]) / 255.0}]
set b [expr {double([lindex $rgblist 2]) / 255.0}]
set max [expr {max($r, $g, $b)}]
set min [expr {min($r, $g, $b)}]
set h $max
set s $max
set v $max
set d [expr {$max - $min}]
if {$max == 0} {
set s 0
} else {
set s [expr {$d / $max}]
}
if {$max == $min} {
set h 0
} else {
if { $max == $r } {
set t 0.0
if { $g < $b } {
set t 6.0
}
set h [expr {($g - $b) / $d + $t}]
}
if { $max == $g } {
set h [expr {($b - $r) / $d + 2.0}]
}
if { $max == $b } {
set h [expr {($r - $g) / $d + 4.0}]
}
set h [expr {$h / 6.0}]
}
return [list $h $s $v]
}
proc HSVtoRGB { hsvlist } {
set h [lindex $hsvlist 0]
set s [lindex $hsvlist 1]
set v [lindex $hsvlist 2]
set i [expr {int($h * 6.0)}]
set f [expr {$h * 6.0 - $i}]
set p [expr {$v * (1.0 - $s)}]
set q [expr {$v * (1.0 - $f * $s)}]
set t [expr {$v * (1.0 - (1.0 - $f) * $s)}]
set im6 [expr {$i % 6}]
if { $im6 == 0 } {
set r $v; set g $t; set b $p
}
if { $im6 == 1 } {
set r $q; set g $v; set b $p
}
if { $im6 == 2 } {
set r $p; set g $v; set b $t
}
if { $im6 == 3 } {
set r $p; set g $q; set b $v
}
if { $im6 == 4 } {
set r $t; set g $p; set b $v
}
if { $im6 == 5 } {
set r $v; set g $p; set b $q
}
return [list [expr {int(round($r * 255.0))}] \
[expr {int(round($g * 255.0))}] \
[expr {int(round($b * 255.0))}]]
}
# HSL
proc RGBtoHSL { rgblist } {
set r [expr {double([lindex $rgblist 0]) / 255.0}]
set g [expr {double([lindex $rgblist 1]) / 255.0}]
set b [expr {double([lindex $rgblist 2]) / 255.0}]
set max [expr {max($r, $g, $b)}]
set min [expr {min($r, $g, $b)}]
set l [expr {($max + $min) / 2.0}]
if { $max == $min } {
set h 0.0
set s 0.0
} else {
set d [expr {$max - $min}]
if { $l > 0.5 } {
set s [expr {$d / (2.0 - $max - $min)}]
} else {
set s [expr {$d / ($max + $min)}]
}
if {$max == $r } {
set g2 0.0
if {$g < $b} { set g2 6.0 }
set h [expr {($g - $b) / $d + $g2}]
} elseif {$max == $g} {
set h [expr {($b - $r) / $d + 2.0}]
} elseif {$max == $b} {
set h [expr {($r - $g) / $d + 4.0}]
}
set h [expr {$h / 6.0}]
}
return [list $h $s $l]
}
# used by HSLtoRGB()
proc hue2rgb {p q t} {
variable vars
if {$t < 0.0} { set t [expr {$t + 1.0}] }
if {$t > 1.0} { set t [expr {$t - 1.0}] }
if {$t < [expr 1.0/6.0]} { return [expr {$p + ($q - $p) * 6.0 * $t}] }
if {$t < 0.5} { return $q }
if {$t < $vars(twothirds)} {
return [expr {$p + ($q - $p) * ($vars(twothirds) - $t) * 6.0}]
}
return $p
}
proc HSLtoRGB { hsllist } {
variable vars
lassign $hsllist h s l
if {$s == 0} {
set r $l
set g $l
set b $l
} else {
if { $l < 0.5 } {
set q [expr {$l * (1.0 + $s)}]
} else {
set q [expr {$l + $s - ($l * $s)}]
}
set p [expr {2.0 * $l - $q}]
set r [hue2rgb $p $q [expr {$h + $vars(onethird)}]]
set g [hue2rgb $p $q $h]
set b [hue2rgb $p $q [expr {$h - $vars(onethird)}]]
}
return [list [expr {round($r * 255.0)}] \
[expr {round($g * 255.0)}] \
[expr {round($b * 255.0)}]];
}
}
package provide colorutils 1.1