import "luxe: world" for Entity, UIClear, UILayoutMode, UIBehave, UIContain, Assets, Material, UI, UIEvent import "luxe: ui" for Control, UIList, UIWindow, UIButton, UIPanel, UIImage import "luxe: id" for ID import "luxe: draw" for PathStyle import "luxe: math" for Math import "luxe: color" for Color var Color_spaces = [ { "name": "RGB", "components": ["r", "g", "b"], "componentsFull": ["Red", "Green", "Blue"], }, { "name": "HSV", "components": ["h", "s", "v"], "componentsFull": ["Hue", "Saturation", "Value"], }, { "name": "HSL", "components": ["h", "s", "l"], "componentsFull": ["Hue", "Saturation", "Lightness"], }, { "name": "Oklab", "components": ["L", "a", "b"], "componentsFull": ["Lightness", "red/green", "blue/yellow"], } ] var Modes = [ //I wanted to do unicode icons but it doesnt render with the current font { "name": "Triangle", "icon": "tri", //▲ }, { "name": "Square", "icon": "box", //■ }, { "name": "Circle", "icon": "round", //● } ] class ColorPicker{ static create(ui: Entity) : Control{ //parameters - we could expose some of this if we wanted to var base_color = [1, 0, 0, 1] var base_hsv = Color.rgb2hsv(base_color) var base_value_gamma = 1.6 var base_sat_gamma = 0.8 var triangle_size = 35 //this is the inner triangle radius var outer_ring_size = 200 var inner_ring_size = 140 //setup root var panel = UIWindow.create(ui) Control.set_size(panel, 350, 400) Control.set_id(panel, "panel.%(ID.unique())") Control.set_state_data(panel, base_color) 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())") var hsv_view = Control.create(ui) Control.set_behave(hsv_view, UIBehave.fill) Control.set_margin(hsv_view, 0, 0, 0, 0) Control.child_add(color_view, hsv_view) Control.set_id(panel, "hsv_view.%(ID.unique())") var color_wheel = Control.create(ui) Control.set_size(color_wheel, outer_ring_size, outer_ring_size) Control.child_add(hsv_view, color_wheel) Control.set_state_data(color_wheel, {"ring":null, "triangle":null, "hue": base_hsv.x, "value": base_hsv.z, "saturation": base_hsv.y, "value_gamma": base_value_gamma, "saturation_gamma": base_sat_gamma}) Control.set_process(color_wheel){|control, state, event, x,y,w,h| 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"){ 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 var color = Control.get_state_data(panel) var hsv = Color.rgb2hsv(color) hsv[0] = hue state["hue"] = hue color = Color.hsv2rgb(hsv) Control.set_state_data(panel, color) UI.events_emit(control, UIEvent.change, state) UI.events_emit(panel, UIEvent.change, color) } else if(state["triangle"] == "captured"){ var diff = [event.x - center.x, event.y - center.y] //position rel to center Math.rotate(diff, 0, 0, state["hue"] * -360) //follow triangle rotation //constrain to triangle constrain_to_triangle(diff, triangle_size) var saturation = (-diff.y + triangle_size) / ((-diff.y + triangle_size) + (Math.dot2D(diff, dir_vec(Math.radians(30), 1)) + triangle_size)) saturation = saturation.pow(state["saturation_gamma"]) saturation = saturation.clamp(0, 1) 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(state["value_gamma"]) value = value.clamp(0, 1) var hue = state["hue"] var color = Control.get_state_data(panel) var hsv = [hue, saturation, value, color.a] //System.print(hsv) hsv[0] = hue state["value"] = value state["saturation"] = saturation color = Color.hsv2rgb(hsv) Control.set_state_data(panel, color) UI.events_emit(control, UIEvent.change, state) UI.events_emit(panel, UIEvent.change, color) } else { 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 > inner_ring_size/2 && distance < outer_ring_size/2){ state["ring"] = "hover" } else if(distance < inner_ring_size/2) { //todo: triangle not round state["triangle"] = "hover" } } } else if(event.type == UIEvent.press && event.button == 1) { 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(hover_ring) state["ring"] = "captured" if(hover_tri) state["triangle"] = "captured" } else if(event.type == UIEvent.release && event.button == 1) { //todo: hover state based on position state["ring"] = null state["triangle"] = null UI.uncapture(control) } } Control.set_allow_input(color_wheel, true) var color_ring = UIImage.create(ui) Control.set_size(color_ring, outer_ring_size, 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 * inner_ring_size / outer_ring_size) UIImage.set_material(color_ring, color_ring_mat) Control.child_add(color_wheel, color_ring) var color_triangle = UIImage.create(ui) Control.set_margin(color_triangle, 30, 30, 0, 0) Control.set_size(color_triangle, triangle_size * 4, 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", base_hsv.x) UIImage.set_material(color_triangle, color_tri_mat) Control.child_add(color_wheel, color_triangle) Control.set_events(color_wheel) {|event| if(event.type == UIEvent.change){ var hue = event.change["hue"] UIImage.set_angle(color_triangle, hue * -360) Material.set_input(color_tri_mat, "triangle.hue", hue) } } 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 color = Control.get_state_data(panel) var wheel_data = Control.get_state_data(color_wheel) var hue = wheel_data["hue"] var value = wheel_data["value"].pow(1/wheel_data["value_gamma"]) var saturation = wheel_data["saturation"].pow(1/wheel_data["saturation_gamma"]) //draw hue ring borders style.color = [0.22, 0.22, 0.22, 1] style.thickness = 2 UI.draw_ring(control, center.x, center.y, depth, outer_ring_size/2, outer_ring_size/2, 0, 360, 8, style) UI.draw_ring(control, center.x, center.y, depth, inner_ring_size/2, inner_ring_size/2, 0, 360, 8, style) //draw hue ring color rect style.thickness = 3 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 = (inner_ring_size + 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 = 3 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 v_dist = (1 - value) * (triangle_size * 3) - triangle_size var v_dir = dir_vec(Math.radians(150), 1) var v_norm = [-v_dir.y, v_dir.x] var width = value * triangle_size * 1.73205080756 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] 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 panel /* //setup colorspace buttons //todo: this should be a dropdown??? var colorspace_list = UIList.create(ui) //could use naked control but logically its a list? Control.child_add(root, colorspace_list) //base button size is 92x32 btw var space_button_height = 30 var space_button_width = 50 for(i in 0...Color_spaces.count){ var space = Color_spaces[i] var colorspace_button = UIButton.create(ui) Control.set_size(colorspace_button, space_button_width, space_button_height) UIList.add(colorspace_list, colorspace_button) UIButton.set_text(colorspace_button, space["name"]) Control.set_pos(colorspace_button, i * space_button_width, 0) //manually set position //todo: add event to actually do stuff here } Control.set_size(colorspace_list, Control.get_width(root).min(space_button_width * Color_spaces.count), space_button_height) //UIList.refresh(colorspace_list) //uilist can only do vertical ordering so far :( //setup uimode buttons //todo: this should be a dropdown??? var mode_list = UIList.create(ui) Control.child_add(root, mode_list) Control.set_pos(mode_list, 0, space_button_height) //base button size is 92x32 btw var mode_button_height = 30 var mode_button_width = 50 for(i in 0...Modes.count){ var mode = Modes[i] var mode_button = UIButton.create(ui) Control.set_size(mode_button, mode_button_width, mode_button_height) UIList.add(mode_list, mode_button) UIButton.set_text(mode_button, mode["icon"]) Control.set_pos(mode_button, i * mode_button_width, 0) //manually set position //todo: add event to actually do stuff here } Control.set_size(mode_list, Control.get_width(root).min(mode_button_width * Modes.count), mode_button_height) //setup main editor var main_editor_root = Control.create(ui) Control.child_add(root, main_editor_root) Control.set_size(main_editor_root, 200, 200) Control.set_pos(main_editor_root, 0, mode_button_height + space_button_height) //todo: configurable default editor build_triangle_editor(ui, main_editor_root) //setup sliders var sliders_root = Control.create(ui) Control.child_add(root, sliders_root) Control.set_size(sliders_root, 300, 140) Control.set_pos(sliders_root, 0, mode_button_height + space_button_height + 200) //setup text input/output var text_io_root = Control.create(ui) Control.child_add(root, text_io_root) Control.set_size(text_io_root, 150, 100) Control.set_pos(text_io_root, 200, 160) //setup preview field (old => new) var preview_root = Control.create(ui) Control.child_add(root, preview_root) Control.set_size(preview_root, 150, 100) Control.set_pos(preview_root, 200, 60) //other stuff? gamma/linear? scene color pick? range toggle (0-1/0-255)? HDR?? //todo return root */ } //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 } } }