luxe-colorpicker/colorpicker.wren
2021-11-27 17:14:50 +01:00

970 lines
No EOL
36 KiB
Text

import "luxe: world" for Entity, UIClear, UILayoutMode, UIBehave, UIContain, Assets, Material, UI, UIEvent, UILayoutBehave, TextAlign
import "luxe: ui" for Control, UIList, UIWindow, UIButton, UIPanel, UIImage, UISlider, UILabel, UIText, UICheck
import "luxe: ui/field/number" for UINumber
import "luxe: id" for ID
import "luxe: draw" for PathStyle
import "luxe: math" for Math
import "luxe: color" for Color
import "luxe: string" for Str
class ColorPickerData {
//parameters - we could make some of this dynamic if we wanted to
triangle_size:Num {35}
outer_ring_size:Num {200}
inner_ring_size:Num {140}
//raw color components in linear space
r:Num{_r}
g:Num{_g}
b:Num{_b}
h:Num{_h}
s:Num{_s}
v:Num{_v}
a:Num{_a}
color_ldr:Color{get_rgba(false, false)}
color_hdr:Color{get_rgba(false, true)}
srgb:Bool{_srgb_hex}
srgb=(value:Bool){_srgb_hex = value}
hdr_multiplier:Num{_hdr_mul}
hdr_multiplier=(value:Num){_hdr_mul = value}
allow_hdr:Bool{_allow_hdr}
allow_hdr=(v:Bool){_allow_hdr = v}
show_hdr:Bool{_show_hdr && allow_hdr}
show_hdr=(v:Bool){_show_hdr=v}
show_components:String{_show_components}
show_components(v:String){_show_components=v}
debug=(v){_debug=v}
toString{"Data - HSV:[%(h), %(s), %(v)] - RGB:[%(r), %(g), %(b)] - A:[%(a)] "+
"(srgb:%(srgb), hdr:(intensity:%(hdr_multiplier)%(allow_hdr?", allowed":"")%(show_hdr?", shown":"")), "+
"show components:%(show_components))%(_debug?("debug: "+_debug):"")"}
construct new(){
_hdr_mul = 1
_show_components = "rgb"
set_rgba(Color.hex(0xFFAABB))
}
get_rgba():Color{get_rgba(srgb, allow_hdr)}
get_rgba(srgb: Bool, hdr: Bool):Color{
var col = [r, g, b, a]
if(srgb){
CPHelper.apply_srgb(col)
}
if(hdr){
col.r = col.r * hdr_multiplier
col.g = col.g * hdr_multiplier
col.b = col.b * hdr_multiplier
}
return col
}
get_hsva_component():Color{get_hsva_component(srgb)}
get_hsva(srgb: Bool):Color{
var h = _h
var s = _s
var v = _v
var a = _a
if(srgb){
//hsv srgb conversion via rgb is the most easy and solid rn
var rgb = [r, g, b, a]
CPHelper.apply_srgb(rgb)
var hsv_srgb = Color.rgb2hsv(rgb)
if(!CPHelper.approx(v, 0) && !CPHelper.approx(s, 0)) h = hsv_srgb.x
if(!CPHelper.approx(v, 0)) s = hsv_srgb.y
v = hsv_srgb.z
}
return [h, s, v, a]
}
set_rgba(col: Color){set_rgba(col, srgb, true)}
set_rgba(col: Color, srgb: Bool){set_rgba(col, srgb, true)}
set_rgba(col: Color, srgb: Bool, update_spaces: Bool){
if(col.r > 1 || col.g > 1 || col.b > 1) Fiber.abort(col)
if(srgb) CPHelper.unapply_srgb(col)
if(!CPHelper.approx(_r, col.r)) _r = col.r
if(!CPHelper.approx(_g, col.g)) _g = col.g
if(!CPHelper.approx(_b, col.b)) _b = col.b
if(!CPHelper.approx(_a, col.a)) _a = col.a
if(!update_spaces) return
var hsv = Color.rgb2hsv(col)
//be careful not to destroy hue when doing rgb to hsv
if(!CPHelper.approx(_h, hsv.x) && !CPHelper.approx(hsv.y, 0) && !CPHelper.approx(hsv.z, 0) &&
!CPHelper.approx(_h, hsv.x-1) && !CPHelper.approx(_h, hsv.x+1)) _h = hsv.x
if(!CPHelper.approx(_s, hsv.y)) _s = hsv.y
if(!CPHelper.approx(_v, hsv.z)) _v = hsv.z
}
set_hsva(col){set_hsva(col, srgb, true)}
set_hsva(col: Color, srgb: Bool){set_hsva(col, srgb, true)}
set_hsva(col: Color, srgb: Bool, update_spaces: Bool){
if(srgb){
//hsv srgb conversion via rgb is the most easy and solid rn
var rgb = Color.hsv2rgb(col)
CPHelper.unapply_srgb(rgb)
var hsv_srgb = Color.rgb2hsv(rgb)
if(!CPHelper.approx(col.z, 0) && !CPHelper.approx(col.y, 0) && !CPHelper.approx(col.x, 1)) col.x = hsv_srgb.x
if(!CPHelper.approx(col.z, 0)) col.y = hsv_srgb.y
col.z = hsv_srgb.z
}
if(!CPHelper.approx(_h, col.x)) _h = col.x
if(!CPHelper.approx(_s, col.y)) _s = col.y
if(!CPHelper.approx(_v, col.z)) _v = col.z
if(!CPHelper.approx(_a, col.a)) _a = col.a
if(!update_spaces) return
var rgb = Color.hsv2rgb(col)
if(!CPHelper.approx(_r, rgb.r)) _r = rgb.r
if(!CPHelper.approx(_g, rgb.g)) _g = rgb.g
if(!CPHelper.approx(_b, rgb.b)) _b = rgb.b
}
set_rgba_component(index: Num, value: Num){set_rgba_component(index, value, srgb)}
set_rgba_component(index: Num, value: Num, srgb: Bool){
var current = get_rgba(srgb, false)
current[index] = value
set_rgba(current)
}
get_rgba_component(index: Num){get_rgba_component(index, srgb, false)}
get_rgba_component(index: Num, srgb: Bool){get_rgba_component(index, srgb, false)}
get_rgba_component(index: Num, srgb: Bool, hdr: Bool){
return get_rgba(srgb, hdr)[index]
}
set_hsva_component(index: Num, value: Num){set_hsva_component(index, value, srgb)}
set_hsva_component(index: Num, value: Num, srgb: Bool){
var current = get_hsva(srgb)
current[index] = value
set_hsva(current, srgb)
}
get_hsva_component(index: Num){get_hsva_component(index, srgb)}
get_hsva_component(index: Num, srgb: Bool){
return get_hsva(srgb)[index]
}
}
class ColorPicker{
static create(ui: Entity) : Control{
var data: ColorPickerData = ColorPickerData.new()
data.allow_hdr = true
//setup root
var panel = UIWindow.create(ui)
UIWindow.set_resizable(panel, false)
//Control.set_size(panel, 350, 500)
Control.set_contain(panel, UIContain.vfit | UIContain.hfit)
Control.set_id(panel, "panel.%(ID.unique())")
//Control.set_state_data(panel, base_color) //turns out UIWindows use their own state, whoops
var color_view = Control.create(ui)
//Control.set_behave(color_view, UIBehave.fill)
Control.set_contain(color_view, UIContain.column | UIContain.start | UIContain.vfit | UIContain.hfit)
Control.set_margin(color_view, 0, 40, 0, 0)
Control.child_add(panel, color_view)
Control.set_id(panel, "color_view.%(ID.unique())")
Control.set_state_data(color_view, data)
var wheel_and_color = Control.create(ui)
Control.set_contain(wheel_and_color, UIContain.row | UIContain.hfit | UIContain.vfit | UIContain.start)
Control.set_behave(wheel_and_color, UIBehave.left)
Control.child_add(color_view, wheel_and_color)
var wheel = CPHelper.create_hsv_wheel(ui, color_view)
Control.set_behave(wheel, UIBehave.left | UIBehave.top)
Control.set_margin(wheel, 8, 8, 0, 0)
Control.child_add(wheel_and_color, wheel)
var color_display = CPHelper.color_display(ui, color_view)
Control.child_add(wheel_and_color, color_display)
//this should be a dropdown
var component_choice = Control.create(ui)
Control.set_contain(component_choice, UIContain.row | UIContain.start)
Control.set_behave(component_choice, UIBehave.hfill | UIBehave.left | UIBehave.top)
Control.set_margin(component_choice, 8, 8, 0, 0)
Control.set_size(component_choice, 0, 32)
Control.child_add(color_view, component_choice)
var rgba_button = UIButton.create(ui)
Control.set_behave(rgba_button, UIBehave.left)
UIButton.set_text(rgba_button, "rgb")
Control.child_add(component_choice, rgba_button)
var hsva_button = UIButton.create(ui)
Control.set_behave(hsva_button, UIBehave.left)
UIButton.set_text(hsva_button, "hsv")
Control.child_add(component_choice, hsva_button)
var srgb_switch = CPHelper.colorspace_choice(ui, color_view)
Control.child_add(component_choice, srgb_switch)
var rgba_components = CPHelper.rgba_values(ui, color_view)
Control.child_add(color_view, rgba_components)
var hsva_components = CPHelper.hsva_values(ui, color_view)
Control.set_visible(hsva_components, false)
Control.child_add(color_view, hsva_components)
Control.set_events(rgba_button) {|event|
if(event.type == UIEvent.press){
Control.set_visible(rgba_components, true)
Control.set_visible(hsva_components, false)
}
}
Control.set_events(hsva_button) {|event|
if(event.type == UIEvent.press){
Control.set_visible(rgba_components, false)
Control.set_visible(hsva_components, true)
}
}
var hex_field = CPHelper.hex_input(ui, color_view)
Control.child_add(color_view, hex_field)
var hdr_settings = CPHelper.hdr_settings(ui, color_view)
Control.child_add(color_view, hdr_settings)
var bottom_offset = Control.create(ui)
Control.set_size(bottom_offset, 0, 8)
Control.set_behave(bottom_offset, UIBehave.left)
Control.child_add(color_view, bottom_offset)
return panel
}
}
#hidden
class CPHelper{
static hdr_settings(ui: UI, data_control: Control){
var data: ColorPickerData = Control.get_state_data(data_control)
var root = Control.create(ui)
Control.set_behave(root, UIBehave.left | UIBehave.hfill)
Control.set_contain(root, UIContain.column | UIContain.vfit)
var hdr_header = Control.create(ui)
Control.set_contain(hdr_header, UIContain.start | UIContain.row | UIContain.vfit)
Control.set_behave(hdr_header, UIBehave.left | UIBehave.hfill)
Control.child_add(root, hdr_header)
var hdr_label = UILabel.create(ui)
UILabel.set_text(hdr_label, "HDR: ")
UILabel.set_text_size(hdr_label, 14)
UILabel.set_align_vertical(hdr_label, TextAlign.center)
Control.set_margin(hdr_label, 8, 8, 0, 0)
Control.set_behave(hdr_label, UIBehave.left | UIBehave.top)
Control.set_size(hdr_label, 32, 20)
Control.child_add(hdr_header, hdr_label)
var hdr_toggle = UICheck.create(ui) //this should probably be a proper vertical swoopline
Control.set_margin(hdr_toggle, 8, 4, 0, 0)
Control.set_size(hdr_toggle, 32, 20)
Control.set_behave(hdr_toggle, UIBehave.left)
Control.child_add(hdr_header, hdr_toggle)
Control.set_events(hdr_toggle){ |event|
if(event.type == UIEvent.change){
if(data.show_hdr == event.change) return
data.show_hdr = event.change
data.debug = "hdr toggle"
UI.events_emit(data_control, UIEvent.change, data)
}
}
var hdr_intensity = Control.create(ui)
Control.set_margin(hdr_intensity, 0, 8, 0, 0)
Control.set_contain(hdr_intensity, UIContain.start | UIContain.row | UIContain.vfit)
Control.set_behave(hdr_intensity, UIBehave.left | UIBehave.hfill)
Control.child_add(root, hdr_intensity)
var hdr_mult_label = UILabel.create(ui)
UILabel.set_text(hdr_mult_label, "Intensity:")
//UILabel.set_text_size(hdr_mult_label, 12)
UILabel.set_color(hdr_mult_label, [0.8, 0.8, 0.8, 1])
Control.set_margin(hdr_mult_label, 8, 0, 0, 0)
Control.set_size(hdr_mult_label, 64, 32)
Control.set_behave(hdr_mult_label, UIBehave.left | UIBehave.top)
Control.child_add(hdr_intensity, hdr_mult_label)
var hdr_intensity_number = UINumber.create(ui)
UINumber.set_value(hdr_intensity_number, 1)
UINumber.set_validation(hdr_intensity_number) { |input: Num|
return input.max(0)
}
Control.set_size(hdr_intensity_number, 64, 32)
Control.set_margin(hdr_intensity_number, 8, 0, 0, 0)
Control.set_behave(hdr_intensity_number, UIBehave.left | UIBehave.top)
Control.child_add(hdr_intensity, hdr_intensity_number)
Control.set_events(hdr_intensity_number){|event|
if(event.type == UIEvent.change){
if(approx(data.hdr_multiplier, event.change)) return
data.hdr_multiplier = event.change
data.debug = "hdr intensity input"
UI.events_emit(data_control, UIEvent.change, data)
}
}
Control.set_events(data_control){|event|
if(event.type == UIEvent.change){
System.print(event.change)
UINumber.set_value(hdr_intensity_number, data.hdr_multiplier)
}
}
var hdr_numbers = Control.create(ui)
Control.set_margin(hdr_numbers, 0, 0, 0, 0)
Control.set_contain(hdr_numbers, UIContain.start | UIContain.row | UIContain.vfit)
Control.set_behave(hdr_numbers, UIBehave.left | UIBehave.hfill)
Control.child_add(root, hdr_numbers)
var hdr_number_display = UILabel.create(ui)
UILabel.set_text(hdr_number_display, "[%(Str.fixed(data.r, 3)), %(Str.fixed(data.g, 3)), %(Str.fixed(data.b, 3)), %(Str.fixed(data.a, 3))]")
UILabel.set_color(hdr_number_display, Color.hex(0x808080))
Control.set_size(hdr_number_display, 180, 32)
Control.set_margin(hdr_number_display, 8, 0, 0, 0)
Control.set_behave(hdr_number_display, UIBehave.left | UIBehave.hfill)
Control.child_add(hdr_numbers, hdr_number_display)
Control.set_events(data_control){ |event|
if(event.type == UIEvent.change){
var hdr = data.color_hdr
UILabel.set_text(hdr_number_display, "[%(Str.fixed(hdr.r, 3)), %(Str.fixed(hdr.g, 3)), %(Str.fixed(hdr.b, 3)), %(Str.fixed(hdr.a, 3))]")
}
}
var maximize_ldr_button = UIButton.create(ui)
UIButton.set_text(maximize_ldr_button, "maximize LDR")
UIButton.set_text_size(maximize_ldr_button, 12)
Control.set_size(maximize_ldr_button, 80, 24)
Control.set_behave(maximize_ldr_button, UIBehave.left | UIBehave.top)
Control.set_margin(maximize_ldr_button, 8, 4, 8, 0)
Control.child_add(hdr_numbers, maximize_ldr_button)
Control.set_events(maximize_ldr_button) {|event|
if(event.type == UIEvent.press){
if(data.v < 1 && data.v > 0.001){
var multiplier = data.hdr_multiplier.min(1/data.v).max(1)
data.hdr_multiplier = data.hdr_multiplier / multiplier
data.set_hsva_component(2, data.v * multiplier)
data.debug = "hdr maximize ldr"
UI.events_emit(data_control, UIEvent.change, data)
}
}
}
var color_hdr = UIPanel.create(ui)
UIPanel.set_color(color_hdr, data.color_hdr)
UIPanel.set_border(color_hdr, 0, Color.clear)
Control.set_size(color_hdr, 64, 32)
Control.set_margin(color_hdr, 8, 4, 8, 0)
Control.set_behave(color_hdr, UIBehave.left | UIBehave.top)
Control.child_add(root, color_hdr)
Control.set_events(data_control){|event|
if(event.type == UIEvent.change){
var color = data.color_hdr
color.a = 1
UIPanel.set_color(color_hdr, color)
}
}
//todo: color steps and more polish
if(!data.allow_hdr){
Control.set_visible(hdr_header, false)
}
if(!data.show_hdr){
Control.set_visible(hdr_intensity, false)
Control.set_visible(hdr_numbers, false)
Control.set_visible(color_hdr, false)
}
Control.set_events(data_control){ |event|
if(event.type == UIEvent.change){
Control.set_visible(hdr_intensity, data.show_hdr)
Control.set_visible(hdr_numbers, data.show_hdr)
Control.set_visible(color_hdr, data.show_hdr)
Control.set_visible(hdr_header, data.allow_hdr)
}
}
return root
}
static color_display(ui: UI, data_control: Control){
var data: ColorPickerData = Control.get_state_data(data_control)
var root = Control.create(ui)
Control.set_behave(root, UIBehave.left | UIBehave.top)
Control.set_contain(root, UIContain.column | UIContain.start | UIContain.hfit | UIContain.vfit)
Control.set_margin(root, 16, 0, 0, 0)
var color_opaque = UIPanel.create(ui)
UIPanel.set_color(color_opaque, data.color_ldr)
UIPanel.set_border(color_opaque, 0, Color.clear)
Control.set_size(color_opaque, 64, 32)
Control.set_margin(color_opaque, 8, 8, 8, 0)
Control.set_behave(color_opaque, UIBehave.left | UIBehave.top)
Control.child_add(root, color_opaque)
var color_w_trans_bg = Control.create(ui)
Control.set_size(color_w_trans_bg, 64, 32)
Control.set_margin(color_w_trans_bg, 8, 0, 8, 0)
Control.set_behave(color_w_trans_bg, UIBehave.left | UIBehave.top)
Control.child_add(root, color_w_trans_bg)
var color_alpha = UIPanel.create(ui)
UIPanel.set_color(color_alpha, data.color_ldr)
UIPanel.set_border(color_alpha, 0, Color.clear)
Control.set_clip(color_alpha, false)
Control.set_margin(color_alpha, 0, 0, 0, 0)
Control.set_behave(color_alpha, UIBehave.fill)
Control.child_add(color_w_trans_bg, color_alpha)
var alpha_bg = UIImage.create(ui)
Control.set_margin(alpha_bg, 0, 0, 0, 0)
Control.set_behave(alpha_bg, UIBehave.fill)
var alpha_mat = Material.create("materials/transparency_grid")
UIImage.set_material(alpha_bg, alpha_mat)
Control.child_add(color_w_trans_bg, alpha_bg)
Control.set_events(data_control){|event|
if(event.type == UIEvent.change){
var color = data.color_ldr
UIImage.set_color(alpha_bg, color)
color.a = 1
UIPanel.set_color(color_alpha, color)
UIPanel.set_color(color_opaque, color)
}
}
return root
}
static colorspace_choice(ui, data_control){
var data: ColorPickerData = Control.get_state_data(data_control)
var root = Control.create(ui)
Control.set_behave(root, UIBehave.left)
Control.set_contain(root, UIContain.row | UIContain.hfit | UIContain.vfit | UIContain.start)
Control.set_margin(root, 8, 0, 0, 0)
var srgb_label = UILabel.create(ui)
Control.set_size(srgb_label, 36, 32)
UILabel.set_text(srgb_label, "srgb")
Control.set_behave(srgb_label, UIBehave.left)
Control.child_add(root, srgb_label)
var srgb_toggle = UICheck.create(ui)
Control.set_size(srgb_toggle, 32, 20)
Control.set_behave(srgb_toggle, UIBehave.left)
Control.child_add(root, srgb_toggle)
Control.set_events(data_control) {|event|
if(event.type == UIEvent.change){
var data: ColorPickerData = event.change
UICheck.set_state(srgb_toggle, data.srgb)
}
}
Control.set_events(srgb_toggle){ |event|
if(event.type == UIEvent.change){
var input = event.change
data.srgb = input
data.debug = "srgb toggle"
UI.events_emit(data_control, UIEvent.change, data)
}
}
return root
}
static hex_input(ui: UI, data_control: Control){
var data: ColorPickerData = Control.get_state_data(data_control)
var root = Control.create(ui)
Control.set_size(root, 0, 32)
Control.set_contain(root, UIContain.row | UIContain.wrap)
Control.set_behave(root, UIBehave.left | UIBehave.top | UIBehave.vfill)
Control.set_margin(root, 8, 8, 8, 0)
var label = UILabel.create(ui)
Control.set_size(label, 36, 32)
UILabel.set_text(label, "hex")
Control.set_behave(label, UIBehave.left)
Control.child_add(root, label)
var text = UIText.create(ui)
UIText.set_text(text, color_to_hex_string(data.get_rgba(data.srgb, false), true))
Control.set_size(text, 128, 32)
Control.set_behave(text, UIBehave.left)
Control.child_add(root, text)
Control.set_events(data_control) {|event|
if(event.type == UIEvent.change){
var data: ColorPickerData = event.change
var color = data.get_rgba(data.srgb, false)
UIText.set_text(text, color_to_hex_string(color, true))
}
}
Control.set_events(text){ |event|
if(event.type == UIEvent.commit){
var input = event.change
var color = hex_string_to_color(input)
if(color){
data.set_rgba(color)
data.debug = "hex input"
UI.events_emit(data_control, UIEvent.change, data)
}
}
}
return root
}
static rgba_values(ui: UI, color_view: Control) {
var rgba_components = Control.create(ui)
Control.set_contain(rgba_components, UIContain.column | UIContain.hfit | UIContain.vfit)
Control.set_behave(rgba_components, UIBehave.hfill)
Control.set_margin(rgba_components, 0, 8, 0, 0)
var red = color_component(ui, "R", 0, color_view, "rgb")
Control.child_add(rgba_components, red)
var green = color_component(ui, "G", 1, color_view, "rgb")
Control.child_add(rgba_components, green)
var blue = color_component(ui, "B", 2, color_view, "rgb")
Control.child_add(rgba_components, blue)
var alpha = color_component(ui, "A", 3, color_view, "rgb")
Control.child_add(rgba_components, alpha)
return rgba_components
}
static hsva_values(ui: UI, color_view: Control) {
var rgba_components = Control.create(ui)
Control.set_contain(rgba_components, UIContain.column | UIContain.hfit | UIContain.vfit)
Control.set_behave(rgba_components, UIBehave.hfill)
Control.set_margin(rgba_components, 0, 8, 0, 0)
var red = color_component(ui, "H", 0, color_view, "hsv")
Control.child_add(rgba_components, red)
var green = color_component(ui, "S", 1, color_view, "hsv")
Control.child_add(rgba_components, green)
var blue = color_component(ui, "V", 2, color_view, "hsv")
Control.child_add(rgba_components, blue)
var alpha = color_component(ui, "A", 3, color_view, "hsv")
Control.child_add(rgba_components, alpha)
return rgba_components
}
static color_component (ui: Entity, name: String, index: Num, color_view: Control, space: String){
var data: ColorPickerData = Control.get_state_data(color_view)
var base = Control.create(ui)
Control.set_size(base, 0, 32)
Control.set_behave(base, UIBehave.top | UIBehave.left | UILayoutBehave.hfill)
Control.set_contain(base, UIContain.row)
Control.set_margin(base, 0, 0, 0, 0)
Control.set_id(base, "component.%(name).%(ID.unique())")
var label = UILabel.create(ui)
UILabel.set_text(label, name)
UILabel.set_text_size(label, 20)
UILabel.set_align(label, TextAlign.right)
Control.set_size(label, 20, 32)
Control.set_behave(label, UIBehave.left | UIBehave.vfill)
Control.set_margin(label, 8, 0, 8, 0)
Control.child_add(base, label)
var number = UINumber.create(ui)
UINumber.set_validation(number) {|input|
return Math.fixed(input)
}
Control.set_behave(number, UIBehave.top | UIBehave.left)
Control.set_margin(number, 0, 0, 0, 0)
Control.set_size(number, 72, 32)
Control.child_add(base, number)
var slider = UISlider.create(ui)
UISlider.set_min(slider, 0)
UISlider.set_max(slider, 1)
UISlider.set_value(slider, 0)
Control.set_behave(slider, UILayoutBehave.top | UILayoutBehave.bottom | UILayoutBehave.hfill)
Control.set_margin(slider, 8, 8, 8, 8)
Control.child_add(base, slider)
if(space == "rgb"){
UINumber.set_value(number, data.get_rgba_component(index))
UISlider.set_value(slider, data.get_rgba_component(index))
} else if(space == "hsv"){
UINumber.set_value(number, data.get_hsva_component(index))
UISlider.set_value(slider, data.get_hsva_component(index))
}
Control.set_events(number) { |event: UIEvent|
if(event.type == UIEvent.change){
if(approx(UISlider.get_value(slider), event.change, 0.001)) return
System.print("number display: %(event.change)")
UI.events_emit(base, UIEvent.change, event.change)
}
}
Control.set_events(slider) { |event: UIEvent|
if(event.type == UIEvent.change){
if(approx(UINumber.get_value(number), event.change, 0.001)) return
System.print("slider display: %(event.change)")
UI.events_emit(base, UIEvent.change, event.change)
}
}
Control.set_events(color_view){|event|
if(event.type == UIEvent.change){
var data: ColorPickerData = event.change
if(space == "rgb"){
UISlider.set_value(slider, Math.fixed(data.get_rgba_component(index)))
UINumber.set_value(number, Math.fixed(data.get_rgba_component(index)))
} else if(space == "hsv"){
UISlider.set_value(slider, Math.fixed(data.get_hsva_component(index)))
UINumber.set_value(number, Math.fixed(data.get_hsva_component(index)))
}
}
}
Control.set_events(base) {|event|
if(event.type == UIEvent.change){
var data: ColorPickerData = Control.get_state_data(color_view)
if(space == "rgb"){
if(approx(event.change, data.get_rgba_component(index))) return
if(event.change > 1) Fiber.abort(event)
data.set_rgba_component(index, event.change)
} else if(space == "hsv"){
if(approx(event.change, data.get_hsva_component(index))) return
data.set_hsva_component(index, event.change)
}
Control.set_state_data(color_view, data)
data.debug = "component display"
UI.events_emit(color_view, UIEvent.change, data)
}
}
return base
}
static create_hsv_wheel(ui: Entity, data_root: Control) : Control {
var data: ColorPickerData = Control.get_state_data(data_root)
var base_value_gamma = 1.6
var base_sat_gamma = 0.8
var color_wheel = Control.create(ui)
Control.set_size(color_wheel, data.outer_ring_size, data.outer_ring_size)
//Control.child_add(parent, color_wheel)
//the wheel has a bunch of state to have stuff be more solid (for example not reset hue when going to [0,0,0,_]) and interaction state
Control.set_state_data(color_wheel, {"ring":null, "triangle":null})
Control.set_process(color_wheel){|control, state, event, x,y,w,h|
//this might not be nessecary anymore?
if(event.control != control) return
if(event.type == UIEvent.move){
x = Control.get_pos_x_abs(control)
y = Control.get_pos_y_abs(control)
var center = [x + w/2, y + h/2]
if(state["ring"] == "captured"){ //if we're editing the ring(hue)
//hue from angle
var diff = [event.x - center.x, event.y - center.y]
var angle = Math.atan2(-diff.x, diff.y) + Num.pi
var hue = angle / Num.tau
//change relevant values
var picker_state: ColorPickerData = Control.get_state_data(data_root)
var hsv = [hue, picker_state.s, picker_state.v, picker_state.a]
picker_state.set_hsva(hsv, false)
data.debug = "ring"
UI.events_emit(data_root, UIEvent.change, picker_state)
} else if(state["triangle"] == "captured"){ //if we're editing the triangle (saturation & value)
var picker_state: ColorPickerData = Control.get_state_data(data_root)
var diff = [event.x - center.x, event.y - center.y] //position rel to center
Math.rotate(diff, 0, 0, picker_state.h * -360) //follow triangle rotation
//constrain to triangle
var triangle_size = picker_state.triangle_size
constrain_to_triangle(diff, triangle_size)
//get saturation based on distance to 2 edges
var saturation = (-diff.y + triangle_size) / ((-diff.y + triangle_size) + (Math.dot2D(diff, dir_vec(Math.radians(30), 1)) + triangle_size))
saturation = saturation.pow(base_sat_gamma)
saturation = saturation.clamp(0, 1)
//similarly get value based on distance to other edge
var value = Math.dot2D(diff, dir_vec(Math.radians(150), 1))
value = value + triangle_size
value = value / (triangle_size * 3)
value = 1 - value
value = value.pow(base_value_gamma)
value = value.clamp(0, 1)
//calculate and apply relevant values
var hsv = [picker_state.h, saturation, value, picker_state.a]
picker_state.set_hsva(hsv, false)
data.debug = "triangle"
UI.events_emit(data_root, UIEvent.change, picker_state)
} else { //if we're not editing anything, lets check what we're hovering over!
var picker_state: ColorPickerData = Control.get_state_data(data_root)
var distance = Math.dist2D(center, event)
//first reset hover state (can only be hover bc we checked for captured)
state["ring"] = null
state["triangle"] = null
//then find actual state
if(distance > picker_state.inner_ring_size/2 && distance < picker_state.outer_ring_size/2){
state["ring"] = "hover"
} else if(distance < picker_state.inner_ring_size/2) { //todo: triangle not round
state["triangle"] = "hover"
}
}
} else if(event.type == UIEvent.press && event.button == 1) {
//if we click, check if we're hovering over anything and if so, change it to captured
var hover_ring = state["ring"] != null //ring is hover (or captured)
var hover_tri = state["triangle"] != null //triangle is hover (or captured)
if(hover_ring || hover_tri) UI.capture(control) //if any of both happens, capture this
if(hover_ring) state["ring"] = "captured"
if(hover_tri) state["triangle"] = "captured"
} else if(event.type == UIEvent.release && event.button == 1) { //if we release a click, let go
var picker_state: ColorPickerData = Control.get_state_data(data_root)
x = Control.get_pos_x_abs(control)
y = Control.get_pos_y_abs(control)
var center = [x + w/2, y + h/2]
//todo: avoid code dupe with hover code
var distance = Math.dist2D(center, event)
//first reset hover state (can only be hover bc we checked for captured)
state["ring"] = null
state["triangle"] = null
//then find actual state
if(distance > picker_state.inner_ring_size/2 && distance < picker_state.outer_ring_size/2){
state["ring"] = "hover"
} else if(distance < picker_state.inner_ring_size/2) { //todo: triangle not round
state["triangle"] = "hover"
}
UI.uncapture(control)
}
}
Control.set_allow_input(color_wheel, true)
//ring visuals, interresting stuff happens in shader
var color_ring = UIImage.create(ui)
Control.set_size(color_ring, data.outer_ring_size, data.outer_ring_size)
Control.set_contain(color_ring, UIContain.justify)
var color_ring_mat = Material.create("materials/color_ring")
Material.set_input(color_ring_mat, "wheel.outer_distance", 0.5)
Material.set_input(color_ring_mat, "wheel.inner_distance", 0.5 * data.inner_ring_size / data.outer_ring_size)
UIImage.set_material(color_ring, color_ring_mat)
Control.child_add(color_wheel, color_ring)
//triangle visual, interresting stuff happens in shader
var color_triangle = UIImage.create(ui)
Control.set_margin(color_triangle, 30, 30, 0, 0)
Control.set_size(color_triangle, data.triangle_size * 4, data.triangle_size*4)
Control.set_contain(color_triangle, UIContain.justify)
var color_tri_mat = Material.create("materials/color_triangle")
Material.set_input(color_tri_mat, "triangle.value_gamma", base_value_gamma)
Material.set_input(color_tri_mat, "triangle.saturation_gamma", base_sat_gamma)
Material.set_input(color_tri_mat, "triangle.hue", data.h)
UIImage.set_material(color_triangle, color_tri_mat)
UIImage.set_angle(color_triangle, data.h * -360)
Control.child_add(color_wheel, color_triangle)
//rotates image when color wheel hue updates
Control.set_events(data_root) {|event|
if(event.type == UIEvent.change){
var hue = event.change.h
UIImage.set_angle(color_triangle, hue * -360)
Material.set_input(color_tri_mat, "triangle.hue", hue)
}
}
//overlay does everything thats easier done with direct UI draw functions
var color_wheel_overlay = Control.create(ui)
Control.set_behave(color_wheel_overlay, UIBehave.fill)
Control.set_margin(color_wheel_overlay, 0, 0, 0, 0)
Control.set_render(color_wheel_overlay){|control, state, x, y, w, h|
//prep data
var depth = UI.draw_depth_of(control, 0)
var center = [x + w/2, y + h/2]
var style: PathStyle = PathStyle.new()
var picker_state: ColorPickerData = Control.get_state_data(data_root)
var color = picker_state.color_ldr
color.a = 1
var hue = picker_state.h
var saturation = picker_state.s.pow(1/base_sat_gamma)
var value = picker_state.v.pow(1/base_value_gamma)
//draw hue ring borders
style.color = [0.22, 0.22, 0.22, 1]
style.thickness = 3
UI.draw_ring(control, center.x, center.y, depth, picker_state.outer_ring_size/2, picker_state.outer_ring_size/2, 0, 360, 8, style)
UI.draw_ring(control, center.x, center.y, depth, picker_state.inner_ring_size/2, picker_state.inner_ring_size/2, 0, 360, 8, style)
//draw hue ring color rect
var wheel_data = Control.get_state_data(color_wheel)
style.thickness = 2
if(wheel_data["ring"] != null) {
style.color = [0.5, 0.5, 0.5, 1]
} else {
style.color = [0.22, 0.22, 0.22, 1]
}
var size = [20, 30]
var angle_degree = hue * -360
var angle_radian = hue * -Num.tau
var avg_radius = (picker_state.inner_ring_size + picker_state.outer_ring_size) / 2 / 2
var offset = [-angle_radian.sin * avg_radius - size.x/2, -angle_radian.cos * avg_radius - size.y/2]
UI.draw_quad(control, center.x + offset.x, center.y + offset.y, depth, size.x, size.y, angle_degree, color)
UI.draw_rect(control, center.x + offset.x, center.y + offset.y, depth, size.x, size.y, angle_degree, style)
//draw triangle color dot
style.thickness = 2
if(wheel_data["triangle"] != null) {
style.color = [0.5, 0.5, 0.5, 1]
} else {
style.color = [0.22, 0.22, 0.22, 1]
}
var triangle_size = picker_state.triangle_size
var v_dist = (1 - value) * (triangle_size * 3) - triangle_size //distance in "value direction" in triangle
var v_dir = dir_vec(Math.radians(150), 1) //"value direction"
var v_norm = [-v_dir.y, v_dir.x] //direction orthogonal to "value direction"
var width = value * triangle_size * 1.73205080756 //width (along value ortho) at current value value
var pos = [v_dir.x * v_dist + (saturation*2-1) * width * v_norm.x, v_dir.y * v_dist + (saturation*2-1) * width * v_norm.y]
//adjust for rotation
Math.rotate(pos, 0, 0, hue * 360)
var dot_size = 7
UI.draw_circle(control, center.x + pos.x, center.y + pos.y, depth, dot_size, dot_size, 0, 360, 8, color)
UI.draw_ring(control, center.x + pos.x, center.y + pos.y, depth, dot_size, dot_size, 0, 360, 8, style)
}
Control.child_add(color_wheel, color_wheel_overlay)
return color_wheel
}
static color_to_hex_string(color: Color, alpha: Boolean): String{
var str: String = Str.hex(Color.hex_color(color, alpha))
str = str[2..-1] //remove 0x
while(str.count < (alpha ? 8 : 6)) str = "0" + str //padding
str = "#"+Str.upper(str)
return str
}
static hex_string_to_color(hex_string: String): Color{
//strip prefix
if(hex_string.startsWith("#")) hex_string = hex_string[1..-1]
if(hex_string.startsWith("0x")) hex_string = hex_string[2..-1]
var length = hex_string.count
var number = Num.fromString("0x"+hex_string)
if(!number) return null
var color = [0, 0, 0, 1]
if(length == 3 || length == 4){ //short half-byte notation
if(length == 4) { //with alpha
color.a = (number & 0xF) / 15
number = number >> 4
}
color.r = ((number >> 8) & 0xF) / 15
color.g = ((number >> 4) & 0xF) / 15
color.b = ((number >> 0) & 0xF) / 15
return color
}
if(length == 6 || length == 8){
if(length == 8) { //with alpha
color.a = (number & 0xFF) / 255
number = number >> 8
}
color.r = ((number >> 16) & 0xFF) / 255
color.g = ((number >> 8) & 0xFF) / 255
color.b = ((number >> 0) & 0xFF) / 255
return color
}
return null
}
static apply_srgb(col: Color){
col.r = col.r.pow(1 / 2.2)
col.g = col.g.pow(1 / 2.2)
col.b = col.b.pow(1 / 2.2)
//no alpha change
}
static unapply_srgb(col:Color){
col.r = col.r.pow(2.2)
col.g = col.g.pow(2.2)
col.b = col.b.pow(2.2)
//no alpha change
}
static approx_rgb(one: Color, other: Color, epsilson: Num) : Bool{
return approx(one.x, other.x, epsilson) && approx(one.y, other.y, epsilson) && approx(one.z, other.z, epsilson)
}
static approx(one: Num, other: Num) : Bool{
return approx(one, other, 0.001)
}
static approx(one: Num, other: Num, epsilon: Num) : Bool{
if(!(one is Num && other is Num)) return false
return (one - other).abs < epsilon
}
//if this goes into the actual engine and not editor code this should take degree
//but I like radians better :)
static dir_vec(angle: Num, length: Num) : List{
return [angle.cos * length, angle.sin * length]
}
static constrain_to_triangle(pos: List, size: Num){
//size is the inner radius, so we get half the base by multiplying with tan(60°)
var base_width = size * 1.73205
//first, we do the cheap calculation for [0, 1]
var edge_dist = -(pos.y - size)
if(edge_dist < 0){
pos.y = size
pos.x = pos.x.clamp(-base_width, base_width)
return
}
//then, for the diagonal edges
var edge_normal = dir_vec(Math.radians(30), 1)
//first we check the distance from the edge
edge_dist = Math.dot2D(pos, edge_normal)
if(edge_dist < -size){
//if the dot is outside the edge also get the position along the edge tangent so we can constrain it
var edge_tangent = [edge_normal.y, -edge_normal.x]
var tangent_dist = Math.dot2D(pos, edge_tangent).clamp(-base_width, base_width)
//then construct the constrained position at the edge, but keeping the relative, constrained, tangent position
pos.x = edge_normal.x * -size + edge_tangent.x * tangent_dist
pos.y = edge_normal.y * -size + edge_tangent.y * tangent_dist
return
}
//same as prev
edge_normal = dir_vec(Math.radians(150), 1)
edge_dist = Math.dot2D(pos, edge_normal)
if(edge_dist < -size){
var edge_tangent = [edge_normal.y, -edge_normal.x]
var tangent_dist = Math.dot2D(pos, edge_tangent).clamp(-base_width, base_width)
pos.x = edge_normal.x * -size + edge_tangent.x * tangent_dist
pos.y = edge_normal.y * -size + edge_tangent.y * tangent_dist
return
}
}
}