From 654209442c8d39b55c46a18b499bbd7b2ea11853 Mon Sep 17 00:00:00 2001 From: Ronja Date: Tue, 23 Nov 2021 19:42:54 +0100 Subject: [PATCH] triangle picker works! --- colorpicker.wren | 186 +++++++++++++++++---- materials/color_triangle.material_basis.lx | 16 +- materials/shaders.emsl | 42 +++-- 3 files changed, 193 insertions(+), 51 deletions(-) diff --git a/colorpicker.wren b/colorpicker.wren index 80fa7ec..dc0cc6d 100644 --- a/colorpicker.wren +++ b/colorpicker.wren @@ -46,22 +46,39 @@ var Modes = [ 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, [1, 0, 0, 1]) + 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.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, 200, 200) - Control.child_add(color_view, color_wheel) - Control.set_state_data(color_wheel, {"ring":null, "triangle":null, "hue": 0}) + 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){ @@ -81,37 +98,78 @@ class ColorPicker{ 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) - if(distance > 70 && distance < 100){ + //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 { - state["ring"] = null + } else if(distance < inner_ring_size/2) { //todo: triangle not round + state["triangle"] = "hover" } } } else if(event.type == UIEvent.press && event.button == 1) { - if(!state["ring"]) return - state["ring"] = "captured" - UI.capture(control) + 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, 200, 200) + 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, 140, 140) + 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| @@ -126,25 +184,54 @@ class ColorPicker{ 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 wheel_state = Control.get_state_data(color_wheel) - var hover = wheel_state["ring"] - var hue = wheel_state["hue"] - var angle = hue * Num.tau - var direction = [angle.sin, -angle.cos] - style.thickness = 4 - style.color = hover ? [0.5,0.5,0.5,1] : [0.22,0.22,0.22,1] - var from = [center.x + direction.x * 70, center.y + direction.y * 70] - var to = [center.x + direction.x * 100, center.y + direction.y * 100] - UI.draw_line(control, from.x, from.y, to.x, to.y, depth, style) + 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"]) - style.color = [0.22,0.22,0.22,1] + //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, w/2, h/2, 0, 360, 8, style) - UI.draw_ring(control, center.x, center.y, depth, w/2*0.7, h/2*0.7, 0, 360, 8, style) + 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) @@ -225,15 +312,44 @@ class ColorPicker{ */ } - static build_triangle_editor(ui: Entity, root: Control){ - Control.clear(root, UIClear.destroy) + //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 build_square_editor(ui: Entity, root: Control){ - Control.clear(root, UIClear.destroy) - } - - static build_circle_editor(ui: Entity, root: Control){ - Control.clear(root, UIClear.destroy) + 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 + } } } \ No newline at end of file diff --git a/materials/color_triangle.material_basis.lx b/materials/color_triangle.material_basis.lx index b477259..440050d 100644 --- a/materials/color_triangle.material_basis.lx +++ b/materials/color_triangle.material_basis.lx @@ -22,12 +22,20 @@ material_basis = { layers = ["default"] inputs = { triangle.hue = { - type = "float", - value = 0.5, + type = "float" + value = 0.5 }, triangle.size = { - type = "float", - value = 0.25, + type = "float" + value = 0.25 } + triangle.value_gamma = { + type = "float" + value = 1 + } + triangle.saturation_gamma = { + type = "float" + value = 1 + } } } \ No newline at end of file diff --git a/materials/shaders.emsl b/materials/shaders.emsl index b36a5a0..b799f01 100644 --- a/materials/shaders.emsl +++ b/materials/shaders.emsl @@ -17,7 +17,9 @@ input ColorWheel { input ColorTriangle { float hue, - float size + float size, + float value_gamma, + float saturation_gamma } stage vertex vert( @@ -61,27 +63,36 @@ stage fragment frag_color_triangle( input { ColorTriangle triangle }, fragment in { float4 color, float2 uv }) { - float TAU = 6.2831853071795864769252867665590057683943387987502116419498891846; + float TAU = 6.283185307179586; float cos30 = 0.866025403784438; //cos(30°) + float tan60 = 1.732050807568877; + float tan60sq = 3.0; + float sin30 = 0.5; - float2 centered_coords = in.uv - 0.5; + float size = input.triangle.size; + float2 centered_coords = in.uv - sin30; float2 down = float2(0.0, -1.0); - float2 top_right = float2(cos30, 0.5); - float2 top_left = float2(-cos30, 0.5); + float2 top_right = float2(cos30, sin30); + float2 top_left = float2(-cos30, sin30); - float down_dist = dot(down, centered_coords); - float top_right_dist = dot(top_right, centered_coords); - float top_left_dist = dot(top_left, centered_coords); + float down_dist = dot(down, centered_coords) + size; + float top_right_dist = dot(top_right, centered_coords) + size; + float top_left_dist = dot(top_left, centered_coords) + size; float dist = min(min(top_right_dist, top_left_dist), down_dist); - dist += input.triangle.size; - float change = length(float2(dFdx(in.uv.x), dFdy(in.uv.y))); //this assumes the object is uniformly scaled + float change = length(float2(dFdx(in.uv.x), dFdy(in.uv.y))); //this assumes the object is uniformly scaled (rotation is fine) float alpha = clamp(dist/change, 0.0, 1.0); - float3 hue = hue2rgb(input.triangle.hue); + float hue = input.triangle.hue; + float saturation = down_dist / (down_dist + top_right_dist); + saturation = pow(saturation, input.triangle.saturation_gamma); + float value = 1.0 - (top_left_dist / (size * tan60sq)); + value = pow(value, input.triangle.value_gamma); + float3 color_rgb = hsv2rgb(float3(hue, saturation, value)); + //color_rgb = float3(fract(saturation)); //debug - float4 color = float4(float3(hue), alpha); + float4 color = float4(color_rgb, alpha); if(color.a <= 0.0) { discard; @@ -91,6 +102,13 @@ stage fragment frag_color_triangle( stage.color[0] = color; } +float3 hsv2rgb(float3 hsv){ + float3 rgb = hue2rgb(hsv.x); //apply hue + rgb = mix(float3(1.0), rgb, hsv.y); //apply saturation + rgb = rgb * hsv.z; //apply value + return rgb; +} + float3 hue2rgb(float hue) { hue = fract(hue); //only use fractional part float r = abs(hue * 6.0 - 3.0) - 1.0; //red