// Théophile Clet - september 2023 // contact@tflcl.xyz - https://tflcl.xyz // Max jsui knob with range and randomization features. // Still a work in progress. // License CC-BY-4.0 mgraphics.init(); mgraphics.relative_coords = 1; mgraphics.autofill = 0; outlets = 3; //UI variables var knob_color = [0.2, 0.2, 0.2, 1.]; var pointer_color = [1., 1., 1., 1.]; var range_color = [0.702, 0.416, 0.886, 0.9]; var range_color_locked = [0.74, 0.63, 0.74, 0.9]; var indicator_color = [0., 0., 0., 1]; var lock_color = [1., 1., 1., 1]; var text_color = [1., 1., 1., 1.]; var knob_radius = 0.8; var knob_width = 0.1; var pointer_length = 0.9; var pointer_offset = 0.; var pointer_width = 0.15; var range_width = 0.2; var range_radius = 0.85; var indicator_lenght = 0.1; // var indicator_width = 0.03; var dead_angle = 0.23; var lock_pos = [0.7, 0.75]; var lock_dim = [0.25, 0.17]; var lock_width = 0.03; var font_name ="Ableton Sans Bold"; var font_size = 9.; //Knob variables var val, val_scaled, val_default = 0; var locked = 0; var range_offset = 0.2; var range_length = 0.5; var range = [0., 1.]; var range_scaled = []; var output_range = [0., 1.]; //Utility variables var width = this.box.rect[2] - this.box.rect[0]; var height = this.box.rect[3] - this.box.rect[1]; var ratio = width/height; var TWOPI = 2 * Math.PI; var rdm = new Rx256(); //random engine var last_x, last_y = 0; var theta, rtheta1, rtheta2; var theta_s = (0.75 - dead_angle * 0.5) * TWOPI; var theta_e = (dead_angle * 0.5 - 0.25) * TWOPI; var cos_s = Math.cos(theta_s); var sin_s = Math.sin(theta_s); var cos_e = Math.cos(theta_e); var sin_e = Math.sin(theta_e); var text; var t_size = []; var pattrVar = []; // arg 0: filename // arg 1: default value // arg 2: min output range // arg 3: max output range // arg 4: min random range // arg 5: max random range if (jsarguments.length == 6) { val_default = jsarguments[1]; output_range = [jsarguments[2], jsarguments[3]]; range = calc_range_2_norm([jsarguments[4],jsarguments[5]]); } if (jsarguments.length == 5) { output_range = [jsarguments[1], jsarguments[2]]; range = calc_range_2_norm([jsarguments[3],jsarguments[4]]); } if (jsarguments.length == 4) { val_default = jsarguments[1]; output_range = [jsarguments[2], jsarguments[3]]; } if (jsarguments.length == 3) { output_range = [jsarguments[1], jsarguments[2]]; } if (jsarguments.length == 2) { val_default = jsarguments[1]; } set_scaled(val_default); function loadbang() { calc_font_size(); update(); } // loadbang(); function paint() { with (mgraphics) { save(); scale(0.9, 0.9); translate(0.1, 0.3); //DRAW RANGE set_source_rgba(locked ? range_color_locked : range_color); set_line_width(range_width); rtheta1 = (0.25 + range[0] * (1 - dead_angle) + dead_angle * 0.5) * TWOPI; rtheta2 = (0.25 + range[1] * (1 - dead_angle) + dead_angle * 0.5) * TWOPI; arc(0, 0, range_radius, rtheta1, rtheta2); stroke(); //DRAW POT BODY set_source_rgba(knob_color); set_line_width(knob_width); set_line_cap('round'); move_to(cos_s * (knob_radius + indicator_lenght), sin_s * (knob_radius + indicator_lenght)); rel_line_to(-cos_s * indicator_lenght, -sin_s * indicator_lenght); arc(0, 0, knob_radius - knob_width / 2, -theta_s, -theta_e); rel_line_to((indicator_lenght + knob_width/2) * cos_e, (indicator_lenght + knob_width/2) * sin_e); stroke(); //DRAW POINTER set_source_rgba(pointer_color); set_line_width(pointer_width); theta = (0.75 - val * (1-dead_angle) - dead_angle * 0.5) * TWOPI; var cos_t = Math.cos(theta); var sin_t = Math.sin(theta); move_to(pointer_offset * knob_radius * cos_t, pointer_offset * knob_radius * sin_t); rel_line_to(pointer_length * knob_radius * cos_t, pointer_length * knob_radius * sin_t); stroke(); restore(); //DRAW LOCK translate(lock_pos[0], -lock_pos[1]); set_source_rgba(lock_color); set_line_width(lock_width); rectangle(0, 0, lock_dim[0], lock_dim[1]); fill(); move_to(lock_width * 0.5, 0); if (!locked) { rel_line_to(0, lock_dim[1] * 0.5); } rel_curve_to(0, lock_dim[1], lock_dim[0] - lock_width, lock_dim[1], lock_dim[0] - lock_width, 0); stroke(); restore(); //DRAW VALUE select_font_face(font_name); set_font_size(font_size); text = (Math.round(val_scaled * 100) / 100).toString(); t_size = text_measure(text); translate(-t_size[0]/width, 1-0.1*t_size[1]/height); move_to(0, 0); set_source_rgba(text_color); show_text(text); } updatePattr(); } // function init(args) { // // TOO MUCH MESS HERE, DO NOT USE // if (arguments.length) { // args = arrayfromargs(arguments); // } else { // args = init_args; // } // if (args.length >= 5) { // val_default = args.shift(); // } // if (args.length == 4) { // output_range = [args.shift(), args.shift()]; // range = [scale2norm(args.shift()), scale2norm(args)]; // } else if (args.length == 3) { // val_default = args.shift(); // output_range = args; // } else if (args.length == 2) { // output_range = args; // } else if (args.length == 1) { // val_default = args; // } // set_scaled(val_default); // } function update() { outlet(0,val); outlet(1, val_scaled); mgraphics.redraw(); } function msg_float(v) { val = Math.min(Math.max(0,v),1); val_scaled = scale2outrange(val); update(); } function bang() { update(); } function randomize() { if (!locked) { val = scale(rdm.nextfloat_unipolar(), [0, 1], range); val_scaled = scale2outrange(val); update(); } } function set_range(args) { //dirty way to allow that function to run with either jsui args or passed variables if (arguments.length) { args = arrayfromargs(arguments); } //dirty way to keep a minimum range > 0 range[0] = Math.min(Math.max(0, args[0]),0.99); range[1] = Math.min(Math.max(0.01, args[1]),1); if (range[0] > range[1]) { var tmp = range[0]; range[0] = range[1]; range[1] = tmp; } range_scaled = calc_range_2_outrange(range); outlet(2, "range", range); outlet(2, "range_scaled", range_scaled); mgraphics.redraw(); } function set_scaled(v) { val = scale2norm(v); val_scaled = v; mgraphics.redraw(); } function set(v) { val = v; val_scaled = scale2outrange(val); mgraphics.redraw(); } function set_lock(v) { locked = v; mgraphics.redraw(); } function lock(v) { locked = v; outlet(2, "lock", v); mgraphics.redraw(); } // UTILITIES function scale(value, inRange, outRange) { var result = (value - inRange[0]) * (outRange[1] - outRange[0]) / (inRange[1] - inRange[0]) + outRange[0]; if (result < outRange[0]) { return outRange[0]; } else if (result > outRange[1]) { return outRange[1]; } return result; } scale.local = 1; function scale2norm(v) { return scale(v, output_range, [0., 1.]); } scale2norm.local = 1; function scale2outrange(v) { return scale(v, [0., 1.], output_range); } scale2outrange.local = 1; function calc_range_2_norm(r) { return [scale2norm(r[0]), scale2norm(r[1])]; } calc_range_2_norm.local = 1; function calc_range_2_outrange(r) { return [scale2outrange(r[0]), scale2outrange(r[1])]; } calc_range_2_outrange.local = 1; function calc_font_size() { font_size = width/5.8; }; calc_font_size.local = 1; // PATTR HANDLING function updatePattr() { //SHOULD WORK ONLY WITH SCALED VALUES TO IMPROVE UX pattrVar[0] = val_scaled; pattrVar[1] = range_scaled[0]; pattrVar[2] = range_scaled[1]; pattrVar[3] = locked; notifyclients(); } function getvalueof() { return pattrVar; } function setvalueof() { val_scaled = arguments[0]; val = scale2norm(val_scaled); range_scaled = [arguments[1], arguments[2]]; range = calc_range_2_norm(range_scaled); locked = arguments[3]; update(); } // MOUSE AND RESIZE INTERACTIONS function onclick(x,y,but,cmd,shift,capslock,option,ctrl) { x_snorm = 2 * x / width - 1; y_snorm = 2 * y / height - 1; // post(x_snorm, y_snorm); post(); if (x_snorm > lock_pos[0] && y_snorm < - lock_pos[1] + lock_dim[1] * 2.5 ) { lock(!locked); } // cache mouse position for tracking delta movements last_x = x; last_y = y; } onclick.local = 1; function ondrag(x,y,but,cmd,shift,capslock,option,ctrl) { var f,dy; dy = y - last_y; // calculate vertical delta movements if (ctrl) { // ctrl + drag to offset range var mult = shift ? 0.001 : 0.01; // fine tune if shift key is down set_range(range[0] - dy * mult, range[1] - dy * mult); } else if (option) { // opttion/alt + drag to change range width var mult = shift ? 0.0005 : 0.005; set_range(range[0] + dy * mult, range[1] - dy * mult); } else { //drag to change val var mult = shift ? 0.001 : 0.01; f = val - dy * mult; msg_float(f); //set new value with clipping + refresh } // cache mouse position for tracking delta movements last_x = x; last_y = y; } ondrag.local = 1; function ondblclick(x,y,but,cmd,shift,capslock,option,ctrl) { last_x = x; last_y = y; set_scaled(val_default); update(); // reset(); } ondblclick.local = 1; function forcesize(w,h) { if (w!=h) { h = w; box.size(w,h); } width = w; height = h; ratio = width/height; calc_font_size(); } forcesize.local = 1; function onresize(w,h) { forcesize(w,h); mgraphics.redraw(); } onresize.local = 1;