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 } } }