diff --git a/colorpicker.wren b/colorpicker.wren index dc0cc6d..fd4ece2 100644 --- a/colorpicker.wren +++ b/colorpicker.wren @@ -5,45 +5,6 @@ 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 @@ -59,13 +20,14 @@ class ColorPicker{ 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) + //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, base_color) //stores state "golbally" for multiple elements in the colorpicker to access var hsv_view = Control.create(ui) Control.set_behave(hsv_view, UIBehave.fill) @@ -76,57 +38,63 @@ class ColorPicker{ var color_wheel = Control.create(ui) Control.set_size(color_wheel, outer_ring_size, outer_ring_size) Control.child_add(hsv_view, 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, - "hue": base_hsv.x, "value": base_hsv.z, "saturation": base_hsv.y, - "value_gamma": base_value_gamma, "saturation_gamma": base_sat_gamma}) + "hue": base_hsv.x, "value": base_hsv.z, "saturation": base_hsv.y}) 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(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 - var color = Control.get_state_data(panel) - var hsv = Color.rgb2hsv(color) - hsv[0] = hue + //change relevant values + var color = Control.get_state_data(color_view) + var hsv = [hue, state["saturation"], state["value"], color.a] state["hue"] = hue color = Color.hsv2rgb(hsv) - Control.set_state_data(panel, color) + Control.set_state_data(control, state) + Control.set_state_data(color_view, color) UI.events_emit(control, UIEvent.change, state) - UI.events_emit(panel, UIEvent.change, color) - } else if(state["triangle"] == "captured"){ + UI.events_emit(color_view, UIEvent.change, color) + } else if(state["triangle"] == "captured"){ //if we're editing the triangle (saturation & value) 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) + //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(state["saturation_gamma"]) + 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(state["value_gamma"]) + value = value.pow(base_value_gamma) value = value.clamp(0, 1) - var hue = state["hue"] - var color = Control.get_state_data(panel) + //calculate and apply relevant values + var hue = state["hue"] + var color = Control.get_state_data(color_view) 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) + Control.set_state_data(control, state) + Control.set_state_data(color_view, color) UI.events_emit(control, UIEvent.change, state) - UI.events_emit(panel, UIEvent.change, color) - } else { + UI.events_emit(color_view, UIEvent.change, color) + } else { //if we're not editing anything, lets check what we're hovering over! var distance = Math.dist2D(center, event) //first reset hover state (can only be hover bc we checked for captured) state["ring"] = null @@ -139,20 +107,52 @@ class ColorPicker{ } } } 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(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) { - //todo: hover state based on position + } else if(event.type == UIEvent.release && event.button == 1) { //if we release a click, let go + 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 > 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" + } UI.uncapture(control) } } Control.set_allow_input(color_wheel, true) + //update values when some other UI that doesnt care about HSV changes the color + Control.set_events(color_view) {|event| + if(event.type == UIEvent.change){ + //construct wheel color + var wheel_data = Control.get_state_data(color_wheel) + var wheel_color = Color.hsv2rgb([wheel_data["hue"], wheel_data["saturation"], wheel_data["value"], 1]) + //ignore if its the same (we likely triggered this ourselves) (though this still fails often when 2 multiple land in the event queue) + if(approx_rgb(wheel_color, event.change, 0.001)) return + //update the wheel hsv if different + var hsv = Color.rgb2hsv(event.change) + //only change hue if saturation is nonzero and theres relevant change (saturation check is bc in rgb greyscale has no hue) + if(hsv.y > 0.001 && !approx(wheel_data["hue"], hsv.x, 0.001)) wheel_data["hue"] = hsv.x + //only change if theres relevant change + if(!approx(wheel_data["saturation"], hsv.y, 0.001)) wheel_data["saturation"] = hsv.y + if(!approx(wheel_data["value"], hsv.z, 0.001)) wheel_data["value"] = hsv.z + //also trigger event in self to update stuff like image rotation + UI.events_emit(color_wheel, UIEvent.change, wheel_data) + } + } + //ring visuals, interresting stuff happens in shader var color_ring = UIImage.create(ui) Control.set_size(color_ring, outer_ring_size, outer_ring_size) Control.set_contain(color_ring, UIContain.justify) @@ -162,6 +162,7 @@ class ColorPicker{ 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, triangle_size * 4, triangle_size*4) @@ -172,6 +173,7 @@ class ColorPicker{ 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) + //rotates image when color wheel hue updates Control.set_events(color_wheel) {|event| if(event.type == UIEvent.change){ var hue = event.change["hue"] @@ -180,6 +182,7 @@ class ColorPicker{ } } + //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) @@ -189,11 +192,11 @@ class ColorPicker{ var center = [x + w/2, y + h/2] var style: PathStyle = PathStyle.new() - var color = Control.get_state_data(panel) + var color = Control.get_state_data(color_view) 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"]) + var value = wheel_data["value"].pow(1/base_value_gamma) + var saturation = wheel_data["saturation"].pow(1/base_sat_gamma) //draw hue ring borders style.color = [0.22, 0.22, 0.22, 1] @@ -223,11 +226,12 @@ class ColorPicker{ } 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 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) @@ -312,6 +316,14 @@ class ColorPicker{ */ } + 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, epsilon: Num) : Bool{ + 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{