import "luxe: world" for Entity, UIClear, UILayoutMode, UIBehave, UIContain, Assets, Material, UI, UIEvent, UILayoutBehave import "luxe: ui" for Control, UIList, UIWindow, UIButton, UIPanel, UIImage, UISlider, UILabel 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 class ColorPickerData { //parameters - we could make some of this dynamic if we wanted to triangle_size {35} outer_ring_size {200} inner_ring_size {140} r{_r} g{_g} b{_b} h{_h} s{_s} v{_v} a{_a} rgba{[_r, _g, _b, _a]} hsva{[_h, _s, _v, _a]} toString{"Data - HSV:[%(h), %(s), %(v)] - RGB:[%(r), %(g), %(b)] - A:[%(a)]"} construct new(){ set_rgba(Color.hex(0xFFAABB)) } set_rgba(col){set_rgba(col, true)} set_rgba(col, update_spaces){ if(!ColorPicker.approx(_r, col.r)) _r = col.r if(!ColorPicker.approx(_g, col.g)) _g = col.g if(!ColorPicker.approx(_b, col.b)) _b = col.b if(!ColorPicker.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(!ColorPicker.approx(_h, hsv.x) && !ColorPicker.approx(_s, 0)) _h = hsv.x if(!ColorPicker.approx(_s, hsv.y)) _s = hsv.y if(!ColorPicker.approx(_v, hsv.z)) _v = hsv.z } set_hsva(col){set_hsva(col, true)} set_hsva(col, update_spaces){ if(!ColorPicker.approx(_h, col.x)) _h = col.x if(!ColorPicker.approx(_s, col.y)) _s = col.y if(!ColorPicker.approx(_v, col.z)) _v = col.z if(!ColorPicker.approx(_a, col.a)) _a = col.a if(!update_spaces) return var rgb = Color.hsv2rgb(col) if(!ColorPicker.approx(_r, rgb.r)) _r = rgb.r if(!ColorPicker.approx(_g, rgb.g)) _g = rgb.g if(!ColorPicker.approx(_b, rgb.b)) _b = rgb.b } set_rgba_component(index: Num, value: Num){ if(index == 0){ set_rgba([value, _g, _b, _a]) } else if(index == 1){ set_rgba([_r, value, _b, _a]) } else if(index == 2){ set_rgba([_r, _g, value, _a]) } else if(index == 3){ set_rgba([_r, _g, _b, value]) } } get_rgba_component(index: Num){ if(index == 0){ return _r } else if(index == 1){ return _g } else if(index == 2){ return _b } else if(index == 3){ return _a } } set_hsva_component(index: Num, value: Num){ if(index == 0){ set_hsva([value, _s, _v, _a]) } else if(index == 1){ set_hsva([_h, value, _v, _a]) } else if(index == 2){ set_hsva([_h, _s, value, _a]) } else if(index == 3){ set_hsva([_h, _s, _v, value]) } } get_hsva_component(index: Num){ if(index == 0){ return _h } else if(index == 1){ return _s } else if(index == 2){ return _v } else if(index == 3){ return _a } } } class ColorPicker{ static create(ui: Entity) : Control{ var data = ColorPickerData.new() //setup root var panel = UIWindow.create(ui) Control.set_size(panel, 350, 500) 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_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 hsv_view = Control.create(ui) Control.set_behave(hsv_view, UIBehave.fill) Control.set_margin(hsv_view, 0, 0, 0, 0) Control.set_contain(hsv_view, UIContain.column | UIContain.start) Control.child_add(color_view, hsv_view) Control.set_id(panel, "hsv_view.%(ID.unique())") var wheel = 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(hsv_view, wheel) 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(hsv_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 rgba_components = rgba_values(ui, color_view) Control.child_add(hsv_view, rgba_components) var hsva_components = hsva_values(ui, color_view) Control.set_visible(hsva_components, false) Control.child_add(hsv_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) } } //todo: next steps: hex input(s?) return panel } 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) 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(" Base event (slider)") 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(" Base event (number)") UI.events_emit(base, UIEvent.change, event.change) } } Control.set_events(color_view){|event| if(event.type == UIEvent.change){ //System.print(event.change) if(space == "rgb"){ UISlider.set_value(slider, Math.fixed(event.change.get_rgba_component(index))) UINumber.set_value(number, Math.fixed(event.change.get_rgba_component(index))) } else if(space == "hsv"){ UISlider.set_value(slider, Math.fixed(event.change.get_hsva_component(index))) UINumber.set_value(number, Math.fixed(event.change.get_hsva_component(index))) } } } Control.set_events(base) {|event| if(event.type == UIEvent.change){ var color = Control.get_state_data(color_view) if(space == "rgb"){ if(approx(event.change, color.get_rgba_component(index))) return color.set_rgba_component(index, event.change) } else if(space == "hsv"){ if(approx(event.change, color.get_hsva_component(index))) return color.set_hsva_component(index, event.change) } Control.set_state_data(color_view, color) System.print("event from color component (%(name))") UI.events_emit(color_view, UIEvent.change, color) } } 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) System.print("Event from hue 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) System.print("event from saturation/value 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.rgba 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 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 } } }