andrepxx / pure-knob

Canvas-based JavaScript UI element implementing touch, keyboard, mouse and scroll wheel support.
Apache License 2.0
52 stars 12 forks source link

Decimal percentage numbers #5

Closed jun5150 closed 5 years ago

jun5150 commented 5 years ago

Hi Andre Plötze,

First thank you for the code, it's great! I'm not a programmer, I just fool around with html and css and I'm trying to make a "spacecraft panel" for my son to use it on a tablet.

Question: I was wondering if is there any way to make this knob display 'decimal percentage numbers', something like 14.7%.

Sorry for the silly question.

Thank you, Jun

jjohnson78 commented 5 years ago

I modified the copy I'm working with to do just that. In pureknob.js I made some minor modifications, I added a parameter to the "this.createKnob" command for the type of value I'm passing:

this.createKnob = function(width, height, value_type) {

Also, there's two "value = Math.round(value);" lines I commented out in order to keep the value as floating until the string to display is set.

Then in the redraw function there's this line: var valueStr = value.toString();

Replace it with the below lines. This is where that value_type parameter I added comes into play:

if (value_type=="percent") { value=Math.floor(value*10)/10; var valueStr = value.toString(); valueStr+="%"; } else { value = Math.round(value); var valueStr = value.toString(); }

Then when you create the knob from the main page just include that new parameter like so: var myKnob = pureknob.createKnob(200, 200, 'percent');

It's a cobbled-together patch job for a specific purpose, but it did the trick - if "value" is a floating point value it doesn't seem to affect the drawing of the gauge:

image

andrepxx commented 5 years ago

Sorry, I haven't been very active over the holidays.

Thanks a lot for your interest in this project and your inspiration.

The "problem" with the percentage thing is that the "internal" floating-point number is derived as the arcus tangens (trigonometric function) of some (relative) cursor coordinates in the mouseEventToValue function. This value, if it was calculated exactly, would usually be irrational, so it would have an infinite fractional representation. Of course, since we're on a computer, we have floating-point numbers, which are a subset of rational numbers, so the fractional representation is finite. However, it will normally have many more "decimal" (or, internally, binary) places than the single decimal digit you display on your gauge. Therefore, you introduce a discrepancy between what the user sees and what is passed to the client application's event handlers as value.

For example, when the user moves the cursor slightly, there might get a value of 89.7143275943... % or 89.7327359681... % set "internally" (and passed to the event handler), but in both cases, the user sees just "89.7 %", which makes different internal states indistinguishable for the user.

In my opinion, this is a bad thing. Therefore, I avoided floating-point numbers and only allowed discrete (integer) steps for the control.

An additional problem is that, since you said you only patched the "drawing" function, you will get problems when the user middle-clicks, double-clicks, double-taps or long-taps on the control in order to enter the value numerically, as the input field will not display a percentage value. And even if you make the input field display a percentage value, you might get problems when the user changes it and wants to assign it back to the control. Therefore, I disallowed fractional values and units altogether. For example, I use the control in an audio application, and what I would do is put a label like "Gain (dB)" above the control instead and then displaying a value like "30" inside the control, instead of labeling the control as "Gain" and displaying a value like "30 dB" inside the control. (In my opinion, it's a lot cleaner anyways. The control just displays a number, and the caption says "this value is in dB" or "in %" or "in meters" or "in seconds" or whatever.)

The solution I would suggest to this problem if one REALLY wants to do fractional values and / or units inside the control, is the following.

I would allow the user to assign a user-defined "formatting" function, which is called when the string representation needs to be derived from the internal value. The internal value will always be an integer, so that we have (at least in principle) discrete steps. (In my opinion, this is a much more common use case than "arbitrary precition values". Often, I will want the user to set a number between 0 and 100 with nothing in between and then it will be good if the discretization is obvious as the user moves / pulls the knob around, so the discretization must be "inherent" to the control. It's not sufficient to discretize the value later when setting / committing the value. Then it's too late. The "action" of the control as it is moved already has to be "discrete" so that the user "feels" the steps and therefore intuitively knows what the control is doing.) In your case, if you want to display a percentage with one decimal fractional digit, you could internally use values from 0 to 1000. Then, if you really want to have a fractional number, you will format it when displayed to the user. In your case, you would ...

  1. Convert the integer to a string.
  2. If the string is just a single character (digit), insert a leading zero ('0').
  3. Split the N-character string into an N - 1 character "prefix" and a 1 character "postfix", then reassemble it as prefix + '.' + postfix. This way, you insert a decimal dot ('.') as the second-last character.
  4. Append a ' %'.

No mathematical operations and / or floating-point numbers required at all, no "division by 10" or "multiplication with 0.1". See how this is pure string manipulation?

On the other hand, when the user entered a value via (on-screen or physical) keyboard, you must strip off the ' %' again (optimally, strip all characters except digits and dot and perhaps sign), parse the value as a floating-point number (okay so we will temporarily have a floating-point number here), multiply it by 10, then convert it to an integer again.

Of course, your event handlers will also get numbers between 0 and 1000, so you will have to multiply by 0.1 there, if you want to have a floating-point percentage value. (However, you could also multiply by 0.001 to have an actual fractional value between 0.0 and 1.0.)

Sounds complicated, but, in my opinion, it's the cleanest and most generic solution. It makes a lot of sense to always use integer values internally. The rest is "formatting" - no floating-point values internally.

I don't know exactly when I will have the time to implement this, so please stand by. And, of course, I will add sample code to document how to make use of the new functionality. The "one fractional digit and percentage" is probably a good example to provide "custom parse and 'inverse parse' functions" for in the documentation.